查看全课大纲(第 2 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

表示学习与端到端训练

表示学习与端到端训练:深度学习如何自动抽取特征?

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

在上一节,我们了解了深度学习的宏观图景。本节我们将深入其核心机制:表示学习与端到端训练。学完本节,你将能回答:为什么深度学习能自动学习特征?端到端训练如何将特征提取和分类器优化统一在一个框架内?这解决了传统机器学习流程中的哪些痛点?

先修:第1章第1节《绪论:深度学习是什么》,了解深度学习的基本定义与应用。 后续:第2章第1节《前馈神经网络:从感知机到多层网络》,我们将具体实现一个端到端训练的网络。

💡 核心导读

  1. 表示学习:深度学习模型能够从原始数据(如图像像素、文本词向量)中自动学习出具有语义的、分层的特征表示,无需人工设计特征工程。
  2. 端到端训练:模型从输入到输出的所有组件(特征提取层、分类/回归层)被整合进一个统一的架构,并通过一个单一的损失函数进行联合优化。
  3. 数据自适应与任务驱动:学习到的特征是针对特定任务和数据集优化的,这使得模型性能通常优于固定的人工特征。
  4. 梯度反向传播:端到端训练得以实现的关键是反向传播算法,它允许损失函数的梯度从输出层一直流回输入层附近的特征提取层,从而指导其参数更新。

🧭 前置知识

  1. 矩阵乘法:理解 (batch, in_dim) 与 (in_dim, out_dim) 矩阵相乘得到 (batch, out_dim) 的形状规则。
  2. 链式法则:理解复合函数求导的基本思想(具体推导见站内数学基础课 math509)。
  3. softmax与交叉熵损失:知道 softmax 将 logits 转换为概率分布,交叉熵损失衡量预测概率与真实标签的差距。

从特征工程到表示学习

在传统机器学习流程中(如支持向量机、随机森林),特征工程是至关重要且耗时的一步。以图像分类为例,工程师需要设计 SIFT、HOG 等特征描述子来提取图像的边缘、纹理等信息,然后将这些特征输入分类器。

传统流程(特征工程 + 分类器):
原始图像 → [人工设计特征提取器] → 特征向量 → [分类器(如SVM)] → 类别标签

这个过程存在两个主要问题:

  1. 特征与任务脱节:特征提取是独立于分类任务设计的,可能包含大量对分类无用的信息,或缺失关键信息。
  2. 泛化能力有限:手工设计的特征在数据集或任务变更时,往往需要重新调整或设计,难以迁移。

深度学习将这两个步骤合二为一:

深度学习流程(端到端表示学习):
原始图像 → [可学习的特征提取层] → [可学习的分类层] → 类别标签
            ↑_________________________↓
                联合优化(通过损失函数)

模型中的每一层(尤其是前面的层)都可以看作是在学习一种数据表示。浅层学习低级特征(如边缘、角点),深层组合这些低级特征形成高级的、语义化的特征(如物体部件、整体形状)。这个过程就是表示学习。

端到端训练机制剖析

端到端训练的核心思想是:用一个统一的损失函数,通过梯度下降法,同时优化模型中的所有参数。这包括负责特征表示的参数和负责最终决策的参数。

一个简单的端到端网络

让我们用代码实现一个极简的两层神经网络,并观察其端到端训练过程。这个网络包含一个特征提取层(layer1)和一个分类层(layer2)。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)  # 固定随机种子,确保结果可复现

# 1. 构造模拟数据:4个样本,每个样本16维特征(例如,展平的小图像块)
batch_size, input_dim = 4, 16
X = torch.randn(batch_size, input_dim)  # 输入数据
y = torch.tensor([1, 0, 0, 0])          # 4个样本的标签(2分类)

print(f"输入数据形状 X.shape: {tuple(X.shape)}")
print(f"标签 y: {y.tolist()}\n")

# 2. 定义一个简单的两层网络
class SimpleNet(nn.Module):
    def __init__(self, input_dim=16, hidden_dim=8, num_classes=2):
        super().__init__()
        self.layer1 = nn.Linear(input_dim, hidden_dim)  # 特征提取层
        self.layer2 = nn.Linear(hidden_dim, num_classes) # 分类层

    def forward(self, x):
        features = self.layer1(x)      # 学习到的中间表示
        logits = self.layer2(features) # 分类分数(logits)
        return features, logits

model = SimpleNet()
print("模型结构:")
print(model)
print()

# 3. 前向传播:获得特征表示和分类logits
learned_features, logits = model(X)
print(f"学习到的表示形状 learned_features.shape: {tuple(learned_features.shape)}")
print(f"分类logits形状 logits.shape: {tuple(logits.shape)}")

# 4. 计算损失(这里使用交叉熵损失,它内部包含了softmax)
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, y)
print(f"当前损失 loss: {loss.item():.4f}")

# 5. 模拟一次反向传播(参数更新)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
optimizer.zero_grad()  # 清空历史梯度
loss.backward()        # 反向传播,计算梯度
optimizer.step()       # 根据梯度更新参数

print("一次反向传播完成,模型参数已更新。")
print("现在,layer1(特征层)和layer2(分类层)的权重都根据分类损失进行了调整。")
输入数据形状 X.shape: (4, 16)
标签 y: [1, 0, 0, 0]

模型结构:
SimpleNet(
  (layer1): Linear(in_features=16, out_features=8, bias=True)
  (layer2): Linear(in_features=8, out_features=2, bias=True)
)

学习到的表示形状 learned_features.shape: (4, 8)
分类logits形状 logits.shape: (4, 2)
当前损失 loss: 0.6349
一次反向传播完成,模型参数已更新。
现在,layer1(特征层)和layer2(分类层)的权重都根据分类损失进行了调整。

关键观察:

  • layer1 输出的 learned_features 形状为 (4, 8),这就是模型从原始16维数据中学到的8维新表示。
  • 损失函数 loss 是根据最终的分类logits计算的。
  • 调用 loss.backward() 后,梯度会从 loss 出发,通过 layer2 一直传播到 layer1。因此,layer1 的权重更新方向是由最终的分类任务所指导的。这就是“任务驱动”的特征学习。

表示容量:结构决定”能表示什么”

模型能学多复杂的特征,取决于它的结构(层数、每层宽度)。前提是网络里有非线性激活。 在有 ReLU 这类激活的网络里,把隐藏层从 4 维加宽到 64 维,能容纳的分段线性区域更多,才谈得上拟合更复杂的边界;如果只有线性层堆叠(没有激活),加宽加深深都等价于一个线性映射,边界不会变复杂。

但要注意一个常见误解:初始损失不能用来判断容量。刚初始化时,无论隐藏层多宽,二分类的交叉熵损失都在 ln⁡2≈0.693\ln 2 \approx 0.693 附近——因为此时输出接近随机。容量体现在”训练之后能拟合什么”,而不是”一开始损失多少”。判断容量要靠训练后的拟合能力,而不是初始数值。

为什么端到端训练有效?损失函数的作用

端到端训练有效的关键在于损失函数提供了统一的优化目标。对于分类任务,交叉熵损失比均方误差(MSE)更常用,因为它能提供更清晰的梯度信号。

import torch
import torch.nn as nn
import torch.nn.functional as F

torch.manual_seed(0)

# 模拟数据
X = torch.randn(4, 10)
# 假设是3分类任务,生成随机标签
y_indices = torch.randint(0, 3, (4,))
y_one_hot = F.one_hot(y_indices, num_classes=3).float()

print("对比两种损失函数在简单网络上的训练效果(模拟3步):")

# 使用 MSE 损失 (需要one-hot标签)
model_mse = nn.Sequential(nn.Linear(10, 5), nn.Linear(5, 3))
optimizer_mse = torch.optim.SGD(model_mse.parameters(), lr=0.1)
criterion_mse = nn.MSELoss()

print("\n使用MSE损失(标签为one-hot):")
for step in range(3):
    optimizer_mse.zero_grad()
    out = model_mse(X)
    loss = criterion_mse(out, y_one_hot)
    loss.backward()
    optimizer_mse.step()
    print(f"Step {step+1}: loss = {loss.item():.4f}")

# 使用 CrossEntropy 损失 (需要类别索引)
model_ce = nn.Sequential(nn.Linear(10, 5), nn.Linear(5, 3))
optimizer_ce = torch.optim.SGD(model_ce.parameters(), lr=0.1)
criterion_ce = nn.CrossEntropyLoss()

print("\n使用CrossEntropy损失(标签为类别索引):")
for step in range(3):
    optimizer_ce.zero_grad()
    out = model_ce(X)
    loss = criterion_ce(out, y_indices)
    loss.backward()
    optimizer_ce.step()
    print(f"Step {step+1}: loss = {loss.item():.4f}")

print("\n结论:分类任务上,CrossEntropy 通常比 MSE 给出更直接、更稳定的梯度信号。")
对比两种损失函数在简单网络上的训练效果(模拟3步):

使用MSE损失(标签为one-hot):
Step 1: loss = 0.6344
Step 2: loss = 0.4650
Step 3: loss = 0.3528

使用CrossEntropy损失(标签为类别索引):
Step 1: loss = 0.9960
Step 2: loss = 0.9206
Step 3: loss = 0.8585

结论:分类任务上,CrossEntropy 通常比 MSE 给出更直接、更稳定的梯度信号。

📝 动手练一练

问题:修改第一个代码示例中的 SimpleNet,增加一个隐藏层(例如,16 -> 8 -> 4 -> 2),并观察学习到的中间表示(第一个隐藏层的输出)在训练几步前后的变化。你可以计算其范数或统计值,感受特征是如何被“塑造”的。

👉 点击查看参考答案
import torch
import torch.nn as nn

torch.manual_seed(0)

# 构造数据
X = torch.randn(4, 16)
y = torch.tensor([1, 0, 0, 0])

# 定义三层网络
class DeeperNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(16, 8)  # 第一特征层
        self.fc2 = nn.Linear(8, 4)   # 第二特征层
        self.fc3 = nn.Linear(4, 2)   # 分类层

    def forward(self, x):
        feat1 = self.fc1(x)   # 第一个学习到的表示
        feat2 = self.fc2(feat1)
        logits = self.fc3(feat2)
        return feat1, logits

model = DeeperNet()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

# 训练前,查看第一个特征表示的统计信息
with torch.no_grad():
    feat1_init, _ = model(X)
    init_mean = feat1_init.mean().item()
    init_std = feat1_init.std().item()

print(f"训练前 - 第一层特征表示: mean={init_mean:.4f}, std={init_std:.4f}")

# 训练3步
for step in range(3):
    optimizer.zero_grad()
    feat1, logits = model(X)
    loss = criterion(logits, y)
    loss.backward()
    optimizer.step()
    print(f"Step {step+1}: loss={loss.item():.4f}, feat1 mean={feat1.mean().item():.4f}, std={feat1.std().item():.4f}")

print("\n观察:损失下降,第一层特征的均值/标准差也在变 —— 表示被任务重塑。")
训练前 - 第一层特征表示: mean=-0.0372, std=0.6635
Step 1: loss=0.5829, feat1 mean=-0.0372, std=0.6635
Step 2: loss=0.5757, feat1 mean=-0.0379, std=0.6642
Step 3: loss=0.5689, feat1 mean=-0.0385, std=0.6652

观察:损失下降,第一层特征的均值/标准差也在变 —— 表示被任务重塑。

思路:通过增加网络深度,我们创造了更复杂的特征变换路径。监控浅层特征的统计量(如均值、标准差)可以直观感受到,即使这些层不直接产生最终输出,它们的参数也会在反向传播的梯度指导下被调整,从而学习到对最终任务更有用的特征表示。

常见错误

错误理解或写法现象或报错原因正确做法
误以为特征层无需梯度特征层参数在训练中不更新,模型性能差。未将特征层参数传递给优化器,或错误地设置了 requires_grad=False。确保所有需要学习的层(包括特征提取层)都在优化器的参数列表中 (optimizer = torch.optim.XXX(model.parameters()))。
损失函数与标签格式不匹配使用 nn.CrossEntropyLoss 时传入 one-hot 标签,或使用 nn.MSELoss 时传入类别索引。CrossEntropyLoss 期望标签是类别索引(LongTensor),而 MSELoss 期望是与预测形状相同的浮点张量。仔细阅读损失函数的文档。分类任务通常使用 CrossEntropyLoss 并传入 (N,) 形状的类别索引标签。
前向传播后忘记清零梯度损失震荡或不收敛,梯度值异常大。loss.backward() 会将梯度累加到参数的 .grad 属性中。如果不清零,每次迭代的梯度会叠加。在每次参数更新前调用 optimizer.zero_grad()。

本章小结

  1. 表示学习是深度学习的核心能力,它使模型能够从原始数据中自动学习分层、语义化的特征,替代了繁琐且依赖经验的特征工程。
  2. 端到端训练通过一个统一的损失函数,将特征学习和任务求解(如分类)整合进一个模型,并利用反向传播算法联合优化所有参数。
  3. 这种范式使得学习到的特征是数据自适应和任务驱动的,通常能获得比固定人工特征更好的性能。
  4. 选择合适的损失函数(如分类用交叉熵)对于提供有效的梯度信号、引导特征学习至关重要。
  5. 梯度通过反向传播从损失函数流回网络各层,确保了即使是网络最前端的特征提取层,其更新方向也由最终的任务目标所决定。

📋 行动清单

  • 运行本节第一个代码块,尝试修改 hidden_dim(如改为 4 或 32),观察 learned_features 形状的变化,并思考其表示能力有何不同。
  • 在“动手练一练”的代码中,增加打印 feat1 的前两个样本的值,直观感受特征向量在训练前后的具体数值变化。
  • 查阅 PyTorch 文档,了解 nn.CrossEntropyLoss 和 nn.MSELoss 输入输出的具体形状要求。

延伸阅读

本段 PyTorch 官方文档/教程条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。

  1. PyTorch 官方文档:torch.nn.CrossEntropyLoss — https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
  2. PyTorch 官方文档:torch.nn.MSELoss — https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html
  3. Goodfellow, I., Bengio, Y. & Courville, A. Deep Learning(2016,MIT Press)第 1 章引言、第 6 章深度前馈网络(尤其 6.2 节 Gradient-Based Learning)—— 官方在线版:https://www.deeplearningbook.org/ —— 对表示学习与端到端训练的经典论述。

—— 小象教研组