一个 M-P 神经元就是一行式子:输入的加权和减掉阈值,大于零给 1,否则给 0。 同一行式子,换三组权重和阈值,就分别成了与门、或门、与非门。 但穷举 68921 组权重,异或的四行真值表最多答对 3 行——一个神经元画不出两条线。
上一节把历史捋完了,从这一节开始拆零件。 最小的零件是 1943 年麦卡洛克和皮茨提出的模型,通常叫 M-P 模型。 它的重要性不在「像大脑」,而在「可以算」。 把一个神经元抽象成「加权求和 + 阈值判断」,从此可以用数学讨论它。 后面所有网络,不管多少层,最底层都是这一行式子。
一、一行式子,两个旋钮
输出 = 阶跃( w1 乘 x1 + w2 乘 x2 减 阈值 )。 括号里大于零给 1,否则给 0。
y = step( w1*x1 + w2*x2 - theta )
step(z) = 1 (z > 0)
0 (z <= 0)
权重管「每个输入有多重要」,阈值管「加起来要到多少才算数」。 把阈值挪到左边写成加一个常数,就是今天代码里的偏置 b = 负的阈值。 两个名字说的是同一件事,只是符号相反。
二、换三组数,三个逻辑门
固定结构,只改权重和阈值:
| 门 | w1 | w2 | 阈值 | 什么时候输出 1 | |:---:|:---:|:---:|:---:|---|---| | 与 | 0.5 | 0.5 | 0.75 | 两个都是 1 | | 或 | 0.5 | 0.5 | 0.25 | 至少一个是 1 | | 与非 | -0.5 | -0.5 | -0.75 | 不全是 1 | 与门的算法:两个都是 1 时,加权和是 1.0,减掉 0.75 剩 0.25,大于零,输出 1。 其它三种情况加权和最多 0.5,减 0.75 是负的,输出 0。 或门只把阈值降到 0.25,于是「有一个 1」就够了。 与非门把权重取负、阈值也取负,等于把判断反过来。 注意一件事:三个门的权重是同一组数(0.5 和 0.5),只有阈值不同。 所以「神经元在算什么」完全由这几个数决定,它本身没有偏好。
三、几何上它是一条线
把 (x1, x2) 当平面上的坐标,条件「w1x1 + w2x2 = 阈值」就是一条直线。 直线一侧输出 1,另一侧输出 0。 所以一个神经元能实现的,恰好是「用一条直线把平面切成两半」这一类判断。 与、或、与非都能用一条直线切开,所以它们都能实现。 这就是为什么这三组权重存在——不是巧合,是几何上允许。
四、异或为什么不行
异或的四行真值表:
| x1 | x2 | 输出 | 在平面上的位置 |
|---|---|---|---|
| 0 | 0 | 0 | 左下 |
| 0 | 1 | 1 | 左上 |
| 1 | 0 | 1 | 右下 |
| 1 | 1 | 0 | 右上 |
| 两个 1 在左上和右下,两个 0 在左下和右上。 | |||
| 两类交错排列,任何一条直线最多只能把其中一个 1 或其中一个 0 划到对面。 | |||
| 实测:把 w1、w2、阈值各自在负 2 到 2 之间按 0.1 步长穷举,共 68921 组,四行里最多答对 3 行。 | |||
| 也就是说「至少错一个点」是这类模型的硬上限,不是没调好。 | |||
| 要分开它,需要两条直线围出一块区域。 | |||
| 两条线意味着两个神经元,再加一个把两边合起来的神经元——这就是下一节的多层感知器。 |
五、这个模型留给今天的东西
第一,把神经元写成「加权求和 + 非线性」这个形式,一直用到今天。 变的只有那个非线性:阶跃换成了 sigmoid、tanh、ReLU。 第二,权重和阈值(偏置)是两个独立的东西。 权重决定「方向」,偏置决定「位置」——这条线往哪边挪,全靠偏置。 第三,单个单元的局限是几何上看得见的。 「只能画一条线」这件事不需要跑实验就能推出来,这也是几何直觉的价值。
六、动手 Demo:一个神经元能算什么,不能算什么
这段代码先用三组权重实现与门、或门、与非门,把四行真值表逐个验一遍; 然后穷举 68921 组权重和阈值,看异或最好能做到几个点。 重点看两处。第二段三张真值表全部 ok;第三段那句「最多答对 3 行」和试探的组数。
# -*- coding: utf-8 -*-
import itertools
import unicodedata
import numpy as np
def wd(s):
return sum(2 if unicodedata.east_asian_width(c) in ('W', 'F') else 1 for c in s)
def pad(s, n):
return s + ' ' * max(0, n - wd(s))
def mp(w, b, x):
"""M-P 神经元:加权求和,减阈值,过阶跃函数。"""
return 1 if np.dot(w, x) + b > 0.0 else 0
def show(name, w, b, gate):
print(' ' + pad(name, 8) + 'w = %-16s 阈值 = %.2f' % (str(tuple(w)), -b))
ok = 0
for x1, x2 in itertools.product((0, 1), repeat=2):
got = mp(w, b, (x1, x2))
want = gate(x1, x2)
ok += got == want
print(' x1=%d x2=%d -> %d 期望 %d %s'
% (x1, x2, got, want, 'ok' if got == want else 'x'))
return ok
print('[1] 一个 M-P 神经元就是一串权重加一个阈值')
print(' 输出 = 阶跃( w1*x1 + w2*x2 - 阈值 ),括号里大于零给 1,否则给 0')
print(' 权重管「每个输入有多重要」,阈值管「加起来要到多少才算数」')
print('')
print('[2] 换不同的权重和阈值,同一个神经元能实现不同逻辑门')
show('与门', (0.5, 0.5), -0.75, lambda a, b: a & b)
print('')
show('或门', (0.5, 0.5), -0.25, lambda a, b: a | b)
print('')
show('与非门', (-0.5, -0.5), 0.75, lambda a, b: 1 - (a & b))
print('')
print('[3] 现在试异或:穷举 w1、w2、阈值,看最好能做到几个点')
best = 0
best_par = None
grid = np.round(np.arange(-2.0, 2.01, 0.1), 2)
for w1, w2, b in itertools.product(grid, grid, grid):
ok = 0
for x1, x2 in itertools.product((0, 1), repeat=2):
ok += mp((w1, w2), b, (x1, x2)) == (x1 ^ x2)
if ok > best:
best, best_par = ok, (float(w1), float(w2), -float(b))
print(' 试探了 %d 组权重与阈值(每维 -2 到 2,步长 0.1)' % (len(grid) ** 3))
print(' 四行真值表里最多答对 %d 行,一组参数是 w = (%s, %s)、阈值 = %.1f'
% (best, best_par[0], best_par[1], best_par[2]))
print(' 3 行意味着「至少错一个点」——异或的要求和一条直线的要求互相打架')
print('')
print('[4] 为什么打架:异或的两个正例在一条斜对角,两个负例在另一条斜对角')
print(' 要把它们分开,得用两条直线围出一块区域,也就是「两层」')
print(' 一个神经元只能给出一条直线,所以它注定做不到')
实跑输出:
--------------------------------------------------------------------
M-P 模型:一个神经元能算什么,不能算什么
--------------------------------------------------------------------
[1] 一个 M-P 神经元就是一串权重加一个阈值
输出 = 阶跃( w1*x1 + w2*x2 - 阈值 ),括号里大于零给 1,否则给 0
权重管「每个输入有多重要」,阈值管「加起来要到多少才算数」
[2] 换不同的权重和阈值,同一个神经元能实现不同逻辑门
与门 w = (0.5, 0.5) 阈值 = 0.75
x1=0 x2=0 -> 0 期望 0 ok
x1=0 x2=1 -> 0 期望 0 ok
x1=1 x2=0 -> 0 期望 0 ok
x1=1 x2=1 -> 1 期望 1 ok
或门 w = (0.5, 0.5) 阈值 = 0.25
x1=0 x2=0 -> 0 期望 0 ok
x1=0 x2=1 -> 1 期望 1 ok
x1=1 x2=0 -> 1 期望 1 ok
x1=1 x2=1 -> 1 期望 1 ok
与非门 w = (-0.5, -0.5) 阈值 = -0.75
x1=0 x2=0 -> 1 期望 1 ok
x1=0 x2=1 -> 1 期望 1 ok
x1=1 x2=0 -> 1 期望 1 ok
x1=1 x2=1 -> 0 期望 0 ok
[3] 现在试异或:穷举 w1、w2、阈值,看最好能做到几个点
试探了 68921 组权重与阈值(每维 -2 到 2,步长 0.1)
四行真值表里最多答对 3 行,一组参数是 w = (-2.0, 0.1)、阈值 = -0.0
3 行意味着「至少错一个点」——异或的要求和一条直线的要求互相打架
[4] 为什么打架:异或的两个正例在一条斜对角,两个负例在另一条斜对角
要把它们分开,得用两条直线围出一块区域,也就是「两层」
一个神经元只能给出一条直线,所以它注定做不到
第二段三张表都只有四行,但每张表都在回答同一个问题:这条线切在哪。
与门的线切在「两个都是 1」和「其余三种」之间;或门切在「至少一个 1」和「都是 0」之间。
注意输出那一列的 ok 是代码逐个比出来的,不是手写的。
表格里每一行的 期望 来自 lambda 里的逻辑运算,实际 来自 mp() 函数。
第三段的 68921 是 41 的三次方:每一维取 41 个候选值。
穷举的意义在于它穷尽了所有可能——只要网格够密,结论就不是「我没找到」,而是「不存在」。
「最多 3 行」这个结果和几何直觉一致。
一条直线切一个正方形,四个角里最多有三个在同一侧。
异或要求的恰恰是「两个对角的在同一侧、另两个对角在另一侧」,直线做不到。
代码里 grid 用了 np.round(..., 2),是为了避免浮点误差让同一个阈值被算两次。
这种细节在写「穷举类」实验时很常见,不处理会让计数虚高。
30 秒实验
- 把异或穷举的网格步长从 0.1 改成 0.5,重跑。组数从 68921 掉到 729,结论还是「最多 3 行」。 说明这个上限和网格密度无关,是结构性的。
- 把
mp里的> 0.0改成>= 0.0,重跑。 与门那一行会多出一个错误:x1=0、x2=0 时加权和减阈值正好是 -0.75,不受影响; 但如果把阈值调成 0.5,边界情况就会翻转。 边界取不取等号,是一个必须明确的约定。 - 给
mp加第三个输入 x3,把与门改成三输入的与门,试着自己定一组权重。 三个 0.5 加阈值 1.25 就能做到。 输入多一个,线就变成三维空间里的平面,判断方式完全一样。
常见误区
- 「M-P 模型是神经网络的雏形,所以很简单,没什么用」。 它的形式(加权求和加非线性)就是今天每个神经元的定义。 今天变的只有非线性那一段,骨架一模一样。
- 「阈值和偏置是两个不同的东西」。 阈值 theta 和偏置 b 只差一个负号:b = 负 theta。 代码里习惯用加 b,书上习惯写减 theta,看到两个词要能自动换算。
- 「异或做不出来是因为权重没调好」。 穷举 68921 组已经把所有可能的权重都试过了,最多 3 行。 这是结构上限,不是调参问题。
- 「神经元越多越好,反正能表达更复杂的东西」。 单个神经元就能做与、或、与非,很多逻辑根本不需要多层。 需要多层的是「区域」类判断,比如异或、比如「在圆环里」。
自测三问(点开对答案)
1. 与门和或门的权重完全一样,只差阈值,为什么结果不同? —— 因为阈值决定这条线往哪边挪。 与门要求加权和达到 0.75 才算数,所以只有两个输入都是 1(和是 1.0)才过线; 或门只要 0.25,一个 1 就够了。权重定方向,阈值定位置。 2. 为什么一个神经元实现不了异或? —— 异或的两类在平面上是对角交叉的,任何一条直线都只能把其中一个点划错。 实测穷举 68921 组权重和阈值,四行真值表最多答对 3 行。 要分开它需要两条直线围出一块区域,也就是两层。 3. 阈值和偏置是什么关系? —— 只差一个负号:偏置 b 等于负的阈值 theta。 写成 w 乘 x 加 b 大于 0,和写成 w 乘 x 减 theta 大于 0,是同一个条件。 代码里习惯用偏置,书上习惯写阈值,看到要能自动换算。M-P 模型的权重是人手定的,这就有了一个明显的问题:数据一多,人怎么知道该填什么数? 1958 年的感知器回答了这个问题——让机器自己调。 下一节看那条只有一句话的学习规则,以及它为什么一定收敛。
- ☐ (本节完,下一节 2.3 感知器)