用多层神经元学习 XOR 逻辑门

从直线到分类

从最简单的线性模型 y = w * x + b 出发,我们可以拟合一条直线,预测一个数值。但现实中的问题不全是“预测数值”,很多时候是“做判断”:是或否、属于 A 还是属于 B。

逻辑门就是很典型的判断问题。

x1x2ORANDNANDXOR
000011
011010
101010
111100

我们想让机器自己从真值表中学会这些规则。

单个神经元

激活函数

如果直接用线性模型 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)

我们可以把它想象成一个“加权投票器”:

  • x1x2 是输入信号。
  • w1w2 是它们的权重。
  • b 是偏置,决定“通过门槛”有多高。
  • sigmoid 决定最终输出是“接近 0”还是“接近 1”。

训练一个 OR 门

下面这段代码可以训练 OR 门。把 dataset 改成 and_gate_train_setnand_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

这四个点无法被一条直线分开,所以单个神经元无论怎么调 w1w2b,都无法学会 XOR。

用三个门组合出 XOR

XOR 可以用更基本的门组合出来:

XOR(x1, x2) = AND(OR(x1, x2), NAND(x1, x2))

验证一下:

x1x2ORNANDAND(OR, NAND) = XOR
00010
01111
10111
11100

这就是标准的 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.000000

AND 门:

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.990819

NAND 门:

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.012005

XOR 被成功学会了。

一个有趣的对比

我们反过来想:如果把复杂的三层网络模型拿去训练 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。

这说明:

  1. 模型复杂度决定了它能解决问题的上限。 单个神经元只能处理线性可分的问题;三层网络可以处理非线性问题。
  2. 复杂模型具有“向下兼容”的能力。 它的表达能力足够强,简单任务自然也能覆盖。
  3. 不是训练方法不够强,而是模型本身表达能力不够。 同样的梯度下降、同样的激活函数,模型复杂一点,就能从“完全不会”变成“完美学会”。

更神奇的发现

如果我们把 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。

核心思想始终没变:

定义模型,计算误差,求梯度,沿着梯度反方向调整参数,让成本越来越小。

但随着模型复杂度提升,它能解决的问题也更多了。单个神经元只能画一条直线分开数据,三层网络可以画出更复杂的决策边界。简单的单元通过组合,就能涌现出让机器“学会”复杂规则的能力。