1. 感知机模型
感知机(Perceptron)是一个线性分类器,其只适用于线性可分的数据。其数学表达形式为:
$$f(\mathbf{x}) = \text{sign}(\mathbf{w}^\mathrm{T} \mathbf{x} + b)$$
其中,$\mathbf{w}$ 是权重向量,$b$ 是偏置,$\mathbf{x}$ 是输入特征。函数 $\text{sign}$ 将输出映射到 -1 或 1。
感知机的目标是在所有的线性可分超平面构成的假设空间中,找到一个能使训练集中的数据可分的超平面。感知机并未对这一超平面做特殊要求,只要能区分开训练数据即可。因此,它找到的超平面并不一定是最优的,即可能只是恰好拟合了训练数据的超平面,其泛化能力并不佳。
2. 学习策略
由于直接最小化误分类点的个数并不可微,感知机的学习策略被设为:最小化误分类点到超平面的距离。
假设超平面为 $\mathbf{w}^\mathrm{T}\mathbf{x} + b = 0$,样本点 $(\mathbf{x}_i, y_i)$ 到超平面的距离为:
$$\frac{1}{||\mathbf{w}||} |\mathbf{w}^\mathrm{T}\mathbf{x}_i + b|$$
对于误分类点,有 $y_i(\mathbf{w}^\mathrm{T}\mathbf{x}_i + b) < 0$,因此损失函数可以定义为:
$$L(\mathbf{w}, b) = -\sum_{i \in M} y_i(\mathbf{w}^\mathrm{T}\mathbf{x}_i + b)$$
其中 $M$ 为误分类点的集合。
在初始化超平面的参数即法向量和偏置后,迭代地计算样本的损失并求出参数的梯度,再基于随机梯度下降(SGD)来更新参数即可,直到损失收敛后停止学习。
3. 基于 Numpy 的感知机实现
以下代码展示了如何使用 Python 和 Numpy 从零实现感知机算法。
import numpy as np
def prepare_data(n=100):
"""
生成 OR 门数据集
"""
def OR(x):
# 定义 OR 门的真实权重和偏置用于生成标签
w_true = np.array([0.5, 0.5])
b_true = -0.2
tmp = np.sum(w_true * x) + b_true
# 统一使用 -1 和 1 作为标签以匹配感知机更新规则
if tmp <= 0:
return -1
else:
return 1
input_size = 2
inputs = np.random.randn(n, input_size)
labels = np.array([OR(inputs[i]) for i in range(n)])
return inputs, labels
class Perceptron:
def __init__(self, input_size, lr=0.001):
# 初始化权重和偏置
self.w = np.random.randn(input_size)
self.b = np.random.randn(1)
self.lr = lr
def predict(self, x):
tmp = np.sum(self.w * x) + self.b
if tmp <= 0:
return -1
else:
return 1
def update(self, x, y):
# 基于 SGD 的参数更新(由最小化误分类点到超平面的距离求导可得)
self.w = self.w + self.lr * y * x
self.b = self.b + self.lr * y
if __name__ == "__main__":
n = 1000 # 训练样本数
ratio = 0.8 # 训练测试比
input_size = 2
print("Preparing Data {}".format(n))
X, Y = prepare_data(n)
clip_num = int(n * ratio)
train_X, train_Y = X[:clip_num], Y[:clip_num]
test_X, test_Y = X[clip_num:], Y[clip_num:]
# Init model
lr = 0.005
model = Perceptron(input_size, lr)
s = model.predict(X[0])
print("Input: ({}, {}), Output: {}".format(X[0][0], X[0][1], s))
# Training
epochs = 100
for i in range(epochs):
loss = 0
wrong_index = []
print("\nEpoch {}".format(i+1))
print("Forward Computing")
for idx in range(clip_num):
pred_y = model.predict(train_X[idx])
if pred_y != train_Y[idx]:
wrong_index.append(idx)
tmp_loss = abs(float(np.sum(model.w * train_X[idx]) + model.b))
loss += tmp_loss
print("Wrong predict samples: {}, Loss: {}".format(len(wrong_index), loss))
print("Learning")
for j in wrong_index:
model.update(train_X[j], train_Y[j])
# Testing
wrong_num = 0
test_loss = 0
for j in range(test_X.shape[0]):
pred_y = model.predict(test_X[j])
if pred_y != test_Y[j]:
tmp_loss = abs(float(np.sum(model.w * test_X[j]) + model.b))
test_loss += tmp_loss
wrong_num += 1
print("\nTest wrong predict samples: {}, Loss: {}".format(wrong_num, test_loss))
4. 感知机的局限性与延伸
感知机是线性模型,它不能学习非线性函数,因而它对线性不可分的数据束手无力。例如,感知机可以拟合与门(AND)、或门(OR)、**非门(NOT)产生的数据,但是不能处理好异或门(XOR)**产生的数据。
为什么无法解决 XOR 问题?
异或门(XOR)的真值表如下:
- (0, 0) -> 0
- (0, 1) -> 1
- (1, 0) -> 1
- (1, 1) -> 0
在二维平面上,类别为 1 的点位于对角线两端,类别为 0 的点位于另外两端。这两类数据无法用一条直线分开,因此线性感知机无法收敛到一个解。
模型的延伸
基于感知机,可以延伸出 LR(逻辑回归)、SVM(支持向量机) 等更强大的模型。此外,值得注意的是,虽然单个感知机的表达能力有限,但是如果将多个感知机叠加起来,则可以具备足够强的表达能力,即 Multi-layer Perception(MLP) 的通用近似定理(给定足够多的数据和足够宽的两层 MLP,可以近似任意连续函数)。
在《深度学习入门:基于 Python 的理论与实现》一书中有个直观的例子。假设用三个 Perceptron 分别拟合与门、非门和或门,再基于数字电路的知识将这三个门组合起来,即可以构成异或门。这展示了多层网络如何通过组合简单的线性单元来解决非线性问题。
5. 总结
本文详细介绍了感知机的基本原理、数学推导及代码实现。通过对比线性可分与不可分的情况,深入理解了感知机的局限性。在实际应用中,理解感知机的边界有助于选择合适的模型架构,例如在处理复杂非线性问题时转向神经网络或多层感知机。掌握基础的感知机算法是深入学习深度学习的必要前提。


