简介:这份资源面向计算机、人工智能、自动化、通信工程等专业的在校学生与教师,以及从事轨道交通信号或工业设备监测的工程人员,提供一套基于LSTM多时间序列特征提取的道岔故障诊断完整实现方案。项目以Python为开发语言,围绕道岔动作电流等时序信号展开特征提取与故障分类建模,适合作为毕业设计、课程设计、项目立项演示或进阶学习案例。压缩包共9个文件,包含7个py源码文件、1个md说明文档和1个docx实验报告,整体约872KB,源码覆盖特征工程、LSTM模型构建、多线程处理与测试脚本等模块,实验报告则系统梳理了方法原理与实验流程。目前已有200人学习关注。读者可据此掌握多时间序列特征提取与LSTM故障诊断的完整链路,理解模型搭建、训练与评估细节,并可在现有代码基础上修改扩展,用于自身课题或工程场景。
1. 从一份道岔故障诊断源码说起:LSTM 多时间序列特征提取能解决什么
道岔是铁路信号系统里故障率最高的室外设备之一,它的动作电流曲线、功率曲线、锁闭时间这些时序信号,直接反映转辙机的工作状态。传统做法靠人工看曲线、设阈值报警,漏报误报都不少。这份railway_fault_detect-master资源,核心就是用 LSTM 对多路时间序列做特征提取,再完成道岔故障分类诊断,附带一份实验报告文档。它适合做毕设、课设、信号专业课题的在校生,也适合想上手 LSTM 时序分类的 Python 学习者。代码文件不多,features.py管特征工程,LSTM.py是模型主体,xiecheng.py和thread.py涉及多线程数据加载,test.py、pythontest.py是测试入口。下面按「数据怎么进、特征怎么提、模型怎么搭、坑在哪」拆开讲。
2. 数据管线拆解:从原始动作曲线到 LSTM 可吃的三维张量
2.1 道岔时序信号的特点与预处理逻辑
道岔动作电流曲线是一条典型的短时非平稳信号,一次完整动作大概 5 到 8 秒,采样率常见 25Hz 到 100Hz 不等。原始数据往往是一堆 CSV 或 Excel,每行一个采样点,列包括时间戳、电流值、功率值、道岔编号、动作方向。直接丢给 LSTM 是不行的,因为 LSTM 要求输入是(样本数, 时间步, 特征数)的三维张量,而原始数据是二维表。
常见做法是先把每条动作曲线切出来,按道岔编号和动作时间分组,然后统一重采样到固定长度。比如原始 6 秒 100Hz 是 600 个点,可以线性插值到 200 个点,这样每个样本的时间步就是 200。特征维度可以是 1(只用电流),也可以是 3(电流、功率、时间差分)。这一步在features.py里通常有对应的重采样和归一化函数。
归一化用 Min-Max 还是 Z-Score,取决于你的数据分布。道岔电流在启动瞬间有冲击,Z-Score 对异常值更敏感,Min-Max 会把冲击压到 0-1 之间,LSTM 收敛更稳。我一般先用 Min-Max,如果发现某类故障被淹没再换 Z-Score 试。
import numpy as np import pandas as pd from scipy.signal import resample def load_and_segment(csv_path, target_len=200): """ 读取原始CSV,按道岔编号和动作时间分组,重采样到固定长度 csv_path: 原始数据路径 target_len: 统一时间步长度 """ df = pd.read_csv(csv_path) # 假设列名: switch_id, action_time, current, power df['action_time'] = pd.to_datetime(df['action_time']) # 按道岔编号和动作时间分组,每组是一条完整动作曲线 groups = df.groupby(['switch_id', pd.Grouper(key='action_time', freq='10S')]) samples = [] labels = [] for (sid, atime), g in groups: if len(g) < 10: # 过滤掉不完整的片段 continue cur = g['current'].values.astype(np.float32) # 重采样到固定长度 cur_resampled = resample(cur, target_len) # Min-Max 归一化 cur_norm = (cur_resampled - cur_resampled.min()) / (cur_resampled.max() - cur_resampled.min() + 1e-8) samples.append(cur_norm) # 标签从文件名或额外列取,这里假设有 label 列 labels.append(g['label'].iloc[0]) X = np.array(samples).reshape(-1, target_len, 1) y = np.array(labels) return X, y这段代码的关键参数是target_len,它决定 LSTM 的时间步数。设太小会丢失曲线形态,设太大会增加计算量。200 是一个经验值,对应 2 秒左右的窗口,能覆盖道岔启动、转换、锁闭三个阶段。freq='10S'是分组窗口,如果你的动作间隔更短,改成5S或3S。归一化里的1e-8是防止除零,血泪经验,不加的话遇到恒定电流段直接出 NaN。
2.2 多时间序列特征提取的两种落地方式
「多时间序列特征提取」这个说法容易让人懵。在这份资源里,它其实有两层意思:一是输入本身就是多路时序(电流、功率、时间差分),二是 LSTM 在时间维度上自动提取特征,不需要手工设计 FFT 或小波。
手工特征和 LSTM 自动特征不是对立的。我一般会先算几个统计量作为辅助输入:均值、方差、峰值、过零率。这些在features.py里可以用几行搞定,然后和原始归一化曲线拼在一起,形成多通道输入。这样 LSTM 既有原始形态,又有统计摘要,收敛更快。
def extract_stat_features(signal): """ 对单条时序信号提取统计特征 signal: 一维数组,长度 target_len 返回: 4维统计特征 """ mean = np.mean(signal) std = np.std(signal) peak = np.max(np.abs(signal)) zcr = np.sum(np.abs(np.diff(np.sign(signal)))) / (2 * len(signal)) return np.array([mean, std, peak, zcr], dtype=np.float32) # 在 load_and_segment 里对每条曲线调用 # stat_feat = extract_stat_features(cur_norm) # 然后把 stat_feat 复制到每个时间步,或者只在最后一步拼接统计特征怎么用,取决于你的模型结构。如果 LSTM 后面接全连接,可以把统计特征单独走一个分支再 concat。如果嫌麻烦,直接把统计特征广播到每个时间步,变成(target_len, 5)的输入,LSTM 也能学到。注意过零率对道岔电流不太敏感,因为电流基本是正的,换成「电流上升沿数量」更合适。
多线程加载在thread.py里,主要是为了加速大批量 CSV 读取。Python 的 GIL 对 IO 密集任务影响不大,用ThreadPoolExecutor读文件是划算的。但别在数据增强阶段用多线程,numpy 操作本身释放 GIL,多线程反而可能因为竞争变慢。
3. LSTM 模型搭建与训练:层数、隐藏单元、过拟合的取舍
3.1 模型结构设计与参数含义
LSTM.py是核心文件,里面大概率是一个nn.LSTM加全连接分类头。LSTM 的关键参数就三个:input_size、hidden_size、num_layers。input_size等于你的特征维度,如果只用电流就是 1,加了统计特征就是 5。hidden_size是隐藏状态维度,太小欠拟合,太大过拟合,道岔数据量通常几千条,128 或 64 够用。num_layers一般 1 到 2 层,2 层以上容易梯度消失,除非你数据量上万。
import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, num_classes=4): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers # batch_first=True 表示输入维度是 (batch, seq, feature) self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.3) self.fc = nn.Linear(hidden_size, num_classes) def forward(self, x): # x: (batch, seq_len, input_size) h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) out, _ = self.lstm(x, (h0, c0)) # 取最后一个时间步的输出做分类 out = out[:, -1, :] out = self.fc(out) return outdropout=0.3是防过拟合的,如果训练集准确率远高于验证集,调到 0.5。batch_first=True一定要设,否则输入维度是(seq, batch, feature),调试时容易搞混。取最后一个时间步out[:, -1, :]是常见做法,也可以用平均池化out.mean(dim=1),后者对长序列更稳。
训练循环里,优化器选 Adam,学习率 1e-3 起步,损失函数用 CrossEntropyLoss。如果类别不均衡(比如正常样本远多于故障样本),加weight参数或者用 Focal Loss。早停策略看验证集 loss,连续 10 个 epoch 不降就停。
3.2 训练流程与验证指标
训练脚本test.py或pythontest.py里应该有完整的 train/val/test 划分。常见坑是随机划分导致同一动作曲线的不同片段同时出现在训练集和验证集,造成数据泄露。正确做法是按道岔编号或按时间段划分,确保验证集里的道岔在训练集里没出现过。
from sklearn.model_selection import train_test_split from torch.utils.data import DataLoader, TensorDataset # 假设 X, y 已经准备好 # 按道岔编号划分,避免同一道岔的样本泄露 switch_ids = np.array([...]) # 每条样本对应的道岔编号 unique_switches = np.unique(switch_ids) train_switches, val_switches = train_test_split(unique_switches, test_size=0.3, random_state=42) train_mask = np.isin(switch_ids, train_switches) val_mask = np.isin(switch_ids, val_switches) X_train, y_train = X[train_mask], y[train_mask] X_val, y_val = X[val_mask], y[val_mask] train_ds = TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)) val_ds = TensorDataset(torch.FloatTensor(X_val), torch.LongTensor(y_val)) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False)验证指标别只看准确率。道岔故障诊断里,漏报(把故障判成正常)比误报代价高得多,所以召回率比精确率重要。实验报告里如果只给了 accuracy,自己补一个 confusion matrix 和 F1。sklearn.metrics.classification_report一行就能出。
训练轮数一般 50 到 100,配合早停。如果 loss 震荡厉害,把 batch_size 调大或者学习率降到 5e-4。GPU 不是必须的,道岔数据量小,CPU 跑几百个 epoch 也就几分钟。
4. 避坑与排查:道岔 LSTM 诊断里最容易翻车的五个地方
4.1 现象:训练 loss 不降,准确率卡在 25%
原因通常是输入维度搞错了。LSTM 要求(batch, seq, feature),如果你把(batch, feature, seq)丢进去,模型学不到时间依赖。另一个可能是标签没转成 0 到 num_classes-1 的整数,CrossEntropyLoss 要求 LongTensor 且从 0 开始。
解决:在 forward 里打印x.shape,确认第二维是时间步。标签用LabelEncoder转一遍,或者手动映射。
4.2 现象:验证集准确率很高,但测试集一塌糊涂
这是典型的数据泄露。同一动作曲线的相邻片段被分到了训练和验证集,模型只是记住了曲线形状。按道岔编号划分能解决大部分问题。如果数据里同一道岔只有一种故障,那按编号划分会导致验证集缺少某些类别,这时候按时间段划分更合适。
解决:划分前先检查每个道岔的故障类型分布,确保训练集和验证集的类别比例接近。
4.3 现象:模型对正常样本全判对,对故障样本全判错
类别不均衡。正常样本可能占 90%,模型只要全判正常就有 90% 准确率。解决:损失函数加weight=torch.FloatTensor([1, 5, 5, 5]),或者用重采样把故障样本过采样到和正常一样多。过采样时注意别简单复制,加一点高斯噪声或时间偏移。
4.4 现象:重采样后曲线形态失真,锁闭段被拉平
线性插值对短时冲击不友好。道岔锁闭瞬间的电流跌落如果被插值平滑掉,模型就学不到关键特征。解决:改用保形插值(PCHIP)或者直接截断/填充到固定长度,不做重采样。如果必须重采样,先做峰值检测,确保峰值位置不变。
4.5 现象:多线程读取时内存暴涨,程序被 kill
thread.py里如果一次性把所有 CSV 读进内存再处理,几千条曲线就是几个 G。解决:用生成器逐批读取,或者用pd.read_csv的chunksize参数分块。多线程只用于 IO,处理完一批就释放。
提示:道岔数据里经常有缺失值,用
df.interpolate()补之前先确认缺失是设备离线还是传感器故障,后者补了反而引入噪声。
5. 从跑通到改出花:迁移到其他时序分类任务的三个技巧
这份资源的代码结构其实不限于道岔。任何「多路传感器时序 + 分类标签」的场景都能套:电机故障诊断、轴承振动分类、心电图异常检测。我拿它改过一个轴承数据集,换了features.py里的重采样长度和LSTM.py的input_size,其他基本没动。
第一个技巧是特征维度的弹性扩展。原始代码如果只用了电流,你可以把功率、温度、振动拼进去,input_size从 1 改成 4。但别一次性加太多,先用相关性分析筛一遍,相关性低于 0.1 的通道直接丢掉,否则 LSTM 会被噪声带偏。
第二个技巧是时间步的滑动窗口增强。如果原始数据只有几百条,可以用滑动窗口切出更多样本。比如一条 600 点的曲线,窗口 200,步长 50,能切出 9 条。标签继承原曲线标签。这样数据量翻几倍,LSTM 的泛化会好很多。注意窗口不能跨曲线切,否则标签就乱了。
def sliding_window_augment(X, y, window=200, stride=50): """ 对长时序做滑动窗口增强 X: (n_samples, seq_len, n_features) y: (n_samples,) """ aug_X, aug_y = [], [] for i in range(len(X)): seq = X[i] for start in range(0, seq.shape[0] - window + 1, stride): aug_X.append(seq[start:start+window]) aug_y.append(y[i]) return np.array(aug_X), np.array(aug_y)第三个技巧是模型集成。单层 LSTM 方差大,跑三次不同随机种子取平均,准确率能稳 2 到 3 个点。如果嫌麻烦,至少把torch.manual_seed固定住,保证实验可复现。我一般会在test.py开头加torch.manual_seed(42)和np.random.seed(42),不然每次跑出来的 confusion matrix 都不一样,写报告时对不上号。
实验报告文档基于LSTM多时间序列特征提取的道岔故障诊断方法new.docx里应该有完整的实验设置和结果分析,建议先看一遍再跑代码,能省不少调试时间。README.md 里有运行说明,按它走基本能跑通。从那以后我每次拿到时序分类的源码,都强制先跑一遍数据形状检查,确认(batch, seq, feature)没搞反,再动模型。希望帮到你。
本文还有配套的精品资源,点击获取