简介:面向高校计算机、电子信息、数学等专业学生的NLP期末大作业完整方案,聚焦深度学习与自然语言处理核心实验,包含源代码、文档说明与实验报告。压缩包共34个文件,大小33.12MB,主要文件包括2个ipynb代码文件、20个txt语料与预处理文本、docx实验报告、pdf英文参考及xml工程配置,代码采用参数化编程,注释明细、思路清晰,且附有运行结果,便于修改参数复现实验。实验内容围绕多部武侠小说语料库展开,涉及文本预处理、中文停用词过滤、词频统计与信息熵计算,并配有实验报告与英文参考资料,覆盖从语料到结果分析的完整流程。已有2071人学习下载,适合作为课程设计、期末大作业或毕业设计的直接参考。作者为某大厂资深算法工程师,具备10年Matlab、Python等仿真经验,若运行遇到问题可通过私信获得支持。
1. 一个能交差的 NLP 期末大作业,核心是“验证”而不是“创新”
每到期末,NLP 自然语言处理方向的课程就会涌出一批“期末大作业”需求:给定一个任务,用深度学习模型做一遍,交源代码、文档说明和实验报告。很多同学把这个过程想成“要做出新模型”,其实课程作业真正考的是“用标准方法保质保量完成一次实验验证”。这篇按我的实操经验,把任务选型、模型选择、代码组织、实验报告写法以及最容易被扣分的几个坑拆开讲一遍。适合正在选课、需要在一周内完成交付的同学,也适合想让手头代码从“能跑”变成“可交付”状态的人。下文以 PyTorch 为例,Windows 和 Linux 都能跟着做。
2. 先把方向定下来:任务选型、模型选型和基线对比
2.1 任务选型:文本分类为什么是期末大作业的默认选项
NLP 课程大作业可选的任务其实不少:文本分类、序列标注、文本匹配、机器翻译、文本摘要都有同学在做。但如果你问我的建议,默认选文本分类,尤其是情感分类或新闻分类,这两类在中文 NLP 里属于“新闻处理”最常见的落地场景,公开数据也好找。原因有三个:数据好拿、指标好算、模型对比空间大。文本分类用准确率和 F1 就能说清楚,不需要额外的评测工具,机器翻译和摘要那种靠自动指标但和人工观感对不上的情况,到答辩阶段最容易被问倒。
数据集规模也要看一眼再定。情感分类可以用酒店评论或电商评论,新闻分类常用 THUCNews 的一个子集,规模从几万到几十万条不等。数据量在一万条以下时,深度学习模型的优势很难体现出来,训练也不稳定;数据量到百万级,CPU 训练时间又可能失控。所以选数据集的原则是:至少能支撑三组对比实验,组内跑一轮不超过两小时。几万到十几万条是一个比较舒服的量级。
另外还要优先满足课程硬性要求。有的课明确要求“必须使用预训练模型”,那就直接选情感分类这种句子级任务,BERT 或 ERNIE 上去效果直观;有的课偏传统方法,那用 Word2Vec 初始化的 BiLSTM 加 Attention 也很稳妥。总之任务选型的顺序是:课程硬性规定,数据可得性,设备承受能力。
2.2 模型选型:从 TextCNN/BiLSTM 到预训练模型,按设备条件来
模型选型的第一原则是机器跑得动,这也是“深度学习环境配置”里最容易被低估的一环。没有 GPU 的笔记本,优先 TextCNN 和 BiLSTM。TextCNN 用多个尺寸的卷积核并行抽取局部 n-gram 特征,参数少、收敛快、调参量小,CPU 跑一个 epoch 只要几分钟。BiLSTM 能建模长距离依赖,但训练时间明显更长,对长文本也更敏感。
如果有 GPU,即便只有 6G 显存,用 HuggingFace 的 transformers 加载 bert-base-chinese 也是可行的。它的参数量在 1 亿出头,batch size 设 16,max_length 设 128,一般都能跑。显存紧张就把 batch size 降到 8,或把 max_length 降到 64。很多人舍不得降 max_length,总担心信息丢失,但实际上中文句子平均字数只有几十个,取 64 的影响并不大,训练时间却能省掉一半。
我一般会把模型体系设计成三级:TF-IDF 加逻辑回归当基线;TextCNN 或 BiLSTM 当深度学习主模型;预训练模型或加了 Attention 的增强版本当高分项。这样实验报告就有一条清晰的性能上升曲线,答辩时也能讲清楚“深度模型比基线强在哪里、预训练模型又强在哪里”。主模型只做一个,不要三个深度模型全上,否则调参会消耗掉你最后几天的宝贵时间,每篇报告都写不深。
2.3 基线设置:用 TF-IDF + 逻辑回归给深度模型当标尺
很多同学的实验报告只列几个深度模型的分数,没有非深度基线。答辩老师大概率会追问一句:你凭什么说深度模型有效?加一个 TF-IDF 加逻辑回归的基线,是边际成本最低的做法。用 sklearn 的 TfidfVectorizer 把文本转成稀疏向量,再喂给 LogisticRegression,十几行代码就能出结果。
这里有一个必须强调的顺序:TfidfVectorizer 的 fit 只放在训练集上,验证集和测试集用同一个已训练的向量器做 transform。一旦对整个数据集 fit,词频统计里就包含了验证集的信息,属于一种数据泄漏,会让效果虚高,答辩时如果被问出“为什么你的测试分数比训练还高”,场面会很尴尬。
参数方面,我一般设置 max_features=10000、ngram_range=(1,2)、min_df=5。max_features 控制特征维度,10000 对几万条中文数据够用;ngram_range=(1,2) 能捕捉“不_喜欢”这种否定组合;min_df=5 过滤只在很少文档里出现的生僻词,降低噪声。这三个参数写进实验报告的超参数表,答辩时就能明确回答“为什么是 10000,不是 50000”。逻辑回归本身在文本分类上并不弱,几万条数据下拿到 80% 以上准确率很常见,所以后续深度模型每提升一两个点,报告的措辞都有据可依。
有一点值得提醒:基线跑完,把它的结果单独存一个 CSV 或写进实验记录,后续每个模型跑完都往这张表里加一行。别用“记在聊天记录里”这种管理方式。期末周事情一多,你大概率会忘了基线是 80.3 还是 83.0。等写实验报告时才发现记录丢了,再重新跑基线,浪费一到两小时已经是最好结局。
3. 源代码怎么组织:目录结构、数据处理与训练脚本
3.1 源码目录:用 config、data、models、utils 四层把逻辑拆开
动手写代码前,先把目录结构定下来。最常见的反面教材是把 dataloader、model、训练循环全塞进一个 train.py,两三百行,答辩老师打开代码就失去耐心。目录结构清晰与否直接影响“文档说明”的观感,我建议按这样的方式组织:
project/ ├── config.py # 所有超参数集中在这里 ├── data/ │ ├── __init__.py │ ├── dataset.py # 数据加载、分词与 DataLoader 构建 │ └── raw/ # 原始数据文件 ├── models/ │ ├── __init__.py │ ├── text_cnn.py # TextCNN 网络定义 │ └── bilstm.py # BiLSTM + Attention 网络定义 ├── utils/ │ ├── __init__.py │ └── metrics.py # accuracy / macro-F1 评估函数 ├── train.py # 训练入口 ├── predict.py # 单条文本预测入口 └── README.md # 运行说明与环境依赖这个结构对应了一种常见的工作方式:config.py 里集中放 batch_size、learning_rate、epochs、max_length 等参数,每组实验改完参数直接跑,不用去翻训练脚本。data 目录只负责“把文本变成 batch”,models 目录只定义网络结构,utils 里的 metrics.py 放评估逻辑。train.py 是唯一暴露给使用者的入口,predict.py 供训练完做 badcase 分析时加载 checkpoint 用。这样拆还有一个现实好处:写实验报告“模型结构”那一节时,直接从 models 下的文件里截取网络定义代码块,不用在一堆训练代码里找。
参数集中放在 config.py 里的意义要再说一句:期末大作业你往往要对比三四组参数,如果参数散落在代码里,改一处漏一处,最后实验报告里写的和实际跑的就不是一回事。这会成为“源代码说明”和“实验报告”对不上的最大来源。
还有一个小经验:config.py 里不要用 argparse 接一堆命令行参数。期末项目不同于生产系统,argparse 要求用户记住每个参数的名称和取值,而 config.py 里写上注释“这里是学习率,调小可提升稳定性”,十天后回来看代码还能读懂。如果课程要求交一个命令行入口,可以留一个薄壳层 train.py --lr 2e-5,内部仍然去覆写 config.py 的默认值,但别把全部参数都搬上命令行,否则 README 会写得很痛苦。
3.2 数据处理代码:中文分词与标签对齐这个老坑
中文 NLP 的第一步通常是分词。常见的做法是加载 jieba 进行分词,再把词用空格拼起来,这样能复用英文 NLP 的 tokenizer 接口。分词本身不难,难的是分词后文本和标签严格对齐。如果你在数据处理里做过过滤、去重、索引重置,合并时很容易把标签错位,这是文本分类任务最隐蔽的“翻车现场”。
我一般这样处理:
import pandas as pd import jieba def load_and_split(data_path, label2id=None): df = pd.read_csv(data_path, header=0) df = df[['text', 'label']].dropna() if label2id is None: labels = sorted(df['label'].unique().tolist()) label2id = {lbl: i for i, lbl in enumerate(labels)} df['label_id'] = df['label'].map(label2id) df['tokens'] = df['text'].apply(lambda s: ' '.join(jieba.cut(s))) # 过滤后必须重置索引,否则后续切分/合并会标签错位 df = df[['tokens', 'label_id']].reset_index(drop=True) return df, label2id逻辑说明:函数先读取 CSV,只保留 text 和 label 两列,去掉空行;然后生成从字符串标签到数字 id 的映射;接着分词并用空格连接;最后 reset_index 保证行号连续。注释里那句话是血泪经验:dropna 之后 index 会出现空洞,如果紧接着做 train_test_split 或按行合并,pandas 会保留原索引,张冠李戴就发生了。
参数说明:label2id 应该在主函数里生成一次,再传给后续所有数据集。验证集和测试集必须复用这份映射,不能各自生成,否则相同标签在三个集合里指向不同数字,模型训练和评估的语义就乱了。切分数据用 train_test_split,设 stratify=df['label_id'],保证各类别比例在三份数据中一致。random_state=42 是固定切分结果的关键,这个值本身没有玄学,只要固定下来就行。数据量在几万条时,我按 8:1:1 切分;不足一万条时改成 7:2:1,让验证集和测试集更有统计意义。
注意一个在 Windows 上常见的小坑:读取 CSV 时如果中文乱码,大多数情况是文件编码不是 UTF-8,而是 GBK。pd.read_csv 里显式指定 encoding='utf-8' 或 encoding='gbk',先试一次就能确定。这个坑不写进代码注释,等换台机器跑就暴露了。我会在 README 里写清楚数据文件的编码,避免同学或助教在加载数据时一头雾水。
3.3 训练脚本:PyTorch 训练循环里必须写清楚的四个环节
train.py 的核心训练循环,是答辩时最容易被追问的地方。下面这段代码删掉了无关部分,只保留关键逻辑,我用的是 PyTorch 的训练范式:每个 batch 先前向、再算 loss、反向、更新。
def train_epoch(model, dataloader, optimizer, scheduler, device): model.train() total_loss, total_correct, total_num = 0.0, 0, 0 for batch in dataloader: input_ids = batch['input_ids'].to(device) labels = batch['labels'].to(device) optimizer.zero_grad() logits = model(input_ids) loss = model.get_loss(logits, labels) loss.backward() # 梯度裁剪:RNN 类模型必开,防止 loss 变 nan nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() scheduler.step() total_loss += loss.item() * labels.size(0) preds = logits.argmax(dim=-1) total_correct += (preds == labels).sum().item() total_num += labels.size(0) return total_loss / total_num, total_correct / total_num逻辑说明:前半段是标准的 forward/backward 流程,后半段是 loss 和准确率的累计。这里有两个容易出错的细节。第一,optimizer.zero_grad() 必须在 loss.backward() 之前执行,否则梯度会跨 batch 累积,虽然不会立即报错,但 loss 曲线会变得混乱。第二,nn.utils.clip_grad_norm_ 的参数 max_norm 我一般设 5.0,BiLSTM 这类模型不裁梯度,很容易在某几个 batch 上让 loss 突然变成 nan。
参数说明:scheduler.step() 放在哪个位置要看你选的调度器。如果是 HuggingFace 的 get_linear_schedule_with_warmup,每个 step 调一次;如果是 PyTorch 的 ReduceLROnPlateau,在每个 epoch 结束后用验证集 loss 调一次。写反了不会报错,但学习率变化规律会和你预想完全不一致,训练曲线也解释不通。还有一个容易忽略的点:CrossEntropyLoss 会自动忽略 label 为 -100 的位置,所以 padding 位置的 label 要设置成 -100,否则模型会在无意义的 padding 上反复计算 loss,训练看起来正常,真实效果却掉几个点。
评估函数单独放在 utils/metrics.py 里,训练时每个 epoch 结束后调用一次:
from sklearn.metrics import accuracy_score, f1_score def evaluate(model, dataloader, device): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for batch in dataloader: input_ids = batch['input_ids'].to(device) labels = batch['labels'].to(device) logits = model(input_ids) preds = logits.argmax(dim=-1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.cpu().tolist()) acc = accuracy_score(all_labels, all_preds) f1 = f1_score(all_labels, all_preds, average='macro') return {'acc': acc, 'macro_f1': f1}逻辑说明:评估函数里必须写 model.eval() 和 torch.no_grad()。eval 模式会关闭 dropout 的随机性,no_grad 不再计算梯度,两者缺一不可。有人忘记 no_grad,结果显存每个 epoch 涨一点,跑到第四五个 epoch 就 OOM,这不是模型问题,是代码习惯问题。代码里先把所有预测和标签收集到列表,再一次性用 sklearn 计算指标,比在循环里累计分子分母更省事,也避免把 padding 一起算进去。
参数说明:average='macro' 是类别不均衡场景下更公平的选择,它给每个类相同的权重。多分类任务里同时报 accuracy 和 macro-F1 已经足够,不必再报 micro-F1,因为 micro-F1 和总体 accuracy 在数学上等价,写多了反而显得不专业。
保存 checkpoint 的规则我是这样写的:每个 epoch 结束做一次验证,如果 dev acc 超过历史最佳,就保存 model.state_dict() 到 checkpoints/best.pt,并记录当前 epoch。同时做早停:连续 3 个 epoch 验证指标不涨就终止训练。这个规则要同步写进 README 和实验报告,因为期末项目往往有训练时长限制,早停能省下两三个小时。保存时把 optimizer 的 state_dict 一起存下来没坏处,但提交作业时不强制交 checkpoint,除非实验报告里写了“支持断点续训”这样的大话。
4. 文档说明与实验报告:把“做了什么”讲成“为什么这么做”
4.1 README 文档:先让一个人能按步骤跑通
“文档说明”在课程交付里对应的是 README 和必要的配置说明。README 的目标不是写得多华丽,而是“一个从没接触过你代码的人,照着它能在自己电脑上 15 分钟内跑通”。
README 我一般固定五块内容:项目简介、环境依赖、快速开始、目录结构、实验结果表。项目简介写两句话即可:“这是一个基于 TextCNN/BiLSTM 的中文新闻文本分类项目,数据集来自 XX,目标是完成 XX 分类”。环境依赖必须具体到版本:torch==2.1.0、transformers==4.40.0、jieba==0.42.1、scikit-learn==1.3.0。不要写“python 3.x”这种模糊表述,版本浮动是代码跑不通的隐形来源。
快速开始部分要写清楚三组命令,让读者只靠复制粘贴就能完成安装、训练、预测:
pip install -r requirements.txt python train.py python predict.py --checkpoint checkpoints/best.pt --text "这家酒店位置很好但房间隔音一般"逻辑说明:第一条命令安装依赖;第二条命令按 config.py 里的默认配置开始训练,正常结束会在 checkpoints 目录下生成 best.pt;第三条命令用训练好的模型预测一条自定义文本。这里把 --text 设计成命令行直接传字符串,省去让用户准备待预测文件的麻烦,也方便课程助教快速验证模型有没有真正训练过。
参数说明:如果你在 Windows 上且没有 GPU,把 config.py 里的 device 改成 cpu,batch_size 从 32 改成 16,README 里写一句“CPU 训练预计需要 XX 分钟”,就能让读者对运行时间有预期。README 还有一个容易被忽视的功能:告诉别人“这个项目不做什么”。比如“本仓库未包含大规模预训练模型微调代码”,这句话能避免下载者期待错误的功能,也减少答辩时被问“为什么不用更大模型”的概率。
4.2 实验报告:表格、对比、可视化三板斧
实验报告的核心不是字数,而是能回答三个问题:任务是什么、怎么做出来的、结果说明了什么。一份老师愿意看下去的报告结构是:问题定义与数据集、模型设计、实验设置(含超参数表)、结果分析与对比、结论与不足。
“结论与不足”是很多同学偷懒不写的部分,恰恰是这一部分最能拉开档次。比如写“模型在长文本上表现较差,原因是 max_length=128 导致关键信息被截断”,这就属于真的分析过问题。反过来写“本文提出的模型在测试集上取得了最优性能”这种话,既没有数值支撑,也看不出个人思考。
结果展示优先用表格,格式固定为三列:模型、准确率、macro-F1。第一行永远是基线模型,然后按复杂度递增往下排。比如:
| 模型 | 准确率 | macro-F1 |
|---|---|---|
| TF-IDF + Logistic Regression | 0.812 | 0.806 |
| TextCNN | 0.861 | 0.857 |
| BiLSTM + Attention | 0.873 | 0.869 |
| BERT-base-Chinese | 0.902 | 0.899 |
表格下面放训练曲线图和混淆矩阵热力图,各一张,不要多。图片配文字说明时,必须回答“这张图说明了什么问题”。训练曲线图要标出最佳 epoch 位置,混淆矩阵要指出哪两个类别最容易被混淆。如果做不到这个标准,图宁可不放,堆图不加分反而减分。
报告里还可以加一张 badcase 表格,挑 3 到 5 个预测错误的例子,列出“原文、真实标签、预测标签、可能原因”四列。这张表比任何泛泛的分析都有说服力,因为它证明了你是真的一条条看了模型的输出,而不是只在终端里打印一个 90% 的准确率就拿去交差。badcase 分析同时也是你自己发现模型边界最快的方式。
4.3 复现说明:随机种子与超参数记录,是“文档说明”里最容易被忽略的一层
期末作业有一个隐藏评分点:能不能复现。训练脚本不固定随机种子,同一个模型同一份数据跑两遍,结果差一个点以上都是正常的。在训练入口处加一个固定种子的函数:
import random import numpy as np import torch def set_seed(seed=42): 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逻辑说明:前三行固定 Python、NumPy、PyTorch 三个随机源;第六行固定 CUDA 上所有设备的随机源;最后两行关闭 cuDNN 的自动搜索与 benchmark,保证同样的输入每次输出一致。代价是训练速度会慢 10% 左右,但对期末项目完全值得。如果不关 cuDNN 的自动调优,模型结构不变时训练更快,但不同 batch 的卷积实现可能不同,结果就不可复现了。
参数说明:跑实验前先在实验记录里写明这一轮的取值,比如 batch_size=32、lr=2e-5、max_length=128、epochs=10、seed=42。我见过一个挺尴尬的情况:实验报告里写的 lr 是 2e-5,但代码里实际是 5e-5,老师让当场改回去跑一遍,结果对不上。要避免这种翻车,唯一的办法是让报告里的超参数和 config.py 里的值逐一对应,可以专门做个“复现说明”小节,列出关键超参数和文件位置。
5. 期末作业避坑指南:最容易翻车的地方与排查办法
写代码和写报告的过程中,翻车点往往集中在数据泄漏、显存、实验记录和复现这几类。下面是按出现频率排序的五个常见问题,每条按“现象、原因、解决”拆开写。
5.1 数据泄漏:验证集里混进了训练集的文本
现象:训练和验证准确率都逼近 99%,但测试集一测只有 70%,报告根本没法解释,总觉得哪里不对劲。
原因:最常见的是 TfidfVectorizer 对整个数据集做了 fit,词频统计包含测试集信息;另一种是数据处理时 label2id 在不同环节重复生成,切分前后的标签编码不一致;还有一种是把去重放在划分之后,同一段文本同时出现在训练集和验证集里。
解决:把数据处理写成流水线,严格按“读取数据、去重、划分 train/dev/test、training 集上 fit 向量器,dev/test 只 transform”的顺序执行。去重放在划分之前,保证任意两条文本不重复。如果验证集准确率依然虚高,检查 dropna 之后有没有 reset_index,索引空洞会导致后续合并错位。
5.2 显存 OOM 与中文预训练模型加载慢
现象:BERT 训练到第 2 个 epoch 时显存溢出;或者加载 bert-base-chinese 等了很久才完成,CPU 版本更明显。
原因:OOM 大概率是 batch size 偏大,或者 max_length 设成 512 而数据里长文本又多。加载慢则是因为预训练权重文件本身有几百 MB,首次使用要下载并解压,机器磁盘速度慢时很折磨人。
解决:batch size 从 16 降到 8,max_length 从 128 降到 64,大概率能在 6G 显存上稳定跑完。max_length 不用舍不得降,中文句子平均字数是几十,取 64 影响很小,但训练时间能省近一半。权重文件下载一次后不要删,HuggingFace 会缓存到本地,后续加载直接读缓存。如果用了比 bert-base 更大的模型还 OOM,那就不是调参能解决的,老老实实换回 base 版。
5.3 实验报告里的图“看起来好看”但说明不了问题
现象:报告里贴了十几张训练曲线,每张都挺收敛,但答辩时老师指着 epoch 3 的一个抖动问“为什么这里掉了”,完全答不上来。
原因:把训练过程所有记录都堆进报告,没有筛选,也没有文字分析,图的表达和论文的论证之间没有形成对应关系。
解决:只保留三张图:dev acc 随 epoch 变化的曲线、混淆矩阵热力图、badcase 表。每张图下配两三句分析,说清楚“说明了什么问题”。曲线图要标出最佳 epoch;混淆矩阵要指出最容易被混淆的两个类;badcase 表要在每行写“可能原因”。凡是写不出分析的图,就不往上放。答辩翻车最直接的原因就是:图在,解释不在。
5.4 换了机器跑不通:路径、版本和随机性
现象:训练在自己的电脑上一切正常,助教换台机器就跑不起来,或者跑出来的结果和你的报告差很多。
原因:代码里用了绝对路径,比如“C:/Users/xxx/data/train.csv”,换机器后路径失效;或者依赖了写死的临时文件路径;还有一种是 PyTorch 或 transformers 版本不同,老版本和新版本的默认行为有差异。
解决:所有路径改成相对路径,README 里明确写“从项目根目录执行命令”。代码开头用 os.makedirs 保证输出目录存在,不要假设目录已经在磁盘上。版本层面,把 requirements.txt 精确到版本号,并在“复现说明”里注明“在 PyTorch 2.1.0 下验证”。如果助教用更新的版本跑出不同结果,可以先让他安装锁定的版本,几分钟就能解决。
5.5 训练不收敛,先别急着换模型
现象:loss 不降、震荡,或者直接变成 nan,第一反应是“模型结构有问题,换一个”。
原因:大部分时候不是结构问题,而是小毛病叠加:学习率太大导致震荡;梯度爆炸没有裁剪;数据没 shuffle,模型学到的是数据顺序;标签里有 nan 或异常值,loss 直接算不下去;还有极少见的场景是 padding 位置没有 mask,CrossEntropyLoss 算上了无效位置。
解决:调参顺序有讲究:先查数据里有没有 nan 和标签错位,再确认 DataLoader 的 shuffle=True,然后给模型加梯度裁剪,最后尝试把学习率降一个数量级。这套流程走完,90% 的“不收敛”都能解决。如果还是 nan,把输入文本打印几条出来看看,分词结果是否和标签一一对应。换模型是最后手段,不是第一反应。
6. 让作业多走一步:把“跑通”变成“可解释”的三种技巧
期末最可惜的不是做得少,而是做完了不会展示。模型跑出来 90% 的准确率,但答辩 PPT 上只有一张表格,老师看不到这个分数怎么来的。下面三个技巧都是小改动,但对“深度学习 + 自然语言处理”类项目的答辩提升非常明显,第一个是参数统计,后两个是可视化与对照实验,都很容易在半天内补上。
6.1 参数量表格:用 torchinfo 一行拿到
答辩时被问“你的模型有多大”是大概率事件。用 torchinfo 可以打印每层参数量:
from torchinfo import summary summary(model, input_size=(16, 128), dtypes=[torch.long])逻辑说明:input_size 里 16 是 batch size,128 是序列长度,输出会列出每一层的参数量和总参数量。把这张表截进实验报告,比写“模型约 XX 万参数”可信得多。注意数据类型要传 torch.long,因为词索引是整数,传 float 会直接报维度不匹配。
6.2 注意力可视化:画出模型做决策时看了哪些词
如果模型带 Attention 层(BiLSTM+Attention 或 BERT 的 attention),可以取一条测试样本,把 attention 权重叠加到原文上,颜色越深代表权重越高。这个可视化在答辩里很讨巧,能直接说明“模型不是黑匣子”。实现的思路是在 forward 时把 attention 权重缓存下来,预测时取出,处理成一个 ndarray,然后用 matplotlib 的 imshow 或 seaborn 的热力图渲染。一个合格的展示是:一句话预测“酒店位置很好但隔音很差”,模型把高权重放在“位置”和“隔音”上。
6.3 做一次消融实验:证明每个模块都有用
消融实验是“证明你的设计是必要的”最直接手段。比如 BiLSTM+Attention 一共两个组件,消融实验就是去掉 Attention 只跑 BiLSTM,再对比结果。如果 macro-F1 掉了 1.5 个点,这个模块存在的价值就被数据支撑了;如果没掉,你就该考虑是 Attention 设计有问题,还是数据本身不需要这个模块。消融实验不必多做,挑一个对结果影响最大的组件做一组就够。实验报告里加一小节“消融实验”,放一张两行三列的小表,老师在答辩时通常会很满意,因为这证明你真的理解自己的模型。
我自己上学时吃过最大的亏,就是模型跑完不记录过程,临交作业补实验记录补到凌晨。后来养成习惯:每跑完一组实验,立刻把 seed、lr、batch size、acc、macro-F1 写进一张表,代码提交前再对着这份记录检查一遍 config.py。这个习惯让我在后来的课程和项目里少踩了很多坑。期末大作业不要求惊艳,要求每个环节都站得住脚,希望这套方法能帮到你。
本文还有配套的精品资源,点击获取