- 用 Python 从零实现感知机,包括权重更新规则和阶跃激活函数
- 解释为什么单个感知机只能解决线性可分问题,并演示 XOR 失效的情况
- 通过组合 OR、NAND 和 AND 门构建多层感知机来解决 XOR
- 使用 sigmoid 激活和反向传播训练双层网络,自动学习 XOR
问题
你知道向量和点积。你知道矩阵会把输入变换成输出。但机器是如何学习该用哪种变换的?
感知机回答了这个问题。它是最简单的学习机器:接收若干输入,乘以权重,加上偏置,然后做出二值决策。接着再调整。仅此而已。所有曾经构建过的神经网络,都是这一想法的层叠。
理解感知机,就是理解「学习」在代码中真正的含义:不断调整数字,直到输出与现实相符。
概念
一个神经元,一个决策
感知机接收 n 个输入,每个输入乘以权重,求和,加上偏置,再把结果传给激活函数。
graph LR
x1["x1"] -- "w1" --> sum["Σ(wi*xi) + b"]
x2["x2"] -- "w2" --> sum
x3["x3"] -- "w3" --> sum
bias["bias"] --> sum
sum --> step["step(z)"]
step --> out["output (0 or 1)"]阶跃函数很粗暴:如果加权总和加偏置后 \ge 0,输出 1;否则输出 0。
这是一种线性分类器。权重和偏置定义了一条线(高维空间中则是超平面),把输入空间分成两个区域。
决策边界
对于两个输入,感知机在二维空间中画出一条线:
x2
┤
│ Class 1 /
│ (0) /
│ /
│ / w1·x1 + w2·x2 + b = 0
│ /
│ / Class 2
│ / (1)
┼───────────/──────────── x1线的一侧所有点输出 0,另一侧输出 1。训练过程会移动这条线,直到正确分开两类。
学习规则
感知机学习规则很简单:
For each training example (x, y_true):
y_pred = predict(x)
error = y_true - y_pred
For each weight:
w_i = w_i + learning_rate * error * x_i
bias = bias + learning_rate * error如果预测正确,\text{error} = 0,权重不变。如果预测为 0 但应为 1,权重增加;如果预测为 1 但应为 0,权重减小。学习率控制每次调整的幅度。
XOR 问题
这里就是它的软肋。看看这些逻辑门:
AND gate: OR gate: XOR gate:
x1 x2 out x1 x2 out x1 x2 out
0 0 0 0 0 0 0 0 0
0 1 0 0 1 1 0 1 1
1 0 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 0AND 和 OR 是线性可分的:你可以用一条直线把 0 和 1 分开。XOR 不是。没有一条直线能把 [0,1] 和 [1,0] 与 [0,0] 和 [1,1] 分开。
AND (separable): XOR (not separable):
x2 x2
1 ┤ 0 1 1 ┤ 1 0
│ / │
0 ┤ 0 / 0 0 ┤ 0 1
┼──/──────── x1 ┼──────────── x1
line works! no single line works!这是一个根本性限制。单个感知机只能解决线性可分问题。Minsky 和 Papert 在 1969 年证明了这一点,神经网络研究因此消沉了近十年。
解决办法是:把感知机堆叠成层。多层感知机可以把两个线性决策组合成一个非线性决策,从而解决 XOR。
构建它
第 1 步:感知机类
pythonclass Perceptron:
def __init__(self, n_inputs, learning_rate=0.1):
self.weights = [0.0] * n_inputs
self.bias = 0.0
self.lr = learning_rate
def predict(self, inputs):
total = sum(w * x for w, x in zip(self.weights, inputs))
total += self.bias
return 1 if total >= 0 else 0
def train(self, training_data, epochs=100):
for epoch in range(epochs):
errors = 0
for inputs, target in training_data:
prediction = self.predict(inputs)
error = target - prediction
if error != 0:
errors += 1
for i in range(len(self.weights)):
self.weights[i] += self.lr * error * inputs[i]
self.bias += self.lr * error
if errors == 0:
print(f"Converged at epoch {epoch + 1}")
return
print(f"Did not converge after {epochs} epochs")第 2 步:用逻辑门训练
pythonand_data = [
([0, 0], 0),
([0, 1], 0),
([1, 0], 0),
([1, 1], 1),
]
or_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 1),
]
not_data = [
([0], 1),
([1], 0),
]
print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
print(f" {inputs} -> {p_and.predict(inputs)}")
print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
print(f" {inputs} -> {p_or.predict(inputs)}")
print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
print(f" {inputs} -> {p_not.predict(inputs)}")第 3 步:观察 XOR 失效
pythonxor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
result = p_xor.predict(inputs)
status = "OK" if result == expected else "WRONG"
print(f" {inputs} -> {result} (expected {expected}) {status}")它永远不会收敛。这就是单个感知机无法学习 XOR 的铁证。
第 4 步:用两层网络解决 XOR
诀窍是:\text{XOR} = (x_1 \text{ OR } x_2) \text{ AND NOT } (x_1 \text{ AND } x_2)。组合三个感知机:
graph LR
x1["x1"] --> OR["OR neuron"]
x1 --> NAND["NAND neuron"]
x2["x2"] --> OR
x2 --> NAND
OR --> AND["AND neuron"]
NAND --> AND
AND --> out["output"]pythondef xor_network(x1, x2):
or_neuron = Perceptron(2)
or_neuron.weights = [1.0, 1.0]
or_neuron.bias = -0.5
nand_neuron = Perceptron(2)
nand_neuron.weights = [-1.0, -1.0]
nand_neuron.bias = 1.5
and_neuron = Perceptron(2)
and_neuron.weights = [1.0, 1.0]
and_neuron.bias = -1.5
hidden1 = or_neuron.predict([x1, x2])
hidden2 = nand_neuron.predict([x1, x2])
output = and_neuron.predict([hidden1, hidden2])
return output
print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
result = xor_network(inputs[0], inputs[1])
print(f" {inputs} -> {result} (expected {expected})")四个输入全部正确。把感知机堆叠成层,就能生成单个感知机无法产生的决策边界。
第 5 步:训练一个双层网络
第 4 步手动设置了权重。对 XOR 这可行,但在实际问题中你并不知道正确权重。解决办法是:把阶跃函数换成 sigmoid,通过反向传播自动学习权重。
pythonclass TwoLayerNetwork:
def __init__(self, learning_rate=0.5):
import random
random.seed(0)
self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.b_output = random.uniform(-1, 1)
self.lr = learning_rate
def sigmoid(self, x):
import math
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def forward(self, inputs):
self.inputs = inputs
self.hidden_outputs = []
for i in range(2):
z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
self.hidden_outputs.append(self.sigmoid(z))
z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
self.output = self.sigmoid(z_out)
return self.output
def train(self, training_data, epochs=10000):
for epoch in range(epochs):
total_error = 0
for inputs, target in training_data:
output = self.forward(inputs)
error = target - output
total_error += error ** 2
d_output = error * output * (1 - output)
saved_w_output = self.w_output[:]
hidden_deltas = []
for i in range(2):
h = self.hidden_outputs[i]
hd = d_output * saved_w_output[i] * h * (1 - h)
hidden_deltas.append(hd)
for i in range(2):
self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
self.b_output += self.lr * d_output
for i in range(2):
for j in range(len(inputs)):
self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
self.b_hidden[i] += self.lr * hidden_deltas[i]pythonnet = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
result = net.forward(inputs)
predicted = 1 if result >= 0.5 else 0
print(f" {inputs} -> {result:.4f} (rounded: {predicted}, expected {expected})")与第 4 步相比有两个关键区别。第一,sigmoid 替代了阶跃函数——它是光滑的,因此存在梯度。第二,train 方法把误差从输出层反向传播到隐藏层,按照每个权重对误差的贡献比例进行调整。这就是 20 行代码实现的反向传播。
这是通往第 03 课的桥梁。d_output 和 hidden_deltas 背后的数学,是链式法则在网络图上的应用。我们会在那里正式推导。
使用它
你刚才从零构建的一切,都可以用一个导入搞定:
pythonfrom sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])
clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])五行代码。你写的 30 行 Perceptron 类做的是同样的事。sklearn 版本增加了收敛检查、多种损失函数和稀疏输入支持——但核心循环完全相同:加权求和、阶跃函数、预测错误时更新权重。
真正的差距在规模上。生产级网络的不同之处:
- 阶跃函数变成 sigmoid、ReLU 或其他光滑激活函数
- 权重通过反向传播自动学习(第 03 课)
- 层数更深:3 层、10 层、100 层以上
- 核心原则不变:每一层都根据前一层的输出构造新特征
单个感知机只能画直线。把它们堆叠起来,你就能画出任何形状。
交付它
本节课产出:
outputs/skill-perceptron.md—— 一份技能文档,说明何时需要单层架构、何时需要多层架构
练习
- 用感知机训练一个 NAND 门(NAND 是通用门——任何逻辑电路都能只用 NAND 构建)。验证它的权重和偏置形成了有效的决策边界。
- 修改 Perceptron 类,在 AND 门训练的每个 epoch 追踪决策边界(w_1 x_1 + w_2 x_2 + b = 0)。打印这条线在训练过程中如何移动。
- 构建一个 3 输入感知机,仅当 3 个输入中至少有 2 个为 1 时输出 1(多数投票函数)。它是线性可分的吗?为什么?
关键术语
| 术语 | 通常说法 | 实际含义 |
|---|---|---|
| 感知机(Perceptron) | "一个假神经元" | 一种线性分类器:输入与权重的点积加上偏置,再通过阶跃函数 |
| 权重(Weight) | "输入有多重要" | 一个乘数,缩放每个输入对决策的贡献 |
| 偏置(Bias) | "阈值" | 一个常数,平移决策边界,使神经元即使输入全为零也能激活 |
| 激活函数(Activation function) | "把值压扁的东西" | 对加权求和结果应用的函数——感知机用阶跃函数,现代网络用 sigmoid / ReLU 等 |
| 线性可分(Linearly separable) | "可以画一条线分开" | 单个超平面就能完美分开两类数据的数据集 |
| XOR 问题(XOR problem) | "感知机做不到的事" | 证明单层网络无法学习非线性可分函数 |
| 决策边界(Decision boundary) | "分类器切换的地方" | 把输入空间分成两类的超平面 w \cdot x + b = 0 |
| 多层感知机(Multi-layer perceptron) | "真正的神经网络" | 把感知机堆叠成层,每一层的输出作为下一层的输入 |
延伸阅读
- Frank Rosenblatt, "The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain" (1958) —— 开启这一切的原始论文
- Minsky & Papert, "Perceptrons" (1969) —— 证明 XOR 无法被单层网络解决、并让感知机研究沉寂十年的著作
- Michael Nielsen, "Neural Networks and Deep Learning", Chapter 1 (http://neuralnetworksanddeeplearning.com/) —— 免费在线,最直观地解释感知机如何组合成网络