拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

滚刀状态识别:基于CNN/LSTM/SVM与随机森林的机器学习实践

滚刀状态识别:基于CNN/LSTM/SVM与随机森林的机器学习实践

简介:滚刀状态识别是隧道掘进设备监测及机器学习毕业设计中的常见方向,这份资源以Python实现,聚焦滚刀运行状态的多模型分类问题,集成了CNN、LSTM、GRU、SVM、随机森林等算法,可在同一套数据流程中完成训练与效果对比,适合需要快速搭建并理解识别方案的本科生与入门开发者。包体为ZIP压缩包,共15个文件、2.63MB,包含8个Python脚本和4个CSV数据集。脚本覆盖特征生成、数据合并、各模型训练及测试等环节,CSV提供可直接使用的样本数据,另附README说明与示意图,目录结构清晰,便于按需查看和修改。目前已有234人学习下载,说明其方案具备一定的参考价值。对毕业设计、课程设计或期末大作业来说,可借鉴完整的多模型对比思路与代码组织方式,省去从零调通环境的时间;代码注释相对清楚,简单部署即可运行,适合作为高分项目基础或继续改进的起点。

1. 滚刀状态识别:为什么机器学习方案比阈值报警更值得做

盾构/TBM 施工里滚刀是破岩核心耗材,状态好坏直接决定掘进效率和换刀成本。过去现场主要靠经验听声音、看油温、查掘进参数突变,误报漏报都不少。把振动、推力、扭矩这些信号喂给机器学习模型做滚刀状态识别,本质是让模型从数据里学出“正常磨损、偏磨、轴承损坏、刀圈崩裂”之间的边界,替代人工拍脑袋。这条路线近几年在隧道工程和矿山机械里落地很多,PyTorch 或者 scikit-learn 都能做,难点不在“跑通一个 CNN”,而在“把信号变成能稳定训练的特征序列”。本文围绕 CNN、LSTM、GRU、SVM、随机森林这套组合方案,讲清楚每个模型为什么选、数据怎么预处理、参数怎么调、以及最容易翻车的几个坑。


2. 从原始信号到训练样本:滚刀数据的采集、切片与特征工程

2.1 滚刀状态识别为什么不能直接拿原始波形训练

滚刀振动传感器采样率通常在 10 kHz 以上,一个掘进循环动辄几十分钟,直接拿整段波形训练不现实。常见做法是先把连续信号切成固定长度的时间窗口,每个窗口对应一个样本,再给样本打上标签。窗口长度和重叠率直接影响样本数量与模型效果,切片太长会把多个状态混在一起,太短又拿不到完整的刀圈旋转周期信息。

我一般会把窗口长度设成滚刀旋转周期的 2 到 3 倍,重叠率设 50%。比如刀盘转速 3 r/min,滚刀公转一圈约 20 秒,那窗口长度取 40 到 60 秒比较稳。切片后每个样本包含多个维度:推进速度、刀盘扭矩、滚刀振动加速度、油温,这些维度量纲差异大,后续必须归一化。

import numpy as np import pandas as pd def sliding_window(data, window_len, overlap_rate=0.5): step = int(window_len * (1 - overlap_rate)) samples = [] for start in range(0, len(data) - window_len + 1, step): samples.append(data[start:start + window_len]) return np.array(samples) # 假设 df 的列包括:vibration_x, vibration_y, thrust_force, torque, temperature df = pd.read_csv("cutter_data.csv", parse_dates=["timestamp"]) cols = ["vibration_x", "vibration_y", "thrust_force", "torque", "temperature"] window_len = 5000 # 按采样率 1000 Hz 算,5 秒窗口 X = sliding_window(df[cols].values, window_len, overlap_rate=0.5) print("样本形状:", X.shape) # (样本数, 窗口长度, 特征维度)

这段代码是后续所有模型的输入基础。sliding_window函数里overlap_rate=0.5是经验值,它让相邻窗口有 50% 重叠,样本量翻倍,模型更容易学到状态过渡段的特征。如果样本量不足,把重叠率调到 0.75 也能凑数,但要注意相邻样本高度相关,训练集和验证集必须按时间顺序切分,不能随机打乱,否则验证集里都是训练集的“近亲”,指标好看但实际部署就露馅。

2.2 三类标签体系:二分类、三分类与五分类怎么选

标签定义决定模型上限。现场最常用的是三分类:正常、磨损、故障。其中“故障”涵盖轴承损坏、刀圈崩裂、偏磨等严重状态。五分类会拆成正常、轻微磨损、中度磨损、严重磨损、故障,但对标注数据量要求高,容易让类间边界模糊。初期项目建议先做三分类,把模型框架跑通再考虑细化。

标签来源一般有两种:一是换刀记录对齐掘进时间,换下来的刀由检修人员目测分级;二是根据掘进参数变化反向推断,比如推力突然升高伴随扭矩波动,大概率是滚刀偏磨。前者准确但样本少,后者样本多但噪声大。我通常的做法是二者结合,以换刀记录为主,用参数突变做辅助扩充,同时把所有不确信的样本丢进一个“未知”类别,不强行归到正常或故障里。

def label_from_maintenance(maintenance_log, cutting_time_df): # 换刀记录包含 start_time, end_time, cutter_id, condition_label merged = pd.merge_asof(cutting_time_df.sort_values("timestamp"), maintenance_log.sort_values("start_time"), left_on="timestamp", right_on="start_time", direction="backward") merged["label"] = merged["condition_label"].fillna("normal") return merged["label"].values y = label_from_maintenance(maintenance_log, time_df) print("标签分布:", pd.Series(y).value_counts().to_dict())

用merge_asof做时间对齐是个小技巧,它能避免手动写循环去匹配每条样本属于哪一段换刀周期。注意刀具状态在停机换刀前的最后一段可能已经恶化,但维护记录里只写了“更换时状态”,所以要把换刀时间往前推一段作为标签生效起点,否则那段样本会被错误标成正常,模型学习时就乱了。

2.3 特征工程:时域、频域与统计特征一个都不能少

虽然 CNN、LSTM 这类深度模型号称能自动提特征,但滚刀信号信噪比低,纯端到端训练容易过拟合。常见做法是给模型同时喂“原始窗口 + 手工特征”或者只喂手工特征。SVM 和随机森林本身处理序列能力弱,所以对它们必须做特征工程。

时域特征包括均方根值、峰值因子、峭度、波形因子;频域特征包括主频幅值、边频带能量、重心频率。峭度对早期轴承故障敏感,边频带能量对齿轮啮合问题敏感。对滚刀来说,刀圈崩裂会产生冲击脉冲,在频谱上表现为宽频能量抬升,而偏磨会让振动主频出现周期性调制。

from scipy import stats, signal def extract_features(window_2d): # 只针对振动通道提取特征,推力与扭矩直接保留原始值 vib = window_2d[:, 0] feats = { "rms": np.sqrt(np.mean(vib ** 2)), "kurtosis": stats.kurtosis(vib), "peak_factor": np.max(np.abs(vib)) / (np.sqrt(np.mean(vib ** 2)) + 1e-8), "crest_factor": np.max(np.abs(vib)) / (np.mean(np.abs(vib)) + 1e-8), } freqs, psd = signal.welch(vib, fs=1000, nperseg=256) feats["main_freq"] = freqs[np.argmax(psd)] feats["main_amp"] = np.max(psd) # 边频带能量:主频两侧 5 Hz 范围内能量之和 main_idx = np.argmax(psd) sideband_energy = np.sum(psd[max(0, main_idx-5): main_idx+6]) feats["sideband_energy"] = sideband_energy return feats feature_rows = [extract_features(X[i]) for i in range(X.shape[0])] feature_df = pd.DataFrame(feature_rows)

这里只对振动通道做了特征提取,推力、扭矩、温度这些通道直接作为额外列拼进特征矩阵。signal.welch的fs参数必须和实际采样率一致,nperseg越大频率分辨率越高,但单次傅里叶变换用到的数据越长,对冲击性信号反而平均掉了细节。经验值是 256 或 512,兼顾分辨率和时间响应。


3. CNN 与 LSTM 在滚刀振动序列上的建模:搭法、输入形状与调参

3.1 CNN 为什么能用在滚刀状态识别

CNN 通常让人想到图像,但一维卷积同样适合振动序列。它通过局部感受野提取短时波形模式,比如冲击脉冲、周期性调制,再通过池化降维保留显著特征。对滚刀信号,一维 CNN 比二维 CNN 更自然,因为输入是一维时间序列,强行转成二维频谱图反而丢失了时间对齐信息。

常见结构是三层一维卷积加全局平均池化,接全连接层输出三分类概率。卷积核大小选 3 或 5,通道数从 16、32、64 递增。我在实际项目里发现卷积核太大容易把突变冲击平滑掉,所以倾向于小卷积核堆深度。

import torch import torch.nn as nn class CutterCNN1D(nn.Module): def __init__(self, n_channels=5, n_classes=3): super().__init__() self.features = nn.Sequential( nn.Conv1d(n_channels, 16, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(16, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.classifier = nn.Linear(64, n_classes) def forward(self, x): # x 形状: (batch, n_channels, window_len) x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x) model = CutterCNN1D(n_channels=5, n_classes=3) dummy = torch.randn(2, 5, 5000) print("输出形状:", model(dummy).shape)

留意nn.Conv1d的输入形状是(batch, channels, length),而 pandas 里数据是(batch, length, channels),所以送入模型前要转置。padding=2和padding=1是为了让卷积不缩短序列长度,池化层负责降维。AdaptiveAvgPool1d(1)把最后一层特征图压成 1 个点,好处是不管输入窗口长度怎么变,全连接层输入维度都是 64。

训练时建议用交叉熵损失和 Adam 优化器,学习率初始 1e-3,每 20 个 epoch 衰减 0.5。数据量少于 5000 样本时,两层卷积就够了,堆第三层容易在验证集上先升后降。

3.2 LSTM 与 GRU:处理长序列时的选择标准

LSTM 适合捕捉振动序列里的长期依赖,比如偏磨状态下周期性调制会持续多个旋转周期。但 LSTM 参数多,滚刀训练数据一般不够大,容易过拟合。GRU 是 LSTM 的简化版,参数少,收敛快,在很多时序分类任务上效果接近 LSTM。

这里的关键不是纠结 LSTM 还是 GRU,而是如何把窗口序列喂进去。如果窗口长度 5000,直接把 5000 步输入 LSTM,显存和时间都吃不消。常见做法是先降采样或者分段,把窗口切成若干个片段,每个片段做一次统计聚合,再把聚合后的序列送入 LSTM。

def segment_pool(sequence, seg_len=500): # sequence: (window_len, n_features) -> (n_segs, n_features_agg) n_segs = sequence.shape[0] // seg_len segs = sequence[:n_segs * seg_len].reshape(n_segs, seg_len, -1) return np.concatenate([segs.mean(axis=1), segs.std(axis=1)], axis=1) seg_feats = segment_pool(X[0]) # (10, 10) class CutterGRU(nn.Module): def __init__(self, input_size=10, hidden_size=32, n_classes=3): super().__init__() self.gru = nn.GRU(input_size, hidden_size, batch_first=True, bidirectional=True, num_layers=1) self.fc = nn.Linear(hidden_size * 2, n_classes) def forward(self, x): out, _ = self.gru(x) # out: (batch, seq, hidden*2) return self.fc(out[:, -1, :]) model = CutterGRU(input_size=seg_feats.shape[1]) print("GRU 输出:", model(torch.from_numpy(seg_feats[None]).float()).shape)

segment_pool把 5000 步压成 10 段,每段用均值和标准差代表,这样 LSTM/GRU 只需处理 10 个时间步,训练速度大幅提升。实际效果比原始序列还好,因为聚合削弱了噪声。GRU 用双向时,输出维度翻倍,最后取最后一个时间步的输出做分类。不要在整个序列上做 mean-pooling,那种做法会丢掉状态切换的时间信息。

3.3 CNN+LSTM 混合模型的正确拼接方式

滚刀状态识别里,CNN 负责局部模式提取,LSTM 负责时间依赖建模,混合模型确实比单一模型准一点,但很多人拼接方式有问题。常见误区是让 CNN 输出直接接 LSTM 层,但没注意 CNN 池化后序列长度变短,LSTM 步数对不上。正确做法是让 CNN 输出的每个时间步对应原始序列的一段,也就是卷积时保持时间分辨率,只在最后一个维度做压缩。

class CutterCNNGRU(nn.Module): def __init__(self, n_channels=5, n_classes=3, hidden_size=32): super().__init__() self.conv = nn.Sequential( nn.Conv1d(n_channels, 16, kernel_size=5, padding=2), nn.ReLU(), ) self.gru = nn.GRU(16, hidden_size, batch_first=True, bidirectional=True) self.fc = nn.Linear(hidden_size * 2, n_classes) def forward(self, x): # x: (batch, channels, steps) x = self.conv(x) # (batch, 16, steps) x = x.transpose(1, 2) # (batch, steps, 16) out, _ = self.gru(x) return self.fc(out[:, -1, :]) model = CutterCNNGRU() print("混合模型输出:", model(torch.randn(2, 5, 5000)).shape)

这个结构里卷积层没有池化,所以时间步不变,GRU 每个时间步接收的是该时间点上 5 个通道经卷积融合后的特征。因为 GRU 本身就在做时序压缩,前面用池化反而多余。若窗口太长,仍然可以先用上面segment_pool缩短到几百步,再接卷积和 GRU,训练效率好很多。


4. SVM 与随机森林:小样本场景下的高性价比基线

4.1 特征矩阵构建与标签编码

SVM 和随机森林不能吃原始时序,得用第 2 节的特征工程结果。特征矩阵每行是一个时间窗口样本,每列是一个特征。特征数量大概 20 到 50 个之间,包含时域统计、频域能量、原始通道的均值和标准差等。分类目标用 0、1、2 表示正常、磨损、故障。

这里必须强调特征缩放。SVM 基于距离计算,如果推力是几百 kN,峭度只有个位数,不加缩放的话推力特征会主导整个模型。随机森林虽然不受缩放影响,但统一缩放能让特征重要性解释更稳定。

from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.model_selection import train_test_split feature_df_scaled = StandardScaler().fit_transform(feature_df) label_encoder = LabelEncoder().fit(np.array(["normal", "wear", "fault"])) y_enc = label_encoder.transform(y) # 按时间顺序划分,不打乱 split_idx = int(len(feature_df_scaled) * 0.8) X_train, X_val = feature_df_scaled[:split_idx], feature_df_scaled[split_idx:] y_train, y_val = y_enc[:split_idx], y_enc[split_idx:] print("训练集大小:", X_train.shape, "验证集大小:", X_val.shape)

train_test_split默认会 shuffle,这里故意用切片替代,因为同一时间窗口的样本内部有强相关性,随机打乱会让验证集泄漏。另一个容易忽略的点是标准化时只用训练集的均值和方差,测试集在预测时用同一组参数变换,不能用fit_transform叠加到测试集上。

4.2 随机森林的关键参数:树的数量、深度与类别权重

随机森林在滚刀状态识别里表现稳定,适合做项目基线。最重要的参数是n_estimators、max_depth、min_samples_leaf、class_weight。类别不平衡在故障样本上特别明显,故障数据可能只有正常的十分之一,不处理时模型会对故障全部预测为正常,准确率看着挺高,召回率接近零。

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=300, max_depth=12, min_samples_leaf=3, class_weight="balanced", n_jobs=-1, random_state=42, ) rf.fit(X_train, y_train) print("随机森林验证集准确率:", rf.score(X_val, y_val)) print("特征重要性 Top5:", pd.Series(rf.feature_importances_, index=feature_df.columns).nlargest(5).to_dict())

class_weight="balanced"会让少数类样本获得更高的惩罚权重,比手动改样本来得省事。n_estimators不是越大越好,超过 300 后准确率基本平稳但推理时间线性增长。max_depth设置到 12 左右能防止单棵树记住噪声,min_samples_leaf=3强制叶子节点至少 3 个样本,缓解过拟合。

4.3 SVM 核函数选择与网格搜索配置

SVM 在样本量只有几千时非常合适,但核函数选错就是灾难。滚刀特征矩阵一般不是线性可分的,RBF 核默认是首选,因为它能把特征映射到无穷维,理论上拟合能力强。但gamma参数很敏感:gamma太大模型只关注离决策边界近的样本,容易过拟合;太小则把所有样本看得太相似,欠拟合。

我用网格搜索配合交叉验证来找C和gamma,但要注意滚刀数据不能做普通的 KFold 交叉验证,要用TimeSeriesSplit保持时间顺序。

from sklearn.svm import SVC from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.001, 0.01, 0.1, 1], "kernel": ["rbf"], } svm = SVC(probability=True, class_weight="balanced", random_state=42) tscv = TimeSeriesSplit(n_splits=4) search = GridSearchCV(svm, param_grid, cv=tscv, scoring="f1_macro", n_jobs=-1) search.fit(X_train, y_train) print("最优参数:", search.best_params_) print("验证集 f1_macro:", search.best_score_)

f1_macro比准确率适合类别不平衡的滚刀数据,它对每个类算 F1 再取平均,少数类表现不好时分数明显下降。probability=True会启用 Platt 缩放,虽然会多花训练时间,但后续要做置信度筛选必须有概率输出。如果数据量超过 2 万样本,SVM 的训练时间会显著变长,可以先用线性核试跑一遍,效果差再换 RBF。


5. 模型训练中的避坑指南:这些问题不看会翻车

5.1 标签时间偏移让模型学到的全是“换刀前异常”

现象:训练集准确率接近 100%,验证集也还行,但部署到现场后频繁报警,且报警时间总在换刀前几天。

原因:换刀记录写的是停机检查时刻的状态,但滚刀状态恶化早在停机前就开始了。标签生效点晚于实际恶化点,模型被迫把“正常段”学成“故障段”。

解决:把换刀记录里的时间往前偏移一个窗口或者多个窗口,偏移量取刀具平均劣化周期的一半,比如平均 20 天换一次刀,就把标签提前两天。同时把换刀前 24 小时内的样本标记为“过渡态”,训练时忽略这一类,只保留状态明确的样本。

def shift_label_time(maintenance_log, shift_hours=48): log = maintenance_log.copy() log["start_time"] = log["start_time"] - pd.Timedelta(hours=shift_hours) return log

5.2 验证集随机打乱导致指标虚高

现象:模型在随机划分的验证集上 F1 超过 0.95,换成按时间顺序切分的验证集只有 0.82,差距巨大。

原因:相邻时间窗口样本高度相似,随机划分时训练集和验证集会包含同一段时间的相似样本,等于模型见过答案再考试。

解决:全部使用TimeSeriesSplit或按时间点切分。滚刀数据只能在“换刀周期”内独立,不能把同一把刀不同时间的样本同时放进训练集和验证集,否则就是数据泄漏。

5.3 原始信号里混入停机段和空推段

现象:模型在训练时把“停机状态”识别成“正常”,因为停机时振动为零、推力为零,这类样本在正常类别里占了很大比例。

原因:数据采集是连续记录的,包含非掘进时段。停机样本特征异常突出,模型很容易找到这条捷径。

解决:在切片前用刀盘转速或推进速度过滤掉非掘进段,比如推进速度小于 5 mm/min 或者刀盘转速为零的段直接删除。还要警惕“空推段”和“停机段”特征相似,但实际刀具没有受力,两种状态不能混为一类。

5.4 特征重要性波动大,换一批数据结果完全不同

现象:随机森林特征重要性排序第一次跑和第二次跑差别很大,模型准确率却差不多。

原因:特征之间存在共线性,比如振动 RMS 与峰值因子强相关,随机森林选哪个作为分裂特征具有随机性,重要性被分散。

解决:先做相关性分析,对相关性高于 0.9 的特征只保留一个。再用递归特征消除确认核心特征集,最终模型只喂 10 到 15 个特征,既稳定又减少过拟合。

corr = feature_df.corr().abs() upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool)) high_corr = [col for col in upper.columns if any(upper[col] > 0.9)] print("高相关特征:", high_corr)

5.5 样本量太少时深度模型不如树模型

现象:只有 2000 多个样本时,LSTM 训练后验证集 F1 只有 0.7,随机森林反而能到 0.85。

原因:LSTM 参数量大,小样本学不到足够泛化的时序模式,树模型对特征直接做切分,样本利用效率更高。

解决:样本不足 5000 时优先用随机森林或 SVM 做基线和上线方案,把深度学习留给数据扩充之后。真要用 CNN/LSTM,就得用滑动窗口生成更多样本,并加入强数据增强:随机缩放、时序遮蔽、加噪声。


6. 模型部署与现场验证:用滚动预测替代一次性判断

最后聊一下部署阶段的具体验证技巧。滚刀状态识别不是“训练完一个模型就交差”,现场更关心模型能不能提前预警,而不是事后报告。我习惯用滚动预测的方式:每 30 秒取一个窗口,用模型输出各类概率,然后用滑动的多数投票或置信度阈值决定是否报警。

def rolling_predict(model, buffer, window_len=5000, step=1500, threshold=0.8): votes = [] for start in range(0, len(buffer) - window_len + 1, step): window = buffer[start:start + window_len] proba = model.predict_proba(window.reshape(1, -1))[0] votes.append(np.argmax(proba)) if len(votes) == 0: return "no_data", 0.0 final_label = max(set(votes), key=votes.count) avg_conf = np.mean([model.predict_proba(buffer[i:i+window_len].reshape(1, -1))[0][final_label] for i in range(0, len(buffer)-window_len+1, step)]) if avg_conf < threshold: return "uncertain", avg_conf return final_label, avg_conf

这个函数模拟了现场边采集边预测的逻辑。step小于窗口长度时会产生重叠预测,重叠越多越平滑,但计算量也翻倍。threshold=0.8表示平均置信度不足 0.8 时宁可报“不确定”,也不乱报故障,这是现场能接受的底线。实际部署时我会把“不确定”单独列为一个状态,让它触发人工复核流程,而不是简单忽略。

验证阶段还要做坏样本回放:把历史换刀记录里明确标注故障的时段挑出来,看模型提前多少小时给出故障预警。我给自己定的标准是至少提前 6 小时,如果模型只能在换刀前 1 小时才报,说明特征还太钝,需要回去加频域边频带特征或调高 LSTM 序列长度。

最后说一条我自己的教训:不要追求模型越复杂越好。我在一个项目里把 CNN+BGRU 的 hidden_size 调到 128,准确率涨了 0.5 个百分点,但部署到工控机上推理时间从 8 毫秒涨到 35 毫秒,现场数据采集线程差点被阻塞。后来把 hidden_size 降到 32,推理时间回到 10 毫秒以内,准确率几乎没掉。滚刀状态识别这个场景,稳定比涨点重要得多,模型结构选参数量最小的那个能达标的方案就行。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表