查看全课大纲(第 5 / 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
学完本节,你将能回答:一个最简单的神经元(感知机)是如何工作的?为什么单个感知机无法解决“异或”问题?通过叠加神经元形成多层感知机(MLP)又如何解决了这个问题?我们将从数学公式、张量形状变化到PyTorch代码实现,完整走通神经网络最基础的前向传播与梯度下降学习过程。本节是理解后续卷积网络和Transformer的基石,先修知识为矩阵乘法和偏导数,后续将接第 2.2 节《反向传播:手推一个两层网络》。
💡 核心导读
- 感知机模型:模仿生物神经元,对输入加权求和后通过激活函数输出,是神经网络的基本单元。
- 线性可分性:单个感知机构成的决策边界是一条直线(或超平面),只能解决线性可分问题(如与、或),无法解决非线性可分问题(如异或)。
- 多层感知机(MLP):通过堆叠多个感知机层,并引入非线性激活函数,网络可以学习复杂的非线性决策边界,从而解决异或等难题。
- 前向传播:数据从输入层,经过隐藏层加权求和与激活,最终到达输出层的过程。我们用矩阵乘法清晰地描述形状变化。
- 梯度下降学习:通过计算损失函数对网络参数的梯度,并沿负梯度方向更新参数,使网络输出逼近真实值。
🧭 前置知识
- 矩阵乘法:若 形状为 , 形状为 ,则 形状为 。计算规则为 。
- 偏导数与链式法则:理解多元函数对某个变量的偏导,以及复合函数求导的链式法则(具体推导见站内数学基础课
math509)。 - Sigmoid函数:,将输入映射到(0,1)区间,早期常用作激活函数。
感知机:一个最简单的神经元
感知机(Perceptron)由Frank Rosenblatt于1957年提出,是神经网络的原型。其数学模型如下:
给定输入向量 ,权重向量 和偏置 ,感知机先计算加权和 ,然后通过一个激活函数 产生输出 。
早期感知机使用阶跃函数作为激活函数: 这使得感知机成为一个二分类器。
在代码中,我们可以用矩阵形式高效处理一个批次的数据。假设批次大小 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)。
线性可分性与异或问题
单个感知机的决策函数为 。当 为阶跃函数时,决策边界是 ,这是一个线性方程(在二维空间是一条直线,三维是一个平面,高维是一个超平面)。因此,单个感知机只能对线性可分的数据进行分类。
经典的反例是异或(XOR)问题。异或运算的真值表如下:
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
将其画在二维平面上,点(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。无论你如何调整权重 和偏置 ,一条直线都无法同时将(0,0)和(1,1)分在一边,将(0,1)和(1,0)分在另一边。
多层感知机:引入非线性与隐藏层
解决非线性可分问题的关键在于引入隐藏层和非线性激活函数。这就是多层感知机(Multilayer Perceptron, MLP),也称为全连接前馈网络。
一个典型的两层MLP(一个隐藏层)结构如下:
- 输入层 -> 隐藏层:,
- 隐藏层 -> 输出层:,
其中 和 是激活函数(如Sigmoid、ReLU)。关键点:隐藏层的激活函数 必须是非线性的。如果 是线性的,那么整个网络可以等价为一个线性变换,退化成单个感知机。
为什么MLP能解决异或?我们可以将问题分解:
- 第一层(两个神经元)可以学习两条不同的直线,将空间划分成几个区域。
- 第二层(一个神经元)学习如何组合这两个区域的判断,形成非线性的决策边界(例如,一个由两条直线围成的区域)。
下面我们用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。在实际中,参数 需要从数据中学习。学习的目标是找到一组参数,使得网络输出 与真实标签 的差距(损失)最小。
最常用的方法是梯度下降(Gradient Descent)。其核心步骤是:
- 前向传播:计算当前参数下的网络输出和损失 。
- 反向传播:利用链式法则,计算损失 对每个参数的梯度 。
- 参数更新:沿着负梯度方向更新参数:,其中 是学习率。
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。 |
本章小结
- 感知机是神经网络的基本单元,进行加权求和与激活,但单个感知机只能产生线性决策边界。
- 异或(XOR)问题是典型的线性不可分问题,揭示了单层网络的局限性。
- 多层感知机(MLP) 通过引入一个或多个隐藏层以及非线性激活函数,获得了强大的非线性拟合能力,能够解决异或等复杂问题。
- 前向传播是数据通过网络层层变换的过程,可以用矩阵乘法清晰地描述其形状变化。
- 梯度下降是神经网络学习参数的核心算法,通过计算损失函数的梯度并沿负梯度方向更新参数,使网络逐步逼近最优解。
- 自动微分(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的梯度,验证与PyTorchautograd的结果是否一致。
延伸阅读
本段 PyTorch 官方文档条目的框架出处:PyTorch: An Imperative Style, High-Performance Deep Learning Library(Paszke et al., 2019,arXiv:1912.01703);页面内容以 PyTorch 2.14.1 为准。
- PyTorch 官方文档 torch.nn.Linear:https://pytorch.org/docs/stable/generated/torch.nn.Linear.html —— 实现全连接层的标准模块。
- PyTorch 官方文档 Autograd mechanics:https://pytorch.org/docs/stable/autograd.html —— 自动微分的工作机制。
The perceptron: a probabilistic model for information storage and organization in the brain(Rosenblatt, 1958, Psychological Review 65(6))—— 感知机的原始论文 - https://doi.org/10.1037/h0042519Learning representations by back-propagating errors(Rumelhart, Hinton & Williams, 1986, Nature 323)—— 反向传播算法的重要论文 - https://doi.org/10.1038/323533a0
—— 小象教研组