滚刀状态识别五模型对比:从数据划分到工程落地
简介一套面向隧道掘进装备运维场景的滚刀状态识别项目涵盖CNN、LSTM、GRU、SVM、随机森林等多种建模思路适合机器学习初学者、毕业设计及课程设计者快速上手。压缩包共15个文件其中8个Python脚本覆盖特征生成、数据融合、模型训练与测试全流程4个CSV为各工况数据集另含1张效果图、1份说明文档及Git配置信息整体仅2.63MB轻量易部署。项目代码结构清晰注释友好从原始数据到最终状态判别均有对应模块便于逐行理解与二次开发。目前已有233人学习下载作为导师认可的98分项目能够直接用于开题演示、答辩展示或作为期末大作业的完整参考。下载后只需按README配置环境并运行主脚本即可复现实验效果。1. 滚刀状态识别为什么值得做一个让盾构机停机的问题在盾构/TBM 施工里滚刀磨损从来不是“换个零件”那么简单。换刀前要停机、要开仓带压作业一次的成本从几十万到上百万不等误判一次换刀时机要么提前换掉还能用的刀造成浪费要么磨到刀圈崩裂伤及刀盘直接变成事故。这个场景做 Python 机器学习本质是把老师傅听声音、看渣样的经验用振动信号和施工参数量化成可复现的模型。CNN、LSTM、GRU 处理原始波形SVM、随机森林处理手工特征五个模型跑同一套数据图的是“不同工况下总有一个能稳”。这篇文章适合正在做机械故障诊断、施工监测系统或者想用公开数据集练一遍完整建模流程的从业者——你会发现这个方向比想象中更依赖数据划分和标签定义模型反而不是瓶颈。2. 数据从哪来、怎么切窗先搞清楚滚刀状态数据长什么样2.1 滚刀状态识别的两种数据形态与标签定义滚刀状态识别的数据来源和轴承故障诊断类似但工况更复杂。常见传感器布置是刀盘主轴承座和刀梁上安装加速度传感器采样率一般在 10kHz 到 25.6kHz 之间同时采集刀盘转速、推力、扭矩、贯入度这些施工参数。所以数据集里通常同时存在两类数据数据形态通道数采样特点适合模型振动加速度信号1-4 通道高频连续采样CNN、LSTM、GRU、特征工程后给 SVM/RF施工参数表5-10 列每 1-10 秒一条直接做表格特征给 SVM/RF标签定义是这个项目的第一道坎。滚刀状态一般按磨损量分级正常、轻微磨损、中等磨损、严重磨损再把偏磨和崩刃单独归为异常类。很多项目实际只分三档——正常、磨损、异常因为偏磨样本太少硬凑四分类会让模型学不到有效边界。import pandas as pd # 标签映射根据刀圈磨损量mm自动打标 def map_wear_label(wear_mm: float) - int: 磨损量 - 分类标签 0: 正常 (2mm) 1: 轻微磨损 (2-5mm) 2: 严重磨损 (5mm) if wear_mm 2.0: return 0 elif wear_mm 5.0: return 1 else: return 2 # 以某次换刀记录为例把每把刀的磨损量映射成标签 tool_records pd.DataFrame({ tool_id: [T01, T02, T03], wear_mm: [1.2, 3.8, 6.5] }) tool_records[label] tool_records[wear_mm].apply(map_wear_label) print(tool_records)这段代码的价值在于把“换刀记录”变成“监督信号”。实际项目中换刀记录是人工填的磨损量是游标卡尺量的数据质量参差不齐标签噪声会直接限制模型上限。我一般会额外加一步同一把刀在临近几个掘进循环的标签做一致性校验换刀记录里磨损量明显不合理比如掘进 5 米就标严重磨损的要人工复核。关于数据集如果手头没有盾构实际项目积累最常见做法是用公开的 XJTU-SY 滚动轴承数据集跑通全流程。轴承和滚刀的失效机理有差异但振动信号的冲击特征和退化趋势是相似的适合用来验证代码链路和调参逻辑最后再替换成自己项目的滚刀数据。2.2 滑窗切分与分组划分防止数据泄漏的第一道关卡拿到原始振动信号后第一步是切窗口。因为滚刀磨损是一个缓慢退化过程一整段连续信号不能直接丢进模型——一个窗口要作为一个独立样本。工程上通常用滑窗把连续信号切成若干段窗口长度 2-10 秒重叠率 50%这样既能保留足够的冲击特征周期又不至于让样本数量膨胀到训练太慢。import numpy as np def sliding_window(data: np.ndarray, window_size: int, overlap: float 0.5): 对一维振动信号做滑窗切分。 data: 单个通道的振动信号形状 (n_samples,) window_size: 窗口点数 采样率 * 窗口时长 overlap: 重叠率 0~1 返回: (n_windows, window_size) step int(window_size * (1.0 - overlap)) n_windows (len(data) - window_size) // step 1 windows np.stack([ data[i * step: i * step window_size] for i in range(n_windows) ]) return windows # 假设 10kHz 采样窗口取 2 秒重叠 50% fs 10000 window_size fs * 2 # 20000 点 raw_signal np.random.randn(fs * 60) # 模拟 60 秒振动信号 windows sliding_window(raw_signal, window_size, overlap0.5) print(f原始长度: {len(raw_signal)}切出窗口数: {windows.shape[0]})窗口长度直接影响模型能看到的时间尺度。2 秒窗口大概能覆盖 20-40 个刀盘转动周期足够捕捉滚刀破岩的周期性冲击如果窗口短到 0.5 秒冲击特征可能只出现一两次模型很难学出稳定的模式。参数设置的核心矛盾是窗口越长、样本越少、训练数据越稀疏窗口越短、样本越多、但单样本信息量不足。切完窗口之后才是真正的坑数据划分。很多人在这里直接把所有窗口打乱随机分训练集和测试集结果训练准确率 99%测试集一塌糊涂——因为同一个掘进区间的相邻窗口内容几乎一样随机划分等于让模型“开卷考试”。正确做法是按刀号或掘进循环分组同一把刀的所有窗口只能出现在一个集合里。from sklearn.model_selection import GroupShuffleSplit # 假设已有窗口数据和每组对应的 tool_id # X: (n_windows, window_size), y: (n_windows,), groups: (n_windows,) splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(splitter.split(X, y, groupsgroups)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx]用到GroupShuffleSplit而不是普通train_test_split核心就是保证同一把刀的数据不会同时出现在训练集和测试集里。这一点没做对后面五个模型全部白跑评估分数全部虚高而且你根本察觉不到问题——直到模型上线遇到新刀准确率直接腰斩。3. 五模型选型端到端交给 CNN/LSTM/GRU手工特征交给 SVM/随机森林3.1 CNN 与 LSTM/GRU振动信号的“空间模式”还是“时序依赖”滚刀破岩时刀圈与岩面接触产生冲击振动。正常磨损的滚刀振动呈现相对均匀的周期性冲击磨损严重的滚刀冲击幅值增大、高频成分变多。这里有两类特征可以捕捉一类是单个冲击波形的形态特征局部的、模式化的另一类是冲击间隔和幅值变化的趋势特征时间上的、依赖上下文的。CNN 适合前者。一维卷积核在时间轴上滑动本质上是在学习“什么样的局部波形像磨损状态”。比如一个卷积核学到“高频小幅振动 周期性大冲击”的组合就能对严重磨损给出较高响应。LSTM/GRU 适合后者它们能记忆一段时间的振动变化趋势——比如冲击幅值在 10 秒内持续爬升这往往是刀圈磨平的前兆。模型输入形态优势劣势收敛速度CNN (Conv1D)原始窗口波形提取局部冲击特征强、训练快对长时间依赖建模弱快LSTM原始窗口波形捕捉时序退化趋势训练慢、容易过拟合慢GRU原始窗口波形LSTM 的轻量替代表达力略弱于 LSTM中SVM (RBF)手工特征向量小样本下稳、可解释特征工程工作量大极快随机森林手工特征向量抗过拟合、特征重要性可解释对高维时序特征不敏感快实际做的时候我一般会让 CNN 和 GRU 都试一遍对比两者差异。振动信号里的磨损特征通常局部性更强GRU 不该比 CNN 有明显优势如果你的数据集里 GRU 显著好于 CNN先检查是不是把同一把刀的数据泄漏进了训练和测试集——这个信号比任何评估指标都更早暴露划分问题。import tensorflow as tf from tensorflow.keras import layers def build_cnn_model(input_shape, num_classes3): 1D CNN 模型Conv1D 提取局部冲击特征 inputs tf.keras.Input(shapeinput_shape) x layers.Conv1D(filters32, kernel_size64, strides4, paddingsame, activationrelu)(inputs) x layers.MaxPooling1D(pool_size4)(x) x layers.Conv1D(filters64, kernel_size32, strides2, paddingsame, activationrelu)(x) x layers.GlobalAveragePooling1D()(x) x layers.Dense(64, activationrelu)(x) outputs layers.Dense(num_classes, activationsoftmax)(x) return tf.keras.Model(inputs, outputs)CNN 的两个关键参数是kernel_size和strides。kernel_size64在 10kHz 采样下意味着每次卷积覆盖 6.4ms 的信号大约能包住一次滚刀冲击的局部脉冲strides4让窗口在时间轴上步进 0.4ms相当于对信号做了一次降采样后续层看到的时间范围更大而参数更少。这两个值如果是拍脑袋定的优先参考信号的冲击周期——FFT 后找到主冲击频率对应周期的一半做初始 kernel size 往往效果更好。3.2 SVM 与随机森林小样本场景下的稳健基线深度学习模型需要大量样本而滚刀状态识别这个场景恰恰常常面临样本不足的问题——一个盾构区间可能只有十几把换下来的刀每把刀切出几百个窗口数据量撑不起深度模型。此时传统机器学习模型是务实的选择甚至应该作为基线优先跑通。传统模型的输入是手工特征。振动信号的常用特征分三组时域特征均方根 RMS、峰值、峭度、波形因子频域特征FFT 主频幅值、频谱重心、频带能量比以及施工参数推力、扭矩、贯入度、刀盘转速。把这三组特征拼成一个向量标准化后送给 SVM 或随机森林。from scipy import stats, fft import numpy as np def extract_features(window: np.ndarray, fs: int 10000) - np.ndarray: 从单个窗口提取手工特征。 返回: [rms, peak, kurtosis, crest_factor, fft_peak_freq, fft_peak_amp, band_energy_ratio] rms np.sqrt(np.mean(window ** 2)) peak np.max(np.abs(window)) kurtosis stats.kurtosis(window) crest_factor peak / (rms 1e-8) spectrum np.abs(fft.rfft(window)) freqs fft.rfftfreq(len(window), 1 / fs) main_freq freqs[np.argmax(spectrum)] main_amp np.max(spectrum) # 频带能量比1kHz-5kHz 能量 / 总能量 band_mask (freqs 1000) (freqs 5000) band_energy np.sum(spectrum[band_mask] ** 2) total_energy np.sum(spectrum ** 2) 1e-8 return np.array([rms, peak, kurtosis, crest_factor, main_freq, main_amp, band_energy / total_energy])峭度对冲击信号特别敏感——正常振动的峭度接近 3磨损严重的滚刀会产生大量冲击脉冲峭度会显著上升。频带能量比在高频段对磨损状态区分度也高。做完特征提取后用StandardScaler标准化再建模SVM 的 RBF 核会同时考虑不同量纲的特征标准化这一步不能省。from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler def build_sklearn_models(): SVM 与随机森林统一走 Pipeline方便对比。 svm_pipeline Pipeline([ (scaler, StandardScaler()), (svm, SVC(kernelrbf, C10, gammascale, class_weightbalanced, probabilityTrue)) ]) rf_pipeline Pipeline([ (scaler, StandardScaler()), (rf, RandomForestClassifier(n_estimators200, max_depth12, class_weightbalanced, random_state42)) ]) return {svm: svm_pipeline, random_forest: rf_pipeline}class_weightbalanced是滚刀识别这类不平衡数据集中最重要的一个参数。正常情况下“正常”样本占大头严重磨损和异常样本很少SVM 如果不加这个参数最优解往往是“全部预测正常”——准确率还挺高。加了之后模型会对少数类样本的误分类施加更高惩罚代价是整体准确率略降但对严重磨损类别的召回率大幅提升。在这个场景里漏报一把坏刀的成本远高于把好刀多报成坏刀宁可错杀不可漏掉。4. 训练与评估统一接口、独立测试集与混淆矩阵4.1 一版代码跑完五个模型统一训练函数的设计思路五个模型来自不同框架TensorFlow 和 scikit-learn如果每个模型写一套训练脚本一旦数据划分方式变了要改五处对比结论也不可信。更麻烦的是你很难看出模型之间的效果差异究竟来自模型本身还是来自预处理细节不一致。所以我把训练流程抽象成一层统一接口吃进去训练数据和配置吐出来训练好的模型和评估指标。def train_and_evaluate(model, X_train, y_train, X_test, y_test, is_deep_modelFalse): 统一训练评估入口。 深度学习模型直接传 compiled modelsklearn 模型传 pipeline。 if is_deep_model: history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs30, batch_size64, callbacks[tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue)] ) test_pred model.predict(X_test).argmax(axis1) else: model.fit(X_train, y_train) test_pred model.predict(X_test) from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_test, test_pred, digits3)) print(Confusion Matrix:) print(confusion_matrix(y_test, test_pred)) return model这套接口的关键是is_deep_model这个开关。深度学习模型用EarlyStopping训练 30 轮但 patience5意味着连续 5 轮验证集不降就不再训练——滚刀数据量小深度学习模型很容易在 15 轮内过拟合把验证集损失打印出来观察下降曲线比固定轮数更靠谱。sklearn 模型则是一次训练完成不需要这套逻辑。任何训练脚本跑出来的指标第一眼看分类报告里“严重磨损”类别的 F1-score而不是整体准确率。如果整体准确率高但严重磨损的召回率很低说明模型被多数类带偏了后面要么调 class_weight要么增加少数类样本的权重浅层方法或者用 SMOTE 做重采样。4.2 用混淆矩阵盯“漏报”不看整体准确率看坏刀能不能被抓住单独一张准确率数字说明不了任何问题。想象一个极端情况测试集里 90% 是正常样本10% 是严重磨损——模型全部预测正常准确率 90%看起来还行但一把坏刀都没找出来。所以分类报告和混淆矩阵必须一起看。import matplotlib.pyplot as plt import itertools def plot_confusion_matrix(cm, class_names[正常, 轻微磨损, 严重磨损]): plt.figure(figsize(6, 5)) plt.imshow(cm, interpolationnearest, cmapBlues) plt.colorbar() tick_marks range(len(class_names)) plt.xticks(tick_marks, class_names) plt.yticks(tick_marks, class_names) thresh cm.max() / 2. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, cm[i, j], hacenter, vacenter, colorwhite if cm[i, j] thresh else black) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.tight_layout() plt.show()每次训练完把混淆矩阵打出来我最关注两个位置左下角真实为严重磨损、预测为正常和右上角真实为正常、预测为严重磨损。左下角是漏报错过了坏刀意味着施工风险右上角是误报提前换刀意味着成本损失。两个数字要一起看模型的阈值调整、类别权重调整目标都应该是让左下角尽可能接近 0同时右上角控制在一个可接受的范围比如不超过正常样本的 20%。配合classification_report输出的 per-class precision/recall/F1我一般会记录一张候选模型对比表五个模型在同一测试集上的 Precision、Recall、F1 和推理耗时用这张表决定最终部署哪个模型。磨刀不误砍柴工这步值得花时间做细。5. 滚刀状态识别的 5 个常见坑与排查建议5.1 训练集准确率 99%、测试集 60%数据泄漏在作祟现象CNN 训练时验证集准确率一路冲到 99%但换到新数据上只有六成多模型表现得像“背过了答案”。原因建了滑窗之后直接用train_test_split随机切分。同一把刀同一个掘进循环的相邻窗口被分到了训练集和测试集它们之间的差异极小模型学到的其实是“记住这些相似的波形”而不是“识别磨损的模式”。解决先用GroupShuffleSplit按刀号分组划分保证同一把刀的所有窗口只出现在一个集合里。如果项目里没有稳定的刀号字段可以用“时间戳 掘进循环”作为分组 ID效果一样。这个改动会让准确率明显下降——不要慌这个下降是好事说明你终于拿到了一个诚实的评估数字。5.2 LSTM/GRU 训练损失不降模型完全学不动现象LSTM 训练 20 轮loss 基本不变化甚至比随机猜测还差。原因最常见的是窗口太长时间步数过大。假设窗口 10 秒、采样率 25.6kHz一个样本就是 256000 个时间步LSTM 在这种输入下反向传播路径极长梯度很容易消失。另一种可能是输入数据没有做归一化LSTM 内部激活函数对量级非常敏感。解决先检查输入维度。把时间步控制在 500 以内方法要么缩短窗口到 1-2 秒要么对原始信号做降采样比如从 25.6kHz 降到 5kHz。再做一次标准化让输入均值接近 0、方差接近 1。如果还是学不动果断退一步GRU 收敛通常比 LSTM 更容易训练速度也更快在滚刀数据上效果往往不差。5.3 SVM 预测结果几乎全部是“正常”少数类样本被无视现象测试集里 10% 的严重磨损样本SVM 一个都没识别出来分类报告里严重磨损的 Precision 和 Recall 都是 0。原因数据集类别不平衡严重默认的 SVM 优化目标追求整体准确率把所有样本判为数量占绝对优势的正常类正好是全局最优解。解决class_weightbalanced是最快解法。如果加了之后过拟合到少数类上可以手动调整类别权重class_weight{0: 1.0, 1: 2.0, 2: 5.0}严重磨损类的权重加到 5 甚至更高。这里有个经验值初始权重按“多数类数量 / 该类别数量”的比例缩放然后以 2 的倍数上下调看混淆矩阵里漏报和误报的平衡点在哪。5.4 混淆矩阵里“轻微磨损”和“严重磨损”混在一起边界模糊现象模型主要错误集中在相邻类别之间——轻微磨损被预测为严重磨损或者反过来但正常类基本不错。原因磨损量本身就是连续变量你把它切成三档边界处的样本本来就“长得像邻居”。比如 5mm 的轻微磨损和 5.5mm 的严重磨损振动信号差异可能极小人工都未必能分清。解决这是标签定义问题不是模型问题。要么引入“中间态”标签把轻微磨损扩展成“可继续掘进”把严重磨损定义成“需要停机”——给模型更清晰的二分类边界要么在评估时把相邻类别的错误降级比如把“严重预测成轻微”算 0.5 个错误而不是 1 个这样评估指标更能反映工程价值。5.5 CNN 训练完部署到新工地效果下降明显现象实验室测试集 F1 0.85换了一个工地数据直接跌到 0.6。原因不同地层的岩性差异很大。硬岩段和软土段对滚刀造成的冲击特征完全不同模型在 A 地层学到的磨损模式在 B 地层不一定适用。解决把地层的岩性信息或贯入度、推力变化区间作为特征加进去让模型学到“相同磨损状态下不同地层中信号表现不同”的对应关系。另一种做法是建立分层模型先根据地层参数区分工况再在每个工况子集中各自建模。这是滚刀识别部署中最棘手的一环数据积累比调参更重要。6. 工程落地置信度阈值与滑窗投票把误报降到可接受范围模型训练完成只是开始。实际部署时模型是每秒都在推理的而施工方只关心一个答案“现在是否需要停机检查”。单个窗口的预测结果噪声很大直接拿来做决策会让现场人员每隔几分钟接到一次误报警报。我一般会在模型输出后面加两道过滤逻辑置信度阈值 连续窗口投票。def sliding_decision(prob_sequences, threshold0.7, need_count3): 滑窗投票决策超过阈值且连续 need_count 次预测一致才触发报警。 prob_sequences: (n_windows, num_classes) 每个窗口的 softmax 输出 threshold: 置信度阈值 need_count: 连续一致窗口数 返回: 最新决策 0正常, 1轻微磨损, 2严重磨损 pred_class prob_sequences.argmax(axis1) max_prob prob_sequences.max(axis1) # 只保留置信度超过阈值的窗口低于阈值视为“不确定”不参与投票 confident max_prob threshold recent list(zip(pred_class, confident))[-need_count:] if len(recent) need_count: return 0 # 数据不足默认正常 classes [c for c, is_conf in recent if is_conf] # 连续 need_count 个窗口全部高于置信度阈值且类别一致才报警 if len(classes) need_count and len(set(classes)) 1: return classes[0] return 0逻辑很简单模型每秒钟输出一个新窗口的预测结果我先看这个预测的 softmax 置信度是否超过 0.7。置信度不足的窗口直接丢弃不参与决策。剩下置信度足够的窗口如果连续 3 个窗口都预测为同一个类别比如严重磨损才触发报警。这样单次异常冲击造成的误报会被过滤掉而真实的磨损状态持续存在时相邻窗口大概率会输出一致结果。阈值怎么选我一般用验证集做一次扫参把 threshold 从 0.5 到 0.9 每隔 0.05 试一遍同时观察漏报率和误报率的变化。之前做类似项目时有个血泪经验把阈值设得太高0.9确实几乎没有误报了但漏报也开始冒头——磨损失效初期的信号本身就模糊要求每个窗口都高置信度会等不到报警。最后落地的阈值通常是 0.65-0.75 这个区间配合连续 3 次投票既能把单点噪声挡住又不至于漏掉真实磨损。这段调参没什么玄学就是拿真实数据反复扫一遍画一张漏报率和误报率随阈值变化的曲线然后让施工方在这条曲线上做选择——他们才是最终为误判买单的人。这个方向真正难的不是把模型训练到 90% 准确率而是面对不同地层、不同刀位、不同掘进参数时模型能不能保持稳定。数据积累是关键每换一次刀把振动数据、施工参数、磨损量记录归档三个月后你手里的数据就是别人拿不走的壁垒。希望帮到你。本文还有配套的精品资源点击获取