2.13 脉冲神经网络

2.13.1 SNN 脉冲神经网络

<center>2.13.1 SNN
2.13.1 SNN
<center>2.13.1 SNN
2.13.1 SNN

一句话

神经元像生物神经元一样积累膜电位,超过阈值才发出一个脉冲;信息由「何时发放」承载。在有事件才计算的神经形态硬件上,没有输入时几乎不计算(用 PyTorch 在 CPU、GPU 上仿真时,每个时间步照样做稠密运算)。

图中只有 LSM(2.14.3)用到了脉冲单元,而且整个储备池不训练。这里讲的是可以端到端训练的脉冲网络。

LIF(泄漏积分发放)神经元:

U_t 是积分后、重置前的膜电位,V_t 是重置后的膜电位。β < 1 是泄漏(没有输入时电位慢慢回落),I_t 是输入电流(上一层脉冲的加权和),Θ 是阶跃函数,发放后当场减去阈值(软重置),和下面的代码一致。另一种常见写法把重置推迟到下一步再减(snnTorch 的默认写法),两者差一个 β 因子,是不同的离散约定。

训练的难点与替代梯度:阶跃函数的导数几乎处处为 0,反向传播传不过去。替代梯度法(Neftci 2019 等)前向时照常用阶跃函数,反向时假装它是一个平滑函数(比如 fast sigmoid),用平滑函数的导数代替。网络按时间展开后就能用 BPTT 训练。另一条路是先训练普通网络,再把它转换成脉冲网络。

事件数据:事件相机(DVS)每个像素独立工作,亮度变化超过阈值才输出一个事件 (x, y, t, 极性),时间分辨率到微秒级,没有固定帧率。事件式触觉传感器同理,只有接触变化时才输出。脉冲网络天然适合这类异步、稀疏的数据。但现实中更常见的做法是把事件流重新积分成「帧」再交给普通的 CNN 或 Transformer:

表示 做法
事件计数图 一个时间窗内每个像素的事件数(正负极性分两个通道)
体素网格 把时间也切成若干格,得到一个时空三维张量
时间面 每个像素记录最近一次事件的时刻,越新越亮

这样做能直接用现成模型,代价是丢掉了一部分时间分辨率。

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
48
49
50
51
52
53
54
55
56
57
import torch
import torch.nn as nn

class SpikeFn(torch.autograd.Function):
"""前向:阶跃函数(发放或不发放);反向:用 fast sigmoid 的导数代替(替代梯度)"""
@staticmethod
def forward(ctx, v):
ctx.save_for_backward(v)
return (v > 0).float()

@staticmethod
def backward(ctx, grad):
(v,) = ctx.saved_tensors
return grad / (1 + 10 * v.abs()) ** 2

class LIFLayer(nn.Module):
def __init__(self, din, dout, beta=0.9, v_th=1.0):
super().__init__()
self.fc = nn.Linear(din, dout)
self.beta, self.v_th = beta, v_th

def forward(self, x): # x: (T, B, din) 脉冲序列
v = torch.zeros(x.size(1), self.fc.out_features)
spikes = []
for t in range(x.size(0)):
v = self.beta * v + self.fc(x[t]) # 泄漏 + 积分
s = SpikeFn.apply(v - self.v_th) # 超过阈值就发放
v = v - s * self.v_th # 软重置
spikes.append(s)
return torch.stack(spikes) # (T, B, dout)

def sweep_batch(n, T=40, C=20):
"""刺激在 20 个传感点上从左扫到右(类 0)或从右扫到左(类 1),比如触觉阵列上的滑动方向"""
y = torch.randint(0, 2, (n,))
x = (torch.rand(T, n, C) < 0.02).float() # 背景噪声脉冲
for i in range(n):
start = torch.randint(0, 10, (1,)).item()
for c in range(C):
ch = c if y[i] == 0 else C - 1 - c
x[start + c, i, ch] = 1.0 # 每个传感点各一个扫描刺激脉冲,时间先后不同(背景噪声脉冲另算)
return x, y

torch.manual_seed(0)
net = nn.ModuleList([LIFLayer(20, 64), LIFLayer(64, 2)])
opt = torch.optim.Adam(net.parameters(), lr=2e-3)
for step in range(300):
x, y = sweep_batch(32)
counts = net[1](net[0](x)).sum(0) # 输出层每个神经元的脉冲总数当 logits
loss = nn.functional.cross_entropy(counts, y)
opt.zero_grad(); loss.backward(); opt.step()

with torch.no_grad():
x, y = sweep_batch(500)
hidden = net[0](x)
acc = (net[1](hidden).sum(0).argmax(1) == y).float().mean().item()
print(f"滑动方向分类准确率 {acc:.3f}(每个传感点有一个扫描刺激脉冲,另有随机背景脉冲;两类的脉冲数分布相同,主要差在先后顺序)")
print(f"隐藏层平均发放率 {hidden.mean():.3f}(每个神经元每个时间步发放的概率;在支持稀疏执行的神经形态硬件上,越低越省能耗)")
<center>SNN脉冲栅格图
SNN脉冲栅格图
<center>SNN脉冲栅格图
SNN脉冲栅格图

上排是两类输入:每个传感点有一个扫描刺激脉冲,再加上随机的背景脉冲;两类的脉冲数分布相同,主要差在扫描的先后顺序。训练后的隐藏层对两种顺序给出不同的放电模式,输出层据此分类。

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

# 事件流 → 三种「帧」表示。事件 = (x, y, t, 极性)
torch.manual_seed(0)
n_ev, H, W = 5000, 32, 32
t = torch.sort(torch.rand(n_ev)).values # 时间戳(秒)
x = (16 + 10 * torch.cos(6 * t) + torch.randn(n_ev)).clamp(0, W - 1).long() # 一个移动的边缘
y = (16 + 10 * torch.sin(6 * t) + torch.randn(n_ev)).clamp(0, H - 1).long()
p = (torch.rand(n_ev) < 0.5).long() # 极性:变亮 1 / 变暗 0

count = torch.zeros(2, H, W).index_put_((p, y, x), torch.ones(n_ev), accumulate=True) # 计数图
B = 5
bins = (t * B).long().clamp(max=B - 1)
voxel = torch.zeros(B, H, W).index_put_((bins, y, x), torch.where(p == 1, 1.0, -1.0), accumulate=True) # 体素网格
surface = torch.zeros(H * W).scatter_reduce(0, y * W + x, t, reduce="amax").view(H, W) # 时间面:每个像素最近一次事件的时刻
print("计数图", tuple(count.shape), "体素网格", tuple(voxel.shape), "时间面", tuple(surface.shape))
print("每个时间格里的事件数:", torch.bincount(bins, minlength=B).tolist())

典型例子

  • 神经形态芯片:Intel Loihi、IBM TrueNorth、SpiNNaker,在特定任务上功耗比 GPU 低一到两个数量级
  • 事件相机:DVS128 Gesture 手势识别、高速运动下的跟踪
  • 事件式触觉:NUS 的 NeuTouch 触觉皮肤配合 SNN 做视触觉物体识别和滑移检测(RSS 2020)
  • 低功耗的语音唤醒词检测
  • 优势:事件触发、稀疏计算,在神经形态硬件上功耗极低;脉冲的时间本身携带信息;和事件相机、事件式触觉的数据形式直接匹配
  • 局限:不可导,要靠替代梯度或网络转换;在 GPU 上仿真反而比普通网络慢;精度普遍低于同规模的普通网络;工具链和硬件还不普及
  • 适合的数据:稀疏的事件流和脉冲数据:事件相机、事件式触觉、神经电生理信号;功耗受限的边缘设备

2.14 储备池与随机网络

这一族的共同点:网络的大部分权重随机生成后固定不变,只训练读出层。读出层用平方损失(岭回归)时,训练就是一次线性代数求解;用逻辑回归之类的读出时,仍要迭代优化。

2.14.1 ESN 回声状态网络 〔图:ESN〕

<center>2.14.1 ESN
2.14.1 ESN
<center>2.14.1 ESN
2.14.1 ESN

一句话

一个随机连接、固定不训练的大循环网络(储备池)把输入序列的历史「搅拌」成高维状态,只训练一个线性层从状态里读出答案。

图中 ESN 的隐藏单元是随机相连的「循环单元」。Jaeger 2001 年提出。

a 是泄漏率。W_in 和 W 随机生成后不变,只有 W_out 用岭回归一次解出:

回声状态性质:储备池的状态只取决于近期的输入历史,初始状态的影响会像回声一样逐渐消失。常用做法是把 W 的谱半径(最大特征值的模)缩放到 1 附近或略小于 1。这是经验规则,单独并不能保证回声状态性质,Yildiz、Jaeger、Kiebel 2012 年构造过谱半径小于 1 仍不满足的反例;对标准 tanh 储备池,最大奇异值小于 1 是一个更保守的充分条件。实际性质还受泄漏率、输入缩放和输入本身影响。在稳定的范围内,谱半径越接近 1,记忆通常越长。

物理储备池计算:既然储备池只要「有记忆的非线性动力系统」就行,就可以用真实的物理系统充当:光学器件、自旋电子器件,甚至软体机器人的身体(Nakajima 等用类章鱼臂的软体结构当储备池做计算)。

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
48
49
50
import math
import torch

def mackey_glass(n, tau=17, dt=0.1, sub=10):
"""Mackey-Glass 混沌时间序列:dx/dt = 0.2·x(t-τ)/(1 + x(t-τ)^10) - 0.1·x(t)"""
hist = int(tau / dt)
x = [1.2] * (hist + 1)
for _ in range((n + 300) * sub):
x_tau = x[-hist - 1]
x.append(x[-1] + dt * (0.2 * x_tau / (1 + x_tau ** 10) - 0.1 * x[-1]))
return torch.tensor(x[-n * sub::sub]) # 每 1 个时间单位取一个点

torch.manual_seed(0)
data = mackey_glass(3000)
data = (data - data.mean()) / data.std() # 用整条序列的均值、标准差归一化:测试段也参与了统计,不是严格只用过去数据(规范做法只用训练段算)

def esn(in_scale=0.5, ridge=1e-4, N=400, leak=0.3, rho=0.9):
torch.manual_seed(0) # 三种设置用同一个随机储备池
W_in = (torch.rand(N) - 0.5) * in_scale # 输入缩放
bias = (torch.rand(N) - 0.5) * in_scale
W = torch.rand(N, N) - 0.5
W[torch.rand(N, N) > 0.1] = 0 # 稀疏随机连接
W *= rho / torch.linalg.eigvals(W).abs().max() # 谱半径缩放到 0.9
step = lambda x, u: (1 - leak) * x + leak * torch.tanh(W_in * u + W @ x + bias)

x, states = torch.zeros(N), []
for u in data[:-1]:
x = step(x, u)
states.append(x)
X, Y = torch.stack(states), data[1:] # X[j]:读入 data[j] 后的储备池状态 (T, N);Y[j] = data[j + 1]:下一个时刻的值
feats = lambda X, U: torch.cat([torch.ones(len(X), 1), U[:, None], X], 1) # 读出层的输入:[1, 当前输入, 状态]
washout, split = 200, 2500 # 前 200 步是过渡期,丢掉
Ftr = feats(X[washout:split], data[washout:split])
W_out = torch.linalg.solve(Ftr.T @ Ftr + ridge * torch.eye(Ftr.size(1)), Ftr.T @ Y[washout:split]) # 岭回归,只训练这一层
pred = feats(X[split:], data[split:-1]) @ W_out
nrmse = (((pred - Y[split:]) ** 2).mean().sqrt() / Y[split:].std()).item()

x, u, outs = X[split - 1], data[split], [] # 自由生成:从训练段最后一个已知点出发,之后把自己的预测当下一步输入
for _ in range(300):
x = step(x, u)
u = torch.cat([torch.ones(1), u.view(1), x]) @ W_out
outs.append(u)
err = (torch.stack(outs) - data[split + 1:split + 301]).abs() # 第 k 步预测 data[split + k],和单步测试一样从 data[split + 1] 起,都没参与拟合
return nrmse, err[:20].mean().item(), err[80:100].mean().item(), err[200:].mean().item()

fmt = lambda e: f"{e:.3f}" if math.isfinite(e) else "∞"
for in_scale, ridge, note in [(0.5, 1e-4, "默认设置"), (1.0, 1e-4, "输入缩放 0.5 → 1.0"), (0.5, 1e-6, "岭系数 1e-4 → 1e-6")]:
nrmse, e1, e2, e3 = esn(in_scale, ridge)
print(f"{note}:单步预测 NRMSE {nrmse:.4f};自由生成误差 第 1–20 步 {fmt(e1)},"
f"第 81–100 步 {fmt(e2)},第 201–300 步 {fmt(e3)}(数据标准差 1)")
<center>ESN混沌序列预测
ESN混沌序列预测
<center>ESN混沌序列预测
ESN混沌序列预测

单步预测几乎和真实序列重合;自由生成时每一步的误差都会带进下一步,300 步内误差缓慢累积,仍然跟得住混沌序列的形状。

常见说法与实测

常见说法:ESN 只训练线性读出层,训练就是一次岭回归,简单、快、稳定;只要谱半径小于 1,储备池就有回声状态性质(后半句是常见误解,见上文)。

实测:同一个随机储备池,三种设置的单步预测都很好(NRMSE 0.009–0.013),自由生成却差别巨大:默认设置(输入缩放 0.5、岭系数 1e-4)300 步内误差都在 0.1 以内;输入缩放改成 1.0,自由生成发散到无穷;岭系数改成 1e-6,单步预测反而最准(0.0090),自由生成第 81–100 步的平均误差到 18。两种预测都从训练段之后的第一个点 data[2501] 开始比较。

怎么理解:「训练简单」说的是求解读出层那一步:一次线性求解,不用梯度迭代;它在数值上稳不稳,要看条件数、精度、岭系数和求解器(见本段最后)。难点在超参。单步预测每一步都喂真实输入,误差不会累积;自由生成把自己的输出当下一步输入,读出层的小误差经过储备池一遍遍放大,正则太弱(读出层过拟合)或者输入缩放太大,都可能让这个闭环失稳。单步预测好,不能说明自由生成也好。另外两件事要分开看:谱半径小于 1 本身保证不了回声状态性质;即使储备池有这个性质,自由生成时输出反馈回输入形成闭环,闭环稳不稳定还要单独判断。另外,发散也可能部分来自数值条件:正规方程 FᵀF 的条件数很大、岭系数又很小时,求解本身就不稳定(换 float64、QR 或 SVD 会更稳),这里没有单独排除。

典型例子

  • 混沌时间序列预测:Jaeger 与 Haas 2004 年在 Science 上用 ESN 预测 Mackey-Glass 序列,误差比此前方法小几个数量级
  • 无线信道均衡、语音特征的时序分类
  • 物理储备池计算:光学、自旋电子、软体机器人身体
  • 优势:训练只是一次岭回归,秒级完成;没有 BPTT 和梯度消失爆炸的问题;储备池可以用任何有衰减记忆的物理系统实现
  • 局限:储备池随机固定,表达能力有限,大规模任务上不如 LSTM、Transformer;对谱半径、泄漏率、输入缩放、正则系数很敏感,尤其是自由生成(上面的例子里输入缩放从 0.5 改成 1.0,自由生成前 20 步的平均误差就到了 2.0,数据标准差才是 1,第 81–100 步已经发散到无穷);储备池要很大
  • 适合的数据:低维连续时间序列的预测与系统辨识:混沌系统、信号均衡、传感器流;物理或类脑硬件上的时序计算

2.14.2 ELM 极限学习机 〔图:ELM〕

<center>2.14.2 ELM
2.14.2 ELM
<center>2.14.2 ELM
2.14.2 ELM

一句话

单隐层前馈网络,输入到隐藏层的权重随机生成后固定,只用最小二乘一次解出隐藏层到输出的权重。

图中 ELM 是输入层到隐藏层之间随机连线的前馈网络。黄广斌等 2004 年以 ELM 之名提出(2006 年的期刊版最常被引用);同样的思想更早有 Pao 1994 年的随机向量函数连接网络(RVFL),后来又有 Rahimi 与 Recht 2007 年的随机特征(2008 年的后续工作叫 random kitchen sinks),学界对命名和原创性有过争议。

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 time
import torch

class ELM:
def __init__(self, in_dim, n_hidden, ridge=1e-3):
self.W = torch.randn(in_dim, n_hidden) # 随机生成,永不训练
self.b = torch.randn(n_hidden)
self.ridge = ridge

def hidden(self, X):
return torch.tanh(X @ self.W + self.b)

def fit(self, X, Y): # 只解一个线性最小二乘
H = self.hidden(X)
self.beta = torch.linalg.solve(H.T @ H + self.ridge * torch.eye(H.size(1)), H.T @ Y)
return self

def predict(self, X):
return self.hidden(X) @ self.beta

torch.manual_seed(0)
X = torch.rand(2000, 2) * 4 - 2
Y = torch.sin(2 * X[:, :1]) * torch.cos(2 * X[:, 1:]) # 二维非线性曲面
Xtr, Ytr, Xte, Yte = X[:1500], Y[:1500], X[1500:], Y[1500:]

t0 = time.time()
elm = ELM(2, 300).fit(Xtr, Ytr)
print(f"ELM:训练 {1000 * (time.time() - t0):.1f} ms,测试 MSE {((elm.predict(Xte) - Yte) ** 2).mean():.5f}")

t0 = time.time()
mlp = torch.nn.Sequential(torch.nn.Linear(2, 64), torch.nn.Tanh(), torch.nn.Linear(64, 1))
opt = torch.optim.Adam(mlp.parameters(), lr=1e-2)
for _ in range(2000):
loss = ((mlp(Xtr) - Ytr) ** 2).mean()
opt.zero_grad(); loss.backward(); opt.step()
print(f"反向传播 MLP:训练 {1000 * (time.time() - t0):.0f} ms,测试 MSE {((mlp(Xte) - Yte) ** 2).mean().item():.5f}")

典型例子

  • 嵌入式和在线场景的快速分类:肌电(EMG)、脑电(EEG)信号的实时识别
  • 工业过程的软测量
  • 在线序列 ELM:数据流到来时增量更新输出权重
  • 优势:训练极快,没有学习率和迭代;λ > 0 时是严格凸问题,解唯一(不加正则时要看 H 是否列满秩,秩不足就用伪逆取最小范数解);本例要调的主要是隐藏层宽度和正则系数(随机权重的尺度、激活函数也会影响结果)
  • 局限:隐藏层不学习,需要比反向传播网络多得多的隐藏单元;不能学特征,对图像等高维原始数据效果差;隐藏单元数 L 很大时,求解 L×L 的 HᵀH 又慢又占内存(样本多时 H 可以分块累加成 HᵀH)
  • 适合的数据:中小规模的低维特征向量,对训练速度要求高的场合:嵌入式在线学习、生理信号快速分类

2.14.3 LSM 液态机 〔图:LSM〕

<center>2.14.3 LSM
2.14.3 LSM
<center>2.14.3 LSM
2.14.3 LSM

一句话

储备池换成随机连接的脉冲神经元:输入像石子投进液体激起涟漪,某一时刻「液体」的状态记录了最近一段输入的历史;只训练线性读出层。

图中 LSM 的隐藏单元是「脉冲隐藏单元」(带三角的绿色圆),随机连接。Maass、Natschläger、Markram 2002 年提出,和 ESN 几乎同时独立出现:ESN 来自机器学习,LSM 来自计算神经科学,后者更关心生物合理性。

结构:输入脉冲 → 随机连接的 LIF 神经元群(不训练)→ 在若干时间窗内统计每个神经元的发放数作为特征 → 线性读出(岭回归或逻辑回归)。

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
import torch

torch.manual_seed(0)
C_in, N, T = 20, 200, 100 # 输入通道、储备池神经元、时间步
W_in = (torch.rand(N, C_in) < 0.2).float() * torch.rand(N, C_in) * 1.5
W_rec = (torch.rand(N, N) < 0.1).float() * torch.randn(N, N) * 0.4 # 随机稀疏的循环连接,不训练

def reservoir(spikes_in, beta=0.9, v_th=1.0):
"""spikes_in: (T, C_in) → 储备池的脉冲 (T, N)"""
v, s, out = torch.zeros(N), torch.zeros(N), []
for t in range(spikes_in.size(0)):
v = beta * v + W_in @ spikes_in[t] + W_rec @ s # LIF:泄漏 + 输入 + 循环
s = (v > v_th).float()
v = v * (1 - s) # 发放后清零
out.append(s)
return torch.stack(out)

templates = (torch.rand(3, T, C_in) < 0.05).float() # 3 类时空脉冲模板
def sample(cls):
x = templates[cls].clone()
shift = torch.randint(-2, 3, (1,)).item()
x = torch.roll(x, shift, 0) # 时间抖动
x = torch.clamp(x + (torch.rand(T, C_in) < 0.01).float(), 0, 1) # 额外的噪声脉冲
return x

def features(x, n_bins=5): # 读出特征:5 个时间窗内各神经元的发放数
return reservoir(x).view(n_bins, T // n_bins, N).sum(1).flatten()

ys_tr = torch.arange(3).repeat(40); ys_te = torch.arange(3).repeat(100)
F_tr = torch.stack([features(sample(c)) for c in ys_tr])
F_te = torch.stack([features(sample(c)) for c in ys_te])
Y = torch.nn.functional.one_hot(ys_tr, 3).float()
Wr = torch.linalg.solve(F_tr.T @ F_tr + 1.0 * torch.eye(F_tr.size(1)), F_tr.T @ Y) # 只训练线性读出
acc = ((F_te @ Wr).argmax(1) == ys_te).float().mean().item()
print(f"3 类时空脉冲模式的分类准确率 {acc:.3f};储备池平均发放率 {F_tr.mean() / (T // 5):.3f}")

典型例子

  • 口语数字识别:脉冲储备池 + 线性读出(Verstraeten 2005)
  • 计算神经科学:作为皮层微环路的计算模型
  • 神经形态硬件上的时序分类
  • 优势:只训练线性读出,训练快、是凸问题;事件触发,适合神经形态芯片低功耗运行;天然处理连续的时间输入流
  • 局限:仿真要小时间步积分,在普通硬件上慢;储备池超参(连接密度、权重尺度、时间常数)全靠调;精度通常不如端到端训练的网络
  • 适合的数据:脉冲和事件流:事件相机、事件式触觉、神经信号;类脑芯片上的时序分类

2.15 其他经典网络

2.15.1 SVM 支持向量机 〔图:SVM〕

<center>2.15.1 SVM
2.15.1 SVM
<center>2.15.1 SVM
2.15.1 SVM

一句话

找一个让两类之间「间隔」最大的超平面;借助核函数,可以在高维特征空间里画线性边界,对应原空间里的非线性边界。

图里 SVM 被画成一个小的前馈结构,作者也说明它严格说不算神经网络。Boser、Guyon、Vapnik 1992 年引入核方法,Cortes 与 Vapnik 1995 年提出软间隔。

软间隔 SVM 的优化目标(合页损失 + L2 正则):

间隔等于 2/‖w‖,最小化 ‖w‖ 就是最大化间隔。合页损失对 yᵢ(wᵀxᵢ + b) ≥ 1 的样本(分对且离边界足够远)为 0,最终的解只由 yᵢ(wᵀxᵢ + b) ≤ 1 的样本决定:正好落在间隔边界上的、进到间隔里的,以及分错的。严格地说,支持向量是对偶解里系数 αᵢ > 0 的样本,它们都在这个范围里;反过来,正好落在间隔边界上的样本也可能 αᵢ = 0(多余的边界点),所以按这个不等式筛出来的是支持向量的候选范围。两类重叠多时,支持向量可以占样本的很大一部分。

核技巧:对偶问题里数据只以内积 xᵢᵀxⱼ 的形式出现,把它换成核函数 k(xᵢ, xⱼ) 就等于在一个高维特征空间里做线性 SVM,而不用显式算出那个空间。最常用的 RBF 核:

和神经网络的关系:线性 SVM 等于「单层线性网络 + 合页损失 + 权重衰减」;RBF 核 SVM 的决策函数和 RBF 网络(2.4.5)形式相同,中心取在支持向量上;随机傅里叶特征可以把核 SVM 近似成「随机特征 + 线性分类器」,和 ELM(2.14.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
import torch

def hinge_svm(X, y, epochs=500, lam=1e-3, lr=0.1):
w = torch.zeros(X.shape[1], requires_grad=True)
b = torch.zeros(1, requires_grad=True)
opt = torch.optim.SGD([w, b], lr=lr)
for _ in range(epochs):
loss = torch.clamp(1 - y * (X @ w + b), min=0).mean() + lam / 2 * w @ w # 合页损失 + L2
opt.zero_grad(); loss.backward(); opt.step()
return w.detach(), b.detach()

torch.manual_seed(0)
n = 400 # 同心圆:线性分不开
r = torch.cat([torch.rand(n // 2), 1.5 + torch.rand(n // 2)])
th = torch.rand(n) * 2 * torch.pi
X = torch.stack([r * torch.cos(th), r * torch.sin(th)], 1)
y = torch.cat([-torch.ones(n // 2), torch.ones(n // 2)])

w, b = hinge_svm(X, y)
print("线性 SVM 训练集准确率:", (torch.sign(X @ w + b) == y).float().mean().item())

# RBF 核的近似:随机傅里叶特征 φ(x),满足 φ(x)·φ(x') ≈ k(x, x')
gamma, D = 1.0, 300
Omega = torch.randn(2, D) * (2 * gamma) ** 0.5
phase = torch.rand(D) * 2 * torch.pi
phi = lambda X: (2 / D) ** 0.5 * torch.cos(X @ Omega + phase)
print("核近似误差:", (phi(X[:5]) @ phi(X[:5]).T - torch.exp(-gamma * torch.cdist(X[:5], X[:5]) ** 2)).abs().max().item())
w, b = hinge_svm(phi(X), y, lr=0.5)
print("RBF 核 SVM(随机特征近似)训练集准确率:", (torch.sign(phi(X) @ w + b) == y).float().mean().item())
margin = y * (phi(X) @ w + b)
print("y·f(x) ≤ 1 的样本数(在间隔上、间隔内或被分错,≈ 支持向量;SGD 只跑了固定轮数,没检查是否到最优):", (margin <= 1.0 + 1e-3).sum().item(), "/", n)
# 实际项目直接用 sklearn.svm.SVC(kernel="rbf"):用精确的核(不做随机特征近似),按设定的容差解对偶问题,支持向量从 support_ 读
<center>SVM线性与核方法
SVM线性与核方法
<center>SVM线性与核方法
SVM线性与核方法

典型例子

  • 文本分类:TF-IDF 特征 + 线性 SVM,垃圾邮件、情感分类的强基线
  • 行人检测:HOG 特征 + SVM(Dalal & Triggs 2005)
  • 手写数字:深度学习之前的最好方法之一
  • R-CNN(2014):用 CNN 提特征,再用 SVM 分类
  • 生物信息:蛋白质分类、基因表达数据分类
  • 优势:最大间隔,泛化好,小样本上稳;凸优化,没有非全局的局部最优(w 通常唯一,偏置 b 不一定唯一);核技巧处理非线性;解只依赖支持向量
  • 局限:核方法训练复杂度大约是样本数的平方到三次方,十万样本以上就很慢;核函数和超参(C、γ)要调;不学特征表示;原生只输出类别,没有概率
  • 适合的数据:几百到几万个样本的固定长度特征向量:手工特征(HOG、TF-IDF)、高维稀疏文本、生物信息特征;深度特征之上的分类器

2.15.2 SOM 自组织映射(Kohonen 网络)〔图:KN〕

<center>2.15.2 SOM
2.15.2 SOM
<center>2.15.2 SOM
2.15.2 SOM

一句话

一张二维网格上的神经元互相竞争,离输入最近的获胜者和它在网格上的邻居一起向输入靠拢;训练完,高维数据被映射到网格上,且相似的数据落在相邻的格子里。

图中 KN 是输入连到一片没有输出层的神经元。Kohonen 1982 年提出,属于竞争学习(1.10)。

训练:每来一个输入 x:

  1. 找获胜单元(BMU):c = argmin_j ‖x − w_j‖
  2. 获胜者和它在网格上的邻居一起更新:

r 是单元在网格上的坐标,学习率 η 和邻域半径 σ 随训练逐渐缩小:先粗略地铺开,再精细地调整。

拓扑保持:邻居一起更新,使得网格上相邻的单元对应输入空间里相近的区域。SOM 因此既是聚类(每个单元是一个原型),也是可视化工具(把高维数据摊在一张二维地图上)。

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

torch.manual_seed(0)
H, W, D = 10, 10, 3
weights = torch.rand(H * W, D) # 每个网格单元一个 3 维权重(RGB)
grid = torch.stack(torch.meshgrid(torch.arange(H), torch.arange(W), indexing="ij"), -1).view(-1, 2).float()
colors = torch.rand(3000, 3) # 训练数据:随机颜色

n_iter = 5000
for t in range(n_iter):
x = colors[torch.randint(0, len(colors), (1,))]
bmu = ((weights - x) ** 2).sum(1).argmin() # 1) 找最像的单元
frac = t / n_iter
lr, sigma = 0.5 * (1 - frac) + 0.01, 4 * (1 - frac) + 0.5 # 学习率、邻域半径逐渐缩小
h = torch.exp(-((grid - grid[bmu]) ** 2).sum(1) / (2 * sigma ** 2))[:, None] # 2) 网格上越近更新越多
weights += lr * h * (x - weights) # 3) 向输入靠拢

w = weights.view(H, W, D)
neighbor = ((w[1:] - w[:-1]).norm(dim=-1).mean() + (w[:, 1:] - w[:, :-1]).norm(dim=-1).mean()) / 2
random_pair = (weights[torch.randperm(H * W)] - weights).norm(dim=1).mean()
print(f"网格上相邻单元的平均色差 {neighbor:.3f},随机两单元 {random_pair:.3f}(相邻的颜色相近 = 拓扑保持)")
quant_err = torch.cdist(colors, weights).min(1).values.mean()
print(f"量化误差(每个颜色到最近原型的平均距离){quant_err:.3f}")
<center>SOM颜色映射
SOM颜色映射
<center>SOM颜色映射
SOM颜色映射

典型例子

  • 高维数据可视化:世界各国生活水平地图、基因表达谱聚类
  • Kohonen 1988 年的「音素打字机」:芬兰语语音识别
  • 客户分群、传感器数据的工况聚类
  • 机器人导航中的拓扑地图
  • 优势:无监督;把高维数据映射到二维,倾向于保留局部的邻近关系(不保证完整保持拓扑),便于可视化;结果可解释(每个单元是一个原型)
  • 局限:网格大小、衰减日程需要人为设定;没有明确的目标函数;在大规模高维数据的可视化上,t-SNE、UMAP 更常用
  • 适合的数据:中等维度的连续向量的聚类与可视化:颜色、传感器特征、基因表达谱、用户画像

2.15.3 NTM 神经图灵机 〔图:NTM〕

<center>2.15.3 NTM
2.15.3 NTM
<center>2.15.3 NTM
2.15.3 NTM

一句话

一个控制器网络(LSTM 或 MLP)加一块外部记忆矩阵,用可微的「软」读写头访问记忆;整个系统可以端到端训练,学会复制、排序这类简单算法。

图中 NTM 的控制器下方挂着一排「记忆单元」,就是外部记忆。Graves、Wayne、Danihelka 2014 年提出。

读:读头输出一个在 N 个记忆槽上的权重分布 w_t(和为 1),读出的是加权平均:

写:先按权重擦除一部分(擦除向量 e),再加上新内容(添加向量 a):

寻址(权重 w 怎么来),四步:

  1. 按内容:控制器给出一个键 k 和锐度 β,与每个记忆槽算余弦相似度后做 softmax
  1. 插值:用门 g 在「按内容的结果」和「上一步的权重」之间插值
  2. 移位:用一个小卷积核把权重向前或向后挪一格(实现「下一个位置」)
  3. 锐化:权重取 γ 次方再归一化,防止移位后变模糊

按内容寻址就是注意力;位置寻址让它能像指针一样顺序读写。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import torch
import torch.nn as nn
import torch.nn.functional as F

class NTM(nn.Module):
"""LSTM 控制器 + 一个读头 + 一个写头;寻址包含内容、插值、移位、锐化四步"""
def __init__(self, in_dim, out_dim, ctrl=100, N=16, Mdim=12):
super().__init__()
self.N, self.Md = N, Mdim
self.ctrl = nn.LSTMCell(in_dim + Mdim, ctrl)
n_addr = Mdim + 1 + 1 + 3 + 1 # 键 k、锐度 β、插值门 g、移位 s(3)、锐化 γ
self.read_head = nn.Linear(ctrl, n_addr)
self.write_head = nn.Linear(ctrl, n_addr + 2 * Mdim) # 外加擦除向量 e、添加向量 a
self.out = nn.Linear(ctrl + Mdim, out_dim)

def address(self, params, w_prev, M):
k, beta, g, s, gamma = params.split([self.Md, 1, 1, 3, 1], -1)
beta, g = F.softplus(beta), torch.sigmoid(g)
s, gamma = F.softmax(s, -1), 1 + F.softplus(gamma)
wc = F.softmax(beta * F.cosine_similarity(M, k[:, None], dim=-1), -1) # 1) 按内容
wg = g * wc + (1 - g) * w_prev # 2) 插值
ws = s[:, :1] * torch.roll(wg, -1, 1) + s[:, 1:2] * wg + s[:, 2:] * torch.roll(wg, 1, 1) # 3) 循环移位
w = ws ** gamma # 4) 锐化
return w / (w.sum(-1, keepdim=True) + 1e-8)

def forward(self, x): # x: (T, B, in_dim)
T, B, _ = x.shape
M = torch.full((B, self.N, self.Md), 1e-6)
h = c = torch.zeros(B, self.ctrl.hidden_size)
r = torch.zeros(B, self.Md)
w_r = w_w = F.one_hot(torch.zeros(B, dtype=torch.long), self.N).float()
outs = []
for t in range(T):
h, c = self.ctrl(torch.cat([x[t], r], -1), (h, c))
wp = self.write_head(h)
w_w = self.address(wp[:, :self.Md + 6], w_w, M)
e, a = torch.sigmoid(wp[:, self.Md + 6:2 * self.Md + 6]), wp[:, 2 * self.Md + 6:]
M = M * (1 - w_w[..., None] * e[:, None]) + w_w[..., None] * a[:, None] # 先擦后写
w_r = self.address(self.read_head(h), w_r, M)
r = (w_r[..., None] * M).sum(1) # 加权读出
outs.append(self.out(torch.cat([h, r], -1)))
return torch.stack(outs)

def copy_task(B, L, bits=6):
"""复制任务:先看一段随机二值向量,遇到结束符后把它原样输出"""
seq = torch.randint(0, 2, (L, B, bits)).float()
x = torch.zeros(2 * L + 1, B, bits + 1)
x[:L, :, :bits] = seq
x[L, :, bits] = 1 # 结束符
return x, seq

torch.manual_seed(0)
model = NTM(7, 6)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for step in range(3000):
L = torch.randint(1, 6, (1,)).item()
x, seq = copy_task(32, L)
out = model(x)[L + 1:] # 只看输出阶段
loss = F.binary_cross_entropy_with_logits(out, seq)
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 10)
opt.step()
if step % 1000 == 999:
with torch.no_grad():
x, seq = copy_task(200, 5)
err = ((torch.sigmoid(model(x)[6:]) > 0.5).float() != seq).float().mean().item()
print(f"第 {step + 1} 步:长度 5 的复制任务,每个比特的错误率 {err:.3f}(瞎猜 0.5)")
# 错误率在稳步下降;原论文训练了数十万条序列才完全学会复制,这里只演示机制和学习趋势

典型例子

  • 原论文:学会复制、重复复制、联想回忆、排序等简单算法,并能推广到比训练时更长的序列
  • 可微神经计算机 DNC(Nature 2016):在伦敦地铁线路图上找路线、在家谱图上推断亲属关系
  • 记忆增强网络的少样本学习(Santoro 2016)
  • 思想延续:注意力就是可微的按内容寻址;检索增强生成(RAG)、带外部记忆的智能体都是「计算与存储分离」的延续
  • 优势:计算与存储分离,记忆容量不受隐状态大小限制;读写可微,能端到端学会简单算法;在复制类任务上能推广到更长的序列
  • 局限:训练不稳定、收敛慢、对超参敏感;实现复杂;实际任务上已被 Transformer 和检索增强取代
  • 适合的数据:需要显式读写外部存储的序列任务:算法学习、图上的推理、少样本快速绑定

2.15.4 胶囊网络 CapsNet

<center>2.15.4 胶囊网络
2.15.4 胶囊网络
<center>2.15.4 胶囊网络
2.15.4 胶囊网络

一句话

用一个向量(胶囊)代替一个标量神经元:向量的长度表示「这个实体存在的概率」,方向表示它的姿态;低层胶囊通过「动态路由」投票决定激活哪个高层胶囊。

Sabour、Frosst、Hinton 2017 年提出。动机:CNN 的池化丢掉了部件之间的精确空间关系,一张五官位置被打乱的脸也可能被认成脸。

squash 非线性:保持方向,把长度压到 0 到 1 之间:

动态路由:低层胶囊 i 用变换矩阵预测高层胶囊 j 的姿态 û_{j|i} = W_ij u_i;高层胶囊的输入是这些预测的加权和 s_j = Σ_i c_ij û_{j|i},耦合系数 c_ij = softmax_j(b_ij)。迭代几次,每次把「预测与结果的一致程度」加到 b_ij 上:

预测一致的低层胶囊会把更多的票投给同一个高层胶囊,相当于「部件的姿态互相吻合,才认定整体存在」。

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
import torch
import torch.nn as nn

def squash(s, dim=-1):
n2 = (s ** 2).sum(dim, keepdim=True)
return n2 / (1 + n2) * s / (n2.sqrt() + 1e-8) # 方向不变,长度压到 (0, 1)

class CapsLayer(nn.Module):
def __init__(self, n_in, d_in, n_out, d_out, iters=3):
super().__init__()
self.W = nn.Parameter(0.01 * torch.randn(1, n_in, n_out, d_out, d_in))
self.iters = iters

def forward(self, u): # u: (B, n_in, d_in) 低层胶囊
u_hat = (self.W @ u[:, :, None, :, None]).squeeze(-1) # (B, n_in, n_out, d_out):对每个高层胶囊的预测
b = torch.zeros(u_hat.shape[:3])
for _ in range(self.iters): # 动态路由
c = b.softmax(dim=2) # 每个低层胶囊把票分给各个高层胶囊
v = squash((c[..., None] * u_hat).sum(1)) # (B, n_out, d_out)
b = b + (u_hat * v[:, None]).sum(-1) # 预测和结果一致 → 下一轮多投票
return v

class CapsNet(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Sequential(nn.Conv2d(1, 64, 9), nn.ReLU()) # 28 → 20
self.primary = nn.Conv2d(64, 16 * 8, 9, stride=2) # 20 → 6:16 种 8 维初级胶囊
self.digits = CapsLayer(16 * 6 * 6, 8, 10, 16) # 10 个 16 维数字胶囊

def forward(self, x): # x: (B, 1, 28, 28)
p = self.primary(self.conv(x)) # (B, 128, 6, 6)
p = p.view(x.size(0), 16, 8, 6, 6).permute(0, 1, 3, 4, 2).reshape(x.size(0), -1, 8) # (B, 576, 8)
v = self.digits(squash(p))
return v.norm(dim=-1) # 每个数字胶囊的长度 = 该类存在的概率

model = CapsNet()
print("输出:", tuple(model(torch.randn(2, 1, 28, 28)).shape)) # (2, 10)
print(f"参数量: {sum(p.numel() for p in model.parameters()):,}(大头在胶囊之间的变换矩阵)")

典型例子

  • MultiMNIST:两个数字大面积重叠时,CapsNet 能把它们分开识别
  • smallNORB:带 EM 路由的矩阵胶囊(2018)对未见过的视角泛化更好
  • 一些医学影像的小规模实验
  • 优势:用向量表示姿态,部件与整体的空间关系显式建模;对视角变化的泛化更好;重叠物体分离效果好
  • 局限:动态路由的迭代计算昂贵,难以扩展到大图像和深层网络;在 ImageNet 等大规模任务上没有超过 CNN 和 ViT;后续发展基本停滞,主要作为「等变表示」思想的一个分支来了解
  • 适合的数据:小尺寸、部件空间关系和视角变化重要的图像