附录 A 深度学习的心智模型
|
本附录导读:深度学习的入门障碍通常不在代码,而在术语——梯度、反向传播、 batch、epoch、logits、过拟合……这些词到处出现,却很少有人停下来说明 它们在程序里对应什么。本附录逐个讲清这些概念,并与日常编程经验对照, 建立完整的心智模型。 阅读定位:选读的背景材料。未接触过深度学习的读者,建议读完第 0 章、 进入第 1 章之前先读本附录;有经验的读者可跳过,用到时再回来查阅。 本附录的习题需要操作第 0 章的实验,请在完成第 0 章后做。 学完本附录你能: . 用「可微函数 + 1.63 亿个可调数字」理解模型与训练的本质; . 说清梯度、反向传播、学习率、优化器在 LibTorch API 里的对应物; . 掌握 batch / epoch / step / train / eval / 过拟合这些高频词; . 打通 logits → softmax → 采样 的概率链路,明白 GPU 到底快在哪。 产出与代码:无新代码。全部概念都映射到第 0 章 |
A.1 模型就是一个函数
剥掉所有封装,「大模型」的结构如下:
// GPT-2 small 的本质,去掉所有修饰之后就是这个形状:
struct GPT2 {
std::vector<float> weights; // 163,009,536 个 float —— 这就是参数
std::vector<float> forward(const std::vector<int>& token_ids); // 推理
};
这个结构背后是三个关键事实:
-
模型就是一个函数:输入一列词元 ID,输出下一个词元的分数。所谓 「推理」,就是调用一次
forward。 -
函数的行为完全由那 1.63 亿个数字决定。没有 if/else,没有规则引擎, 也没有查找表——第 4 章会亲手验证:全部代码只有矩阵乘法、加法、 求指数和取最大值这几种运算。
-
这个函数是可微分的:输出对每一个参数的偏导数都存在,而且可以 自动算出来。这是整个深度学习体系的地基——A.5 讲为什么。
于是「训练」的定义随之确定:用某种算法系统地调整这 1.63 亿个数字,
让函数的输出越来越接近期望。第 0 章 chapter00_hello_torch 里的第一个「模型」
只有 17 个可训练参数,GPT-2 有 1.63 亿个——训练它们在机制上是同一件
事,区别只在规模。
A.2 参数的组织形式:矩阵
参数不是散装的 1.63 亿个数字,而是组织成一个又一个矩阵。全书最常用的 零件是全连接层:
torch::nn::Linear(768, 3072)
// 数学上就是:y = W · x + b
// 其中 W 是 768×3072 的矩阵(2,359,296 个参数),b 是 3072 维向量
每一次 Linear::forward 就是一次矩阵乘向量加偏置。深度学习如此依赖
矩阵乘法,原因有两个:
-
表达力:矩阵乘法是「对所有输入特征做所有可能的线性组合」—— 2.36M 个参数各司其职,把 768 维输入映射成 3072 维特征;
-
硬件效率:矩阵乘法可以完全并行(A.8),GPU 把它跑到极致。
那么「知识」存在哪里?就存在这些数字的取值里。第 5 章训练后你会看到, 正是这些经梯度反复微调的数字,让模型从输出乱码变成输出英文句子; 第 5 章末尾把 OpenAI 官方权重装进我们手写的模型——同一份结构、不同的 数字取值,立刻生成连贯文本。这是「数字即知识」最直接的证据。
A.3 张量 = 多维数组 + 梯度记录
torch::Tensor 本质上是两样东西的组合:
-
一个带 shape 的多维数组——对标嵌套
std::vector<float>,但内存 连续、支持广播与高效切片。全书反复出现的形状语义要记熟:[batch, seq_len, emb_dim]的三个维度分别是「一次并行处理几条样本 / 每条多少词元 / 每个词元用多长的向量表示」。 -
一套梯度记录——当张量的
requires_grad()为 true 时,LibTorch 会记录所有涉及它的运算,构成一张计算图(A.5)。调用backward()后,梯度就写入tensor.grad()。
第 0 章 §E 的实验值得回头看:对 x 设置 requires_grad 后执行
y.sum().backward(),x.grad() 输出 [2, 4, 6]——梯度不是查表查出
来的,而是沿着计算图自动算出来的。这套记录机制,就是训练能够自动化
的基础。
A.4 训练 = 梯度下降:一行更新规则的三个组成部分
「训练」拆开是三个概念加一行更新公式:
-
损失(loss):一个标量,量化「这次输出有多差」。损失越小越好—— 它就是训练这个优化问题的目标函数。不同任务用不同的损失(生成用 交叉熵,分类也用交叉熵,第 5/6/7 章都会遇到)。
-
梯度(gradient):\(\partial\,\mathrm{loss}/\partial w\)——对每个参数 w,loss 在它这个方向上 的斜率。斜率为正说明「调大 w 会更差」,斜率为负说明「调大 w 会 更好」。
-
更新规则:沿负梯度方向挪一小步:
其中 lr 是步长,即「学习率」。
三个概念在 LibTorch API 里的对应物,就是第 0 章 §H 已经跑过的训练 五步模板:
optimizer.zero_grad(); // 1. 清空上一轮的梯度
loss.backward(); // 2. 反向传播:计算并写入全部梯度(A.5)
optimizer.step(); // 3. 按更新规则调整全部参数(内含 w -= lr·grad)
学习率是全书出场率最高的超参数:太小,参数每步移动太慢,训练耗时 极长;太大,每一步跨过谷底、来回震荡,甚至发散(loss 变成 NaN)。 第 8 章的「预热 + 余弦衰减」就是让步长随训练进程按计划变化——这是 从大量实验中总结出的工程经验。
A.5 反向传播 = 链式法则的自动实现
梯度从哪来?数学上是链式法则:复合函数 \(L(f(g(w)))\) 对 w 的导数, 等于沿途每一层的偏导数相乘。手推两层就能看清:
其中 \(t\) 是目标值;两个因子分别是外层导数 \(\partial L/\partial y\) 与内层导数 \(\partial y/\partial w\), 相乘即可。
真实模型是几万层这样的复合,手推不现实。LibTorch 的方案:forward 时把每一步运算记进计算图,backward 时从 loss 出发沿图反向走,沿途把 局部导数相乘、汇总到每个参数的梯度上。这就是「自动微分」:你只写 forward,框架自动补全 backward。
两个成本直觉,后面章节都会用到:
-
backward 的计算量约为 forward 的 2 倍——反向要走同样的图;
-
计算图的中间结果(激活值)必须保留到 backward 使用——这是训练比 推理更吃显存的根本原因,第 9 章 LoRA 的显存估算、第 10 章训练/ 推理分离的动机都源于此。
A.6 训练词汇表:batch / epoch / step / optimizer / 过拟合
这组词在后续每一章都会高频出现,这里一次讲清:
| 术语 | 含义 | 直观类比 |
|---|---|---|
batch(批) |
一次并行前向的样本数。GPU 是 SIMD 机器,batch 维是「免费」的并行度——8 个样本同时算和 1 个样本单独算耗时几乎相同 |
批处理 / 向量化 |
step(步) |
一次参数更新(一组 batch 前向 + backward + step) |
循环体执行一次 |
epoch(轮) |
完整遍历一遍训练数据。 |
外层循环跑了几圈 |
optimizer(优化器) |
更新规则的实现库。SGD 就是朴素的 |
带缓存与自适应的策略升级 |
train() / eval() |
模式开关:dropout 等机制只在 train 模式生效(第 3/4 章),评估前必须切 eval |
debug 与 release 的行为差异 |
过拟合(overfitting) |
训练 loss 一路下降、验证 loss 反而上升——模型把训练集「背下来了」而不是学到规律 |
刷题背答案 vs 掌握方法:换一套新题就不会了 |
「验证集」最贴切的类比是单元测试:训练过程中定期用没参与训练的数据 评估一次,监控泛化能力。第 5 章会看到一次典型的过拟合 (train 0.67 / val 6.42)——训练集上表现越好,验证集上表现越差。 第 8 章会讲诊断它的三个信号。
A.7 从分数到概率:logits → softmax → 采样
语言模型的输出是 logits——一个 50257 维的向量,每个分量是「下一个
词元是该词」的未归一化分数(可正可负,无界)。它还不是概率,中间隔
一步 softmax:
logits: [2.1, 0.3, -1.2, ...] ← 未归一化分数
↓ softmax:指数化后归一化,全部变成 (0,1) 且和为 1
probs: [0.79, 0.13, 0.04, ...] ← 概率分布
↓ 采样(按概率抽签)或 argmax(贪心取最大)
next_token: "的" ← 下一个词元
softmax 有两个重要性质:它保持分数的大小顺序(所以 「取概率最大」等价于「取 logit 最大」,第 4 章的贪心解码直接用 argmax);它处处可导(因此概率值才能参与训练)。
「生成一段话」由此成为一个循环——自回归:把采样出的词元拼回输入, 再跑一次 forward,再采样……直到出现停止符。第 4 章实现这个循环的 最简版(贪心),第 5 章加上温度与 Top-k 两个调节分布形状的参数, 第 13 章的推理模型(思维链、投票)也全部建立在这个循环上。
A.8 为什么需要 GPU:并行与显存估算
神经网络 95% 以上的浮点运算是大矩阵乘法(A.2),而矩阵乘法由海量相互 独立的乘加构成,正是 GPU 擅长的运算类型:
-
CPU:十几个核心,单个延迟极低,擅长复杂分支逻辑;
-
GPU:上千个乘加单元,吞吐量极高,擅长「同一操作对一大片数据」。
[8, 1024, 768] × [768, 3072] 这样的乘法在 GPU 上由几万个线程同时
计算,这也是 batch 维「免费并行」的原因(A.6)。
显存要提前估算,这个框架全书多处会用到:fp32 训练一个 N 参数的模型 ≈ 4N 字节(参数)+ 4N(梯度)+ 8N(Adam 系优化器状态)≈ 16N 字节。 124M 参数的小模型就要约 2 GB 起步,355M 的 medium 在长序列下会占用 十几 GB——选购硬件前先算这笔账。第 6 章冻结 94% 的参数(可训练 5.70%)、第 9 章 LoRA 只训 2.67M 参数(2.10%),省的正是梯度与优化器状态这两块。
A.9 编程概念 ↔ 深度学习概念 对照表
最后把两套术语对应起来,这张表值得反复回看:
| 日常编程 | 深度学习 | 全书出现位置 |
|---|---|---|
struct 的数据成员 |
参数 / 权重 |
第 4 章参数核算 |
函数调用 |
forward / 推理 |
每一章 |
单元测试 |
验证集评估 |
第 5–7 章 |
profile 热点图 |
loss 曲线 |
第 5/8 章 |
调优(线程数/缓存大小) |
调超参(lr/batch/epoch) |
第 5/8 章 |
编译失败 |
不收敛 / loss 变 NaN |
第 8 章梯度裁剪 |
未固定随机种子的并发 bug |
没固定 seed 的不可复现 |
第 0 章四行开场 |
用注释解释「为什么」 |
用消融实验证明「为什么」 |
第 8/13 章 |
热更新 / 插件 |
LoRA 适配器 |
第 9 章 |
Web 服务部署 |
训练-推理分离 + REST API |
第 10 章 |
本附录小结
-
模型 = 一个由 1.63 亿个数字决定行为的可微函数;推理是调用,训练是调参。
-
参数以矩阵形式组织(
Linear即 \(y = W\,x + b\));「知识」就是数字的取值。 -
张量 = 多维数组 + 梯度记录;
requires_grad/backward()/grad()是训练引擎的三个核心 API。 -
训练 = 梯度下降:loss 定目标、grad 定方向、lr 定步长、optimizer 定策略。
-
batch/epoch/step 是计数单位,train/eval 是行为开关,过拟合 = 记住了训练集而非学到规律。
-
logits → softmax → 采样是概率链路;文本由自回归循环生成;GPU 的价值在并行矩阵乘法与 16N 字节的显存估算。
自测清单
能不回看就答出这些,本章就算过关:
-
能向同事解释「训练一个大模型」和「调 1.63 亿个变量的函数最小化」是同一件事
-
说出
zero_grad / backward / step三行各自对应什么数学操作 -
手推 A.5 两层例子的 \(\partial L/\partial w = 2\,(y - t)\,x\), 并理解为什么 backward ≈ 2× forward
-
给定样本数 1045、batch_size 8,算出 1 epoch 的 step 数 (≈130,drop_last 会丢弃不足一批的尾巴)
-
解释为什么 fp32 训练显存 ≈ 16N 字节(N 为参数个数),是推理 (4N,即权重本身)的 4 倍
-
说出 train/eval 模式差异的一个具体例子(dropout)
-
从 logits 到采样词元的完整链路能不看书画出来
习题
-
在第 0 章
chapter00_hello_torch的 §E 实验里把x的三个值改掉,先手算grad应该是多少,再运行验证——确认自己已掌握梯度的自动计算规则。 -
把 §H 训练模板的学习率放大 1000 倍再缩小 100 倍各跑一次,观察 loss 的变化——直观体会 A.4 里「步长」的三种状态。
-
用 A.8 的显存公式估算:fp32 训练 GPT-2 medium(355M)需要多少 GB? 与第 6 章给出的「≥12 GiB」建议对照,解释差额去了哪里(激活值)。
-
写一个 10 行以内的 C++ 循环,模拟「自回归生成」:给定一张 5 词的 概率表(
std::discrete_distribution),循环采样 20 步并打印—— 这是语言模型生成的最小骨架,第 4 章会把它换成真正的 GPT。 -
(思考)A.9 映射表里「消融实验 ↔ 用注释解释为什么」这一行,你认同 吗?代码注释可能出错,实验结果不会——第 8 章会专门讨论这个方法论。
拓展阅读
-
LibTorch 官方 Autograd 教程: https://pytorch.org/cppdocs/ 本附录 A.3/A.5 的机制,在官方文档的 Autograd 一节有更完整的表述
-
3Blue1Brown 的神经网络系列视频(第 1–4 集)——梯度下降与反向传播 最直观的可视化讲解,与本章内容互为补充