# 07-16 上午：机器学习基础

> [!NOTE]
>
> **与后续实验的联系**
>
> Lab 5 会用到模型结构、损失函数、训练与推理差异。学习时应将概念落实为可计算对象，例如数据怎样切分、张量形状、损失对参数的导数和评估指标。公式与实现细节可对照深度学习教材、PyTorch 文档和 Transformer 原论文。[^/study/hpc/notes/0716a/#dlbook][^/study/hpc/notes/0716a/#pytorch-autograd][^/study/hpc/notes/0716a/#transformer]

监督学习从带答案的数据中拟合函数。房价、气温、购买意愿和图像类别的任务形式不同，但都要先约定输入是什么、答案是什么，以及怎样判断预测是否可靠。交叉熵损失（cross-entropy）、梯度、神经网络和语言模型都建立在这套边界上。[^/study/hpc/notes/0716a/#dlbook]

机器学习中的**特征**是输入里的一个可测量量，例如房屋面积、像素灰度或 token 编号；**标签**是希望模型给出的答案。训练会调整**参数**，学习率、网络层数和 batch 大小等训练前设置称为**超参数**。

张量可以先按维数理解。标量是一个数，向量是一列数，矩阵是二维表，三维及以上的数组统称张量。文本 batch 常记为 `[B,T,d]`：`B` 是同批样本数，`T` 是每个样本的 token 数，`d` 是每个 token 的向量维度。读模型日志时，将这三个维度对应到实际对象，shape 就不再只是一串数字。

## 人工智能范式

课上从图灵测试和 1956 年达特茅斯会议讲起。早期研究者关心的是怎样让机器表现出推理、学习和决策能力，而不只是会做算术。沿着这个问题，人工智能形成过三种很有代表性的思路。

| 范式 | 知识从哪里来 | 适合的问题与边界 |
| --- | --- | --- |
| 符号主义 | 人写出符号、规则和推理链 | 规则可检查，适合边界清楚的领域。真实世界例外太多时，规则难以穷举。 |
| 连接主义 | 从样本中调整大量连接权重 | 擅长从图像、语音、文本中学习模式。内部表示较难逐条解释。 |
| 行为主义 | 根据动作后的奖励或惩罚调整策略 | 适合连续决策。奖励定义和环境反馈会直接塑造行为。 |

工程系统常混合这些路线。大模型可以调用规则工具，强化学习也可以用神经网络表示策略。

深度学习近年的扩展依赖算法、数据、GPU 和工程工具同时成熟。模型规模增大后，数据重复、显存、通信和推理延迟都会成为系统约束。

机器学习是连接主义中常用的技术路径之一。机器学习包括线性模型、树模型、核方法和神经网络，神经网络只是其中一类模型。下面先沿监督学习这条最容易观察的数据路径展开：给定输入和答案，怎样定义损失、更新参数，再检查模型能否处理未见数据。

## 监督学习模型

给定输入特征 \\(x\\) 和期望输出 \\(y\\)，模型 \\(f(x;\\theta)\\) 用参数 \\(\\theta\\) 产生预测 \\(\\hat y\\)。训练通过调整参数，使模型在未见样本上也能给出有用的预测。

在写模型之前，先把任务说明白。描述一个监督学习问题时，应写清下列三项。

- 输入是什么
- 输出是什么
- 怎样判定预测好坏

房价预测中，面积、位置等可作输入，房价是连续输出。垃圾邮件识别中，邮件内容是输入，类别是离散输出。前者是回归，后者是分类。

回归和分类的区别先在输出类型。回归输出实数，例如明天气温是 23.4 度，预测偏 0.5 度和偏 10 度有程度差别。

分类输出类别，例如邮件是垃圾邮件或正常邮件。类别之间通常没有大小顺序。因此回归常用平方误差，分类常用概率和交叉熵。

用前两天的温度预测明天温度时，直接平均两个温度属于人先写好的规则。若把前两天温度分别乘以待学习的系数 \\(A,B\\)，再除以待学习的尺度 \\(C\\)，哪些历史量更重要就交给数据决定。这个例子说明，输入选择带有人的判断，参数由训练得到，输出可信度还要由独立数据检验。

*建模先定义输入、输出和参数化函数。训练数据用于估计参数，任务定义仍需明确。*

模型复杂度要和问题匹配。输入与输出关系近似线性时，较深网络会增加训练成本和过拟合机会。数据明显弯曲、分段或带有交互关系时，一条直线会留下系统性误差。数据质量、标签定义和评价指标往往更早决定结论是否可信。

## 数据集划分

模型开始训练前，数据需要先分开。训练集、验证集、测试集和应用数据各自承担不同角色。

- 训练集用于拟合模型参数。
- 验证集用于选择模型、超参数、训练轮数等。
- 测试集只用于最后一次独立评估。
- 应用数据是真实部署时模型将面对的新数据。

可以把训练集理解为平时练习，验证集是反复模拟考试，测试集是最后不能提前看题的考试。若看着测试集结果不断改模型，测试集事实上已参与训练，报告的分数会过于乐观。

数据泄露的形式不止把测试样本直接放进训练集。若按病人划分前就做归一化统计，测试病人的信息已经影响了训练用的均值和方差；若按时间预测天气却随机打乱后划分，模型可能利用未来信息；若同一用户的多条记录分别落在训练集和测试集，测试集也不再是完全独立的新用户。这些情况都让评估分数好看，部署效果却变差。

*训练集拟合参数，验证集调整超参数，测试集只做最后评估。应用数据是部署时真正面临的新输入。*

形式上，一个监督学习数据集可写为 \\(D=\\{(x\_1,y\_1),\\ldots,(x\_n,y\_n)\\}\\)。每个 \\(x\_i\\) 是一行输入特征，\\(y\_i\\) 是它的标签。训练集通常占较大部分，因为参数需要足够多的样本来估计。验证集和测试集的比例取决于数据量、任务风险、时间顺序和评估成本。数据量很小时，留出过大的测试集会使训练不稳定，可用交叉验证缓解。

### 数据泄露

训练/测试的样本即使文件名不同，也可能高度相关。例如同一个人的多张相似照片、一段时间序列切出的相邻窗口、同一病人的多次记录，不能随意随机分到两边。模型在训练时已经看过非常接近的信息，测试分数就不代表面对真正新对象的能力。

划分时要保证独立性，并使训练和应用的分布尽量一致。按人、按设备、按时间或按场景分组，有时比简单随机切分更合理。所谓数据泄露，不只是把标签列放进特征，也包括这种不应跨集合的关联。

*划分单位应服从真实部署时的新样本。按人、按时间或按场景分组常比简单随机更合理。*

一个例子用同一人的不同照片来说明这一点。若训练集中已经有某人的多张正面照，测试集中再放同一个人换角度的照片，模型可能靠背景、脸部细节或拍摄条件取得很高分，实际却没有学会识别新的人。医学数据常要按患者分组，传感器数据常要按设备或采集批次分组，时间序列则通常用过去训练、未来测试。划分单位应当服从部署时真正面对的新样本是什么。

### 留出法与交叉验证

数据足够多时，可以留出一部分，例如 80% 训练、20% 测试，再从训练部分划出验证集。数据很少时，单次划分的偶然性较大，可用 \\(K\\) 折交叉验证。数据分成 \\(K\\) 份，每次用一份验证或测试，其余部分训练，最后平均多个结果。

交叉验证能更充分利用小数据集，但成本是训练 \\(K\\) 次。无论采用哪种方法，最后仍应保留一份完全不参与调参的测试数据，作为最终报告依据。

*交叉验证反复轮换训练和验证折，用于减小小数据集单次划分的偶然性。最终测试集仍不参与调参。*

> [!NOTE]
>
> **思考题**
>
> 为什么验证集分数变好不代表测试集一定变好？如果反复用验证集调模型，它承担了什么角色？
>
> > **答案**
> >
> > 验证集用于选择模型和超参数。反复根据它的分数调整，会把验证集的信息泄漏进决策，分数逐渐偏向乐观估计。它从“独立评估”变成“训练过程的一部分”。测试集应只在最后使用，且不能参与模型选择、阈值调优或预处理统计的计算。

> [!NOTE]
>
> **思考题**
>
> 为什么不能把测试集里的样本拿来帮助选择模型？
>
> > **答案**
> >
> > 测试集的意义是估计模型在未见数据上的表现。一旦参与选择，它的信息就进入决策，分数会偏乐观。应从训练集中再划出验证集用于调参，测试集只在最后评估一次。

> [!NOTE]
>
> **思考题**
>
> 为什么计算均值和标准差时只能用训练集？如果用全集统计后再划分，会留下什么问题？
>
> > **答案**
> >
> > 全集统计把测试样本的分布信息带进了预处理参数。模型训练时虽然只看训练样本，但输入已经被测试集影响过，评估不再独立。这属于数据泄露，常见于归一化、特征选择和缺失值填充等步骤。

## 回归

最简单的一元线性回归写成

\\\[ \\hat y=wx+b \\\]

\\(w\\) 是斜率，\\(b\\) 是截距。多特征时写成 \\(\\hat y=w^Tx+b\\)。真实数据通常不能被一条线完全穿过。训练的任务是选出一条总体上误差较小的线。

*特征面积与标签价格成对出现。线性回归寻找一条总体误差较小的直线。*

这里的 \\(w\\) 是从训练数据估出来的参数，它会随数据变化。面积单位换成平方厘米时，\\(w\\) 的数值会相应变化。加入位置、楼层、建成年份后，模型变为多元回归，每个特征各有一个权重。把不同量纲的特征直接放在一起时，常常还需要标准化，以免某一列的数值范围压过其他特征，并让梯度下降更稳定。

*这个回归实例把抽象公式落到学习时间与考试分数的样本表上，便于对照预测与真实标签。*

常用损失为均方误差（MSE，Mean Squared Error）。

\\\[ \\mathrm{MSE}(w,b)=\\frac{1}{n}\\sum\_{i=1}^n(\\hat y\_i-y\_i)^2 \\\]

其中 \\(n\\) 是本次参与计算的样本数，\\(\\hat y\_i\\) 是第 \\(i\\) 个样本的预测值，\\(y\_i\\) 是对应标签。误差平方后，大偏差受到更大惩罚，也避免正负误差互相抵消。对线性回归，最小二乘法可直接求解；也可以和更一般的模型一样用梯度下降优化。

*平方损失让较大的残差承担更高代价，并把整批样本汇总为一个可优化标量。*

如果只有一个特征和截距，最小二乘法能通过令损失对 \\(w,b\\) 的偏导数为零，直接解出一组参数。课堂没有要求手推这套公式，理解它的地位就够了。线性模型结构简单时可以解析求解；模型一复杂，往往就没有这样的闭式解，只能靠后面介绍的迭代优化。两条候选直线看上去都差不多时，损失函数提供了统一的比较标准。

线性回归适合参数以线性方式出现的模型。作图观察数据是必要步骤。关系弯曲时，可增加合理的非线性特征，例如 \\(x^2\\)，或选择适合的非线性模型。加入 \\(x^2\\) 后，对参数仍然是线性的，\\(\\hat y=w\_1x+w\_2x^2+b\\) 仍可用线性回归训练。

*残差随输入呈现规律时，误差可能包含尚未表达的趋势。*

异常值会显著拉动最小二乘拟合的直线。处理前应判断异常点是录入错误、测量噪声、少见但真实的对象，还是任务本来就应覆盖的情形。不同结论会导致不同的数据清洗或鲁棒回归选择。

*少数远离主体的异常点会明显改变最小二乘直线。处理前先判断其来源，再决定清洗还是使用鲁棒方法。*

> [!NOTE]
>
> **思考题**
>
> 平方误差为什么对离群点敏感？什么时候这会成为实际问题？
>
> > **答案**
> >
> > 平方误差把误差平方，远距离样本的影响按平方放大，少数异常房价或测量错误可能主导梯度。若任务更关心多数样本或希望对异常稳健，可以检查数据、截断目标、改用 Huber 等损失，或明确任务本身确实要重罚大误差。

## 分类与概率输出

二分类常从线性得分 \\(z=w^Tx+b\\) 开始。\\(z\\) 可取任意实数，不能直接当作概率，因此用 sigmoid 映射。

\\\[ \\sigma(z)=\\frac{1}{1+e^{-z}},\\qquad p(y=1\\mid x)=\\sigma(z) \\\]

逻辑回归用于分类。模型先输出属于正类的概率，再按阈值作决定。阈值不必固定为 0.5。漏报和误报的代价不同，合适的阈值也不同。

*sigmoid 将线性得分映射为正类概率。阈值再把概率转成决策。*

*二分类用 sigmoid 得到单个概率。多分类使用 softmax，为多个类别输出一组和为 1 的概率。*

以购买意愿为例，年龄和收入可以组成特征 \\(x=(x\_1,x\_2)\\)，模型计算 \\(z=\\theta\_0+\\theta\_1x\_1+\\theta\_2x\_2\\)，再得到 \\(p=\\sigma(z)\\)。若业务把 0.5 作为阈值，\\(p\\ge0.5\\) 判为购买。这个阈值只是一种决策规则，不是概率模型自己决定的。医疗筛查常希望少漏掉阳性样本，会把阈值降低；人工复核昂贵或误报代价很高时，阈值又可能提高。调阈值时必须在验证集上观察精确率、召回率等指标，不能只盯总体准确率。

*真实表格数据进入逻辑回归时，一批特征，例如年龄和收入，对应一个二分类标签，例如购买意愿。*

### 决策边界

对于两个特征，\\(z=\\theta\_0+\\theta\_1x\_1+\\theta\_2x\_2=0\\) 在平面上是一条直线。sigmoid 在 \\(z=0\\) 时恰好输出 0.5，所以这条直线就是阈值为 0.5 时的决策边界。边界一侧的点概率大于 0.5，另一侧小于 0.5。特征更多时，几何图形不便直接画出，线性组合仍会把输入空间划成两个半空间。

这也说明逻辑回归能够解决的范围。它的输入到概率的关系是 S 形的，决策边界对原始特征却仍是线性的。若两类样本像同心圆一样交错，单条直线分不开，可以加入 \\(x\_1^2,x\_2^2,x\_1x\_2\\) 等特征，或者改用更复杂的模型。模型名称中的线性指参数组合形式。边界形状还取决于送入模型的特征空间。

> [!NOTE]
>
> **什么是 TP、FP、FN？**
>
> 二分类评估要先约定正类。以购买意愿为例，若把会购买作为正类，验证集中每个样本都会落入下面四类之一。
>
> | 缩写 | 英文 | 真实标签 | 模型判断 | 通俗含义 |
> | --- | --- | --- | --- | --- |
> | TP | true positive | 会购买 | 判为购买 | 真会买的人被找到了 |
> | FP | false positive | 不会购买 | 判为购买 | 误报，把不会买的人当成会买 |
> | FN | false negative | 会购买 | 判为不会购买 | 漏报，真会买的人被漏掉 |
> | TN | true negative | 不会购买 | 判为不会购买 | 不会买的人被正确排除 |
>
> 精确率（precision）是 \\(TP/(TP+FP)\\)，问的是模型判为购买的人里，真正会买的占多少。召回率（recall）是 \\(TP/(TP+FN)\\)，问的是真正会买的人里，模型找到了多少。两者的分母不同，所以一个高并不保证另一个也高。

分类数据还要检查标签是否可信。购买意愿按点击、加购还是成交定义，会得到不同任务；人工标注不一致会形成噪声。类别极不均衡时，准确率可能掩盖问题，应结合精确率、召回率和错误代价选择阈值。阈值越高，误报通常越少，漏报也可能更多。

多分类把多个类别的得分经 softmax 归一化。

\\\[ p\_k=\\frac{e^{z\_k}}{\\sum\_j e^{z\_j}} \\\]

这里 \\(z\_k\\) 是第 \\(k\\) 个类别的原始分数，\\(p\_k\\) 是 softmax 后的概率；分母对所有类别下标 \\(j\\) 求和。每个 \\(p\_k\\) 在 0 到 1 之间，全部类别概率和为 1。二分类 sigmoid 可看作两类 softmax 的特例。

softmax 的指数会放大得分差距，也带来数值计算问题。实践中通常先从所有 \\(z\_k\\) 中减去最大值再取指数。

\\\[ \\operatorname{softmax}(z)\_k= \\frac{e^{z\_k-\\max\_jz\_j}}{\\sum\_j e^{z\_j-\\max\_jz\_j}} \\\]

分子分母同时乘以同一常数不会改变结果，却能避免 \\(e^{z\_k}\\) 因为 \\(z\_k\\) 很大而溢出。这个小技巧说明，公式成立和程序稳定运行之间还有一道工程关卡。深度学习框架常把 softmax 与交叉熵合成一个稳定算子，避免先得到接近零的概率、再单独取对数。

> [!NOTE]
>
> **思考题**
>
> 二分类模型输出 0.73，人工设定的判定阈值是 0.50。这个 0.73 是概率吗？阈值又由什么决定？
>
> > **答案**
> >
> > 若输出层经过 sigmoid，0.73 可以解释为模型预测的正类概率。阈值是把概率变成标签的决策规则，不一定取 0.50。召回更重要时可以降低阈值，误报代价更高时可以提高阈值，最终应通过验证集上的业务指标选择。

## 交叉熵与 NLL

交叉熵先看模型给真实答案多少概率，再对这个概率取负对数。设真实类别是 \\(k\\)，模型经 softmax 输出的类别概率为 \\(q\_k\\)，这一个样本的交叉熵就是

\\\[ L=-\\log q\_k \\\]

若把真实标签写成 one-hot 向量 \\(y\\)，同一个式子可以写成

\\\[ L=-\\sum\_i y\_i\\log q\_i \\\]

因为只有一个 \\(y\_i\\) 是 1，其余全是 0，所以最后仍只剩 \\(-\\log q\_k\\)。这里的 \\(\\log\\) 通常以 \\(e\\) 为底，概率越小，负对数越大。

| 模型给正确类别的概率 | 交叉熵 |
| --- | ---: |
| 0.01 | 4.61 |
| 0.10 | 2.30 |
| 0.50 | 0.69 |
| 0.90 | 0.11 |

两个模型可能都把类别选对，但一个给正确类别 0.9，另一个只给 0.3。只数错误个数时二者相同；交叉熵会让前者损失更小，也让后者知道正确答案的概率还不够高。

逻辑回归的二元形式是它的特例。若 \\(p\\) 表示正类概率，标签 \\(y\\in\\{0,1\\}\\)，则

\\\[ L=-\[y\\log p+(1-y)\\log(1-p)\] \\\]

若真实标签是 1、模型给出很小的 \\(p\\)，损失会很大；模型对正确类别越自信，损失越小。这个损失与概率模型匹配，且其梯度在 sigmoid 饱和区域的行为通常比 MSE 更适合优化。

对单个正样本 \\(y=1\\)，损失化为 \\(-\\log p\\)。当模型给出 \\(p=0.9\\) 时，损失较小；若自信地给出 \\(p=0.01\\)，\\(-\\log p\\) 会很大。负样本的情况正好对应 \\(-\\log(1-p)\\)。这种设计会特别惩罚方向错而且非常自信的预测，和分类概率的含义吻合。

*真实类别的概率趋近 0 时，负对数损失迅速增大。正确且自信时，损失趋近 0。*

损失函数不只是用来评分的一个公式。它决定训练时参数朝什么方向更新，也决定模型会重视哪类错误。回归、分类、类别不平衡、异常值敏感的任务，都可能需要不同损失。

还有个优化上的原因。sigmoid 两端趋于平坦，导数接近零。若对 sigmoid 后的概率使用均方误差，链式求导会额外乘上这个很小的导数；模型即使犯了严重错误，也可能得到很弱的更新信号。二元交叉熵和 sigmoid 配合时，输出层对线性得分的梯度可化简为 \\(p-y\\)，错误越大，校正信号通常越直接。这是逻辑回归把交叉熵作为默认损失的原因。

语言模型训练用的也是这个损失。每个位置的真实下一个 token 是类别 \\(k\\)，模型对词表做 softmax 得到 \\(q\_k\\)，就用 \\(-\\log q\_k\\) 训练。对 batch 内所有 token 取平均，通常称为 NLL（negative log-likelihood，负对数似然）。量化后仍用平均 NLL 检查模型是否为真实文本分配接近的概率；速度或显存改善不能替代这项质量检查。

> [!NOTE]
>
> **思考题**
>
> 语言模型每个位置为什么可以看成分类问题？为什么用交叉熵训练，而不是只统计下一个 token 是否猜对？
>
> > **答案**
> >
> > 词表里的每个 token 都是一个候选类别，真实下一个 token 是正确类别。softmax 输出整个词表上的概率，交叉熵就是 \\(-\\log q\_k\\)。只统计猜对或猜错没有连续梯度，也无法区分“正确 token 概率 0.4”和“概率 0.001”的差别；交叉熵会强烈惩罚后一种情况。

> [!NOTE]
>
> **交叉熵为何只看真实 token**
>
> 关键在 **one-hot 编码**，它表示只有目标位置是 1、其余都是 0 的一种向量形式。比如词表里 `cat` 是第 3 个词，那么下一个词是 cat 这个标签的 one-hot 向量在第 3 位是 1、其它位全是 0。
>
> 于是词表大小可以是数万，某个位置真实标签为 `cat` 时，one-hot 标签在 `cat` 位置为 1、其余位置为 0；交叉熵相乘后也只留下 `-log p(cat)`。模型给 `cat` 的概率从 0.1 提高到 0.5，损失从约 2.30 降到约 0.69，因此它直接惩罚真实下一个 token 概率太低。

## 一条训练样本怎样更新模型

先用一条很短的文本样本把训练过程落到张量上。设 tokenizer 已把一段序列编码为四个编号，其中最后一个是结束标记。

```text
完整 token 序列： [11, 27, 42, 2]
模型输入 input_ids： [11, 27, 42]
训练标签 labels：    [27, 42, 2]
```

第一个位置读到 `11` 后要预测 `27`，第二个位置读到 `11, 27` 后要预测 `42`。这就是标签右移一位的含义。图像分类也遵循相同结构，只是输入换成像素张量，标签换成类别编号。

模型前向计算后，会给每个位置的词表候选打分。若 batch 大小为 1、序列长为 3、词表大小为 \\(V\\)，输出 logits 的形状就是 `[1,3,V]`。交叉熵取第 0 个位置中编号 `27` 的分数、第 1 个位置中编号 `42` 的分数、第 2 个位置中结束标记的分数，汇总成一个损失 \\(L\\)。

下面的伪代码省略了数据加载和混合精度等工程细节，只保留一次更新真正发生的顺序。

```python
optimizer.zero_grad()                 # 清除上一个 batch 累积的梯度
logits = model(input_ids)             # [B, T, V]
loss = cross_entropy(logits, labels)  # 一个标量
loss.backward()                       # 计算每个可训练参数的梯度
optimizer.step()                      # 按学习率更新参数
```

`backward()` 沿前向计算图应用链式法则。梯度 \\(\\nabla\_\\theta L\\) 指向使当前损失上升最快的方向，优化器使用负梯度方向调整参数。重复处理许多 batch 后，训练损失通常下降；验证集和测试集随后检查这份变化是否能推广到未见样本。

推理只保留 `logits = model(input_ids)` 这一条前向路径，再根据任务输出类别或下一个 token。训练还要保存 activation（反向求导需要的中间张量）、计算梯度并维护优化器状态，因此显存和计算量通常更大。大模型的分布式训练页面会继续解释梯度同步、参数分片和多卡通信。

> [!NOTE]
>
> **PyTorch 自动微分文档的说明（意译）**
>
> 前向计算记录由张量运算组成的计算图。反向传播从损失出发沿这张图应用链式法则，计算需要训练的参数梯度。[^/study/hpc/notes/0716a/#pytorch-autograd]

## 梯度下降

一次参数更新包含四步。

1. 用当前参数对输入做前向计算，得到预测；
2. 将预测与标签比较，计算损失；
3. 对参数求损失的梯度；
4. 按学习率沿负梯度方向更新参数，再处理下一批数据。

> [!NOTE]
>
> **Google Machine Learning Crash Course 对梯度下降的说明（意译）**
>
> 梯度下降从一组初始参数开始，反复计算损失对参数的梯度，并沿着使损失下降的方向更新参数。学习率决定每次更新的步长。[^/study/hpc/notes/0716a/#google-mlcc-gd]

下面的图把这四步连成一个训练循环。阅读时先看数据和当前权重怎样得到预测，再看损失如何导出更新方向。

*Google Machine Learning Crash Course 的图中，箭头对应前向计算、损失、梯度和参数更新。深度网络在每一步会包含更多张量与参数。[^/study/hpc/notes/0716a/#google-mlcc-gd]*

损失是参数的函数。梯度 \\(\\nabla\_\\theta L\\) 指向当前位置上升最快的方向，因此沿反方向更新。

\\\[ \\theta\\leftarrow\\theta-\\eta\\nabla\_\\theta L \\\]

\\(\\eta\\) 是学习率。训练的基本循环是，取一批训练样本，前向计算预测和损失，反向计算梯度，更新参数，重复直到达到停止条件。批量梯度下降一次用全体样本；随机/小批量梯度下降一次用一个或一小批样本，后者是深度学习的常见做法。

可以用只有两个样本的一元回归把这一步算完整。设 \\(w=0,b=0\\)，学习率 \\(\\eta=0.1\\)，样本为 \\((x,y)=(1,2)\\) 和 \\((2,3)\\)。前向预测是 \\(0,0\\)，残差是 \\(-2,-3\\)，梯度为

\\\[ \\frac{\\partial L}{\\partial w}=\\frac{1}{2}\[(-2)\\cdot1+(-3)\\cdot2\]=-4,\\qquad \\frac{\\partial L}{\\partial b}=\\frac{1}{2}\[(-2)+(-3)\]=-2.5 \\\]

更新后 \\(w=0.4,b=0.25\\)，两个预测都向上移动。若学习率太大，下一次可能越过合适值来回震荡；若太小，收敛速度会很慢。

*Google Machine Learning Crash Course 的图只展示两个参数时的几何直觉。深度网络的参数空间维度远高于三维图能够直接画出的范围。[^/study/hpc/notes/0716a/#google-mlcc-gd]*

实际训练会先初始化参数，再重复计算当前 batch 的损失和梯度，按学习率更新，直到达到预定轮数或验证指标不再改善。停止准则可以是梯度很小、参数变化很小，或验证集表现不再提高。对深度网络而言，找到足够好的点通常更实际。

*梯度下降反复执行初始化后的梯度计算和负梯度更新。学习率决定每一步走多远。*

### 线性回归的梯度

一元线性回归的预测是 \\(\\hat y\_i=wx\_i+b\\)，均方误差是各样本残差平方的平均。对 \\(w,b\\) 求导可得

\\\[ \\frac{\\partial L}{\\partial w}=\\frac{2}{n}\\sum\_{i=1}^n(\\hat y\_i-y\_i)x\_i, \\qquad \\frac{\\partial L}{\\partial b}=\\frac{2}{n}\\sum\_{i=1}^n(\\hat y\_i-y\_i) \\\]

模型对大多数样本预测偏低时，残差 \\(\\hat y-y\\) 大多为负，截距的梯度也会偏负。按减去梯度的规则更新后，\\(b\\) 会增大，整条线向上移动。斜率的更新还要乘上 \\(x\_i\\)，它会根据不同位置的误差决定线该变陡还是变缓。这个例子展示了梯度如何连续校正预测误差。

训练中常把一次完整遍历训练集称为一个 epoch。全批量梯度下降每个 epoch 只更新一次，梯度很稳定，但需要等所有样本算完。随机梯度下降一次使用一个样本，更新频繁但噪声较大。小批量方法介于两者之间。GPU 擅长对一批样本做同形矩阵运算，因此实际深度学习常用几十到几千个样本组成 batch。batch 的选择会同时影响显存、吞吐、梯度噪声和训练轮数。

学习率过大可能越过低点、震荡甚至发散。学习率过小时，训练进展很慢。学习率、网络层数、batch size、训练轮数等人为设置的量叫超参数。训练得到的权重和偏置才是模型参数。验证集用于选择超参数。

*区分参数和超参数，有助于正确使用验证集和测试集。*

在线性回归 \\(\\hat y=wx+b\\) 中，\\(w,b\\) 是参数，会随着训练数据自动变化。学习率、训练轮数和 batch 大小是超参数，需要在验证集上比较后确定。确定方案后，测试集只用于报告一次结果。

batch 大小会同时影响显存、吞吐和梯度噪声。训练记录应保存数据版本、随机种子、模型、学习率计划和评估代码。训练损失下降而验证指标不变时，应回看数据、任务和优化设置。

> [!NOTE]
>
> **思考题**
>
> 学习率过大或过小分别会出现什么现象？为什么同一模型里不同参数也可能需要不同尺度？
>
> > **答案**
> >
> > 学习率过大时，更新可能越过谷底，损失震荡甚至发散；过小时收敛很慢，可能停在平缓区域。特征尺度、参数初始值和损失面曲率不同，会让同一学习率对某些方向太慢、对另一些方向太快。归一化、自适应优化器和学习率调度都是针对这个问题。

> [!NOTE]
>
> **思考题**
>
> 梯度下降里的一步更新是 \\(\\theta \\leftarrow \\theta - \\eta\\nabla L(\\theta)\\)。这个式子中每个符号对应代码里的什么对象？
>
> > **答案**
> >
> > \\(\\theta\\) 是待训练参数，\\(\\nabla L\\) 是反向传播得到的梯度，\\(\\eta\\) 是学习率。一次 `optimizer.step()` 通常就是按这个式子把每个参数沿着负梯度方向移动。梯度的模长指示当前位置的坡度，学习率决定这一步走多远。

## 过拟合与欠拟合

训练集损失持续下降时，还要看模型在新数据上的表现。训练集越来越好、验证集先好后坏时，模型开始记住训练样本的偶然细节，这就是过拟合。训练和验证都不好时，常见原因包括模型表达能力不足、特征不足、训练不够或优化失败。

*训练曲线反映训练集上的拟合程度，验证曲线反映泛化。两者分离的位置可用于 early stopping。*

训练轮数也是超参数。训练损失下降、验证损失回升时，可用 early stopping 保留验证集最好的 checkpoint。处理过拟合时，可从有效数据、数据划分、模型容量、正则化和训练时长几个方向检查。标签错误和泄露仍要回到数据层修正。

正则化的形式也能说明它在做什么。L2 正则常写为

\\\[ L\_{\\text{total}}=L\_{\\text{data}}+\\lambda\\lVert w\\rVert\_2^2 \\\]

其中 \\(L\_{\\text{data}}\\) 是原来的数据损失，\\(\\lVert w\\rVert\_2^2\\) 是所有权重平方和，\\(\\lambda\\) 控制这项额外代价的强度。它在贴合训练数据之外，对过大的权重另加代价。\\(\\lambda\\) 太小作用不明显，太大又可能把真正需要的关系压平，所以它本身也是要在验证集上选择的超参数。L1 正则使用权重绝对值和，常会得到更多接近零的权重。两者都只能在数据和任务定义合理的前提下发挥作用。

> [!NOTE]
>
> **思考题**
>
> 训练损失继续下降，验证损失开始上升，这说明模型正在发生什么？
>
> > **答案**
> >
> > 模型正在记住训练样本中的偶然细节，泛化能力开始变差，这是过拟合的典型信号。可以增加数据、正则化、早停、简化模型或改进数据划分。若训练损失也高，才更像欠拟合。

## 神经网络与 Transformer

神经网络把线性变换和非线性激活多层组合，能够表达更复杂的输入输出关系。反向传播利用链式法则高效计算各层参数的梯度。Transformer 通过注意力机制处理序列关系，是现代大语言模型等系统的基础结构。它们仍沿用同一套流程，需要划分训练、验证和测试数据，定义损失，优化参数并检查泛化。

Transformer 处理文本时，token 是分词器切出的单位，可以是词片、汉字、标点或字节片段。attention 可以先理解为按内容加权的读取：当前位置形成 query，与各位置的 key 比较，再按权重汇聚 value。后面的 Q、K、V 小节会把这件事写成矩阵计算。

可以把一层神经网络看成 \\(h=\\phi(Wx+b)\\)。\\(W x+b\\) 和线性回归一样先做加权组合，\\(\\phi\\) 再提供非线性。层层组合后，模型可以表达弯曲的边界和层次化特征。反向传播利用链式法则，把最终损失对各层参数的影响从输出端高效传回。梯度下降仍使用前面介绍的更新规则。

从线性模型过渡到神经网络时，要注意线性变换和激活函数的组合。连续堆叠多层线性变换时，整体仍可合并为一次线性变换。ReLU、sigmoid、tanh 等激活函数在层间引入非线性，使网络能够组合出更复杂的函数。隐藏层中的数值是为降低损失形成的中间表示。

前向传播从输入一路算到输出。图像经过若干卷积或注意力层得到分类概率，文本 token 经过嵌入和 Transformer block 得到下一个 token 的分布。反向传播从损失出发，利用链式法则依次计算每一层参数的梯度。它复用前向保存的中间激活值（activation）来求导。训练时 activation 占用显存，正是因为反向传播需要这些中间量。

深层网络的训练还会遇到梯度消失、梯度爆炸和数值精度问题。sigmoid 在两端导数很小，层数增加后许多导数相乘可能使早期层几乎收不到更新。残差连接会把子层输出加回输入。合理初始化、归一化层（normalization）和合适的激活函数都有助于保持信号和梯度的尺度。

卷积网络和 Transformer 对数据结构有不同偏好。卷积用局部共享的滤波器处理网格状图像，利用相邻像素常有相关性。Transformer 以 token 为单位建立可学习的注意力关系，较容易处理文本、序列和跨模态输入。选择结构时，应考虑数据的组织方式、需要的上下文范围和计算成本。

模型输出层要和任务相接。回归常直接输出实数，二分类常接 sigmoid，多分类常接 softmax。中间层的激活函数服务于表示能力和训练稳定性。设计网络前先写清目标、输出形式和评价指标，能减少后续调参时的歧义。

反向传播在代码中通常由自动微分系统完成，理解计算图仍很有用。每个张量操作都会记录它如何由前面的张量得到。调用反向传播时，框架按依赖的反方向累积梯度，优化器读取这些梯度更新参数。梯度会累积，因此实际训练循环中需要在每个 step 前清零。评估不需要梯度，应切换到推理模式并关闭梯度记录，以节约显存。

token 不是完整的词。它可能是英文词的一部分、一个汉字、一个标点，或经过字节级切分得到的片段。tokenizer 的词表给每个 token 一个编号，embedding 表的每一行正好对应一个编号。若词表大小为 \\(V\\)、隐藏维度为 \\(d\\)，embedding 表就是一个 \\(V\\times d\\) 的矩阵。输入序列 `[4, 18, 7]` 查表后得到 \\(3\\times d\\) 的矩阵，矩阵第 1 行就是编号 4 对应的向量。

位置编码可以和 embedding 相加，也可以作为独立信息进入 attention。没有位置信息时，“猫追狗”和“狗追猫”的 token 集合相同，模型无法区分两个词的主语和宾语关系。位置信息的加入让第 2 个“狗”和第 4 个“狗”即使使用同一行 embedding，也能在序列中携带不同的上下文含义。

UvA Deep Learning 教程中的结构图把 Transformer block 的常见组件放在同一张图里。左路负责输入表示和多头注意力，右路是前馈网络，中间的残差与归一化把两条子层的输出稳定地合回主干。以后读模型代码时，可以把每一行张量操作按这张图归类，知道它属于投影、注意力计算、FFN（feed-forward network，对每个 token 独立执行的前馈网络）还是残差连接。

Transformer 也没有跳出这条线。文本先由 tokenizer（将原始文本切成 token 并映射为编号的规则和词表）切分并编码；模型查表后把每个 token 变成一行长度为 \\(d\\) 的数。若一次输入有 \\(T\\) 个 token，最常见的表示就是一个 \\(T\\times d\\) 的矩阵 \\(X\\)。第 \\(t\\) 行 \\(x\_t\\) 是这个位置在当前层携带的隐藏状态，即表示该 token 及其上下文的向量。后面的计算就是反复读入、更新和写出这些行。

attention 让一个位置根据其他位置的表示更新自己，随后经过前馈网络和多层堆叠；训练语言模型时，目标仍是预测正确的下一个 token，常用损失仍是交叉熵。参数更多、数据更多、训练硬件更强，并不能掩盖基础问题。一个测试集泄露、指标不匹配或数据分布漂移的问题，不会因为模型更大而消失。

后面的语言模型例子只需先保留一条直观认识。token 的向量进入第一个 Transformer block，经过 attention、FFN、残差和归一化后，仍以同样的形状交给下一层。重复许多层后，最后一层的表示再被投影成词表上的 logits。翻译模型还会额外有 encoder 与 cross-attention；这里的自回归例子暂时只沿 decoder 路径展开。

下面用四个 token 说明一次前向传播的张量流。设 batch 为 1，序列长 \\(T=4\\)，隐藏维度 \\(d=6\\)。输入先是 token 编号 `[5, 12, 8, 3]`，查 embedding 得到

```text
X: [1, 4, 6]
第 1 行 = token 5 的隐藏向量
第 2 行 = token 12 的隐藏向量
第 3 行 = token 8 的隐藏向量
第 4 行 = token 3 的隐藏向量
```

经过第 1 个 Transformer block 后仍是 `[1, 4, 6]`。行数没有变，因为每个 token 在层内更新自己的表示；列数没有变，因为残差连接要求子层输出能直接加回输入。第 24 层输出的最后一行可投影成 `[1, V]` 的 logits，经 softmax 得到下一个 token 的概率分布。

### Attention 与 FFN

Transformer block 可以先拆成 attention 和 FFN（feed-forward network）。attention 处理这个 token 应从哪些位置取信息；FFN 不在这一步混合不同 token，它对每一行隐藏向量各自做一次更宽的非线性变换。忽略归一化和残差连接时，一个普通 FFN 常写成

\\\[ H=\\phi(XW\_{\\mathrm{up}}+b\_{\\mathrm{up}}),\\qquad Y=HW\_{\\mathrm{down}}+b\_{\\mathrm{down}} \\\]

若 \\(X\\in\\mathbb{R}^{T\\times d}\\)，则 \\(W\_{\\mathrm{up}}\\in\\mathbb{R}^{d\\times h}\\) 会把每行从 \\(d\\) 维投影到更宽的 \\(h\\) 维；激活函数 \\(\\phi\\) 对元素分别计算；\\(W\_{\\mathrm{down}}\\in\\mathbb{R}^{h\\times d}\\) 再把它投回 \\(d\\) 维。不同模型可能采用 GELU（Gaussian Error Linear Unit，高斯误差线性单元）、SiLU（Sigmoid Linear Unit，Sigmoid 线性单元）或门控结构，代码中的细节会变，但矩阵乘法、逐元素非线性、再一次矩阵乘法的主干不变。

一个极小的 FFN 数值例子能帮助把矩阵乘还原成标量计算。设某个 token 的输入向量 \\(x=\[1,2\]\\)，权重

\\\[ W\_{\\mathrm{up}}= \\begin{bmatrix}1&amp;0\\\\2&amp;1\\end{bmatrix}, \\qquad W\_{\\mathrm{down}}= \\begin{bmatrix}1&amp;1\\\\0&amp;2\\end{bmatrix} \\\]

先算 \\(h=xW\_{\\mathrm{up}}=\[5,2\]\\)。若激活函数是 ReLU（Rectified Linear Unit，修正线性单元），它会把负数置为 0、保留正数，这里结果仍为 \\(\[5,2\]\\)；再算 \\(y=hW\_{\\mathrm{down}}=\[5,9\]\\)。如果 batch 里有 8 个 token，每个 token 都做同样的两步，张量形状就从 `[8, 2]` 变为 `[8, 2]`。GPU 的收益正来自把 8 行一起交给矩阵乘法库，而不是循环执行 8 次小向量运算。

从一个元素看，第一步就是点积。

\\\[ H\_{tj}=\\sum\_{i=0}^{d-1}X\_{ti}(W\_{\\mathrm{up}})\_{ij} \\\]

这里 \\(t\\) 是 token 的行号，\\(i\\) 是输入特征下标，\\(j\\) 是扩展后的中间通道下标。它把第 \\(t\\) 行输入的所有特征按权重混合起来。把多个 token 同时放进 \\(X\\)，就是矩阵乘法而不是许多孤立的标量计算，SIMD、AMX、GPU Tensor Core 和矩阵库优化的对象正是这种成块的矩阵乘。矩阵的行对应 token，列对应特征或中间维度。读模型代码时先把张量形状写下来，很多看似复杂的 `reshape`、`transpose` 和 batch 维度才有落脚处。

MoE 是 FFN 的一种变体。每行由 router 选择少数几个 expert FFN，再按权重合并结果。它仍建立在每个 token 是一行向量、FFN 是规则矩阵计算之上。token 按 expert 分组后会形成数据重排、通信和负载均衡问题。

注意力可以先理解为按内容读取信息。一个 token 的 query 与其它位置的 key 比较得到权重，再对对应 value 加权求和。这样同一个词在不同上下文中可以关注不同位置。自回归语言模型使用因果 mask，使当前位置只查看自己和前面的 token。大规模执行时，矩阵计算、KV cache 和设备间通信共同决定吞吐。

> [!NOTE]
>
> **思考题**
>
> attention 和 FFN 分别混合序列中的什么信息？
>
> > **答案**
> >
> > attention 沿序列维度工作，让当前 token 根据相似度从其他 token 取信息。FFN 对每个 token 的隐藏向量独立做两次矩阵投影和非线性变换，不在这一层混合不同 token。两者分工不同，常常交替堆叠。

### Q、K、V 与注意力矩阵

下面的三个向量都由同一个 hidden state 经过不同线性投影得到。Q（query，查询向量）表示当前位置想寻找的信息；K（key，键向量）提供可匹配的索引表示；V（value，值向量）是在匹配后被加权汇入输出的内容。softmax 会把一行匹配分数变成总和为 1 的权重。

设输入隐藏状态为 \\(X\\in\\mathbb{R}^{T\\times d}\\)。一层 attention 先用三组权重做线性投影。

\\\[ Q=XW\_Q,\\qquad K=XW\_K,\\qquad V=XW\_V \\\]

每一行仍对应一个 token。可以把 query 理解为当前位置想找什么，key 理解为当前位置提供什么索引，value 则是被取走的内容。这个说法只是帮助记忆，真正参与计算的是三张矩阵。

UvA 教程的注意力示例图展示 query、key、value 三路投影。同一份输入分别经过三个权重矩阵，得到 Q、K、V。Q 用来发起查询，K 用来匹配查询，V 是被加权合并的内容。

*Q、K、V 来自同一输入经过三组不同权重得到的三个矩阵。后续计算包括矩阵乘、缩放、逐行 softmax 和加权求和。[^/study/hpc/notes/0716a/#uva-attention]*

设 \\(T=4,d=8\\)。单头时 \\(W\_Q,W\_K,W\_V\\) 都是 \\(8\\times8\\)，三组投影后 Q、K、V 的形状都是 `[1, 4, 8]`。模型有 4 个 head 时，通常把每个 head 的维度设为 \\(d\_h=2\\)。把 Q 重排成 `[1, 4, 4, 2]` 后，计算 \\(QK^T\\) 得到 `[1, 4, 4, 4]`。去掉 batch 和 head 两个维度后，每个 head 的分数矩阵都是 `[4,4]`。矩阵乘法消掉 head 维度 \\(d\_h\\)，留下 query 位置和 key 位置。

第 \\(t\\) 个 query 和第 \\(j\\) 个 key 的相似度是点积 \\(q\_tk\_j^T\\)。把所有 token 两两比较，得到 \\(T\\times T\\) 的分数矩阵。

\\\[ A=\\frac{QK^T}{\\sqrt{d\_h}} \\\]

> [!NOTE]
>
> **为什么 Q 要乘 \\(K^T\\)**
>
> Q（query）和 K（key）在同一个 head 内具有相同的最后一维 \\(d\_h\\)。单个 query \\(q\_i\\) 与单个 key \\(k\_j\\) 都可看作长度为 \\(d\_h\\) 的行向量，点积
>
> \\\[ q\_i k\_j^T=\\sum\_{m=1}^{d\_h}q\_{i,m}k\_{j,m} \\\]
>
> 给出位置 \\(i\\) 对位置 \\(j\\) 的原始匹配分数。分数更高表示这对经训练得到的 query/key 投影更匹配；分数接近 0 或为负表示匹配较弱。这里衡量的是模型学习到的匹配关系，不能简单等同于原始词向量的语义距离。
>
> 把所有 token 一次放进矩阵后，若 \\(Q,K\\in\\mathbb{R}^{T\\times d\_h}\\)，则 \\(K^T\\in\\mathbb{R}^{d\_h\\times T}\\)，因此
>
> \\\[ QK^T\\in\\mathbb{R}^{T\\times T}, \\qquad (QK^T)\_{ij}=q\_i k\_j^T \\\]
>
> 矩阵的第 \\(i\\) 行包含第 \\(i\\) 个 token 对所有 key 的分数；第 \\(j\\) 列表示所有 query 对第 \\(j\\) 个 key 的分数。softmax 会把每一行转成权重，随后用这些权重加权对应位置的 V（value）向量。Q/K 负责决定读取哪些位置，V 提供被读取并汇入输出的内容。
>
> Q 和 K 来自独立的可训练投影 \\(Q=XW\_Q\\)、\\(K=XW\_K\\)。因此模型可以学习哪些特征适合作为查询，哪些特征适合作为被匹配的键。decoder-only 模型再在这个分数矩阵上加入因果 mask，将未来位置的分数屏蔽后才做 softmax。

\\(d\_h\\) 是一个 head 的维度。除以 \\(\\sqrt{d\_h}\\) 可以控制点积的数值范围，避免 softmax 过早变得极端。对每一行做 softmax 后，再与 \\(V\\) 相乘。

> [!NOTE]
>
> **为什么除以 \\(\\sqrt{d\_h}\\)**
>
> 在常见的初始化近似下，把 \\(q\_i,k\_i\\) 看作相互独立、均值为 0、方差约为 1 的分量，则
>
> \\\[ q\\cdot k=\\sum\_{i=1}^{d\_h}q\_i k\_i, \\qquad \\operatorname{Var}(q\\cdot k)\\approx d\_h \\\]
>
> 点积的标准差约为 \\(\\sqrt{d\_h}\\)。用它相除后
>
> \\\[ \\operatorname{Var}\\!\\left(\\frac{q\\cdot k}{\\sqrt{d\_h}}\\right) =\\frac{\\operatorname{Var}(q\\cdot k)}{d\_h}\\approx1 \\\]
>
> softmax 只关心分数之间的差，不关心它们共同增加了多少。因而
>
> \\\[ \\operatorname{softmax}(\[100,101\]) =\\operatorname{softmax}(\[1000,1001\]) \\approx\[0.2689,\\;0.7311\] \\\]
>
> 真正让输出变尖的是差距被整体放大。例如
>
> \\\[ \\operatorname{softmax}(\[1,2\])\\approx\[0.2689,\\;0.7311\], \\qquad \\operatorname{softmax}(\[10,20\])\\approx\[0.000045,\\;0.999955\] \\\]
>
> 不缩放的点积会随 \\(d\_h\\) 增大而出现这种更大的相对差距，softmax 容易接近 one-hot；除以 \\(d\_h\\) 时，方差约为 \\(1/d\_h\\)，分数会过平。其它实现也可以通过 query/key 归一化或可学习 temperature 调整分数尺度，但原始 Transformer 采用的是 \\(\\sqrt{d\_h}\\) 缩放。[^/study/hpc/notes/0716a/#transformer]

UvA 教程的缩放点积注意力图把这段计算画成了一条流水线。Q 和 K 相乘得到分数，除以 \\(\\sqrt{d\_h}\\)，可选地加 mask，然后按行 softmax，最后乘 V 得到输出。

softmax 的公式是

\\\[ p\_i=\\frac{\\exp(a\_i)}{\\sum\_j\\exp(a\_j)} \\\]

它把任意实数分数变成正数，并让同一行所有数相加为 1。若某一行分数为 `[2, 0, -1]`，softmax 后概率约为 `[0.844, 0.114, 0.042]`。这一行对应一个 query 对三个 key 的偏好，较大的分数得到较大权重，但不代表其它位置完全不能贡献。

> [!NOTE]
>
> **练习：手算一次加权求和**
>
> 设三个位置的 value 分别为
>
> ```text
> v0 = [1, 0]
> v1 = [0, 2]
> v2 = [2, 1]
> ```
>
> 上面的 softmax 权重是 `[0.844, 0.114, 0.042]`。写出这个 query 的输出。
>
> > **详细答案**
> >
> > ```text
> > output = 0.844×[1,0] + 0.114×[0,2] + 0.042×[2,1]
> >        = [0.844,0] + [0,0.228] + [0.084,0.042]
> >        = [0.928,0.270]
> > ```
> >
> > 第一维由三个 value 的第一维加权得到，第二维同理。权重之和为 1，因此输出是各 value 向量按注意力权重形成的加权组合。这一步对应 `softmax(A) × V` 中的一行。

\\\[ \\operatorname{Attention}(Q,K,V)=\\operatorname{softmax}(A)V \\\]

因此第 \\(t\\) 行输出是所有 value 的加权和，权重由第 \\(t\\) 个 token 与各位置 key 的匹配程度决定。若序列长为 \\(T\\)，显式的 \\(A\\) 有 \\(T^2\\) 个元素。这是长上下文为什么既有大量计算，也有显存和访存压力的起点。

自回归模型在位置 \\(t\\) 只能读取当前位置和此前的 token。实现时会在 softmax 前给右上三角的非法位置加上一个非常小的值，等价于 \\(-\\infty\\)。softmax 后这些位置的权重为 0。因果 mask 保证预测下一个 token 时只使用已有上下文。推理时新的 token 尚未生成，因果性也会保留。

四个 token 的因果 mask 可以写成一个矩阵。行是 query 位置，列是 key 位置，1 表示允许读取，0 表示屏蔽。

```text
        key0 key1 key2 key3
query0   1    0    0    0
query1   1    1    0    0
query2   1    1    1    0
query3   1    1    1    1
```

训练时四个位置同时前向计算，位置 2 的正确标签是位置 3 的 token。如果不加 mask，位置 2 可以通过 attention 读到位置 3，训练损失会异常低，生成能力却没有真正学到。

> [!TIP]
>
> **怎样读上面的 mask 矩阵**
>
> 矩阵中第 \\(t\\) 行对应第 \\(t\\) 个 query，第 \\(j\\) 列对应可被读取的第 \\(j\\) 个 key/value。左上到右下的下三角为 1，表示当前位置及其历史可见；右上三角为 0，表示未来位置被屏蔽。最左位置只能看自己，越靠右的位置可读取的历史越多。

多头注意力把 hidden dimension 切成若干 head，每个 head 使用自己的 \\(W\_Q,W\_K,W\_V\\)。不同 head 可以学习不同的关联模式，随后把各 head 的输出拼接并再做一次投影。代码里的 `reshape`、`transpose` 往往只是在整理 `[batch, sequence, heads, head_dim]` 的维度顺序。阅读这类代码时，应先标出 sequence 和 head 所在维度。

UvA 教程的多头注意力图显示了拆分、并行计算和拼接的完整路径。

以 \\(B=2,T=5,H=8,h=4,d\_h=2\\) 为例，线性投影后的 Q 理论上是 `[2, 5, 8]`。代码常执行 `view(2, 5, 4, 2)`，再 transpose 成 `[2, 4, 5, 2]`。这种变换只改变形状解释，不复制或重新计算数值。attention score 是 `[2, 4, 5, 5]`，输出在乘 V 后是 `[2, 4, 5, 2]`，最后 transpose 回 `[2, 5, 8]` 并乘输出投影 \\(W\_O\\)。实验里维度对不上时，可先在纸上画出这条形状链。

> [!NOTE]
>
> **思考题**
>
> 输入形状是 `[B,S,H]`，线性投影后 Q 的形状是 `[B,S,H]`，把它切成 8 个 head 后每个 head 的形状应写成什么？为什么最后能拼回 `[B,S,H]`？
>
> > **答案**
> >
> > 每个 head 的形状是 `[B,S,H/8]`，也可以记成 `[B,8,S,H/8]`。每个 head 在自己的低维子空间里计算注意力，输出仍是每个 token 一个 `H/8` 维向量。把 8 个 head 在最后一维拼接，得到 `[B,S,H]`，再经过输出投影变换回模型隐藏维度。

> [!NOTE]
>
> **思考题**
>
> 若 \\(Q\\) 和 \\(K\\) 都是 `[S, d]`，为什么 \\(QK^T\\) 是 `[S, S]`？这个矩阵的行列各代表什么？
>
> > **答案**
> >
> > `[S, d]` 乘 `[d, S]` 得到 `[S, S]`。第 \\(i\\) 行第 \\(j\\) 列是第 \\(i\\) 个 query 与第 \\(j\\) 个 key 的点积分数。对每一行做 softmax 后，就得到第 \\(i\\) 个 token 对所有位置的注意力权重。

> [!NOTE]
>
> **思考题**
>
> 注意力分数在除以 \\(\\sqrt{d\_k}\\) 后再 softmax。为什么需要这个缩放？去掉它可能带来什么现象？
>
> > **答案**
> >
> > \\(QK^T\\) 是两个 \\(d\_k\\) 维向量的内积，维度越大，分数的方差通常越大。很大的分数经过 softmax 会接近 one-hot，梯度也容易变小。除以 \\(\\sqrt{d\_k}\\) 把分数方差控制在相对稳定的范围，让训练更稳定。它不改变注意力矩阵的形状，只改变数值分布。

> [!NOTE]
>
> **术语自测**
>
> 1. embedding 查表输入是 token 编号还是文本字符串？
> 2. logits 和 softmax 后的概率有什么区别？
> 3. 因果 mask 如何限制当前位置可见的 token？
> 4. multi-head 为什么会把隐藏维度拆成多个子空间？
>
> > **详细答案**
> >
> > 1. embedding 查表的输入是 token ID。文本字符串首先由 tokenizer 转为整数 ID，embedding 表再把每个 ID 查成一个向量。
> > 2. logits 是词表或类别上的未归一化实数分数。softmax 将一组 logits 转为非负、总和为 1 的概率。采样和交叉熵通常使用 logits 或由 logits 得到的概率。
> > 3. 因果 mask 将未来位置的注意力分数屏蔽。位置 \\(t\\) 只能读取 \\(\\leq t\\) 的 token，因此训练时不会提前读取标签位置，生成时也只依赖已出现的历史。
> > 4. multi-head 将隐藏维度拆成多个子空间。每个 head 有独立的 Q/K/V 投影，可以学习不同的相关性模式。各 head 输出拼接后再投影回原 hidden size。

### 残差、归一化与一个 Block 的数据流

attention 和 FFN 的子层输出通常会加回原输入，形成残差连接。

\\\[ X' = X + \\operatorname{Attention}(\\operatorname{Norm}(X)), \\qquad Y = X' + \\operatorname{FFN}(\\operatorname{Norm}(X')) \\\]

这里 \\(X\\) 是当前 block 的输入，\\(X'\\) 是 attention 残差相加后的中间表示，\\(Y\\) 是 FFN 残差相加后的 block 输出。残差给深层网络保留了一条较直接的信息和梯度路径。归一化层把每个 token 内各特征的尺度维持在稳定范围附近，减少层间数值漂移。不同模型会使用 LayerNorm（Layer Normalization，层归一化）或 RMSNorm（Root Mean Square Normalization，均方根归一化），也可能调整 norm 放在子层前还是后。读代码时可先看顺序，输入经过 norm，进入 attention 或 FFN，输出再与原张量相加。

| 张量 | 常见 shape | 每一维的含义 |
| --- | --- | --- |
| block 输入/输出 | \\(\[B,T,d\]\\) | batch、序列长度、隐藏维度 |
| Q/K/V | \\(\[B,h,T,d\_h\]\\) | 多头拆分后的序列表示 |
| attention 分数 | \\(\[B,h,T,T\]\\) | 每个 head 中 token 两两比较 |
| FFN 中间激活 | \\(\[B,T,d\_{ff}\]\\) | 每个 token 独立扩展到更宽维度 |

训练时反向传播要保存许多中间量。推理不保存梯度，却要为每层保留已出现 token 的 K、V，这就是 KV cache 的来源。

大模型训练还会面对数据与评估问题。预训练语料的来源、重复、质量和许可会影响模型能力。指令微调和偏好训练会改变模型输出的行为倾向。基准集长期公开讨论后，也可能以各种方式进入训练数据，导致评估被污染。规模增大后，数据治理、复现实验和失效分析更需要严格执行。

> [!NOTE]
>
> **思考题**
>
> embedding 表的行数和列数分别由什么决定？
>
> > **答案**
> >
> > 行数等于词表大小，每个 token 编号对应一行。列数是每个 token 的向量维度。输入 `[5, 12, 8]` 查表后得到三行向量，向量长度就是 embedding 的列数。

> [!NOTE]
>
> **思考题**
>
> 残差连接为什么要求子层输出的形状能直接加回输入？
>
> > **答案**
> >
> > 残差的计算是逐元素相加，两个张量每个维度的大小必须一致。若子层把隐藏维度改掉，就不能直接相加。常见做法是让 attention 和 FFN 输出都回到 `[B, T, d]`，再与原输入相加。

[^/study/hpc/notes/0716a/#google-mlcc-gd]: Google, *Machine Learning Crash Course: Gradient Descent*, [https://developers.google.com/machine-learning/crash-course/linear-regression/gradient-descent](<https://developers.google.com/machine-learning/crash-course/linear-regression/gradient-descent>).

[^/study/hpc/notes/0716a/#dlbook]: I. Goodfellow, Y. Bengio, A. Courville, *Deep Learning*, [https://www.deeplearningbook.org/](<https://www.deeplearningbook.org/>).

[^/study/hpc/notes/0716a/#pytorch-autograd]: PyTorch, *Autograd mechanics*, [https://docs.pytorch.org/docs/stable/notes/autograd.html](<https://docs.pytorch.org/docs/stable/notes/autograd.html>).

[^/study/hpc/notes/0716a/#transformer]: A. Vaswani et al., *Attention Is All You Need*, [https://arxiv.org/abs/1706.03762](<https://arxiv.org/abs/1706.03762>).

[^/study/hpc/notes/0716a/#uva-attention]: UvA Deep Learning Tutorials, *Transformers and Multi-Head Attention*, [https://uvadlc-notebooks.readthedocs.io/en/latest/tutorial\_notebooks/tutorial6/Transformers\_and\_MHAttention.html](<https://uvadlc-notebooks.readthedocs.io/en/latest/tutorial_notebooks/tutorial6/Transformers_and_MHAttention.html>).
