2.4 前馈网络

信息只往一个方向流、没有环的网络。图里的 P、FF、RBF、DFF 四个结构几乎一样(输入 → 隐藏 → 输出),区别在层数和激活函数。

2.4.1 MLP 多层感知机 〔图:DFF〕

<center>2.4.1 MLP
2.4.1 MLP
<center>2.4.1 MLP
2.4.1 MLP

一句话

若干个全连接层 + 激活函数堆起来,每一层的每个神经元都连着上一层的所有神经元。

图中 DFF(Deep Feed Forward)指隐藏层多于一层的前馈网络。「深」带来的好处在 1.2 节讲过:同样的表达能力,深网络需要的神经元比浅网络少得多。2006–2012 年间,ReLU、更好的初始化、Dropout、GPU 和大数据一起让深 MLP 变得可训练。

现在的角色:MLP 很少单独当主角,但几乎每个模型里都有它:

位置 例子
分类头 / 回归头 / 动作头 主干网络之后的最后几层
Transformer 的 FFN 每个 block 里 attention 后面那个两层 MLP(2.1.1)
门控、路由、条件生成 MoE 的 router、FiLM 里生成 γ 和 β 的小网络
坐标网络 NeRF 用 MLP 把坐标映射成颜色和密度(2.11.1)
强化学习策略 状态是低维向量时,PPO / SAC 常用 2 层 64–256 宽的 MLP
低维信号编码 机器人关节角、末端位姿、6 维力/力矩先过一个小 MLP 再融合

表格数据的提醒:在列含义各不相同的表格数据上,梯度提升树(XGBoost、LightGBM)常常比 MLP 更好、更省事。MLP 的优势在数据量大、需要和其他网络端到端训练的场合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch
import torch.nn as nn

class MLP(nn.Module):
def __init__(self, in_dim, hidden_dims, out_dim, p_drop=0.1):
super().__init__()
layers, d = [], in_dim
for h in hidden_dims:
layers += [nn.Linear(d, h), nn.LayerNorm(h), nn.GELU(), nn.Dropout(p_drop)]
d = h
layers.append(nn.Linear(d, out_dim))
self.net = nn.Sequential(*layers)

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

# MNIST 尺寸的经典 MLP 基线:图像先展平,之后的全连接层不再显式利用像素之间的邻近关系
model = MLP(28 * 28, [512, 256], 10)
print("输出:", tuple(model(torch.randn(32, 1, 28, 28).flatten(1)).shape),
"参数量:", sum(p.numel() for p in model.parameters()))

# 表格式回归:10 个特征,目标含交互项和非线性;对比线性回归
torch.manual_seed(0)
X = torch.randn(3000, 10)
y = (X[:, 0] * X[:, 1] + torch.sin(2 * X[:, 2]) + 0.5 * X[:, 3] ** 2 + 0.1 * torch.randn(3000)).unsqueeze(1)
Xtr, ytr, Xte, yte = X[:2500], y[:2500], X[2500:], y[2500:]
mlp = MLP(10, [64, 64], 1, p_drop=0.0)
opt = torch.optim.Adam(mlp.parameters(), lr=3e-3)
for step in range(1500):
loss = ((mlp(Xtr) - ytr) ** 2).mean()
opt.zero_grad(); loss.backward(); opt.step()
A = torch.cat([Xtr, torch.ones(2500, 1)], 1)
w = torch.linalg.lstsq(A, ytr).solution # 线性回归的闭式解
with torch.no_grad():
print(f"测试 MSE:MLP {((mlp(Xte) - yte) ** 2).mean():.3f},"
f"线性回归 {((torch.cat([Xte, torch.ones(500, 1)], 1) @ w - yte) ** 2).mean():.3f},目标方差 {yte.var():.3f}")

典型例子

  • MNIST:两三层 MLP 可以到 98% 左右的准确率
  • 推荐系统:Wide & Deep、DLRM 把类别特征嵌入后接 MLP
  • 强化学习:MuJoCo 机器人控制里状态输入的策略网络、价值网络
  • 行为克隆的早期做法:观测特征 → MLP → 动作(多峰演示时会平均,见 1.4)
  • 各种模型的头和胶水层
  • 优势:通用;实现最简单;推理极快;作为其他网络的组件无处不在
  • 局限:参数随输入维度增长,没有共享,费数据;图像展平后用全连接处理,不再显式利用局部性和平移结构(像素的位置信息还在,只是没有对应的归纳偏置);对平移等变换没有不变性;在表格数据上不一定比树模型好
  • 适合的数据:固定长度、各维度含义不同、没有明显局部结构的向量:表格特征、机器人关节状态、低维物理状态;任何网络末端的头

2.4.2 感知机 〔图:P〕

<center>2.4.2 感知机
2.4.2 感知机
<center>2.4.2 感知机
2.4.2 感知机

一句话

一个神经元 + 阶跃激活,分错了就把权重往正确方向挪一点;数据线性可分时保证收敛。

图中 P 就是两个输入直接连一个输出,没有隐藏层。Rosenblatt 1958 年提出,并造出了硬件 Mark I Perceptron(20×20 个光电管当输入)。

学习规则:遍历样本,只在分错时更新:

直觉:正样本被判成负,就把 w 往 x 的方向拉,让 w·x 变大;反之往反方向推。

收敛定理:只要数据线性可分,感知机在有限步内一定找到一个能把训练集全部分对的超平面。数据不可分时,权重会一直来回震荡。

XOR 问题:1969 年 Minsky 和 Papert 在《Perceptrons》一书中指出,单层感知机连 XOR 都学不了(四个点没法用一条直线分开)。这本书让神经网络研究冷了十几年,直到多层网络加反向传播(2.4.3)出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch

def train_perceptron(X, y, epochs=20, lr=1.0):
w, b = torch.zeros(X.shape[1]), torch.tensor(0.0)
for _ in range(epochs):
mistakes = 0
for xi, yi in zip(X, y):
if yi * (w @ xi + b) <= 0: # 分错了(或正好在边界上)才更新
w += lr * yi * xi
b += lr * yi
mistakes += 1
return w, b, mistakes # mistakes:最后一轮遍历里分错的次数

X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
for name, y in [("AND", torch.tensor([-1., -1., -1., 1.])), # 线性可分
("OR ", torch.tensor([-1., 1., 1., 1.])), # 线性可分
("XOR", torch.tensor([-1., 1., 1., -1.]))]: # 线性不可分
w, b, mistakes = train_perceptron(X, y)
print(f"{name}: w = {w.tolist()}, b = {b.item():.0f},第 20 轮仍分错 {mistakes} 次")
# AND、OR 很快不再出错;XOR 每一轮都在分错、权重来回震荡,永远停不下来

典型例子

  • 历史:Mark I Perceptron 识别简单的字母和形状
  • CPU 分支预测:AMD Zen 等处理器的分支预测器用了感知机类的预测器(AMD 称为神经网络预测),因为它够简单,能在硬件里一个时钟周期内给出预测
  • 在线学习:大规模稀疏特征(文本词袋)上的流式线性分类,平均感知机(averaged perceptron)是常用基线
  • 优势:极简、可解释(权重就是特征的重要性);可以在线逐个样本学习;线性可分时保证收敛;计算量极小
  • 局限:只能画线性边界;数据不可分时不收敛;输出没有概率含义
  • 适合的数据:线性可分或接近线性可分的固定长度特征向量;高维稀疏特征;需要在线、低成本更新的流式数据

2.4.3 单隐层前馈网络 〔图:FF〕

<center>2.4.3 单隐层前馈网络
2.4.3 单隐层前馈网络
<center>2.4.3 单隐层前馈网络
2.4.3 单隐层前馈网络

一句话

在输入和输出之间加一层隐藏神经元,用反向传播训练;隐藏层把数据变换到一个新空间,原来线性不可分的问题在新空间里变得可分。

图中 FF 是 2 个输入、2 个隐藏单元、1 个输出,正好是能解 XOR 的最小网络。1986 年 Rumelhart、Hinton、Williams 在 Nature 上发表反向传播,让这类网络可以训练。

为什么一层隐藏层就能解 XOR:两个隐藏单元可以分别学成「OR」和「AND」(或等价的组合),XOR = OR 且非 AND,在 (OR, AND) 这个新坐标系里四个点线性可分,输出层一条直线就够了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import torch
import torch.nn as nn

X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
y = torch.tensor([[0.], [1.], [1.], [0.]])

def train(hidden, seed):
torch.manual_seed(seed)
net = nn.Sequential(nn.Linear(2, hidden), nn.Sigmoid(), nn.Linear(hidden, 1)) # 图里的 2-2-1 结构
opt = torch.optim.Adam(net.parameters(), lr=0.05)
for _ in range(3000):
loss = nn.functional.binary_cross_entropy_with_logits(net(X), y)
opt.zero_grad(); loss.backward(); opt.step()
return net, ((torch.sigmoid(net(X)) > 0.5).float() == y).all().item()

for seed in range(10): # 找一个训练成功的种子来看隐藏层
net, ok = train(hidden=2, seed=seed)
if ok:
break
print(f"种子 {seed}:2 个隐藏单元学会了 XOR")
with torch.no_grad():
h = torch.sigmoid(net[0](X))
print("隐藏层把 (0,0) (0,1) (1,0) (1,1) 映射到:\n", h.numpy().round(2)) # 新坐标系里线性可分
for hidden in [2, 4]:
wins = sum(train(hidden, s)[1] for s in range(10))
print(f"{hidden} 个隐藏单元,10 个随机种子里成功 {wins} 次") # 2 个单元常卡在局部最优(10 次只成功 4 次)
<center>XOR决策边界与隐藏层
XOR决策边界与隐藏层
<center>XOR决策边界与隐藏层
XOR决策边界与隐藏层

左:输入空间里 XOR 的两类点需要两条直线才能分开。右:隐藏层把 (0,1) 和 (1,0) 映射到同一点,四个点在新坐标里一条直线就能分开。

典型例子

  • XOR、奇偶校验等经典小问题
  • NETtalk(1987):单隐层网络学会把英文字母序列读成音素,训练过程中「从咿呀学语到能读」
  • ALVINN(1989,CMU):单隐层网络把 30×32 的低分辨率道路图像直接映射成方向盘转角,在 Navlab 车上实现了自动驾驶,是端到端驾驶和行为克隆的早期代表
  • 优势:万能逼近定理保证了表达能力;结构最小,容易分析;训练快
  • 局限:复杂函数需要非常宽的隐藏层,深网络效率更高;隐藏单元少时容易卡在局部最优
  • 适合的数据:低维、规模小的固定长度向量;教学和理论分析

2.4.4 KAN(Kolmogorov-Arnold Network)

<center>2.4.4 KAN
2.4.4 KAN
<center>2.4.4 KAN
2.4.4 KAN

一句话

把 MLP 里「边上是固定的数(权重)、节点上是固定的激活函数」反过来:每条边上是一个可学习的一元函数,节点只做求和。

理论来源:Kolmogorov-Arnold 表示定理说,任何多元连续函数都可以写成有限个一元函数的复合与求和:

KAN 层(Liu 2024):

每个 φ_ji 用一组基函数(原论文用 B 样条)的线性组合表示,系数可学习。训练完可以把每条边上的函数画出来,如果像 sin、exp、x²,还能直接替换成符号表达式,得到一个公式。

争议:原论文在低维科学函数拟合上显示了更好的精度和可解释性;后续的公平对比(如 Yu 等 2024)发现,在图像、文本等一般机器学习任务上,参数量对齐后 MLP 通常持平或更好,KAN 训练也更慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import torch
import torch.nn as nn

class KANLayer(nn.Module):
"""每条边一个一元函数 φ(x) = Σ_k c_k·B_k(x) + w·silu(x)。
基函数用高斯径向基代替原论文的 B 样条(FastKAN 的做法),代码短、效果接近"""
def __init__(self, din, dout, n_basis=8, x_range=(-2, 2)):
super().__init__()
self.register_buffer("centers", torch.linspace(*x_range, n_basis))
self.width = (x_range[1] - x_range[0]) / (n_basis - 1)
self.coef = nn.Parameter(torch.randn(dout, din, n_basis) * 0.1) # 每条边一组系数
self.base = nn.Linear(din, dout)

def forward(self, x): # x: (B, din)
basis = torch.exp(-((x[..., None] - self.centers) / self.width) ** 2) # (B, din, n_basis)
return torch.einsum("bik,oik->bo", basis, self.coef) + self.base(nn.functional.silu(x))

def edge_function(self, j, i, xs): # 取出第 i→j 条边上的函数
basis = torch.exp(-((xs[:, None] - self.centers) / self.width) ** 2)
return basis @ self.coef[j, i] + self.base.weight[j, i] * nn.functional.silu(xs)

torch.manual_seed(0)
f = lambda x: torch.exp(torch.sin(torch.pi * x[:, :1]) + x[:, 1:] ** 2) # KAN 论文里的例子
X, Xte = torch.rand(2000, 2) * 2 - 1, torch.rand(1000, 2) * 2 - 1
Y, Yte = f(X), f(Xte)

def fit(model, steps=3000):
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
for _ in range(steps):
loss = ((model(X) - Y) ** 2).mean()
opt.zero_grad(); loss.backward(); opt.step()
with torch.no_grad():
return ((model(Xte) - Yte) ** 2).mean().item()

count = lambda m: sum(p.numel() for p in m.parameters())
kan = nn.Sequential(KANLayer(2, 5), KANLayer(5, 1))
mlp = nn.Sequential(nn.Linear(2, 32), nn.SiLU(), nn.Linear(32, 32), nn.SiLU(), nn.Linear(32, 1))
print(f"KAN:参数 {count(kan)},测试 MSE {fit(kan):.2e}")
print(f"MLP:参数 {count(mlp)},测试 MSE {fit(mlp):.2e}")
xs = torch.linspace(-1, 1, 5)
with torch.no_grad(): # 看第一层里 x₁、x₂ 各自变化最大的那条边
for i, name in [(0, "x₁"), (1, "x₂")]:
curves = torch.stack([kan[0].edge_function(j, i, xs) for j in range(5)])
j = (curves.max(1).values - curves.min(1).values).argmax()
print(f"{name} 的边函数在 x = -1, -0.5, 0, 0.5, 1 处:", curves[j].numpy().round(2))
# x₂ 那条边左右对称、形状像 x²;x₁ 的作用分摊在几条边上,单看一条不像 sin。
# 原论文训练时加稀疏正则、剪掉多余的边,剩下的边才适合逐条符号化

典型例子

  • 科学发现:原论文用 KAN 拟合物理公式、重新发现纽结理论中的不变量关系
  • 偏微分方程求解、符号回归
  • 需要可解释公式的工程拟合
  • 优势:边上的函数可视化、可符号化,可解释性强;在低维、光滑的科学函数上参数效率高
  • 局限:训练慢(基函数计算多,GPU 利用率不如矩阵乘);在图像、语言等一般任务上没有显示出对 MLP 的优势;网格范围和基函数数量要调
  • 适合的数据:低维、光滑、背后可能有简洁公式的科学和工程数据

2.4.5 RBF 径向基函数网络 〔图:RBF〕

<center>2.4.5 RBF
2.4.5 RBF
<center>2.4.5 RBF
2.4.5 RBF

一句话

隐藏层的每个神经元是一个「以某点为中心的高斯小山包」,输入离中心越近响应越大;输出层把这些响应线性加权。

图中 RBF 和 FF 结构完全一样,区别在隐藏层的激活:FF 是「加权和过 sigmoid」,RBF 是「到中心的距离过高斯」。

局部响应:每个隐藏单元只管中心附近一小片区域,远处输出接近 0。网络整体就是若干个局部小山包的叠加,和核方法、高斯过程是同一类思想。

经典两阶段训练(Broomhead & Lowe 1988):

  1. 定中心:用 k-means 聚类,或直接从训练样本里挑
  2. 定输出权重:中心固定后,输出层是线性的,一次最小二乘直接解出来,不需要迭代

也可以把中心、宽度、权重一起用梯度下降端到端训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch
import torch.nn as nn

class RBFNet(nn.Module):
def __init__(self, in_dim, n_centers, out_dim):
super().__init__()
self.centers = nn.Parameter(torch.randn(n_centers, in_dim))
self.log_sigma = nn.Parameter(torch.zeros(n_centers))
self.linear = nn.Linear(n_centers, out_dim)

def features(self, x): # x: (B, in_dim)
d2 = torch.cdist(x, self.centers) ** 2 # (B, M) 到每个中心的距离平方
return torch.exp(-d2 / (2 * self.log_sigma.exp() ** 2)) # 高斯响应,越远越接近 0

def forward(self, x):
return self.linear(self.features(x))

torch.manual_seed(0)
x = torch.linspace(-3, 3, 200).unsqueeze(1)
y = torch.sin(2 * x) + 0.1 * torch.randn_like(x)

# 方式 1:两阶段。中心取训练样本,宽度固定,输出层一次最小二乘解出
model = RBFNet(1, 20, 1)
with torch.no_grad():
model.centers.copy_(x[torch.randperm(200)[:20]])
model.log_sigma.fill_(torch.log(torch.tensor(0.4)))
Phi = torch.cat([model.features(x), torch.ones(200, 1)], 1)
w = torch.linalg.lstsq(Phi, y).solution # 只用 Phi @ w 算误差演示,w 没有写回 model.linear;方式 2 的输出层仍从随机初始化开始训练
print(f"两阶段(不迭代)MSE = {((Phi @ w - y) ** 2).mean():.4f}")

# 方式 2:中心、宽度、权重一起梯度下降
opt = torch.optim.Adam(model.parameters(), lr=0.02)
for step in range(1000):
loss = ((model(x) - y) ** 2).mean()
opt.zero_grad(); loss.backward(); opt.step()
print(f"端到端训练 MSE = {loss.item():.4f}(噪声方差 0.01)")
with torch.no_grad():
print("远离所有中心时(x=10)输出 ≈ 偏置:", round(model(torch.tensor([[10.]])).item(), 3),
" 偏置 =", round(model.linear.bias.item(), 3))
<center>RBF局部基函数
RBF局部基函数
<center>RBF局部基函数
RBF局部基函数

典型例子

  • 函数插值:散乱数据插值、地形重建、计算机图形学里的网格变形
  • 混沌时间序列的局部建模
  • 控制:机械臂自适应控制里用 RBF 网络在线逼近未知的动力学项(这类论文在控制领域很多)
  • 和 SVM 的高斯核是同一个函数
  • 优势:局部响应,训练快,输出层可以闭式求解;插值性能好;结构可解释(每个中心管一片区域)
  • 局限:维度灾难:输入维度一高,要指数级多的中心才能覆盖空间;外推差,远离所有中心时输出退回偏置
  • 适合的数据:低维(一般十维以内)连续输入的回归与插值;控制系统里的非线性函数逼近