用Python从零实现感知机李航《统计学习方法》例题3.1实战解析当你第一次翻开李航老师的《统计学习方法》看到感知机那一章的数学公式和抽象描述时是否感到一头雾水作为机器学习中最基础的分类模型感知机看似简单但真正理解它的工作原理却需要将理论与代码实践相结合。本文将带你用Python从零开始实现感知机通过亲手编写每一行代码来直观理解这个经典算法。我们将以书中例题3.1的数据集为例完整展示如何用Python实现感知机的训练过程。从数据准备、模型初始化到梯度下降的每一步迭代最后可视化决策边界你将看到抽象的数学公式如何转化为实际运行的代码。这种做中学的方式远比死记硬背公式有效得多。1. 感知机基础与准备工作1.1 感知机模型的核心思想感知机(Perceptron)是Frank Rosenblatt在1957年提出的二分类线性模型它奠定了神经网络的基础。其核心思想非常简单找到一个能够将两类数据分开的超平面。对于二维数据这个超平面就是一条直线三维则是平面更高维度我们统称为超平面。感知机的数学模型可以表示为f(x) sign(w·x b)其中w是权重向量b是偏置项sign是符号函数输出1或-11.2 开发环境配置在开始编码前我们需要准备Python环境。推荐使用Anaconda创建虚拟环境conda create -n perceptron python3.8 conda activate perceptron pip install numpy matplotlib主要依赖库NumPy处理矩阵运算Matplotlib数据可视化1.3 准备例题数据集根据例题3.1我们创建以下数据集import numpy as np # 正实例点 positive np.array([[3, 3], [4, 3]]) # 负实例点 negative np.array([[1, 1]]) # 合并数据集和标签 X np.vstack((positive, negative)) y np.array([1, 1, -1]) # 1代表正类-1代表负类2. 感知机的Python实现2.1 初始化感知机类我们首先创建一个Perceptron类包含模型初始化方法class Perceptron: def __init__(self): self.w None # 权重 self.b 0 # 偏置 def initialize(self, input_dim): 初始化模型参数 self.w np.zeros(input_dim) self.b 02.2 实现预测函数预测函数根据当前权重和偏置计算输出def predict(self, x): 预测单个样本的分类 return np.sign(np.dot(self.w, x) self.b)2.3 训练过程实现训练过程是感知机的核心采用随机梯度下降法def train(self, X, y, lr1, epochs100): 训练感知机模型 n_samples, n_features X.shape self.initialize(n_features) for epoch in range(epochs): misclassified 0 for idx, x_i in enumerate(X): if y[idx] * self.predict(x_i) 0: # 误分类判断 self.w lr * y[idx] * x_i # 更新权重 self.b lr * y[idx] # 更新偏置 misclassified 1 print(fEpoch {epoch1}: 更新后 w{self.w}, b{self.b}) if misclassified 0: # 如果没有误分类点 print(f训练完成于第{epoch1}轮迭代) break3. 运行与调试模型3.1 训练模型并观察迭代过程现在我们可以实例化感知机并进行训练perceptron Perceptron() perceptron.train(X, y, lr1)运行后会看到类似以下的输出这与书中例题3.1的迭代过程完全一致Epoch 1: 更新后 w[3. 3.], b1 Epoch 2: 更新后 w[2. 2.], b0 Epoch 3: 更新后 w[1. 1.], b-1 Epoch 4: 更新后 w[0. 0.], b-2 Epoch 5: 更新后 w[3. 3.], b-1 训练完成于第6轮迭代3.2 决策边界可视化理解感知机的最好方式就是可视化它的决策边界。我们添加以下可视化代码import matplotlib.pyplot as plt def plot_decision_boundary(model, X, y): # 绘制数据点 plt.scatter(X[y1, 0], X[y1, 1], colorblue, markero, label正类) plt.scatter(X[y-1, 0], X[y-1, 1], colorred, markerx, label负类) # 计算并绘制决策边界 x1 np.linspace(0, 5, 100) if model.w[1] ! 0: x2 -(model.w[0] * x1 model.b) / model.w[1] plt.plot(x1, x2, g-, label决策边界) plt.xlabel(特征1) plt.ylabel(特征2) plt.legend() plt.title(感知机决策边界) plt.grid(True) plt.show() plot_decision_boundary(perceptron, X, y)4. 深入理解感知机的局限性4.1 线性可分性与感知机的收敛性感知机的一个重要特性是当数据线性可分时算法保证收敛。这被称为感知机收敛定理。在我们的例子中数据明显是线性可分的因此感知机能够在有限步内找到分离超平面。验证线性可分性的简单方法是观察数据分布from sklearn.decomposition import PCA pca PCA(n_components1) X_pca pca.fit_transform(X) plt.scatter(X_pca, y) plt.title(数据在一维投影上的分布) plt.show()4.2 感知机无法解决异或问题感知机作为线性模型无法解决非线性可分问题如经典的异或(XOR)问题# XOR数据集 X_xor np.array([[0,0], [0,1], [1,0], [1,1]]) y_xor np.array([-1, 1, 1, -1]) # 尝试用感知机解决XOR问题 xor_perceptron Perceptron() xor_perceptron.train(X_xor, y_xor, epochs100) plot_decision_boundary(xor_perceptron, X_xor, y_xor)运行后会看到感知机无法找到合适的决策边界这解释了为什么需要更复杂的模型如多层感知机来处理非线性问题。5. 扩展与优化5.1 对偶形式的感知机实现除了原始形式感知机还有对偶形式的实现这在某些情况下计算效率更高class DualPerceptron: def __init__(self): self.alpha None # 对偶变量 self.b 0 self.gram None # Gram矩阵 def fit(self, X, y, lr1, epochs100): n_samples X.shape[0] self.alpha np.zeros(n_samples) self.b 0 self.gram np.dot(X, X.T) # 预计算Gram矩阵 for epoch in range(epochs): misclassified 0 for i in range(n_samples): if y[i] * (np.sum(self.alpha * y * self.gram[i]) self.b) 0: self.alpha[i] lr self.b lr * y[i] misclassified 1 if misclassified 0: break # 计算最终权重 self.w np.sum((self.alpha * y).reshape(-1,1) * X, axis0)5.2 添加学习率调度为了提高训练稳定性我们可以实现学习率调度def lr_scheduler(initial_lr, epoch, total_epochs): 学习率衰减函数 return initial_lr * (0.95 ** epoch) # 在train方法中使用 current_lr lr_scheduler(initial_lr, epoch, epochs) self.w current_lr * y[idx] * x_i5.3 批量梯度下降实现除了随机梯度下降我们还可以实现批量梯度下降版本def batch_train(self, X, y, lr1, epochs100, batch_size2): n_samples X.shape[0] self.initialize(X.shape[1]) for epoch in range(epochs): # 随机打乱数据 indices np.random.permutation(n_samples) X_shuffled X[indices] y_shuffled y[indices] for i in range(0, n_samples, batch_size): batch_X X_shuffled[i:ibatch_size] batch_y y_shuffled[i:ibatch_size] # 计算批量梯度 grad_w np.zeros_like(self.w) grad_b 0 misclassified 0 for x_i, y_i in zip(batch_X, batch_y): if y_i * self.predict(x_i) 0: grad_w y_i * x_i grad_b y_i misclassified 1 if misclassified 0: self.w lr * grad_w / misclassified self.b lr * grad_b / misclassified