2.4 前馈网络 信息只往一个方向流、没有环的网络。图里的 P、FF、RBF、DFF 四个结构几乎一样(输入 → 隐藏 → 输出),区别在层数和激活函数。
2.4.1 MLP 多层感知机 〔图:DFF〕
一句话
若干个全连接层 + 激活函数堆起来,每一层的每个神经元都连着上一层的所有神经元。
图中 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 torchimport torch.nn as nnclass 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) 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())) 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():.3 f} ," f"线性回归 {((torch.cat([Xte, torch.ones(500 , 1 )], 1 ) @ w - yte) ** 2 ).mean():.3 f} ,目标方差 {yte.var():.3 f} " )
典型例子
MNIST:两三层 MLP 可以到 98% 左右的准确率
推荐系统:Wide & Deep、DLRM 把类别特征嵌入后接 MLP
强化学习:MuJoCo 机器人控制里状态输入的策略网络、价值网络
行为克隆的早期做法:观测特征 → MLP → 动作(多峰演示时会平均,见 1.4)
各种模型的头和胶水层
优势 :通用;实现最简单;推理极快;作为其他网络的组件无处不在
局限 :参数随输入维度增长,没有共享,费数据;图像展平后用全连接处理,不再显式利用局部性和平移结构(像素的位置信息还在,只是没有对应的归纳偏置);对平移等变换没有不变性;在表格数据上不一定比树模型好
适合的数据 :固定长度、各维度含义不同、没有明显局部结构的向量:表格特征、机器人关节状态、低维物理状态;任何网络末端的头
2.4.2 感知机 〔图:P〕
一句话
一个神经元 + 阶跃激活,分错了就把权重往正确方向挪一点;数据线性可分时保证收敛。
图中 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 torchdef 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 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():.0 f} ,第 20 轮仍分错 {mistakes} 次" )
典型例子
历史:Mark I Perceptron 识别简单的字母和形状
CPU 分支预测:AMD Zen 等处理器的分支预测器用了感知机类的预测器(AMD 称为神经网络预测),因为它够简单,能在硬件里一个时钟周期内给出预测
在线学习:大规模稀疏特征(文本词袋)上的流式线性分类,平均感知机(averaged perceptron)是常用基线
优势 :极简、可解释(权重就是特征的重要性);可以在线逐个样本学习;线性可分时保证收敛;计算量极小
局限 :只能画线性边界;数据不可分时不收敛;输出没有概率含义
适合的数据 :线性可分或接近线性可分的固定长度特征向量;高维稀疏特征;需要在线、低成本更新的流式数据
2.4.3 单隐层前馈网络 〔图:FF〕
一句话
在输入和输出之间加一层隐藏神经元,用反向传播训练;隐藏层把数据变换到一个新空间,原来线性不可分的问题在新空间里变得可分。
图中 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 torchimport torch.nn as nnX = 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 )) 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} 次" )
左:输入空间里 XOR 的两类点需要两条直线才能分开。右:隐藏层把 (0,1) 和 (1,0) 映射到同一点,四个点在新坐标里一条直线就能分开。
典型例子
XOR、奇偶校验等经典小问题
NETtalk(1987):单隐层网络学会把英文字母序列读成音素,训练过程中「从咿呀学语到能读」
ALVINN(1989,CMU):单隐层网络把 30×32 的低分辨率道路图像直接映射成方向盘转角,在 Navlab 车上实现了自动驾驶,是端到端驾驶和行为克隆的早期代表
优势 :万能逼近定理保证了表达能力;结构最小,容易分析;训练快
局限 :复杂函数需要非常宽的隐藏层,深网络效率更高;隐藏单元少时容易卡在局部最优
适合的数据 :低维、规模小的固定长度向量;教学和理论分析
2.4.4 KAN(Kolmogorov-Arnold Network)
一句话
把 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 torchimport torch.nn as nnclass 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 ): basis = torch.exp(-((x[..., None ] - self.centers) / self.width) ** 2 ) return torch.einsum("bik,oik->bo" , basis, self.coef) + self.base(nn.functional.silu(x)) def edge_function (self, j, i, xs ): 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 ) 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):.2 e} " )print (f"MLP:参数 {count(mlp)} ,测试 MSE {fit(mlp):.2 e} " )xs = torch.linspace(-1 , 1 , 5 ) with torch.no_grad(): 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 ))
典型例子
科学发现:原论文用 KAN 拟合物理公式、重新发现纽结理论中的不变量关系
偏微分方程求解、符号回归
需要可解释公式的工程拟合
优势 :边上的函数可视化、可符号化,可解释性强;在低维、光滑的科学函数上参数效率高
局限 :训练慢(基函数计算多,GPU 利用率不如矩阵乘);在图像、语言等一般任务上没有显示出对 MLP 的优势;网格范围和基函数数量要调
适合的数据 :低维、光滑、背后可能有简洁公式的科学和工程数据
2.4.5 RBF 径向基函数网络 〔图:RBF〕
一句话
隐藏层的每个神经元是一个「以某点为中心的高斯小山包」,输入离中心越近响应越大;输出层把这些响应线性加权。
图中 RBF 和 FF 结构完全一样,区别在隐藏层的激活:FF 是「加权和过 sigmoid」,RBF 是「到中心的距离过高斯」。
局部响应 :每个隐藏单元只管中心附近一小片区域,远处输出接近 0。网络整体就是若干个局部小山包的叠加,和核方法、高斯过程是同一类思想。
经典两阶段训练 (Broomhead & Lowe 1988):
定中心:用 k-means 聚类,或直接从训练样本里挑
定输出权重:中心固定后,输出层是线性的,一次最小二乘直接解出来,不需要迭代
也可以把中心、宽度、权重一起用梯度下降端到端训练。
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 torchimport torch.nn as nnclass 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 ): d2 = torch.cdist(x, self.centers) ** 2 return torch.exp(-d2 / (2 * self.log_sigma.exp() ** 2 )) 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) 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 print (f"两阶段(不迭代)MSE = {((Phi @ w - y) ** 2 ).mean():.4 f} " ) 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():.4 f} (噪声方差 0.01)" )with torch.no_grad(): print ("远离所有中心时(x=10)输出 ≈ 偏置:" , round (model(torch.tensor([[10. ]])).item(), 3 ), " 偏置 =" , round (model.linear.bias.item(), 3 ))
典型例子
函数插值:散乱数据插值、地形重建、计算机图形学里的网格变形
混沌时间序列的局部建模
控制:机械臂自适应控制里用 RBF 网络在线逼近未知的动力学项(这类论文在控制领域很多)
和 SVM 的高斯核是同一个函数
优势 :局部响应,训练快,输出层可以闭式求解;插值性能好;结构可解释(每个中心管一片区域)
局限 :维度灾难:输入维度一高,要指数级多的中心才能覆盖空间;外推差,远离所有中心时输出退回偏置
适合的数据 :低维(一般十维以内)连续输入的回归与插值;控制系统里的非线性函数逼近