
简介一套基于长短期记忆网络的MATLAB数据分类预测程序面向需要处理多特征输入、单输出的二分类或多分类任务的研究者与工程师适用于故障诊断、状态识别、模式分类等场景。程序内注释详细替换数据集即可直接运行支持输出分类效果图、迭代优化图和混淆矩阵图便于直观评估模型。压缩包共9个文件约1.88MB包含3个M脚本主程序、初始化函数等、4张结果示例图、1份Excel数据集和1个txt说明文件M脚本负责训练与预测流程Excel提供样本数据图片展示分类结果说明文件交代使用要点结构清晰便于上手。目前已有207人学习下载适合作为LSTM分类建模的入门参考或快速验证工具也可在此基础上调整网络层数、学习率等参数适配不同规模的分类任务。1. 当LSTM遇上分类预测多输入单输出模型到底解决什么问题LSTM过去被贴上“时间序列预测”的标签但工业界最常见的使用场景反而是一张“多输入单输出”的分类表格设备状态来自几十个传感器通道判断这组数据对应正常还是故障交易记录由历史账单的特征序列组成输出是欺诈或正常患者生理信号多通道切片输出是疾病类别。这类任务的特征不是一个独立样本而是一段带前后依赖的序列窗口普通全连接网络会把每个时刻的特征当作独立采样处理丢掉顺序信息。LSTM的细胞状态恰好能记住窗口内的短期依赖用一个接一个的时序特征去逼近最终类别。本文会从数据变形、模型搭建、训练参数到实际部署把这条最短路径完整跑通——如果你手头正好有一个多特征、单分类标签的LSTM数据集照着步骤可以直接动手。2. 多输入单输出的数据怎么喂给LSTM滑窗构造与三维张量重塑LSTM分类和普通MLP分类的第一处不同在输入形状。全连接网络接收的X是二维的[样本数特征数]LSTM要求X是三维的[样本数时间步数每个时间步的特征数]。这个差异决定了你拿到一个多输入单输出的数据表后不能直接丢给模型得先回答一个问题这组数据里面的“时间步”到底是什么。2.1 多输入单输出任务的定义特征矩阵与标签的对齐方式先明确什么叫“多输入单输出”。设备监测数据集里一行是某一时刻10个传感器读数标签是当前时刻是否故障。如果行与行之间没有时间先后关系那就是普通多分类一旦样本按时间顺序采集相邻几行共同刻画同一个工况单靠当前时刻的特征不足以做出分类故障往往从前几个时刻的渐变里发展出来就需要把连续多行打包成一个窗口用窗口里的全部时序特征去推断这一个标签。所以“多输入单输出”在LSTM语境下的落地方式是输入是一个三维块——(窗口数窗口长度特征维度)输出是一个一维标签向量——(窗口数)或者(窗口数类别数)取决于你用的损失函数格式。窗口长度就是seq_len它是LSTM分类里第一个要拍板的超参数。选太大模型会去记忆过长的历史训练慢且容易过拟合选太小时间依赖抓不住。工业设备数据常用20到100个时间步具体数值可以先用领域经验定一个再拿验证集F1反馈调整。这里有个细节经常被忽略滑窗的步长。原始数据2000行×10个特征seq_len取30步长取1可得到大约1970个窗口每个窗口形状(30, 10)。但相邻窗口有29个时间步是重叠的信息冗余极高。步长取5窗口数量降到394计算量直接少五分之四标签之间的相关度也明显降低。窗口数和步长的关系是近似线性反比动手之前先算清楚避免构造出一个大而无当的LSTM数据集。2.2 构造LSTM需要的三维输入滑窗函数与数据集切分拿到二维DataFrame后第一步是特征列与标签列分离。特征矩阵形状(n_samples, n_features)。第二步确认要不要滑窗如果数据本身就是“一条样本等于一条完整序列”比如每个样本是长度固定为50的传感器序列那样本本身已具备时间维度只用reshape成(样本数, 50, 通道数)如果数据是平铺的长表必须先滑窗。我一般把滑窗放在预处理阶段而不是模型内部原因是实际调试方便。预处理阶段用pandas和numpy就能高效切好想改seq_len不用重新训练模型forward里做滑窗意味着每个batch都重新切一次数据效率低还容易把预处理逻辑和模型逻辑耦合到一起。LSTM时间序列预测的Python路径里第一步几乎都是这种滑窗重构。下面这段代码是我在LSTM分类任务里常用的滑窗构造函数输入二维特征矩阵和一维标签序列输出三维输入张量和对应标签。import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler def sliding_window(features: np.ndarray, labels: np.ndarray, seq_len: int 30, step: int 1): 将二维时序特征表转换为 LSTM 需要的三维输入。 参数 features: shape (n_samples, n_features)按时间升序排列 labels: shape (n_samples,)与 features 行对齐 seq_len: 每个样本窗口包含多少个时间步 step: 窗口滑动步长大于1可显著减少窗口重叠 返回 X_seq: shape (n_windows, seq_len, n_features) y_seq: shape (n_windows,) X_seq, y_seq [], [] n len(features) for start in range(0, n - seq_len 1, step): end start seq_len X_seq.append(features[start:end]) y_seq.append(labels[end - 1]) # 用窗口最后一个时刻的标签 return np.array(X_seq), np.array(y_seq) # df 为原始数据feature_cols 为特征列label 为分类标签 feature_cols [c for c in df.columns if c ! label] scaler StandardScaler() features_scaled scaler.fit_transform(df[feature_cols].values) labels df[label].values X_seq, y_seq sliding_window(features_scaled, labels, seq_len30, step5) print(X_seq.shape) # 例如 (394, 30, 10) print(y_seq.shape) # (394,)关于代码逻辑有三个关键点。第一标签取的是窗口最后一个时刻的标签隐含逻辑是“前seq_len个时刻的状态决定此刻类别”如果业务上认为要用窗口中间时刻标签或窗口内多数投票就修改y_seq那一行。第二step取5而非1能显著降低相邻窗口的相似度样本稀缺时改回step1但必须配合后续的时序切分来抑制数据泄漏。第三归一化在滑窗之前做用全量特征数据——这里藏着一个经典陷阱如果先切窗口再分别归一化每个窗口的均值方差不一样送入LSTM的特征尺度不一致模型会非常难收敛。切好窗口后还有一个绕不开的步骤把numpy数组包成PyTorch的Dataset和DataLoader。要点是按时间顺序切分train/valid/test不能随机打乱否则验证集指标会虚高。from torch.utils.data import TensorDataset, DataLoader # 按时间顺序切分前70%训练中间10%验证后20%测试 train_end int(len(X_seq) * 0.7) val_end int(len(X_seq) * 0.8) X_train, X_val, X_test X_seq[:train_end], X_seq[train_end:val_end], X_seq[val_end:] y_train, y_val, y_test y_seq[:train_end], y_seq[train_end:val_end], y_seq[val_end:] train_dataset TensorDataset( torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.long) ) val_dataset TensorDataset( torch.tensor(X_val, dtypetorch.float32), torch.tensor(y_val, dtypetorch.long) ) train_loader DataLoader(train_dataset, batch_size32, shuffleFalse) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse)为什么这里把shuffleFalseLSTM的输入在时间顺序上连续相邻窗口来自重叠的原始时间区间打乱顺序不会带来普通MLP那种“去相关”的好处。真正重要的是训练集和验证集严格按时间切分没有重叠窗口。如果两个集合存在重叠比如step小于seq_len导致同一段原始序列被切到不同集合shuffleTrue会放大泄漏。我的做法是训练阶段仍然shuffle但前提是切分点已经保证集合间无重叠这样每个epoch内窗口顺序打乱能改善收敛又不泄漏信息。注意滑窗里标签对齐方式要和你业务里的“判定时刻”严格一致。故障标签打在窗口起点、中点还是终点决定了LSTM学到的时序因果方向。很多项目在这一点上翻车表现在验证集上就是准确率死活上不去。2.3 类别不均衡先给CrossEntropy加权重别急着重采样多输入单输出的分类场景里正负样本不均衡是常态故障率经常在10%以下。直接用平均CrossEntropy模型梯度被大量多数类样本主导验证集准确率能做到98%但少数类的F1可能不到0.3这不是好模型。解决顺序一般是先给损失函数加class weight再考虑重采样。PyTorch里给CrossEntropyLoss传weight参数样本少的类别给大权重实现最直接不改变数据分布也不破坏时序窗口的完整性。重采样里最常用的是SMOTE但SMOTE本质是k近邻插值插值出来的“窗口”在时间轴上根本不存在——LSTM学到的是时间相关结构人工插值样本的依赖关系是假的效果往往适得其反。随机重复少数类窗口可以保留真实时序结构但增加的是样本量而非信息量对小数据集的帮助有限且会增加训练时间。import torch import torch.nn as nn # y_train 为滑窗切出的训练标签 class_counts np.bincount(y_train) total len(y_train) class_weights total / (len(class_counts) * class_counts) class_weights torch.tensor(class_weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weights)建议先把权重打印出来看看比例多数类权重应该在0.5上下少数类权重在2到5之间比较合理。如果少数类权重超过10说明类别不均衡过于严重光加权重不够还要考虑换评估指标用F1而不是准确率以及针对性补充少数类样本。权重过大时训练会震荡少数类过拟合表现就是训练集F1高、验证集F1骤降。3. LSTM分类模型代码逐行拆解从nn.LSTM到分类头的完整实现第2章已经得到了形状正确的三维输入这一章解决核心问题模型怎么写。很多开源代码把LSTM用在回归预测上最后一层直接输出连续值。分类任务只需要把输出层改成“线性分类头 Softmax”结构上就是“LSTM提取时序特征 全连接降维 分类输出”。但输出到底取哪个隐藏状态、hidden_size设多大、分类头要不要加Dropout这些细节直接决定模型能不能稳定收敛差别不是一点半点。3.1 为什么分类头用全连接加Softmax而不是LSTM直接输出类别LSTM的输出有两种拿法一是返回每个时间步的隐藏状态序列形状(seq_len, batch, hidden_size)二是只返回最后一个时间步的隐藏状态形状(batch, hidden_size)。多输入单输出任务里一个窗口对应一个类别通常取最后时刻的隐藏状态h_t。它经过逐时刻更新已经聚合了窗口内全部有效信息再接线性层映射到类别数即可。为什么不直接让LSTM输出num_classes维因为LSTM的隐藏状态维度和输出维度绑定在hidden_size上如果把hidden_size设成类别数比如2或3每一时间步的隐藏状态只有2到3维特征表达能力被极度压缩LSTM根本学不出复杂边界。隐藏状态至少要有32维再通过一个Linear层压到类别数这是分类头的第一个设计原则。Softmax的问题也容易踩PyTorch的CrossEntropyLoss内部已经做了Softmax归一化所以训练时模型输出raw logits如果你在模型最后又套了一层nn.Softmax相当于连续做两次软化梯度在softmax处被削弱模型收敛变慢。推理阶段想看类别概率再单独做一次softmax训练阶段绝对不要加。3.2 完整PyTorch实现LSTM 全连接分类头的可复制代码下面是一个可以直接复制修改的LSTM分类模型。用nn.LSTM封装batch_firstTrue取最后一层隐藏状态过一个ReLU和Dropout最后接一个Linear分类头。import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, input_size, hidden_size, num_layers, num_classes, 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, ) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, (h_n, c_n) self.lstm(x) # lstm_out: (batch, seq_len, hidden_size) # h_n: (num_layers, batch, hidden_size) last_hidden h_n[-1] # 取最后一层的隐藏状态 out self.dropout(torch.relu(last_hidden)) logits self.fc(out) # (batch, num_classes) return logits这段代码里的参数需要逐个说清楚。input_size就是第2章三维输入的第三维比如10个传感器特征就是10。hidden_size建议从32到128之间选太小表达不够太大显存占用高且容易过拟合。num_layers是堆叠层数实际项目2层足够3层以上训练时间增长、收益递减。batch_firstTrue把输入输出都排成(batch, seq, feature)不设置的话默认是(seq, batch, feature)新手很容易在维度上翻车。提示如果你从Keras转过来习惯了return_sequencesTruePyTorch里nn.LSTM默认返回完整状态序列取h_n[-1]就能拿到最后一个时间步的隐藏状态不用再写lstm_out[:, -1, :]那套索引。nn.LSTM的dropout参数有一个隐蔽行为只有num_layers大于1时生效作用在层与层之间不会作用在最后一层输出上。所以我在模型输出位置又加了一个nn.Dropout(dropout)这是对LSTM最后输出的正则化两处各管一段很多从网上抄来的LSTM模型代码会漏掉后者。如果你想用单特征序列input_size1即可特征维度超过50时建议先加一层Conv1d降维再进LSTM这会大幅减少循环单元里的参数总量同时让LSTM关注更抽象的时间模式。3.3 训练循环与关键参数学习率、batch size、损失函数的选择模型结构定好后训练循环本身很常规但超参数组合很讲究。学习率是LSTM里最敏感的一个超参数建议从1e-3开始loss震荡就降到3e-4或1e-4batch size推荐16到64之间LSTM内存占用随batch和seq_len线性增长显存不够先减batch别减seq_len。优化器选AdamW不要选Adam。AdamW正确解耦了权重衰减LSTM这类循环网络参数量大AdamW的收敛更稳定weight_decay建议5e-4到1e-2之间。损失函数直接用nn.CrossEntropyLoss类别不均衡时把weight参数传进去。下表是我在LSTM分类任务里的默认参数起点多数情况下从这个组合开始调就能收敛。超参数推荐起始值调整方向hidden_size64欠拟合加过拟合减num_layers2追求更高精度再加但优先调hidden_sizedropout0.3训练集和验证集差距大时上调到0.5batch_size32显存不足时降数据量小时别超过64learning_rate1e-3震荡降半平稳不动可以加倍weight_decay1e-4过拟合时往1e-2方向调下面给一个最小可用的训练循环带梯度裁剪def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 0 for X_batch, y_batch in dataloader: X_batch X_batch.float().to(device) y_batch y_batch.long().to(device) optimizer.zero_grad() logits model(X_batch) loss criterion(logits, y_batch) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * X_batch.size(0) preds logits.argmax(dim1) correct (preds y_batch).sum().item() total y_batch.size(0) return total_loss / total, correct / total # 示例调用 model LSTMClassifier(input_size10, hidden_size64, num_layers2, num_classes2, dropout0.3) optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss(weightclass_weights) for epoch in range(50): train_loss, train_acc train_one_epoch( model, train_loader, optimizer, criterion, device) if (epoch 1) % 10 0: print(fepoch {epoch1}: loss{train_loss:.4f}, acc{train_acc:.4f})训练循环里最值得强调的就是clip_grad_norm_。LSTM在长序列上反向传播会累积梯度范数很容易爆到几千加上这一行clip max_norm5.0就能防住大部分NaN训练失败。它不改变梯度方向只是把超出的范数缩回来代价是极端情况下收敛变慢换来的是稳定性值得。如果训练时发现loss突然变NaN第一件事就是先检查是不是忘了clip再考虑学习率问题。4. 训练验证与调参准确率、F1和隐藏层大小的设定逻辑训练循环能跑起来只是第一步模型能不能用要看验证集怎么设计和指标怎么解读。LSTM分类预测任务里验证集设计比模型本身更容易出错。如果验证集和训练集有时间重叠或者用了随机分割而不是切时间段指标虚高会让你上线后翻车。4.1 评估指标准确率、F1、混淆矩阵在LSTM场景下的真实含义多输入单输出任务里准确率是最直观的指标但最容易误导。设备故障数据里故障样本占5%一个模型把所有样本都预测成正常准确率就是95%看起来很好实际一故障都抓不到。所以要同时看F1、精确率和召回率。F1是精确率和召回率的调和平均。精确率关注“预测为故障的样本里有多少是真故障”召回率关注“真实故障里有多少被找出来”。不同场景对两者的权重不同设备检修场景更看重召回率漏检一台故障设备可能造成停机损失宁肯多停机检查也要把故障找出来欺诈检测场景更看重精确率误判一笔正常交易带来的客诉成本可能高过漏判。训练完不要只看loss把F1和混淆矩阵打印出来。混淆矩阵在LSTM分类里还有一个额外用途观察误分类样本在时间上的分布。如果误分类集中在某一段连续时间内说明模型在某个时段的状态切换期失效也说明当前seq_len可能没覆盖到关键转折点。如果误分类随机散布说明是特征区分度不足重点转向特征工程。下面这段验证代码我每次训练完都会跑一遍输出每个类别的precision、recall、F1和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix def evaluate_model(model, dataloader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for X_batch, y_batch in dataloader: X_batch X_batch.float().to(device) logits model(X_batch) preds logits.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(y_batch.numpy()) print(classification_report(all_labels, all_preds, digits3)) print(confusion_matrix(all_labels, all_preds))打印出来的数字怎么解读如果类别0是正常、类别1是故障看分类报告里类别1的F1低于0.5基本可以判断模型没有真正学会故障模式。再拿混淆矩阵看误分类样本的分布把预测成正常的故障样本挑出来核对它们的时间戳。如果集中在同一时段回去翻原始数据往往是那一时段传感器出现漂移或缺失属于数据质量问题不是模型问题。4.2 关键超参数hidden_size、num_layers、seq_len、dropout的设定顺序LSTM分类模型的超参数没有绝对正确答案但有一套高效的调整顺序。我建议按seq_len → hidden_size → num_layers → dropout的优先级来调不要一上来就同时调四个参数否则你根本不知道是哪个起了作用。seq_len是最影响结果的一个超参数它决定模型能看到多长的“记忆”。一种判断方法是做自相关分析计算标签序列的自相关系数看第几阶滞后之后相关性衰减到接近0这个滞后长度可以作为seq_len的下限。另一种更实操的方法是固定其他参数跑seq_len[10, 20, 30, 50, 80]的对比实验画出验证集F1随seq_len的变化。F1还在上升说明时间上下文不够长模型还在吃信息红利F1持平后下降说明已经开始引入噪声。取拐点附近的seq_len。hidden_size直接关系参数总量。一个LSTM单元的参数近似为4 * (input_size * hidden_size hidden_size²)输入10维、hidden_size64时大约4*(6404096)18944个参数hidden128时变成约4*(128016384)70656个。样本量只有几千时hidden_size128已经偏大需要配合更强的dropout。我一般先定hidden_size64欠拟合明显再往上加每加一次就减少一些训练epoch数因为参数多了收敛更快。num_layers一般取1或2。单层LSTM对大多数工业时序分类足够如果任务有明显的多尺度时间模式比如短期波动叠加长期趋势2层会好一点第一层负责提取局部模式第二层负责捕捉跨时间步的组合。3层以上容易在训练集上过拟合除非你有十万级样本否则不建议一上来就堆深度。dropout在LSTM里的作用比全连接更明显。循环网络同一组参数在每个时间步重复使用每一小步的误差都会累积dropout是打断这种累积最简单的手段。建议输出层的nn.Dropout用0.3起步如果验证集F1和训练集F1差距大于10个百分点把dropout提到0.5。4.3 过拟合与欠拟合的识别训练曲线判读与后续动作训练曲线判读规则比参数本身更重要。把训练集loss和验证集loss画在同一张图上训练loss稳步下降、验证loss先降后升是典型过拟合动作是加大dropout、加weight_decay、减hidden_size两个loss都居高不下是欠拟合动作是加hidden_size或num_layers、加seq_len、降低dropout验证loss震荡剧烈可能是学习率过大或batch size过小先把学习率降一半试试。还有一种LSTM独有的奇怪现象训练loss在下降但验证集准确率迟迟不升。这通常不是过拟合而是标签时间对齐出了问题。多输入单输出任务里标签选最后一个时刻如果故障标签实际打在窗口中间时刻LSTM会学到错误的映射。排查方法是把误分类样本的标签往前或往后平移若干个时间步再看验证集指标如果有明显改善说明标签对齐应该用另一个滞后时间。这个坑在工业界非常常见故障从发生到被系统标记存在响应延迟标记逻辑错了模型再怎么调参都白搭。另一个常见情况是class weight加上后验证集准确率反而下降。这不是坏事。准确率下降、F1上升说明模型从“全部预测为多数类”转向了“开始真正区分少数类”。调参的目标应该锚定F1不是准确率。准确率只作为参考不作为主指标。5. LSTM分类预测避坑指南时间泄漏、数据泄漏与收敛失败排查这一章专门写我实际踩过的坑。LSTM分类任务里最隐蔽的错误不是模型写错而是数据处理阶段埋下的“时间泄漏”。因为LSTM的输入窗口有前后依赖很多处理习惯在普通MLP上没问题一到LSTM就变成无效甚至有害。5.1 数据泄漏随机切分训练集和验证集现象验证集准确率95%以上训练集准确率也很高模型上线后准确率骤降到60%以下。原因训练集和验证集是从同一条长序列滑窗出来的step1时相邻窗口有seq_len-1个时间步重叠随机切分会把几乎相同的窗口同时分配到训练集、验证集和测试集模型等于提前看到了答案。解决按时间顺序切分train取前70%val取中间10%test取最后20%并确保切分点在滑窗之前先切原始时间范围再各自滑窗否则切分点附近仍然有重叠窗口。更严格的做法是“按事件切分”。如果数据集来自多个设备、多个批次要把同一个设备或批次的数据全部划到同一个集合不能让同一批次的窗口横跨训练集和验证集。解决方法是先在数据上加一个group列设备号或批次号用GroupKFold按组切分而不是按行数硬切。5.2 归一化泄漏StandardScaler在整段数据上拟合现象验证集F1不错把模型部署到新数据上效果骤降。原因在滑窗前对全量数据做了fit_transform归一化统计量里混入了验证集和测试集的信息。均值方差虽然不直接是标签LSTM对输入统计分布很敏感训练时见过未来数据分布线上数据分布一变就崩。解决先按时间或组切分原始数据只在train段上fit scaler再transform train/val/test三段。这个习惯在时间序列任务里应该成为肌肉记忆。5.3 梯度爆炸导致loss变成NaN现象训练到第几个epochloss突然变成nan或者一开始就是nan。原因LSTM内部有累乘结构反向传播时梯度沿着时间步累乘长序列下梯度范数爆炸数值溢出。解决第一步在optimizer.step()前加torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)第二步检查学习率是否大于1e-2是就降到1e-3第三步检查输入里是否有NaN或Inf数据处理阶段先做数值检查用np.isnan(features).any()这类语句扫一遍。梯度裁剪的max_norm建议5.0太大没用太小收敛慢5.0是实用折中点。5.4 多输入特征观测值量纲差异大现象训练loss下降非常慢验证准确率始终在随机水平附近。原因LSTM使用tanh激活函数输入特征数值范围如果跨数量级大数值特征会主导遗忘门和输入门的计算小尺度特征的梯度被淹没。解决统一做z-score归一化偏态严重的特征改用RobustScaler。归一化参数必须在训练段上拟合归一化要发生在滑窗之前不要对每个窗口单独归一化否则每个窗口均值为0LSTM就捕捉不了绝对水平信息模型等于在“零中心化”的假数据上学。5.5 预测结果全部落在同一个类别现象模型训练完测试集预测全都是0类或者少数类一个都预测不出来。原因一类是类别不均衡交叉熵被多数类主导加class weight可以缓解另一类是决策阈值问题分类头输出的概率即使正确argmax仍然偏向多数类。解决先用class weight然后在验证集上计算概率分布选一个能最大化F1的阈值而不是默认用0.5。下面这段代码演示了二分类时怎么搜索最优判别阈值import numpy as np from sklearn.metrics import f1_score def find_best_threshold(model, dataloader, device): model.eval() prob_pos, labels_all [], [] with torch.no_grad(): for X_batch, y_batch in dataloader: X_batch X_batch.float().to(device) logits model(X_batch) probs torch.softmax(logits, dim1)[:, 1].cpu().numpy() prob_pos.extend(probs) labels_all.extend(y_batch.numpy()) best_th, best_f1 0.0, 0.0 for th in np.arange(0.1, 0.9, 0.05): preds (np.array(prob_pos) th).astype(int) f1 f1_score(labels_all, preds) if f1 best_f1: best_f1, best_th f1, th return best_th, best_f1多分类时为每个类别分别搜索阈值会复杂一些但思路一致不要假设0.5就是最优决策点。类别不均衡越严重最优阈值偏离0.5越远。6. 把模型部署到实际预测场景推理封装、阈值选择与端到端验证模型训练收敛只是走完一半路另一半是让它稳定地跑在真实数据上。LSTM分类模型部署到实际预测任务时最容易断裂在“训练时的预处理逻辑”和“推理时的预处理逻辑”不一致。训练时做了StandardScaler、滑窗、时序切分部署时只要三步中任何一步顺序或参数不对模型等于在喂一种它没见过的数据分布。我的习惯是把预处理和模型一起封装。训练完成后把scaler对象、滑动窗口参数(seq_len, step)、类别映射字典都序列化保存推理时加载同一个scaler对象和同一套参数确保数据变换路径完全一致。下面是一个可复用的推理封装示例import joblib import numpy as np import torch class LSTMClassifierInference: def __init__(self, model, scaler, seq_len, step, deviceNone): self.model model self.model.eval() self.scaler scaler self.seq_len seq_len self.step step self.device device or (cuda if torch.cuda.is_available() else cpu) self.model.to(self.device) def predict_one(self, recent_window: np.ndarray): # recent_window 形状 (seq_len, n_features)特征列顺序必须和训练时一致 scaled self.scaler.transform(recent_window) x_tensor torch.tensor(scaled, dtypetorch.float32).unsqueeze(0) with torch.no_grad(): logits self.model(x_tensor.to(self.device)) prob torch.softmax(logits, dim1).cpu().numpy()[0] return prob # 返回概率向量由业务方根据阈值决定最终分类这种封装的价值在于训练和推理共用同一个scaler和窗口参数不会出现“推理忘了归一化”这种低级错误。部署前最后一个验证动作是拿一段和训练集完全不相交的新数据跑一遍“端到端回放”从原始表开始走完整的滑窗、归一化、预测、阈值判断流程确认输出的类别和时间戳能对齐概率值落在合理区间。这一步能一次性暴露数据泄漏和封装问题。如果推理延迟有硬指标可以再用torch.jit.script或ONNX导出模型做图优化。LSTM在GPU上已经很快但CPU实时推理时序列长度变长会拖慢速度脚本化导出通常能降30%到50%的延迟。我自己的教训是永远不要跳过样本外回放直接上线。所有时间序列分类项目我固定要求三条规范——数据预处理只在训练段拟合、滑窗切分必须在时间轴上完成后再分区、上线前必须做一次时间顺序的样本外验证。这三条看似简单但能挡住大多数隐性翻车。希望帮到你。本文还有配套的精品资源点击获取