跟着LAP啥都学 跟着LAP啥都学

附录 A 深度学习的心智模型

      +

      本附录导读:深度学习的入门障碍通常不在代码,而在术语——梯度、反向传播、 batch、epoch、logits、过拟合……这些词到处出现,却很少有人停下来说明 它们在程序里对应什么。本附录逐个讲清这些概念,并与日常编程经验对照, 建立完整的心智模型。

      阅读定位:选读的背景材料。未接触过深度学习的读者,建议读完第 0 章、 进入第 1 章之前先读本附录;有经验的读者可跳过,用到时再回来查阅。 本附录的习题需要操作第 0 章的实验,请在完成第 0 章后做。

      学完本附录你能: . 用「可微函数 + 1.63 亿个可调数字」理解模型与训练的本质; . 说清梯度、反向传播、学习率、优化器在 LibTorch API 里的对应物; . 掌握 batch / epoch / step / train / eval / 过拟合这些高频词; . 打通 logits → softmax → 采样 的概率链路,明白 GPU 到底快在哪。

      产出与代码:无新代码。全部概念都映射到第 0 章 chapter00_hello_torch 已跑过的 实验,以及后续每一章的对应位置。

      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);  // 推理
      };

      这个结构背后是三个关键事实:

      1. 模型就是一个函数:输入一列词元 ID,输出下一个词元的分数。所谓 「推理」,就是调用一次 forward。

      2. 函数的行为完全由那 1.63 亿个数字决定。没有 if/else,没有规则引擎, 也没有查找表——第 4 章会亲手验证:全部代码只有矩阵乘法、加法、 求指数和取最大值这几种运算。

      3. 这个函数是可微分的:输出对每一个参数的偏导数都存在,而且可以 自动算出来。这是整个深度学习体系的地基——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 本质上是两样东西的组合:

      1. 一个带 shape 的多维数组——对标嵌套 std::vector<float>,但内存 连续、支持广播与高效切片。全书反复出现的形状语义要记熟: [batch, seq_len, emb_dim] 的三个维度分别是「一次并行处理几条样本 / 每条多少词元 / 每个词元用多长的向量表示」。

      2. 一套梯度记录——当张量的 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 会 更好」。

      • 更新规则:沿负梯度方向挪一小步:

      \[w \leftarrow w - \mathrm{lr} \cdot \mathrm{grad}\]

      其中 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 的导数, 等于沿途每一层的偏导数相乘。手推两层就能看清:

      \[y = w\,x, \quad L = (y - t)^{2}, \qquad \frac{\partial L}{\partial w} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial w} = 2\,(y - t)\,x\]

      其中 \(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(轮)

      完整遍历一遍训练数据。总 step 数 ≈ 样本数 ÷ batch_size × epoch 数

      外层循环跑了几圈

      optimizer(优化器)

      更新规则的实现库。SGD 就是朴素的 w -= lr·grad;Adam/AdamW 额外维护每个参数的「动量」与「自适应步长」,收敛更快更稳

      带缓存与自适应的策略升级

      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. 能向同事解释「训练一个大模型」和「调 1.63 亿个变量的函数最小化」是同一件事

      2. 说出 zero_grad / backward / step 三行各自对应什么数学操作

      3. 手推 A.5 两层例子的 \(\partial L/\partial w = 2\,(y - t)\,x\), 并理解为什么 backward ≈ 2× forward

      4. 给定样本数 1045、batch_size 8,算出 1 epoch 的 step 数 (≈130,drop_last 会丢弃不足一批的尾巴)

      5. 解释为什么 fp32 训练显存 ≈ 16N 字节(N 为参数个数),是推理 (4N,即权重本身)的 4 倍

      6. 说出 train/eval 模式差异的一个具体例子(dropout)

      7. 从 logits 到采样词元的完整链路能不看书画出来

      习题

      1. 在第 0 章 chapter00_hello_torch 的 §E 实验里把 x 的三个值改掉,先手算 grad 应该是多少,再运行验证——确认自己已掌握梯度的自动计算规则。

      2. 把 §H 训练模板的学习率放大 1000 倍再缩小 100 倍各跑一次,观察 loss 的变化——直观体会 A.4 里「步长」的三种状态。

      3. 用 A.8 的显存公式估算:fp32 训练 GPT-2 medium(355M)需要多少 GB? 与第 6 章给出的「≥12 GiB」建议对照,解释差额去了哪里(激活值)。

      4. 写一个 10 行以内的 C++ 循环,模拟「自回归生成」:给定一张 5 词的 概率表(std::discrete_distribution),循环采样 20 步并打印—— 这是语言模型生成的最小骨架,第 4 章会把它换成真正的 GPT。

      5. (思考)A.9 映射表里「消融实验 ↔ 用注释解释为什么」这一行,你认同 吗?代码注释可能出错,实验结果不会——第 8 章会专门讨论这个方法论。

      拓展阅读

      • LibTorch 官方 Autograd 教程: https://pytorch.org/cppdocs/ 本附录 A.3/A.5 的机制,在官方文档的 Autograd 一节有更完整的表述

      • 3Blue1Brown 的神经网络系列视频(第 1–4 集)——梯度下降与反向传播 最直观的可视化讲解,与本章内容互为补充