第 20 讲(拓展):默认期末项目实战——精简 GPT-2 与下游任务

Natural Language Processing with Deep Learning · Lecture 20。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:CS224N 近几年的默认期末项目(实现精简版 GPT-2,并用于下游任务)。本讲不是官方讲座,而是把整门课串起来的一份”项目指南”。项目的具体要求每年会变化,以当年官方发布的项目说明为准。 配套代码:code/lecture20/(需要 PyTorch;gpt2_minimal.py 的 --hf 选项另外需要 transformers 和网络) 预计阅读时间:2.5 小时(真正做项目需要几周)


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 说出默认项目的整体结构:实现 GPT-2 的关键组件 → 加载预训练权重并验证 → 在下游任务上微调 → 扩展与实验 → 写报告。
  2. 逐层说出 GPT-2 的结构、参数的名字和形状,以及它和第 7 讲迷你 GPT 的对应关系;算出 GPT-2 small 的 124,439,808 个参数从哪里来。
  3. 解释 GPT-2 的分词器(字节级 BPE),以及加载公开权重时要注意的细节(Conv1D 的转置、GELU 的近似、层归一化的 ε)。
  4. 实现 AdamW,并解释它和 Adam + L2 正则的区别。
  5. 把 GPT-2 用到分类、句对任务和文本生成上,知道每种任务常见的坑。
  6. 设计一组可信的实验(基线、消融、多个随机种子、置信区间、误差分析),写出一份好的项目报告。

0.2 先修知识

本讲用到几乎整门课的内容,最相关的是:第 7 讲(Transformer)、第 8 讲(实践方法)、第 9 讲(预训练与微调)、第 11 讲(LoRA)、第 13 讲(评估)、第 16 讲(分词)、第 19 讲(伦理考量)。

0.3 本讲地图

第1节  默认项目是什么
第2节  GPT-2 逐层回顾                           ← gpt2_minimal.py
第3节  分词器与预训练权重:加载、验证、常见的错误   ← gpt2_minimal.py
第4节  优化器:AdamW                             ← 01_adamw_from_scratch.py
第5节  下游任务:分类、句对、生成
第6节  高效微调与训练技巧
第7节  扩展方向
第8节  实验设计
第9节  写报告
第10节 整门课回顾,以及接下来学什么
第11–12节 练习、延伸阅读

关于本讲的代码:两个文件都只做离线的自我检查(几秒钟),编写讲义时完整运行过;gpt2_minimal.py --hf(下载权重并和 Hugging Face 的实现比对)需要 transformers 和网络,编写时的环境没有,没有运行过。


1. 默认项目是什么

CS224N 每年提供一个”默认项目”,给不想自己选题的同学一个结构清晰的起点。近几年的默认项目围绕 GPT-2 展开,大致包括:

  1. 实现:补全一个精简版 GPT-2 的关键代码:多头自注意力、Transformer 层、整个模型的前向传播,以及 AdamW 优化器;
  2. 验证:加载 OpenAI 公开的 GPT-2 预训练权重,确认你的实现和参考实现的输出一致,并且能生成通顺的文本;
  3. 下游任务:把预训练的 GPT-2 微调到几个任务上,比如情感分类、复述检测(判断两句话意思是否相同)、诗歌生成;
  4. 扩展:在基础版本上做改进(更好的微调方法、数据增强、多任务学习、参数高效微调……),用实验说明你的改进是否有效、为什么;
  5. 报告:写一份类似会议论文的报告。

具体用哪些任务、哪些数据集、评分标准是什么,每年都可能变化,请以当年的项目说明为准。下面的内容侧重于”不论具体要求怎样变都用得上”的知识和方法。

这个项目的价值在于把整门课串起来:第 7 讲的 Transformer、第 9 讲的预训练与微调、第 11 讲的高效适配、第 13 讲的评估、第 16 讲的分词,全都会用到。


2. GPT-2 逐层回顾

2.1 整体结构

GPT-2(Radford 等 2019)就是第 7 讲迷你 GPT 的”大号”版本:

词元编号 (B, T)
   │ wte:词嵌入 (50257 × 768)      wpe:位置嵌入 (1024 × 768)
   ▼
 x = wte[idx] + wpe[0..T−1]   →  dropout
   │
   │  重复 12 次(Block):
   │     x = x + Attn(LN₁(x))        多头因果自注意力:c_attn (768 → 2304)、c_proj (768 → 768)
   │     x = x + MLP(LN₂(x))         前馈网络:c_fc (768 → 3072)、GELU、c_proj (3072 → 768)
   ▼
 ln_f:最后的层归一化
   │
 lm_head:输出层 (768 → 50257),权重和 wte 共享
   ▼
 logits (B, T, 50257)

gpt2_minimal.py 实现了这个结构,参数的名字(transformer.wte.weight、transformer.h.0.attn.c_attn.weight……)和 Hugging Face 的 GPT-2 完全一致,这样就能直接装入公开的权重。

2.2 和第 7 讲的对应

GPT-2第 7 讲 transformer.py说明
wte、wpetok_emb、pos_emb可学习的位置嵌入,最长 1024
attn.c_attn、attn.c_projattn.qkv、attn.proj一次算出 Q、K、V;12 个头,每个 64 维
mlp.c_fc、mlp.c_projffn.net[0]、ffn.net[2]中间层宽度 4D;GELU 用 tanh 近似
ln_1、ln_2、ln_fln1、ln2、ln_fPre-LN;ε = 1e-5
lm_head(和 wte 共享)head(tie_weights=True)权重共享

GPT-2 相对于原始 Transformer(第 7 讲)的几处改动:Pre-LN(层归一化放在子层之前,并在最后加一个 ln_f)、GELU 激活、可学习的位置嵌入、权重共享,以及一个初始化细节:每个残差分支最后的投影层(c_proj)的初始化标准差再乘以 1/2L1/\sqrt{2L}(LL 是层数)。每一层都往残差流上加东西,层数越多加得越多,缩小这些投影层的初始值,残差流的方差就不会随层数增长。

2.3 参数量

第 7 讲推导过:一个块有 12D2+13D12D^2 + 13D 个参数。GPT-2 small(D=768D = 768、12 层、词表 50257、位置 1024):

部分参数
词嵌入 50257×76850257 \times 76838,597,376
位置嵌入 1024×7681024 \times 768786,432
12 个块 × (12×7682+13×768)(12 \times 768^2 + 13 \times 768)85,054,464
最后的层归一化1,536
输出层(和词嵌入共享,不另计)0
合计124,439,808

gpt2_minimal.py 的自我检查实际构造了这个模型,数出的参数个数正好是 124,439,808。注意词嵌入占了三分之一,这是小模型的特点;模型越大,块的参数占比越高。


3. 分词器与预训练权重:加载、验证、常见的错误

3.1 分词器

GPT-2 用字节级 BPE(第 16 讲):基本单位是 256 个字节,加上 50,000 次合并,再加一个特殊词元 <|endoftext|>,一共 50,257 个词元。训练前先用一个正则表达式做预切分(把字母、数字、标点、空格分开,单词前面的空格并入单词),合并不跨越预切分的边界。所以 “Hello world” 被切成 “Hello” 和 ” world”(带前导空格)。

几个实际的注意事项(第 16 讲第 3.6 节):

  • 前导空格很重要:” yes” 和 “yes” 是不同的词元。用”比较 yes / no 的概率”做分类时,要用模型在那个位置真正会生成的那个版本。
  • <|endoftext|> 既是文档的分隔符,也常被用作填充符号;填充时一定要配合注意力掩码,并且在计算损失时忽略填充位置。

3.2 加载公开权重

OpenAI 的原始代码(以及 Hugging Face 的实现)用的是一种叫 Conv1D 的层,权重矩阵的形状是 (输入, 输出),而 PyTorch 的 nn.Linear 是 (输出, 输入)。加载时,c_attn、c_proj、c_fc 这四种权重要转置(gpt2_minimal.py 的 from_pretrained)。注意力里名叫 “bias” 的缓冲区其实是因果掩码,不是参数,要跳过。

3.3 怎样验证你的实现是对的

最可靠的办法是逐数比对:用同一个输入,比较你的模型和参考实现输出的 logits,最大差别应该在 10−410^{-4} 以下(gpt2_minimal.py --hf 就做这件事)。如果对不上,逐层比对:先比嵌入层的输出,再比第一个块的输出……第一个对不上的地方就是 bug 所在。

更早的阶段,给每个模块写单元测试:

  • 注意力:用很小的维度、固定的输入,和手算(或者一个最朴素的实现)比对;
  • 因果性:改变最后一个词元,前面位置的输出不应该改变(gpt2_minimal.py 和第 7 讲都做了这个检查);
  • 初始损失:随机初始化时,预测下一个词元的损失应该约等于 ln⁡V\ln V(gpt2_minimal.py 的小配置:4.653,ln⁡100=4.605\ln 100 = 4.605)。

编写 gpt2_minimal.py 的自我检查时,第一版把输入本身当作了目标(m(X, X)),初始损失只有 3.87,远低于 ln⁡100\ln 100。原因是权重共享:残差流里保留着当前词元自己的嵌入,它和输出层的同一行做点积,模型在初始化时就偏向”预测自己”。改成预测下一个词元(m(X[:, :-1], X[:, 1:]))之后,又暴露了另一个 bug:切片得到的张量内存不连续,.view 会报错,要用 .reshape。一个”不对劲”的初始损失往往能揭示数据或实现的问题(第 8 讲第 4 节)。

3.4 常见的错误清单

症状可能的原因
加载权重后输出乱码、损失很大忘了转置 Conv1D 的权重;参数名对应错了
输出和参考实现差一点点(10−310^{-3} 量级)GELU 用了精确版而不是 tanh 近似;层归一化的 ε 不同;注意力的缩放因子不对
训练时损失很低、生成却很差因果掩码错了(模型偷看了后面的词元)
批处理和逐条处理的结果不同填充位置没有被掩码;位置编号对填充处理不当
微调后验证集表现时好时坏评估时没有切换到 model.eval()(dropout 还开着)
分类时取错了位置右侧填充时,“最后一个词元”应该是最后一个真实的词元,而不是序列的最后一个位置

4. 优化器:AdamW

默认项目通常要求自己实现 AdamW(Loshchilov & Hutter 2019)。对每个参数 θ\theta(梯度 gg),第 tt 步:

θ←θ−ηλθ① 解耦的权重衰减m←β1m+(1−β1)g,v←β2v+(1−β2)g2② ③ 一阶、二阶矩m^=m/(1−β1t),v^=v/(1−β2t)④ 偏差修正θ←θ−η m^/(v^+ϵ)⑤ 更新\begin{aligned} \theta &\leftarrow \theta - \eta\lambda\theta && \text{① 解耦的权重衰减} \\ m &\leftarrow \beta_1 m + (1-\beta_1) g, \quad v \leftarrow \beta_2 v + (1-\beta_2) g^2 && \text{② ③ 一阶、二阶矩} \\ \hat{m} &= m/(1-\beta_1^t), \quad \hat{v} = v/(1-\beta_2^t) && \text{④ 偏差修正} \\ \theta &\leftarrow \theta - \eta\, \hat{m}/(\sqrt{\hat{v}} + \epsilon) && \text{⑤ 更新} \end{aligned}

偏差修正:mm、vv 从 0 开始,前几步会偏小。mm 是 gg 的指数滑动平均,E[mt]=(1−β1t) E[g]\mathbb{E}[m_t] = (1 - \beta_1^t)\,\mathbb{E}[g],除以 (1−β1t)(1-\beta_1^t) 就无偏了。

代码 01 的演示 1 把从零实现的 AdamW 和 torch.optim.AdamW 放在同一个小 GPT-2 上、用同样的数据训练 50 步,参数逐位相同(最大差别 0.0)。但编写时第一版用了一种数学上等价、运算顺序不同的写法(先算 m^\hat{m}、v^\hat{v} 再相除),50 步后的差别约为 7×10−57 \times 10^{-5}:每一步只差 10−810^{-8} 左右的浮点误差,但训练是一个”混沌”的过程,微小的差别会被一步步放大。比对自己的实现时,要么严格按参考实现的运算顺序写,要么只比较一步的结果。

为什么要”解耦”? Adam + L2 正则是把 λθ\lambda\theta 加进梯度里,它会和损失的梯度一起被除以 v^\sqrt{\hat{v}}。代码 01 的演示 2:两个参数的损失梯度都只是噪声,但大小不同,λ=0.1\lambda = 0.1,学习率 0.001,训练 5000 步(20 次运行取平均):

参数 1(梯度小)参数 2(梯度大)
Adam + L2 正则−0.0010.624
AdamW0.5830.624

(AdamW 的理论值 (1−10−4)5000=0.607(1 - 10^{-4})^{5000} = 0.607。)在 Adam + L2 下,梯度小的参数很快被压到 0,梯度大的参数衰减得慢得多:同样的 λ\lambda,正则化的强度却取决于参数的梯度有多大。AdamW 让所有参数按同样的比例衰减。今天训练 Transformer 几乎都用 AdamW;通常不对偏置和层归一化的参数做权重衰减。


5. 下游任务:分类、句对、生成

5.1 单句分类(如情感分类)

做法和第 9 讲 03 号文件、第 11 讲一样:取最后一个真实词元的隐藏状态(GPT-2 是因果模型,只有最后一个位置”读过”整句话),接一个线性分类头(gpt2_minimal.py 的 GPT2Classifier)。

  • 右侧填充时,最后一个真实词元的位置 = 真实长度 − 1,要用注意力掩码排除填充;
  • 预训练主体通常用比分类头小的学习率(比如主体 10−510^{-5} 量级、分类头 10−310^{-3} 量级),以免”冲掉”预训练学到的东西;
  • 类别不平衡时,看宏平均 F1(第 13 讲)。

5.2 句对任务(如复述检测)

判断两句话是否意思相同。两种常见的做法:

  • 分类头:把两句话拼起来(中间加分隔符),取最后一个词元的隐藏状态做二分类;
  • 完形填空式(cloze):把任务写成一个提示,比如 Question 1: "..." Question 2: "..." Are these questions asking the same thing? Answer:,比较模型生成 ” yes” 和 ” no” 的概率(第 9 讲的零样本分类、第 11 讲的提示)。微调时只在答案词上计算损失。这种做法更接近预训练的目标,在数据少时往往更好。

注意拼接的顺序:两句话的地位对称,可以把 (A, B) 和 (B, A) 都放进训练数据(数据增强),测试时两个顺序取平均。

5.3 文本生成(如诗歌生成)

在目标风格的文本上继续训练语言模型(就是第 10 讲的 SFT,只是没有指令),再用第 5 讲的解码方法生成:

  • 解码策略影响巨大:贪心解码容易重复;温度采样、top-k、top-p(核采样)在流畅和多样之间权衡;
  • 评估是最难的部分:自动指标(chrF、BLEU 等和参考文本比对,第 13 讲)只能衡量很表面的相似,诗歌这种开放式的生成更需要人类评估或者大模型评委,而它们各有偏差(第 13 讲第 7 节);
  • 结构约束(比如十四行诗的行数、韵脚)可以在解码时加入,也可以用强化学习、DPO 等方法(第 10 讲)优化。

6. 高效微调与训练技巧

  • 全量微调还是 LoRA? GPT-2 small 只有 1.2 亿参数,全量微调在一张普通显卡上就可以;更大的版本(gpt2-large、gpt2-xl)或者显存有限时,用 LoRA(第 11 讲)。gpt2_minimal.py 的自我检查演示了怎样用第 11 讲的 peft.add_lora 给 GPT-2 的注意力层加 LoRA(参数名以 attn.c_attn、attn.c_proj 结尾)。比较全量微调和 LoRA 的效果、显存、速度,本身就是一个好的扩展实验。
  • 学习率:微调的学习率比预训练小得多;用预热 + 衰减(第 7 讲的 warmup_cosine);不同层可以用不同的学习率(越靠近输出层越大,逐层学习率衰减)。
  • 混合精度:用 bf16 训练能节省显存、加快速度(第 11 讲第 7.1 节)。
  • 梯度累积:显存不够放大批量时,多算几个小批量再更新一次。
  • 早停与检查点:在验证集上选最好的模型;测试集只在最后用一次(第 8 讲)。
  • 先在一小批数据上过拟合(第 8 讲):确认模型、损失和数据管线都没问题,再开始正式训练。

7. 扩展方向

在基础版本跑通之后,项目的核心是扩展:提出一个改进,并用实验说明它是否有效、为什么。一些和本课内容相关的方向:

方向相关讲次例子
更好的微调第 9、11 讲逐层学习率衰减、正则化(如对抗扰动的平滑正则)、先在任务领域的无标签文本上继续预训练
参数高效微调第 11 讲LoRA、适配器、提示微调的比较;不同的 rr、放在哪些层
数据第 8 讲数据增强(回译、同义改写)、清洗标注噪声、课程学习
多任务学习第 9 讲几个下游任务共享主体、交替训练,看任务之间是互相帮助还是互相干扰
对比学习第 12、18 讲用对比损失学习句子表示,用于复述检测
提示与上下文学习第 11 讲完形填空式的任务表述、少样本提示,和微调比较
偏好优化第 10 讲用 DPO 让生成更符合某种偏好(比如更押韵)
推理与测试时计算第 14、15 讲采样多个候选、用验证器挑选
分析第 13、17 讲误差分析、探针、注意力和归因分析:模型到底学到了什么

好的扩展有一个清楚的假设(“我认为 X 会改善 Y,因为 Z”),并且设计实验去检验它,而不是把很多技巧堆在一起看分数有没有涨。


8. 实验设计

第 8、13 讲的方法在这里全部用得上:

  1. 基线:先建立可靠的基线(基础版本的微调、简单的非神经模型)。没有基线,任何改进都无从谈起。
  2. 一次只改一个变量:每个实验只改变一件事,才能知道效果来自哪里。
  3. 消融实验:你的方法有好几个组成部分时,逐个去掉,看每个部分贡献了多少。
  4. 多个随机种子:同样的设置跑 3~5 个随机种子,报告均值和标准差。随机种子带来的波动常常比方法之间的差别还大(第 8 讲第 6 节)。
  5. 置信区间与显著性:测试集上的分数要有误差棒;比较两个方法时用配对检验(第 13 讲第 6 节)。
  6. 公平的比较:给每种方法同样的调参预算、同样的计算预算;不要只给自己的方法仔细调参。
  7. 误差分析:看模型在哪些例子上出错,错误有什么共同点。好的误差分析常常比分数本身更有价值,也能启发下一个改进。
  8. 记录与可复现:记录每个实验的配置、随机种子、代码版本和结果(用 wandb 等工具,或者至少一个表格);报告里写清楚怎样复现。

计算预算:在动手之前估算一下每个实验要多长时间、一共要跑多少个实验,确保在截止日期之前能完成。先在小数据、小模型上快速迭代,确定方向之后再做完整的实验。


9. 写报告

9.1 结构

一份好的项目报告大致和一篇会议论文一样:

  1. 摘要:一段话说清楚问题、方法、主要结果;
  2. 引言:问题是什么、为什么重要、你做了什么、主要发现;
  3. 相关工作:已有的方法,你的工作和它们的关系;
  4. 方法:清楚到别人可以复现;用公式和图说明你的模型;
  5. 实验:数据、评估指标、基线、实验设置(超参数、计算资源)、结果(表格带误差棒);
  6. 分析:消融、误差分析、可视化,解释为什么有效或无效;
  7. 结论:总结发现、局限性、未来的工作;
  8. 伦理考量(第 19 讲):数据的来源和潜在偏见、可能的误用、对哪些人群效果可能更差。

9.2 一些建议

  • 结论要被证据支持:不要说”显著提高”,除非做了显著性检验;不要从一个随机种子的结果得出结论。
  • 负面结果也有价值:一个精心设计、但没有提升的实验,只要分析清楚了原因,同样是好的研究。如实报告,不要只挑好看的结果。
  • 图表自明:每张图、每个表都要有清楚的标题和说明,读者不看正文也能看懂。
  • 先写大纲:先确定”我要说服读者相信什么”,再安排实验和段落。
  • 留出时间修改:初稿写完之后,隔一天再读一遍,常常能发现很多不清楚的地方。

10. 整门课回顾,以及接下来学什么

10.1 二十讲的主线

词怎么变成向量(1–2)  →  神经网络怎么学习(3)  →  句子的结构(4)
     ↓
序列建模:n 元模型 → RNN → LSTM → 注意力(5–6)  →  Transformer(7)  →  怎么做实验(8)
     ↓
大语言模型:预训练(9)→ 后训练(10)→ 高效适配(11)→ 检索与智能体(12)→ 评估(13)
            → 推理:思维链(14)→ 验证与测试时计算(15)
     ↓
专题:分词与多语言(16)、可解释性(17)、多模态(18)、社会影响与开放问题(19)
     ↓
把它们串起来:精简 GPT-2 与下游任务(20)

贯穿始终的几个问题:

  • 知识放在哪里? 规则 → 统计 → 参数 → 外部检索(第 1、12 讲);
  • 为什么规模有效,规模不能解决什么?(第 9、15、19 讲);
  • 怎样知道模型真的会了? 评估、可解释性、忠实性(第 13、14、17 讲);
  • 怎样负责任地构建和使用这些技术?(第 19 讲)。

10.2 接下来学什么

  • CS336(同目录):从零构建语言模型的系统与工程:分词器、GPU 与并行、规模定律、数据管线、对齐的大规模实现;
  • CME295(同目录):Transformer 与大语言模型的另一个视角;
  • CS234(同目录):强化学习,理解 RLHF、GRPO 背后的理论;
  • 读论文、做项目:跟进 ACL、EMNLP、NeurIPS、ICLR 等会议的论文;在一个你感兴趣的问题上,从复现一篇论文开始。

11. 练习

练习 1(参数量)★:算出 GPT-2 medium(24 层、D=1024D = 1024、16 个头、词表 50257、位置 1024)的参数量。词嵌入占多大比例?和 GPT-2 small 比呢?

参考答案

词嵌入 50257×1024=51,463,16850257 \times 1024 = 51{,}463{,}168;位置嵌入 1024×1024=1,048,5761024 \times 1024 = 1{,}048{,}576;每个块 12×10242+13×1024=12,582,912+13,312=12,596,22412 \times 1024^2 + 13 \times 1024 = 12{,}582{,}912 + 13{,}312 = 12{,}596{,}224,24 个块共 302,309,376302{,}309{,}376;ln_f 20482048。合计 354,823,168354{,}823{,}168,约 3.55 亿。词嵌入占约 14.5%;GPT-2 small 里占约 31%。模型越大,块的参数占比越高。

练习 2(加载权重)★:(a) 为什么加载 GPT-2 权重时,c_attn.weight 要转置,而 wte.weight 不用?(b) 如果忘了转置 c_attn.weight,代码会报错吗?

参考答案

(a) c_attn 在原始实现里是 Conv1D,权重形状 (768, 2304) = (输入, 输出),nn.Linear 需要 (2304, 768) = (输出, 输入);wte 是嵌入矩阵,两边的形状都是 (50257, 768),含义相同。 (b) c_attn 不是方阵,形状对不上,copy_ 会报错(gpt2_minimal.py 里也有形状断言)。但 attn.c_proj.weight 是 768 × 768 的方阵,忘了转置不会报错,模型能运行,输出却是错的。这是最隐蔽的一类 bug,只有逐数比对才能发现。

练习 3(AdamW)★★:(a) 第 1 步(t=1t = 1)时,没有偏差修正的 Adam 更新量是多少?有偏差修正呢?(设 β1=0.9\beta_1 = 0.9、β2=0.999\beta_2 = 0.999,忽略 ϵ\epsilon 和权重衰减,梯度为 gg。)(b) 解释为什么 Adam + L2 正则下,梯度大的参数衰减得慢。

参考答案

(a) m1=0.1gm_1 = 0.1g,v1=0.001g2v_1 = 0.001g^2。没有修正:更新量 η⋅0.1g/0.001g2=η⋅0.1/0.0316⋅sign(g)≈3.16η⋅sign(g)\eta \cdot 0.1g/\sqrt{0.001g^2} = \eta \cdot 0.1/0.0316 \cdot \text{sign}(g) \approx 3.16\eta \cdot \text{sign}(g),步长被放大了约 3 倍(因为 vv 被低估得更厉害)。有修正:m^=g\hat{m} = g,v^=g2\hat{v} = g^2,更新量 η⋅sign(g)\eta \cdot \text{sign}(g)。 (b) L2 正则的梯度 λθ\lambda\theta 加在损失梯度上,整体被除以 v^\sqrt{\hat{v}};梯度大的参数 v^\sqrt{\hat{v}} 大,λθ/v^\lambda\theta/\sqrt{\hat{v}} 小,衰减就弱。AdamW 把衰减项移到了除法之外。

练习 4(分类的位置)★:一批句子右侧填充到长度 20,第一句的真实长度是 7。(a) 用 GPT-2 做分类时,应该取第几个位置的隐藏状态?(b) 如果错误地取了第 20 个位置,会怎样?(c) 如果用左侧填充呢?

参考答案

(a) 第 7 个位置(下标 6),最后一个真实词元。 (b) 第 20 个位置是填充符号,它的隐藏状态混合了真实词元和填充的信息(如果掩码处理得当,它看到的是真实词元,但它自己的输入是填充符号,和训练时的分布不同),分类效果会变差,而且不同长度的句子之间不一致。 (c) 左侧填充时,最后一个真实词元总在序列的最后一个位置,可以直接取最后一个位置;但位置编号要处理好(真实词元的位置应该从 0 开始,而不是从填充的个数开始),否则位置嵌入会错位。

练习 5(实验设计)★★:你提出了一个新的微调方法,在验证集上比基线高 1.2 个百分点(单个随机种子)。在写进报告之前,你还应该做哪些事?

参考答案

① 多跑几个随机种子(基线和新方法都跑),报告均值和标准差;② 在测试集上(只评估一次)用配对的方法算差值的置信区间或做显著性检验;③ 确认两者的调参预算、训练步数、计算量相同;④ 做消融,确认提升来自你认为的那个组成部分;⑤ 做误差分析,看提升出现在哪类例子上;⑥ 如果有多个下游任务,看提升是否一致。1.2 个百分点很可能在随机种子的波动范围之内(第 8 讲)。

练习 6(报告):为你的项目写一段”局限性与伦理考量”(第 19 讲),至少涉及数据、评估和可能的误用三个方面。

参考答案

开放题。一个例子(情感分类项目):数据来自电影评论网站,评论者以英语母语、特定年龄段的人群为主,模型在其他领域、方言或非母语者的文本上可能表现更差;评估只用了一个测试集和准确率,没有按人群拆开评估,也没有测试对否定、反讽的鲁棒性;情感分析可能被用于监控舆论或针对特定人群的言论,部署前应该评估不同人群上的错误率,并明确用途的边界。

练习 7(动手):

(a) 安装 transformers 后运行 python gpt2_minimal.py --hf,确认 logits 的最大差别在 10−410^{-4} 以下,并看看贪心生成的文本。 (b) 故意制造练习 2(b) 里的 bug(不转置 attn.c_proj.weight),看输出会差多少、生成的文本变成什么样。 (c) 用 gpt2_minimal.py 加载 GPT-2,在第 8 讲的玩具影评上做情感分类:比较只训练分类头、全量微调、LoRA 三种做法的准确率和训练时间。 (d) 在代码 01 里实现”不对偏置和层归一化做权重衰减”(用两个参数组),和对所有参数做衰减的结果比较。


12. 延伸阅读

  • Radford et al.,Language Models are Unsupervised Multitask Learners(2019):GPT-2。
  • Vaswani et al.,Attention Is All You Need(2017)。
  • Loshchilov & Hutter,Decoupled Weight Decay Regularization(2019):AdamW。
  • Kingma & Ba,Adam: A Method for Stochastic Optimization(2015)。
  • Hu et al.,LoRA(2021)。
  • Andrej Karpathy 的 nanoGPT 和 “Let’s build GPT” 系列视频:从零实现 GPT-2 的优秀教程。
  • CS224N 历年的默认项目说明和优秀项目报告(课程主页上可以找到)。
  • Dodge et al.,Show Your Work: Improved Reporting of Experimental Results(2019):怎样可信地报告实验结果。

结语

这门课从”一个词怎么变成向量”开始,到”怎样构建、评估和负责任地使用大语言模型”结束。技术会继续快速变化,今天的方法几年后可能就过时了;但一些东西不会变:理解问题本身(语言有什么特点、任务真正要求什么)、对实验结果保持怀疑(基线、误差棒、消融)、追问”为什么有效”、以及思考技术对人的影响。希望这些讲义和代码能成为你继续探索的起点。

← 返回课程目录