
简介该资源面向需要掌握时序数据分类的MATLAB用户与机器学习初学者提供基于长短期记忆神经网络LSTM的多输入单输出分类预测方案可同时支持二分类与多分类任务适用于故障诊断、行为识别、信号判别等场景。压缩包共9个文件约1.88MB包含3个m脚本文件主程序、初始化与评价指标计算、1个xlsx数据集、1个说明文档及4张png结果图脚本内注释详细替换数据即可直接运行。程序可输出分类效果图、迭代优化图和混淆矩阵图便于直观评估模型性能。目前已有207人学习下载适合希望快速搭建LSTM分类基线、理解训练流程与结果可视化的读者参考使用。1. 多输入单输出 LSTM 分类为什么你的第一版模型总在验证集上翻车你手里有若干路传感器信号、几列工况参数、一段历史行为序列想预测的却只有一个标签——正常还是异常、A 类还是 B 类。这种「多输入单输出」的结构恰恰是 LSTM 分类预测最常翻车的场景。很多人第一次用 PyTorch 搭 LSTM训练集准确率冲到 98%验证集却卡在 60% 上下震荡于是开始怀疑 LSTM 是不是被 Transformer 淘汰了。真相往往不是模型不行而是输入张量的形状、标签的编码方式、以及序列切窗的逻辑从一开始就错了。长短期记忆神经网络LSTM解决的是标准 RNN 的梯度消失问题靠输入门、遗忘门、输出门三个门控单元决定「记住什么、丢掉什么、输出什么」。把它用在数据分类预测上本质是让模型从多路输入的时间序列里抽出一条固定长度的特征向量再交给全连接层做分类。多输入意味着特征维度不止一列单输出意味着最后只出一个类别概率分布。这套结构在设备故障诊断、工况识别、行为分类里非常常见也是 LSTM 最容易被低估的落地形态。下面从数据组织讲到训练排错每一步都给可复现的代码和参数说明。2. 多输入单输出到底怎么组织数据从原始表格到 LSTM 能吃的张量2.1 先想清楚 LSTM 要的输入形状是 (batch, seq_len, input_size)LSTM 分类预测的第一个门槛不是写模型而是把数据整理成三维张量。假设你有 8 路传感器采样 200 个时间点样本数 1000那么原始数据是 1000×200×8。PyTorch 的nn.LSTM默认要求输入形状为(seq_len, batch, input_size)如果开了batch_firstTrue则是(batch, seq_len, input_size)。我一般会显式设置batch_firstTrue因为后面接全连接层时维度直觉更顺。多输入的关键在于input_size等于特征列数而不是样本数。很多人把 8 路信号拼成一维长向量塞进去结果input_size变成 1600序列长度变成 1LSTM 直接退化成全连接网络时序建模能力归零。判断方法很简单如果你的seq_len等于 1那 LSTM 就没在干 LSTM 的活。标签方面单输出分类要求标签是LongTensor取值范围[0, num_classes-1]。二分类可以用一个输出节点配BCEWithLogitsLoss也可以两个节点配CrossEntropyLoss。我倾向后者因为扩展到多分类时不用改结构。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class MultiInputDataset(Dataset): def __init__(self, features, labels, seq_len200): # features: (N, T, F) numpy array # labels: (N,) int array self.X torch.tensor(features, dtypetorch.float32) self.y torch.tensor(labels, dtypetorch.long) self.seq_len seq_len def __len__(self): return len(self.y) def __getitem__(self, idx): # 滑动窗口切分每个样本取 seq_len 长的一段 x self.X[idx] # (T, F) if x.shape[0] self.seq_len: start torch.randint(0, x.shape[0] - self.seq_len, (1,)).item() x x[start:start self.seq_len] return x, self.y[idx]这段代码做了两件事把 numpy 转成张量以及用滑动窗口保证每条样本序列长度一致。seq_len是你要调的第一个参数太小抓不到周期特征太大显存吃紧且容易过拟合。设备振动信号一般 128 到 512 之间比较稳工况参数这种慢变量可以短一些。2.2 标准化要按通道做别把 8 路信号混在一起算均值多输入场景下不同传感器量纲差异巨大温度可能是 20 到 90电流可能是 0 到 300振动加速度可能是 -50 到 50。如果你把整个(N, T, F)拉平算一个全局均值和方差量纲大的通道会主导梯度量纲小的通道等于没输入。正确做法是按特征维度F分别标准化即对每个通道单独算均值和标准差。import numpy as np def per_channel_standardize(features): # features: (N, T, F) mean features.mean(axis(0, 1), keepdimsTrue) # (1,1,F) std features.std(axis(0, 1), keepdimsTrue) 1e-8 return (features - mean) / std, mean, std注意axis(0,1)表示在样本维和时间维上统计保留特征维。1e-8是防止某通道恒定为零导致除零。标准化参数必须只用训练集统计然后应用到验证集和测试集否则就是数据泄漏验证集指标会虚高上线后打回原形。这个坑我在设备寿命预测项目里踩过一次离线 F1 0.93现场只有 0.71排查两天才发现是标准化时把测试集也算进去了。2.3 类别不平衡时先动采样策略再动损失函数分类预测里正负样本 9:1 甚至 99:1 很常见。直接训练会让模型学会「全预测多数类」也能拿高准确率。处理顺序我建议先看能不能重采样再考虑加权损失。过采样少数类容易过拟合欠采样多数类会丢信息折中方案是用WeightedRandomSampler做样本权重采样。from torch.utils.data import WeightedRandomSampler def make_sampler(labels): class_count np.bincount(labels) class_weight 1.0 / class_count samples_weight class_weight[labels] samples_weight torch.tensor(samples_weight, dtypetorch.float64) return WeightedRandomSampler(samples_weight, len(samples_weight))class_count统计每类样本数class_weight取倒数样本权重就是它所属类别的权重。这样少数类会被更频繁地抽到。如果采样后仍不理想再在损失函数里加weight参数两者不要同时拉满否则模型会对少数类过度敏感误报率飙升。3. 把 LSTM 分类网络搭起来层数、隐藏单元、Dropout 怎么定3.1 一个够用的 LSTM 分类器长什么样多输入单输出的 LSTM 分类网络结构可以很简洁LSTM 层负责抽时序特征取最后一个时间步的输出接一层全连接映射到类别数。不需要堆很深两层 LSTM 加一层 FC 在多数工业数据集上已经够用。class LSTMClassifier(nn.Module): def __init__(self, input_size, hidden_size128, num_layers2, num_classes2, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0, bidirectionalFalse ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) last out[:, -1, :] # 取最后时间步 last self.dropout(last) logits self.fc(last) return logitsinput_size等于你的特征通道数hidden_size是隐藏单元数num_layers是 LSTM 堆叠层数。dropout只在层数大于 1 时生效这是 PyTorch 的行为单层 LSTM 传 dropout 不会报错但也不起作用。out[:, -1, :]取最后一个时间步也可以对所有时间步做平均池化后者对长序列更稳但会弱化末端突变信息。3.2 隐藏单元和层数的取值边界hidden_size不是越大越好。我一般从 64 起步按 64、128、256 试三档。超过 256 后收益递减明显显存和训练时间却线性上涨。num_layers从 1 到 3 试超过 3 层在中小数据集上几乎必然过拟合而且梯度回传变慢。判断依据看验证集损失曲线如果训练损失持续下降而验证损失在几个 epoch 后抬头就是过拟合先减层数或加 dropout而不是加数据。序列长度seq_len和hidden_size存在隐性权衡。序列越长LSTM 需要越大的隐藏状态来记住早期信息。经验比例是hidden_size不小于seq_len的十分之一。比如seq_len500hidden_size别低于 64否则早期时间步的信息在门控里被稀释掉。3.3 双向 LSTM 什么时候值得开bidirectionalTrue会让模型同时看正向和反向序列对离线分类任务通常能涨 1 到 3 个点。但代价是参数量翻倍且不能用于实时流式预测因为反向序列需要未来数据。如果你的场景是「攒够一段数据再判断」双向可以开如果是「每来一个采样点就要出结果」必须用单向。这个边界在设备在线监测里特别重要很多论文刷双向指标落地时根本没法用。# 双向版本取正向和反向最后时间步拼接 self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x): out, _ self.lstm(x) last torch.cat([out[:, -1, :hidden_size], out[:, 0, hidden_size:]], dim1) return self.fc(self.dropout(last))双向时out[:, -1, :hidden_size]是正向最后一步out[:, 0, hidden_size:]是反向最后一步对应原始序列开头。拼接后维度是hidden_size*2全连接层输入要同步改。4. 训练循环与参数设置学习率、batch size、早停的实际取值4.1 训练循环要记录哪些量训练循环不只是跑loss.backward()要同时记录训练损失、验证损失、验证准确率和 F1。分类不平衡时准确率会骗人F1 更可信。下面是一个最小可用的训练框架。from sklearn.metrics import f1_score def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() * x.size(0) return total_loss / len(loader.dataset) torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() preds, trues, total_loss [], [], 0 for x, y in loader: x, y x.to(device), y.to(device) logits model(x) total_loss criterion(logits, y).item() * x.size(0) preds.extend(logits.argmax(dim1).cpu().numpy()) trues.extend(y.cpu().numpy()) f1 f1_score(trues, preds, averagemacro) return total_loss / len(loader.dataset), f1clip_grad_norm_的max_norm1.0是 LSTM 训练必备梯度爆炸在长序列上非常常见不加这行损失会突然变成 nan。averagemacro让每个类别权重相同适合不平衡数据。4.2 学习率和 batch size 的搭配LSTM 对学习率比 CNN 敏感。Adam 优化器下学习率从 1e-3 起步如果验证损失震荡就降到 3e-4 或 1e-4。batch size 从 32 或 64 起步太小梯度噪声大太大泛化变差。显存允许的话64 是比较稳的默认值。学习率调度用ReduceLROnPlateau比固定衰减更省心它监控验证损失连续几个 epoch 不降就砍半。optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, min_lr1e-6 )weight_decay1e-5是轻量 L2 正则配合 dropout 一起用。patience5表示验证损失 5 个 epoch 不改善就降学习率min_lr防止降到零。4.3 早停和模型保存早停是防止过拟合最直接的手段。监控验证集 F1连续 10 个 epoch 不创新高就停同时保存 F1 最高的那版权重。best_f1, patience_counter 0.0, 0 for epoch in range(100): train_loss train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_f1 evaluate(model, val_loader, criterion, device) scheduler.step(val_loss) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_lstm.pt) patience_counter 0 else: patience_counter 1 if patience_counter 10: break保存state_dict而不是整个模型加载时先实例化结构再load_state_dict这样代码重构后不会因为 pickle 路径问题加载失败。best_f1初始为 0如果第一轮就保存说明模型至少比随机猜好。5. 避坑与排查LSTM 分类预测最常见的 5 个翻车现场5.1 验证集准确率远高于测试集现象验证集 F1 0.92测试集只有 0.65。原因通常是标准化或采样时把验证/测试数据混进了训练统计或者滑动窗口切分时同一段序列被分到训练和验证两边造成泄漏。解决按样本 ID 或时间段切分数据集先切分再标准化标准化参数只从训练集算。时间序列尤其不能随机打乱切分要用时间前段做训练、后段做测试。5.2 损失变成 nan 或突然爆炸现象训练几个 epoch 后 loss 变 nan。原因长序列梯度爆炸或者学习率太大。解决加clip_grad_norm_(max_norm1.0)学习率降到 1e-4检查输入里有没有 inf 或超大值。标准化后仍有个别通道方差极小除出来数值巨大加1e-8不够时可以改用RobustScaler或对该通道单独处理。5.3 模型只预测多数类现象准确率等于多数类占比F1 极低。原因类别不平衡且没做任何处理。解决先用WeightedRandomSampler再在CrossEntropyLoss里加weightclass_weights。如果还不行检查标签编码是否从 0 开始连续CrossEntropyLoss要求标签在[0, C-1]有跳号会静默出错。5.4 训练慢到无法接受现象一个 epoch 要跑十几分钟。原因seq_len太长、hidden_size太大、或者 DataLoader 的num_workers没设。解决num_workers4或 8pin_memoryTrueseq_len先用 128 跑通再往上加。另外确认没有在 forward 里做 CPU 到 GPU 的反复拷贝数据要在进入模型前就.to(device)。5.5 推理时结果和训练时对不上现象训练完 eval 模式指标正常部署后单条推理结果乱跳。原因推理时忘了model.eval()dropout 还在随机丢弃或者单条输入没做 batch 维度扩展形状变成(seq_len, input_size)被当成(batch, seq_len)解析。解决推理前固定model.eval()和torch.no_grad()输入用x.unsqueeze(0)补 batch 维标准化用训练时保存的 mean/std不要重新算。6. 让多输入 LSTM 分类真正可用的两个进阶技巧第一个技巧是给不同输入通道加注意力权重。多输入场景里8 路信号对分类的贡献往往不均等有的通道全是噪声。与其手工筛特征不如在 LSTM 输出后加一层轻量注意力让模型自己学通道重要性。做法是把 LSTM 每个时间步的输出过一个线性层得到分数softmax 后加权求和替代简单的取最后时间步。class AttentionPooling(nn.Module): def __init__(self, hidden_size): super().__init__() self.score nn.Linear(hidden_size, 1) def forward(self, lstm_out): # lstm_out: (batch, seq_len, hidden_size) weights torch.softmax(self.score(lstm_out), dim1) # (batch, seq_len, 1) return (lstm_out * weights).sum(dim1) # (batch, hidden_size)把分类器里的out[:, -1, :]换成AttentionPooling(hidden_size)(out)参数量只多一个线性层但在通道贡献差异大的数据集上通常能涨 2 到 4 个点。注意dim1是在时间维做 softmax不是 batch 维写错会导致整个 batch 共享权重效果反而变差。第二个技巧是用验证集上的混淆矩阵反推阈值。二分类默认取 0.5 做阈值但工业场景里漏报和误报代价不同。如果漏报代价高就把阈值往低调牺牲精确率换召回率。具体做法是在验证集上跑一遍概率输出画精确率-召回率曲线选一个满足召回率下限的阈值固化到推理代码里。这个阈值不要用测试集调否则又是泄漏。我自己的习惯是每个 LSTM 分类项目都先跑一个「最小可复现基线」单向单层、hidden_size64、seq_len128、不做任何采样和注意力先看 F1 落在哪。基线跑通再逐项加双向、加层、加采样、加注意力每加一项记录验证集变化。这样即使最后效果不理想也能清楚知道是哪一项拖了后腿而不是对着一堆超参数凭感觉调。希望帮到你。本文还有配套的精品资源点击获取