ARTICLE DETAIL

资讯详情

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

滚刀状态识别机器学习实战:CNN/LSTM/GRU对比与特征工程全解析

滚刀状态识别机器学习实战:CNN/LSTM/GRU对比与特征工程全解析 简介一套基于机器学习的滚刀刀具状态识别项目源码面向智能制造与设备维护方向的学习者覆盖CNN、LSTM、GRU、SVM、随机森林等多种模型解决利用时序信号识别初期磨损、正常磨损、急剧磨损的问题。项目采用铣刀1、铣刀4、铣刀6的走刀数据以铣刀1为训练样本、铣刀4和6为测试样本并结合一维卷积神经网络等算法完成时间序列分类适合入门到进阶的机器学习实践。资源包共15个文件以8个Python源码、4个CSV数据集为主辅以说明文档和示意图压缩包整体仅2.54MB轻量易用。其中源码按模型拆分如cnn.py、lstm.py等数据文件对应不同铣刀状态目录与命名清晰便于逐一复现和对比。已有279人在CSDN学习下载适合希望通过完整项目快速积累工业时序建模经验、参考数据划分与模型调参思路的读者。1. 滚刀状态识别这份 Python 机器学习源码能不能直接跑通做滚刀状态识别的工程落地最怕的不是看不懂算法而是手里只有一堆不知所云的 CSV 和一两个孤零零的 py 脚本。这份项目把“基于机器学习的滚刀状态识别”这件事拆成了能直接跑的代码训练集用铣刀 1 的走刀数据测试集用铣刀 4 和 6类别是初期磨损、正常磨损、急剧磨损三分类。模型侧同时覆盖了 CNN、LSTM、GRU以及 SVM、随机森林这类传统机器学习算法还带了特征生成脚本 gen_feature.py、Ext.py 和数据合并脚本 merge_data.py。它适合两类人一类是搞设备状态监测的工程师想快速拿到一套可对比的基线另一类是正在学时间序列分类的学生想看看一维卷积和循环网络在真实工业数据上各自的表现。判断一份源码能不能落地我先看三件事数据划分是否合理、训练和测试脚本是否分离、有没有特征工程入口这个项目三样都占了。2. 数据准备与特征工程从走刀 CSV 到模型能吃的输入2.1 数据集划分逻辑与类别不平衡问题拿到压缩包后先看数据侧的文件data_c1.csv、data_c4.csv、data_c6.csv分别对应 1 号、4 号、6 号铣刀其中的“走刀数据”是滚刀或铣刀在切削过程中采集到的时间序列信号。data.csv 可以看成合并或备份文件具体以 README 里的说明为准。摘要里给的关键划分是铣刀 1 的样本全部用于训练铣刀 4 和 6 的样本全部用于测试。每个类别在每把刀里的数量是固定的我整理成一张表方便对照。样本来源初期磨损正常磨损急剧磨损合计训练集铣刀 13021075315测试集铣刀 43021075315测试集铣刀 63021075315这个划分有两个值得注意的地方。第一它按刀具划分而不是按样本随机划分这使得测试集来自模型完全没见过的另一把刀比随机拆分更接近真实部署场景因为工厂里上线后预测的永远是新刀。第二类别并不平衡正常磨损样本是初期磨损的 7 倍网络很容易偷懒把输入一律判成正常磨损也能拿到 66% 的准确率。我在跑的时候会显式调整 loss 权重比如把初期磨损的权重设成 2.0急剧磨损设成 1.5正常磨损保持 1.0让模型对少数类别更敏感。data_c1.csv、data_c4.csv、data_c6.csv 的读取方式要统一。常见做法是先确认文件有没有表头。这个项目里的文件名没有带 header 字样我一般用 headerNone 读然后打印前两行确认列数是否一致。如果不一致说明不同刀的信号通道数不同后面的网络输入维度就得单独处理否则换一把刀就会报维度错。2.2 gen_feature.py 与 Ext.py特征怎么提、参数怎么改特征工程在这类小样本时序任务里比调模型结构更值钱。gen_feature.py 负责把原始 CSV 变成模型能吃的输入常见做法是先归一化、再滑窗切分、最后把每个窗口作为一个样本。下面是一个可以直接套用的滑窗提取函数逻辑和 gen_feature.py 做的事一致import numpy as np import pandas as pd def extract_windows(csv_path, window_size400, step200): df pd.read_csv(csv_path, headerNone) raw df.values.astype(np.float32) # 每列独立做 z-score 归一化避免不同通道量纲差异影响训练 mean raw.mean(axis0) std raw.std(axis0) 1e-8 norm (raw - mean) / std windows [] for start in range(0, norm.shape[0] - window_size 1, step): w norm[start:start window_size] windows.append(w) return np.stack(windows)这段代码里 window_size400 表示每个样本覆盖 400 个连续采样点step200 表示相邻窗口起点间隔 200也就是有 50% 的重叠。重叠窗口能缓解滑动截断带来的边界突变代价是样本数量变大、训练时间变长。如果你的 CSV 第一行是列名把 headerNone 改成 header0 就好。std 加 1e-8 是为了防止某一列信号恒定不变时除零。Ext.py 我理解是特征扩展脚本常见做法是在滑窗后对每个窗口再做统计特征提取比如均值、方差、峰峰值、均方根、过零率拼成一个向量。这类统计特征对后面跑 SVM 和随机森林特别重要因为原始高维信号直接喂给树模型和核方法通常效果不好而且计算慢。如果你打算对比深度模型和传统模型建议把 gen_feature.py 的输出同时保留一份统计特征版本分类任务里多一套特征就多一条后路。2.3 merge_data.py把多把刀的样本合并时最容易丢标签merge_data.py 的名字说明它干的是数据合并。数据合并看似简单最容易翻车的不是数据本身而是标签错位。我一般不会手动拼接而是写一个显式传标签的函数import pandas as pd def merge_with_label(file_label_pairs, output_path): frames [] for path, label in file_label_pairs: df pd.read_csv(path, headerNone) df[label] label df[source] path frames.append(df) merged pd.concat(frames, ignore_indexTrue) merged.to_csv(output_path, indexFalse)这个函数对应 merge_data.py 的核心逻辑。file_label_pairs 可以传 [(data_c1.csv, 0), (data_c4.csv, 1)] 这样的列表label 列会自动加到原数据末尾。source 列记录每个样本来自哪个文件排查数据泄漏时非常有用。合并完成后建议顺手检查一下每个类别的样本数比如用 merged[label].value_counts() 确认 0/1/2 三类比例和摘要里给的一致别等模型训完才发现标签少了一类或顺序错了。提示如果后续训练脚本是按 CSV 的 label 列读标签注意有的 csv 保存时会把 label 变成浮点数 0.0/1.0/2.0。读取后先转成 int再转成 torch 的 LongTensor 或 sklearn 的 y避免分类目标类型不匹配。3. 1DCNN 一维卷积网络为什么滚刀磨损识别要选它参数又怎么设3.1 一维卷积在时间序列分类上的优势刀具磨损状态识别本质上是时间序列分类问题。CNN 的一维卷积版本1DCNN在这个场景有两个明显优势第一卷积核沿时间轴滑动天然适合捕捉局部波形模式比如切削力突变、振动幅值变化、颤振信号这类持续时间很短的局部特征第二一维卷积的参数数量远小于二维卷积在只有几百个样本的工业数据集上更不容易过拟合。相比之下二维 CNN 需要把一维信号折叠成二维矩阵或图像这会打乱原始时间上的先后关系效果往往不如一维卷积直接。你可能会问为什么不直接上 LSTM历史数据量小的时候LSTM 的门结构参数多收敛慢而且样本量不足时更容易过拟合。1DCNN 的平移不变性让模型对相位偏移和轻微的时间错位更鲁棒这在滚刀磨损数据里很重要——同一把刀在不同走刀阶段的信号不是严格对齐的。所以项目把 1DCNN 放在模型列表第一位是有道理的它通常是这类任务的第一个强基线。3.2 cnn.py 里的网络结构Conv1d 的 kernel、stride、padding 怎么配如果 cnn.py 用的是 PyTorch常见的 1DCNN 结构是几层 Conv1d 加 ReLU 加 MaxPool1d最后接全局平均池化和全连接层。下面这个结构参数比较中庸在小样本上不容易翻车import torch import torch.nn as nn class Hob1DCNN(nn.Module): def __init__(self, n_classes3): super().__init__() self.features nn.Sequential( nn.Conv1d(1, 64, kernel_size7, padding3), nn.ReLU(), nn.MaxPool1d(kernel_size3, stride2), nn.Conv1d(64, 128, kernel_size5, padding2), nn.ReLU(), nn.MaxPool1d(kernel_size3, stride2), nn.Conv1d(128, 256, kernel_size3, padding1), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.classifier nn.Linear(256, n_classes) def forward(self, x): x x.unsqueeze(1) # (batch, 1, seq_len) feat self.features(x).squeeze(-1) return self.classifier(feat)Conv1d 的输入要求是三维张量顺序是 batch、通道数、序列长度。原始数据 shape 是 (batch, seq_len)所以在 forward 里先 unsqueeze(1) 把通道维插到第二个位置也就是把信号当成单通道。第一层卷积 kernel_size7相当于卷积核一次看连续 7 个采样点padding3 保证卷积后长度不变stride 保持默认 1用后面的 MaxPool1d 做下采样。通道数从 1 涨到 64、128、256是在逐步把局部模式抽象成更高层的语义。AdaptiveAvgPool1d(1) 负责把每个通道压成一个值不管输入序列多长最后都能得到固定维度的特征向量再接 Linear 输出 3 类。这里有几个参数值得记进实验笔记。卷积核大小影响感受野7 适合采样率不高、单通道信号如果你手里的信号是 10kHz 以上的高采样率可以考虑把第一层 kernel_size 加到 15 或 31让第一层看到更长的一段波形。池化层步长 stride2 会让序列长度每层减半如果输入序列太短比如低于 64 个点三层池化后可能只剩下几个点这时候要减少池化层数量或不用池化改用 stride2 的卷积做下采样。3.3 从 cnn.py 到 cnn_test.py训练和测试脚本为什么要分开项目里同时有 cnn.py 和 cnn_test.py这是工程上很合理的分层。cnn.py 负责搭建网络结构、定义训练流程、保存权重cnn_test.py 负责读取已训练好的权重、加载测试集、输出预测结果。训练和测试分离的基本原则是测试脚本不能碰训练逻辑否则你在调参过程中很容易被测试集带偏最后报告出来的准确率是被测试集“优化”过的乐观值。我跑这类项目时习惯在训练脚本里保存两类文件一类是最好的模型权重按 epoch 记录验证集 loss 最低的那一次另一类是训练曲线数据包括每个 epoch 的训练 loss、验证 loss、学习率。保存的时候用 torch.save(model.state_dict(), cnn_best.pt)测试脚本里先用同样的类定义实例化网络再 load_state_dict。命令行入口一般长这样python gen_feature.py data_c1.csv train_windows.npy python cnn.py --epochs100 --batch_size32 --lr1e-3 python cnn_test.py --weightscnn_best.pt --test_filedata_c4.csv如果项目源码里没有这些命令行参数就直接用脚本里的常量配置训练完成后在 cnn_test.py 里把测试文件路径改成 data_c4.csv 或 data_c6.csv 就行。有一点要提醒测试脚本里不要做任何数据增强也不要用测试集做早停保持测试集是“一次性”的结果才干净。4. LSTM 与 GRU 对比循环网络在滚刀小样本数据上的调参差异4.1 循环网络处理分段序列的两个前提LSTM 和 GRU 在时间序列分类上的思路和 CNN 不同CNN 靠卷积核提取局部特征循环网络靠在时间步之间传递隐藏状态来记忆前后依赖。滚刀磨损信号里磨损劣化是一个渐变过程当前时刻的振动特征和几十毫秒前的状态有关联这正好是 LSTM 的设计动机。但在这个小数据集上直接上 LSTM 有两个前提第一信号要先滑窗分段不能把一整把刀的连续信号全扔进 LSTM否则时间步太长、显存和计算量都受不了梯度也容易消失第二分段后每个样本的序列长度要一致torch 的 LSTM 不支持变长序列直接组成 batch。lstm.py 里通常就是这么干的先调用 gen_feature.py 或自己内嵌滑窗逻辑把每个样本切成长度固定的窗口再 reshape 成 (batch, seq_len, input_size)。注意 PyTorch 的 LSTM 默认接受 (seq_len, batch, input_size) 的顺序如果用 batch_firstTrue输入就是 (batch, seq_len, input_size)。我第一次用 LSTM 时忘了这个顺序转换直接用原始数据喂进去结果训练 loss 完全不下降白白浪费了十几分钟这个问题我放在避坑章里细说。初始化网络时的写法大致像这样import torch.nn as nn class HobLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, n_classes3): super().__init__() self.lstm nn.LSTM( input_size, hidden_size, num_layers, batch_firstTrue, dropout0.3 ) self.classifier nn.Linear(hidden_size, n_classes) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) return self.classifier(out[:, -1, :])最后一层只取最后一个时间步的隐藏状态也就是 out[:, -1, :]把它作为整个序列的压缩表示。如果你的序列长度很长也可以先用全局平均池化把所有时间步的输出都平均一下再接分类器这在小样本上往往更稳因为不会把整个序列的判断押在最后一步。4.2 LSTM 和 GRU 参数对比隐藏层大小、dropout、学习率项目里同时给了 lstm.py 和 gru.py对比这两个模型的参数差异能帮你理解它们的脾气。LSTM 有输入门、遗忘门、输出门三个门参数总量比 GRU 大表达能力更强GRU 只有更新门和重置门参数更少收敛更快。在小样本场景表达能力强不一定是好事因为参数量大了更容易把训练集的噪声记住。我在同一个窗口长度和 batch 条件下分别跑 LSTM 和 GRU直观感受是 GRU 的验证 loss 更平稳LSTM 在 80 个 epoch 后开始出现训练 loss 下降、验证 loss 反弹的过拟合迹象。下面这张表是我在类似小样本时序分类上常用的参数范围这个项目里可以直接拿来当起点参数推荐值不推荐说明隐藏层 size32 ~ 128256 以上训练样本只有 315 时128 已经偏大层数 num_layers1 ~ 23 层以上层数越多越容易过拟合dropout0.2 ~ 0.50循环层之间的 dropout 要开一点学习率1e-3 ~ 1e-41e-2 及以上循环网络对学习率比对 CNN 更敏感batch_size16 ~ 3264 以上小 batch 相当于隐式正则化如果你把隐藏层 size 从 128 调到 16训练时间会明显缩短但分类准确率可能不降反升因为数据量天生撑不起大网络。这是我跑这类项目最大的体会之一在小数据集上正则化手段dropout、小隐藏层、早停比模型容量更值得投资。4.3 SVM 和随机森林怎么接入特征矩阵直接喂 sklearn项目标题里列了 SVM 和随机森林但没有单独的 svm.py 或 rf.py说明这两个模型大概率不是用深度网络那套脚本跑的。常见做法是在 gen_feature.py 输出的特征矩阵上直接调 sklearn。如果你把每个窗口的原始信号摊平成长向量喂给随机森林和 SVM效果通常很差但用上 Ext.py 生成的统计特征均值、方差、均方根、峰峰值等传统模型的表现会立刻好起来。from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score X train_feats # 形状 (315, n_features) y train_labels # 形状 (315,) svm SVC(kernelrbf, C1.0, gammascale, class_weightbalanced) rf RandomForestClassifier(n_estimators200, max_depth8, class_weightbalanced, random_state42) for name, model in [(svm, svm), (rf, rf)]: acc cross_val_score(model, X, y, cv5).mean() print(name, f{acc:.4f})class_weightbalanced 会自动按类别频率给样本加权正好解决训练集里 30 : 210 : 75 的不平衡。SVC 的 C 控制对误分类的惩罚C 越大越容易过拟合gammascale 表示按特征数量自动缩放适合特征量纲不完全一致的场景。随机森林的 max_depth8 是限制单棵树的深度配合 n_estimators200 能在小样本上稳住方差。如果你之前跑过深度网络这里值得注意传统模型不需要 GPU几分钟就能出结果完全可以先用它跑一个基线再看深度模型有没有必要上。5. 避坑指南数据划分、序列切分与收敛问题的五个常见坑5.1 训练集准确率超过 99%测试集却只有 70%刀具间的分布漂移现象cnn.py 里训练集准确率刷到 99%切到 cnn_test.py 跑测试集准确率掉到 70% 出头。原因训练集是铣刀 1测试集是铣刀 4 和 6。不同刀具即使工况相同采集到的信号幅值、噪声分布也有差异这种分布漂移在小样本上会被模型放大网络记住了铣刀 1 的“个性”而不是磨损状态的“共性”。解决先分别统计训练集和测试集每个特征的均值和方差如果差异过大优先做一次按刀具对齐的归一化。更稳妥的办法是用训练集的统计量去归一化测试集而不是各算各的。如果差异主要来自采样条件可以把信号先做带通滤波或去趋势项再做特征提取降低低频漂移的影响。5.2 时间序列被随机切分导致的数据泄漏现象用 sklearn 的 train_test_split 随机打散样本后测试集准确率出奇地高高到不像一个真实任务。原因滑窗生成的相邻窗口高度重叠随机切分把同一段信号的连续窗口同时送进训练集和测试集相当于模型见过“考卷”的一部分内容这属于典型的数据泄漏。解决严格按刀具 ID 或样本文件分组划分训练集只包含铣刀 1测试集只包含铣刀 4 和 6不要先滑窗再随机切分。如果同一把刀有多个文件也要确保同一个文件的所有窗口进同一侧。我在 merge 阶段保留的 source 列在这时候就能派上用场用 groupby 检查有没有跨集合的文件名出现。5.3 Conv1d 报错 “Expected 3D input, got 2D”现象跑 cnn.py 时 torch 直接报维度错类似 Expected 3D input, got 2D。原因Conv1d 的输入必须是 (batch, channels, length)而 CSV 读出来的原始数据只有 (batch, length) 两维通道维度缺失。解决在 forward 入口对齐输入维度常见做法是 x x.unsqueeze(1)把单通道信号补成 (batch, 1, length)。如果输入是 (batch, length, channels) 这种布局需要用 x.permute(0, 2, 1) 先把通道维换到第二个位置。另外注意全连接层接在卷积层后面时要先经过 AdaptiveAvgPool1d 或 Flatten别把 (batch, 256, 1) 直接塞给 Linear。5.4 LSTM 训练不收敛loss 在 0.9 附近来回震荡现象lstm.py 训练了 50 个 epochloss 卡在 0.9 左右不下降或者上下波动明显。原因循环网络的训练对学习率和梯度尺度很敏感常见原因是学习率设成 1e-3 到 1e-2 太大叠加没有做梯度裁剪梯度爆炸让参数一把拉飞另一种可能是没有把输入转成 float32。解决先把学习率调到 1e-3 以下试试再给优化器加梯度裁剪PyTorch 里是 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果还没有下降趋势检查输入数据是不是同一个数量级不同通道的量纲差异过大时先做 z-score 归一化再进 LSTM。还有一点LSTM 的初始隐藏状态用 zeros 即可不需要自定义默认调用时会自动生成全零隐藏状态通常够用了。5.5 随机种子不固定结果每次跑都不一样现象同一份代码、同一个数据集连续跑三次测试集准确率相差 3% 到 5%个别情况差更多。原因PyTorch、numpy、random 各有自己的随机数生成器不加固定种子时网络初始化、数据加载顺序、dropout 行为都不同而这种小数据集对初始化和样本顺序都敏感。解决训练脚本开头统一固定三个种子常见的写法是import random, numpy as np, torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True最后一行的 cudnn.deterministic 会让 GPU 上的卷积算法变成确定性算法代价是稍微慢一点但对结果可复现非常有效。如果你还要对比 LSTM、GRU、CNN 三个模型务必在三个脚本里都用同一个种子否则模型间的差异会被随机性掩盖比较就没有意义了。6. 验证与进阶技巧混淆矩阵、滑窗参数与多模型投票6.1 别只看准确率先看混淆矩阵滚刀状态三分类里准确率只告诉你整体正确率不告诉你模型在哪种状态上翻车。如果初期磨损样本几乎全被误判成正常磨损准确率可能还有 82%但对实际生产的危害很大——初期磨损漏掉就意味着继续切削后面大概率打刀。我在 cnn_test.py 或 lstm_test.py 里都会补一段评估代码from sklearn.metrics import confusion_matrix, classification_report y_true test_labels y_pred model.predict(test_windows) print(confusion_matrix(y_true, y_pred)) print(classification_report( y_true, y_pred, target_names[初期磨损, 正常磨损, 急剧磨损] ))混淆矩阵的第 i 行第 j 列表示真实类别 i 被预测成类别 j 的样本数。正常磨损样本多它的召回率通常会高而初期磨损只有 30 个测试样本不小心全被吞进正常磨损类里准确率也看不出来只能靠 classification_report 里的 recall 和 f1-score 暴露问题。我一般以“急剧磨损的召回率不低于 0.9”作为验收线这条不满足就回去调类别权重或换特征。6.2 滑窗长度与重叠率先用小参数试出稳定基线窗口长度直接决定一条样本覆盖多长的信号重叠率决定样本数量和相邻样本的相关性。我在这个项目上试过的参数范围给你一个参考窗口长度重叠率单把刀样本数效果特点1000%1500样本多噪声大基线波动25050%750折中适合快速对比模型50075%800单样本信号充分训练时间长重叠率高会让相邻样本高度相关训练集内部的信息冗余变大结果是训练变快、测试集准确率虚高。如果想快速看模型好不好用 250/50% 这档就够了想冲最终精度再试试 500/75%对比三次取平均值避免单次运气。6.3 多模型投票用争执区判断该不该信任自动判别最后说一个我常用的技巧把 CNN、LSTM、GRU 三个模型预测结果拉出来统计它们意见不一致的样本。三个模型都判成同一个类别的样本可信度通常很高如果三个模型各执一词这些样本基本都落在初期磨损和急剧磨损的边界上。把那部分争执样本喂给 SVM 和随机森林做二次投票准确率还能再抬 1 到 2 个百分点。从那以后我每次跑这种小型时序分类项目都会强制走一遍固定流程固定随机种子、按刀具划分数据、先看混淆矩阵、再调滑窗参数最后才轮到模型结构对比和投票融合。这个顺序帮我避开了绝大多数脏数据带来的假结果。希望这份滚刀状态识别源码的拆解能帮到你下载下来先别急着调模型把数据划分和特征脚本跑通你已经赢了一半。本文还有配套的精品资源点击获取
返回列表