深度学习课程设计:从CNN、GRU到Transformer的文本分类实战与避坑
简介这是一份北京邮电大学22级人工智能专业《神经网络与深度学习》课程设计的完整资料包面向需要系统复现图像描述项目的深度学习学习者或准备相关课设的学生。项目围绕时尚图片自动生成自然语言描述展开基于DeepFashion-MultiModal数据集完整覆盖从CNNGRU基线、引入GloVe词向量、注意力机制到Transformer编解码器再到Qwen2-VL大模型微调的技术演进路径并对比了区域特征与全局特征两种图像编码方式以及自注意力、交叉注意力等关键模块设计。压缩包共76个文件以28个py源码为核心涵盖数据预处理、模型搭建、训练、预测和打分全流程16个json保存了不同模型的生成描述、词映射和评测输出另有9个md、2个pdf及ipynb笔记包含开题、中期、结题报告和详细说明文档整体仅19.35MB。资源按模型类型分目录整理并提供不同数据集上的测试脚本已有87人学习下载适合用来对照学习模型演进路线比较自训练模型与预训练大模型在BLEU、METEOR、ROUGE、CIDEr四种指标下的效果差异。1. 这个课程设计的源码主线CNN、GRU到Transformer到底在让你练什么北邮人工智能专业的神经网络与深度学习课程设计最终交付形态多半是一个装满源码和数据的工程包里面从最基础的CNN、GRU一路做到Transformer。第一次拿到这种工程的人最容易懵三个模型做的是同一类任务为什么非要把三种结构都做一遍这篇笔记就沿着这条主线讲清楚——数据怎么喂、CNN和GRU怎么搭、Transformer的哪些细节最容易翻车以及最终如何把三个模型对比成一张能拿得出手的实验表。适合正在被课程设计卡住、手里有数据集但不知道怎么组织代码的人也适合已经跑通基础模型、想补齐Transformer实现细节的熟手。2. 数据集与预处理跑通模型前先解决的三个问题课程设计的大部分时间其实不在模型而在数据入口。文件读不进来、标签和文本对不上、padding错了导致模型成绩忽高忽低这些都比模型结构本身更折磨人。我一般先把数据入口做成三个独立的小模块Dataset读取、collate_fn动态padding、数据集切分。这三个点只要有一个没想清楚后面三个模型都会跟着出问题。2.1 标签文件怎么读从原始CSV到PyTorch Dataset常见的数据集格式是CSV两列text和label。文本分类是CNN、GRU、Transformer三者都能跑的最小任务所以我下面都用它举例。最省事的做法是用HuggingFace的Tokenizer配合PyTorch的Dataset封装让每个样本返回input_ids、attention_mask和label三个Tensor。# dataset.py —— 把原始CSV封装成PyTorch Dataset import pandas as pd import torch from torch.utils.data import Dataset class TextClsDataset(Dataset): def __init__(self, csv_path, tokenizer, max_len128): self.data pd.read_csv(csv_path) self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.data) def __getitem__(self, idx): row self.data.iloc[idx] text str(row[text]) label int(row[label]) encoded self.tokenizer( text, max_lengthself.max_len, paddingFalse, # 先不pad交到collate_fn统一处理 truncationTrue, return_tensorspt ) return { input_ids: encoded[input_ids].squeeze(0), attention_mask: encoded[attention_mask].squeeze(0), label: torch.tensor(label, dtypetorch.long) }这里的关键是paddingFalse。每个样本保持原始长度返回到DataLoader里再用collate_fn统一padding。这样做的好处是短样本不用被硬塞到128的定长里训练速度快一点模型也不会在满屏PAD里浪费注意力。attention_mask是Tokenizer自动生成的长度和input_ids一致真实token位置是1PAD位置是0。这个mask后面会一直用到。如果不想依赖HuggingFace的Tokenizer也可以基于训练集自己建词表按词频排序取前V个词配一个word2idx字典把句子拆成token id序列。注意自己建词表时一定要把pad固定在0号位否则padding_value和embedding的padding_idx对不上。2.2 动态padding与collate_fnCNN、GRU、Transformer对输入格式的三种要求CNN理论上可以接受变长输入但PyTorch在batch计算时要堆成张量所以同一batch必须等长。GRU是循环网络按时间步推进虽然支持pack_padded_sequence处理变长但排序和还原顺序在代码里很容易写错。Transformer对长度最严格因为自注意力会把PAD位置也纳入计算必须靠attention_mask把这些位置盖住。与其三个模型各写一套batch逻辑不如统一用动态padding。也就是说只在每个batch内部把样本pad到该batch的最大长度而不是全局固定成max_len。# collate_fn.py —— 动态padding避免全局固定max_len from torch.nn.utils.rnn import pad_sequence def collate_fn(batch): input_ids [item[input_ids] for item in batch] attention_mask [item[attention_mask] for item in batch] labels [item[label] for item in batch] input_ids pad_sequence(input_ids, batch_firstTrue, padding_value0) attention_mask pad_sequence(attention_mask, batch_firstTrue, padding_value0) labels torch.stack(labels) return { input_ids: input_ids, attention_mask: attention_mask, label: labels }pad_sequence的batch_firstTrue表示输出形状是[B, L, D]。padding_value必须和Tokenizer的pad_token_id一致对BERT类Tokenizer来说是0对自建词表也一样。attention_mask对应的padding_value是0。注意如果用HuggingFace的Tokenizer且前面没写paddingFalse那你拿到手的所有input_ids已经等长collate_fn里的pad_sequence就完全没意义了。这是新手最常见的无效操作预处理里已经pad到128collate_fn又pad一次浪费算力还混淆逻辑。2.3 训练集/验证集切分时序数据别用随机切分噪声数据集上随机切分没问题。但课程设计里很多同学会拿医疗日志、传感器时序、评论带时间戳的数据这时如果直接train_test_split(random_state42)时间靠后的样本会混进训练集验证集指标虚高答辩时一追问就露馅。两个场景分别处理。普通分类任务按标签分层切分保证每个类在训练集和验证集里的比例一致时序/日志类数据按时间戳前向切分前80%训练后20%验证。from sklearn.model_selection import train_test_split # 场景1普通分类任务按标签分层 train_df, val_df train_test_split( df, test_size0.1, stratifydf[label], random_state42 ) # 场景2时序/日志数据按时间前向切分不随机 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx] val_df df.iloc[split_idx:]很多人会忽略stratify这个参数。类别不均衡时随机切分很容易让某个类在验证集里一个样本都没有模型预测时完全偏到多数类。前向切分还要注意一定先按时间排序再切不要切完再sort那样等于白切。 这一章属于“看着简单做错全崩”的部分。数据入口不干净后面三个模型的差异都说不清楚因为成绩差可能不是模型结构造成的而是数据切分或padding策略造成的。3. 用CNN提取局部特征用GRU接手序列关系两个基础模型的实现课程设计里要求先做基础模型很常见的是CNN和GRU甚至要求把两者组合起来。CNN卷积神经网络擅长抓局部n-gram特征GRU擅长按时间步建模序列关系。两者不是替代关系而是互补关系。下面按“单模型实现、组合实现”的顺序展开。3.1 CNN卷积神经网络kernel_size、通道数与池化的选择逻辑文本CNN的输入是embedding后的向量序列。Conv1d在长度维度上滑动卷积核等价于提取连续的n-gram特征。一般把embedding后的张量从[B, L, D]转成[B, D, L]因为PyTorch的Conv1d默认输入通道在第二维。# cnn_encoder.py —— 基础CNN文本编码器 import torch from torch import nn class CNNEncoder(nn.Module): def __init__(self, vocab_size, embed_dim128, num_filters128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.conv nn.Conv1d(embed_dim, num_filters, kernel_size3, padding1) self.relu nn.ReLU() self.pool nn.AdaptiveMaxPool1d(1) def forward(self, input_ids, attention_mask): x self.embedding(input_ids) # [B, L, D] x x.transpose(1, 2) # [B, D, L] x self.relu(self.conv(x)) x self.pool(x).squeeze(-1) # [B, num_filters] return x参数逻辑kernel_size3配合padding1卷积不改变序列长度最后用AdaptiveMaxPool1d(1)把所有位置压缩成一个向量。num_filters128是通道数也就是卷积核数量一般课程设计里128够用再大就是纯堆参数量。用AdaptiveMaxPool1d而不是自定义窗口长度的MaxPool1d是为了省去根据序列长度手算窗口的麻烦。这里有一个多数教程不提的边界MaxPool是对整个序列取最大值如果PAD位置恰好是embedding向量里的较大值会被误当成特征。所以文本CNN要么用padding_idx0让PAD embedding全是0要么先对padding位置做mask再池化。课程设计里图省事90%的人都直接用padding_idx0实际也够用但答辩被问到时需要能讲清楚。3.2 GRU编码器为什么文本和时序场景里GRU比LSTM更省心LSTM有两个隐状态GRU只有一个参数少了约四分之一。LSTM的三个门控和GRU的两个门控在中等规模数据上效果差距很小GRU训练更快也更不容易在小数据集上过拟合。对课程设计来说GRU是比LSTM更稳的起点。GRU编码器的常见实现有两种取最后时刻的隐状态或者对所有时刻的隐状态做masked mean pooling。取最后一个时刻在理论上简单但很多人会忽略pack_padded_sequence导致最后一个时刻实际是PAD位置的信息。我更推荐用mean pooling配合attention_mask逻辑直观且不用处理变长排序。# gru_encoder.py —— 用masked mean pooling代替pack_padded_sequence from torch import nn class GRUEncoder(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_size256, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.gru nn.GRU(embed_dim, hidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.dropout nn.Dropout(dropout) def forward(self, input_ids, attention_mask): x self.embedding(input_ids) # [B, L, D] out, _ self.gru(x) # [B, L, hidden_size] mask attention_mask.unsqueeze(-1).float() seq_len mask.sum(dim1, keepdimTrue).float() pooled (out * mask).sum(dim1) / seq_len return self.dropout(pooled)attention_mask.unsqueeze(-1)把mask从[B, L]变成[B, L, 1]和out做乘法后PAD位置的隐状态变成0。除以seq_len是求真实token的均值。注意seq_len不能为0如果某个样本被截断成全PAD会除0报错实际训练数据里不太会出现但代码健壮性起见可以加一个seq_len.clamp(min1)。如果显存紧张GRU部分还是建议用pack_padded_sequence省掉PAD位置的所有计算。但pyTorch的pack要求序列按长度降序传入而且输出后要按原始索引恢复顺序。这个排序-压缩-解压的过程是GRU编码器里最容易被写反的地方。课程设计阶段masked mean pooling的实现代价更低也更容易debug。3.3 把CNN和GRU接成一条前向传播链特征拼接不是唯一方案CNN提取局部n-gramGRU建模全局顺序常见做法是两者并行最后把特征拼起来。也可以用CNN输出作为GRU输入做过两层串行结构。串行结构的问题是CNN会把序列压短不利于GRU看到完整上下文并行结构在两个分支上各做各的最后拼接逻辑更清楚。# cnn_gru.py —— 并行CNN GRU分类器 class CNNGRUClassifier(nn.Module): def __init__(self, vocab_size, num_classes, embed_dim128, cnn_filters128, hidden_size256, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) # CNN分支 self.convs nn.ModuleList([ nn.Sequential( nn.Conv1d(embed_dim, cnn_filters, kernel_sizek, paddingk // 2), nn.ReLU(), nn.AdaptiveMaxPool1d(1) ) for k in (1, 3, 5) ]) # GRU分支 self.gru nn.GRU(embed_dim, hidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.dropout nn.Dropout(dropout) self.fc nn.Linear(cnn_filters * 3 hidden_size, num_classes) def forward(self, input_ids, attention_mask): x self.embedding(input_ids) # [B, L, D] # CNN分支1,3,5三种窗口并行 xt x.transpose(1, 2) conv_features [conv(xt).squeeze(-1) for conv in self.convs] conv_feature torch.cat(conv_features, dim-1) # GRU分支masked mean pooling out, _ self.gru(x) mask attention_mask.unsqueeze(-1).float() seq_len mask.sum(dim1, keepdimTrue).float() gru_feature (out * mask).sum(dim1) / seq_len feature torch.cat([conv_feature, gru_feature], dim-1) return self.fc(self.dropout(feature))kernel_size取1、3、5对应unigram、bigram、trigram三种窗口。每种窗口输出cnn_filters128维三个窗口拼起来是384维。GRU输出256维。拼接后特征640维最后接一个全连接层参数量可控不会直接过拟合。 这里的nn.ModuleList只是把三个卷积模块装进一个list方便用for循环依次执行不会影响模型梯度。如果你用普通的Python list卷积模块会注册不到模型里训练时参数不会更新。这是PyTorch里一个常见翻车点自定义Module的__init__里所有子模块必须被nn.Module容器持有。4. 用Transformer替换GRU位置编码、多头注意力和掩码的实现细节CNN和GRU跑通之后替换成Transformer是这次课程设计里最考验理解的一步。Transformer本身不是模型而是一个特征提取器结构核心是自注意力。自注意力机制没有递归所以它处理长序列的能力强但它天然不关心词的先后顺序必须由位置编码把“顺序”信息补回去。4.1 位置编码位置信息怎么算为什么Transformer必须自己加顺序RNN是天然按时间步读取的CNN靠卷积核的窗口位置也能隐式感知局部顺序。而self-attention在计算两个token的相关性时只是两个向量做点积完全不看距离和方向。所以必须把位置信息编码成向量加到embedding上。常见做法有两种学习式位置编码和固定正弦位置编码。BERT用的是学习式但课程设计里我更推荐原始Transformer的固定正弦编码。它的好处是能在超过训练长度时仍然给出合理的位置向量学习式编码一旦超过max_len就只有截断。# positional_encoding.py —— 固定正弦位置编码 import torch import math def get_sinusoidal_position_encoding(max_len: int, d_model: int): pe torch.zeros(max_len, d_model) pos torch.arange(max_len).unsqueeze(1).float() # [max_len, 1] div_term torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(pos * div_term) # 偶数维用sin pe[:, 1::2] torch.cos(pos * div_term) # 奇数维用cos return pe.unsqueeze(0) # [1, max_len, d_model]pos是句子中的位置div_term让不同维度的频率递减。第0维频率最高越往后频率越低。这样每个位置得到一个唯一向量两个位置的向量差能反映它们的相对距离。注意这里的pe不需要梯度因为它只参与加法不参与训练。在模型forward里直接用它即可。4.2 多头注意力与两种掩码padding mask和attention mask别混用Transformer分类器用Encoder结构这里的自注意力是双向的每个token都能看到前后所有token所以不需要因果mask。真正要关心的是src_key_padding_mask它的含义是哪些位置是PAD不参与注意力计算。PyTorch的nn.TransformerEncoderLayer里src_key_padding_mask为True的位置会被忽略。很多同学把attention_mask直接传进去发现模型不收敛就是因为真值表示的含义反了。# transformer_encoder.py —— 基于nn.TransformerEncoder的分类器 from torch import nn class TransformerEncoderClassifier(nn.Module): def __init__(self, vocab_size, num_classes, d_model128, nhead4, num_layers2, dim_feedforward256, dropout0.3, max_len128): super().__init__() self.embedding nn.Embedding(vocab_size, d_model, padding_idx0) self.pos_encoding get_sinusoidal_position_encoding(max_len, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwarddim_feedforward, dropoutdropout, activationrelu, norm_firstTrue, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layers) self.dropout nn.Dropout(dropout) self.fc nn.Linear(d_model, num_classes) def forward(self, input_ids, attention_mask): x self.embedding(input_ids) x x * math.sqrt(self.embedding.embedding_dim) x x self.pos_encoding[:, :x.size(1), :].to(x.device) causal_mask None # 分类任务用Encoder不需要时序mask padded_mask (attention_mask 0) # True表示忽略该位置 out self.encoder(x, maskcausal_mask, src_key_padding_maskpadded_mask) pooled out.mean(dim1) # [B, d_model] return self.fc(self.dropout(pooled))norm_firstTrue表示先LayerNorm再进入注意力层这个设置在Transformer里普遍比后归一化更容易训练稳定。batch_firstTrue让输入输出都是[B, L, D]不然就要按[L, B, D]传入。x x * math.sqrt(d_model)是从原始Transformer论文里沿用的做法。embedding向量的L2范数随d_model增大乘上根号d_model让embedding和位置编码的尺度匹配。这个细节写在很多论文复现里但PyTorch官方示例里不一定会出现不调的话小数据上也能跑但收敛会更慢。4.3 用nn.TransformerEncoder组装分类器PyTorch自带层的边界在哪PyTorch从1.8开始提供了比较完整的TransformerEncoderLayer和TransformerEncoder。大多数人不需要手动实现多头注意力除非课程设计要求从零实现。自带的Layer已经把QKV线性变换、多头拆分、缩放点积注意力、残差和LayerNorm封装好了代码清爽很多。自带的边界也很明确第一次用nn.TransformerEncoderLayer时最容易踩的是参数不匹配。d_model必须能被nhead整除128/4没问题但如果d_model100、nhead4就会直接报错。另一个是dim_feedforward默认是2048对课程设计的数据量来说过大我一般改成256或512否则模型参数爆炸小数据集直接过拟合到训练集上。# 查看Transformer分类器的参数量确认不是“瞎堆参数” def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) model TransformerEncoderClassifier(vocab_sizelen(vocab), num_classes2) print(fTrainable params: {count_parameters(model):,})如果不想把整个Transformer当黑匣子可以打印self.encoder.children()看每一层结构。PyTorch的TransformerEncoder内部是多个TransformerEncoderLayer的堆叠每个Layer内部有self_attn、linear1、linear2和两个norm。答辩时被问到“Transformer层的内部结构”能画出这个就足够证明你是动手搭过的而不是只调了包。5. 训练与评估避坑参数、损失和指标里最容易翻车的五个点模型本身能跑通只算完成30%剩下70%在训练和评估。这一章的五个坑是我认为课程设计里出现频率最高、也最不好定位的。每一条都按“现象、原因、解决”写照着排查能省很多时间。5.1 学习率与warmupTransformer对学习率敏感不像CNN那样给个1e-3就行现象CNN用1e-3能正常收敛换成Transformer后loss一开始就在一个高位震荡甚至前几个step直接变成NaN。 原因Transformer通过LayerNorm和残差连接放大梯度在Adam优化器下初始步长太大。尤其使用warmup不充分时网络在早期还在调整Embedding尺度已经被大学习率推离了正常搜索区域。 解决降学习率到1e-4或5e-5并加线性warmup。前几个step用很小的学习率预热之后逐步升高到峰值再线性衰减。import math from torch.optim import AdamW from torch.optim.lr_scheduler import LambdaLR def get_linear_warmup_scheduler(optimizer, warmup_steps200, total_steps2000): def lr_lambda(step): if step warmup_steps: return (step 1) / warmup_steps progress (step - warmup_steps) / (total_steps - warmup_steps) return max(0.0, 1.0 - progress) return LambdaLR(optimizer, lr_lambda) optimizer AdamW(model.parameters(), lr2e-4) scheduler get_linear_warmup_scheduler(optimizer, warmup_steps200, total_steps2000) # 每个batch更新后调用scheduler.step()CNN和GRU不必要warmup也能收Transformer强烈建议要。warmup_steps一般取总训练步数的10%比如总共2000步前200步做预热。初期warmup不够时nn.TransformerEncoder的梯度范数能比CNN高出两个量级这不算玄学是实打实的训练稳定性问题。5.2 类别不均衡准确率容易骗人换macro-F1看现象验证集准确率到了0.93看起来很好但打印出分类报告才发现某个类别完全没预测对。原因是数据里90%是负类10%是正类模型只要永远输出负类就有90%准确率。 原因损失函数用普通CrossEntropyLoss没有调整类别权重评估指标只看accuracy。 解决损失函数给少数类更高的权重评估指标换成macro-F1或AUC。课程设计里最稳妥的组合是CrossEntropyLoss(weightclass_weights)加sklearn.metrics.classification_report。from sklearn.metrics import f1_score # 训练时在loss初始化里传入类别权重 class_counts train_df[label].value_counts().sort_index().to_numpy() class_weights 1.0 / torch.tensor(class_counts, dtypetorch.float) class_weights class_weights / class_weights.sum() criterion nn.CrossEntropyLoss(weightclass_weights.to(device)) # 评估时用macro-F1而不是accuracy preds torch.cat(all_preds).cpu().numpy() labels torch.cat(all_labels).cpu().numpy() print(macro-F1:, f1_score(labels, preds, averagemacro))注意class_weights不能直接等于1/class_counts就完事最好归一化否则会改变loss的量级引起学习率不匹配。答辩时常被问“为什么用macro-F1不用accuracy”标准回答是在意每个类别上的平均表现而不是被多数类带偏。5.3 模型保存与Early Stopping最优模型不等于最后一个epoch现象训练曲线显示train loss一直下降val loss在第6个epoch开始上升。最后拿第10个epoch的模型去测试效果比第6个差。 原因没有保存验证集上最好的模型而是想当然地认为“epoch越多越好”。这是过拟合最常见的一种表现。 解决保存验证集macro-F1最高时的模型而不是训练结束时的模型。best_f1 0.0 for epoch in range(epochs): train_loss train_one_epoch(model, train_loader, criterion, optimizer) val_f1 evaluate(model, val_loader) if val_f1 best_f1: best_f1 val_f1 torch.save({ model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), epoch: epoch }, best_model.pt)每轮评估完把最优模型直接覆盖为best_model.pt。同时保存optimizer_state是为了万一要继续训练时能恢复不至于只能从头再来。如果连续5个epoch没有提升可以提前结束训练这就是Early Stopping。这个操作相当于给自己留后悔药是课程设计里最容易被忽略的一环。5.4 复现性固定随机种子但还要关掉cudnn benchmark现象同一份代码、同一个数据集上午跑和下午跑最终分数差0.5到1个百分点。有人会把这当成“模型随机性”然后不了了之。 原因PyTorch默认使用非确定性算子数据加载器开启多进程后顺序也不完全固定。cudnn的benchmark模式还会针对不同卷积自动选择最快算法导致同一输入在不同时刻的执行顺序不一致。 解决设置随机种子并关闭cudnn benchmark同时给DataLoader指定生成器种子。import torch import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)固定种子不是做实验的终点。一次完整的对比实验建议跑3个种子取均值和标准差。答辩时更好看的不是“准确率0.95”而是“三个随机种子下macro-F1均值0.93标准差0.01”。这样别人不会因为一次跑分高低来质疑你。5.5 显存溢出梯度累积把batch size平摊到4步现象用Transformer之后显存频繁报OOM。CNN时batch size设64没事Transformer同样设64直接爆。 原因自注意力的计算和存储复杂度是O(n²)序列长度和batch size一起涨显存占用是平方级的。kernel_size为5的CNN窗口再多显存占用也只随序列长度线性涨。 解决降低batch size并使用梯度累积达到等效大batch的效果。accum_steps 4 optimizer.zero_grad() for step, batch in enumerate(train_loader): outputs model(batch) loss criterion(outputs, batch[label]) / accum_steps loss.backward() if (step 1) % accum_steps 0: optimizer.step() optimizer.step() # 如果用了scheduler在这里更新 scheduler.step() optimizer.zero_grad()梯度累积的原理很简单每4个小batch的梯度累加在一起攒够后再更新一次参数等效于batch size从16变成64但峰值显存还是16的占用。注意loss要除以accum_steps否则累计4次后的梯度是单次梯度的4倍学习率等效被放大了Transformer本来就对学习率敏感这里很容易无声地翻车。6. 从跑通到能答辩用消融实验把三个模型对比成一张表模型都跑通之后课程设计最重要的收尾工作是做对比实验。不要只给一行准确率而是把CNN、GRU、Transformer的参数量、训练时间、macro-F1全部列出来形成一张能讲述完整故事的实验表。模型参数量embed_dim128训练耗时macro-F1示例CNN约0.7M最快0.88CNN GRU约1.5M中等0.90Transformer2层约2.0M最慢0.91这张表的关键不是数值本身而是“趋势”从CNN到Transformer效果提升有限但参数量和训练成本明显上升。你组的答辩老师最常问的一句就是“既然Transformer效果这么好为什么实际工业场景里还是有很多RNN和CNN结构”答案就在这张表里小数据上Transformer的优势不够覆盖它的训练和部署成本。你要做的就是把这种权衡讲出来。更耐问的是单变量消融实验。选一个变量只改它其他全部不变。比如在Transformer上做三组实验有位置编码、没有位置编码、位置编码替换成可学习编码。这样的实验比直接对比三个模型更能证明你理解了Transformer的每个部件。我自己的习惯是提前把验证集最优模型单独放到一个固定目录不覆盖、不重命名防止答辩前一天找不到能复现最优结果的权重。等到写报告时再把每个模型的三个随机种子结果整理到表格连同预测错误的样本一起打印出来。这些细节不会增加模型的涨点但能让你在答辩时多一份底气希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →