☰
滚刀状态识别机器学习实战:CNN/LSTM/GRU对比与特征工程全解析
2026/10/3 3:06:21 网站建设 项目流程

简介:一套基于机器学习的滚刀(刀具)状态识别项目源码,面向智能制造与设备维护方向的学习者,覆盖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 字样,我一般用 header=None 读,然后打印前两行确认列数是否一致。如果不一致,说明不同刀的信号通道数不同,后面的网络输入维度就得单独处理,否则换一把刀就会报维度错。

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_size=400, step=200): df = pd.read_csv(csv_path, header=None) raw = df.values.astype(np.float32) # 每列独立做 z-score 归一化,避免不同通道量纲差异影响训练 mean = raw.mean(axis=0) std = raw.std(axis=0) + 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_size=400 表示每个样本覆盖 400 个连续采样点,step=200 表示相邻窗口起点间隔 200,也就是有 50% 的重叠。重叠窗口能缓解滑动截断带来的边界突变,代价是样本数量变大、训练时间变长。如果你的 CSV 第一行是列名,把 header=None 改成 header=0 就好。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, header=None) df["label"] = label df["source"] = path frames.append(df) merged = pd.concat(frames, ignore_index=True) merged.to_csv(output_path, index=False)

这个函数对应 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_classes=3): super().__init__() self.features = nn.Sequential( nn.Conv1d(1, 64, kernel_size=7, padding=3), nn.ReLU(), nn.MaxPool1d(kernel_size=3, stride=2), nn.Conv1d(64, 128, kernel_size=5, padding=2), nn.ReLU(), nn.MaxPool1d(kernel_size=3, stride=2), nn.Conv1d(128, 256, kernel_size=3, padding=1), 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_size=7,相当于卷积核一次看连续 7 个采样点,padding=3 保证卷积后长度不变;stride 保持默认 1,用后面的 MaxPool1d 做下采样。通道数从 1 涨到 64、128、256,是在逐步把局部模式抽象成更高层的语义。AdaptiveAvgPool1d(1) 负责把每个通道压成一个值,不管输入序列多长,最后都能得到固定维度的特征向量,再接 Linear 输出 3 类。

这里有几个参数值得记进实验笔记。卷积核大小影响感受野,7 适合采样率不高、单通道信号;如果你手里的信号是 10kHz 以上的高采样率,可以考虑把第一层 kernel_size 加到 15 或 31,让第一层看到更长的一段波形。池化层步长 stride=2 会让序列长度每层减半,如果输入序列太短,比如低于 64 个点,三层池化后可能只剩下几个点,这时候要减少池化层数量或不用池化,改用 stride=2 的卷积做下采样。

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 --epochs=100 --batch_size=32 --lr=1e-3 python cnn_test.py --weights=cnn_best.pt --test_file=data_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_first=True,输入就是 (batch, seq_len, input_size)。我第一次用 LSTM 时忘了这个顺序转换,直接用原始数据喂进去,结果训练 loss 完全不下降,白白浪费了十几分钟,这个问题我放在避坑章里细说。初始化网络时的写法大致像这样:

import torch.nn as nn class HobLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, n_classes=3): super().__init__() self.lstm = nn.LSTM( input_size, hidden_size, num_layers, batch_first=True, dropout=0.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(kernel="rbf", C=1.0, gamma="scale", class_weight="balanced") rf = RandomForestClassifier(n_estimators=200, max_depth=8, class_weight="balanced", random_state=42) for name, model in [("svm", svm), ("rf", rf)]: acc = cross_val_score(model, X, y, cv=5).mean() print(name, f"{acc:.4f}")

class_weight="balanced" 会自动按类别频率给样本加权,正好解决训练集里 30 : 210 : 75 的不平衡。SVC 的 C 控制对误分类的惩罚,C 越大越容易过拟合;gamma="scale" 表示按特征数量自动缩放,适合特征量纲不完全一致的场景。随机森林的 max_depth=8 是限制单棵树的深度,配合 n_estimators=200 能在小样本上稳住方差。如果你之前跑过深度网络,这里值得注意:传统模型不需要 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 个 epoch,loss 卡在 0.9 左右不下降,或者上下波动明显。

原因:循环网络的训练对学习率和梯度尺度很敏感,常见原因是学习率设成 1e-3 到 1e-2 太大,叠加没有做梯度裁剪,梯度爆炸让参数一把拉飞;另一种可能是没有把输入转成 float32。

解决:先把学习率调到 1e-3 以下试试,再给优化器加梯度裁剪,PyTorch 里是 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)。如果还没有下降趋势,检查输入数据是不是同一个数量级,不同通道的量纲差异过大时,先做 z-score 归一化再进 LSTM。还有一点,LSTM 的初始隐藏状态用 zeros 即可,不需要自定义,默认调用时会自动生成全零隐藏状态,通常够用了。

5.5 随机种子不固定,结果每次跑都不一样

现象:同一份代码、同一个数据集,连续跑三次,测试集准确率相差 3% 到 5%,个别情况差更多。

原因:PyTorch、numpy、random 各有自己的随机数生成器,不加固定种子时,网络初始化、数据加载顺序、dropout 行为都不同,而这种小数据集对初始化和样本顺序都敏感。

解决:训练脚本开头统一固定三个种子,常见的写法是:

import random, numpy as np, torch def set_seed(seed=42): 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 个百分点。

从那以后我每次跑这种小型时序分类项目,都会强制走一遍固定流程:固定随机种子、按刀具划分数据、先看混淆矩阵、再调滑窗参数,最后才轮到模型结构对比和投票融合。这个顺序帮我避开了绝大多数脏数据带来的假结果。希望这份滚刀状态识别源码的拆解能帮到你,下载下来先别急着调模型,把数据划分和特征脚本跑通,你已经赢了一半。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询