查看全课大纲(第 5 / 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

学完本节,你将能回答:一个最简单的神经元(感知机)是如何工作的?为什么单个感知机无法解决“异或”问题?通过叠加神经元形成多层感知机(MLP)又如何解决了这个问题?我们将从数学公式、张量形状变化到PyTorch代码实现,完整走通神经网络最基础的前向传播与梯度下降学习过程。本节是理解后续卷积网络和Transformer的基石,先修知识为矩阵乘法和偏导数,后续将接第 2.2 节《反向传播:手推一个两层网络》。

💡 核心导读

  1. 感知机模型:模仿生物神经元,对输入加权求和后通过激活函数输出,是神经网络的基本单元。
  2. 线性可分性:单个感知机构成的决策边界是一条直线(或超平面),只能解决线性可分问题(如与、或),无法解决非线性可分问题(如异或)。
  3. 多层感知机(MLP):通过堆叠多个感知机层,并引入非线性激活函数,网络可以学习复杂的非线性决策边界,从而解决异或等难题。
  4. 前向传播:数据从输入层,经过隐藏层加权求和与激活,最终到达输出层的过程。我们用矩阵乘法清晰地描述形状变化。
  5. 梯度下降学习:通过计算损失函数对网络参数的梯度,并沿负梯度方向更新参数,使网络输出逼近真实值。

🧭 前置知识

  1. 矩阵乘法:若 AA 形状为 (m,n)(m, n),BB 形状为 (n,p)(n, p),则 C=ABC = AB 形状为 (m,p)(m, p)。计算规则为 Cij=∑k=1nAikBkjC_{ij} = \sum_{k=1}^{n} A_{ik} B_{kj}。
  2. 偏导数与链式法则:理解多元函数对某个变量的偏导,以及复合函数求导的链式法则(具体推导见站内数学基础课 math509)。
  3. Sigmoid函数:σ(x)=11+e−x\sigma(x) = \frac{1}{1 + e^{-x}},将输入映射到(0,1)区间,早期常用作激活函数。

感知机:一个最简单的神经元

感知机(Perceptron)由Frank Rosenblatt于1957年提出,是神经网络的原型。其数学模型如下:

给定输入向量 x=[x1,x2,...,xd]T\mathbf{x} = [x_1, x_2, ..., x_d]^T,权重向量 w=[w1,w2,...,wd]T\mathbf{w} = [w_1, w_2, ..., w_d]^T 和偏置 bb,感知机先计算加权和 z=wTx+b=∑i=1dwixi+bz = \mathbf{w}^T \mathbf{x} + b = \sum_{i=1}^{d} w_i x_i + b,然后通过一个激活函数 f(⋅)f(\cdot) 产生输出 a=f(z)a = f(z)。

早期感知机使用阶跃函数作为激活函数: f(z)={1if z≥00otherwisef(z) = \begin{cases} 1 & \text{if } z \geq 0 \\ 0 & \text{otherwise} \end{cases} 这使得感知机成为一个二分类器。

在代码中,我们可以用矩阵形式高效处理一个批次的数据。假设批次大小 batch_size=2,输入维度 input_dim=3,输出维度 output_dim=1(二分类)。

import torch
torch.manual_seed(0)

batch_size = 2
input_dim = 3
output_dim = 1

# 构造一个批次的数据,形状 (batch_size, input_dim)
X = torch.tensor([[0.5, -1.2, 0.8],
                  [-0.3, 0.7, 1.1]])
# 权重和偏置
W = torch.tensor([[0.4], [0.9], [-0.6]])  # 形状 (input_dim, output_dim)
b = torch.tensor([0.1])  # 形状 (output_dim,)

# 前向传播: Z = X @ W + b
Z = X @ W + b  # @ 表示矩阵乘法,广播机制将 b 加到每一行
print("加权和 Z (shape: {}):".format(tuple(Z.shape)))
print([[round(v.item(), 4)] for v in Z])

# 使用阶跃函数激活
A = (Z >= 0).float()  # 将布尔值转换为浮点数 (1.0 或 0.0)
print("\n激活输出 A (shape: {}):".format(tuple(A.shape)))
print([[round(v.item(), 4)] for v in A])
加权和 Z (shape: (2, 1)):
[[-1.26], [-0.05]]

激活输出 A (shape: (2, 1)):
[[0.0], [0.0]]

形状变化解读:

  • 输入 X: (batch_size=2, input_dim=3)
  • 权重 W: (input_dim=3, output_dim=1)
  • 矩阵乘法 X @ W: (2,3) @ (3,1) -> (2,1),符合矩阵乘法规则。
  • 加偏置 b (广播): (2,1) + (1,) -> (2,1)
  • 输出 A: (2,1),每个样本一个预测类别(0或1)。

线性可分性与异或问题

单个感知机的决策函数为 f(wTx+b)f(\mathbf{w}^T \mathbf{x} + b)。当 ff 为阶跃函数时,决策边界是 wTx+b=0\mathbf{w}^T \mathbf{x} + b = 0,这是一个线性方程(在二维空间是一条直线,三维是一个平面,高维是一个超平面)。因此,单个感知机只能对线性可分的数据进行分类。

经典的反例是异或(XOR)问题。异或运算的真值表如下:

x1x_1x2x_2y=x1⊕x2y = x_1 \oplus x_2
000
011
101
110

将其画在二维平面上,点(0,0)和(1,1)属于类别0,点(0,1)和(1,0)属于类别1。你无法用一条直线将这两类点完美分开。因此,单个感知机无法解决异或问题。

import torch
torch.manual_seed(0)

# 异或问题的输入和输出
X_xor = torch.tensor([[0., 0.],
                      [0., 1.],
                      [1., 0.],
                      [1., 1.]])
y_xor = torch.tensor([0., 1., 1., 0.])

# 尝试用单个感知机(线性模型)拟合
# 模型: y_pred = w1*x1 + w2*x2 + b
W_single = torch.tensor([[1.0], [1.0]], requires_grad=True)
b_single = torch.tensor([-0.5], requires_grad=True)

# 前向传播
predictions = X_xor @ W_single + b_single
print("单个感知机对异或输入的预测(加权和):")
print([[round(v.item(), 4)] for v in predictions])

# 应用阶跃函数
pred_class = (predictions >= 0).float().squeeze()
print("\n预测类别 (阶跃函数后):", [int(c) for c in pred_class.tolist()])
print("真实类别:", [int(c) for c in y_xor.tolist()])
print("是否匹配?", torch.all(pred_class == y_xor).item())
单个感知机对异或输入的预测(加权和):
[[-0.5], [0.5], [0.5], [1.5]]

预测类别 (阶跃函数后): [0, 1, 1, 1]
真实类别: [0, 1, 1, 0]
是否匹配? False

可以看到,对于输入(1,1),感知机错误地预测为1。无论你如何调整权重 w\mathbf{w} 和偏置 bb,一条直线都无法同时将(0,0)和(1,1)分在一边,将(0,1)和(1,0)分在另一边。

多层感知机:引入非线性与隐藏层

解决非线性可分问题的关键在于引入隐藏层和非线性激活函数。这就是多层感知机(Multilayer Perceptron, MLP),也称为全连接前馈网络。

一个典型的两层MLP(一个隐藏层)结构如下:

  1. 输入层 -> 隐藏层:z(1)=XW(1)+b(1)\mathbf{z}^{(1)} = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}, a(1)=f(z(1))\mathbf{a}^{(1)} = f(\mathbf{z}^{(1)})
  2. 隐藏层 -> 输出层:z(2)=a(1)W(2)+b(2)\mathbf{z}^{(2)} = \mathbf{a}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}, a(2)=g(z(2))\mathbf{a}^{(2)} = g(\mathbf{z}^{(2)})

其中 ff 和 gg 是激活函数(如Sigmoid、ReLU)。关键点:隐藏层的激活函数 ff 必须是非线性的。如果 ff 是线性的,那么整个网络可以等价为一个线性变换,退化成单个感知机。

为什么MLP能解决异或?我们可以将问题分解:

  1. 第一层(两个神经元)可以学习两条不同的直线,将空间划分成几个区域。
  2. 第二层(一个神经元)学习如何组合这两个区域的判断,形成非线性的决策边界(例如,一个由两条直线围成的区域)。

下面我们用PyTorch实现一个能解决异或问题的两层MLP。为了清晰,我们手动定义参数并执行前向传播。

import torch
import torch.nn.functional as F
torch.manual_seed(0)

# 网络结构: 输入2维 -> 隐藏层2维 -> 输出1维
# 参数初始化(这些值是手动设计来演示解决XOR的,实际中通过学习得到)
W1 = torch.tensor([[ 5.0, -5.0],   # 第一个隐藏神经元权重 [w11, w12]
                   [-5.0,  5.0]])  # 第二个隐藏神经元权重 [w21, w22]
b1 = torch.tensor([-2.0, -2.0])    # 隐藏层偏置
W2 = torch.tensor([[10.0],        # 输出层权重
                   [10.0]])
b2 = torch.tensor([-5.0])          # 输出层偏置

# 异或输入数据 (batch_size=4, input_dim=2)
X = torch.tensor([[0., 0.],
                  [0., 1.],
                  [1., 0.],
                  [1., 1.]])

# 前向传播
# 第一层: Z1 = X @ W1.T + b1 (注意W1定义时行是神经元,需要转置)
Z1 = X @ W1.T + b1  # 形状 (4, 2)
print("第一层加权和 Z1:")
print([[round(v, 4) for v in row] for row in Z1.tolist()])

# 使用Sigmoid激活函数 (早期常用)
A1 = torch.sigmoid(Z1)
print("\n第一层激活输出 A1 (Sigmoid):")
print([[round(v, 4) for v in row] for row in A1.tolist()])

# 第二层: Z2 = A1 @ W2 + b2
Z2 = A1 @ W2 + b2  # 形状 (4, 1)
print("\n第二层加权和 Z2:")
print([[round(v.item(), 4)] for v in Z2])

# 输出层也使用Sigmoid,将结果映射到(0,1),>0.5视为类别1
A2 = torch.sigmoid(Z2)
print("\n最终输出 A2 (Sigmoid):")
print([[round(v.item(), 4)] for v in A2])

# 预测类别
pred_class = (A2 > 0.5).float().squeeze()
print("\n预测类别:", [int(c) for c in pred_class.tolist()])
print("真实类别:", [0, 1, 1, 0])
print("是否匹配?", torch.all(pred_class == torch.tensor([0., 1., 1., 0.])).item())
第一层加权和 Z1:
[[-2.0, -2.0], [-7.0, 3.0], [3.0, -7.0], [-2.0, -2.0]]

第一层激活输出 A1 (Sigmoid):
[[0.1192, 0.1192], [0.0009, 0.9526], [0.9526, 0.0009], [0.1192, 0.1192]]

第二层加权和 Z2:
[[-2.6159], [4.5349], [4.5349], [-2.6159]]

最终输出 A2 (Sigmoid):
[[0.0681], [0.9894], [0.9894], [0.0681]]

预测类别: [0, 1, 1, 0]
真实类别: [0, 1, 1, 0]
是否匹配? True

机制解读:

  • 第一层的两个神经元分别学习到两条决策边界(直线)。观察 A1:
    • 对于输入(0,0)和(1,1),两个神经元的激活值都很低(~0.119)。
    • 对于输入(0,1),第一个神经元激活值低(~0.001),第二个高(~0.953)。
    • 对于输入(1,0),第一个神经元激活值高(~0.953),第二个低(~0.001)。
  • 第二层学习到的是:两个激活值都低时(~0.119,对应(0,0)或(1,1)),加权和约 10×0.119 + 10×0.119 - 5 ≈ -2.6,输出小于 0.5;一个高一个低时(对应(0,1)或(1,0)),加权和约 10×0.001 + 10×0.953 - 5 ≈ 4.5,输出大于 0.5。这正好是异或的逻辑——第一层把两个「同类」角点压到同一个隐藏状态,第二层只需做一个线性判断。

网络的学习:梯度下降

我们手动设置了网络参数来解决XOR。在实际中,参数 W(1),b(1),W(2),b(2)\mathbf{W}^{(1)}, \mathbf{b}^{(1)}, \mathbf{W}^{(2)}, \mathbf{b}^{(2)} 需要从数据中学习。学习的目标是找到一组参数,使得网络输出 a(2)\mathbf{a}^{(2)} 与真实标签 y\mathbf{y} 的差距(损失)最小。

最常用的方法是梯度下降(Gradient Descent)。其核心步骤是:

  1. 前向传播:计算当前参数下的网络输出和损失 LL。
  2. 反向传播:利用链式法则,计算损失 LL 对每个参数的梯度 ∂L∂W,∂L∂b\frac{\partial L}{\partial \mathbf{W}}, \frac{\partial L}{\partial \mathbf{b}}。
  3. 参数更新:沿着负梯度方向更新参数:W←W−η∂L∂W\mathbf{W} \leftarrow \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}},其中 η\eta 是学习率。

PyTorch的自动微分(autograd)引擎为我们完成了反向传播中复杂的梯度计算。我们只需在定义张量时设置 requires_grad=True,并在计算损失后调用 .backward(),梯度就会自动填充到参数的 .grad 属性中。

下面我们用一个极简的线性回归例子演示梯度下降的完整步骤。

import torch
torch.manual_seed(0)

# 1. 定义超简单的网络和数据
# 网络: y_pred = w * x + b
w = torch.tensor([0.5], requires_grad=True)  # 标记需要计算梯度
b = torch.tensor([-0.2], requires_grad=True)
# 数据: 真实关系是 y = 2*x
x_data = torch.tensor([1.0, 2.0, 3.0])
y_data = torch.tensor([2.0, 4.0, 6.0])

# 2. 前向传播和损失计算 (MSE)
y_pred = w * x_data + b  # 广播
loss = ((y_pred - y_data) ** 2).mean()  # 均方误差损失
print(f"初始参数: w={w.item():.4f}, b={b.item():.4f}")
print(f"初始预测: {[round(v.item(),4) for v in y_pred]}")
print(f"初始损失: {loss.item():.4f}")

# 3. 反向传播 (自动计算梯度)
loss.backward()
print(f"\n梯度: ∂L/∂w = {w.grad.item():.4f}, ∂L/∂b = {b.grad.item():.4f}")

# 4. 执行一步梯度下降更新 (学习率 lr=0.1)
lr = 0.1
with torch.no_grad():  # 关闭梯度跟踪,防止更新操作被记录到计算图中
    w -= lr * w.grad
    b -= lr * b.grad
    # 梯度清零,为下一次迭代准备
    w.grad.zero_()
    b.grad.zero_()

print(f"\n更新后参数: w={w.item():.4f}, b={b.item():.4f}")

# 5. 验证更新后的损失
y_pred_new = w * x_data + b
loss_new = ((y_pred_new - y_data) ** 2).mean()
print(f"更新后预测: {[round(v.item(),4) for v in y_pred_new]}")
print(f"更新后损失: {loss_new.item():.4f} (比初始损失 {loss.item():.4f} 小)")
初始参数: w=0.5000, b=-0.2000
初始预测: [0.3, 0.8, 1.3]
初始损失: 11.7400

梯度: ∂L/∂w = -14.8000, ∂L/∂b = -6.4000

更新后参数: w=1.9800, b=0.4400
更新后预测: [2.42, 4.4, 6.38]
更新后损失: 0.1603 (比初始损失 11.7400 小)

注意:这个特例里梯度很大(∂L/∂w = -14.8),初始点虽然离最优解 w=2 还有距离,lr=0.1 一步就把参数推到 w=1.98,损失从 11.74 降到 0.16。看起来「一步到位」,但这只是因为问题本身是凸的、只有 3 个样本、梯度方向恰好指向最优解;换成非凸的 MLP 或不同数据,同样的学习率就可能震荡甚至发散。学习率没有普适值,要结合实际损失曲线来判断。

对于MLP,流程完全一样,只是网络更复杂,计算图更大。PyTorch的 autograd 会处理所有细节。

📝 动手练一练

问题:将上面梯度下降示例中的学习率 lr 改为 0.01,重新运行代码,观察参数更新后的损失变化。与 lr=0.1 相比,损失下降是更快还是更慢了?为什么?

👉 点击查看参考答案

思路:学习率控制着参数更新的步长。学习率太小,更新步伐小,收敛慢;学习率太大,可能跳过最优点,甚至导致损失震荡或发散。修改学习率后重新运行即可观察。

import torch
torch.manual_seed(0)

w = torch.tensor([0.5], requires_grad=True)
b = torch.tensor([-0.2], requires_grad=True)
x_data = torch.tensor([1.0, 2.0, 3.0])
y_data = torch.tensor([2.0, 4.0, 6.0])

# 前向与损失
y_pred = w * x_data + b
loss_init = ((y_pred - y_data) ** 2).mean()
print(f"初始损失: {loss_init.item():.4f}")

# 反向传播
loss_init.backward()

# 用 lr=0.01 更新
lr_small = 0.01
with torch.no_grad():
    w -= lr_small * w.grad
    b -= lr_small * b.grad
    w.grad.zero_()
    b.grad.zero_()

# 计算新损失
y_pred_new = w * x_data + b
loss_new = ((y_pred_new - y_data) ** 2).mean()
print(f"学习率 {lr_small} 更新后损失: {loss_new.item():.4f}")
print(f"损失变化: {loss_new.item() - loss_init.item():.4f} (负数表示下降)")
初始损失: 11.7400
学习率 0.01 更新后损失: 9.2842
损失变化: -2.4558 (负数表示下降)

分析:lr=0.01 时损失从 11.7400 降到 9.2842,只下降 2.4558;而 lr=0.1 时一步就从 11.7400 降到 0.1603,下降 11.5797。在这个特例里大学习率反而更快——因为梯度本身很大(∂L/∂w = -14.8),小步长要走很多轮才到最优点。

但「学习率越大越好」是有边界的:把本例的 lr 加到 0.3 以上,参数会冲过最优点 w=2,损失反而回升甚至来回震荡。所以正确结论不是「越大越好」,而是步长要与梯度量级匹配,并靠观察损失曲线来调。

常见错误

错误理解或写法现象或报错原因正确做法
误以为单个感知机可以解决任何二分类问题。在类似异或的数据上训练,准确率永远达不到100%。单个感知机的决策边界是线性的,只能解决线性可分问题。对于非线性问题,必须使用带隐藏层和非线性激活函数的多层感知机(MLP)。
在MLP的隐藏层使用线性激活函数。网络性能很差,深度增加没有带来增益,等价于单层网络。多个线性变换的复合仍是线性变换,网络无法获得非线性表达能力。隐藏层必须使用非线性激活函数,如ReLU、Sigmoid、Tanh。
忘记在梯度下降更新参数后清零梯度(.zero_())。梯度会不断累积,导致更新方向错误,训练不稳定或发散。PyTorch中,backward() 计算的梯度会累加到 .grad 属性中。在每次参数更新后,调用 optimizer.zero_grad() 或手动对每个参数的 .grad 属性调用 .zero_()。
在定义网络参数时未设置 requires_grad=True。调用 loss.backward() 后,参数的 .grad 属性为 None,无法更新。PyTorch默认不跟踪普通张量的梯度以节省内存。对需要被优化的参数(如权重、偏置),在创建时设置 requires_grad=True。

本章小结

  1. 感知机是神经网络的基本单元,进行加权求和与激活,但单个感知机只能产生线性决策边界。
  2. 异或(XOR)问题是典型的线性不可分问题,揭示了单层网络的局限性。
  3. 多层感知机(MLP) 通过引入一个或多个隐藏层以及非线性激活函数,获得了强大的非线性拟合能力,能够解决异或等复杂问题。
  4. 前向传播是数据通过网络层层变换的过程,可以用矩阵乘法清晰地描述其形状变化。
  5. 梯度下降是神经网络学习参数的核心算法,通过计算损失函数的梯度并沿负梯度方向更新参数,使网络逐步逼近最优解。
  6. 自动微分(autograd) 由PyTorch等框架实现,自动计算复杂计算图中所有参数的梯度,极大简化了反向传播的实现。

📋 行动清单

  • 在异或问题的MLP示例中,尝试将隐藏层的Sigmoid激活函数改为 torch.tanh(),观察输出值的变化,理解不同激活函数的影响。
  • 修改梯度下降示例,将学习率 lr 改为 0.001 并运行,然后改为 0.5 再运行,观察损失的变化情况,直观感受学习率对训练稳定性的影响。
  • 手动计算一个简单例子:输入 x=2.0, w=3.0, b=1.0,目标 y=10.0,使用均方误差损失。计算损失对 w 和 b 的梯度,验证与PyTorch autograd 的结果是否一致。

延伸阅读

本段 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.Linear:https://pytorch.org/docs/stable/generated/torch.nn.Linear.html —— 实现全连接层的标准模块。
  2. PyTorch 官方文档 Autograd mechanics:https://pytorch.org/docs/stable/autograd.html —— 自动微分的工作机制。
  3. The perceptron: a probabilistic model for information storage and organization in the brain(Rosenblatt, 1958, Psychological Review 65(6))—— 感知机的原始论文 - https://doi.org/10.1037/h0042519
  4. Learning representations by back-propagating errors(Rumelhart, Hinton & Williams, 1986, Nature 323)—— 反向传播算法的重要论文 - https://doi.org/10.1038/323533a0

—— 小象教研组