# 深度学习

> Leon | AI Field Guides

[EN](AI-DL.en.md) · [中文](AI-DL.zh.md)

![Leon — 学习路线图](../assets/maps/dl.zh.svg)

<a id="extensions"></a>

## 继续深入：完整案例

先读对应章节，再跟着故事预测输出、运行完整程序、核对解释。每个案例都在原有概念上增加一个实际问题。

1. [概率下溢时仍能计算有限损失](#lab-dl-softmax-loss-stable-loss-gradient-check) — 连接 log-sum-exp、交叉熵和梯度核对。

2. [微批次大小不同时正确累积梯度](#lab-dl-training-loop-unequal-batch-accumulation) — 沿损失计算和更新过程跟踪样本权重。

用十六个耐心引导的章节理解深度学习。从数组与函数开始，追踪形状、损失、梯度和完整的小型训练循环，再组合卷积、嵌入、注意力、玩具 Transformer 模块与适应方法。每章都含有解释的 Python、带答案问题与两个练习案例。

### 运行 Python 示例

使用 Python 3.12 或更新版本。把代码保存为 .py 文件，在终端中用 `python 文件名.py` 运行；部分系统使用 `python3`。若出现 `import numpy as np`，先运行 `python -m pip install numpy`。这行 import 加载 NumPy 数值工具包，并把它简称为 np。示例无需下载数据集或模型权重。

[Python 官方下载](https://www.python.org/downloads/) · [NumPy 官方安装说明](https://numpy.org/install/)

## 把知识连接起来

四阶段覆盖张量轴、非线性特征、softmax 损失、网络形状、反向传播、优化器、训练循环、dropout、归一化、卷积、嵌入、注意力、Transformer 模块、迁移学习与低秩适应。所有示例原创。代码只用 Python 与 NumPy，不下载数据或预训练权重。

### 开始前需要了解

- 掌握四则运算、分数、平方与简单函数。
- 阅读共享 Python 与数学入门，了解列表、向量、求和、对数与斜率。
- 机器学习指南中的损失与验证章节有帮助，但无需深度学习框架经验。

### 你将学到什么

- 解释每个轴、层、损失与梯度的含义。
- 运行并修改每个主题的小示例，对照已检查输出。
- 追踪卷积、嵌入、掩码注意力与完整玩具 Transformer 模块。
- 区分学习、验证、迁移、参数节省与推理证据。

## 知识框架

### 1 · 基础：数组、激活与预测

- [张量：让每个维度都有含义](#dl-tensors)
- [激活函数：在加权和之间加入弯折](#dl-activations)
- [多层感知机：学习有用的中间特征](#dl-representations)
- [Softmax 与损失：把分数变为学习信号](#dl-softmax-loss)

### 2 · 核心：追踪并训练模型

- [追踪整个网络的形状](#dl-shape-tracing)
- [反向传播：沿依赖链求导](#dl-autograd)
- [小批次与优化器：将梯度转化为更新](#dl-optimization)
- [训练循环：预测、衡量、更新、验证](#dl-training-loop)

### 3 · 应用：稳定的层与结构化输入

- [正则化：训练时引入噪声，评估时明确模式](#dl-regularization)
- [归一化：说明哪些值共享统计量](#dl-normalization)
- [卷积与残差路径：局部证据与保留的通路](#dl-convolution)
- [词元嵌入：把编号变为可训练向量](#dl-token-embeddings)

### 4 · 进阶：注意力、迁移与高效更新

- [注意力：通过学习到的比较提取上下文](#dl-attention)
- [Transformer 模块：把组件接起来](#dl-transformer-block)
- [迁移学习：适应特征，同时守住评估边界](#dl-transfer)
- [低秩适应与可检查的推理路径](#dl-efficient-adaptation)

<a id="dl-tensors"></a>

## 张量：让每个维度都有含义

### 01 / 从一个完整故事开始

米娜收集三个传感器的记录，每条记录变成三个摘要数值。她想一次处理两条。程序可以运行，但换掉同批的另一条记录时，原记录的摘要也会改变。原来她沿错误的轴取了平均。

米娜画出表格：行是记录，列是传感器特征。每次计算前先标轴，并手算一行。改变求平均的轴后，每条记录重新保有自己的摘要。她学到，数组大小匹配只是第一项检查，各个值的含义也要正确。

### 02 / 理解核心概念

张量是数值数组。一个数没有轴，列表有一个轴，表格有两个轴。形状列出每个轴的长度。批次把多个样本组合起来计算。

全连接层的每个输出都是输入特征的加权和再加偏置。矩阵乘法同时计算多个这样的和；逐元素乘法则只乘对应位置。转置交换轴，重塑只是重新分组条目，不会自动改变顺序。使用操作前先写下每个轴的含义。

### 03 / 如何运用这个概念

在数据加载器入口和每层的边界写下形状约定。明确哪个维度代表批次、特征按什么顺序排列，以及归约后是否应为每个样本保留一个结果。大规模运行之前，先让两个刻意不同的样本经过流程。检查改变其中一个样本是否意外影响另一个，并区分有意计算的批次统计量与意外的数据混合。

### 04 / 官方教程与研究中的应用

PyTorch 官方张量教程用张量表示输入和模型参数，检查其形状、数据类型与设备，并区分矩阵乘法和逐元素乘法。这些都是神经网络训练前实际使用的数据准备与检查操作。教程还演示了显式设备转移，说明为什么一个数学上正确的表达式，在具体实现中仍然需要彼此兼容的存储位置。

### 05 / 公式与逐项解析

```text
X ∈ ℝ^(B×d), W ∈ ℝ^(d×k), b ∈ ℝ^k
Y = XW + b
Y[i,j] = Σ(r=1…d) X[i,r]W[r,j] + b[j]
```

X 是输入矩阵；B 是样本数，d 是每个样本的特征数。W 是学习得到的权重矩阵，k 是输出特征数。b 是长度为 k、沿各行广播的偏置向量。Y 是所得的 B 行 k 列输出矩阵。i 指定样本，j 指定输出特征，r 指定求和中的输入特征。ℝ 表示实数，× 标明各维度大小，Σ 将全部 d 个输入特征对应的乘积相加。方括号用于选取单个元素。

### 06 / 一步步算出结果

取两条记录 X = [[2, 1, 0], [0, 3, 1]]。选择 W = [[1, −1], [2, 0], [0, 3]]，b = [0.5, −0.5]。第一行得到 [2×1 + 1×2 + 0×0 + 0.5, 2×(−1) + 1×0 + 0×3 − 0.5] = [4.5, −2.5]。第二行得到 [6.5, 2.5]，因此 Y 的形状为 2 行 2 列。沿记录维度求平均得到 [5.5, 0]，即每个输出特征一个均值。沿特征维度求平均则得到 [1, 4.5]，即每条记录一个均值。两个运算都返回两个数；只有维度含义能告诉我们哪一个回答了预期问题。这个全连接层使用六个权重和两个偏置，与当前批次大小无关。

### 先认识这些词

- **张量:** 具有明确形状与轴含义的数值数组。

- **批次:** 一起处理的一组样本。

- **广播:** 沿较大轴重复使用形状兼容的小数组，例如每个输出列共用一个偏置。

### 一步步想清楚

**为什么 2×3 输入乘 3×2 权重得到 2×2？**

两个输入行分别与两个输出列组合。每对组合将三个对应特征相乘再相加。

**为什么两个求平均方向都能返回两个数？**

输出碰巧为 2×2。沿记录方向平均得到每个特征的均值，沿特征方向平均得到每条记录的均值。相同长度掩盖了不同含义。

**形状检查能发现传感器名称被交换吗？**

不能。维度可能仍匹配。要保持固定特征顺序，并用已知行做手算检查。

<a id="lab-dl-tensors-dl-dense-axis-check"></a>

### 给全连接层标明各轴

行表示记录，列表示特征。

```python
import numpy as np
x = np.array([[2.,1.,0.], [0.,3.,1.]])
w = np.array([[1.,-1.], [2.,0.], [0.,3.]])
b = np.array([0.5,-0.5])
out = x @ w + b
print('shape:', out.shape)
print('output:', out.tolist())
print('mean per feature:', out.mean(axis=0).tolist())
print('mean per recording:', out.mean(axis=1).tolist())
```

**在本地运行**

```sh
python dl-tensors-dl-dense-axis-check.py
```

**预期输出**

```text
shape: (2, 2)
output: [[4.5, -2.5], [6.5, 2.5]]
mean per feature: [5.5, 0.0]
mean per recording: [1.0, 4.5]
```

**逐步读懂代码**

@ 将每个输入行与每个权重列组合。加 b 时，两项偏置在各记录间重复。shape 报告轴长度。mean(axis=0) 消去记录轴，mean(axis=1) 消去特征轴。不同均值说明轴命名的重要性。权重为手工选择，代码只是层运算，不是训练好的记录分类器。

### 这些知识可以用在哪里

**两条记录**

只改变第二个输入行。在这个全连接层中，第一输出行应不变。这能检查样本是否被意外混合。

**不同批大小**

增加第三条记录，仍含三个特征。输出变为 3×2。层仍只有六个权重和两个偏置，批大小不会增加参数。

**类比的边界与注意事项:** 形状检查无法发现特征名称调换、物理单位不匹配，或标签对应了错误行等问题。广播可能悄悄产生比预期更大的结果。依赖批次的层也会让样本之间出现合理交互，因此解释独立性检查时，需要考虑网络结构及其训练模式。

**记住这一点:** 把每个张量维度视为有名称的约定；维度匹配是必要条件，而保留其含义才能保证计算正确。

### 本节资料来源

- [PyTorch: 张量——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html)

<a id="dl-activations"></a>

## 激活函数：在加权和之间加入弯折

### 01 / 从一个完整故事开始

安雅为两个传感器读数画网络。她不断增加加权和，希望规则更灵活，但组合模型仍像一个直线变换。她需要能改变规则形状的步骤。

安雅尝试 ReLU：正值通过，负值变为零。她也计算 sigmoid，结果始终在零与一之间。函数行为不同，因此承担不同任务。ReLU 可构建隐藏特征，sigmoid 可表示二分类概率估计。她还检查斜率，因为训练要沿所选函数把变化传回去。

### 02 / 理解核心概念

激活对层中每个值应用函数。非线性函数无法在整个范围内表示为一个加权和加偏置。ReLU 在零以下输出零，零以上等于输入。这种弯折让后续层组合不同激活区域。

Sigmoid 平滑地把分数映射到 (0,1)，斜率在零附近最大，在很大的正负分数处很小。小斜率可能削弱反向信号。ReLU 在负半轴斜率为零，正半轴为一；零点处实现需要选定约定。

### 03 / 如何运用这个概念

选择激活时先明确角色。隐藏层需要非线性特征，单个二元输出可用 sigmoid 概率解释，多类别输出则需要下一部分讲的另一种归一化。检查隐藏单元是否始终为零或处于饱和区。

### 04 / 官方教程与研究中的应用

PyTorch 的 ReLU 文档定义整流函数，模型构建教程展示在线性层之间使用激活。这里用标准库直接比较函数值与斜率，不训练网络。

### 05 / 公式与逐项解析

```text
ReLU(z) = max(0,z)
σ(z) = 1/(1+exp(−z))
σ′(z) = σ(z)(1−σ(z))
```

z 是层的原始值，max 取较大参数。σ 表示 sigmoid，exp 是指数函数。σ′ 中的撇号表示对 z 求导，即输入微小变化时输出的局部变化率。

### 06 / 一步步算出结果

z=0 时，sigmoid 为 1/(1+1)=0.5，斜率为 0.5×0.5=0.25。z=2 时，sigmoid 约为 0.880797，斜率约为 0.104994。ReLU 把 [−2,0,2] 映射为 [0,0,2]。两者保留的值与梯度不同。

### 先认识这些词

- **激活函数:** 应用于层数值的函数，常用于加入非线性。

- **饱和:** 输入改变却几乎不改变输出的区域。

- **局部斜率:** 某个输入值附近，输出的微小变化率。

### 一步步想清楚

**为什么增加线性层还不够？**

组合仍可化为一个仿射变换。非线性激活加入无法合并进单个矩阵与偏置的弯折。

**Sigmoid 在零处的斜率为什么为 0.25？**

函数值为 0.5，代入 σ(1−σ)，得到 0.5×(1−0.5)=0.25。

**ReLU 在零点的导数为一吗？**

该点不可导，因为左右斜率不同。库通常为反向传播选一个可用约定，例如零。

<a id="lab-dl-activations-dl-activation-values"></a>

### 打印函数值与斜率

小型预设输入让计算容易检查。

```python
from math import exp
for z in [-2.,0.,2.]:
    relu = max(0.,z)
    sigmoid = 1/(1+exp(-z))
    slope = sigmoid*(1-sigmoid)
    print(f'z={z:.0f}: ReLU={relu:.1f}, sigmoid={sigmoid:.6f}, slope={slope:.6f}')
```

**在本地运行**

```sh
python dl-activations-dl-activation-values.py
```

**预期输出**

```text
z=-2: ReLU=0.0, sigmoid=0.119203, slope=0.104994
z=0: ReLU=0.0, sigmoid=0.500000, slope=0.250000
z=2: ReLU=2.0, sigmoid=0.880797, slope=0.104994
```

**逐步读懂代码**

循环分别计算三个输入。max 实现 ReLU，exp 提供 sigmoid 指数，sigmoid*(1−sigmoid) 计算解析导数。格式化输出显示 −2 与 2 的斜率对称。这个直接 sigmoid 表达式适合当前小数值；大幅值输入需要数值稳健实现。

### 这些知识可以用在哪里

**隐藏门控**

对 [−3,−1,1,3] 应用 ReLU，只有后两个值保留。先加正偏置，再检查哪些单元开始激活。

**饱和分数**

比较 sigmoid 在 0、2、8 的斜率。8 附近斜率小得多。解释为什么小反向信号可能让该分数变化缓慢。

**类比的边界与注意事项:** ReLU 单元可能在所有相关样本上都不激活。Sigmoid 可能饱和。两种激活都不能补齐缺失信息，也不证明模型已经校准。本课比较函数，不比较完整模型质量。

**记住这一点:** 激活既改变向前传递的特征，也改变向后传递的斜率。

### 本节资料来源

- [PyTorch: ReLU 激活](https://docs.pytorch.org/docs/2.14/generated/torch.nn.ReLU.html)

- [PyTorch: 构建神经网络——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html)

<a id="dl-representations"></a>

## 多层感知机：学习有用的中间特征

### 01 / 从一个完整故事开始

拉维观察两个本应一致的仪表。只要一起升高，高读数本身无害。加权平均漏掉了他关心的差异。他再加线性层，问题仍在，因为两个线性变换仍像一个变换。

拉维建立两个隐藏单元：一个在第一读数比第二大一以上时响应，另一个检查反向差异。ReLU 去掉负响应，再将剩余值相加，得到分歧分数。小网络现在能表示目标模式。手工权重展示表示能力，但训练仍需从数据中学出有用规则。

### 02 / 理解核心概念

多层感知机 MLP 让输入经过多个全连接层。隐藏表示位于输入与输出之间的中间值。激活在加权和之间加入非线性弯折，前一章解释了弯折为何重要。

没有激活时，多个仿射层可合并为一个仿射映射。仿射就是加权和加偏置。加入 ReLU 后，不同输入区域可以激活不同隐藏单元。训练学习权重，从而建立中间特征。有用的隐藏单元不一定对应人能命名的概念。

### 03 / 如何运用这个概念

从输入特征和输出含义都明确的小型网络开始。将最后一层与损失函数一起选择：分类原始分数、概率和连续预测值承担不同角色。检查激活分布，找出始终为零或异常大的单元。检查数据质量与验证表现之后，再增加宽度或深度，让额外容量用于解决已经观察到的限制。

### 04 / 官方教程与研究中的应用

PyTorch 的构建神经网络教程使用展平操作、线性层和 ReLU 激活构建 FashionMNIST 分类器。教程区分网络输出的原始类别分数，以及随后经过 softmax 运算得到的概率。这个官方示例展示了基础多层感知机如何将图像元素连接到类别输出；本节的仪表情境及全部数值均为独立设计的说明。

### 05 / 公式与逐项解析

```text
h = ReLU(W₁x + b₁), ReLU(u) = max(0,u)
ŷ = W₂h + b₂
P = Hd + H + kH + k
```

x 是包含 d 个输入特征的列向量。W₁ 有 H 行 d 列，b₁ 包含 H 个隐藏层偏置。h 是长度为 H 的隐藏表征。W₂ 有 k 行 H 列，b₂ 包含 k 个输出偏置。ŷ 是长度为 k 的预测向量。ReLU 对每个标量预激活值 u 分别应用 max(0,u)。P 统计这个两层网络的所有权重和偏置。下标 1 和 2 指代两个仿射层，不表示乘方或时间步。

### 06 / 一步步算出结果

令 d = 2、H = 2、k = 1。设 W₁ = [[1, −1], [−1, 1]]，b₁ = [−1, −1]，W₂ = [[1, 1]]，b₂ = 0。对于 x = [3, 1]，隐藏层预激活值为 [1, −3]，因此 h = [1, 0]，ŷ = 1。将输入交换为 [1, 3]，得到 h = [0, 1]，输出仍为 1。相同输入 [2, 2] 在 ReLU 前得到 [−1, −1]，预测值为 0。这个网络测量两个方向上超出一个单位容差的不一致。参数量为 2×2 + 2 + 1×2 + 1 = 9。这些手选权重展示的是表示能力，并不意味着训练必然找到这一解。

### 先认识这些词

- **隐藏单元:** 网络内部计算得到的中间特征。

- **仿射映射:** 加权求和后加上偏置的变换。

- **表征:** 某一层用来描述输入的数值特征。

### 一步步想清楚

**为什么两层仿射变换仍是一个仿射映射？**

把 h=Ax+a 代入 Bh+b，得到 BAx+(Ba+b)，仍是矩阵乘 x 再加偏置。

**[3,1] 为什么得到隐藏结果 [1,0]？**

第一单元算 3−1−1=1，第二单元算 −3+1−1=−3。ReLU 保留 1，将 −3 置零。

**输入变了，参数数量会变吗？**

不会。两个输入、两个隐藏单元、一个输出共有 4+2+2+1=9 个参数，与输入数值无关。

<a id="lab-dl-representations-dl-mlp-gauges"></a>

### 追踪两个隐藏单元

用 NumPy 向量实现列向量公式。

```python
import numpy as np
w1 = np.array([[1.,-1.], [-1.,1.]])
b1 = np.array([-1.,-1.])
w2 = np.array([1.,1.]); b2 = 0.
for values in [[3.,1.], [1.,3.], [2.,2.]]:
    x = np.array(values)
    hidden = np.maximum(0, w1 @ x + b1)
    output = w2 @ hidden + b2
    print('input:', values, 'hidden:', hidden.tolist(), 'output:', float(output))
print('parameters:', w1.size + b1.size + w2.size + 1)
```

**在本地运行**

```sh
python dl-representations-dl-mlp-gauges.py
```

**预期输出**

```text
input: [3.0, 1.0] hidden: [1.0, 0.0] output: 1.0
input: [1.0, 3.0] hidden: [0.0, 1.0] output: 1.0
input: [2.0, 2.0] hidden: [0.0, 0.0] output: 0.0
parameters: 9
```

**逐步读懂代码**

w1 @ x 计算两个加权和。加 b1 等于减去容差。maximum 对各单元应用 ReLU。第二次点积加总剩余信号。size 统计存储的标量，最后加一统计输出偏置。权重为手工设计，用于展示表示能力，脚本没有拟合参数。

### 这些知识可以用在哪里

**仪表一致**

测试 [2,2]、[3,1]、[1,3]，输出为 0、1、1。解释为何交换不等读数会保持这个网络的分歧分数。

**改变容差**

把第一层两个偏置从 −1 改为 −2，此时 [3,1] 输出零。隐藏规则从超过一改为超过二才响应。

**类比的边界与注意事项:** 多层感知机不会自动利用图像的局部性或序列顺序。更多参数可能增加记忆训练数据的程度、计算量以及对预处理的敏感性。若 ReLU 单元在整个训练中都接收负输入，该分支可能无法获得有用梯度。拟合出的关系即使看起来合理，也不等于已经证明它能泛化。

**记住这一点:** 非线性隐藏层的重要性在于改变可用的表征；仅增加线性层的深度，无法构建非线性的决策规则。

### 本节资料来源

- [PyTorch: 构建神经网络——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html)

<a id="dl-softmax-loss"></a>

## Softmax 与损失：把分数变为学习信号

### 01 / 从一个完整故事开始

妮娅为三种简单形状建立玩具分类器。最后一层输出三个数，但它们可以为负，和也不为一。她最初叫它们概率，后来发现它们只是原始分数。

妮娅使用 softmax，在共同尺度上比较分数。最大分数对应最大概率。随后她检查模型给正确形状多少概率，并把负对数概率作为损失。自信且正确时损失小，自信地犯错时损失大。训练于是有了精确目标，不再只统计哪个分数最大。

### 02 / 理解核心概念

Logit 是归一化前的原始类别分数。Softmax 对各分数取指数，再除以指数总和。所有概率为正，和为一。给每个 logit 减去同一个数不会改变结果；为数值稳定，常减去最大值。

单一正确类别的交叉熵是其概率的负自然对数。自然对数是 exp 的逆运算。概率一得到损失零，概率接近零则损失很大。即使获胜类别不变，这个目标仍鼓励给正确类别更多概率。

### 03 / 如何运用这个概念

对互斥的图像或文本类别，将 logits 与合适的交叉熵目标配对。库常直接接收原始 logits，并在内部结合归一化与损失。除非接口要求，不要额外重复应用 softmax。

### 04 / 官方教程与研究中的应用

PyTorch CrossEntropyLoss 文档说明未归一化 logits 与类别目标。这里为教学显式展示 NumPy 概率步骤。生产损失应使用数值稳定的组合实现。

### 05 / 公式与逐项解析

```text
pⱼ = exp(zⱼ−m)/Σ(k=1…C) exp(zₖ−m), m = maxₖ zₖ
L = −log p_y
```

zⱼ 是类别 j 的原始分数。C 是类别数，k 遍历类别，m 是最大原始分数。pⱼ 是归一化概率。y 为正确类别索引，p_y 是其概率。L 为损失，exp 为指数，log 为自然对数。

### 06 / 一步步算出结果

Logits 为 [2,1,0]，减去 2 得 [0,−1,−2]。指数约为 [1,0.367879,0.135335]，和为 1.503214。概率为 [0.665241,0.244728,0.090031]。若正确类别为零，L=−log(0.665241)≈0.407606。给各 logit 加 1000 不改变任何概率。

### 先认识这些词

- **原始类别分数:** 转换为类别概率之前的分数。

- **Softmax 归一化:** 将分数向量通过指数归一化转为和为一的概率。

- **交叉熵:** 这里指分配给正确类别的概率的负自然对数。

### 一步步想清楚

**平移后最大指数为何恰为一？**

最大 logit 减自身为零，exp(0)=1。其他平移后分数非正，有助于避免溢出。

**分类正确为什么仍有正损失？**

类别零以约 0.665 的概率胜出，并非一。负对数仍为正，训练仍可奖励提高该类概率。

**所有 logits 相等会怎样？**

各指数相同，三类概率均为 1/3。正确类损失为 log(3)，约 1.098612。

**为什么显示的目标概率为零，损失却仍有限？**

这里的零来自浮点下溢。基于 logits 的公式直接表示对数概率，无须先把指数值舍入成零。

<a id="lab-dl-softmax-loss-dl-stable-softmax-loss"></a>

### 计算稳定概率与交叉熵

Python 示例中的类别编号从零开始。

```python
import numpy as np
logits = np.array([2.,1.,0.]); target = 0
def softmax(values):
    shifted = values-values.max()
    weights = np.exp(shifted)
    return weights/weights.sum()
probabilities = softmax(logits)
loss = -np.log(probabilities[target])
print('probabilities:', np.round(probabilities,6).tolist())
print(f'sum: {probabilities.sum():.6f}; loss: {loss:.6f}')
print('shift unchanged:', bool(np.allclose(probabilities,softmax(logits+1000))))
```

**在本地运行**

```sh
python dl-softmax-loss-dl-stable-softmax-loss.py
```

**预期输出**

```text
probabilities: [0.665241, 0.244728, 0.090031]
sum: 1.000000; loss: 0.407606
shift unchanged: True
```

**逐步读懂代码**

max 找到平移值。exp 将相对分数转为正权重，除以总和完成归一化。用 target 索引取正确类别概率，再以 log 计算损失。allclose 在浮点容差内检查平移不变性。这行简单损失适合当前 logits；极端情况应使用稳定的 log-sum-exp 损失实现。

<a id="lab-dl-softmax-loss-stable-loss-gradient-check"></a>

### 概率下溢时仍能计算有限损失

米娜发现网络给错误类别极大的分数，目标类别的概率被舍入为零，直接算 -log(概率) 就出问题。她回到 logits，用 log-sum-exp 计算交叉熵，损失仍然有限。随后逐一微调 logit，用数值斜率核对解析梯度。

```python
from math import exp, log, isclose

def probabilities(logits):
    peak = max(logits)
    weights = [exp(z - peak) for z in logits]
    return [w / sum(weights) for w in weights]

def cross_entropy(logits, target):
    peak = max(logits)
    return (peak - logits[target]) + log(sum(exp(z - peak) for z in logits))

logits = [1000.0, 0.0, -1000.0]
target = 2
p = probabilities(logits)
loss = cross_entropy(logits, target)
gradient = [value - int(j == target) for j, value in enumerate(p)]
epsilon = 1e-4
numeric = []
for j in range(len(logits)):
    plus, minus = logits.copy(), logits.copy()
    plus[j] += epsilon
    minus[j] -= epsilon
    numeric.append((cross_entropy(plus, target) - cross_entropy(minus, target)) / (2 * epsilon))
assert p[target] == 0.0 and loss == 2000.0
assert all(isclose(a, b, abs_tol=1e-6) for a, b in zip(gradient, numeric))
print("target probability:", p[target])
print(f"finite loss: {loss:.1f}")
print("analytic gradient:", gradient)
print("numeric gradient:", [round(value, 6) for value in numeric])
```

**在本地运行**

```sh
python dl-softmax-loss-stable-loss-gradient-check.py
```

**预期输出**

```text
target probability: 0.0
finite loss: 2000.0
analytic gradient: [1.0, 0.0, -1.0]
numeric gradient: [1.0, 0.0, -1.0]
```

**按执行顺序理解**

1. 取指数前减去最大 logit，不需要计算正指数。

2. 直接计算 log-sum-exp 减去目标 logit，不对已经被舍入的概率取 log。

3. 把 p_j-1[j=target] 与中心差分比较，每次只改变一个坐标。

**逐步读懂代码**

对有限 logits z 与目标 t，L=(m-z_t)+log(Σ_j exp(z_j-m))，其中 m=max_j z_j。代数上它等于 -log softmax(z)_t，但避免先生成极小的目标概率。本例 m-z_t=2000，剩余对数项约为零。

梯度为 dL/dz_j=p_j-1[j=t]，指标函数只在目标类别处为一。得到 [1,0,-1]：降低最大的错误 logit，或提高目标 logit，都能降低损失。中心差分为 (L(z+epsilon×e_j)-L(z-epsilon×e_j))/(2×epsilon)，e_j 只改变第 j 个坐标。

有限差分是诊断工具，不是反向传播的替代。epsilon 太大会引入近似误差，太小会在相减时损失精度。稳定公式也不能修复 NaN 输入或任意浮点范围限制。框架交叉熵接口通常接受 logits，不要重复应用 softmax。

### 这些知识可以用在哪里

**更换真实标签**

保持 logits [2,1,0]，把真实标签设为类别二。损失约为 2.407606，因为模型给正确类的概率最小。

**安全平移分数**

给全部 logits 加 1000，再运行稳定函数。概率保持不变。直接对大原始 logits 取指数可能溢出，因此平移具有实际作用。

**类比的边界与注意事项:** Softmax 概率不自动得到校准。单标签交叉熵假定类别互斥。多个标签可同时为真的任务需要合适的多标签目标。微型示例没有学习权重。

**记住这一点:** 区分原始分数、归一化概率与损失这三个量。

### 本节资料来源

- [PyTorch: CrossEntropyLoss：原始分数与目标](https://docs.pytorch.org/docs/2.14/generated/torch.nn.CrossEntropyLoss.html)

<a id="dl-shape-tracing"></a>

## 追踪整个网络的形状

### 01 / 从一个完整故事开始

伊莱为 2×2 灰度图像构建小网络。输入含批次轴与两个图像轴，全连接层需要每图四个数。他把整个数组展平，意外将两张图合成一条长列表。

伊莱重新画路径，每一步都写形状。他保留批次轴，只展平图像轴。隐藏层现在每行接收一张图。他还在运行前数清权重。修好的网络为每图输出两个类别分数。短小的形状追踪避免长时间训练建立在错误排列上。

### 02 / 理解核心概念

形状追踪沿整个计算检查轴大小。展平把图像像素重组为特征，不应合并独立样本。重塑保持元素数量，不会学习特征，也不知道哪个轴表示批次。

输入 d 维、输出 H 维的全连接层有 dH 个权重与 H 个偏置。批大小改变激活存储量，不改变这些参数数量。应同时追踪形状与轴含义，并用断言在训练前发现不匹配。

### 03 / 如何运用这个概念

准备小型图像分类器或序列预测头时可用形状追踪。写清预期输入形状、特征顺序、隐藏宽度与输出类别数。用两个明显不同的样本测试，检查是否意外混合。

### 04 / 官方教程与研究中的应用

NumPy reshape 文档说明元素数兼容规则与自动推断的 −1 维度。PyTorch 模型教程展示在全连接层前展平。这里用固定权重，仅检查形状。

### 05 / 公式与逐项解析

```text
X: B×h×w → X_flat: B×d, d=hw
H₁ = ReLU(X_flat W₁ + b₁): B×H
Z = H₁W₂+b₂: B×C
P = dH+H+HC+C
```

B 是批大小，h、w 是图像高宽，d 是展平像素数。H 是隐藏宽度，C 是类别数。W₁ 形状为 d×H，b₁ 有 H 项；W₂ 为 H×C，b₂ 有 C 项。H₁ 表示隐藏激活，Z 表示 logits，P 是总参数量，箭头表示重塑。

### 06 / 一步步算出结果

两张 2×2 图像的形状是 2×2×2，共八项。保留两行后展平为 2×4。H=3、C=2 时，后续形状为 2×3、2×2。参数量为 4×3+3+3×2+2=23。四张图使输入存储翻倍，但仍使用 23 个参数。

### 先认识这些词

- **展平:** 将选定轴合为特征轴，同时保留样本边界。

- **参数量:** 存储的可训练标量权重与偏置数量。

- **断言:** 必要条件不成立时停止执行的程序检查。

### 一步步想清楚

**为什么用 reshape(batch_size, −1)？**

第一维保留样本，自动推断的第二维收集每个样本剩余条目。这里八项除以二得到四。

**23 个参数从哪里来？**

第一层有十二个权重与三个偏置，第二层有六个权重与两个偏置。15 加 8 得 23。

**八个元素也能 reshape(4,2) 吗？**

数学上可以，但它把两图变成四行。合法数组运算仍可能破坏预期样本边界。

<a id="lab-dl-shape-tracing-dl-shape-path"></a>

### 打印每层形状

固定层只检查数据排列，不衡量识别质量。

```python
import numpy as np
images = np.arange(8,dtype=float).reshape(2,2,2)
flat = images.reshape(images.shape[0],-1)
w1 = np.ones((4,3)); b1 = np.zeros(3)
w2 = np.ones((3,2)); b2 = np.zeros(2)
hidden = np.maximum(0,flat @ w1+b1)
logits = hidden @ w2+b2
assert flat.shape == (2,4)
for name, array in [('images',images),('flat',flat),('hidden',hidden),('logits',logits)]:
    print(name, array.shape)
print('first flattened image:', flat[0].tolist())
print('parameters:', w1.size+b1.size+w2.size+b2.size)
```

**在本地运行**

```sh
python dl-shape-tracing-dl-shape-path.py
```

**预期输出**

```text
images (2, 2, 2)
flat (2, 4)
hidden (2, 3)
logits (2, 2)
first flattened image: [0.0, 1.0, 2.0, 3.0]
parameters: 23
```

**逐步读懂代码**

arange 提供八个已知值。第一次 reshape 创建两图，第二次保留批维。ones 与 zeros 明确建立权重和偏置形状。矩阵乘法保留批轴。断言检查计划的展平方式，最后统计标量参数。全一权重导致输出列相同是预期行为；这不是训练好的分类器。

### 这些知识可以用在哪里

**增加一张图**

创建三张 2×2 图像，检查路径 3×2×2 → 3×4 → 3×3 → 3×2。保持相同权重形状与参数量。

**改变类别数**

把最终类别数从二改为四。最后一层变为 3×4 加四个偏置，总参数量为 15+12+4=31。

**类比的边界与注意事项:** 形状兼容不证明像素顺序或标签正确。展平移除了显式空间轴。全连接图像模型可以学习空间模式，但没有卷积那样内置的局部权重共享。

**记住这一点:** 保留样本轴，追踪每步变换，并将参数数量与批大小区分开。

### 本节资料来源

- [NumPy: numpy.reshape：形状与索引顺序](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html)

- [PyTorch: 构建神经网络——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html)

<a id="dl-autograd"></a>

## 反向传播：沿依赖链求导

### 01 / 从一个完整故事开始

埃琳娜给小型传感器模型加入可训练缩放。预测看似合理，损失却一直不变。她画出输入到预测再到损失的路径，逐段询问：这一环稍微变化，会怎样影响最终误差？

手算得到首个权重的非零梯度，程序却没有该梯度。原来用于日志的转换切断了记录的依赖。埃琳娜让原张量继续参与模型，只转换独立显示值。学习现在能到达这个权重。她的调试问题变得具体：链条在哪里断了？

### 02 / 理解核心概念

导数是局部变化率。链式法则把依赖操作路径上的变化率相乘。反向传播从损失向参数应用这条法则。如果多条路径到达同一个值，各路径贡献相加。

自动微分记录操作并组合其导数，不是反复扰动每个权重来估计梯度。前向计算预测，反向计算梯度，优化器随后改变参数。这是三件不同的事。下面先手算小链条，再理解框架的工作。

### 03 / 如何运用这个概念

如果模型表现可疑，就缩小前向计算，直到能够手工对单个样本和少量参数求导。检查每个可训练参数是否获得梯度，以及梯度符号是否合理。在预定的更新边界清除累积梯度。将日志、数值转换和推理操作，与训练求导所依赖的张量路径分开。

### 04 / 官方教程与研究中的应用

官方 autograd 教程为参数启用梯度跟踪，构造损失，调用 backward，并读取参数梯度。它还演示了关闭跟踪和分离张量。优化教程说明了梯度累积与重置。两者共同展示了依赖链背后的具体框架机制：前向结果有效，并不足以证明所需的反向路径仍然存在。

### 05 / 公式与逐项解析

```text
z = wx + b, a = max(0,z), ŷ = va
L = ½(ŷ − y)²
∂L/∂v = (ŷ − y)a
∂L/∂w = (ŷ − y)v·1[z>0]·x
∂L/∂b = (ŷ − y)v·1[z>0]
θ_new = θ − η·∂L/∂θ
```

x 和 y 分别是一个标量输入及其目标值。w 和 b 定义第一个仿射运算，z 是预激活值，a 是整流后的激活值。v 将 a 缩放为预测 ŷ。L 是预测误差平方的一半。符号 ∂ 表示在其他独立输入固定时的偏导数。z 为正时 1[z>0] 等于一，为负时等于零；本例在零处采用零导数。θ 可以指 w、b 或 v 中的任意参数，θ_new 是其更新值，η 是学习率。

### 06 / 一步步算出结果

取 x = 2、y = 1、w = 0.5、b = 0、v = 2。前向计算得到 z = 1、a = 1、ŷ = 2，以及 L = 0.5。预测误差为 1，所以梯度为 ∂L/∂v = 1、∂L/∂w = 4、∂L/∂b = 2。令 η = 0.05，使用这组在旧参数处计算的梯度同步更新三个参数：v 变为 1.95，w 变为 0.3，b 变为 −0.1。新的 z 为 0.3×2 − 0.1 = 0.5，因此 ŷ = 0.975。新损失为 0.5×(−0.025)² = 0.0003125。这次下降验证了这个具体更新，并不保证所有学习率或所有样本都会下降。

### 先认识这些词

- **导数:** 一个输入变化时，输出的局部变化率。

- **链式法则:** 沿依赖操作相乘局部变化率，并将不同路径贡献相加。

- **反向传播:** 从后向前高效应用链式法则，计算参数梯度。

### 一步步想清楚

**为什么权重梯度为 4？**

损失误差为 1，输出权重为 2，ReLU 在 z=1 的斜率为 1，输入 x 为 2。相乘 1×2×1×2=4。

**为什么所有参数都从旧梯度更新？**

这些梯度描述同一个旧网络。如果先更新某权重，再计算另一梯度，就混用了不同网络状态。

**有限差分检查可以取代反向传播吗？**

它适合检查小型光滑例子，但要重复计算损失。权重多时成本高，在不可导点附近也难处理。

<a id="lab-dl-autograd-dl-chain-rule-check"></a>

### 检查梯度并更新网络

使用手工导数与数值检查，不依赖自动微分库。

```python
x, target = 2., 1.
w, b, v = 0.5, 0., 2.
def loss(weight, bias, output_weight):
    prediction = output_weight * max(0., weight*x+bias)
    return 0.5 * (prediction-target)**2
z = w*x+b; a = max(0., z); error = v*a-target
gv = error*a; gw = error*v*int(z>0)*x; gb = error*v*int(z>0)
epsilon = 1e-6
numeric = (loss(w+epsilon,b,v)-loss(w-epsilon,b,v))/(2*epsilon)
print(f'gradient w: {gw:.6f}; finite difference: {numeric:.6f}')
w, b, v = w-0.05*gw, b-0.05*gb, v-0.05*gv
print(f'new loss: {loss(w,b,v):.7f}')
```

**在本地运行**

```sh
python dl-autograd-dl-chain-rule-check.py
```

**预期输出**

```text
gradient w: 4.000000; finite difference: 4.000000
new loss: 0.0003125
```

**逐步读懂代码**

loss 定义精确前向目标。z、a、error 保留链式法则需要的中间值。int(z>0) 给出远离零点时 ReLU 的斜率。中心差分比较 w 两侧的邻近损失。所有更新使用保存的旧梯度。两者一致只检查了这个局部光滑例子，并不验证整个训练流程。

### 这些知识可以用在哪里

**ReLU 路径不通**

保持 x=2、w=0.5，将 b 设为 −2。激活前值为 −1，ReLU 不激活。此例沿 w 与 b 的梯度为零。

**检查一个导数**

运行 w 的中心有限差分检查，与链式法则结果比较。使用小扰动且远离 ReLU 拐点；浮点计算不要求完全相等。

**类比的边界与注意事项:** 梯度可能消失、爆炸，或无法反映跨越不可微选择时的有效进展。Autograd 对实际实现的目标求导，也会对其中的错误照样求导。浮点运算还会限制精度。小型有限差分检查能在光滑的玩具示例中暴露错误，但无法证明整个训练流程都正确。

**记住这一点:** 梯度是实际计算中依赖关系的轨迹；在期待优化器改善模型之前，先确保这条轨迹保持完整。

### 本节资料来源

- [PyTorch: 使用 torch.autograd 自动微分](https://docs.pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html)

- [PyTorch: 优化模型参数——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html)

<a id="dl-optimization"></a>

## 小批次与优化器：将梯度转化为更新

### 01 / 从一个完整故事开始

诺亚用多天录音训练模型，每次更新前读取全档案很慢。他改用小批次，显示损失开始跳动，于是怀疑训练坏了。检查后发现各批样本不同，有些难得多。

诺亚改看较长的训练趋势与固定验证集，还比较 SGD 和 Adam。相同学习率产生不同首步，因为 Adam 保存并缩放梯度统计量。他同时记录批大小、损失平均方式和优化器设置，现在能解释测量噪声与更新差异。

### 02 / 理解核心概念

批梯度平均多个样本的梯度。在合适随机采样下，它估计完整训练目标的梯度。较小批次通常更嘈杂，但单次更新更便宜。一个 epoch 遍历训练集一次，一个 step 更新参数一次。

SGD 减去学习率乘梯度。Adam 保存梯度与梯度平方的移动平均，修正初始为零造成的偏差，再除以由平方梯度得到的尺度。这会改变每个参数的更新幅度。两种优化器都不能修复错误损失或错误标签。

### 03 / 如何运用这个概念

选择学习率之前，先明确损失是取平均还是求和。在符合数据假设时打乱训练样本；若观测相互依赖，则保留必要的分组。同时记录更新次数和 epoch 数。在受控的验证流程下比较优化器；如果恢复运行时应延续已有动量与自适应历史，就需要保存优化器状态。

### 04 / 官方教程与研究中的应用

PyTorch 优化教程使用显式训练循环，其中包含损失计算、反向传播、参数更新和梯度重置。Adam 参考文档给出了移动平均与偏差修正算法。这些一手来源支持同一训练接口下的两种具体实现：梯度提供局部信息，而优化器决定怎样利用保存的状态和超参数将信息转化为权重变化。

### 05 / 公式与逐项解析

```text
gₜ = (1/B) Σ(i=1…B) ∇θ ℓᵢ(θₜ₋₁)
SGD: θₜ = θₜ₋₁ − ηgₜ
Adam: mₜ = β₁mₜ₋₁ + (1−β₁)gₜ
vₜ = β₂vₜ₋₁ + (1−β₂)gₜ²
m̂ₜ = mₜ/(1−β₁ᵗ), v̂ₜ = vₜ/(1−β₂ᵗ)
θₜ = θₜ₋₁ − ηm̂ₜ/(√v̂ₜ + ε), m₀ = v₀ = 0
```

θ 是参数向量，t 统计更新次数，η 是学习率。B 是批次大小，i 索引批次内样本。ℓᵢ 是第 i 个样本的损失，∇θ 表示对参数求梯度，Σ 将梯度相加，gₜ 是其平均值。mₜ 和 vₜ 跟踪梯度的一阶矩与二阶矩。β₁ 和 β₂ 是介于零与一之间的衰减系数，其上标 t 表示乘方。帽号表示偏差修正。ε 是正的稳定项。平方、开方、乘积和除法均逐元素进行。初始矩为零；上述公式中 SGD 不含动量，两种优化器都不含权重衰减。

### 06 / 一步步算出结果

考虑两个标量目标 2 和 4，预测均为 θ，每个样本的损失为 ½(θ−y)²。在 θ₀ = 0 时，梯度分别为 −2 和 −4，因此 g₁ = −3。SGD 取 η = 0.1，得到 θ₁ = 0.3，平均损失从 5 降至 4.145。对于 Adam，独立地从零重新开始，取 β₁ = 0.9、β₂ = 0.999、ε = 10⁻⁸。首次得到 m₁ = −0.3、v₁ = 0.009。偏差修正后，m̂₁ = −3、v̂₁ = 9。参数增量约为 +0.1，平均损失约为 4.705。因此，相同学习率未必意味着相同更新幅度。这个单批次计算比较的是更新机制，并不是最终泛化表现，也不能判断哪个优化器更好。

### 先认识这些词

- **小批次:** 用于一次梯度估计的训练样本子集。

- **轮遍历:** 完整遍历训练样本一次。

- **移动平均:** 把旧摘要与新值的一部分结合起来的连续统计。

### 一步步想清楚

**为什么平均梯度为 −3？**

预测为零时，二分之一平方损失对目标 2 与 4 的梯度为 −2 与 −4。均值为 (−2−4)/2=−3。

**为什么 Adam 移动约 0.1，而非 0.3？**

偏差修正后，首阶梯度统计为 −3，平方梯度统计为 9。除以 √9 后，梯度幅度约为一。

**十次更新等于十轮遍历吗？**

只有每轮恰好一次更新时才相同。100 个样本、批大小 10 时，通常一轮有十次更新。

<a id="lab-dl-optimization-dl-sgd-adam-first-step"></a>

### 比较 SGD 与 Adam 的第一步

两种方法都从相同标量参数重新开始。

```python
import numpy as np
targets = np.array([2.,4.]); theta = 0.; rate = 0.1
gradient = np.mean(theta-targets)
sgd = theta-rate*gradient
beta1, beta2 = 0.9, 0.999
m = (1-beta1)*gradient; v = (1-beta2)*gradient**2
m_hat = m/(1-beta1); v_hat = v/(1-beta2)
adam = theta-rate*m_hat/(np.sqrt(v_hat)+1e-8)
for name, value in [('SGD',sgd), ('Adam',adam)]:
    loss = np.mean(0.5*(value-targets)**2)
    print(f'{name}: parameter {value:.6f}; loss {loss:.6f}')
```

**在本地运行**

```sh
python dl-optimization-dl-sgd-adam-first-step.py
```

**预期输出**

```text
SGD: parameter 0.300000; loss 4.145000
Adam: parameter 0.100000; loss 4.705000
```

**逐步读懂代码**

mean 形成一个批梯度。SGD 直接使用它。Adam 从零统计量开始，进行首次更新，再除以首步偏差修正项。sqrt 与小量 epsilon 构成分母。两个损失都采用二分之一平方的约定。这只解释一批数据的更新机制，不能判断哪个优化器普遍更好。

### 这些知识可以用在哪里

**求和与平均**

使用同样的两个样本梯度，其和为 −6、均值为 −3。学习率固定时，从平均改为求和会使 SGD 步长加倍。

**带状态恢复**

列出 Adam 恢复训练需要的内容：权重、步数和两种移动平均。只恢复权重会重置适应历史，改变后续更新。

**类比的边界与注意事项:** 单个批次损失有噪声，并不足以诊断发散；反过来，平滑的训练曲线也不能证明泛化。Adam 的自适应无法修复错误标签、数据泄漏或不合理目标。梯度累积还需要一致的归一化：累积多个批次均值却不适当缩放，会改变有效梯度，也可能改变预期更新。

**记住这一点:** 区分梯度估计与更新规则，并在明确批次大小、归一化方式和优化器状态的条件下比较学习过程。

### 本节资料来源

- [PyTorch: 优化模型参数——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html)

- [PyTorch: Adam——优化器参考文档](https://docs.pytorch.org/docs/main/generated/torch.optim.Adam.html)

<a id="dl-training-loop"></a>

## 训练循环：预测、衡量、更新、验证

### 01 / 从一个完整故事开始

奥马尔理解一次梯度更新，却不清楚完整训练程序怎样组合。他从三个合成点开始，目标都是输入的两倍，微型模型只有一个权重。每次循环计算预测、衡量误差、求梯度，再更新权重。

奥马尔另留两个验证点，每次更新后评估，但从不用验证目标计算梯度。他保存验证分数最好的权重。小型运行有了清楚的起点与终点。通过这个简单例子，他检查了日后用于大网络的基本流程。

### 02 / 理解核心概念

训练循环重复学习步骤。框架中的常见顺序是清除旧梯度、前向、算损失、反向传播、更新参数。此 NumPy 示例用公式代替自动微分，每轮只有一个完整批次。

验证衡量固定模型而不更新它。检查点保存供以后使用的候选状态。用验证选择检查点，再在未使用测试集评估最终选择。本例演示拟合与验证记录，不提出最终测试结论。

### 03 / 如何运用这个概念

拟合图像、声音或文本模型前，用已知微型数据检查训练流程。确认训练损失能下降，再核实验证不更新参数，所选检查点可恢复。

### 04 / 官方教程与研究中的应用

PyTorch 优化教程记录训练与评估阶段。这里的单权重模型去掉框架细节，使每个操作与保存状态都能检查。

### 05 / 公式与逐项解析

```text
ŷᵢ = wxᵢ
L_train = (1/n) Σᵢ (wxᵢ−yᵢ)²
g = (2/n) Σᵢ (wxᵢ−yᵢ)xᵢ
w_new = w−ηg
```

w 是唯一权重，xᵢ、yᵢ 是训练行 i 的输入与目标，ŷᵢ 是预测。n 为训练行数，L_train 为训练均方误差，g 为对 w 的导数，η 为学习率。验证使用其他行，从不贡献给 g。

### 06 / 一步步算出结果

使用 x=[−1,0,1]、y=[−2,0,2]、w=0。初始损失为 8/3≈2.666667，梯度为 (2/3)[−2+0−2]=−8/3。η=0.3 时，w 变为 0.8。新训练 MSE 为 (1.44+0+1.44)/3=0.96。验证输入 x=[−1.5,1.5]、目标 [−3,3]，此步后验证 MSE 为 3.24。

### 先认识这些词

- **训练循环:** 反复计算损失、梯度并更新参数的步骤序列。

- **检查点:** 某个训练步骤保存的模型状态。

- **验证遍历:** 在保留样本上预测评分而不改变参数。

### 一步步想清楚

**为什么首次权重变为 0.8？**

减去学习率乘梯度：0−0.3×(−8/3)=0.8。负梯度使权重朝真实斜率二增加。

**关系完全相同，验证损失为何更大？**

验证输入幅度更大。相同斜率误差在 ±1.5 处造成的预测误差大于 ±1 处。

**最佳验证检查点能称作测试结果吗？**

不能。验证参与了选择。最终测试必须与该选择过程分开。

**什么时候可以把批平均梯度同权平均？**

当各批样本数相同、损失使用相同逐样本归约方式，且梯度在相同参数处计算时可以。大小不同的最后一批会破坏第一个条件。

<a id="lab-dl-training-loop-dl-one-weight-training-loop"></a>

### 运行五步透明训练

用实际学习的单参数回归模型讲解循环结构。

```python
import numpy as np
x = np.array([-1.,0.,1.]); y = 2*x
valid_x = np.array([-1.5,1.5]); valid_y = 2*valid_x
weight = 0.; best_loss = float('inf'); best_weight = weight
for epoch in range(1,6):
    error = weight*x-y
    gradient = 2*np.mean(error*x)
    weight -= 0.3*gradient
    train_loss = np.mean((weight*x-y)**2)
    valid_loss = np.mean((weight*valid_x-valid_y)**2)
    if valid_loss < best_loss:
        best_loss, best_weight = valid_loss, weight
    print(f'epoch {epoch}: w={weight:.4f}, train={train_loss:.4f}, valid={valid_loss:.4f}')
print(f'best weight: {best_weight:.5f}')
```

**在本地运行**

```sh
python dl-training-loop-dl-one-weight-training-loop.py
```

**预期输出**

```text
epoch 1: w=0.8000, train=0.9600, valid=3.2400
epoch 2: w=1.2800, train=0.3456, valid=1.1664
epoch 3: w=1.5680, train=0.1244, valid=0.4199
epoch 4: w=1.7408, train=0.0448, valid=0.1512
epoch 5: w=1.8445, train=0.0161, valid=0.0544
best weight: 1.84448
```

**逐步读懂代码**

前两行建立不同的训练与验证输入。error 和 gradient 只用训练数组，减法更新标量权重。两种损失都在更新后重算，确保比较一致。if 保存最佳验证状态，标量赋值会复制数值。这里五轮就是五次整批更新。代码确实学习合成直线，但未评估真实神经网络应用。

<a id="lab-dl-training-loop-unequal-batch-accumulation"></a>

### 微批次大小不同时正确累积梯度

米娜的设备一次装不下四个样本，于是拆成三条和一条两个微批次。直接平均两批梯度，会让最后那一个样本占一半权重。她改为按样本数加权各批平均梯度，在累积期间固定参数，最后只做一次优化器更新。

```python
from math import isclose
x = [1.0, 2.0, 3.0, 4.0]
y = [2.0 * value for value in x]
weight = 0.0

def mean_gradient(indices, weight):
    return sum(2 * (weight * x[i] - y[i]) * x[i] for i in indices) / len(indices)

batches = [[0, 1, 2], [3]]
batch_gradients = [mean_gradient(batch, weight) for batch in batches]
wrong = sum(batch_gradients) / len(batches)
accumulated = sum(len(batch) * gradient for batch, gradient in zip(batches, batch_gradients)) / len(x)
full = mean_gradient(list(range(len(x))), weight)
updated = weight - 0.01 * accumulated
assert isclose(full, -30.0) and isclose(accumulated, full)
assert not isclose(wrong, full) and isclose(updated, 0.3)
print("batch gradients:", [round(g, 6) for g in batch_gradients])
print(f"unweighted mean: {wrong:.6f}")
print(f"sample-weighted gradient: {accumulated:.6f}")
print(f"one optimizer step: {updated:.6f}")
```

**在本地运行**

```sh
python dl-training-loop-unequal-batch-accumulation.py
```

**预期输出**

```text
batch gradients: [-18.666667, -64.0]
unweighted mean: -41.333333
sample-weighted gradient: -30.000000
one optimizer step: 0.300000
```

**按执行顺序理解**

1. 两个微批次使用相同 weight；若第一批后就更新，第二批梯度会在另一个参数点计算。

2. 第一批三条、第二批一条，两个平均值不能同权。

3. 每批平均梯度乘以该批样本数，累加后除以总数，最后更新一次。

**逐步读懂代码**

N 个样本的损失 L=(1/N)Σ_i l_i。若微批次 b 有 n_b 条样本，平均梯度为 g_b，则整体平均梯度为 g=Σ_b(n_b/N)g_b。这里平方误差 l_i=(w×x_i-y_i)^2 的导数是 2(w×x_i-y_i)x_i。

w=0 时，逐样本梯度是 -4、-16、-36、-64。第一批均值为 -56/3，第二批为 -64。把两批同权平均会得到 -124/3，约 -41.3333；正确均值是 (-4-16-36-64)/4=-30。学习率 0.01 时，w 更新为 0.3。

自动微分循环中，在累积前清空一次梯度，每批平均损失乘 n_b/N，逐批反向传播，最后更新一次。严格等价要求损失能逐样本相加，且参数固定。BatchNorm 等依赖批次的操作、随机层、每微批次单独裁剪以及浮点求和，都可能改变结果。因此梯度累积并不自动等同于所有大批次训练配置。

### 这些知识可以用在哪里

**不更新的情况**

把学习率设为零。权重保持零，两种损失均不变。用它确认学习确实来自更新那一行。

**恢复所选状态**

循环结束后用 best_weight 预测，不要假定最后权重一定最好。在这个简单例子中两者相同，真实噪声任务可能选择较早步骤。

**类比的边界与注意事项:** 在这条精确直线上损失下降，不能证明真实数据泛化。反复选择检查点也可能过拟合验证。大模型还需合适批处理、状态管理与独立测试。

**记住这一点:** 分开参数更新与模型评估，保存验证实际选择的状态。

### 本节资料来源

- [PyTorch: 优化模型参数——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html)

- [PyTorch: 在 PyTorch 中清空梯度](https://docs.pytorch.org/tutorials/recipes/recipes/zeroing_out_gradients.html)

<a id="dl-regularization"></a>

## 正则化：训练时引入噪声，评估时明确模式

### 01 / 从一个完整故事开始

索菲亚用乐器声音训练网络。训练准确率上升，验证准确率却不再改善。她尝试 dropout，在训练中移除部分隐藏激活，并用相同保留录音公平比较改动。

演示时，同一个声音多次运行得到不同分数。索菲亚发现模型仍在训练模式，每次使用不同 dropout 掩码。评估模式消除了这部分随机性，梯度记录则另行控制。这件事让她理解：正则化要由证据选择，使用模型时也要明确设置执行模式。

### 02 / 理解核心概念

Dropout 在训练中随机移除部分激活。反向缩放 dropout 把保留下来的值除以保留概率，使每个激活的期望不变。期望指重复随机掩码后的平均，不是每次运行都得到同一个值。

评估时，这个 dropout 层直接传递输入。框架的评估模式改变 dropout、批归一化等层的行为。关闭梯度记录则是另一件事：避免保存导数图。两种开关不会自动替代彼此。

### 03 / 如何运用这个概念

使用同一份预留数据和明确的选择指标比较正则化设置。在工作流程中显式标明训练与评估模式切换。普通验证时选择评估行为，并在分析不需要梯度时关闭梯度记录。如果分数仍不稳定，还应检查数据增强及其他随机操作；改变 dropout 模式无法消除全部变化来源。

### 04 / 官方教程与研究中的应用

PyTorch 的 Dropout 参考文档明确说明了训练时随机置零与按存活概率缩放，以及评估时的恒等行为。Module 参考文档说明，评估模式会影响 dropout 和批归一化等模块。这些是实际库中的运行差异，而不只是术语。它们解释了为什么保存一组权重时，还必须配套适当的评估流程。

### 05 / 公式与逐项解析

```text
mⱼ ∼ Bernoulli(1−p), 0 ≤ p < 1
h̃ⱼ = mⱼhⱼ/(1−p)
E[h̃ⱼ | hⱼ] = hⱼ
Var(h̃ⱼ | hⱼ) = hⱼ²p/(1−p)
h_eval,ⱼ = hⱼ
```

hⱼ 是 dropout 前的第 j 个激活，在计算期望时将其视为固定值。p 是丢弃该激活的概率，所以 1−p 是存活概率。mⱼ 是来自伯努利分布的二值掩码：一表示保留，零表示删除。h̃ⱼ 是按存活概率缩放后的随机训练输出。E 表示期望，Var 表示方差，竖线表示以原始激活为条件。h_eval,ⱼ 是对应的评估输出。下标 j 选择一个元素；普通逐元素 dropout 会为每个元素采样掩码。

### 06 / 一步步算出结果

取固定激活 hⱼ = 4，dropout 概率 p = 0.25。输出以 0.75 的概率为 4/0.75 = 16/3，否则为零。其期望为 0.75×16/3 = 4。二阶矩为 0.75×(16/3)² = 64/3，所以方差为 64/3 − 4² = 16/3，约为 5.3333。对于向量 [2, 4] 和一次采样得到的掩码 [1, 0]，训练输出为 [8/3, 0]；评估输出则为 [2, 4]。一次随机前向传播显然不同于期望值。因此，保持均值并不消除训练噪声，也不保证下游非线性预测等于所有掩码下预测的平均值。

### 先认识这些词

- **Dropout 掩码:** 表示某次训练中哪些激活保留的零一选择。

- **期望:** 按概率加权的可能结果平均值。

- **评估模式:** 用于评估的层行为设置，与梯度记录不同。

### 一步步想清楚

**为什么保留值要除以 0.75？**

它只有 75% 的次数被保留。保留概率 0.75 乘缩放值 h/0.75，期望仍为 h。

**为什么一次输出可以为零，期望却为四？**

期望平均所有可能结果。某次被丢弃就为零，其他次数使用较大的保留值，使平均得到平衡。

**均值相同就保持整个网络预测吗？**

不一定。后续非线性操作可能改变平均。这里的精确结论仅针对 dropout 层激活的期望。

<a id="lab-dl-regularization-dl-dropout-expectation"></a>

### 精确计算 dropout 结果

枚举标量结果，并用固定向量掩码保证输出可重复。

```python
import numpy as np
probability = 0.25; h = 4.
outcomes = np.array([0., h/(1-probability)])
weights = np.array([probability, 1-probability])
expected = weights @ outcomes
variance = weights @ (outcomes-expected)**2
vector = np.array([2.,4.]); mask = np.array([1.,0.])
print(f'expectation: {expected:.6f}; variance: {variance:.6f}')
print('training:', np.round(mask*vector/(1-probability),6).tolist())
print('evaluation:', vector.tolist())
```

**在本地运行**

```sh
python dl-regularization-dl-dropout-expectation.py
```

**预期输出**

```text
expectation: 4.000000; variance: 5.333333
training: [2.666667, 0.0]
evaluation: [2.0, 4.0]
```

**逐步读懂代码**

outcomes 列出被丢弃和保留的标量值，weights 保存其概率。两次点积精确计算期望与方差，无需抽样。固定掩码展示一种可能的向量传播，评估则返回原向量。代码演示 dropout 机制，没有训练或验证声音模型。

### 这些知识可以用在哪里

**两个固定掩码**

对 [2,4] 用 p=0.25，分别应用掩码 [1,0] 与 [0,1]。比较两者输出与评估时不变的输入。固定演示掩码不等于随机训练。

**稳定的声音演示**

对框架模型分别设置评估行为与关闭梯度记录。若分数仍波动，先检查随机预处理和其他操作，再判断是否与 dropout 有关。

**类比的边界与注意事项:** 当小模型本来就难以利用信息时，dropout 删除这些信息可能反而有害。其比例和位置需要验证。评估模式本身不会冻结参数，也不会关闭梯度；关闭梯度也不会关闭 dropout。其他正则化方法、增强方式和归一化层各有行为，不能视为可互换的方法。

**记住这一点:** 正则化是训练选择；评估模式与梯度记录是独立的执行选择，都必须符合每次前向传播的目的。

### 本节资料来源

- [PyTorch: Dropout——层参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.Dropout.html)

- [PyTorch: Module——训练与评估模式](https://docs.pytorch.org/docs/main/generated/torch.nn.Module.html)

- [PyTorch: 自动微分机制——梯度与评估模式](https://docs.pytorch.org/docs/main/notes/autograd.html)

<a id="dl-normalization"></a>

## 归一化：说明哪些值共享统计量

### 01 / 从一个完整故事开始

玛拉检查声音模型的隐藏特征，有些行整体数值很大，有些很小。她希望下一步变换前的尺度更可控，于是尝试减均值、除标准差。计算似乎简单，但一个问题会改变一切：统计量来自哪些值？

玛拉比较每行自己的特征与跨样本汇总的值，两者是不同操作。她为当前草图选择层归一化，并明确标出特征轴。分母还加入一个小正数，让一行没有变化时也能稳定计算。

### 02 / 理解核心概念

层归一化在每个样本内部，沿指定特征轴计算均值和方差。先减均值，再除以方差加 epsilon 后的平方根，最后可应用学习的尺度与偏移。尺度和偏移让训练恢复有用范围。

批归一化跨批次统计，具体层还可能包含空间轴，评估时通常使用运行统计量。层归一化在训练和评估时都使用当前样本的指定轴。两者都不同于只在训练数据上拟合一次的输入缩放器。

### 03 / 如何运用这个概念

在序列或图像网络中加入归一化时，明确指定轴。用均值差异很大的样本组成批次测试。在这里逐样本层归一化规则下，改变一个样本不应改变另一个。

### 04 / 官方教程与研究中的应用

PyTorch LayerNorm 文档定义所选末尾维度、总体形式方差及学习尺度与偏移。BatchNorm2d 文档说明运行统计量。本例单独演示尺度为一、偏移为零的层归一化。

### 05 / 公式与逐项解析

```text
μ = (1/d)Σⱼ xⱼ; v = (1/d)Σⱼ(xⱼ−μ)²
hⱼ = γⱼ (xⱼ−μ)/√(v+ε) + βⱼ
```

xⱼ 是单个样本的第 j 个特征，d 是参与归一化的特征数。μ 为其均值，v 为总体方差，ε 为防止分母为零的小正数。γⱼ、βⱼ 为学习尺度与偏移，hⱼ 为归一化输出。公式逐个样本计算。

### 06 / 一步步算出结果

对 [1,3]，均值为 2，方差为 ((−1)²+1²)/2=1。γ=[1,1]、β=[0,0]、ε=0.00001 时，输出约 [−0.999995,0.999995]。对 [10,14]，均值 12、方差 4，输出约 [−0.999999,0.999999]。Epsilon 使输出方差接近一，但不精确等于一。

### 先认识这些词

- **层归一化:** 在单个样本内部沿指定特征轴归一化。

- **方差:** 这里是各值到均值之差的平方平均。

- **Epsilon 小量:** 加入分母相关计算、使其有定义的小正数。

### 一步步想清楚

**计算行均值时为什么 keepdims=True？**

它保留形状为批次×1 的均值列。相减时，每个均值用于自己那一行，避免意外对齐别的轴。

**[5,5] 会怎样？**

中心化值与方差均为零。Epsilon 使分母为正，因此加学习偏移前输出 [0,0]。

**层归一化需要同批其他样本吗？**

按这里逐行规则，不需要。每行提供自己的统计量，与跨批次规则不同。

<a id="lab-dl-normalization-dl-row-layernorm"></a>

### 逐行归一化特征

使用总体方差约定与明确的 epsilon。

```python
import numpy as np
x = np.array([[1.,3.], [10.,14.]])
mean = x.mean(axis=1,keepdims=True)
variance = ((x-mean)**2).mean(axis=1,keepdims=True)
normalized = (x-mean)/np.sqrt(variance+1e-5)
print('means:', mean.ravel().tolist())
print('variances:', variance.ravel().tolist())
print('normalized:', np.round(normalized,6).tolist())
print('row means:', np.round(normalized.mean(axis=1),6).tolist())
```

**在本地运行**

```sh
python dl-normalization-dl-row-layernorm.py
```

**预期输出**

```text
means: [2.0, 12.0]
variances: [1.0, 4.0]
normalized: [[-0.999995, 0.999995], [-0.999999, 0.999999]]
row means: [0.0, 0.0]
```

**逐步读懂代码**

axis=1 选择行内特征，keepdims 保持统计量对齐以便广播。中心化差值平方后沿相同轴平均得到总体方差。分母先将方差加 epsilon，再开平方。ravel 仅为把统计量显示为一维。运算省略可训练尺度与偏移，等价于分别取一和零。

### 这些知识可以用在哪里

**改变另一行**

给第二行两项都加 100。第一归一化行保持不变，检查了逐样本隔离。

**恢复尺度**

归一化后把第二特征乘二，它的范围会变化。学习尺度可以在标准化步骤之后有意改变输出范围。

**类比的边界与注意事项:** 归一化可能去掉有用的幅度信息。轴错误会改变操作。学习尺度与偏移使最终输出不必均值为零、方差为一。批归一化状态需要明确评估策略。

**记住这一点:** 只有说明轴、方差约定与评估行为，归一化公式才完整。

### 本节资料来源

- [PyTorch: LayerNorm：轴、方差与仿射参数](https://docs.pytorch.org/docs/2.14/generated/torch.nn.LayerNorm.html)

- [PyTorch: BatchNorm2d——运行统计量参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.BatchNorm2d.html)

<a id="dl-convolution"></a>

## 卷积与残差路径：局部证据与保留的通路

### 01 / 从一个完整故事开始

俊拍摄带细小划痕的金属样本。划痕可能出现在画面各处，他希望同一个局部检测器能用于不同位置。滑动滤波器在图像上重复使用一组权重，因此不必为每个位置单独建检测器。

俊随后堆叠多层，并添加捷径，让输入绕过一组变换。这组变换可以学习对输入的修正。他的图现在区分两件事：卷积在空间共享局部操作，残差连接则增加直接穿过网络的路径。两者仍需合适数据与训练。

### 02 / 理解核心概念

卷积层让一个小权重表在相邻条目上滑动。重复使用权重表称为权重共享。多数神经网络库使用互相关，即不翻转滤波器。步幅是相邻位置的间距，填充是在边界外增加条目。

残差连接把输入加到学习出的修正上。两个数组必须形状匹配、含义对齐。维度不同时，可用学习到的投影改变一条分支。卷积与残差相加解决不同结构问题。捷径并不会让所有深网都容易训练。

### 03 / 如何运用这个概念

当邻域结构和重复空间模式是有用假设时，使用局部滤波器。每次改变步长或填充后，都要追踪空间尺寸。在加入残差分支之前，确认两条分支的形状和特征位置一致。分别比较训练与验证行为：捷径带来的优化改善，本身并不能证明未见图像上的表现也会提高。

### 04 / 官方教程与研究中的应用

ResNet 原论文在图像识别中评估残差网络，并在目标检测实验中使用其特征。论文说明了恒等捷径，以及维度不匹配时使用的投影。PyTorch 的 Conv2d 参考文档规定了互相关运算和空间尺寸规则。两者将具体实现的局部算子，与在学习变换旁设置捷径路径的实际网络结构联系起来。

### 05 / 公式与逐项解析

```text
Z[i,j] = b + Σ(u=0…k−1) Σ(v=0…k−1) K[u,v]X[i+u,j+v]
H_out = floor((H + 2p − k)/s) + 1
y = x + F(x;θ)
```

X 是图像的一个输入通道，K 是边长为 k 的方形卷积核，b 是标量偏置，Z 是输出。i 和 j 选择输出位置，u 和 v 索引卷积核元素。第一个公式假定步长为一且没有填充。H 是输入高度，p 是每侧填充量，s 是步长，H_out 是膨胀率为一时的输出高度；floor 表示向下取整。在残差公式中，x 和 y 是形状兼容的特征张量，F 是学习分支，θ 包含其参数。Σ 表示求和。

### 06 / 一步步算出结果

设 X = [[1, 0, 2], [2, 1, 0], [0, 3, 1]]，K = [[1, 0], [0, −1]]，b = 0。没有填充且步长为一时，输出高度为 floor((3−2)/1)+1 = 2，宽度也为 2。四个窗口的计算依次为 1−1 = 0、0−0 = 0、2−3 = −1，以及 1−1 = 0。因此 Z = [[0, 0], [−1, 0]]。另考虑残差输入 x = [2, −1]，学习得到的修正 F(x;θ) = [0.5, 0.25]。两者之和为 [2.5, −0.75]。如果该残差块在相加后应用 ReLU，最终输出为 [2.5, 0]。区分相加结果与后续激活，能避免错误计算捷径输出。

### 先认识这些词

- **卷积核:** 在多个输入位置重复使用的小权重表。

- **步幅:** 两次相邻滤波操作之间移动的位置数。

- **残差连接:** 把输入表征与形状兼容的学习修正相加。

### 一步步想清楚

**3×3 输入配 2×2 滤波器为何输出 2×2？**

步幅一、无填充时，滤波器有两个行起点与两个列起点，共四个窗口。

**左下输出为什么为 −1？**

窗口为 [[2,1],[0,3]]。非零滤波权重选中 2 并减去 3，得到 −1。

**元素数量相同就能做残差相加吗？**

不能。轴形状与特征含义必须对齐。仅为了尺寸匹配而重塑，可能悄悄混合不同含义。

<a id="lab-dl-convolution-dl-filter-residual"></a>

### 滑动滤波器并添加捷径

两个独立计算演示两种不同机制。

```python
import numpy as np
x = np.array([[1.,0.,2.], [2.,1.,0.], [0.,3.,1.]])
kernel = np.array([[1.,0.], [0.,-1.]])
out = np.zeros((2,2))
for row in range(2):
    for col in range(2):
        out[row,col] = np.sum(x[row:row+2,col:col+2] * kernel)
residual_sum = np.array([2.,-1.]) + np.array([0.5,0.25])
print('filter output:', out.tolist())
print('residual sum:', residual_sum.tolist())
print('after ReLU:', np.maximum(0,residual_sum).tolist())
```

**在本地运行**

```sh
python dl-convolution-dl-filter-residual.py
```

**预期输出**

```text
filter output: [[0.0, 0.0], [-1.0, 0.0]]
residual sum: [2.5, -0.75]
after ReLU: [2.5, 0.0]
```

**逐步读懂代码**

嵌套循环选择各个有效窗口。切片取出 2×2 条目，逐元素相乘再求和，实现不翻核的互相关。残差计算按对应坐标相加，maximum 随后应用 ReLU。这个手选滤波器不是训练好的划痕检测器，残差向量也是独立示例。

### 这些知识可以用在哪里

**常数图像**

把输入改为全一。滤波权重之和为零，因此每个有效窗口输出零。这展示局部对比检测，而不是物体识别。

**先残差相加，再激活**

将 [2,−1] 与 [0.5,0.25] 相加，先得 [2.5,−0.75]，再用 ReLU 得 [2.5,0]。要区分这两个操作。

**类比的边界与注意事项:** 权重共享不会带来对平移、旋转或尺度的完全不变性；边界和下采样都会产生影响。捷径不能直接相加维度不符或语义位置不一致的张量。残差网络仍依赖合适的初始化、优化、数据和容量。本节二维玩具滤波器只演示算术，并不是完整的划痕检测器。

**记住这一点:** 卷积在不同位置共享局部证据；残差连接保留绕过变换的通路。两者的作用来自不同的结构选择。

### 本节资料来源

- [PyTorch: Conv2d——卷积参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.Conv2d.html)

- [He et al. / arXiv: 用于图像识别的深度残差学习](https://arxiv.org/abs/1512.03385)

<a id="dl-token-embeddings"></a>

## 词元嵌入：把编号变为可训练向量

### 01 / 从一个完整故事开始

苔丝想分类机器人短指令。词不能直接进入矩阵乘法，她先把句子拆成词元，为每个词元分配整数编号，再按编号查找小向量。

苔丝用填充让同批序列长度一致。最初平均时包含填充，导致短句特征缩小。她加入掩码，只平均真实词元。后来交换两个词，发现平均值仍不变。查表和掩码已经正确，但当词序影响指令含义时，还需要感知位置的处理。

### 02 / 理解核心概念

嵌入表为每个词表编号保存一个向量。查编号就是选行。整数是地址，不是数值含义：词元 8 并不比词元 4 意义大一倍。训练通过任务损失改变表项。

填充加入占位位置，使序列等长。掩码指出哪些位置真实。掩码均值池化加总真实词元向量，再除以其数量。池化得到单个序列向量，却丢失顺序。后面可用位置嵌入或序列模型提供顺序信息。

### 03 / 如何运用这个概念

嵌入查表是文本分类器与语言模型的组成部分。训练与使用时保持分词器和词表一致。有关平均与损失要排除填充。解释嵌入前，检查未知词元处理。

### 04 / 官方教程与研究中的应用

PyTorch 嵌入文档说明按整数查权重表。词嵌入教程展示小型语言任务中的可训练词向量。这里的向量为手选，单独演示查表、填充与池化。

### 05 / 公式与逐项解析

```text
eᵢ = E[tᵢ]
s = (Σᵢ₌₁ᵀ mᵢeᵢ)/(Σᵢ₌₁ᵀ mᵢ)
```

E 是 V×d 嵌入表，V 为词表条目数，d 为向量维数。tᵢ 是位置 i 的词元编号，eᵢ 是选中行。T 为填充后的序列长度。真实词元 mᵢ 为一，填充为零，s 为池化向量。至少一个掩码项必须为一。

### 06 / 一步步算出结果

robot 映射为 [1,0]，moves 为 [0,2]，填充为 [0,0]。序列 [robot,moves,padding] 的掩码为 [1,1,0]，掩码和为 [1,2]，计数为 2，均值为 [0.5,1]。若除以填充长度 3，会错误地得到 [1/3,2/3]，不再是真实词元均值。

### 先认识这些词

- **嵌入表:** 为每个词表编号分配向量的可训练表。

- **填充:** 为了统一序列长度而增加的占位位置。

- **池化:** 将多个位置向量合为较小摘要，例如均值。

### 一步步想清楚

**为什么编号 2 不是编号 1 的两倍？**

编号只选择表中的行，数值间距没有预设语言含义。选出的向量承载学到的信息。

**为什么除以二而不是三？**

只有两个位置是真实词元。掩码从分子与分母中都排除占位项。

**池化向量能区分 robot moves 与 moves robot 吗？**

这个均值不能。加法忽略顺序。感知位置的结构必须保留或增加无序平均之外的信息。

<a id="lab-dl-token-embeddings-dl-embedding-mask-mean"></a>

### 查找词元并忽略填充

表的最后一行表示填充；向量为示意值，未经学习。

```python
import numpy as np
table = np.array([[1.,0.], [0.,2.], [0.,0.]])
ids = np.array([0,1,2]); padding_id = 2
vectors = table[ids]
mask = ids != padding_id
pooled = (vectors*mask[:,None]).sum(axis=0)/mask.sum()
reversed_ids = np.array([1,0,2])
reversed_mean = table[reversed_ids][reversed_ids != padding_id].mean(axis=0)
print('vectors:', vectors.tolist())
print('real-token count:', int(mask.sum()))
print('masked mean:', pooled.tolist())
print('reversed mean:', reversed_mean.tolist())
```

**在本地运行**

```sh
python dl-token-embeddings-dl-embedding-mask-mean.py
```

**预期输出**

```text
vectors: [[1.0, 0.0], [0.0, 2.0], [0.0, 0.0]]
real-token count: 2
masked mean: [0.5, 1.0]
reversed mean: [0.5, 1.0]
```

**逐步读懂代码**

table[ids] 为每个位置选一行。比较生成布尔掩码。[:,None] 添加特征广播轴，让每个位置的掩码作用于整个向量。沿轴零求和组合位置，mask.sum 统计真实词元。逆序得到相同均值，展示顺序丢失。代码只执行查表与池化。

### 这些知识可以用在哪里

**增加填充**

追加一个填充编号与掩码零。掩码均值必须仍为 [0.5,1]；没有掩码的均值会再次改变。

**未知词元策略**

在玩具词表中保留未知编号。评估时用它表示未见文本，不要临时编造新未训练行。检查它出现的频率。

**类比的边界与注意事项:** 查表本身不产生语义理解，训练任务塑造向量含义。平均对顺序不敏感。全填充序列计数为零，除法前需明确处理。

**记住这一点:** 把词元编号当作地址，保留掩码，并区分查表向量与有上下文的表征。

### 本节资料来源

- [PyTorch: Embedding：整数查找可训练表](https://docs.pytorch.org/docs/2.14/generated/torch.nn.Embedding.html)

- [PyTorch: 词嵌入：编码词汇语义](https://docs.pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html)

<a id="dl-attention"></a>

## 注意力：通过学习到的比较提取上下文

### 01 / 从一个完整故事开始

阿玛拉比较两条关于泵与阀门的笔记。用词相似，却交换了故障部件。平均词向量丢失了许多差别，她需要让各位置获取对自己有用的上下文。

阿玛拉为一个位置设置查询，让它与键比较，再按得到的权重混合值向量。她添加位置信息，使顺序能发挥作用。预测下一个词元时，还遮住未来位置。她的图现在解释了一个注意力头，但要成为有用语言模型，还需要其他层与训练。

### 02 / 理解核心概念

注意力是值向量的加权平均。查询表示要寻找什么，键提供用于比较的向量。点积给每对查询与键打分，再除以键宽度的平方根，随后使用损失章节学过的 softmax。每行权重之和为一。

掩码禁止不允许的信息连接。预测下一个词元时，因果掩码阻止访问未来。自注意力从同一序列取得查询、键和值，交叉注意力可读取另一序列。注意力本身不编码顺序，所以模型还需要位置机制。

### 03 / 如何运用这个概念

选择注意力掩码之前，先明确每个查询允许读取哪些位置。分别追踪查询长度、键长度和特征维度，尤其是在交叉注意力中。用一个很小的示例检查掩码：让禁止访问的位置包含极端数值；改变这个数值，不应影响无法关注该位置的输出。将填充限制与因果限制视为不同的掩码要求。

### 04 / 官方教程与研究中的应用

Vaswani 及其同事在《Attention Is All You Need》中提出 Transformer，并在机器翻译上进行评估。原论文说明了缩放点积注意力、多头机制、位置编码和带掩码的解码器注意力。这是该机制在序列建模中的实际研究应用。本节的维护记录情境和数值向量用于解释运算，并未复现论文实验，也不宣称获得了论文结果。

### 05 / 公式与逐项解析

```text
S = QKᵀ/√dₖ + M
A[i,j] = exp(S[i,j]) / Σ(r=1…nₖ) exp(S[i,r])
O = AV
```

Q 包含 n_q 行查询，每行有 dₖ 个特征。K 包含 nₖ 行键，使用相同的 dₖ 个特征；Kᵀ 是其转置。V 包含 nₖ 行值，每行有 dᵥ 个特征。S 是分数矩阵，M 是加性掩码：允许连接的位置为零，禁止连接的位置为负无穷。A 包含注意力权重，O 包含 n_q 行输出，每行有 dᵥ 个特征。i 索引查询；j 和 r 索引键。exp 是指数函数，Σ 沿键求和，√dₖ 是正平方根。每个查询至少必须有一个允许访问的键。

### 06 / 一步步算出结果

使用一个查询 Q = [[√2, 0]]、两个键 K = [[1, 0], [0, 1]]，以及值 V = [[2, 0], [0, 4]]。键维度 dₖ 为 2。加入掩码前，缩放分数为 [√2/√2, 0/√2] = [1, 0]。Softmax 得到 [exp(1)/(exp(1)+1), 1/(exp(1)+1)] ≈ [0.731059, 0.268941]。因此输出约为 [1.462117, 1.075766]，即两个值向量的加权组合。如果禁止访问第二个键，就使用掩码 [0, −∞]。第二个权重变为零，第一个权重变为一，输出为 [2, 0]。掩码改变的是哪些信息可用，而不仅是对禁止位置的偏好强弱。这些数值描述单个注意力头在输出投影之前的结果。

### 先认识这些词

- **查询、键、值:** 用于提出比较、接受比较以及被混合的三类向量。

- **注意力头:** 一组查询、键、值投影及其上下文混合计算。

- **因果掩码:** 阻止一个位置读取后续位置的规则。

### 一步步想清楚

**为什么分数 [1,0] 不产生权重 [1,0]？**

Softmax 对两个有限分数都取指数。exp(0)=1，因此第二位置仍有正权重。禁止它需要掩码。

**输出第二坐标如何变成 1.075766？**

第一个值向量在该坐标贡献零，第二个贡献 0.268941×4，约为 1.075766。

**如果所有键都被遮住会怎样？**

这一行没有允许的信息源。对全负无穷做 softmax 未定义。设计掩码时应确保必要查询有有效来源，或规定明确的特殊规则。

<a id="lab-dl-attention-dl-attention-mask"></a>

### 比较允许与遮蔽的上下文

这是学习输出投影之前的单头数值示例。

```python
import numpy as np
q = np.array([[np.sqrt(2.),0.]])
k = np.array([[1.,0.], [0.,1.]])
v = np.array([[2.,0.], [0.,4.]])
scores = q @ k.T / np.sqrt(k.shape[1])
def softmax(row):
    values = np.exp(row-row.max(axis=-1,keepdims=True))
    return values/values.sum(axis=-1,keepdims=True)
weights = softmax(scores)
masked = softmax(scores + np.array([[0.,-np.inf]]))
print('weights:', np.round(weights,6).tolist())
print('output:', np.round(weights @ v,6).tolist())
print('masked output:', (masked @ v).tolist())
```

**在本地运行**

```sh
python dl-attention-dl-attention-mask.py
```

**预期输出**

```text
weights: [[0.731059, 0.268941]]
output: [[1.462117, 1.075766]]
masked output: [[2.0, 0.0]]
```

**逐步读懂代码**

@ 生成配对分数，.T 将键向量排成列。缩放使用键维度而非序列长度。softmax 减去行最大值以保持稳定。负无穷让被禁止键的指数为零，最后矩阵乘法混合值。向量是手选的，示例未实现分词、语言训练或完整 Transformer。

### 这些知识可以用在哪里

**测试禁止访问的值**

遮住第二个键，再把其值改为 [1000,1000]。输出应仍为 [2,0]。这直接检查信息边界。

**下一个词元的上下文**

对三个词元画出各查询可读取的位置。查询零只能读零，查询一可读零和一。不要让训练把未来目标当上下文。

**类比的边界与注意事项:** 稠密注意力为每对查询和键计算分数，长序列下成本可能很高。注意力权重并不自动构成可信的因果解释。缺少位置信息、掩码错误或整行被完全遮蔽，都可能让看似合理的计算失效。这个小示例省略了学习投影及完整 Transformer 所需的其他组件。

**记住这一点:** 注意力决定如何混合可用上下文；维度、位置信息与掩码，共同决定这种选择究竟具有怎样的含义。

### 本节资料来源

- [Vaswani et al. / arXiv: 注意力机制就是你所需要的](https://arxiv.org/abs/1706.03762)

<a id="dl-transformer-block"></a>

## Transformer 模块：把组件接起来

### 01 / 从一个完整故事开始

韦能计算一个注意力头，但语言模型图仍显得拥挤：注意力方框周围还有归一化、加法和另一个网络。他只用已经学过的操作重画一个模块。

韦让两个词元向量依次经过归一化、带掩码注意力与残差相加，然后由小型前馈网络逐位置变换，再做一次残差相加。序列长度与特征宽度始终不变。他先用手选权重检查模块，再考虑训练。现在这张图描述的是可追踪计算，但它仍只是组件，不是完整语言模型。

### 02 / 理解核心概念

Transformer 模块组合注意力、逐位置前馈层、残差连接与归一化。注意力跨位置混合信息，前馈网络用共享权重改变各位置内部特征，残差提供绕过变换的加法路径。

下面采用前置归一化：每次变换前归一化，再把输出加回残差流。还有其他排列，例如原论文的后置归一化。本例只有一个注意力头和因果掩码。假设位置信息在模块之前已提供；玩具数值不编码真实句子。

### 03 / 如何运用这个概念

Transformer 模块支持语言建模与翻译等序列任务。检查实现时，核对掩码方向、归一化位置、特征宽度与残差分支对齐。先验证一个模块，再堆叠多个。

### 04 / 官方教程与研究中的应用

原 Transformer 论文介绍组件与翻译任务。PyTorch TransformerEncoderLayer 文档说明前馈层与 norm_first 选项。本例明确选择微型前置归一化变体，不复刻预训练架构。

### 05 / 公式与逐项解析

```text
U = LN(X)
Y = X + Attention(U,U,U)
Z = Y + ReLU(LN(Y)W₁+b₁)W₂+b₂
```

X 是每行一个词元的序列表，LN 是逐词元层归一化，U 是其归一化结果。在这个恒等投影示例中，注意力以 U 同时作为查询、键和值。Y 是第一次残差和，W₁、b₁、W₂、b₂ 为前馈参数，Z 是模块输出。所有加法保持相同序列长度与特征宽度。

### 06 / 一步步算出结果

使用两个相同的行 [1,−1] 与很小 epsilon，归一化后约为 [1,−1]。第一查询只能读自己，权重为 [1,0]；第二查询看到两个相等分数，权重为 [0.5,0.5]。上下文向量均约为 [1,−1]。第一次残差得到 [2,−2]。这里 I = [[1,0],[0,1]] 是单位矩阵，乘以它不会改变向量。取 W₁=I、W₂=0.5I、偏置零，前馈修正约为 [0.5,0]，最终行约为 [2.5,−2]。

### 先认识这些词

- **残差流:** 沿连续加法捷径传递的表征。

- **逐位置前馈网络:** 同一个小网络分别作用于各词元特征向量。

- **前置归一化:** 在应用子层之前先归一化其输入。

### 一步步想清楚

**哪部分在词元之间混合信息？**

注意力负责混合。这里层归一化与前馈网络在每个词元行内部工作，残差相加组合对应位置。

**第二行注意力权重为何各为 0.5？**

允许读取的两个键向量相同，分数与指数相同，因此各分得一半权重。

**这是原论文的完全相同模块吗？**

不是。这里明确使用前置归一化、单头与恒等投影。原论文的归一化位置不同，参数结构也更丰富。

<a id="lab-dl-transformer-block-dl-tiny-transformer-block"></a>

### 运行完整玩具前置归一化模块

恒等注意力投影与重复行让结果容易检查。

```python
import numpy as np
def layer_norm(values):
    centered = values-values.mean(axis=-1,keepdims=True)
    return centered/np.sqrt((centered**2).mean(axis=-1,keepdims=True)+1e-8)
x = np.array([[1.,-1.], [1.,-1.]])
u = layer_norm(x)
scores = u @ u.T / np.sqrt(u.shape[1])
allowed = np.tril(np.ones((len(x),len(x)),dtype=bool))
scores = np.where(allowed,scores,-np.inf)
weights = np.exp(scores-scores.max(axis=1,keepdims=True))
weights /= weights.sum(axis=1,keepdims=True)
y = x + weights @ u
w1 = np.eye(2); w2 = 0.5*np.eye(2)
z = y + np.maximum(0,layer_norm(y) @ w1) @ w2
print('attention:', np.round(weights,6).tolist())
print('first residual:', np.round(y,6).tolist())
print('block output:', np.round(z,6).tolist())
```

**在本地运行**

```sh
python dl-transformer-block-dl-tiny-transformer-block.py
```

**预期输出**

```text
attention: [[1.0, 0.0], [0.5, 0.5]]
first residual: [[2.0, -2.0], [2.0, -2.0]]
block output: [[2.5, -2.0], [2.5, -2.0]]
```

**逐步读懂代码**

layer_norm 中心化每个词元的特征行。tril 建立下三角因果许可掩码，where 把禁止分数设为负无穷。稳定逐行 softmax 给出混合权重。y 将注意力加回原残差流。最后表达式归一化后，应用零偏置的双层 ReLU 网络，再加回修正。所有权重固定用于教学，没有语言训练或预训练权重。 np.eye(2) 创建这个 2 × 2 单位矩阵。把它乘以 0.5 后，对角线元素变为 0.5，其余元素仍为零。

### 这些知识可以用在哪里

**改变后一个词元**

只替换第二个输入行。在因果掩码下，第一输出行不应改变。可用此测试模块信息边界。

**去掉前馈修正**

把 W₂ 设为全零，保持 b₂ 为零。模块输出等于第一次残差结果 Y，能够单独观察第二子层的贡献。

**类比的边界与注意事项:** 玩具例子省略多头、学习到的查询键值投影、dropout 与词表输出头。相同行使计算简单，却没有测试丰富上下文行为。模块能运行本身不意味着学会语言。

**记住这一点:** Transformer 由熟悉操作按特定结构组合而成。逐分支追踪，并说明选择的变体。

### 本节资料来源

- [Vaswani et al. / arXiv: 注意力机制就是你所需要的](https://arxiv.org/abs/1706.03762)

- [PyTorch: TransformerEncoderLayer：组件顺序](https://docs.pytorch.org/docs/2.14/generated/torch.nn.TransformerEncoderLayer.html)

<a id="dl-transfer"></a>

## 迁移学习：适应特征，同时守住评估边界

### 01 / 从一个完整故事开始

莉娜有一小批手工陶瓷砖照片。大型图像模型从头训练时，很快记住这些照片。她改用预训练编码器的特征，只学习新的最终分类器。初次验证分数很高，但她发现同一砖块的近似照片同时存在于两边。

莉娜按完整砖块与拍摄批次隔离数据，重新建立冻结特征基线，再测试有限微调。独立测试集始终保留。现在她问两个问题：哪些权重需要改变，什么证据能公平支持改变？复用特征不会消除可靠实验的必要性。

### 02 / 理解核心概念

迁移学习从先前任务学到的参数开始。编码器把输入变成特征向量，预测头把向量映射为新答案。冻结编码器指不更新其参数；微调则更新部分或全部参数。

迁移是否有用，取决于旧特征是否适合新任务。要保持预训练模型要求的预处理，并决定如何处理批归一化运行统计等可变状态。只冻结梯度不会冻结这些状态。通过验证选择改动，并保持最终测试独立。

### 03 / 如何运用这个概念

先建立冻结特征基线，并采用与选定预训练权重兼容的预处理。调参之前确定训练、验证和测试分组。解冻层时，显式将其参数纳入优化器，并决定归一化统计量如何运行。在相同验证流程下比较变化，记录选定检查点，并在模型选择完成后再评估保留测试集。

### 04 / 官方教程与研究中的应用

PyTorch 迁移教程把 ImageNet 预训练 ResNet 用于蚂蚁与蜜蜂，演示微调及固定特征提取器配新最后一层。其流程分开训练与验证，并保留选定检查点。这里的砖块照片故事与固定向量预测头计算均为原创。

### 05 / 公式与逐项解析

```text
z = fθ(x), s = wᵀz + b, p = 1/(1+exp(−s))
L = −y log(p) − (1−y) log(1−p)
w_new = w − η(p−y)z
b_new = b − η(p−y)
θ_new = θ
```

x 是输入，fθ 是参数为 θ 的预训练编码器。z 是它输出的固定特征列向量。w 是分类器权重向量，wᵀ 是其转置，b 是标量偏置，s 是原始分数。p 是通过 sigmoid 得到的类别一概率；y 是取零或一的二元目标。L 是二元交叉熵，log 是自然对数，exp 是指数函数。η 是学习率。下标 new 表示同步更新后的数值；θ 保持不变，说明本次计算使用冻结编码器。

### 06 / 一步步算出结果

假设冻结编码器返回 z = [2, −1]。预测头初始值为 w = [0.1, 0.2]、b = 0，目标为 y = 1。原始分数为 0.1×2 + 0.2×(−1) = 0，因此 p = 0.5，L ≈ 0.693147。权重梯度为 (0.5−1)[2, −1] = [−1, 0.5]，偏置梯度为 −0.5。取 η = 0.1，新权重为 [0.2, 0.15]，新偏置为 0.05。新的分数为 0.4 − 0.15 + 0.05 = 0.3。因此 p ≈ 0.574443，L ≈ 0.554355。编码器没有改变。一个训练样本的损失下降，本身并不能说明微调是否会改善独立验证数据上的表现。

### 先认识这些词

- **编码器:** 将输入变成数值特征的模型组件。

- **预测头:** 把特征映射为任务答案的最终组件。

- **微调:** 用新任务数据更新先前学到的参数。

### 一步步想清楚

**只训练预测头时哪些值改变？**

预测头权重与偏置改变，编码器参数不变。本例假定固定特征向量已给定，没有可变编码器状态。

**为什么初始分数为零？**

对应项相乘后相加：0.1×2+0.2×(−1)+0=0。零的 sigmoid 为概率 0.5。

**这一步损失降低，是否足以支持解冻编码器？**

不够。它只说明一次预测头更新改善一个训练样本。冻结与微调需要按有效验证流程比较。

<a id="lab-dl-transfer-dl-frozen-feature-head"></a>

### 在固定特征上训练预测头

用预设向量代替编码器输出，不加载预训练模型。

```python
import numpy as np
z = np.array([2.,-1.]); w = np.array([0.1,0.2]); b = 0.; y = 1.
def probability(weights, bias):
    return 1/(1+np.exp(-(weights @ z+bias)))
p = probability(w,b)
gw = (p-y)*z; gb = p-y
w, b = w-0.1*gw, b-0.1*gb
new_p = probability(w,b)
print('new weights:', np.round(w,3).tolist())
print(f'new bias: {b:.3f}; new probability: {new_p:.6f}')
print(f'loss before: {-np.log(p):.6f}; after: {-np.log(new_p):.6f}')
print('fixed features:', z.tolist())
```

**在本地运行**

```sh
python dl-transfer-dl-frozen-feature-head.py
```

**预期输出**

```text
new weights: [0.2, 0.15]
new bias: 0.050; new probability: 0.574443
loss before: 0.693147; after: 0.554355
fixed features: [2.0, -1.0]
```

**逐步读懂代码**

probability 计算预测头点积与 sigmoid。二元交叉熵对原始分数的导数为 p−y，乘 z 得到权重梯度。更新只改变 w 与 b。由于 y=1，损失为 −log(p)。示例训练微型预测头一步，不能证明真实预训练特征迁移有效。

### 这些知识可以用在哪里

**砖块照片分组**

将同一砖块的所有照片放在同一集合。若拍摄批次带来共同光照，也要考虑这些分组。近似视角不是对新砖块的独立测试。

**固定特征的预测头更新**

运行代码并确认 z 从未改变。预测头分数由 0 升至 0.3。这样能把预测头更新与编码器适应分开。

**类比的边界与注意事项:** 预训练可能迁移无关特征或偏差，而过于激进的微调可能破坏有用特征。即使没有直接进行梯度更新，反复比较验证结果也可能过拟合验证集。小数据集仍带来较大不确定性。冻结主干还需要明确运行统计量与预处理策略；仅冻结参数并不是完整的可复现方案。

**记住这一点:** 复用有用表征，只进行证据支持的调整，并保护验证与测试边界，让关于适应新任务的结论值得信任。

### 本节资料来源

- [PyTorch: 计算机视觉迁移学习教程](https://docs.pytorch.org/tutorials/beginner/transfer_learning_tutorial.html)

- [PyTorch: Module——训练与评估模式](https://docs.pytorch.org/docs/main/generated/torch.nn.Module.html)

- [PyTorch: BatchNorm2d——运行统计量参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.BatchNorm2d.html)

<a id="dl-efficient-adaptation"></a>

## 低秩适应与可检查的推理路径

### 01 / 从一个完整故事开始

伊沃想让大型文本模型适应短设备笔记。更新全部权重需要许多可训练数值与优化器状态。他研究较小改动：固定基础矩阵，只学习两个瘦矩阵的乘积。

使用真实模型前，伊沃先在微型表上检查代数。他统计可调整项，并比较两种输出计算：一种保留独立修正分支，另一种把修正并入基础矩阵。两种答案在浮点容差内一致。他验证了实现思路，但任务准确率与真实速度仍需另行测量。

### 02 / 理解核心概念

低秩适应用 BA 表示权重变化。A 把输入压缩到 r 个坐标，B 将修正扩展到输出宽度。秩表示矩阵能产生多少独立方向。小 r 限制更新方向与可训练项数。

固定基础 W₀ 时，输出为 W₀x+sB(Ax)。适配器固定后，有效矩阵 W₀+sBA 在数学上给出相同结果。合并可在推理中移除额外适配器分支，但不会消除基础模型存储，也不保证每个系统都加速。质量与资源使用需分别比较。

### 03 / 如何运用这个概念

语言适应中，将任何参数更新与冻结特征或提示基线比较。记录适应了哪些矩阵、秩与缩放。使用合并模型前，在代表输入上检查数值一致性，并评估目标任务。

### 04 / 官方教程与研究中的应用

LoRA 原论文为语言模型提出冻结基础权重、训练低秩更新，并讨论合并。微型示例演示矩阵恒等式与参数数量，不声称复现论文基准成绩或真实适应模型。

### 05 / 公式与逐项解析

```text
W_eff = W₀+sBA
y = W₀x+sB(Ax) = W_eff x
P_adapter = r(d_in+d_out); P_full = d_in d_out
```

W₀ 是固定的 d_out×d_in 基础矩阵，x 有 d_in 项，y 有 d_out 项。A 形状为 r×d_in，B 为 d_out×r。r 为选定秩上界，s 为固定尺度，W_eff 为合并矩阵。P_adapter 统计可训练适配器项，P_full 统计完整矩阵项。这里省略偏置。

### 06 / 一步步算出结果

输入宽度 4、输出宽度 3、r=1。A 有四项、B 有三项，共七个可训练项，而完整矩阵有十二项。取 x=[1,2,3,4]、A=[1,0,−1,0]、B=[0.1,0.2,0.3]ᵀ，得到 Ax=−2，B(Ax)=[−0.2,−0.4,−0.6]。s=1、基础输出 [1,2,3] 时，最终为 [0.8,1.6,2.4]。

### 先认识这些词

- **低秩:** 矩阵只能表达少量独立输出方向。

- **适配器:** 附加在固定基础模型上的小型可训练修正。

- **推理:** 用选定模型计算输出，不在查询上拟合其权重。

### 一步步想清楚

**为什么适配器有七项？**

A 为 1×4，B 为 3×1，大小相加为四加三。固定的十二项基础矩阵仍然存在。

**为什么可以合并适配器？**

矩阵乘法对加法满足分配律：(W₀+sBA)x 等于 W₀x+sB(Ax)。浮点运算顺序可能引起微小数值差别。

**七个可训练项是否证明质量优于十二项？**

不能。这只描述优化参数数量。质量需要验证，速度和内存需要在真实执行配置下测量。

<a id="lab-dl-efficient-adaptation-dl-low-rank-merge"></a>

### 比较独立与合并适配器路径

所有矩阵都为算例预设，既未预训练，也未拟合。

```python
import numpy as np
base = np.eye(3,4)
a = np.array([[1.,0.,-1.,0.]])
b = np.array([[0.1],[0.2],[0.3]])
x = np.array([1.,2.,3.,4.]); scale = 1.
separate = base @ x + scale*b @ (a @ x)
merged = (base+scale*b @ a) @ x
print('full entries:', base.size, 'adapter entries:', a.size+b.size)
print('output:', np.round(separate,6).tolist())
print('merged agrees:', bool(np.allclose(separate,merged)))
```

**在本地运行**

```sh
python dl-efficient-adaptation-dl-low-rank-merge.py
```

**预期输出**

```text
full entries: 12 adapter entries: 7
output: [0.8, 1.6, 2.4]
merged agrees: True
```

**逐步读懂代码**

eye(3,4) 创建矩形恒等式基础映射。a 把四个输入压成一个修正坐标，b 再扩为三个输出。独立表达式保留两条路径，合并表达式先加矩阵修正。size 统计项数，allclose 检查数值一致。这个微型计算不支持速度结论，也没有训练模型适应。

### 这些知识可以用在哪里

**改变秩**

对 100×100 权重矩阵比较秩二与秩八。适配器项数为 400 与 1600，完整矩阵为 10000。更大适配能力仍需验证。

**检查合并**

对多个固定输入运行两条计算路径。用 allclose 而非浮点完全相等比较。检查合并结果时保留原基础矩阵与适配器。

**类比的边界与注意事项:** 低秩限制可用更新，可能遗漏任务需要的变化。可训练项少，不代表基础模型更小或推理普遍更快。量化权重等表示可能让合并更复杂。本例使用普通浮点数组与预设适配器值。

**记住这一点:** 区分可训练参数节省、数值等价、任务质量与实测推理成本。

### 本节资料来源

- [Hu et al. / arXiv: LoRA：大型语言模型的低秩适应](https://arxiv.org/abs/2106.09685)

## 知识速查

### [张量：让每个维度都有含义](#dl-tensors)

把每个张量维度视为有名称的约定；维度匹配是必要条件，而保留其含义才能保证计算正确。

### [激活函数：在加权和之间加入弯折](#dl-activations)

激活既改变向前传递的特征，也改变向后传递的斜率。

### [多层感知机：学习有用的中间特征](#dl-representations)

非线性隐藏层的重要性在于改变可用的表征；仅增加线性层的深度，无法构建非线性的决策规则。

### [Softmax 与损失：把分数变为学习信号](#dl-softmax-loss)

区分原始分数、归一化概率与损失这三个量。

### [追踪整个网络的形状](#dl-shape-tracing)

保留样本轴，追踪每步变换，并将参数数量与批大小区分开。

### [反向传播：沿依赖链求导](#dl-autograd)

梯度是实际计算中依赖关系的轨迹；在期待优化器改善模型之前，先确保这条轨迹保持完整。

### [小批次与优化器：将梯度转化为更新](#dl-optimization)

区分梯度估计与更新规则，并在明确批次大小、归一化方式和优化器状态的条件下比较学习过程。

### [训练循环：预测、衡量、更新、验证](#dl-training-loop)

分开参数更新与模型评估，保存验证实际选择的状态。

### [正则化：训练时引入噪声，评估时明确模式](#dl-regularization)

正则化是训练选择；评估模式与梯度记录是独立的执行选择，都必须符合每次前向传播的目的。

### [归一化：说明哪些值共享统计量](#dl-normalization)

只有说明轴、方差约定与评估行为，归一化公式才完整。

### [卷积与残差路径：局部证据与保留的通路](#dl-convolution)

卷积在不同位置共享局部证据；残差连接保留绕过变换的通路。两者的作用来自不同的结构选择。

### [词元嵌入：把编号变为可训练向量](#dl-token-embeddings)

把词元编号当作地址，保留掩码，并区分查表向量与有上下文的表征。

### [注意力：通过学习到的比较提取上下文](#dl-attention)

注意力决定如何混合可用上下文；维度、位置信息与掩码，共同决定这种选择究竟具有怎样的含义。

### [Transformer 模块：把组件接起来](#dl-transformer-block)

Transformer 由熟悉操作按特定结构组合而成。逐分支追踪，并说明选择的变体。

### [迁移学习：适应特征，同时守住评估边界](#dl-transfer)

复用有用表征，只进行证据支持的调整，并保护验证与测试边界，让关于适应新任务的结论值得信任。

### [低秩适应与可检查的推理路径](#dl-efficient-adaptation)

区分可训练参数节省、数值等价、任务质量与实测推理成本。

## 官方教程与原始研究

故事、讲解与计算例子由 Leon 原创编写。链接中的教程和研究属于各自作者；本资料为独立学习笔记，并非官方译本或官方认可课程。

- [张量——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html) — PyTorch

- [构建神经网络——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/buildmodel_tutorial.html) — PyTorch

- [使用 torch.autograd 自动微分](https://docs.pytorch.org/tutorials/beginner/basics/autogradqs_tutorial.html) — PyTorch

- [优化模型参数——基础入门](https://docs.pytorch.org/tutorials/beginner/basics/optimization_tutorial.html) — PyTorch

- [Adam——优化器参考文档](https://docs.pytorch.org/docs/main/generated/torch.optim.Adam.html) — PyTorch

- [Dropout——层参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.Dropout.html) — PyTorch

- [Module——训练与评估模式](https://docs.pytorch.org/docs/main/generated/torch.nn.Module.html) — PyTorch

- [Conv2d——卷积参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.Conv2d.html) — PyTorch

- [用于图像识别的深度残差学习](https://arxiv.org/abs/1512.03385) — He et al. / arXiv

- [注意力机制就是你所需要的](https://arxiv.org/abs/1706.03762) — Vaswani et al. / arXiv

- [计算机视觉迁移学习教程](https://docs.pytorch.org/tutorials/beginner/transfer_learning_tutorial.html) — PyTorch

- [自动微分机制——梯度与评估模式](https://docs.pytorch.org/docs/main/notes/autograd.html) — PyTorch

- [BatchNorm2d——运行统计量参考文档](https://docs.pytorch.org/docs/main/generated/torch.nn.BatchNorm2d.html) — PyTorch

- [ReLU 激活](https://docs.pytorch.org/docs/2.14/generated/torch.nn.ReLU.html) — PyTorch

- [CrossEntropyLoss：原始分数与目标](https://docs.pytorch.org/docs/2.14/generated/torch.nn.CrossEntropyLoss.html) — PyTorch

- [LayerNorm：轴、方差与仿射参数](https://docs.pytorch.org/docs/2.14/generated/torch.nn.LayerNorm.html) — PyTorch

- [Embedding：整数查找可训练表](https://docs.pytorch.org/docs/2.14/generated/torch.nn.Embedding.html) — PyTorch

- [TransformerEncoderLayer：组件顺序](https://docs.pytorch.org/docs/2.14/generated/torch.nn.TransformerEncoderLayer.html) — PyTorch

- [numpy.reshape：形状与索引顺序](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html) — NumPy

- [词嵌入：编码词汇语义](https://docs.pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html) — PyTorch

- [LoRA：大型语言模型的低秩适应](https://arxiv.org/abs/2106.09685) — Hu et al. / arXiv

- [在 PyTorch 中清空梯度](https://docs.pytorch.org/tutorials/recipes/recipes/zeroing_out_gradients.html) — PyTorch
