叠得更深为何仍只是一条直线?激活函数如何让神经网络表达 XOR
从多层线性变换仍会折叠成一层出发,手算 ReLU 网络如何分开 XOR,追踪张量形状,并用 PyTorch 2.13 实现可调试的两层感知机。
上一篇的 UMAP 在无标签数据上构建近邻图,再为每个训练样本直接优化一个低维坐标。它能展开复杂形状,却没有用任务损失学到一个快速的多层映射。
神经网络(Neural Network)正是把简单变换逐层组合。但有一个看似反直觉的门槛:如果层与层之间没有激活函数,叠再多线性层也只是一层。 本文只追问这一个核心问题:仿射变换、激活函数(Activation Function)与非线性决策边界究竟怎样连在一起?
01 为什么逻辑回归无法分开 XOR?#
异或(Exclusive OR,XOR)只有四个样本:两个输入不同时标签为 1,相同时为 0。
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
任何逻辑回归都先计算 ,再以 作为一条直线边界。可 XOR 的两个正类落在对角,无法用一条直线与另两个点分开。
x₂
▲ y=1 ● ○ y=0
│
│ y=0 ○ ● y=1
└──────────────────────► x₁
不论直线如何旋转,都会把一个对角点分错。text为原始特征手工加入 能解决这个例子,但真实任务有数以千计的特征和未知交互,穷举特征工程很快失控。我们需要让模型从数据中学出有用的中间表示。
02 一个人工神经元究竟计算什么?#
人工神经元(Artificial Neuron)先做仿射变换(Affine Transformation),再通过激活函数 :
- :一个样本的 个输入特征;
- :神经元对每个特征的权重;
- :偏置(Bias),让切分不必穿过原点;
- :激活前值(Pre-activation);
- :传给下一层的激活值(Activation)。
一层中并排 个神经元,对 个样本一次计算:
| 张量 | 形状 | 每个轴的含义 |
|---|---|---|
| 个样本,每个 个特征 | ||
| 从 维输入到 个隐单元 | ||
| 沿 batch 维广播的偏置 | ||
| 每个样本的 个中间特征 |
03 为什么叠两个线性层仍是一层?#
假设中间没有激活函数:
代入并合并同类项:
和 仍只定义了一次仿射变换。层数增加了,可表达的函数类别没有增加;它仍无法分开 XOR。
无激活: X ── W₁,b₁ ── W₂,b₂ ──► Z
可折叠为 W*,b*
有激活: X ── W₁,b₁ ── ReLU ── W₂,b₂ ──► Z
▲
分段折叠,不可整体合并text04 ReLU 怎样创造分段线性区域?#
整流线性单元(Rectified Linear Unit,ReLU)逐元素计算:
它在 时关闭单元,在 时保留线性值。每个隐单元都用 在输入空间划出一个开关边界;多个开关的组合把空间分成多个区域,每个区域内是线性函数,区域之间可以改变斜率。
z < 0 z > 0
──────────────●╱──────────────► z
0
ReLU 输出 0 ReLU 输出 ztext因此 ReLU 网络虽然由线性片段组成,整体却可以形成弯折的分段线性边界。
05 用两个隐单元手算 XOR#
令 ,设置两个隐单元:
输出 logit 为:
| 0 | 0 | 0 | -1 | 0 | |
| 1 | 1 | 0 | 1 | 1 | |
| 1 | 1 | 0 | 1 | 1 | |
| 2 | 2 | 1 | -1 | 0 |
四个点全部正确。两个 ReLU 不是在“记住四张表格”:第一个编码总和,第二个只在两个输入同时为 1 时开启,输出层用后者把过大的总和拉回负类。这就是一个可解释的中间表示。
06 Sigmoid、tanh 和 ReLU 应该放在哪里?#
| 激活 | 定义 | 输出范围 | 常见用法 | 主要风险 |
|---|---|---|---|---|
| Sigmoid | 二分类推理概率、门控 | 大绝对值时梯度接近 0 | ||
| tanh | 需要零中心有界激活的结构 | 两端饱和 | ||
| ReLU | 普通 MLP 隐藏层的强基线 | 负区域长期无梯度 | ||
| Leaky ReLU | 希望负区域仍有小梯度 | 是新设计选择 |
隐藏层和输出层的职责不同。二分类训练时,隐藏层可用 ReLU,最后一层应输出无界 logits,直接交给 BCEWithLogitsLoss。该损失在内部合并 Sigmoid 与 BCE,利用 log-sum-exp 获得更稳定的数值计算。只在推理或展示概率时手动 sigmoid(logits)。
07 完整数据流与张量形状#
对 个二维样本、 个隐单元:
X [N,2]
│ Linear(2,4): X @ W₁ᵀ + b₁
▼
Z₁ [N,4]
│ ReLU,形状不变
▼
H₁ [N,4]
│ Linear(4,1): H₁ @ W₂ᵀ + b₂
▼
logits [N,1]
│ squeeze(-1),只删除最后的单例维
▼
logits [N] ── BCEWithLogitsLoss(y [N]) ──► loss []textPyTorch 的 nn.Linear(in_features, out_features) 把权重存成 [out_features,in_features],因此数学上计算 。不要因为纸上把 写成 [D,H] 就手动改变模块参数的维度约定。
08 训练和推理的最小伪代码#
初始化 W₁,b₁,W₂,b₂
对每个 epoch:
对每个 mini-batch (X, y):
Z₁ = affine(X, W₁, b₁)
H₁ = ReLU(Z₁)
logits = affine(H₁, W₂, b₂)
loss = stable_binary_cross_entropy(logits, y)
清空旧梯度
求 loss 对所有参数的梯度
更新参数
推理:
关闭梯度记录
probability = sigmoid(model(X_new))
prediction = probability >= validated_thresholdtext本篇聚焦前向表示;“求所有参数的梯度”不是黑箱口号,下一篇会沿计算图逐边手算。
09 用 PyTorch 2.13 实现可检查的两层网络#
当前稳定 API 中,nn.Linear ↗ 完成仿射变换,nn.ReLU ↗ 保持输入输出形状,BCEWithLogitsLoss ↗ 要求输入与目标形状一致。
import torch
from torch import nn
torch.manual_seed(7)
X = torch.tensor([
[0.0, 0.0],
[0.0, 1.0],
[1.0, 0.0],
[1.0, 1.0],
], dtype=torch.float32) # [N=4,D=2]
y = torch.tensor([0.0, 1.0, 1.0, 0.0]) # [4]
model = nn.Sequential(
nn.Linear(in_features=2, out_features=4), # [4,2] -> [4,4]
nn.ReLU(),
nn.Linear(in_features=4, out_features=1), # [4,4] -> [4,1]
)
loss_fn = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.03)
for step in range(2000):
optimizer.zero_grad(set_to_none=True)
logits = model(X).squeeze(-1) # [4]
loss = loss_fn(logits, y) # []
loss.backward()
optimizer.step()
if step % 200 == 0:
assert torch.isfinite(loss)
model.eval()
with torch.inference_mode():
logits = model(X).squeeze(-1) # [4]
probabilities = torch.sigmoid(logits) # [4]
predictions = (probabilities >= 0.5).to(torch.int64)
print('logits:', logits)
print('probabilities:', probabilities)
print('predictions:', predictions)
assert torch.equal(predictions, y.to(torch.int64))python这个数据集小到只能验证表达能力,不能证明泛化。随机初始化和优化路径也可能使极小网络未在固定步数内学会 XOR;因此测试代码应固定种子并检查最终预测,不要只看损失打印。
10 从第一个失败开始调试#
- 先查形状。 打印每层输入输出;二分类的
logits和target必须完全同形。 - 再查数值。 在前向、损失、梯度和更新后分别查验
isfinite,定位第一个 NaN/Inf。 - 查激活占比。 记录
(hidden > 0).float().mean();所有 ReLU 长期为 0 表示“死亡 ReLU”。 - 查最后一层。
BCEWithLogitsLoss前不能再加 Sigmoid;否则损失把概率误当成 logit。 - 尝试过拟合一个极小 batch。 若一个有可学信号的小 batch 都无法拟合,优先怀疑实现、标签和学习率,而非泛化。
可以用显式模块捕获中间值:
linear1 = model[0]
relu = model[1]
with torch.no_grad():
preactivation = linear1(X) # [4,4]
hidden = relu(preactivation) # [4,4]
print('active_fraction:', (hidden > 0).float().mean().item())
print('weight_shapes:', [tuple(p.shape) for p in model.parameters()])python11 常见错误、工程边界与失败场景#
- 用
squeeze()删掉所有单例维。 batch size 为 1 时可能连 batch 维也消失;优先squeeze(-1)。 - 利用广播隐藏了标签形状错误。
[N,1]与[N]的组合可能在其他损失中扩展成[N,N];每次明确断言。 - 为所有层都加 ReLU。 最后 logit 被截断为非负后,模型对负类的表达会被破坏。
- 用很大学习率把所有 ReLU 推入负区域。 降低学习率,检查初始化,或在确有需要时比较 Leaky ReLU。
- 把两层 MLP 当成万能结构。 它没有显式利用图像平移、时序因果或图邻接等结构,往往需要更多样本和参数。
- 忽视数据边界。 网络容量更大,并不会自动阻止泄漏、标签错位或分布漂移。
12 与相近方法的边界#
| 方法 | 非线性从哪里来 | 学习对象 | 主要特点 |
|---|---|---|---|
| 逻辑回归 | 原始特征上没有 | 一组线性权重 | 几何清晰,表达有限 |
| 多项式特征 + 线性模型 | 人工交互项 | 扩展后的线性权重 | 可解释,特征数可爆炸 |
| 核 SVM | 核函数隐式特征 | 支持向量系数 | 中小数据强,大样本可扩展性受限 |
| 决策树 | 轴对齐分裂 | 递归规则 | 表格数据强,不用梯度 |
| ReLU MLP | 可学仿射 + 分段开关 | 多层特征与输出 | 表示灵活,优化与调试更复杂 |
理论上,足够宽的单隐藏层网络可以近似很广的连续函数类;但这不保证有限数据下能学到、优化器能找到,或分布外能正确。“能表达”、“能训练”和“能泛化”是三个不同问题。
13 今天真正需要记住什么?#
- 仿射层先计算 ;多个无激活的仿射层可合并为一层。
- ReLU 为每个隐单元引入开关边界,多个边界组合出分段线性的非线性模型。
- XOR 用两个 ReLU 隐单元就能手算分开,关键是学中间表示,而不是单纯增加层数。
- PyTorch
nn.Linear的权重存储为[out,in];二分类训练应输出 logits 并交给BCEWithLogitsLoss。 - 表达能力不等于可优化性或泛化;形状、数值、激活占比和小 batch 过拟合是最先的调试证据。
14 思考题与小练习#
- 删掉示例中的
nn.ReLU(),保留两个nn.Linear,多次改变隐藏宽度和种子。为什么它无法稳定把 XOR 四点全部分对? - 在手算网络中把输入改为 和 ,计算 与概率。画出 在 中的分段边界。
- 分别使用 ReLU、tanh 和 LeakyReLU 训练同一个小网络,记录损失、隐单元零值比例和每层梯度范数;先固定数据、初始权重与优化器。
相关工作#
- McCulloch & Pitts (1943), A Logical Calculus of the Ideas Immanent in Nervous Activity ↗:将简化神经元表述为逻辑计算单元的早期奠基工作。
- Rosenblatt (1958), The Perceptron ↗:感知机学习与线性可分类问题的经典论文。
- Cybenko (1989), Approximation by Superpositions of a Sigmoidal Function ↗:单隐藏层网络通用近似性的代表性结果。
- Glorot & Bengio (2010), Understanding the Difficulty of Training Deep Feedforward Neural Networks ↗:连接激活、初始化与深层优化困难的系统分析。
- Nair & Hinton (2010), Rectified Linear Units Improve Restricted Boltzmann Machines ↗:推动整流线性单元广泛使用的代表性工作。
15 下一篇预告#
现在一个两层网络已经能把 XOR 折成可分的中间表示,但手算权重不会扩展到真实数据。下一篇将把前向计算展开成有向计算图,用链式法则追踪输出误差如何分配给每一层的权重与偏置。