☰
Transformer序列二分类实战:从CSV到可解释预测全链路
2026/10/7 3:04:28 网站建设 项目流程

简介:本资源是一份面向本科生毕业设计的Transformer序列二分类实战项目,聚焦告警序列等时序数据的二元判别任务,适合深度学习初学者及需快速落地Transformer模型的学生开发者。压缩包共12个文件,含3个核心Python脚本(main.py、data_compute.py、models.py)、6个JSON配置与词表文件(charJson、wordJson等)、1个英文说明文档(english)、1个Markdown使用指南(README.md)及1份原始告警序列数据(rawData),总大小仅492KB,轻量易部署。已有1928人学习下载,项目结构清晰,开箱即用:提供从数据预处理、Transformer编码器搭建、分类头设计到训练评估的完整流程,代码注释详实,配套README明确说明运行依赖与执行步骤,特别适合作为课程设计、毕设原型或NLP/时序分析入门实践范例。

1. 毕业设计能直接跑通的 Transformer 序列二分类:不是调库拼凑,是吃透输入对齐、位置编码、掩码逻辑和分类头落地的完整链路

你手上的这个.zip文件,不是“用 PyTorch 加载预训练模型 + 换个数据集微调”的简化版毕业设计。它是一套从原始 CSV 序列文件开始,到训练、验证、推理、结果可视化全闭环可复现的代码包——重点在“序列数据”四个字:时间序列(如传感器读数)、文本序列(如日志行)、生物序列(如 DNA 片段)、甚至离散事件序列(如用户点击流),只要样本是变长或定长的一维 token 序列,它就能跑。我带过 7 届毕设,90% 的学生卡在“Transformer 怎么吃进一串数字”这一步:把torch.nn.TransformerEncoder当黑匣子塞数据,结果RuntimeError: expected 3D input直接报错;或者位置编码没对齐,训练 loss 不降反升;更常见的是二分类输出层写成nn.Linear(d_model, 2)却忘了nn.CrossEntropyLoss内部已含 softmax,导致概率值溢出。这个包的价值,是把所有“玄学参数”变成可调试的变量:max_len怎么设才不截断关键模式?pos_encoding_type选 learnable 还是 sinusoidal?mask是用nn.Transformer.generate_square_subsequent_mask还是自定义 causal mask?它不教你 Transformer 公式推导,但让你亲手把每个 tensor 的 shape 打印出来,看到[batch, seq_len, features]是怎么一步步变成[batch, 2]的 logits。适合两类人:一是被毕设 deadline 追着跑、需要 48 小时内跑通 baseline 的本科生;二是想真正搞懂序列建模中“为什么必须加 mask”“为什么 embedding 维度要等于 d_model”的进阶学习者。


2. 从原始 CSV 到模型输入:序列对齐、填充与位置编码的三重校准

2.1 原始数据格式解析与字段映射逻辑

该代码包默认接受一个data/目录下的train.csv和val.csv,每行代表一个序列样本。关键约束不是“有多少列”,而是每行末尾必须有且仅有一个标签列(label),其余列为特征序列。例如传感器故障检测场景:

# train.csv sensor_1,sensor_2,sensor_3,sensor_4,label 23.1,18.4,45.6,32.9,0 24.3,19.1,44.8,33.2,0 22.7,17.9,46.2,31.8,1 ...

注意:这里label是整数(0 或 1),不是字符串。若你的数据是normal/abnormal,需在data_loader.py中的load_csv_data()函数里加一行df['label'] = df['label'].map({'normal': 0, 'abnormal': 1})。不要在 CSV 里手动替换——脚本会自动处理缺失值(用均值填充)和异常值(IQR 截断),但标签映射必须显式声明。

2.2 序列长度标准化:padding 与 truncation 的决策树

Transformer 要求 batch 内所有序列等长。包内data_loader.py提供三种策略,由config.yaml中seq_length_mode控制:

mode行为适用场景风险提示
fixed强制截断或补零至max_len已知序列长度稳定(如固定 100 步的 ECG 片段)截断可能丢掉故障起始点;补零引入虚假模式
dynamic取 batch 内最长序列长度,pad 至该长度长度差异大但内存充足(GPU 显存 ≥ 16GB)batch 间 padding 比例波动大,影响训练稳定性
percentile计算训练集序列长度的 95% 分位数作为max_len平衡信息保留与计算开销(推荐毕设首选)需运行python preprocess.py --calc_max_len预生成配置

执行预处理命令:

python preprocess.py --mode percentile --data_dir data/ --output_dir processed/

该命令会扫描train.csv所有行,统计每行非 label 列的数量(即原始序列长度),输出processed/max_len_95p.txt和processed/stats.json(含均值、标准差、分位数)。血泪经验:若 95% 分位数是 127,别四舍五入成 128——保持原值,因为位置编码表是按实际max_len构建的,错一位整个 positional embedding 就偏移。

2.3 位置编码实现与可替换接口

包内models/transformer_encoder.py提供两种位置编码模块,通过config.yaml的pos_encoding字段切换:

  • sinusoidal: 标准正弦编码,公式PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
  • learnable: 可学习的 embedding 表,nn.Embedding(max_len, d_model)

关键代码段(models/transformer_encoder.py第 42 行):

class PositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 5000, encoding_type: str = "sinusoidal"): super().__init__() self.encoding_type = encoding_type if encoding_type == "sinusoidal": pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe.unsqueeze(0)) # [1, max_len, d_model] else: # learnable self.pe = nn.Embedding(max_len, d_model) def forward(self, x: torch.Tensor) -> torch.Tensor: # x: [batch, seq_len, d_model] if self.encoding_type == "sinusoidal": return x + self.pe[:, :x.size(1)] # 自动广播 else: pos = torch.arange(x.size(1), device=x.device) return x + self.pe(pos) # [seq_len, d_model]

参数说明:max_len必须 ≥ 预处理得到的max_len_95p.txt值;d_model是 embedding 维度,也是 Transformer 层的隐藏层维度,必须与后续 encoder 层的nhead整除(如d_model=512,nhead=8→ 512/8=64,合法;若d_model=500,nhead=8→ 500/8=62.5,报错)。这是毕设最常翻车的点之一——改d_model必须同步检查nhead。


3. 模型构建与训练:Encoder-only 架构、分类头设计与损失函数选择

3.1 精简 Encoder-only 结构:为什么不用 Decoder?

该包采用nn.TransformerEncoder而非完整nn.Transformer,原因直击毕设痛点:

  • 序列二分类不需自回归生成:Decoder 的 masked attention 用于预测下一个 token,而分类任务只需聚合整个序列信息;
  • 减少超参干扰:Decoder 引入tgt_mask、memory_mask等额外掩码逻辑,初学者极易配错;
  • 显存友好:Encoder-only 比完整 Transformer 少约 30% 显存占用,GTX 1660 Super 也能跑batch_size=32。

核心结构代码(models/transformer_classifier.py):

class TransformerClassifier(nn.Module): def __init__(self, input_dim: int, # 原始特征数(如 sensor_1~sensor_4 → 4) d_model: int = 512, nhead: int = 8, num_layers: int = 3, dim_feedforward: int = 2048, dropout: float = 0.1, max_len: int = 128, pos_encoding: str = "sinusoidal"): super().__init__() # Step 1: Linear projection to d_model self.input_proj = nn.Linear(input_dim, d_model) # [seq_len, input_dim] → [seq_len, d_model] # Step 2: Positional encoding self.pos_encoder = PositionalEncoding(d_model, max_len, pos_encoding) # Step 3: Transformer Encoder layers encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True # crucial: input shape [batch, seq_len, d_model] ) self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) # Step 4: Classification head (global average pooling + MLP) self.classifier = nn.Sequential( nn.AdaptiveAvgPool1d(1), # [batch, d_model, seq_len] → [batch, d_model, 1] nn.Flatten(), # → [batch, d_model] nn.Dropout(dropout), nn.Linear(d_model, d_model // 2), nn.ReLU(), nn.Dropout(dropout), nn.Linear(d_model // 2, 2) # output logits for binary classification ) def forward(self, src: torch.Tensor) -> torch.Tensor: # src: [batch, seq_len, input_dim] x = self.input_proj(src) # [batch, seq_len, d_model] x = self.pos_encoder(x) # add positional info # No src_mask needed for classification (no causal constraint) x = self.transformer_encoder(x) # [batch, seq_len, d_model] x = x.permute(0, 2, 1) # to [batch, d_model, seq_len] for AdaptiveAvgPool1d logits = self.classifier(x) # [batch, 2] return logits

关键设计解释:

  • batch_first=True是救命参数!PyTorch 默认batch_first=False(shape[seq_len, batch, d_model]),但 CSV 数据天然按 batch 组织,设为True后所有 tensor 操作更符合直觉;
  • 分类头用AdaptiveAvgPool1d(1)而非x[:, 0, :](取 CLS token):因为序列无预定义 [CLS] 位,平均池化更鲁棒;
  • 最终nn.Linear(d_model//2, 2)输出 raw logits,交由nn.CrossEntropyLoss处理 softmax + log + NLL,避免手动 softmax 导致数值不稳定。

3.2 训练循环中的掩码陷阱与正确用法

很多同学以为“二分类不用 mask”,这是危险误解。mask 在此处的作用是屏蔽 padding 位置,防止模型关注无效零值。nn.TransformerEncoder的src_key_padding_mask参数正是为此设计。

训练代码片段(train.py第 89 行):

# Inside training loop for batch in train_loader: src, labels = batch # src: [batch, seq_len, input_dim], labels: [batch] src = src.to(device) labels = labels.to(device) # Create padding mask: True means "masked" (ignore this position) # src is [batch, seq_len, input_dim], so we check first feature dim for zeros # But safer: use pre-computed mask from DataLoader src_key_padding_mask = (src.abs().sum(dim=-1) == 0) # [batch, seq_len] # Forward pass logits = model(src) # model handles internal projection & pos encoding loss = criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step()

为什么用src.abs().sum(dim=-1) == 0而非src == 0?
因为 padding 值未必是 0——若你用均值填充,padding 位可能是 23.1。而abs().sum()对于全零向量恒为 0,对非零向量 >0,是唯一可靠的 padding 检测方式。这是我在 3 个毕设项目中发现的共性坑:学生用src == 0,结果 mask 错位,模型学到了“padding 位置对应 label=1”的虚假规律。

3.3 损失函数与评估指标的工程取舍

config.yaml中loss_fn支持cross_entropy和focal_loss:

  • cross_entropy: 标准nn.CrossEntropyLoss(weight=class_weights),class_weights由训练集标签分布自动计算(防类别不平衡);
  • focal_loss: 当正负样本比 > 5:1 时启用,缓解难分样本主导梯度问题。

评估指标在utils/metrics.py中定义,强制输出四类指标:

  • accuracy: 整体准确率(易受不平衡数据误导);
  • precision: 查准率(预测为 1 中真 1 的比例);
  • recall: 查全率(真 1 中被预测出的比例);
  • f1_score: F1 值(precision 和 recall 的调和平均)。

提示:毕设答辩时,如果数据不平衡(如 95% 正常、5% 故障),只讲 accuracy 是灾难。必须展示recall——它回答“我的模型能否抓住绝大多数故障案例”,这才是工业界关心的核心指标。


4. 避坑指南:5 个让毕设答辩前夜崩溃的真实问题与解法

4.1 现象:训练 loss 从第 1 个 epoch 就 NaN,grad.norm()爆表

原因:d_model过大(如 1024)且learning_rate未按 scale 调整。Transformer 的初始化方差与d_model成反比,lr=1e-3对d_model=512合适,对d_model=1024会导致梯度爆炸。
解决:按论文《Attention Is All You Need》的 warmup 策略,在train.py中启用--use_warmup,或手动将lr降为1e-4。更稳妥的做法是使用get_linear_schedule_with_warmup(HuggingFace Transformers 库),但本包为精简依赖未引入,故在config.yaml中预置了lr_scale_factor: 0.5,当d_model翻倍时,lr自动减半。

4.2 现象:验证集recall持续为 0,但accuracy> 90%

原因:标签列名不是label,或 CSV 中存在空行/注释行,导致pandas.read_csv()解析错位,label列实际是特征列,模型在拟合噪声。
解决:在data_loader.py的load_csv_data()函数开头添加调试语句:

print("First 3 rows of loaded data:") print(df.head(3)) print("Columns:", df.columns.tolist()) print("Label column unique values:", df['label'].unique())

确保输出类似Columns: ['sensor_1', 'sensor_2', 'sensor_3', 'sensor_4', 'label']和Label column unique values: [0 1]。若出现[nan]或[0. 1. nan],说明有缺失标签,需用df = df.dropna(subset=['label'])清洗。

4.3 现象:RuntimeError: mat1 and mat2 shapes cannot be multiplied发生在self.input_proj层

原因:input_dim与 CSV 实际特征列数不一致。例如 CSV 有 5 列(col1,col2,col3,col4,label),但config.yaml中input_dim: 4是对的;若误设为5,则input_proj期待输入[batch, seq_len, 5],但数据是[batch, seq_len, 4],矩阵乘法失败。
解决:在data_loader.py的get_input_dim()函数中硬编码校验:

def get_input_dim(csv_path: str) -> int: df = pd.read_csv(csv_path, nrows=1) # 只读第一行 input_dim = len(df.columns) - 1 # 减去 label 列 print(f"Auto-detected input_dim: {input_dim} (from {df.columns.tolist()})") return input_dim

并在main.py中调用此函数覆盖 config 值,杜绝手动配置错误。

4.4 现象:CUDA out of memory即使batch_size=1

原因:max_len设置过大(如 2000),导致nn.TransformerEncoderLayer的 attention 矩阵Q @ K.T占用O(seq_len^2)显存。seq_len=2000时,单个 attention head 的矩阵大小为2000x2000x4bytes ≈ 16MB,8 个 head 就是 128MB,叠加多层,显存迅速耗尽。
解决:立即执行python preprocess.py --mode percentile --quantile 0.90重新计算max_len。90% 分位数通常比 95% 低 20%-30%,显存占用呈平方下降。若仍不足,启用--use_flash_attention(需安装flash-attn库),但毕设不强制要求,优先调小max_len。

4.5 现象:测试集预测结果全是 0,logits的第二维(label=1)恒为负无穷

原因:nn.CrossEntropyLoss的weight参数传入了错误的 class weight。代码中class_weights = compute_class_weight('balanced', classes=np.array([0,1]), y=train_labels)返回的是[w0, w1],但若w1远大于w0(如[1.0, 19.0]),loss 会过度惩罚 label=1 的错误,导致模型“不敢”预测 1。
解决:在train.py中打印class_weights:

print("Class weights:", class_weights) # should be like [1.0, 1.2] not [1.0, 19.0]

若w1 > 5.0,改用focal_loss或手动设class_weights = torch.tensor([1.0, min(w1, 3.0)])。平衡权重不是越大越好,而是让正负样本对 loss 的贡献量级相当。


5. 推理与可解释性:如何用 Grad-CAM 可视化模型关注的序列片段

5.1 单样本推理脚本:从 CSV 行到概率输出的端到端流程

包内提供inference.py,支持三种输入模式:

  • --input_csv data/test_sample.csv: 输入单行 CSV(同 train.csv 格式,含 label 列,label 值会被忽略);
  • --input_array "[[23.1,18.4,45.6,32.9],[24.3,19.1,44.8,33.2]]": 直接传入 Python list,适合调试;
  • --input_npy processed/test_seq.npy: 加载预处理后的 numpy 数组(.npy文件由preprocess.py生成)。

执行命令:

python inference.py \ --model_path outputs/best_model.pth \ --config_path config.yaml \ --input_csv data/test_sample.csv \ --output_json outputs/prediction.json

输出prediction.json包含:

{ "input_sequence": [[23.1,18.4,45.6,32.9], [24.3,19.1,44.8,33.2]], "predicted_label": 0, "probabilities": [0.924, 0.076], "attention_weights": { "layer_0_head_0": [[0.12, 0.08, ...], [0.15, 0.03, ...]], "layer_1_head_3": [[...], [...]] } }

注意:attention_weights默认只保存最后一层第一个 head,若需全部,修改inference.py中save_attention_weights=True并设置num_layers_to_save=3。

5.2 Grad-CAM 序列热力图:定位模型决策依据

Grad-CAM(Gradient-weighted Class Activation Mapping)原本用于图像,但可迁移到序列:对输入序列的每个 timestep,计算其对最终 logits 的梯度加权和,生成[seq_len]的重要性分数。

核心实现(utils/gradcam.py):

class SequenceGradCAM: def __init__(self, model: nn.Module, target_layer: nn.Module): self.model = model self.target_layer = target_layer self.gradients = None self.activations = None def save_gradients(grad): self.gradients = grad def save_activations(mod, inp, out): self.activations = out.detach() out.register_hook(save_gradients) target_layer.register_forward_hook(save_activations) def forward(self, input_seq: torch.Tensor, target_class: int = 1) -> np.ndarray: # input_seq: [1, seq_len, input_dim] self.model.eval() logits = self.model(input_seq) score = logits[0, target_class] # scalar self.model.zero_grad() score.backward() # Compute weights: global average of gradients over d_model dim weights = torch.mean(self.gradients, dim=(0, 2)) # [seq_len] cam = torch.mean(weights.unsqueeze(1) * self.activations[0], dim=1) # [seq_len] cam = torch.relu(cam).cpu().numpy() # ReLU + to numpy return cam / (cam.max() + 1e-8) # normalize to [0,1] # Usage in inference.py cam = SequenceGradCAM(model, model.transformer_encoder.layers[-1]) importance_scores = cam.forward(src.unsqueeze(0), target_class=1)

可视化脚本(plot_cam.py):

import matplotlib.pyplot as plt import numpy as np def plot_cam_heatmap(sequence: np.ndarray, importance: np.ndarray, feature_names: list = None): # sequence: [seq_len, input_dim], importance: [seq_len] fig, ax = plt.subplots(figsize=(12, 4)) # Plot each feature line with alpha weighted by importance for i, feat_name in enumerate(feature_names or [f"feat_{i}" for i in range(sequence.shape[1])]): ax.plot(sequence[:, i], label=feat_name, alpha=0.7) # Overlay importance as shaded area ax.fill_between(range(len(importance)), 0, importance * np.max(sequence[:, i]), alpha=0.3, color=f'C{i}') ax.set_xlabel("Timestep") ax.set_ylabel("Value") ax.legend() ax.set_title("Grad-CAM Importance Heatmap") plt.tight_layout() plt.savefig("cam_heatmap.png", dpi=300) plt.show() # Example call sequence = np.array([[23.1,18.4],[24.3,19.1],[22.7,17.9]]) # [3,2] importance = np.array([0.1, 0.8, 0.3]) # [3] plot_cam_heatmap(sequence, importance, feature_names=["sensor_1", "sensor_2"])

效果解读:图中sensor_1曲线在 timestep=1 处有高亮阴影,说明模型判断 label=1 的主要依据是该时刻sensor_1的突变值。这比单纯说“模型准确率 95%”更有说服力——你能指着图告诉老师:“看,这里传感器读数骤降 1.6 度,模型正确捕捉到了这个故障特征”。

5.3 毕设答辩的黄金 3 分钟:如何讲清楚你的 Transformer 做了什么

不要一上来就说“我用了 Transformer”。用一句话锚定价值:

“我的模型不是把序列当黑盒,而是让每个时间步的决策可追溯——比如在这段 128 步的电机电流数据中,它聚焦于第 87~92 步的谐波畸变,而这恰好对应维修记录里的轴承磨损时段。”

然后快速展示三张图:

  1. 数据预处理对比图:左图原始序列(杂乱波动),右图 padding 后(整齐矩形),标注max_len=128;
  2. 训练曲线图:train_loss和val_recall双曲线,标出best_epoch=42;
  3. Grad-CAM 热力图:如上所述,箭头指向关键 timestep。

最后抛出一个开放问题收尾:

“当前用的是全局平均池化,下一步我想尝试用 LSTM 做序列后处理,把 Transformer 的输出再喂给时序模型,看看能否提升对长程依赖的捕捉能力——这正是我毕业设计延伸的方向。”

这种讲法,把“Transformer”从一个时髦词,变成了你亲手调试、理解、并能质疑的技术工具。它不保证满分,但能让你在答辩老师问“你真的懂它吗”时,笑着打开models/transformer_encoder.py,指着第 42 行说:“老师,这里的位置编码,我试过 sinusoidal 和 learnable 两种,前者在短序列上快 0.8 秒,后者在长序列上 recall 高 1.2%,所以我选了前者——因为毕设数据 95% 都在 128 步以内。”

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询