
简介这是一份面向自然语言处理课程期末大作业的完整项目包覆盖深度学习与NLP常见任务流程适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计参考。压缩包共34个文件约33.12MB核心为两个ipynb代码文件、NLP实验报告docx、说明文档md与英文信息熵参考pdf另含按金庸小说整理的txt语料、预处理后文本、输出结果、git配置及项目工程文件便于直接查看运行效果或在此基础上修改参数复现。已有2071人学习。代码采用参数化编写注释清晰关键步骤包含运行结果适合对照实验报告逐段理解分词、文本处理、语料库信息熵等知识点文档说明与报告可帮助快速梳理大作业框架减少从零搭建的工作量。1. NLP期末大作业到底在考察什么从交了就行到能答辩NLP期末大作业这几个字在很多高校的深度学习课程里意味着一次完整交付一个能跑的深度学习模型、一份源代码、一份文档说明和一份实验报告。我见过太多同学在答辩前夜从GitHub拉一个开源项目改掉变量名就提交结果被老师一句你这个模型的输入是什么形状问得下不来台。这门课真正想验证的不是你能跑通一行python main.py而是有没有把自然语言处理任务从数据清洗、模型选型、训练调参到结果分析这条链路走通。这篇笔记按我自己做课程项目的顺序展开从任务选型到代码落地再到实验报告怎么写和踩坑点给你一条照着走就能交付的路径。2. 先定任务再写代码文本分类、情感分析还是序列标注NLP自然语言处理方向的大作业第一步不是找模型而是定任务。任务选错了后面所有代码都在给自己挖坑。我在帮同学调过多次作业后越来越坚信一个原则任务是拿来展示你对深度学习流程的理解不是拿来做智力比拼的。一个你能完整解释的任务远好过一个你能跑通却说不清的任务。2.1 任务选型文本分类、情感分析、序列标注怎么选先看课程任务书。有些老师会明确指定用RNN或Transformer做一个文本分类有些会给一个宽泛的Neural NLP题目。如果老师给了选择空间最常见的三类任务是文本分类、情感分析、序列标注如命名实体识别。我一般优先推荐情感分析或文本分类原因有三条。第一数据多且好拿。情感分析数据集无论是中文还是英文都非常成熟公开渠道能下载到整理好的CSVlabel就是正负或几个等级洗数据的成本低。第二评估指标简单。准确率、精确率、召回率、F1这些指标在文本分类里含义清晰答辩时老师问为什么F1比准确率低你可以用正负样本不均匀来解释这是现成的加分点。第三baseline模型多。从词袋模型到BiLSTM再到预训练模型你可以轻松搭一个baseline再做一个升级版形成对比实验这正好满足实验报告里实验对比的要求。序列标注像NER虽然看起来更NLP但标注数据难找评估还要处理BIO标签序列匹配一旦分错一个边界整个句子的指标都会波动。期末大作业的时间通常只有两三周我不建议拿序列标注当第一个项目。如果你手里已经有一份标注好的NER数据那就另说但也要想好怎么展示标签层面的错误。选好任务后把任务定义和数据集来源写进README。文档说明不要空泛写清楚输入是一句中文评论输出是0或1类别分布是XX。这能避免答辩时说你文档说明和代码不一致也能帮你理清后面对齐实验报告。2.2 数据准备从原始文本到训练集、验证集、测试集无论是自己爬数据还是找公开数据集拿到原始数据后别急着建模。我会先做这几步统一编码、去除空行、查看类别分布、切分数据集。以中文酒店评论情感分类为例假设你拿到的CSV只有text和label两列label为0和1。第一步用pandas加载并检查分布如果发现某类样本只有几十条后面模型大概率会过拟合。遇到这种数据要么换数据要么在报告里明确说明类别不均衡并使用加权损失。切分数据集是关键操作很多同学只分成train和test然后在训练过程中反复看test上的准确率这是作业里的大忌。测试集应当是最后验证模型泛化能力的考试卷不能用它调参。所以必须再从训练集里切一个验证集用于早停和模型选择。切分时用train_test_split并设置stratify保证正负样本比例在训练、验证、测试三个集合里一致。下面是顺便记录数据统计的小脚本它会同时输出划分后的数据文件。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(data/hotel_reviews.csv, encodingutf-8) df df.dropna(subset[text, label]) df[label] df[label].astype(int) print(df[label].value_counts()) print(句长描述统计) print(df[text].str.len().describe(percentiles[.5, .9, .95])) train_df, test_df train_test_split( df, test_size0.2, random_state42, stratifydf[label] ) train_df, val_df train_test_split( train_df, test_size0.15, random_state42, stratifytrain_df[label] ) train_df.to_csv(data/train.csv, indexFalse) val_df.to_csv(data/val.csv, indexFalse) test_df.to_csv(data/test.csv, indexFalse)这段代码里的random_state固定为42保证每次运行划分结果一致实验报告里写训练集1523条验证集269条时别人重跑能复现。stratify参数让label比例在各集中保持一致。test_size设为0.2再从剩余训练集中取0.15作为验证集整体比例大约是训练集68%、验证集12%、测试集20%。如果你的数据集比较大比如几万条验证集比例还可以再下调到0.1。这里有个容易忽略的坑如果原始数据里label是字符串比如positive/negative需要先映射成0/1否则torch的CrossEntropyLoss会直接报错。我在写成CSV之前就把label转成int后面所有代码都省心。2.3 预处理代码分词、构建词表、生成Dataloader模型吃的是数值张量不是字符串。所以预处理要做四件事分词、去停用词可选、构建词表、把句子pad成等长序列。分词我一般用jieba它很简单适合课程作业里讲清楚分词这一步。这里要特别注意分词和词表构建只能在训练集上做验证集和测试集遇到没见过的词一律映射为 。如果你在划分数据前就用全量数据构建词表等于让模型间接看到了测试集里的词汇分布结果会虚高答辩时容易被追问逻辑漏洞。词表构建我习惯统计词频过滤掉低频词。低频词通常是噪声或错字保留它们会放大embedding层参数矩阵容易过拟合。min_freq设为2或3具体看数据量。数据只有一两万条时设为2够用数据量大时可以设5。词表里必须包含两个特殊符号 用于补齐 用于未知词。padding idx固定为0这在后面模型配置PaddingMask时有用。import jieba from collections import Counter from torch.utils.data import Dataset, DataLoader import torch def build_vocab(texts, min_freq2): counter Counter() for text in texts: counter.update(jieba.lcut(text)) vocab {pad: 0, unk: 1} for word, freq in counter.items(): if freq min_freq: vocab[word] len(vocab) return vocab def encode(text, vocab, max_len100): tokens jieba.lcut(text)[:max_len] ids [vocab.get(w, vocab[unk]) for w in tokens] ids [vocab[pad]] * (max_len - len(ids)) return ids def collate_batch(batch): xs torch.tensor([x for x, _ in batch], dtypetorch.long) ys torch.tensor([y for _, y in batch], dtypetorch.long) return xs, ys class SentimentDataset(Dataset): def __init__(self, df, vocab, max_len100): self.texts df[text].tolist() self.labels df[label].tolist() self.vocab vocab self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): x encode(self.texts[idx], self.vocab, self.max_len) return x, self.labels[idx]构建词表的逻辑是按词频过滤。counter.update(jieba.lcut(text))会逐句分词并累计词频。vocab字典中word映射到索引索引从2开始递增。encode函数先分词截断然后转索引最后用 补齐。这里隐含一个设计如果一句话长度超过max_len只保留前max_len个token。我这儿的max_len取100因为前面句长统计的90分位在80左右100足够覆盖大部分句子。你也可以根据数据调整但max_len不是越大越好过大只会增加计算量并且在padding区域引入不必要的attention噪音。collate_batch里用torch.tensor批量转成long tensor。注意batch里的pad id是0所以后面模型forward中计算的mask就是batch_x ! 0。Dataloader使用时会自动调用collate_batch不写这个函数的话PyTorch默认会尝试把不同长度的list打包导致报错。提示如果你用的是英文数据分词用字符串的split或spaCy都行但中文没有空格分隔必须用分词器。这一步的常见翻车点在后面避坑章节单独细说。3. 模型选型与实现从Embedding到BiLSTM Attention任务和数据定了下一步就是选择深度学习模型。很多教程一上来就上BERT但对期末大作业来说自己搭一个从词向量到循环神经网络的模型反而更容易拿到分。一方面模型的每层你都能解释清楚另一方面调参的空间更大实验报告里可以写对比。3.1 为什么基线用 Embedding BiLSTM Attention先解释模型三个组件的职责。Embedding层把离散的词索引映射成稠密向量这是让模型理解词汇语义的第一层。BiLSTM负责建模上下文两个方向的LSTM拼接让每个位置的隐状态既包含前文信息也包含后文信息。Attention层的作用是告诉模型这句话里哪些词对最终判断更重要它通过可学习打分函数给每个时间步一个权重然后对BiLSTM的输出做加权求和。对于情感分类这类任务Attention的效果可以直接可视化你在实验报告里放一张高权重词的热力图展示房间服务卫生这些词被重点关注是很直观的加分项。为什么不直接上Transformer期末大作业的时间线不适合从头训一个Transformer它需要更大的数据、更复杂的positional encoding以及处理mask的策略。而你用PyTorch搭一个BiLSTM几十行就能跑出结果。如果老师允许用预训练模型你可以把基线换成BERT做对比实验但务必先跑通BiLSTM再升级。这个思路在很多动手深度学习实践中都是默认路径先有一个能跑的baseline再谈优化。配置上我常用的初始参数是embed_dim128hidden_dim128num_layers2。注意num_layers不一定是越多越好期末作业的数据量不大2层往往效果最好再加深很容易过拟合训练时间也翻倍。3.2 用PyTorch实现一个最小可运行的NLP分类模型源代码的组织方式直接影响助教跑你代码的体验。我一般会建一个这样的目录data/放原始和划分后的数据models/放模型定义utils/放分词、词表和评估函数train.py是入口README.md写环境和运行说明。你不一定要严格照搬但至少别把模型定义、训练逻辑和数据清洗全塞进同一个.py文件否则自己后期调参都难受。下面给出模型定义它包含了上述三个组件建议写在models/bilstm_attn.py里。import torch import torch.nn as nn import torch.nn.functional as F class BiLSTMAttn(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers2, num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue) self.attn_score nn.Linear(hidden_dim * 2, 1) self.fc nn.Linear(hidden_dim * 2, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): emb self.dropout(self.embedding(x)) # [B, T, E] out, _ self.lstm(emb) # [B, T, 2H] score self.attn_score(out).squeeze(-1) # [B, T] if mask is not None: score score.masked_fill(mask 0, -1e9) weight F.softmax(score, dim1) # [B, T] ctx torch.bmm(weight.unsqueeze(1), out).squeeze(1) # [B, 2H] logits self.fc(self.dropout(ctx)) return logits这段代码的forward过程是输入x的形状是B×T的索引张量经过Embedding变成B×T×E其中E是embed_dim。lstm层输出out的形状是B×T×(2H)。attn_score是一个线性层把每个位置的隐状态映射成一个实数分数然后softmax变成权重。这里关键的masked_fill操作把序列padding位置的分值替换成-1e9这样softmax后这些位置的权重接近于0避免padding对语义表示产生干扰。ctx是注意力加权后的上下文向量形状B×(2H)。最后经过一个全连接层输出logits。这里要注意nn.LSTM默认初始化在有些场景下会让梯度不稳定但配合Adam优化器通常没问题。如果你的模型loss始终不降可以试试把LSTM的隐藏层初始化换成均匀分布这属于调参玄学后面讲训练时再细说。3.3 损失函数、优化器与评估指标模型输出logits后损失函数用CrossEntropyLoss它是softmax加负对数似然的组合。不要在最后再手动加softmax因为CrossEntropyLoss内部已经做了重复加会导致数值不稳定。为什么不用MSE做分类因为交叉熵对错误分类的梯度更合理且概率输出更自然。对于类别不均衡的数据还可以在CrossEntropyLoss里传入weight给样本少的类别更大惩罚系数。优化器我一般首选Adam学习率从1e-3起调。adam已经有自适应学习率比SGD省心但要配合weight_decay防止大权重过拟合PyTorch里写optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)。评估指标不要只看准确率。情感分析的正负样本可能不均衡准确率高但正类召回率很低。所以我会同时输出recall、precision和F1。这里贴一个简单的评估函数from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score def evaluate(model, loader, devicecuda): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for x, y in loader: x, y x.to(device), y.to(device) mask (x ! 0) logits model(x, mask) preds logits.argmax(dim1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(y.cpu().tolist()) return { acc: accuracy_score(all_labels, all_preds), f1: f1_score(all_labels, all_preds), precision: precision_score(all_labels, all_preds), recall: recall_score(all_labels, all_preds), }evaluate里model.eval()很关键它关闭dropout和batch normalization的训练行为保证验证阶段输出稳定。mask的构造是x ! 0和模型forward里的masked_fill对应。测试集和验证集都用这种方式评估实验报告里的指标就从这里打印出来不要手写在Word里避免数字对不上。4. 训练与调参以及把实验报告写成能答辩的文档模型写好了接下来就是训练循环、过拟合处理和文档整理。这一章很重要因为很多同学的代码跑起来后就不动脑子了最后实验报告像流水账。这里的目标是让训练过程可控、结果可复现实验报告能讲出我调了什么、发生了什么、为什么。4.1 训练循环与学习率设置训练循环本身不难难点在于你用什么节奏去观察它。我习惯每个epoch打印训练loss和验证指标同时把数据存到日志里。下面是最小代码。torch.manual_seed(42) device torch.device(cuda if torch.cuda.is_available() else cpu) model BiLSTMAttn(vocab_sizelen(vocab)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(30): model.train() train_loss 0 for x, y in train_loader: x, y x.to(device), y.to(device) mask (x ! 0) logits model(x, mask) loss criterion(logits, y) optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item() val_metrics evaluate(model, val_loader, device) print(fepoch {epoch:02d} loss {train_loss/len(train_loader):.4f} facc {val_metrics[acc]:.4f} f1 {val_metrics[f1]:.4f})学习率是最重要的超参数。1e-3是我对Adam在文本分类上的默认值如果你的loss发散或严重振荡把lr降到3e-4或1e-4。还有一种常见做法是前几个epoch用较大的学习率后期再衰减。但对于期末作业线性衰减就够了甚至不衰减都行。你可以在实验报告里比较固定lr1e-3和过了10个epoch后lr变为1e-4两种设置。另一个容易忽略的点是gradient clipping在loss.backward()后加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)可以防止LSTM梯度爆炸特别是当你的数据里有很长句子时。训练步数不要拍脑袋。设30个epoch但通过早停来决定真正使用的epoch数这正好引出4.2。4.2 过拟合怎么办Dropout、早停、权重衰减期末作业数据量通常只有几千到几万条BiLSTM参数量却不小所以过拟合是必然的。特征很典型训练loss一直降验证loss先降后升。应对手段有三个按优先级排列。第一个是Dropout。前面的模型定义里已经加了两处dropout分别是embedding输出和attention上下文向量之后。dropout0.5是常见默认值如果验证集表现比训练集差很多可以调到0.6或0.7。注意不要在LSTM层内部再加dropoutPyTorch的nn.LSTM虽然也有dropout参数但小数据上作用有限还影响可解释性。第二个是早停。早停就是当验证集的指标连续几个epoch没有提升时停止训练保留历史最佳模型。实现一个极简早停类class EarlyStopping: def __init__(self, patience3, delta0): self.patience patience self.delta delta self.counter 0 self.best_score None self.early_stop False def step(self, score): if self.best_score is None or score self.best_score self.delta: self.best_score score self.counter 0 else: self.counter 1 if self.counter self.patience: self.early_stop Truepatience3表示验证集F1连续3个epoch没有超过历史最佳就停。早停的意图是在模型快过拟合之前刹车。实际使用时你需要在每个epoch保存最优模型权重。最常见做法是当验证F1创新高时把model.state_dict()存到checkpoints/best.pt。第三个是权重衰减。优化器里的weight_decay就是L2正则化等效于在损失函数上加一个权重的平方和惩罚。PyTorch里写weight_decay1e-4就行。如果模型过拟合严重提高到1e-3但太高会让模型欠拟合表现是训练loss也降不下去。这三个手段组合起来能覆盖绝大多数过拟合场景。4.3 实验记录表与实验报告撰写的五个要点很多同学把实验报告当成项目介绍写一个大段怎么设计模型然后贴一张acc0.92的图。但老师更想看到的是你做过对比实验的过程。我建议你在训练过程中就做一张实验记录表每跑完一组参数就填一行最后把它原样贴进报告。实验编号模型embed_dimhidden_dimlrbatch_sizedropoutepoch验证acc测试F11BiLSTMAttn1281281e-3640.5早停90.9140.9012BiLSTM no Attn1281281e-3640.5早停70.9020.8893BiLSTMAttn2562561e-3320.5早停50.9080.895这张表第一行是你最终提交的模型第二行是删掉Attention后的对比第三行是加大隐藏层维度但效果变差的例子。有了这张表实验报告的实验与结果章节就有血有肉了。还要写清楚每个模型的训练曲线loss下降曲线用matplotlib画出来放到文末附录。实验报告的五个要点我总结成五句话一、讲清楚数据和预处理包括数据来源、样本量、类别分布、分词器、词表大小二、讲模型结构画一个简单的网络结构示意不用太精细但要标出张量形状变化三、讲训练配置loss函数、优化器、学习率、batch size、早停条件这些必须和代码里的常量对得上四、讲结果给出实验记录表说明最佳模型的准确率和F1并附上验证集上的典型预测结果五、讲错误分析从测试集里挑出三条预测失败的样本分析为什么失败。这五个要点写全文档说明和实验报告这部分基本就是门面了。文档说明里还要在README中写清楚如何复现环境依赖、运行顺序、数据文件放置路径。你可能会觉得这些内容老师不会看但助教跑你的代码时第一件事就是看README。README里有一句pip install -r requirements.txt和python train.py比你写一万字都管用。具体避坑放在下一章展开。5. 大作业避坑指南源代码、环境和实验报告的三类典型问题期末大作业的翻车点往往不在模型原理上而在工程细节上。我自己见过太多明明在本机能跑的作业交上去后被退回。这里写四个最常踩的坑每个都按现象、原因、解决来讲。这些血泪经验能帮你省下至少一个晚上的查错时间。5.1 环境不一致本机能跑别人跑不起来现象你在自己笔记本上跑得好好的传到老师机器或助教机器上一运行就报ModuleNotFoundError: No module named torch或者报CUDA out of memory。你确认自己的代码没问题但对方就是跑不起来。原因你没有把运行环境锁死。常见的触发条件是你本地用conda建了一个环境里面装好了torch和jieba但交给对方时只交了.py文件没有requirements.txt也没有说明Python版本。对方用了系统Python自然缺包。更隐蔽的问题是torch版本不一致你在本地用torch 2.x开发对方环境是torch 1.x一些API比如masked_fill行为可能有差异。解决在你的conda环境里执行pip freeze requirements.txt把版本号全部带上。然后在README开头写下三步1) conda create -n nlp python3.9 2) pip install -r requirements.txt 3) python train.py。如果你的代码要GPU才能跑但老师机器没有GPU建议在数据量不大时直接做成CPU可跑。把代码开头改成device torch.device(cuda if torch.cuda.is_available() else cpu)训练代码里不要硬编码.cuda()。这样无论对方有没有GPU都能运行。5.2 随机种子没固定实验报告的数字对不上现象你的实验报告写测试准确率0.912助教用你的代码重新训练得到0.885于是认为你造假。你也很委屈因为你确实拿过0.912。原因训练过程有太多随机性。PyTorch的模型初始化、数据Loader打乱顺序、Dropout丢弃的神经元、GPU上的部分算子都是随机行为。如果不固定种子每次运行结果差异可能达到几个百分点。更常见的是你报告里写的0.912是用n次训练里最好的一次而助教只跑了一次。这在学术诚信上说不清。解决在代码最开头固定所有能固定的种子并且把最终结果定义为固定随机种子下的一次运行或多次运行的平均值。import random import numpy as np import torch 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 Falseset_seed(42)放到数据划分、模型创建之前。注意torch.backends.cudnn.deterministic会让某些算子变慢但换来可复现期末作业这点性能损失值得。同时实验报告里要写清实验重复5次取平均值并用标准差写出波动范围。这样即使助教重跑有一点偏差也在你给出的范围里。5.3 中文编码和分词词表里全是乱码现象你打开vocab字典看到的是一堆或鈥之类的乱码有些词甚至被拆成单个字。模型训练出来准确率特别低和用随机猜测差不多。原因八成是文件编码不一致。你用Excel把数据另存为CSV时默认可能不是UTF-8或者你在read_csv里没有指定encodingutf-8导致中文字符被错误解析。另一个原因是jieba在默认词典下对新词或生僻词分词效果差把自然语言处理切成自然语言处理倒还好但把人名或专业术语切碎了也很影响embedding。解决读文件时统一指定encodingutf-8最好在数据准备脚本里增加编码校验。如果数据本身不是UTF-8用pandas的encodinggbk尝试然后另存为UTF-8。分词方面如果发现专有名词被切碎可以加载自定义词典把自然语言处理酒店差评这种高频词固定为一个token。代码是jieba.add_word(自然语言处理)加在build_vocab之前。更稳的做法是在词表里保留出现频率高的字符级n-gram但这会引入特征工程期末作业里用自定义词典就够讲清楚了。5.4 显存/内存不足OOM像一个定时炸弹现象你训练到第二个epoch突然报torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 512 MiB。有时候CPU环境下也会内存溢出。原因常见原因有三个batch size太大每批的句子pad得比实际长度长太多LSTM隐藏层维度过大。对于NLP作业最大的显存黑洞是padding。如果一批里有句长100的也有句长10的你不做长度分桶所有句子都按100来算浪费了大半计算资源。解决先降batch size从64降到32看还报不报错。其次降max_len把100改成平均句长的60到70分位比如50数据预处理时多截断一些。如果还不行把hidden_dim从128降到64。一般而言期末作业数据量下这三项足够解决OOM。如果你想进一步提升效率可以按句子长度把相似长度的样本分到同一个batchPyTorch里用一个自定义sampler就能实现但这部分可以在文档说明里坦白说未使用能跑就行。降batch size对最终指标的影响很小而OOM的影响是直接交不了作业所以优先保运行。6. 让作业多拿分的验证技巧从跑通到讲得清最后这一步决定你是拿85分还是70分。很多同学代码能跑就停了但答辩时老师最常问的是你这个模型到底学到了什么挑一条测试集样本讲讲。所以我会在提交前做三个附加验证第一对最佳模型保存的checkpoint做一次完整的测试集预测输出样例第二挑两三条预测失败的样本分析可能的失败原因第三用一条全新的样本跑一次预测顺便把attention权重可视化。第三条是最容易加分的因为直观展示了模型关注哪些词。这里贴一个简单的推理代码从checkpoints/best.pt加载模型对一条用户输入打印预测结果和attention分数较高的词。def predict_text(text, model, vocab, max_len100): model.eval() ids torch.tensor([encode(text, vocab, max_len)], dtypetorch.long) with torch.no_grad(): logits model(ids, mask(ids ! 0)) prob torch.softmax(logits, dim1) pred prob.argmax(dim1).item() return pred, prob[0, pred].item()如果你想让attention可视化更直观可以在模型forward里把weight返回但我上面的模型定义没有返回weight所以临时改一下也行。我更喜欢直接用这个predict函数看对错挑一条房间很大但隔音很差的样本如果预测成负面你可以解释为两个正面词里夹着负面词模型被房间很大带偏如果预测成负面说明attention学会了重点看隔音差。这个拿着真实样本聊模型行为的环节比背一堆公式更有说服力。我自己的教训是早期交作业时只贴一条准确率答辩时老师问哪些样本分错了为什么我当场卡住。后来每份大作业都强制自己固定随机种子、记录日志、保存最佳checkpoint并且在提交前把验证集的典型预测和失败样本写进实验报告。从那以后答辩基本没再被问倒过。文档说明也不光写环境安装还会写模型在哪些场景容易失效这反而让老师觉得你动手做了思考。希望这份路径能帮你的NLP期末大作业少走点弯路把精力花在真正分高的地方。本文还有配套的精品资源点击获取