Skip to content

第 2 章 1980—1986:反向传播、多层感知机与 Hopfield 网络 ​

学习目标 ​

  • 理解多层感知机(MLP)为什么能解决 XOR
  • 亲手用链式法则实现反向传播(1986)
  • 用 PyTorch 自动微分训练 MLP,掌握 DataLoader 与训练 / 验证 / 测试流程
  • 实现 Hopfield 网络(1982),理解内容寻址记忆

2.1 时代背景:连接主义的复兴 ​

第一次寒冬里,神经网络研究几乎停滞。但两股力量在暗处积蓄:

1982 年,物理学家 John Hopfield 提出 Hopfield 网络——一个能存储和回忆模式的循环网络,用物理学家的能量函数证明它一定收敛,让「神经网络能记忆」重新得到学术界的正视。

1986 年,David Rumelhart、Geoffrey Hinton 和 Ronald Williams 发表《Learning representations by back-propagating errors》,把反向传播(backpropagation)系统化。配合 1980 年福岛邦彦提出的 Neocognitron(卷积神经网络的雏形),连接主义(connectionism)正式复兴。

2.2 多层感知机:给感知机加一层 ​

感知机是「输入 → 输出」的单层结构。多层感知机(MLP)在中间加一个或多个隐藏层,每个隐藏层先做线性变换,再经过非线性激活函数:

输入 x → 线性(w₁x+b₁) → 激活 σ → 线性(w₂h+b₂) → 输出

关键是中间的非线性激活函数。没有它,两层线性变换叠加仍是线性变换,MLP 就和单层一样弱。有了 sigmoid / tanh / ReLU,MLP 才能表达 XOR 这样的非线性函数。

2.2.1 手工反向传播:XOR 的救赎 ​

反向传播的本质是链式法则:把输出误差沿网络逐层往回传,算出每个权重对误差的「责任」,然后按梯度下降更新。

对 2-2-1 网络(sigmoid 激活 + MSE 损失),更新公式为:

输出层误差  δ₂ = (ŷ - y) · σ'(z₂)
隐藏层误差  δ₁ = (δ₂·w₂ᵀ) · σ'(z₁)

完整代码见 examples/ch02_backprop.py,其中没有任何自动微分——每个梯度都是手写的:

python
def train_step(xb, yb, lr=0.5):
    global w1, b1, w2, b2
    h, out = forward(xb)
    d_out = (out - yb) * out * (1 - out)          # 输出层误差
    d_h = (d_out @ w2.T) * h * (1 - h)            # 隐藏层误差
    w2 -= lr * h.T @ d_out
    b2 -= lr * d_out.sum(0)
    w1 -= lr * xb.T @ d_h
    b1 -= lr * d_h.sum(0)

运行结果:

训练前 MSE = 0.2518
训练后 MSE = 0.0180
XOR 预测: [0, 1, 1, 0] (期望 [0, 1, 1, 0])

第 1 章判定「不可能」的 XOR,加了一层隐藏层后轻松学会。Minsky 的悲观结论被推翻,深度学习的第一块基石就此落下。

2.3 PyTorch 自动微分与 MLP 实战 ​

手写反向传播适合理解原理,但真实项目不会手写。PyTorch 的 自动微分(autograd) 会记录张量的运算图,调用 .backward() 后自动计算所有梯度。

训练一个 MLP 需要完整的数据管线:

  1. Dataset 提供样本,DataLoader 分批(batch)加载;
  2. 划分训练集 / 验证集 / 测试集——验证集用来调超参,测试集只评估一次;
  3. 每个 batch:前向 → 算损失 → loss.backward() → optimizer.step() → 清梯度;
  4. 每轮结束在验证集上评估,决定是否早停、是否调整学习率。

examples/ch02_mlp.py 在 Fashion-MNIST(10 类服装图片,28×28 灰度)上训练三层 MLP:

python
class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(28 * 28, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 10),
        )

    def forward(self, x):
        return self.net(x)

运行结果:

epoch 1:loss = 0.5841,验证准确率 = 82.08%
epoch 2:loss = 0.3665,验证准确率 = 86.26%
epoch 3:loss = 0.2997,验证准确率 = 88.38%
测试准确率 = 87.57%

注意 loss.item() 取的是 Python 标量,loss.backward() 要求 loss 是标量——这是新手最常见的报错来源之一。

2.4 Hopfield 网络:会记忆的循环网络 ​

Hopfield 网络用 Hebbian 规则把二值图案存进权重矩阵:

W = (Σ pᵀp - n·I) / N

回忆时,把带噪的图案作为初始状态,反复执行 s ← sign(W·s) 直到稳定。稳定点就是网络「记起」的模式。因为能量函数单调下降,网络保证收敛——这就是它的理论魅力。

examples/ch02_hopfield.py 存入 T、L、X 三个 8×8 图案,然后把每个图案翻转 20% 的像素作为探针:

图案 T:翻转 20% 后回忆,与 T/L/X 的汉明距离 [0, 26, 23],回忆结果 → T(正确)
图案 L:翻转 20% 后回忆,与 T/L/X 的汉明距离 [26, 0, 25],回忆结果 → L(正确)
图案 X:翻转 20% 后回忆,与 T/L/X 的汉明距离 [23, 25, 0],回忆结果 → X(正确)

回忆结果与正确图案的汉明距离为 0,说明噪声被完全清除。这就是内容寻址记忆:不需要知道位置,给个大致的样子,就能把完整内容「勾」出来。Hopfield 网络的容量约为存储单元数的 14%(这里是 64 单元、3 图案,在容量内);存得太多会出现虚假吸引子。

2.5 三足鼎立:这条路上还有什么 ​

1980—1986 年还出现了另外两个重要的思想:1980 年 Fukushima 的 Neocognitron(卷积 + 池化的原型,第 3 章的远祖),以及 1985 年 Ackley、Hinton、Sejnowski 的 玻尔兹曼机(用随机神经元和统计力学训练)。它们与反向传播一起,构成了连接主义复兴的三块拼图。前者的故事在第 3 章继续。

完整代码 ​

本章用到的完整示例代码:

examples/ch02_backprop.py ​

py
"""第 2 章示例(1986):手工实现反向传播训练 XOR。

一个 2-2-1 网络,隐藏层用 sigmoid,输出层用 sigmoid。
不用任何自动微分库,亲手写出链式法则的两个误差项。

运行方式:
    uv run python examples/ch02_backprop.py
"""
import torch

torch.manual_seed(1)

X = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y = torch.tensor([[0.0], [1.0], [1.0], [0.0]])

w1 = torch.randn(2, 2) * 0.5
b1 = torch.zeros(2)
w2 = torch.randn(2, 1) * 0.5
b2 = torch.zeros(1)


def sigmoid(z):
    return 1 / (1 + torch.exp(-z))


def forward(x):
    h = sigmoid(x @ w1 + b1)
    return h, sigmoid(h @ w2 + b2)


def mse():
    _, out = forward(X)
    return ((out - y) ** 2).mean()


def train_step(xb, yb, lr=0.5):
    global w1, b1, w2, b2
    h, out = forward(xb)
    # 输出层误差:对 sigmoid + MSE 的链式法则
    d_out = (out - yb) * out * (1 - out)
    # 隐藏层误差:沿 w2 传回,再乘 sigmoid 导数
    d_h = (d_out @ w2.T) * h * (1 - h)
    w2 -= lr * h.T @ d_out
    b2 -= lr * d_out.sum(0)
    w1 -= lr * xb.T @ d_h
    b1 -= lr * d_h.sum(0)


print(f"训练前 MSE = {mse().item():.4f}")
for _ in range(4000):
    train_step(X, y)
print(f"训练后 MSE = {mse().item():.4f}")
_, out = forward(X)
print("XOR 预测:", [round(float(v)) for v in out.squeeze()], "(期望 [0, 1, 1, 0])")

examples/ch02_mlp.py ​

py
"""第 2 章示例(1986):用 PyTorch 自动微分训练多层感知机。

数据管线:Dataset/DataLoader 分批加载 Fashion-MNIST,
划分训练/验证/测试集,训练完成后报告测试准确率。

运行方式:
    uv run python examples/ch02_mlp.py
"""
import torch
from torch import nn
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms

torch.manual_seed(0)
device = "cuda" if torch.cuda.is_available() else "cpu"

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.2860,), (0.3530,)),
])
full_train = datasets.FashionMNIST("data", train=True, download=True, transform=transform)
test = datasets.FashionMNIST("data", train=False, download=True, transform=transform)
train_set, val_set = random_split(
    full_train, [55000, 5000], generator=torch.Generator().manual_seed(0)
)
train_loader = DataLoader(train_set, batch_size=256, shuffle=True)
val_loader = DataLoader(val_set, batch_size=1024)
test_loader = DataLoader(test, batch_size=1024)


class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Flatten(),
            nn.Linear(28 * 28, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 10),
        )

    def forward(self, x):
        return self.net(x)


model = MLP().to(device)
opt = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
scheduler = torch.optim.lr_scheduler.StepLR(opt, step_size=2, gamma=0.5)
criterion = nn.CrossEntropyLoss()


def evaluate(loader):
    model.eval()
    correct = total = 0
    with torch.no_grad():
        for xb, yb in loader:
            pred = model(xb.to(device)).argmax(1)
            correct += (pred == yb.to(device)).sum().item()
            total += len(yb)
    return correct / total


for epoch in range(3):
    model.train()
    total_loss = 0
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        opt.zero_grad()
        loss = criterion(model(xb), yb)
        loss.backward()
        opt.step()
        total_loss += loss.item() * len(xb)
    print(f"epoch {epoch + 1}:loss = {total_loss / len(train_set):.4f},"
          f"验证准确率 = {evaluate(val_loader) * 100:.2f}%")
    scheduler.step()

print(f"测试准确率 = {evaluate(test_loader) * 100:.2f}%")

examples/ch02_hopfield.py ​

py
"""第 2 章示例(1982):Hopfield 网络——内容寻址记忆。

把若干二值图案"烧录"到权重矩阵中;给一个被噪声污染的图案,
网络通过迭代更新逐步"回忆起"最接近的已存图案。

运行方式:
    uv run python examples/ch02_hopfield.py
"""
import torch

torch.manual_seed(0)

# 三个 8×8 图案:T、L、X(用 ±1 编码)
T = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
     0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
     0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
L = [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
     1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
     1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
X = [1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0,
     0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0,
     0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]
patterns = [torch.tensor(p, dtype=torch.float32).view(1, 64) * 2 - 1 for p in (T, L, X)]

# Hebbian 学习:W = (Σ pᵀp - n·I)/64
P = torch.cat(patterns)
W = (P.T @ P - 3 * torch.eye(64)) / 64


def recall(probe, max_iters=50):
    state = probe.clone()
    for _ in range(max_iters):
        new = torch.sign(state @ W)
        if torch.equal(new, state):
            break
        state = new
    return state


def hamming(a, b):
    return int((a != b).sum().item())


for name, p in zip("TLX", patterns):
    flip = torch.rand(1, 64) < 0.2          # 20% 像素翻转
    probe = torch.where(flip, -p, p)
    rec = recall(probe)
    dists = [hamming(rec, q) for q in patterns]
    best = "TLX"[dists.index(min(dists))]
    print(f"图案 {name}:翻转 20% 后回忆,与 T/L/X 的汉明距离 {dists},"
          f"回忆结果 → {best}({'正确' if best == name else '错误'})")

动手实践 ​

  1. 运行 examples/ch02_backprop.py,把隐藏层节点数从 2 改成 3 和 1,分别观察训练后 MSE,解释为什么 1 个隐藏节点学不会 XOR。
  2. 在 examples/ch02_mlp.py 中把 ReLU 换成 Sigmoid,训练 3 轮,对比 loss 与验证准确率,解释差异。
  3. 修改 Hopfield 的图案为字母 A、B、C(自行设计 8×8),验证网络能否正确回忆;然后存入 5 个图案,观察回忆正确率下降。
  4. 在 MLP 训练循环里故意不调用 optimizer.zero_grad(),运行后解释梯度累积现象。

常见错误 ​

错误写法现象原因
loss = criterion(...) 后不 zero_grad()loss 不降或乱跳梯度在累积,应在每个 step 前清空
loss.backward() 前 loss 不是标量RuntimeError: grad can be implicitly created only for scalar outputs对向量 loss 要先 .sum() 或 .mean()
tensor.item() 用在 requires_grad 张量上RuntimeError: Can't call numpy() on Tensor that requires grad先 detach() 或用 .item()(标量)
手写反向传播时漏乘激活导数不收敛链式法则漏项,σ'(z) 必须出现
Hopfield 用 0/1 编码图案回忆效果差用 ±1 编码更符合 Hebbian 更新与符号阈值
把测试集反复用来调参测试准确率虚高测试集只能评估一次,调参用验证集

章末练习 ​

基础

  1. 为什么隐藏层必须配非线性激活?两层纯线性层等价于什么?
  2. 写出 2-2-1 网络中隐藏层误差 δ₁ 的推导(链式法则)。
  3. 训练 / 验证 / 测试三个集合各自的用途是什么?

提高

  1. 用反向传播推导 3 层网络(2-4-2-1)的更新公式,并实现它训练 XOR。
  2. 在 Hopfield 网络中,为什么 W 要减去 n·I?试想如果图案彼此高度相似,会发生什么。
  3. 给 MLP 增加 dropout(第 7 章详细讲),在 Fashion-MNIST 上对比验证集准确率。

挑战

  1. 实现一个 4 层 MLP(784-512-256-64-10),在 Fashion-MNIST 上训练,画出 train/val 准确率曲线,观察过拟合出现的 epoch。
  2. 用 Hebbian 规则把 10 个随机图案存入 Hopfield 网络,统计回忆成功率与图案数量的关系。

章末自测 ​

  1. 反向传播的核心数学工具是?
    • A. 矩阵求逆
    • B. 链式法则
    • C. 傅里叶变换
    • D. 蒙特卡洛
  2. MLP 能解决 XOR 的关键是?
    • A. 更多训练数据
    • B. 隐藏层 + 非线性激活
    • C. 更大的学习率
    • D. 更快的 GPU
  3. PyTorch 中计算梯度的调用是?
    • A. loss.forward()
    • B. loss.backward()
    • C. optimizer.grad()
    • D. loss.compute()
  4. 每个训练 step 之前必须?
    • A. optimizer.zero_grad()
    • B. optimizer.save()
    • C. model.eval()
    • D. 什么都不用做
  5. Hopfield 网络使用什么规则存储模式?
    • A. 梯度下降
    • B. Hebbian 规则
    • C. 遗传算法
    • D. 随机搜索
  6. 测试集的正确用法是?
    • A. 反复调参
    • B. 只评估一次最终模型
    • C. 参与训练
    • D. 验证集太小的时候代替
  7. nn.Linear(784, 256) 的参数个数是?
    • A. 784
    • B. 256
    • C. 784×256+256
    • D. 784+256
  8. Fashion-MNIST 的输入形状是?
    • A. (28, 28) 灰度
    • B. (28, 28, 3) 彩色
    • C. (32, 32)
    • D. (10,)
  9. Hopfield 网络的回忆过程是?
    • A. 一次前向
    • B. 反复迭代 s ← sign(W·s) 直到稳定
    • C. 反向传播
    • D. 随机采样
  10. 1986 年把反向传播系统化的论文作者不包括?
    • A. Rumelhart
    • B. Hinton
    • C. Williams
    • D. LeCun