(neural-training-basics)=
# 神经网络训练基础

{doc}`fc-layer-basics`和{doc}`cnn-basics`中我们学习了如何**搭建**神经网络架构。但知道"建什么"不等于知道"怎么训练"——就像有了一辆车不等于会开车。本节我们将从{ref}`gradient-descent`和{ref}`back-propagation`的理论出发，掌握让神经网络真正"学会"的实战技巧。

## 训练的本质：从数据到能力

想象你在教小孩认数字。你不会只给他看一遍卡片就指望他记住，而是需要：
- **反复练习**：同一组卡片看很多遍（多轮训练）
- **及时反馈**：猜对了表扬，猜错了纠正（计算损失）
- **循序渐进**：从简单到复杂，调整学习节奏（学习率调度）
- **防止死记**：确保他理解数字的本质，不只是记住卡片（防止过拟合）

神经网络训练完全遵循同样的逻辑。{ref}`gradient-descent`告诉我们沿着梯度方向更新参数，但实践中还有大量细节需要把握。

---

## 核心训练概念

在深入之前，我们需要统一几个基本概念：

**Epoch（轮次）**：完整遍历整个训练数据集一次。就像把整副卡片从头到尾看一遍。MNIST有60,000张训练图片，一个epoch就是看完全部60,000张。

**Batch（批次）**：一次更新参数时使用的样本集合。如果一次看一张就更新，效率太低；如果看完60,000张才更新，内存可能不够。**Batch Size**就是每批的样本数，常用32、64、128等2的幂次。

**Iteration（迭代）**：完成一个批次的训练。一个epoch包含的迭代次数 = 总样本数 ÷ Batch Size。

```{note}
MNIST训练示例

- 总训练样本：60,000张
- Batch Size：64
- 每epoch的迭代次数：60,000 ÷ 64 ≈ 938次
- 训练10个epoch：总共迭代9,380次，更新参数9,380次
```

---

## 损失函数的选择

{ref}`loss-functions`中我们讨论了不同任务的损失函数设计。在实战中，选择主要取决于**任务类型**：

| 任务类型 | 推荐损失函数 | 为什么 |
|---------|------------|-------|
| 回归 | MSE / MAE | 直接衡量数值偏差 |
| 二分类 | BCE Loss | 衡量概率校准程度 |
| 多分类 | CrossEntropy | 结合Softmax，梯度更稳定 |

```{admonition} MNIST的交叉熵
:class: tip

MNIST是10类分类，使用**交叉熵损失**：

$$
\mathcal{L} = -\sum_{c=1}^{10} y_c \log(p_c)
$$

- $y_c$：真实标签的one-hot编码（目标类别为1，其余为0）
- $p_c$：Softmax输出的概率

**为什么不用MSE？** 分类任务的输出是概率分布，交叉熵对这种"分布间的差异"更敏感，且避免了梯度消失问题（见{ref}`back-propagation`）。
```

---

## 过拟合与欠拟合：学习的两种失败

训练神经网络就像准备考试——有两种典型的失败模式：

### 欠拟合：学得太少

想象一个学生只翻了翻课本，没做练习题就去考试：
- 训练时表现就不好（训练损失高）
- 考试时更差（验证损失也高）
- **原因**：模型太简单，没能捕捉数据的基本规律
- **解决**：增加网络深度、训练更长时间、降低正则化强度

### 过拟合：死记硬背

另一个学生把所有练习题都背下来了：
- 做练习题时几乎全对（训练损失很低）
- 遇到新题就不会（验证损失高，差距大）
- **原因**：模型记住了训练数据的噪声，而非学习通用规律
- **解决**：使用正则化、早停、数据增强、降低模型复杂度

```{tikz} 模型复杂度与泛化性能
\begin{tikzpicture}[scale=1.0]
  % 坐标轴
  \draw[->] (0,0) -- (8,0) node[right] {模型复杂度};
  \draw[->] (0,0) -- (0,5) node[above] {损失};
  
  % 训练损失：快速下降再趋平
  \draw[thick, blue, domain=0.5:7.5, smooth] 
    plot ({\x}, {3.0*exp(-0.35*\x) + 0.5});
  \node[blue] at (6.5,0.4) {训练损失};
  
  % 验证损失：U形，保证 > 训练损失
  \draw[thick, red, domain=0.5:7.5, smooth] 
    plot ({\x}, {3.0*exp(-0.35*\x) + 0.5 + 0.15*(\x-2)^2 + 0.2});
  \node[red] at (1.8,3.3) {验证损失};
  
  % 分区虚线
  \draw[dashed, gray] (2.2,0) -- (2.2,5);
  \node[gray] at (1,4.5) {欠拟合区};
  \draw[dashed, gray] (5.8,0) -- (5.8,5);
  \node[gray] at (6.8,2.5) {过拟合区};
  
  % 最佳点
  \fill[green!70!black] (3.2,1.9) circle (0.08);
  \node[green!70!black] at (3.3,2.6) {最佳点};
\end{tikzpicture}
```

图中的"最佳点"是我们追求的目标：足够复杂的模型捕捉规律，但不至于过拟合。找到这个点需要**验证集**——用训练集学习，用验证集调参，最后用测试集评估。

---

## 数据划分：训练集、验证集、测试集

为什么需要三组数据？

- **训练集**：学习参数（权重和偏置）
- **验证集**：选择超参数（学习率、网络结构、正则化强度）
- **测试集**：最终评估（只能用一次！）

```{warning}
**测试集的禁忌**

测试集只能用于最终报告，绝不能用来调参或选择模型。否则就像考试时偷看答案——分数虚高，真实能力被高估。
```

MNIST的标准划分：60,000训练 + 10,000测试。实践中，我们会从训练集中分出10%（6,000张）作为验证集，剩下54,000张真正用于训练。

---

(regularization)=
## 正则化：防止死记硬背的四种方法

正则化的核心思想：**限制模型的"记忆能力"，逼迫它学习"理解能力"**。

为什么需要限制？想象一个拥有超强记忆力的学生——他可以背下所有练习题答案，考试时遇到原题全对，但稍微变化就不会。神经网络也有这种"过强记忆"的能力，特别是参数量大时。正则化就是防止这种"死记硬背"。

---

### 1. L2正则化（权重衰减）：强制"粗笔画"

```{admonition} 核心洞察：权重大小决定决策边界的"粗细"
:class: tip

想象用画笔画画：
- **细笔画**（大权重）：可以画出非常锐利、复杂的边界，精确勾勒每个训练样本的细节——就像用0.5mm的针管笔，能画出极细的线条
- **粗笔画**（小权重）：只能画出平滑、模糊的轮廓，捕捉大体形状——就像用毛笔，线条自然柔和

神经网络的决策边界也是由权重决定的。大权重让边界变得"尖锐"，可以"钻牛角尖"去拟合每个训练点；小权重让边界保持"平滑"，只能学习通用的模式。
```

#### 为什么L2惩罚能实现这一点？

L2正则化在损失函数中加入权重的平方和：

$$
\mathcal{L}_{\text{total}} = \mathcal{L}_{\text{original}} + \frac{\lambda}{2} \sum_{i} w_i^2
$$

**关键洞察**：平方惩罚对大权重的"惩罚力度"远大于小权重。

| 权重值 | 原始损失惩罚 | 平方惩罚 | 相对增加 |
|-------|------------|---------|---------|
| 0.1 | 1 | 0.01 | 1% |
| 1.0 | 1 | 0.5 | 50% |
| 10.0 | 1 | 50 | 5000% |

**结果**：优化器会发现，与其让少数权重变得很大（承担巨额平方惩罚），不如让所有权重都保持较小。这就像税收——平方税率对大收入者更严厉，促使财富分布更均匀。

```python
# PyTorch中的L2正则化（weight_decay）
optimizer = torch.optim.Adam(
    model.parameters(),
    lr=0.001,
    weight_decay=1e-4  # λ = 0.0001
)
```

**为什么有效？** 小权重的网络更"保守"，不会为了拟合某个训练样本的噪声而剧烈调整输出。它必须找到更通用的规律，才能在保持权重小的同时降低原始损失。

---

### 2. Dropout：随机"罢工"迫使协作

Dropout 由 Srivastava 等人在 2014 年提出 {cite}`srivastava2014dropout`，是防止神经网络过拟合的有效方法。

```{admonition} 核心洞察：随机"点名"防止依赖"学霸"
:class: tip

想象一个班级有50人，老师提问时总是叫同一个"学霸"回答：
- 学霸越来越厉害（某些神经元权重变得极大）
- 其他同学从不思考（其他神经元权重趋于0）
- 结果：班级极度依赖学霸，一旦学霸生病（过拟合到新数据），全班表现崩盘

Dropout就像**随机点名**：每次提问随机抽一半同学，学霸也可能被抽中休息。
- **后果**：每个人都必须准备好回答问题
- **结果**：知识必须在全班**分布式存储**，形成**冗余表示**
```

#### 为什么冗余表示能防止过拟合？

过拟合的本质是"记忆"——模型记住了训练数据的具体特征（包括噪声）。如果知识只存储在少数几个神经元中，这些神经元就会"死记硬背"训练样本。

Dropout迫使知识分散存储：
- 识别"横线"的特征不是由某1个神经元专门负责，而是由10个神经元共同分担
- 即使其中5个被"dropout"了，剩下5个还能大致识别横线
- 每个神经元都不能"偷懒"记忆特定样本，必须学习通用特征

```python
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.dropout = nn.Dropout(0.5)  # 50%的dropout
        self.fc2 = nn.Linear(256, 10)
        
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.dropout(x)  # 训练时随机丢弃，测试时自动关闭
        return self.fc2(x)
```

**测试时为什么关闭Dropout？** 训练时的随机丢弃相当于训练了多个"子网络"（2^N种可能的组合）。测试时我们使用完整网络，相当于**集成（ensemble）**了所有这些子网络的预测，取平均——这通常比任何单个子网络更稳定。

---

(early-stopping)=
### 3. 早停法：在"最佳点"刹车

```{admonition} 核心洞察：训练就像在山谷中行走，不要越过谷底
:class: tip

想象你在一个山谷中（损失函数的Loss Landscape），从高处往低处走（梯度下降）：

**初期**：你在宽阔的山谷顶部，大步流星地向谷底走去——训练损失和验证损失都在下降，方向基本一致。

**中期**：你接近谷底，步伐变小，损失下降变慢——但仍朝着正确的方向微调。

**后期**：如果你继续走，可能会**越过谷底**，走上另一侧山坡——训练损失还在下降（因为你更贴合训练数据），但验证损失开始上升（因为你开始"钻牛角尖"，拟合了训练集的噪声）。
```

```{tikz} 早停法：在谷底停止，不要越过
\begin{tikzpicture}[scale=0.9]
  % Loss curve
  \draw[thick, domain=-3:3, smooth] plot (\x, {0.3*\x*\x});
  
  % Starting point
  \fill[blue] (-2.5, 1.875) circle (0.1);
  \node[blue] at (-2.5, 2.3) {起点};
  
  % Arrow showing descent
  \draw[->, thick, blue] (-2.5, 1.875) -- (-1.5, 0.675);
  \draw[->, thick, blue] (-1.5, 0.675) -- (-0.5, 0.075);
  
  % Optimal point
  \fill[green!70!black] (0, 0) circle (0.1);
  \node[green!70!black] at (0, -0.5) {最佳点（早停）};
  
  % Overfitting region
  \fill[red] (2, 1.2) circle (0.1);
  \node[red] at (2, 1.7) {过拟合区域};
  \draw[->, thick, red, dashed] (0.5, 0.075) -- (2, 1.2);
  
  % Labels
  \node at (0, 3) {损失};
  \node at (3.5, 0) {训练时间};
\end{tikzpicture}
```

#### 为什么验证损失上升意味着过拟合？

训练损失只衡量模型在**训练数据**上的表现。当你训练太久，模型开始"记住"训练数据中的噪声——那些只存在于训练集、不存在于真实世界的随机波动。

验证集是**独立的**数据，不包含训练集的噪声。所以当模型开始拟合训练噪声时，它在验证集上的表现反而会变差。

**早停法的本质**：在"学习真实规律"和"记忆训练噪声"的临界点停止训练。

```{admonition} 工程实现：框架中的早停
:class: tip

社团框架将早停封装为一行参数——`--patience 5` 表示验证损失连续 5 轮不下降即停止训练。详见{ref}`framework-experiments`。
```

---

### 4. 数据增强：免费的"新数据"

```{admonition} 核心洞察：更多数据帮助区分"本质"与"偶然"
:class: tip

想象你要学会"识别圆形"：
- 只看过1个圆 → 你可能以为"红色、半径5cm、在画面中央"才是圆的本质特征
- 看过100个圆（不同颜色、不同大小、不同位置）→ 你意识到"到中心点距离相等"才是本质，颜色和大小只是偶然特征

**过拟合的本质是"将偶然当成必然"**——把训练数据中的特定属性（如"这个位置的像素总是白色"）当成了类别的本质特征。

数据增强通过创造**人工变体**，让模型看到更多样的样本：
```

| 原始样本 | 增强变体 | 学到的规律 |
|---------|---------|-----------|
| 数字"3"在中央 | 数字"3"向左移2像素 | 位置不重要，形状才重要 |
| 数字"3"正常大小 | 数字"3"放大10% | 大小不重要，比例才重要 |
| 数字"3"竖直 | 数字"3"轻微旋转 | 方向不重要，拓扑结构才重要 |

```python
train_transform = transforms.Compose([
    transforms.RandomRotation(10),      # ±10度旋转
    transforms.RandomAffine(0, translate=(0.1, 0.1)),  # ±10%平移
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])
```

#### 为什么有效？

数据增强迫使模型学习**不变性**（invariance）——哪些特征在变换下保持不变。模型发现：
- 无论"3"在什么位置，它都是"3"
- 无论"3"多大，它都是"3"
- 无论"3"是否倾斜，它都是"3"

因此，模型必须抓住**本质特征**（形状、拓扑结构），而不能依赖**偶然特征**（位置、大小、角度）。这些偶然特征在不同样本间变化，无法作为可靠的分类依据。

---

## 如何选择正则化方法？

{doc}`fc-layer-basics`中我们讨论了全连接网络的参数量爆炸问题，{doc}`cnn-basics`中则看到CNN如何通过归纳偏置减少参数。但无论架构如何，**正则化都是必需的**——它是防止模型"死记硬背"的最后防线。

| 方法 | 使用场景 | 实现难度 |
|-----|---------|---------|
| 早停法 | **总是使用** | ⭐ 最简单 |
| L2正则化 | 几乎所有情况 | ⭐ 添加一个参数 |
| Dropout | 深层网络、过拟合严重时 | ⭐⭐ 需调整比例 |
| 数据增强 | 图像、语音等数据 | ⭐⭐ 需设计变换 |

**初学者路径**：
1. **第一步**：只用早停法（零成本，必做）
2. **第二步**：加上L2正则化（weight_decay=1e-4）
3. **第三步**：添加Dropout（0.3-0.5）
4. **第四步**：尝试数据增强

---

## 批量大小与学习率

### 批量大小的权衡

| 批量大小 | 优点 | 缺点 |
|---------|-----|------|
| 小（32-64） | 泛化性好，内存占用低 | 梯度噪声大，训练不稳定 |
| 大（256+） | 梯度准确，可并行加速 | 可能陷入尖锐局部最优，内存需求大 |

**直觉**：小批量就像用少量样本"试探"方向，虽然每次方向不太准，但不容易被困住；大批量像精准测量，但可能错过更好的路径。

这与{ref}`gradient-descent`中讨论的"噪声帮助逃离局部最优"原理一致——适度的梯度噪声反而有助于找到更好的解。

### 学习率调度

固定学习率不一定是最佳选择。回顾{ref}`gradient-descent`中的"下山"类比——刚开始你可能希望大步快走，接近谷底时需要小步微调。常见策略：

- **预热（Warmup）**：刚开始用较小学习率，逐渐增加——避免初期震荡（防止"一脚踩空"）
- **衰减（Decay）**：训练后期逐渐减小学习率——精细化调整（在谷底附近小心探索）
- **余弦退火（Cosine Annealing）**：周期性变化——帮助逃离局部最优（周期性地"震荡"寻找更好的山谷）

```python
# 余弦退火调度
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=100  # 100个epoch为一个周期
)

# 训练循环中
for epoch in range(num_epochs):
    train(...)  # 训练
    scheduler.step()  # 更新学习率
```

```{admonition} 工程实现：框架中的调度器
:class: tip

社团框架内置 5 种调度器（Step / Cosine / Plateau / Exponential / None），CLI 参数 `--scheduler cosine --scheduler-t-max 50` 即可启用。详见{ref}`framework-config`。
```

---

## 训练监控指标

有效的训练需要实时监控：

| 指标 | 正常趋势 | 异常信号 |
|-----|---------|---------|
| 训练损失 | 逐渐下降 | 不降或震荡 |
| 验证损失 | 先降后平 | 上升（过拟合） |
| 训练准确率 | 逐渐提高 | 停滞在低位（欠拟合） |
| 验证准确率 | 跟踪训练准确率 | 与训练差距大（过拟合） |

**关键观察**：
- 训练损失 $<$ 验证损失：正常现象
- 训练损失 $\ll$ 验证损失：严重过拟合，加强正则化
- 两者都高：欠拟合，增加模型容量或训练时间

回顾本节讨论的**过拟合与欠拟合**现象——这些监控指标就是诊断工具，帮助你在训练过程中实时判断模型状态。

```{admonition} 工程实现：框架中的自动监控
:class: tip

社团框架每轮自动记录损失、准确率、学习率、训练速度，训练结束后自动绘制 4 面板训练曲线图（`runs/expN/training_curves.png`），无需手动记录和绘图。详见{ref}`framework-experiments`。
```

---

## 总结：训练检查清单

开始训练前，确认以下事项：

| 检查项 | 建议 | 框架实现 | 参考章节 |
|-------|-----|---------|---------|
| 数据划分 | 训练/验证/测试 = 70%/15%/15% 或类似比例 | 数据集类内置划分 | 本节"数据划分"部分 |
| 损失函数 | 分类用CrossEntropy，回归用MSE | 模型自带 `get_criterion()` | {ref}`loss-functions` |
| 优化器 | Adam {cite}`kingma2014adam` 是默认首选，学习率0.001 | `--optimizer adamw` | {ref}`gradient-descent` |
| 正则化 | 至少使用早停法和L2正则化 | `--patience` + `--weight-decay` | 本节"正则化"部分 |
| 批量大小 | 从64或128开始，根据内存调整 | `--batch-size 64` | 本节"批量大小"部分 |
| 监控指标 | 同时关注训练和验证的表现 | 自动记录并绘制曲线 | 本节"训练监控指标"部分 |

本节将{ref}`gradient-descent`和{ref}`back-propagation`的理论转化为实践——从选择{ref}`loss-functions`、设计正则化策略，到监控训练过程。现在你已经掌握了让神经网络"学会"而不是"记住"的完整工具箱。

---

## 下一步

掌握了训练基础后，你可以在{doc}`../pytorch-practice/using-framework`中用社团框架将本节的理论转化为工程实践——配置 `--optimizer`、`--scheduler`、`--patience` 一行命令验证不同训练策略的效果。

在{doc}`../cnn-ablation-study/index`中，我们还将通过**消融实验**量化 CNN 各组件对性能的影响，用数据回答：
- 哪些训练技巧是"雪中送炭"，哪些是"锦上添花"？
- 不同正则化方法的实际贡献有多大？
- 如何通过控制变量法做出科学的设计决策？

从"知道怎么训练"进化到"知道怎么科学地验证训练方案"！

---

## 参考文献

```{bibliography}
:filter: docname in docnames
```
