查看全课大纲(第 4 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
训练三要素:数据、模型、目标函数
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
在上一节,我们了解了深度学习的基本概念和发展脉络。从本节开始,我们将进入实战环节。任何深度学习任务都离不开三个核心要素:数据、模型和目标函数。理解这三者如何协同工作,是构建和训练一切神经网络的基础。学完本节,你将能清晰地回答:一个典型的训练循环是如何组织的?数据如何输入模型?模型如何产生预测?我们又该如何衡量预测的好坏并指导模型改进?本节是后续所有具体网络结构(如卷积神经网络、Transformer)训练流程的先修知识,后续章节将在此基础上引入更复杂的模型组件。
💡 核心导读
- 数据:是学习的源泉,通常组织为
(batch, ...)形状的张量,包含特征X和(监督学习下的)标签y。 - 模型:是一个可学习的函数
f,将输入数据映射为预测输出(如logits)。其核心是包含需要优化参数(如权重W和偏置b)的层。 - 目标函数(损失函数):量化模型预测
ŷ与真实标签y之间的差距,如交叉熵损失。训练的目标就是最小化这个损失。 - 优化器:根据损失函数计算出的梯度,按照特定规则(如 SGD、Adam)更新模型参数,驱动模型向损失更小的方向“学习”。
- 训练循环:将数据输入模型、计算损失、反向传播梯度、优化器更新参数,这四个步骤的反复迭代。
🧭 前置知识
- 张量与形状:理解如
(batch_size, feature_dim)的张量表示。 - 矩阵乘法:知道
(m, n)矩阵与(n, p)矩阵相乘得到(m, p)矩阵。 - 链式法则:理解复合函数求导的基本思想(具体求导计算由 PyTorch 的
autograd自动完成)。 - softmax 与交叉熵:知道 softmax 将 logits 转换为概率分布,交叉熵衡量两个概率分布间的差异。
数据:学习的燃料
在深度学习中,数据通常被表示为张量。对于监督学习任务,每一份数据样本都包含两部分:
- 特征(Feature):模型的输入,例如一张图片的像素值、一段文本的词向量。我们用
X表示。 - 标签(Label):我们期望模型预测的目标值,例如图片的类别、文本的情感。我们用
y表示。
为了高效利用计算资源,我们通常将多个样本打包成一个批次(Batch) 进行并行处理。因此,特征张量 X 的形状通常是 (batch_size, ...),其中 ... 代表样本本身的维度。标签 y 的形状通常是 (batch_size,) 或 (batch_size, num_classes),具体取决于任务类型。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(0) # 固定随机种子,确保结果可复现
# 模拟一个简单的分类任务数据
# 假设每个样本有10个特征,共有4个样本(batch_size=4)
X = torch.randn(4, 10) # 形状:(batch_size=4, feature_dim=10)
print("特征数据 X.shape =", X.shape)
# 假设这是一个3分类任务,标签是类别索引(0, 1, 2)
y = torch.tensor([0, 2, 1, 0]) # 形状:(batch_size=4,)
print("标签数据 y =", y.tolist())特征数据 X.shape = torch.Size([4, 10])
标签数据 y = [0, 2, 1, 0]模型:可学习的函数
模型 f 是一个将输入特征 X 映射到预测输出(如 logits)的函数。它由一系列层(Layer) 组成,每层包含可学习的参数(Parameter),主要是权重(Weight)和偏置(Bias)。最简单的模型之一是多层感知机(MLP)。
一个两层的 MLP 前向传播过程如下:
- 线性变换1:
h = X @ W1^T + b1(@表示矩阵乘法) - 非线性激活:
a = ReLU(h)(引入非线性) - 线性变换2:
logits = a @ W2^T + b2
这里的 W1, b1, W2, b2 就是模型需要学习的参数。logits 是模型对每个样本在各个类别上的原始打分,尚未归一化成概率。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
torch.manual_seed(0)
X = torch.randn(4, 10) # 本块内重建同一份小批量数据(固定种子)
y = torch.tensor([0, 2, 1, 0])
torch.manual_seed(0)
class SimpleMLP(nn.Module):
def __init__(self, input_dim=10, hidden_dim=5, num_classes=3):
super().__init__()
# 第一层全连接层:参数 W1 (hidden_dim, input_dim), b1 (hidden_dim,)
self.fc1 = nn.Linear(input_dim, hidden_dim)
# 第二层全连接层(输出层):参数 W2 (num_classes, hidden_dim), b2 (num_classes,)
self.fc2 = nn.Linear(hidden_dim, num_classes)
# 激活函数(非线性变换)
self.relu = nn.ReLU()
def forward(self, x):
h = self.fc1(x) # 线性变换1
a = self.relu(h) # 非线性激活
logits = self.fc2(a) # 线性变换2,输出 logits
return logits
# 实例化模型
model = SimpleMLP()
print("模型结构:", model)
print("\n模型参数:")
for name, param in model.named_parameters():
print(f" {name}: shape={tuple(param.shape)}")
# 前向传播:将数据 X 输入模型,得到预测 logits
logits = model(X)
print(f"\n模型预测 logits.shape = {logits.shape}")
print("第一个样本的 logits =", [round(v, 4) for v in logits[0].tolist()])模型结构: SimpleMLP(
(fc1): Linear(in_features=10, out_features=5, bias=True)
(fc2): Linear(in_features=5, out_features=3, bias=True)
(relu): ReLU()
)
模型参数:
fc1.weight: shape=(5, 10)
fc1.bias: shape=(5,)
fc2.weight: shape=(3, 5)
fc2.bias: shape=(3,)
模型预测 logits.shape = torch.Size([4, 3])
第一个样本的 logits = [-0.3796, -0.1649, 0.1955]目标函数与优化:指引学习的方向
得到预测 logits 后,我们需要一个标准来衡量它的好坏,这就是目标函数或损失函数(Loss Function)。对于分类任务,最常用的是交叉熵损失(Cross-Entropy Loss)。PyTorch 的 nn.CrossEntropyLoss 将 softmax 归一化和交叉熵计算合并在一起,直接接受 logits 和类别索引标签 y。
损失值是一个标量,值越小表示模型预测得越好。我们的目标就是找到一组模型参数,使得在训练数据上的平均损失最小。
如何找到这组参数?我们使用梯度下降的思想。通过反向传播(Backpropagation) 算法(由 PyTorch 的 autograd 自动完成),可以计算出损失相对于每个模型参数的梯度 ∂loss/∂W。优化器(Optimizer) 则根据这些梯度来更新参数。
最基础的优化器是随机梯度下降(SGD),其更新规则为:W = W - lr * g,其中 lr 是学习率,g 是梯度。PyTorch 的 optim.SGD 实现了该算法(根据官方文档,其更新公式为 v_{t+1} = μ * v_t + g_{t+1}; p_{t+1} = p_t - lr * v_{t+1},其中 μ 是动量,p 是参数)。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class SimpleMLP(nn.Module):
"""两层全连接 + ReLU;本块内定义,保证块自包含。"""
def __init__(self, input_dim=10, hidden_dim=5, num_classes=3):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
self.relu = nn.ReLU()
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
torch.manual_seed(0)
X = torch.randn(4, 10) # 本块内重建同一份小批量数据(固定种子)
y = torch.tensor([0, 2, 1, 0])
torch.manual_seed(0)
# 重新初始化模型、优化器和损失函数,确保起点一致
model = SimpleMLP()
optimizer = optim.SGD(model.parameters(), lr=0.1) # 使用随机梯度下降,学习率0.1
loss_fn = nn.CrossEntropyLoss()
# 模拟一个极简的训练步骤(1个batch,更新1次)
print("=== 训练前 ===")
logits_before = model(X)
loss_before = loss_fn(logits_before, y)
print(f"损失 loss_before = {loss_before.item():.4f}")
# 训练步骤开始
optimizer.zero_grad() # 重要!清空上一轮计算的梯度
loss_before.backward() # 反向传播,计算梯度
optimizer.step() # 优化器根据梯度更新参数
print("\n=== 执行 optimizer.step() 后 ===")
logits_after = model(X)
loss_after = loss_fn(logits_after, y)
print(f"损失 loss_after = {loss_after.item():.4f}")
print(f"损失变化: {loss_before.item():.4f} -> {loss_after.item():.4f} (期望下降)")=== 训练前 ===
损失 loss_before = 1.2862
=== 执行 optimizer.step() 后 ===
损失 loss_after = 1.2295
损失变化: 1.2862 -> 1.2295 (期望下降)可以看到,执行一次 optimizer.step() 后,损失有所下降,说明参数更新朝着减少损失的方向进行。
完整的训练循环
将以上步骤组合起来,就构成了深度学习中最核心的训练循环。我们通常会在多个轮次(Epoch) 上遍历整个训练数据集,每个批次(Batch)执行一次参数更新。
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class SimpleMLP(nn.Module):
"""两层全连接 + ReLU;本块内定义,保证块自包含。"""
def __init__(self, input_dim=10, hidden_dim=5, num_classes=3):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
self.relu = nn.ReLU()
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
torch.manual_seed(0)
X = torch.randn(4, 10) # 本块内重建同一份小批量数据(固定种子)
y = torch.tensor([0, 2, 1, 0])
torch.manual_seed(0)
# 1. 准备组件
model = SimpleMLP()
optimizer = optim.SGD(model.parameters(), lr=0.1)
loss_fn = nn.CrossEntropyLoss()
# 2. 模拟一个极简的训练循环(2个epoch,每个epoch 1个batch)
print("开始训练循环...")
for epoch in range(2):
# 假设每个epoch我们都用同样的数据X, y(实际中会打乱和分批)
optimizer.zero_grad()
logits = model(X)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}: loss = {loss.item():.4f}")
# 3. 训练后评估(同样用X,实际应用应在未见过的数据上评估)
model.eval() # 将模型设置为评估模式(影响Dropout、BatchNorm等层的行为)
with torch.no_grad(): # 评估时不计算梯度,节省内存
final_logits = model(X)
final_loss = loss_fn(final_logits, y)
predictions = final_logits.argmax(dim=-1) # 取概率最大的类别作为预测
accuracy = (predictions == y).float().mean()
print(f"\n训练后评估:")
print(f" 最终损失 = {final_loss.item():.4f}")
print(f" 预测类别 = {predictions.tolist()}")
print(f" 真实类别 = {y.tolist()}")
print(f" 准确率 = {accuracy.item():.4f}")开始训练循环...
Epoch 1: loss = 1.2862
Epoch 2: loss = 1.2295
训练后评估:
最终损失 = 1.1918
预测类别 = [2, 2, 2, 2]
真实类别 = [0, 2, 1, 0]
准确率 = 0.2500在这个极简的例子中,由于数据随机、模型简单且训练步数极少,准确率不高。但这完整演示了数据、模型、目标函数和优化器如何在一个循环中协同工作。
📝 动手练一练
问题:将上面训练循环中的优化器从 SGD 改为 Adam,并保持学习率 lr=0.01,运行 2 个 epoch。观察损失下降的速度和最终准确率与 SGD 有何不同?(提示:Adam 是一种自适应学习率的优化器,通常收敛更快。)
👉 点击查看参考答案
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
class SimpleMLP(nn.Module):
"""两层全连接 + ReLU;本块内定义,保证块自包含。"""
def __init__(self, input_dim=10, hidden_dim=5, num_classes=3):
super().__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, num_classes)
self.relu = nn.ReLU()
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
torch.manual_seed(0)
# 数据
X = torch.randn(4, 10)
y = torch.tensor([0, 2, 1, 0])
# 使用 Adam 优化器(重置种子,确保与 SGD 示例从同一初始参数出发)
torch.manual_seed(0)
model = SimpleMLP()
optimizer = optim.Adam(model.parameters(), lr=0.01) # 改用 Adam,学习率通常设得更小
loss_fn = nn.CrossEntropyLoss()
print("使用 Adam 优化器训练...")
for epoch in range(2):
optimizer.zero_grad()
logits = model(X)
loss = loss_fn(logits, y)
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}: loss = {loss.item():.4f}")
# 评估
model.eval()
with torch.no_grad():
final_logits = model(X)
final_loss = loss_fn(final_logits, y)
predictions = final_logits.argmax(dim=-1)
accuracy = (predictions == y).float().mean()
print(f"\nAdam 训练后评估:")
print(f" 最终损失 = {final_loss.item():.4f}")
print(f" 预测类别 = {predictions.tolist()}")
print(f" 准确率 = {accuracy.item():.4f}")使用 Adam 优化器训练...
Epoch 1: loss = 1.2862
Epoch 2: loss = 1.2405
Adam 训练后评估:
最终损失 = 1.2045
预测类别 = [2, 2, 2, 2]
准确率 = 0.2500思路:把优化器换成 Adam、学习率降到 0.01,其余保持不变(数据与模型都用 torch.manual_seed(0) 重建,两种优化器从同一组初始参数出发)。对照两个印刷块:第 1 个 epoch 的损失完全相同(1.2862),第 2 个 epoch 时 Adam 为 1.2405、SGD 为 1.2295;训练后评估的最终损失 Adam 为 1.2045、SGD 为 1.1918。
也就是说,在这个只更新两次参数的极小例子里,Adam 反而略慢。原因不是 Adam 更差,而是它的自适应步长要靠累积梯度的一阶矩、二阶矩估计才能发挥作用,2 次更新远不足以体现优势;「Adam 通常收敛更快」是统计规律,并非每一步都更快。想观察到差异,需要把 epoch 增加到 20 以上、把 hidden_dim 或样本量调大——步数一多,Adam 对学习率尺度不敏感的优势才会显现。
常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
忘记 optimizer.zero_grad() | 损失不下降或波动异常;梯度值异常大。 | PyTorch 会累积梯度。如果不清零,本次计算的梯度会与历史梯度相加,导致更新方向错误。 | 在每个训练步骤(step())前,务必调用 optimizer.zero_grad()。 |
将 nn.CrossEntropyLoss 的输入误以为是概率 | 损失计算错误或出现数值不稳定(如 NaN)。 | nn.CrossEntropyLoss 设计为直接接收未归一化的 logits,它在内部高效地结合了 softmax 和交叉熵计算。 | 直接将模型输出的 logits 传给 nn.CrossEntropyLoss,不要手动做 softmax。 |
在训练循环内使用 torch.no_grad() | 模型参数无法更新,损失不变。 | torch.no_grad() 上下文管理器会禁用梯度计算,导致 loss.backward() 无法生成梯度。 | torch.no_grad() 只应用于不需要计算梯度的环节,如模型评估、推理。训练时必须在其外部。 |
混淆 model.train() 和 model.eval() | 在评估时得到与训练时不一致的随机结果(如使用 Dropout 时)。 | 某些层(如 nn.Dropout, nn.BatchNorm)在训练和评估时的行为不同。model.train() 和 model.eval() 用于切换这些模式。 | 训练前调用 model.train(),评估前调用 model.eval()。 |
本章小结
- 深度学习训练围绕数据(X, y)、模型(f)、目标函数(loss) 三要素展开。
- 模型是一个包含可学习参数(权重、偏置)的函数,它将输入数据映射为预测值(如
logits)。 - 目标函数(如交叉熵损失)量化预测与真实的差距,训练的目标是最小化它。
- 优化器(如 SGD、Adam)利用损失函数反向传播得到的梯度,按照特定算法更新模型参数。
- 标准的训练循环包括:前向传播计算损失、反向传播计算梯度、优化器更新参数,并循环多个轮次。
- 注意常见陷阱,如梯度清零、正确使用损失函数和模型模式切换。
📋 行动清单
- 运行本节第一个代码块,将
batch_size从 4 改为 2,观察X和y形状的变化。 - 在“完整的训练循环”代码块中,将
epoch循环次数增加到 5,观察损失和准确率的变化趋势。 - 尝试修改
SimpleMLP类的hidden_dim(例如改为 10 或 20),重新运行训练循环,感受模型容量对学习的影响。
延伸阅读
- PyTorch
optim.SGD官方文档(2024,对应 PyTorch 2.x) – https://pytorch.org/docs/stable/generated/torch.optim.SGD.html - PyTorch
nn.CrossEntropyLoss官方文档(2024) – https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
—— 小象教研组