当研究者把"二进制加法"这个小学算术题交给一个小小的前馈神经网络时,意外发生了:模型并没有死记硬背,而是自发"长"出了类似半加器 / 全加器 的内部结构。把它剖开看,就像用示波器探针扎进电路板:某些隐层单元趋近 XOR (求和位),另一些单元近似 AND/OR(进位位)。这篇文章尝试用一种直白、可复现实验的方式,把这段"神经网络学会加法"的故事讲清楚,并把"它是怎么做到的"一步步逆向出来。
从"背答案"到"学结构"
把二进制加法写成逐位相加、向高位传播进位 的过程,本质就是一个有记忆的递推:
- 当前位的和:( s_i = a_i \oplus b_i \oplus c_i )
- 下一位的进位:( c_{i+1} = \text{majority}(a_i, b_i, c_i) )
对神经网络而言,最直接的难点是 XOR 的非线性 和 进位传播的"长依赖"。然而在足够数据与适当正则之下,一个小巧的网络(甚至无循环,仅依赖输入展开)也能学会把进位"在隐层里搬运"。逆向后的结论通常有两点:
- 隐层的一组方向实现了XOR 近似(权重呈对称正负,激活在 01 边界处翻转)。
- 另一组方向实现了AND/OR 近似(进位检测与累加),并在更高层做"门控式"混合,形成"可传播"的进位信号。
一个可复现实验:让网络自己悟出加法
下面给出一个可运行的最小实验:把两个固定长度的二进制数拼接后喂给多层感知机(MLP),输出它们的和。实验重点不在 SOTA,而在可逆向。
python
# 二进制加法的可复现实验(PyTorch)
import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(0)
BITS = 8 # 每个数 8 位
IN = BITS * 2
OUT = BITS + 1 # 可能产生最高位进位
class AdderMLP(nn.Module):
def __init__(self, hid=32):
super().__init__()
self.fc1 = nn.Linear(IN, hid)
self.fc2 = nn.Linear(hid, hid)
self.fc3 = nn.Linear(hid, OUT)
def forward(self, x):
x = self.fc1(x); x = torch.tanh(x)
x = self.fc2(x); x = torch.tanh(x)
return torch.sigmoid(self.fc3(x)) # 输出位的概率
def int_to_bits(n, k):
return [(n >> i) & 1 for i in range(k)]
# 生成训练数据
N = 50000
a = torch.randint(0, 2**BITS, (N,))
b = torch.randint(0, 2**BITS, (N,))
X = torch.tensor([int_to_bits(ai, BITS)+int_to_bits(bi, BITS) for ai,bi in zip(a,b)], dtype=torch.float32)
y_int = a + b
Y = torch.tensor([int_to_bits(yi, OUT) for yi in y_int], dtype=torch.float32)
model = AdderMLP(32)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for _ in range(40):
opt.zero_grad()
p = model(X)
loss = F.binary_cross_entropy(p, Y)
loss.backward()
opt.step()
# 简单评估
with torch.no_grad():
p = (model(X) > 0.5).int()
acc = (p == Y.int()).float().mean().item()
print("bit-wise accuracy:", round(acc, 4))
PyTorch 文档 :https://pytorch.org/
上面这段代码经常能在很短的训练后达到接近 100% 的逐位准确率。但真正有趣的并不是"它会了",而是"它内部怎么会的"。
逆向:把"黑箱"掀开一角
把第一层权重矩阵 (W_1) 抠出来,观察每个隐层神经元对输入位的敏感性。一个极为常见的画面是:
- 某些单元对 ([a_i, b_i]) 呈对称权重 (如 ([+w, -w]) 与 ([-w, +w]) 的组合),在 ((0,1))、((1,0)) 处激活最高,近似 XOR。
- 另一些单元对 ([a_i, b_i]) 都给正权(或再叠加对 (c_i) 的正权),在 ((1,1)) 或"任两者为 1"时更活跃,近似 AND/OR/majority。
- 第二层把这些"局部门电路"的信号线性混合,再由非线性折叠,得到稳定的和位 / 进位位分布。
示意性可视化(伪代码):
python
W1 = model.fc1.weight.detach().clone() # [H, IN]
H = W1.shape[0]
# 每两位(a_i, b_i)对应输入的两个列索引
def pair_cols(i): return (i, i+BITS)
scores = []
for h in range(H):
# 衡量"XOR-like":对 a_i 与 b_i 权重接近对称且幅度大
xor_like = []
for i in range(BITS):
ia, ib = pair_cols(i)
wa, wb = W1[h, ia].item(), W1[h, ib].item()
xor_like.append(abs(wa + wb) - abs(wa - wb)) # 越负越像 XOR
scores.append((h, sum(xor_like)/BITS))
scores = sorted(scores, key=lambda x: x[1]) # 越靠前越像 XOR
top_xor_neurons = [h for h,_ in scores[:5]]
print("XOR-like neurons (sample):", top_xor_neurons)
这类分数粗陋却有效:XOR-like 单元往往排在前列;相反,如果把"对同位的两输入都给正权且对进位列也给正权"的单元打分,就会捕捉到更像 AND/OR/majority 的那批。
NumPy 官网 :https://numpy.org/
为什么会学到"电路感"的表示?
原因并不神秘。二进制加法的数据流 本身就要求"把不同位的 XOR 与 进位逻辑组合起来",而标准的全连接网络在足够样本覆盖下会倾向寻找可分、可复用的特征方向:
- XOR-like 方向帮助在"同位相加"的判别边界上做出清晰决策;
- 进位方向捕捉高阶交互(majority),并在高层形成"门控传递";
- 训练过程中的权重衰减 与初始化对称性,又鼓励了这些"对称---反对称"的分解。
在更长位宽的实验里,还能观察到:中间层将来自低位的进位"压扁"为更线性的通道,再把它送到高位相关的单元上。这个通道不必显式对齐输入位序,但其激活模式与ripple-carry十分相似。
arXiv(机器学习论文预印本) :https://arxiv.org/
更进一步:从"看懂"到"可控"
理解之后,就能"施加可控性"。两个简单方向:
-
可解释训练目标
在损失中加入对"隐层门电路"的轻微约束(如对某些权重对称性加正则),可让网络更快、更稳地收敛到"像电路"的解,从而便于部署到资源受限的场景(FPGA/MCU 上的表驱动近似)。
-
蒸馏到规则
把隐层的 XOR/AND 方向投影出来,再用可微门电路 或布尔张量网络重建一个"显式电路",这样部署时的能耗和延迟可预期,且方便形式化验证。
一个小结:黑箱并非不可读,尤其是当任务自带结构
二进制加法把"可读性"拱手送给了研究者:它既简单,又结构清晰。正因如此,神经网络在这类任务上的学习过程容易被逆向重构为近似电路 。当任务复杂一些(例如多位多数表决、可变长进位预测),类似的方法也能给出足够可工作的内部解释:先在权重里找"电路感"的对称和反对称,再观测中层激活的"传递通道",最后用可解释的模块去拟合它。
Scikit-learn 官网 :https://scikit-learn.org/。