神经元与逻辑门
Table of content
用多层神经元学习 XOR 逻辑门
从直线到分类
从最简单的线性模型 y = w * x + b 出发,我们可以拟合一条直线,预测一个数值。但现实中的问题不全是“预测数值”,很多时候是“做判断”:是或否、属于 A 还是属于 B。
逻辑门就是很典型的判断问题。
| x1 | x2 | OR | AND | NAND | XOR |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 | 1 |
| 0 | 1 | 1 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 |
我们想让机器自己从真值表中学会这些规则。
单个神经元
激活函数
如果直接用线性模型 y = w1*x1 + w2*x2 + b,输出可能是任意数值。但逻辑门的输出只能是 0 或 1,所以我们需要一个函数把输出“压”到 0 和 1 之间。
这就是 sigmoid 函数:
float sigmoidf(float x) {
return 1.f / (1.f + expf(-x));
}它的输出范围是 (0, 1)。输入越大,输出越接近 1;输入越小,输出越接近 0。
模型
单个神经元的计算过程是:
y = sigmoid(w1 * x1 + w2 * x2 + b)我们可以把它想象成一个“加权投票器”:
x1、x2是输入信号。w1、w2是它们的权重。b是偏置,决定“通过门槛”有多高。- sigmoid 决定最终输出是“接近 0”还是“接近 1”。
训练一个 OR 门
下面这段代码可以训练 OR 门。把 dataset 改成 and_gate_train_set 或 nand_gate_train_set,就可以训练 AND 或 NAND。
train_set or_gate_train_set[] = {
{0, 0, 0},
{0, 1, 1},
{1, 0, 1},
{1, 1, 1}
};
float cost_function(float w1, float w2, float b) {
float result = 0.0;
for (size_t i = 0; i < TRAIN_SIZE; i++) {
float x1 = dataset[i][0];
float x2 = dataset[i][1];
float y = sigmoidf(x1 * w1 + x2 * w2 + b);
float d = y - dataset[i][2];
result += d * d;
}
return result / TRAIN_SIZE;
}
int main(void) {
srand(time(0));
float w1 = random_float();
float w2 = random_float();
float b = random_float();
float eps = 1e-1;
float rate = 1e-1;
for (size_t i = 0; i < 1000000; i++) {
float cost = cost_function(w1, w2, b);
float dw1 = (cost_function(w1 + eps, w2, b) - cost) / eps;
float dw2 = (cost_function(w1, w2 + eps, b) - cost) / eps;
float db = (cost_function(w1, w2, b + eps) - cost) / eps;
w1 -= dw1 * rate;
w2 -= dw2 * rate;
b -= db * rate;
}
// ...
}训练完成后,对于任意输入 (x1, x2),模型输出的值都会非常接近真实结果:接近 0 表示“假”,接近 1 表示“真”。
XOR 门:一个神经元不够
为什么 XOR 难学?
OR、AND、NAND 都是线性可分的问题。你可以在平面上画一条直线,把输出为 0 的点和输出为 1 的点分开。
但 XOR 不行:
(0, 0)输出 0(0, 1)输出 1(1, 0)输出 1(1, 1)输出 0
这四个点无法被一条直线分开,所以单个神经元无论怎么调 w1、w2、b,都无法学会 XOR。
用三个门组合出 XOR
XOR 可以用更基本的门组合出来:
XOR(x1, x2) = AND(OR(x1, x2), NAND(x1, x2))验证一下:
| x1 | x2 | OR | NAND | AND(OR, NAND) = XOR |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 |
| 0 | 1 | 1 | 1 | 1 |
| 1 | 0 | 1 | 1 | 1 |
| 1 | 1 | 1 | 0 | 0 |
这就是标准的 XOR 真值表。
三层网络
代码里用一个结构体把三个门的参数打包在一起:
typedef struct {
float or_w1, or_w2, or_b;
float nand_w1, nand_w2, nand_b;
float and_w1, and_w2, and_b;
} Xor;前向传播过程是:
float forward(Xor m, float x1, float x2) {
float a = sigmoid(m.or_w1 * x1 + m.or_w2 * x2 + m.or_b);
float b = sigmoid(m.nand_w1 * x1 + m.nand_w2 * x2 + m.nand_b);
return sigmoid(m.and_w1 * a + m.and_w2 * b + m.and_b);
}- 第一层:一个 OR 神经元、一个 NAND 神经元。
- 第二层:一个 AND 神经元,把第一层的两个输出作为输入。
三个神经元组合起来,就能解决单个神经元无法解决的 XOR 问题。
训练
每个参数都要单独求梯度,代码里用了一个很直观的方法:把某个参数临时加 eps,算 cost 变化,再恢复。九个参数,就重复九次。
Xor finite_diff(Xor m, float eps) {
float c = cost_function(m);
Xor diff;
float saved = m.or_w1;
m.or_w1 += eps;
diff.or_w1 = (cost_function(m) - c) / eps;
m.or_w1 = saved;
// 对 or_w2, or_b, nand_w1, ... 重复同样操作
// ...
}
Xor train(Xor m, Xor d, float rate) {
m.or_w1 -= d.or_w1 * rate;
m.or_w2 -= d.or_w2 * rate;
// ...
return m;
}经过足够多轮迭代后,网络就能学会 XOR。
训练输出
下面是单个神经元模型分别训练 OR、AND、NAND 门的结果。输出值接近 0 表示“假”,接近 1 表示“真”。
OR 门:
c = 0.000024, w1 = 10.270295, w2 = 10.270295, b = -4.953744
0 | 0 = 0.007007
0 | 1 = 0.995114
1 | 0 = 0.995114
1 | 1 = 1.000000AND 门:
c = 0.000045, w1 = 9.651584, w2 = 9.651584, b = -14.621734
0 | 0 = 0.000000
0 | 1 = 0.006894
1 | 0 = 0.006894
1 | 1 = 0.990819NAND 门:
c = 0.000046, w1 = -9.651487, w2 = -9.651487, b = 14.521586
0 | 0 = 1.000000
0 | 1 = 0.992386
1 | 0 = 0.992386
1 | 1 = 0.008315可以看到,单个神经元经过训练后,确实能很好地模拟这三种门。
再看三层网络模型训练 XOR 的输出:
0 xor 0 = 0.986397
0 xor 1 = 0.010386
1 xor 0 = 0.988315
1 xor 1 = 0.012005XOR 被成功学会了。
一个有趣的对比
我们反过来想:如果把复杂的三层网络模型拿去训练 OR、AND、NAND,它还能学会吗?
答案是:能,而且同样学得很好。
复杂模型训练 OR:
cost = 0.000225
0 | 0 = 0.022891
0 | 1 = 0.986341
1 | 0 = 0.986541
1 | 1 = 0.997383复杂模型训练 AND:
cost = 0.000250
0 | 0 = 0.001256
0 | 1 = 0.015271
1 | 0 = 0.015834
1 | 1 = 0.977288复杂模型训练 NAND:
cost = 0.000259
0 | 0 = 0.998915
0 | 1 = 0.984271
1 | 0 = 0.984032
1 | 1 = 0.023106但是,如果把简单的单个神经元模型拿去训练 XOR,它完全学不会:
c = 0.250156, w1 = 0.000010, w2 = 0.000010, b = -0.050013
0 xor 0 = 0.487499
0 xor 1 = 0.487502
1 xor 0 = 0.487502
1 xor 1 = 0.487504四个输入的输出都徘徊在 0.5 附近,相当于模型“放弃”了,把所有情况都预测成 0.5。这是因为单个神经元无法画出 XOR 需要的非线性决策边界。
模型复杂度的力量
把上面的对比放在一起,结论非常清晰:
| 模型 | 能学会 OR/AND/NAND | 能学会 XOR |
|---|---|---|
| 单个神经元 | ✅ 能 | ❌ 不能 |
| 三层网络 | ✅ 能 | ✅ 能 |
这就出现了一个很有意思的现象:
为 XOR 准备的复杂模型,不仅能学会 XOR,也能轻松学会更简单的 OR、AND、NAND;但为简单门准备的单个神经元,却无法学会 XOR。
这说明:
- 模型复杂度决定了它能解决问题的上限。 单个神经元只能处理线性可分的问题;三层网络可以处理非线性问题。
- 复杂模型具有“向下兼容”的能力。 它的表达能力足够强,简单任务自然也能覆盖。
- 不是训练方法不够强,而是模型本身表达能力不够。 同样的梯度下降、同样的激活函数,模型复杂一点,就能从“完全不会”变成“完美学会”。
更神奇的发现
如果我们把 XOR 网络中间三个子门的输出也打印出来,会发现一个更有趣的现象:
OR (internal):
0 | 0 = 0.049883
0 | 1 = 0.940193
1 | 0 = 0.038892
1 | 1 = 0.923761
NAND (internal):
0 | 0 = 0.151169
0 | 1 = 0.782849
1 | 0 = 0.134311
1 | 1 = 0.758495
AND (internal):
0 | 0 = 0.991422
0 | 1 = 0.883753
1 | 0 = 0.056958
1 | 1 = 0.003957
XOR (output):
0 xor 0 = 0.981322
0 xor 1 = 0.011147
1 xor 0 = 0.983545
1 xor 1 = 0.013454中间层的输出并不总是严格对应我们人类熟悉的 OR、NAND、AND 真值表。但神奇的是,这三个子门组合在一起,最终依然能输出正确的 XOR。
这意味着:网络为了完成 XOR 任务,自己找到了一套有效的内部表示。每次训练随机初始化不同,内部表示也可能不同,但最终任务都能完成。简单的单元通过组合,涌现出了复杂的行为。
总结
我们从判断问题出发,比较了两种模型:
- 单个神经元:
y = sigmoid(w1 * x1 + w2 * x2 + b),能学会 OR、AND、NAND 这些线性可分的门,但无法学会 XOR。 - 三层网络:把多个神经元组合起来,形成更复杂的模型,既能学会 XOR,也能学会更简单的 OR、AND、NAND。
核心思想始终没变:
定义模型,计算误差,求梯度,沿着梯度反方向调整参数,让成本越来越小。
但随着模型复杂度提升,它能解决的问题也更多了。单个神经元只能画一条直线分开数据,三层网络可以画出更复杂的决策边界。简单的单元通过组合,就能涌现出让机器“学会”复杂规则的能力。