Skip to content

第 6 章 权重共享、损失、预训练映射与上下文裁剪 ​

6.1 从编号到每个位置的预测 ​

GPT.forward(idx, targets=None) 先读出批次和长度,要求 T≤Tmax,再创建位置编号 0,...,T-1。token embedding 为 (B,T,C),position embedding 为 (T,C),相加时位置向量在批次维广播。经过输入 dropout、L 个 block 和最终 LayerNorm,隐藏状态仍是 (B,T,C)。forward

这套位置编码是学出来的绝对位置表。生成时若只保留最近窗口,留下来的 token 会重新从位置 0 编号;原代码没有旋转位置编码,也没有保留无限增长的全局位置。不要把别的模型的位置机制套到这里。

有 targets 时,输出头计算全部位置的 logits,形状为 (B,T,V)。无 targets 时,只取 x[:,[-1],:],保留长度为 1 的时间维,返回 (B,1,V),损失为 None。优化只省掉最后输出头对早期位置的计算;各层注意力仍处理整个输入窗口,因此它不是 KV cache。

6.2 交叉熵为什么要把前两维展平 ​

令 zbtv 是位置 (b,t) 对词表项 v 的分数,目标编号为 ybt。单位置损失为

ℓbt=−zbt,ybt+log⁡∑v=0V−1ezbtv.

对所有非忽略位置求平均得到最终 loss。展平 logits 为 (BT,V)、targets 为 (BT),就是把每个位置当一条分类样本。若 logits 全相同,每项概率 1/V,损失为 log⁡V。五字符合成实验中,这个均匀基线是 log⁡5≈1.6094;随机初始化的实际初始验证损失 1.6728 略高,并不异常。

ignore_index=-1 允许把某些目标标为不参与损失,但输入编号依然不能是 -1:嵌入查表和目标掩码是不同接口。若所有目标都被忽略,均值没有有效分母,不应期待一个正常的训练损失。若有不同数量的有效目标,等权平均微批次损失也不再自动等于对全部有效 token 的平均,第 8 章讨论这个条件。

源码使用 targets.view(-1),要求内存步幅兼容。我们用 y[:,:4] 截取原来长度 6 的二维张量时,真实触发了 RuntimeError;调用 .contiguous() 后通过。原 get_batch 用 stack 生成连续目标,因此标准训练不受影响。自己改 loader 或对目标裁切时,应检查连续性,或在修改版中明确使用 reshape。不能把这个接口失败误诊为 checkpoint 损坏。

6.3 共享不是拷贝,梯度会从两个角色汇合 ​

self.transformer.wte.weight = self.lm_head.weight 令两处引用同一个 Parameter。前向时它一边提供输入向量,一边把最终特征映回词表;反向时这两条使用路径对同一个参数的梯度相加。若只复制数值而创建新 Parameter,两份权重下一步就可能分离,那不叫权重共享。

以共享矩阵元素 E2,3 为例,本书 double 测试的自动梯度为 -0.11059136136513018。将同一个元素增加和减少 ϵ=10−5,用

L(E+ϵ)−L(E−ϵ)2ϵ

得到 -0.11059136064694995,误差 7.18×10−10。有限差分同时改变输入和输出两处使用,才与自动梯度比较的是同一个函数。这里没有分别给两套共享引用施加扰动。

参数遍历默认去重,configure_optimizers 又从 named_parameters 建字典;本书检查优化器内 Parameter 身份没有重复。否则同一权重可能被更新两遍,训练异常却很难从 state dict 的重复名字看出来。

6.4 从 GPT-2 导入时,名字和形状都要对齐 ​

from_pretrained 只接受四个型号,并仅允许覆盖 dropout。它固定词表 50257、长度 1024、bias=True,再按型号确定 (L,H,C):小型 (12,12,768),medium (24,16,1024),large (36,20,1280),xl (48,25,1600)。这些是结构约束,不能在导入权重时任意改宽度。导入路径

源码从 Hugging Face GPT2LMHeadModel 取 state dict,排除注意力 mask buffer,核对键数量,再逐键核对形状。四类权重需要转置:attn.c_attn.weight、attn.c_proj.weight、mlp.c_fc.weight、mlp.c_proj.weight。原因是上游 GPT-2 使用的 Conv1D 包装与本地 Linear 存储方向不同。例如从 (C,3C) 转到 (3C,C),计算语义才一致。其余权重直接拷贝,不应为了“统一”把 embedding 也转置。

拷贝在 torch.no_grad() 下进行,因为导入是状态赋值,不是优化目标中的可求导操作。键数量相等只是第一关;键名、形状、词表语义和层结构都匹配才有意义。本书没有下载这些权重,这部分是固定源码静态核对,不声称验证了当前 Transformers 版本兼容性或输出一致性。

6.5 裁长度会换 Parameter,时机很重要 ​

crop_block_size(new_T) 要求新长度不大于旧长度,更新 config,截取位置表前 new_T 行并包装成新的 Parameter;手工注意力路径的 mask 也截到相同大小。它不能扩展位置表,不能让一个只学过 1024 位置的模型无代价支持更长上下文。

裁剪应在创建优化器之前完成,原 train.py 正是如此。因为位置表 Parameter 被替换,先创建的优化器可能还持有旧对象。恢复训练另有一层风险:如果先裁小模型,再加载原 checkpoint 的优化器状态,位置表的一、二阶矩形状仍可能对应旧长度,后续更新会冲突。权重裁剪成功不等于优化器状态也已裁好;需要同步迁移状态或明确重建优化器。

补充练习。 无 targets 的前向输出只有最后一个位置,能否用它直接计算整个训练窗口的交叉熵?

答案。 不能。它只有 (B,1,V),而整窗目标为 (B,T)。要传 targets 走训练分支,或修改前向返回所有 logits。不能靠广播把缺少的预测补出来。

本章来源 ​

保留各书来源与许可 · 示例运行条件以正文为准