简介:该资源面向需要掌握时序数据分类的MATLAB用户与机器学习初学者,提供基于长短期记忆神经网络(LSTM)的多输入单输出分类预测方案,可同时支持二分类与多分类任务,适用于故障诊断、行为识别、信号判别等场景。压缩包共9个文件,约1.88MB,包含3个m脚本文件(主程序、初始化与评估函数)、1个xlsx数据集、1个说明文档及4张png结果图,脚本内注释详细,替换数据即可直接运行。程序可输出分类效果图、迭代优化图和混淆矩阵图,便于直观评估模型性能并定位误分类样本。目前已有207人学习下载,适合希望快速搭建LSTM分类基线、理解网络训练流程与结果可视化的读者参考使用。
1. 多输入单输出的 LSTM 分类:为什么你的模型总在验证集上翻车
你手里有七八个传感器通道,采样频率各不相同,标签只有一列——正常或故障。你想用 LSTM 做分类预测,把多路时序信号塞进去,输出一个类别。听起来简单,但真正动手时,十个人里有八个会卡在同一个地方:验证集 loss 震荡不收敛,或者训练集准确率 99% 而验证集只有 60%。
这不是 LSTM 本身的问题,而是多输入单输出这个结构对数据对齐、通道融合和标签窗口的要求比单变量预测苛刻得多。LSTM 分类预测和 LSTM 回归预测是两条路:回归输出连续值,分类输出离散类别,损失函数、输出层激活、标签编码方式全都不一样。很多人拿回归的代码改一改就上分类任务,不翻车才怪。
这篇内容面向的是已经了解 LSTM 基本结构、但还没跑通多通道分类任务的工程师。我会从数据组织讲到 PyTorch 实现,再到调参和排查,每一步都给出可复现的代码和参数说明。读完你应该能自己搭一个能跑通的多输入单输出 LSTM 分类管道,并且知道哪里容易出问题。
2. 多输入单输出 LSTM 的数据组织与模型结构
2.1 多通道时序数据怎么切成 LSTM 能吃的张量
LSTM 的输入要求是三维张量,形状为(batch_size, sequence_length, input_size)。单变量时序很好办,input_size=1。但多输入场景下,input_size等于你的特征通道数。比如你有加速度、温度、电流、电压四个通道,每个时间步取一个值,那input_size=4。
关键在于滑动窗口的切法。假设原始数据是(total_timesteps, num_channels)的二维数组,你需要用窗口长度seq_len和步长stride把它切成多个样本。每个样本的标签取窗口末端或窗口覆盖区间内的多数类。
import numpy as np import torch from torch.utils.data import Dataset, DataLoader def sliding_window(data, labels, seq_len, stride=1): """ data: (T, C) 多通道时序 labels: (T,) 每个时间步的类别标签 seq_len: 窗口长度 stride: 滑动步长 返回: (N, seq_len, C) 和 (N,) """ X, y = [], [] for start in range(0, len(data) - seq_len + 1, stride): end = start + seq_len window = data[start:end, :] # (seq_len, C) # 标签取窗口内出现次数最多的类别 window_labels = labels[start:end] label = np.bincount(window_labels).argmax() X.append(window) y.append(label) return np.array(X, dtype=np.float32), np.array(y, dtype=np.int64) class MultiChannelDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X) # (N, seq_len, C) self.y = torch.tensor(y) # (N,) def __len__(self): return len(self.y) def __getitem__(self, idx): return self.X[idx], self.y[idx]这段代码里有两个容易忽略的点。第一,标签的取法。如果窗口内标签不一致,取众数是最常见的做法,但窗口边界处会出现标签模糊的样本,建议在切窗之前先做标签平滑或剔除过渡段。第二,stride的选择。训练集可以用stride=1做数据增强,但验证集和测试集必须用不重叠的窗口,否则相邻窗口高度相似,验证指标会虚高。
seq_len怎么定?一个经验规则是覆盖至少两个完整周期。如果你的信号主频是 10Hz,采样率 100Hz,那一个周期 10 个点,seq_len至少取 20。太小了 LSTM 看不到完整模式,太大了梯度传播路径过长,训练慢且容易过拟合。
2.2 LSTM 分类模型的 PyTorch 实现与输出层设计
多输入单输出分类模型的结构很直接:LSTM 层负责提取时序特征,最后一个时间步的隐状态接全连接层映射到类别数。
import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, # 通道数 hidden_size=hidden_size, # 隐层维度,常用 64/128/256 num_layers=num_layers, # 堆叠层数,通常 1~3 batch_first=True, # 输入格式 (batch, seq, feature) dropout=dropout if num_layers > 1 else 0.0, bidirectional=False # 分类任务常用单向 ) self.classifier = nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size // 2, num_classes) ) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出 last_step = lstm_out[:, -1, :] # (batch, hidden_size) logits = self.classifier(last_step) # (batch, num_classes) return logits几个参数需要说清楚。hidden_size不是越大越好,64 到 256 之间足够覆盖大多数工业场景,再大容易过拟合。num_layers超过 3 层后收益急剧下降,而且dropout只在层间生效,单层 LSTM 设了也没用。bidirectional在离线分类任务里可以用双向,但如果是实时预测,必须用单向,否则你会用到未来信息。
输出层不加 Softmax。PyTorch 的CrossEntropyLoss内部已经做了 log-softmax,你在模型里再加一层 Softmax 会导致数值不稳定。这是血泪经验,很多人第一次写分类模型都会多此一举。
损失函数和优化器的搭配:
model = LSTMClassifier(input_size=4, hidden_size=128, num_layers=2, num_classes=3) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5)weight_decay设 1e-4 到 1e-3 之间,对 LSTM 的权重矩阵做正则化。ReduceLROnPlateau在验证 loss 不降时自动降学习率,比手动调省事得多。
3. 训练流程、类别不平衡处理与评估指标
3.1 训练循环与早停策略
训练循环本身不复杂,但分类任务有几个细节和回归不同。每个 epoch 结束后要在验证集上算准确率和混淆矩阵,不能只看 loss。
def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0, 0, 0 for X_batch, y_batch in loader: X_batch, y_batch = X_batch.to(device), y_batch.to(device) optimizer.zero_grad() logits = model(X_batch) loss = criterion(logits, y_batch) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() * len(y_batch) preds = logits.argmax(dim=1) correct += (preds == y_batch).sum().item() total += len(y_batch) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 all_preds, all_labels = [], [] for X_batch, y_batch in loader: X_batch, y_batch = X_batch.to(device), y_batch.to(device) logits = model(X_batch) loss = criterion(logits, y_batch) total_loss += loss.item() * len(y_batch) preds = logits.argmax(dim=1) correct += (preds == y_batch).sum().item() total += len(y_batch) all_preds.extend(preds.cpu().numpy()) all_labels.extend(y_batch.cpu().numpy()) return total_loss / total, correct / total, all_preds, all_labelsclip_grad_norm_的max_norm=1.0是 LSTM 训练的标配。LSTM 的反向传播路径长,梯度容易爆炸,不裁剪的话 loss 会突然变成 NaN。这个值可以调,但一般不超过 5.0。
早停策略用验证 loss 而不是验证准确率。准确率在类别不平衡时会失真,loss 更能反映模型是否还在学到东西。
best_val_loss = float('inf') patience_counter = 0 patience = 10 for epoch in range(100): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc, _, _ = evaluate(model, val_loader, criterion, device) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss patience_counter = 0 torch.save(model.state_dict(), 'best_model.pt') else: patience_counter += 1 if patience_counter >= patience: print(f'Early stop at epoch {epoch}') break3.2 类别不平衡的三种处理方式与选择依据
工业场景里正常样本远多于故障样本,类别比例可能到 100:1。不处理的话模型会把所有样本预测为多数类,准确率看起来很高但完全没用。
第一种方式是在损失函数里加权重。CrossEntropyLoss的weight参数接受一个与类别数等长的张量,权重设为类别频率的倒数。
from collections import Counter label_counts = Counter(y_train) total = sum(label_counts.values()) weights = torch.tensor([total / (len(label_counts) * label_counts[i]) for i in range(num_classes)], dtype=torch.float32) criterion = nn.CrossEntropyLoss(weight=weights.to(device))第二种方式是过采样少数类。用WeightedRandomSampler让少数类样本在每个 batch 里出现得更频繁。
from torch.utils.data import WeightedRandomSampler sample_weights = [1.0 / label_counts[y] for y in y_train] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) train_loader = DataLoader(dataset, batch_size=64, sampler=sampler)第三种方式是数据增强。对少数类做时间轴抖动、幅度缩放、加噪声。这种方式在信号分类里效果通常最好,但要注意增强后的样本不能改变类别语义。
我一般会先试损失加权,如果效果不够再加过采样,最后才考虑增强。三种方式不要同时上,否则超参数空间太大,调不动。
3.3 分类评估:准确率之外必须看的指标
多分类任务只看准确率是不够的。你需要看每个类别的 precision、recall、F1,以及混淆矩阵。
from sklearn.metrics import classification_report, confusion_matrix _, _, preds, labels = evaluate(model, test_loader, criterion, device) print(classification_report(labels, preds, digits=4)) print(confusion_matrix(labels, preds))classification_report会给出每个类别的 precision、recall、F1-score 和支持样本数。如果某个类别的 recall 特别低,说明模型漏报严重,需要针对性处理。混淆矩阵能告诉你哪些类别之间容易混,比如“轻微故障”和“正常”经常被混淆,这时候要考虑这两个类别的特征区分度是否足够。
对于二分类任务,AUC-ROC 也是常用指标。多分类可以算 macro-average AUC 或每个类别的 one-vs-rest AUC。
4. 多输入 LSTM 分类的避坑与排查清单
4.1 坑一:验证集 loss 震荡不收敛
现象:训练 loss 正常下降,验证 loss 上下跳动,准确率忽高忽低。
原因:最常见的是验证集窗口重叠。如果验证集也用了stride=1切窗,相邻样本高度相似,模型在验证集上的表现会随 batch 组成剧烈波动。另一个原因是 batch size 太小,BatchNorm 或 Dropout 的随机性被放大。
解决:验证集和测试集用不重叠窗口,stride=seq_len。batch size 至少 32,最好 64 或 128。如果还震荡,检查学习率是否太大,试试降到 1e-4。
4.2 坑二:训练准确率高但验证准确率低
现象:训练集准确率 98% 以上,验证集只有 60% 到 70%。
原因:过拟合。LSTM 参数量大,如果训练样本少或者hidden_size太大,模型会记住训练集。另一个可能的原因是训练集和验证集的分布不一致,比如不同工况的数据混在一起没有做分层采样。
解决:先加 Dropout 和 weight_decay。如果不够,减小hidden_size或num_layers。再不够就加数据。分层采样用sklearn.model_selection.train_test_split的stratify参数,保证训练集和验证集的类别比例一致。
4.3 坑三:模型把所有样本预测为多数类
现象:验证集准确率等于多数类占比,混淆矩阵里所有预测都集中在一列。
原因:类别不平衡没有处理,或者损失函数权重设置不对。
解决:按 3.2 节的方式加类别权重或过采样。检查权重计算是否正确,权重张量的顺序要和类别标签的编码顺序一致。如果用了WeightedRandomSampler,确认replacement=True,否则少数类样本可能被采样不到。
4.4 坑四:输入数据归一化方式不对
现象:模型完全不收敛,loss 从一开始就是 NaN 或者一直不降。
原因:多通道数据的量纲差异大。比如温度是 20 到 80,电流是 0.1 到 10,不归一化的话 LSTM 的输入门会被大量纲通道主导。另一个常见错误是对整个数据集做了归一化再切分,导致验证集信息泄露到训练集。
解决:先切分训练集和验证集,再用训练集的均值和方差对两个集做标准化。每个通道单独归一化。
# 正确做法:先切分,再算统计量 train_mean = X_train.mean(axis=(0, 1)) # 对时间和样本维度求均值 train_std = X_train.std(axis=(0, 1)) X_train = (X_train - train_mean) / (train_std + 1e-8) X_val = (X_val - train_mean) / (train_std + 1e-8)4.5 坑五:序列长度选择不当导致信息丢失或训练过慢
现象:seq_len太小,模型学不到完整模式;seq_len太大,训练一个 epoch 要几个小时。
原因:没有根据信号周期和采样率计算合理的窗口长度。
解决:先对信号做 FFT 或自相关分析,找到主周期对应的采样点数。seq_len取主周期的 2 到 3 倍。如果数据量不够,可以用stride小于seq_len来增加样本数,但验证集不要这么做。
5. 从单模型到可落地的进阶技巧
5.1 用双向 LSTM 还是单向 LSTM:一个决策表
| 场景 | 推荐结构 | 理由 |
|---|---|---|
| 离线批量分类 | 双向 LSTM | 可以利用完整序列的前后文信息,精度更高 |
| 实时在线预测 | 单向 LSTM | 不能使用未来信息,双向会导致数据泄露 |
| 边缘设备部署 | 单向 + 小 hidden_size | 计算量小,延迟低 |
| 长序列(>1000 步) | 单向 + 梯度裁剪 | 双向的梯度传播路径翻倍,训练不稳定 |
我一般会在离线任务里先跑一个双向 LSTM 看精度上限,然后用单向 LSTM 去逼近这个上限。如果单向和双向差距在 2% 以内,说明时序依赖主要是单向的,部署时用单向就够了。
5.2 多通道特征融合的两种策略
多输入 LSTM 的通道融合有两种做法。第一种是早期融合,把所有通道在输入层就拼在一起,LSTM 直接处理多通道序列。第二种是晚期融合,每个通道单独过一个 LSTM,然后把隐状态拼接或求平均后再分类。
早期融合实现简单,适合通道之间相关性强的场景。晚期融合参数量大,但每个通道可以有自己的 LSTM 结构,适合通道采样率不同或物理含义差异大的场景。
class LateFusionLSTM(nn.Module): def __init__(self, channel_sizes, hidden_size, num_classes): super().__init__() self.lstms = nn.ModuleList([ nn.LSTM(size, hidden_size, batch_first=True) for size in channel_sizes ]) self.classifier = nn.Sequential( nn.Linear(hidden_size * len(channel_sizes), 128), nn.ReLU(), nn.Linear(128, num_classes) ) def forward(self, x_list): # x_list: 每个通道一个 (batch, seq_len, 1) 张量 hiddens = [] for lstm, x in zip(self.lstms, x_list): out, _ = lstm(x) hiddens.append(out[:, -1, :]) combined = torch.cat(hiddens, dim=1) return self.classifier(combined)晚期融合的坑在于每个通道的序列长度必须一致,如果采样率不同,需要先重采样到统一时间轴。
5.3 一个验证模型是否真正学到东西的小技巧
训练完成后,不要只看指标。把验证集中预测错误的样本拿出来,单独看它们的输入序列。如果错误样本的波形看起来和正确样本没有明显区别,说明模型可能学到了数据里的伪相关,比如某个通道的直流偏置而不是动态模式。
另一个技巧是打乱通道顺序重新训练。如果打乱后精度大幅下降,说明模型依赖通道间的特定关系;如果精度不变,说明模型可能只用了其中一两个通道。这时候可以用通道消融实验逐个去掉通道,看精度变化,找出真正重要的通道。
我自己的习惯是每次训练完都保存一份错误样本的原始波形图,过一遍眼睛。很多问题不是调参能解决的,而是数据本身有标注错误或者采集异常。这个习惯帮我省了很多次盲目调参的时间。
希望帮到你。
本文还有配套的精品资源,点击获取