跳到主要内容

地基与加料 — PyTorch 底座、训练三件套、LoRA

这一章收三个附录: 附录 A 把前面九章一直在用却没细讲的 PyTorch 底座摊开; 附录 D 给第 06 章那个「最小可用」训练循环补上真实训练的三件稳定器; 附录 E 讲 LoRA——一种只动 2% 参数就能微调大模型的省法。 位置在全书的角色:它们是「回到地基」,读正文时随时可以翻到这一章。

1. 这一章讲什么

前九章我们一直在用 PyTorch,却没回答过「它到底替我们做了什么」; 第 06 章的训练循环能跑,但真实训练里还要再加三样东西才稳; 第 08、09 章的微调动了 1%~100% 的参数,而工业界今天最常用的微调其实只动 2%。 三个附录正好各补一个洞。

2. 顶层全景

附录 A 的三块(第 02~09 章每一行代码都站在上面):
张量库 —— 多维数组 + GPU 加速
自动微分引擎 —— 你写前向,它替你算所有梯度(反向传播的工程实现)
网络积木 —— nn.Module / Linear / Dataset / DataLoader / 优化器

附录 D 的三件稳定器(加在第 06 章的循环上):
学习率先爬坡(预热)→ 再沿余弦曲线降下来(余弦衰减)→ 梯度太长就截短(梯度裁剪)

附录 E 的省法(对比第 08 章的全量微调):
不学完整 ΔW,学两个小矩阵 A、B 让 AB≈ΔW;原权重冻结不动
1.24 亿参数冻结 → 只训 267 万个,准确率不降

图说:A 管「底下怎么跑」,D 管「怎么训得稳」,E 管「怎么微调得省」。

3. 核心原理

3.1 附录 A:PyTorch 就三样东西

第一样:张量库。 张量就是多维数组:标量(单个的数)是 0 维、向量是 1 维、矩阵是 2 维,再往上统称几维张量。 PyTorch 的张量和 NumPy 数组界面几乎一样,但多两个本事:能搬上 GPU 算、能自动记梯度1。 默认数据类型是 32 位浮点——精度与效率的折中,GPU 电路就是为 32 位优化的2

第二样:自动微分引擎(autograd)。 这是全书最该讲透的地基。 第 06 章说「反向传播一次算清全部参数的调整方向」,这里看它是怎么做到的。

先立一个概念:计算图——把一串运算画成有向图,每个节点是一次运算,边是数据的流向。 PyTorch 在你写前向代码时,在后台悄悄把这张图建了起来3。 书里用一行 logistic 回归走完整个机制(本章主走查之一,全是真实数字)4:

输入 x1=1.1,权重 w1=2.2,偏置 b=0.0,真标签 y=1.0
z = x1*w1 + b = 2.42 (净输入)
a = sigmoid(z) ≈ 0.918 (压到 0~1 之间,当「是类别 1」的置信度)
loss = 二元交叉熵(a, y) (与真标签比)

grad(loss, w1) = -0.0898 ← autograd 沿图倒着走算出来的
grad(loss, b) = -0.0817

负号的读法:损失对 w1 的梯度是负的,意味着「把 w1 往大里调,损失会变小」。 反向传播就是微积分的链式法则在这张图上从右往左推——每个节点只需知道局部的求导规则, 乘起来就得到损失对任何一个参数的影响5。而你要做的全部工作,是调用一次 .backward(); 算完梯度存在每个参数的 .grad 属性里6。 书里把读者该带走的压缩成一句:微积分由 PyTorch 替你做,你不需要手算任何导数7

补充(不在书里,依据我们的 frontier 书架):autograd 引擎内部怎么挂反向图、怎么调度, 我们拆过 PyTorch 源码。依据: shelf=ai-frontier-reference/pytorch#03-autograd-engine.md 事实=前向时每个输出挂上 grad_fn 节点,backward() 时 C++ 引擎按依赖计数拓扑调度整张图, 边算边把梯度累加进叶子参数的 .grad。

第三样:网络积木。 三条使用纪律,前九章其实都默默在用,这里说破:

  • 写模型 = 继承 nn.Module,__init__ 里定义层,forward 里写数据怎么流8;
  • 权重要用小的随机数初始化,为的是打破对称——如果所有节点初始相同, 它们在训练里做同样的运算、收同样的更新,永远学不出分工9;
  • 推理时包一层 torch.no_grad()——不训练就不必建图,省内存省算力10

数据侧的纪律:自定义 Dataset 只需写 __init____getitem____len__ 三个方法, 洗牌、拼批、多进程加载都交给 DataLoader11num_workers=0 时数据在主进程加载, GPU 会空等;作者的经验值是 4,但小数据集上开多进程反而更慢12

3.2 附录 A 的后半:GPU 与多卡

GPU 使用只有一条铁律:参与同一个运算的所有张量必须在同一块设备上, 否则 PyTorch 当场报 RuntimeError13。小模型上 GPU 可能反而更慢(数据搬运有成本), LLM 这种规模才真正见得着加速14

多卡训练书里讲了 PyTorch 的 DDP(DistributedDataParallel,分布式数据并行): 每张卡跑一个独立进程、各持一份完整模型副本;数据被 DistributedSampler 切成互不重叠的份; 每轮各卡独立算梯度,然后把所有卡的梯度取平均、同步回去,保证各副本不跑偏。 理想情况下两卡快一倍,八卡快八倍15。 两个实战注脚:DDP 在 Jupyter 笔记本里跑不起来(要起多进程,得用脚本); CUDA_VISIBLE_DEVICES=0,2 可以在不改代码的情况下挑卡16

3.3 附录 D:真实训练的三件稳定器

第 06 章的循环是教学版。真实训练里学习率不是常数,梯度也可能抽风;三件套各管一件事17:

① 学习率预热(warmup)。 开局先把学习率从很小的值线性爬到峰值 (书里的例子:0.0001 → 0.01,爬 20 步)。刚开局时模型还是随机的,大步更新容易把训练打散; 先小步热身再全速。预热步数通常取总步数的 0.1%~20%18

② 余弦衰减(cosine decay)。 爬到峰值后,学习率沿半个余弦曲线缓缓降到接近零。 越到后期步幅越小,免得在损失谷底附近来回冲过头19

③ 梯度裁剪(gradient clipping)。 给所有参数的梯度算一个总长度,这个长度叫 L2 范数(各分量平方和再开根号——多维世界里「长度」的算法); 超过阈值就整体等比缩小。书里的演示:裁剪前最大梯度 0.0411,按 max_norm=1.0 裁完变成 0.0185—— 一次偶发的梯度暴涨被摁住,不会把参数一脚踹飞20

三件套装进训练循环后,书在《The Verdict》上重训了一遍:15 轮,训练损失 0.041—— 函数工作正常;验证损失照样停在 6.9 附近——小数据上的过拟合,稳定器救不了,那是数据量的事21

3.4 附录 E:LoRA,用两个小矩阵冒充一次大更新

最后一个附录回答一个贵问题:第 08 章的微调只放开了 1% 的参数, 但工业界今天微调大模型的主流做法动得更少——它叫 LoRA(low-rank adaptation,低秩(只用少数几个方向——「秩」是矩阵里真正独立的方向数)适配)22

回顾微调在数学上做什么:对预训练权重 W,学一个增量 ΔW,让 W+ΔW 更适合新任务。 ΔW 和 W 一样大,全量微调就是要学这么多数。 LoRA 的主张:ΔW 其实用不着那么多自由度,拿两个小矩阵 A 和 B 的乘积来逼近它就够了23

具体账:W 是 768×768,全量 ΔW 要学 589,824 个数; LoRA 选一个很小的「秩」r(书里用 16),让 A 是 768×16、B 是 16×768, 乘积 AB 仍是 768×768,但要学的只有 768×16×2 = 24,576 个数——少了 24 倍24。 「低秩」这个名字就来自这里:把更新限制在一个低维子空间里, 赌「任务适配所需的调整方向」本来就集中在少数几个方向上。 另一个旋钮 alpha 是缩放系数,管 LoRA 旁路的输出对原层影响多大;常取 rank 的一半到两倍25

工程上有三个漂亮性质,都是书里实测的:

  • 起点即原模型。 B 初始化为全零,所以一开始 AB=0,模型行为和微调前逐位相同—— 书里验证过:加上 LoRA 后的初始准确率 46.25%,和第 08 章的数字一模一样26;
  • 原权重不动,旁路外挂。 由矩阵乘法的分配律,W·x + AB·x 可以分开算, 于是预训练权重保持原样,每个客户/任务只存一份小小的 LoRA 矩阵27;
  • 替换是机械的。 一个递归(自己调用自己,一层层往模型的子模块里钻)函数把模型里所有 nn.Linear 换成「原层 + LoRA 旁路」即可28

参数总账(本章第二个主走查,全是真实读数): 冻结全部参数后,可训参数从 124,441,346 变成 0; 挂上 rank=16、alpha=16 的 LoRA 后,可训参数是 2,666,528——约是原来的 2%29。 在第 08 章的垃圾短信任务上训 5 轮:训练准确率 100%、验证 96.64%、测试 98.00%—— 2% 的参数,达到并略超全量微调的成绩(97.21/97.32/95.67)30

但书里也诚实地记了一笔反直觉的账:这次 LoRA 训练花了 12.10 分钟, 比第 08 章的全量微调(约 6 分钟)还慢——小模型上 LoRA 不省时间, 因为前向多算了一步旁路,而 1.24 亿参数的反向传播本来也不贵。 LoRA 省时间要到大模型上才显现:那时反向传播是全账大头,冻结 98% 参数的梯度计算才是真省31

补充(不在书里,依据我们的 frontier 书架):LoRA 在工业界的标准实现(Hugging Face PEFT 库) 我们拆过,包括它怎么注册目标模块、怎么把训练好的旁路并回基座权重。 依据: shelf=ai-frontier-reference/peft#01-lora-math.md 事实=LoRA 的全部数学是三行:ΔW=B·A、h=Wx+(α/r)·BAx、B 初始化为零,其余全是工程。

4. 作者的判断与证据

有证据的: logistic 回归的两个梯度、梯度裁剪前后(0.0411→0.0185)、 LoRA 的三组参数计数与最终准确率——全部是书里印的真实输出4202930。 DDP 的「梯度跨卡平均同步」是 PyTorch 官方机制的忠实转述15

作者的判断:

  • num_workers=4 是「在许多真实数据集上的经验值」,不是定理12
  • rank=16、alpha=16 是「好的默认起点」;alpha 取 rank 的一半到两倍是通行做法25
  • 「LoRA 在大模型上才省时间」是作者基于机制的分析+本书的实测对照31

判断(我们的,不是书里的): 三个附录的编排(哪块内容放正文、哪块放附录的安排)泄露了这本书的诚实: 正文讲「最小可用」,附录才讲「真实该用」。读这本书的正确姿势是把附录当正文的一部分—— 真实项目里,D 的三件套是默认开启的,E 的 LoRA 是微调大模型的默认姿势。 如果错,会错在: 如果读者只做教学实验,正文的最小循环确实够用; 但只要模型上到十亿参数级,跳过附录 D/E 的写法会直接撞上不稳定和显存墙。

5. 边界与局限

  • 附录 A 是「够读这本书」的 PyTorch,不是 PyTorch 全貌;分布式只讲了 DDP 一种 (FSDP、流水线并行、张量并行都不在)。
  • LoRA 只演示了线性层上的应用;对其他结构(嵌入层、卷积)的适配不在书里。
  • 附录 D 的演示仍在《The Verdict》上,三件稳定器在真正的大规模训练里的收益书里没有实测数据—— 它们是行业共识,书里以「为什么防什么」的方式交代,没给大模型对照实验。
  • 附录 E 结束语里 97.33% 与 98.00% 两个测试准确率数字不一致(同一段落前后),原文如此; 按所列输出的最后一行,应是 98.00%。

6. 可带走的

  1. PyTorch 三件套:张量库、自动微分、网络积木;你写前向,梯度它来。
  2. 反向传播 = 链式法则在计算图上从右往左推;.backward() 一次算清,梯度落在 .grad
  3. 随机小初始化为了打破对称;推理用 no_grad() 省掉建图开销。
  4. 同一运算的所有张量必须在同一设备;DDP = 每卡一份模型 + 各吃一份数据 + 梯度平均同步。
  5. 训练三件套:学习率先预热(防开局打散)、再余弦衰减(防谷底冲过头)、梯度裁剪(防暴涨踹飞参数)。
  6. LoRA = 用 AB 两个小矩阵冒充 ΔW;B 置零保证起点即原模型;原权重不动、旁路可插拔。
  7. LoRA 的真实收益:参数省到 2%、每个任务只存小矩阵;小模型上它不省时间,大模型上才省。
  8. rank 和 alpha 的默认起点:16/16;alpha 取 rank 的一半到两倍。
  9. 附录不是可选读物:D 和 E 才是「真实训练与微调」的默认形态。

7. 原文地图

主题原书章原文位置
PyTorch 三件套appendix A—Introduction to PyTorchtext/16-apx-a-appendix-a-introduction-to-pytorch.txt:32(搜「three core components」)
计算图与 logistic 走查同上text/16-apx-a-appendix-a-introduction-to-pytorch.txt:462(搜「logistic regression forward pass」) · text/16-apx-a-appendix-a-introduction-to-pytorch.txt:572(搜「-0.0898」)
链式法则与 .backward()同上text/16-apx-a-appendix-a-introduction-to-pytorch.txt:503(搜「chain rule」) · text/16-apx-a-appendix-a-introduction-to-pytorch.txt:578(搜「.backward」)
随机初始化破对称同上text/16-apx-a-appendix-a-introduction-to-pytorch.txt:744(搜「performing the same operations」)
num_workers 与 GPU 铁律同上text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1055(搜「num_workers=4」) · text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1337(搜「same device」)
DDP同上text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1506(搜「DistributedDataParallel」) · text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1465(搜「averaged and synchronized」)
预热与余弦衰减appendix D—Adding bells and whistles to the training looptext/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:78(搜「warmup」) · text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:157(搜「Cosine decay」)
梯度裁剪实测同上text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:271(搜「0.0411」) · text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:281(搜「0.0185」)
LoRA 原理与分配律appendix E—Parameter-efficient fine-tuning with LoRAtext/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:28(搜「2106.09685」) · text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:62(搜「distributive law」)
LoRA 参数账与成绩同上text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:459(搜「124,441,346」) · text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:470(搜「2,666,528」) · text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:660(搜「100.00%」)
LoRA 在小模型上不省时间同上text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:619(搜「took longer」)

Footnotes

  1. 出处:「appendix A—Introduction to PyTorch」第 288 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:288,搜「similar to NumPy arrays」)。

  2. 出处:「appendix A—Introduction to PyTorch」第 345 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:345,搜「consuming less memory and computational resources」)。

  3. 出处:「appendix A—Introduction to PyTorch」第 493 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:493,搜「builds such a computation graph in the background」)。

  4. 出处:「appendix A—Introduction to PyTorch」第 462 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:462,搜「logistic regression forward pass」)与第 572 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:572,搜「-0.0898」)。 2

  5. 出处:「appendix A—Introduction to PyTorch」第 503 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:503,搜「chain rule」)。

  6. 出处:「appendix A—Introduction to PyTorch」第 578 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:578,搜「.backward」)。

  7. 出处:「appendix A—Introduction to PyTorch」第 593 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:593,搜「all you need to take away」)。

  8. 出处:「appendix A—Introduction to PyTorch」第 625 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:625,搜「subclass the torch.nn.Module」)。

  9. 出处:「appendix A—Introduction to PyTorch」第 744 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:744,搜「performing the same operations」)。 原文:「initializing model weights with small random numbers is desired to break symmetry during training」。

  10. 出处:「appendix A—Introduction to PyTorch」第 798 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:798,搜「no_grad」)。

  11. 出处:「appendix A—Introduction to PyTorch」第 914 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:914,搜「three main components of a custom Dataset」)。

  12. 出处:「appendix A—Introduction to PyTorch」第 1055 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1055,搜「num_workers=4」)。 2

  13. 出处:「appendix A—Introduction to PyTorch」第 1337 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1337,搜「same device」)。

  14. 出处:「appendix A—Introduction to PyTorch」第 1409 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1409,搜「memory transfer cost」)。

  15. 出处:「appendix A—Introduction to PyTorch」第 1506 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1506,搜「DistributedDataParallel」)与第 1465 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1465,搜「averaged and synchronized」)。 2

  16. 出处:「appendix A—Introduction to PyTorch」第 233 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:233,搜「Jupyter」)与第 1614 段(text/16-apx-a-appendix-a-introduction-to-pytorch.txt:1614,搜「CUDA_VISIBLE_DEVICES」)。

  17. 出处:「appendix D—Adding bells and whistles to the training loop」第 4 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:4,搜「learning rate warmup, cosine decay, and gradient clipping」)。

  18. 出处:「appendix D—Adding bells and whistles to the training loop」第 78 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:78,搜「warmup」)与第 94 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:94,搜「0.1% and 20%」)。

  19. 出处:「appendix D—Adding bells and whistles to the training loop」第 157 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:157,搜「Cosine decay」)。

  20. 出处:「appendix D—Adding bells and whistles to the training loop」第 215 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:215,搜「Gradient clipping」)与第 271 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:271,搜「0.0411」)。 2

  21. 出处:「appendix D—Adding bells and whistles to the training loop」第 389 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:389,搜「0.041」)与第 394 段(text/19-apx-d-appendix-d-adding-bells-and-whistles-to-the-trai.txt:394,搜「overfit」)。

  22. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 4 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:4,搜「Low-rank adaptation」)。

  23. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 28 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:28,搜「2106.09685」)。 LoRA 原论文:Hu et al., arXiv:2106.09685。

  24. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 18 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:18,搜「large weight matrix」)与第 334 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:334,搜「rank governs」)。 24 倍这一算式是我们代算的:768×768=589,824 对 768×16×2=24,576,比例 24:1——属于对书里机制的算术展开,不是书里印的数字。

  25. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 473 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:473,搜「good default choices」)。 2

  26. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 387 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:387,搜「initialized with zero values」)与第 561 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:561,搜「46.25%」)。

  27. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 62 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:62,搜「distributive law」)与第 73 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:73,搜「separate」)。

  28. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 394 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:394,搜「replace_linear_with_lora」)。

  29. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 459 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:459,搜「124,441,346」)与第 470 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:470,搜「2,666,528」)。 2

  30. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 660 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:660,搜「100.00%」)。 原文:「Training accuracy: 100.00% Validation accuracy: 96.64% Test accuracy: 98.00%」。 2

  31. 出处:「appendix E—Parameter-efficient fine-tuning with LoRA」第 617 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:617,搜「12.10 minutes」)与第 619 段(text/20-apx-e-appendix-e-parameter-efficient-fine-tuning-with-.txt:619,搜「took longer」)。 原文:「for larger models, where backpropagation becomes more costly, models typically train faster with LoRA than without it」。 2