资讯动态

PyTorch Transformer 一维信号分类:预处理、数据划分与部署

发布时间:2026/9/29 7:15:11 来源:尧图企业网站定制
1. 一维信号分类为什么值得单独拿出来讲先把场景说清楚。这里的一维信号指的是按时间或空间顺序排列的单变量或多变量序列比如加速度计的三轴振动数据、心电信号的单导联波形、工业设备的电流谐波序列、语音的梅尔频谱时间帧、雷达回波的时序采样点。它们和图像的差别在于图像有明确的空间局部性相邻像素构成纹理而一维信号的核心信息藏在时序依赖里——某个时刻的数值要结合前后几十甚至几百个采样点才能判断它属于哪一类。传统的做法是人工提特征比如均值、方差、峰值因子、峭度、FFT主频、小波包能量然后丢给SVM或者随机森林。这条路我走过很多次优点是样本需求小、可解释性强缺点是特征工程的天花板很低换个工况、换个传感器型号之前调好的特征可能就废了。深度学习进来之后1D-CNN成了主流但它有个硬伤卷积核的感受野是固定的想覆盖更长的依赖就得堆层数或者加空洞参数量和显存消耗跟着上涨。Transformer换了个思路——用自注意力让序列中任意两个位置直接建立联系感受野天然是全局的。这对一维信号特别友好因为很多分类任务的关键证据恰恰跨越了长距离比如电机故障的周期性冲击、心律失常中RR间期的变化模式。Pytorch实现Transformer序列分类的门槛其实不高真正容易翻车的地方在输入的组织方式和位置编码的处理上这两块我后面会重点拆。这篇内容适合三类人一是已经会写Pytorch基础模型、想做时序分类但不知道怎么套Transformer的二是用CNN效果卡住了、想换架构试试的三是需要一份可以直接改改就跑的代码骨架的。我会给出一套完整的工程化实现包括数据预处理、模型结构、训练循环、以及我踩过的那些坑。2. 把原始信号喂进Transformer之前的必要变形2.1 输入张量的形状到底该长什么样Pytorch的nn.TransformerEncoder默认期望的输入形状是(seq_len, batch, d_model)注意序列维度在最前面这是很多人第一个栽跟头的地方。而我们在做数据处理时习惯的格式通常是(batch, seq_len, channels)所以要决定在哪个环节做转置。我的建议是把数据管道统一成(batch, channels, seq_len)然后在模型forward里第一步就转置写成x x.permute(2, 0, 1)。这样做的好处是数据加载、切窗、标准化这些操作都在(batch, channels, seq_len)的直觉下完成只有进Transformer之前才转换代码可读性高排查问题时不容易搞混维度。这里有个关键判断你的信号是单通道还是多通道单通道意味着channels1投影层就是nn.Linear(1, d_model)多通道比如三轴加速度channels3投影层是nn.Linear(3, d_model)。投影之后所有通道的信息被融合进一个d_model维的向量表示序列长度保持不变。class SignalTransformer(nn.Module): def __init__(self, in_channels1, d_model128, nhead8, num_layers4, num_classes5, seq_len512, dropout0.1): super().__init__() self.input_proj nn.Linear(in_channels, d_model) self.pos_embed nn.Parameter(torch.zeros(1, seq_len, d_model)) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.norm nn.LayerNorm(d_model) self.head nn.Linear(d_model, num_classes) def forward(self, x): # x: (batch, channels, seq_len) x x.permute(0, 2, 1) # - (batch, seq_len, channels) x self.input_proj(x) # - (batch, seq_len, d_model) x x self.pos_embed x self.encoder(x) x self.norm(x.mean(dim1)) # 全局平均池化 return self.head(x)2.2 位置编码可学习参数还是正弦函数Transformer本身是置换不变的不给位置信息的话打乱序列顺序模型输出不变这对时序数据是致命的。所以位置编码必须加。两种主流选择一是固定正弦编码二是可学习的位置嵌入。在做一维信号分类时我更倾向可学习位置嵌入原因是信号的采样率、窗口长度在项目里通常是固定的可学习参数能自适应地学到哪些位置更重要实测在振动和心电数据集上比正弦编码的精度略高一点大约0.5到1.5个百分点视数据集而定。但可学习位置嵌入有个必须注意的点序列长度必须是固定的。如果你的测试集里出现了比训练集更长的序列位置嵌入的索引会越界。解决办法有两个要么在预处理阶段强制统一窗口长度要么用插值的方式动态调整pos_embed的尺寸。我一般选前者简单可靠。正弦编码则天然支持任意长度外推代价是表达灵活性差一些。如果业务场景里序列长度会变就用正弦编码公式不用手写Pytorch里几行就能生成。提示位置嵌入的初始化建议用torch.nn.init.trunc_normal_标准差设成0.02比默认的全零或者标准正态都稳训练初期梯度不会爆炸。2.3 标准化与窗口切分细节决定成败原始信号的量纲差异极大加速度计可能是±16g心电信号是毫伏级直接送进网络梯度尺度会乱成一锅粥。我用的标准化策略是按通道做Z-score每个通道减均值除标准差均值和标准差从训练集统计出来然后应用到验证集和测试集绝对不能各自标准化。窗口切分也很关键。假设采样率1000Hz你想让模型看到1秒的数据那窗口长度就是1000。但这里有个反直觉的经验窗口不是越长越好。我做过对比实验窗口从256增到2048精度先升后降原因是太长的窗口里大部分采样点对分类没有贡献反而稀释了关键片段的信息密度。一般来说让窗口长度覆盖2到3个典型周期就够用了。如果样本量不够可以用重叠切窗做数据增强重叠率50%是常见起点。但要注意重叠切窗会让同一条原始记录切出来的多个窗口被分到训练集和测试集造成数据泄露——测试精度虚高。正确做法是按原始记录划分数据集而不是按窗口划分这一点我后面还会专门说。3. 模型结构里那些值得推敲的设计选择3.1 池化方式CLS token、平均池化还是最大池化Transformer Encoder输出的形状是(batch, seq_len, d_model)要变成(batch, num_classes)必须做聚合。常见三种做法池化方式实现适用场景我遇到的实际表现全局平均池化x.mean(dim1)通用场景稳定性好收敛快精度中上最稳妥全局最大池化x.max(dim1).values关键冲击特征明显对故障类信号偶尔更好但易受噪声影响CLS token序列前拼一个可学习tokenBERT风格需要更多数据小数据集上不如平均池化一维信号分类里我默认用平均池化。原因很实际信号里普遍存在噪声和无效片段平均池化对这些干扰的鲁棒性强。最大池化只在明确知道关键证据是短时冲击时才考虑比如轴承的早期点蚀故障。CLS token因为引入了一个额外的可学习向量需要足够多的数据才能学好样本少于几千条的时候不推荐。3.2 层数与注意力头数的取舍d_model必须能被nhead整除这是硬约束。常见的组合是d_model128, nhead8每个头16维或者d_model256, nhead8每个头32维。层数方面一维信号分类的任务复杂度普遍低于NLP4到6层足够。我试过堆到12层精度没提升多少训练时间翻倍还更容易过拟合。参数量控制在50万到300万之间是个比较舒服的区间。注意力头数也不是越多越好。头数多意味着每个头负责的子空间维度小能捕捉更多样的模式但也更容易过拟合。我的经验是数据量在1万条窗口以下用4到8个头超过10万条可以上16个头。3.3 前馈网络维度与dropout的配合nn.TransformerEncoderLayer里dim_feedforward默认是d_model的4倍这个比例在信号任务里基本适用。如果你发现模型容量过剩可以降到2倍如果欠拟合升到6倍试试。dropout的设置有个容易忽略的细节不要只在大模块上设dropout注意力权重上的dropout更关键。Pytorch的EncoderLayer里dropout参数同时作用于注意力权重和前馈网络所以调一个值就够了。但我发现单独给注意力设更高的dropout比如0.2前馈用0.1在噪声大的信号上效果更好。如果需要这种细粒度控制就得自己拆开写注意力模块。3.4 一个容易忽视的点LayerNorm的位置Pytorch的nn.TransformerEncoderLayer默认是Post-LN残差之后做归一化这对深网络训练不太友好通常需要warmup。而Pre-LN残差之前归一化训练更稳定几乎不需要warmup。从Pytorch 1.12开始EncoderLayer支持norm_firstTrue参数开启Pre-LN。我的建议是一律用norm_firstTrue。实测在信号分类任务上Pre-LN的收敛速度和最终精度都更优尤其是层数超过4层之后差别明显。这个改动只需一个参数收益却不小。4. 训练过程中的数据划分与调参实战4.1 按窗口划分数据集是精度虚高的元凶这一条我要重点讲因为它是我见过最常见的坑。假设你有100条原始信号记录每条切成100个重叠窗口总共1万个窗口。如果你把这1万个窗口随机打乱8:2分成训练和测试那么训练集里的某个窗口和测试集里的某个窗口很可能来自同一条原始记录它们高度相似甚至部分重叠。模型见过类似样本测试精度自然虚高可能到95%以上但上线后掉到70%。正确做法按原始记录ID划分比如80条记录训练20条测试然后再各自切窗。这样训练集和测试集的窗口来源完全隔离评估结果才可信。from sklearn.model_selection import GroupShuffleSplit splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(splitter.split(windows, labels, groupsrecord_ids))用了GroupShuffleSplit之后如果你的精度从95%掉到78%别慌这不是模型变差了而是之前的评估是假的。这个78%才是真实的泛化能力。4.2 学习率调度warmup不是必需品因为用了Pre-LNwarmup可以省掉。直接用AdamW学习率设1e-3到3e-4之间配合余弦退火调度器。optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxepochs, eta_min1e-6 )weight_decay建议用1e-4信号任务里正则化对抑制过拟合帮助明显。如果发现训练loss下降但验证loss上升先把weight_decay加到5e-4试试。batch size方面序列分类的显存消耗和batch × seq_len × d_model成正比。seq_len512, d_model128时batch size设64在8GB显存上跑得动。如果显存不够优先降batch size别降d_model因为d_model影响每个位置表达能力的上限。4.3 早停与模型保存的判据监控验证集的宏平均F1比准确率更可靠尤其是类别不平衡的信号数据。早停的patience设10到15保存验证F1最高的那个epoch的权重。from sklearn.metrics import f1_score best_f1 0 patience_counter 0 for epoch in range(epochs): train_one_epoch(model, train_loader, optimizer) val_preds, val_labels evaluate(model, val_loader) f1 f1_score(val_labels, val_preds, averagemacro) if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best.pth) patience_counter 0 else: patience_counter 1 if patience_counter 15: break scheduler.step()有个小细节scheduler.step()放在早停判断之后或者之前都行但要和你的调度策略一致。用余弦退火时我习惯每个epoch都step不纠结。4.4 类别不平衡的处理优先级信号数据里类别不平衡太常见了正常状态的样本远多于故障状态。处理手段按优先级排序加权交叉熵最简单nn.CrossEntropyLoss(weightclass_weights)权重按类别频率的倒数算。重采样对少数类做重叠切窗扩充或者对多数类下采样。注意重采样要在按记录划分数据集之后做避免泄露。Focal Loss类别极度不平衡比如1:100时才考虑实现稍微复杂点。我一般先用加权交叉熵如果少数类召回率还是上不去再试重采样。两者叠加通常没必要容易矫枉过正。5. 我踩过的坑与对应排查链路5.1 第一个坑模型训练loss不降一直卡在随机水平这个现象我遇到过两次排查链路如下。第一步检查输入和标签是否对齐。用torch.utils.data.DataLoader取一个batch打印x.shape和y的前几个值确认不是标签错位。有一次我写预处理脚本时shuffle开了但没同步打乱标签模型学了半天学了个寂寞。第二步检查位置编码有没有加上。忘了加位置编码的概率不低因为forward里那行x x self.pos_embed很容易在复制粘贴时漏掉。不加位置编码模型对时序顺序不敏感在依赖顺序的任务上精度就是随机水平。第三步检查标准化。如果某些通道的数值大到几百甚至几千梯度会爆炸或者消失。我习惯在数据加载后打印每个通道的均值和标准差正常标准化后应该接近0和1。第四步把学习率降到1e-4再试。Transformer对学习率敏感3e-4在某些数据集上偏大梯度震荡导致loss不降。5.2 第二个坑训练精度极高验证精度崩盘这是典型的过拟合但信号任务里过拟合往往有特殊原因。先看数据泄露就是前面说的按窗口划分数据集。如果确认划分没问题再看模型容量。4层Transformer、d_model128、几万参数在几千条样本上确实容易过拟合。这时候加dropout到0.3、加weight_decay、减少层数三管齐下。还有一个隐蔽原因验证集和测试集的标准化参数用了各自的统计量。前面强调过标准化参数必须从训练集统计应用到所有集合。如果每个集合各自标准化验证集的分布被手工对齐了训练时学的统计规律对不上验证精度自然崩。5.3 第三个坑推理时显存占用远超训练训练时好好的部署推理时显存爆了原因通常是没切eval()模式或者没关梯度。model.eval() with torch.no_grad(): logits model(x) probs torch.softmax(logits, dim-1) preds probs.argmax(dim-1)另外如果序列很长比如seq_len4096自注意力的计算复杂度是O(seq_len^2)显存占用是平方级增长。这种情况要么缩短窗口要么用滑窗分段推理再融合结果。我做过一个设备状态监测项目seq_len1600时单条推理显存占用接近1GB改成seq_len400滑窗四段推理再投票显存降到200MB以内精度还略有提升因为分段后每段的信噪比更高。5.4 第四个坑不同批次间精度波动大训练日志里验证精度忽高忽低差的时候能掉十几个点这个现象背后通常有两三个原因叠加。一是batch size太小梯度估计噪声大。把batch size从16提到64波动会明显收敛。二是数据分布本身不均匀某些batch里全是某一类。用DataLoader的shuffleTrue能缓解但如果类别极度不平衡最好用WeightedRandomSampler保证每个batch的类别分布相对均匀。三是验证集太小评估指标方差大。验证集至少要有几百条样本指标才稳定。如果数据实在少用交叉验证把每个fold的结果平均。6. 从训练到部署的工程化收尾6.1 模型导出与推理性能训练完的模型要部署最省事的方式是导出成TorchScript。model.eval() example torch.randn(1, in_channels, seq_len) traced torch.jit.trace(model, example) traced.save(signal_transformer.pt)导出的模型不依赖训练代码加载后可以直接推理推理速度比Python层的模型快20%到40%。需要注意的是torch.jit.trace对控制流敏感如果你的forward里有if分支或者动态形状trace会固定成一种路径得改用torch.jit.script。如果部署环境是CPU可以用torch.quantization做动态量化模型体积减半推理速度提升1.5到2倍精度损失通常在1个百分点以内。对信号分类这种任务量化后的精度基本够用。6.2 在线推理的预处理一致性部署阶段最容易出问题的不是模型而是预处理不匹配。训练时用的标准化参数、窗口长度、重采样方式推理时必须一模一样。我的做法是把这些参数和模型权重一起打包保存推理时从同一个配置文件读取。artifact { model_state: model.state_dict(), mean: train_mean, std: train_std, seq_len: seq_len, label_map: label_map } torch.save(artifact, deploy.pt)这样一来无论谁部署、在哪部署只要加载这个artifact预处理和模型就是绑定的不会出现训练和推理两套逻辑的问题。这个习惯帮我省了很多排查时间。6.3 一个实用的调参顺序建议最后分享我实践下来的调参顺序按收益从大到小排列先固定d_model128, nhead8, num_layers4跑通全流程确认没有bug。调窗口长度从256、512、1024各跑一遍选验证F1最高的。调dropout和weight_decay解决过拟合问题。调层数和d_model在数据量允许的范围内微调。最后才调学习率和调度器前面几步对了学习率的影响相对小。很多人一上来就调学习率其实窗口长度和数据划分的影响比学习率大得多。先把前面几项定下来后面的调参才有意义。另外可视化的东西别省。混淆矩阵、各类别的PR曲线、注意力权重的heatmap这些图能告诉你模型到底在关注序列的哪些位置。我做过一个心电分类的任务注意力权重可视化后发现模型一直在看窗口开头的几十个点而那部分是切窗时的填充区域——这就是预处理引入的伪影靠看指标是看不出来的。整套流程走下来一维信号分类用Transformer并不比CNN复杂多少但在处理长程依赖上有天然优势。真正的门槛在数据划分和预处理的一致性上这两块做扎实了模型本身的调参反而是最省心的环节。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑