用Python从零实现感知器算法:手把手教你用NumPy和鸢尾花数据集画决策边界

第一次接触机器学习时,很多人都会被那些复杂的数学公式吓退。但如果你真正动手实现过一个算法,就会发现它背后的思想往往出奇地简单。今天我们就用Python和NumPy,从零开始实现感知器算法——这个神经网络的最基础构建单元。不需要任何框架,只用基础的科学计算库,我们就能理解机器学习最本质的权重更新过程。

鸢尾花数据集是机器学习入门的经典选择。它包含了三种鸢尾花的四个特征(萼片长度、萼片宽度、花瓣长度、花瓣宽度),而我们只需要其中的两个特征就能清晰地展示感知器的工作原理。通过这个实践,你不仅能掌握算法实现,还能直观地看到决策边界是如何随着权重调整而变化的。

1. 环境准备与数据加载

在开始之前,确保你的Python环境已经安装了以下库:

pip install numpy matplotlib scikit-learn

让我们先从加载和观察数据开始。鸢尾花数据集包含三个类别,但感知器是一个二分类器,所以我们只选取其中两个类别:

import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets

# 加载鸢尾花数据集
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 只选择前两个类别和前两个特征
X = X[y < 2, :2]
y = y[y < 2]

# 可视化数据
plt.scatter(X[y==0, 0], X[y==0, 1], color='red', label='类别0')
plt.scatter(X[y==1, 0], X[y==1, 1], color='blue', label='类别1')
plt.xlabel('萼片长度(cm)')
plt.ylabel('萼片宽度(cm)')
plt.legend()
plt.show()

你会看到红蓝两色的点分别代表两种不同的鸢尾花。我们的目标是找到一条直线,能够完美地将这两类点分开。

提示:在实际项目中,数据预处理往往比模型本身更重要。这里我们跳过了许多步骤,但在真实场景中,你需要检查缺失值、异常值,并考虑是否需要特征缩放。

2. 感知器算法原理与实现

感知器的核心思想很简单:它试图找到一个权重向量w,使得对于所有正类样本,w·x > 0;对于所有负类样本,w·x < 0。算法通过不断调整权重来减少分类错误。

2.1 权重更新规则

感知器的学习规则可以用一句话概括:当分类错误时,朝着正确的方向调整权重。具体来说:

  • 如果样本属于正类但被误分为负类(即w·x < 0但应该是>0),则增加权重:w = w + ηx
  • 如果样本属于负类但被误分为正类(即w·x > 0但应该是<0),则减少权重:w = w - ηx

这里η是学习率,控制每次调整的幅度。

2.2 完整实现

让我们把这些规则转化为Python代码:

class Perceptron:
    def __init__(self, learning_rate=0.01, max_iter=1000):
        self.lr = learning_rate  # 学习率
        self.max_iter = max_iter  # 最大迭代次数
    
    def fit(self, X, y):
        # 初始化权重(包括偏置项)
        self.w = np.random.rand(X.shape[1] + 1)
        
        # 添加偏置项到特征矩阵
        X_with_bias = np.insert(X, 0, 1, axis=1)
        
        for _ in range(self.max_iter):
            errors = 0
            for xi, target in zip(X_with_bias, y):
                # 计算预测值
                predict = np.sign(np.dot(self.w, xi))
                # 感知器输出是±1,所以需要转换标签
                target = 1 if target == 1 else -1
                
                # 更新权重
                if predict != target:
                    update = self.lr * (target - predict) * xi
                    self.w += update
                    errors += 1
            
            # 如果没有错误,提前终止
            if errors == 0:
                break
    
    def predict(self, X):
        X_with_bias = np.insert(X, 0, 1, axis=1)
        return np.where(np.dot(X_with_bias, self.w) >= 0, 1, 0)

这个实现包含了感知器的核心逻辑。注意到我们做了几件重要的事情:

  1. 添加了偏置项(相当于w₀),这是通过在每个样本前插入1实现的
  2. 使用np.sign()作为激活函数
  3. 实现了提前终止机制,当所有样本都被正确分类时停止训练

3. 数据归一化与模型训练

在实际应用中,特征缩放往往能显著提高模型的性能。让我们对数据进行归一化:

def normalize(X):
    """ 将特征归一化到[0,1]范围 """
    min_vals = X.min(axis=0)
    max_vals = X.max(axis=0)
    return (X - min_vals) / (max_vals - min_vals)

X_normalized = normalize(X)

现在我们可以训练模型了:

# 创建感知器实例
perceptron = Perceptron(learning_rate=0.1, max_iter=100)

# 训练模型
perceptron.fit(X_normalized, y)

# 在训练集上评估
predictions = perceptron.predict(X_normalized)
accuracy = np.mean(predictions == y)
print(f"训练准确率: {accuracy:.2f}")

4. 可视化决策边界

理解模型如何做出决策的最好方法就是可视化它的决策边界。让我们编写一个函数来绘制这条边界:

def plot_decision_boundary(model, X, y):
    # 设置坐标轴范围
    x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
    y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
    
    # 生成网格点
    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
                         np.linspace(y_min, y_max, 100))
    
    # 预测每个网格点的类别
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    # 绘制决策边界和区域
    plt.contourf(xx, yy, Z, alpha=0.3)
    plt.scatter(X[y==0, 0], X[y==0, 1], color='red', label='类别0')
    plt.scatter(X[y==1, 0], X[y==1, 1], color='blue', label='类别1')
    plt.xlabel('归一化萼片长度')
    plt.ylabel('归一化萼片宽度')
    plt.legend()
    plt.title('感知器决策边界')
    plt.show()

plot_decision_boundary(perceptron, X_normalized, y)

这张图直观地展示了模型是如何区分两类鸢尾花的。决策边界是一条直线,这正是线性分类器的特点。

5. 感知器的局限性及改进

虽然我们的感知器在这个简单数据集上表现良好,但它有几个重要限制:

  1. 只能处理线性可分数据 :如果数据不是线性可分的,感知器算法将无法收敛
  2. 对特征缩放敏感 :不同尺度的特征会影响收敛速度
  3. 单一决策边界 :只能学习简单的线性决策边界

5.1 处理线性不可分数据

让我们创建一个线性不可分的数据集来看看感知器的表现:

from sklearn.datasets import make_circles

X_nonlinear, y_nonlinear = make_circles(n_samples=100, noise=0.1, factor=0.5)

plt.scatter(X_nonlinear[y_nonlinear==0, 0], X_nonlinear[y_nonlinear==0, 1], color='red')
plt.scatter(X_nonlinear[y_nonlinear==1, 0], X_nonlinear[y_nonlinear==1, 1], color='blue')
plt.show()

尝试用感知器分类这个数据:

perceptron_nonlinear = Perceptron(max_iter=1000)
perceptron_nonlinear.fit(X_nonlinear, y_nonlinear)
plot_decision_boundary(perceptron_nonlinear, X_nonlinear, y_nonlinear)

你会发现无论训练多久,感知器都无法完美分类这个数据集。这就是为什么我们需要更复杂的模型,如多层感知机(神经网络)来捕捉非线性关系。

5.2 改进方向

虽然基础感知器有局限,但我们可以通过以下方式增强它:

  • 添加多项式特征 :通过特征工程引入非线性
  • 使用不同的激活函数 :如sigmoid或ReLU
  • 引入正则化 :防止过拟合
  • 组合多个感知器 :构建神经网络

6. 与scikit-learn实现对比

为了验证我们的实现是否正确,让我们与scikit-learn的Perceptron进行比较:

from sklearn.linear_model import Perceptron as SKPerceptron

sk_perceptron = SKPerceptron(max_iter=100, eta0=0.1, random_state=42)
sk_perceptron.fit(X_normalized, y)

print("我们的权重:", perceptron.w)
print("scikit-learn权重:", np.insert(sk_perceptron.coef_[0], 0, sk_perceptron.intercept_[0]))

如果两个模型的权重相近,说明我们的实现是正确的。你也可以比较它们的决策边界:

plot_decision_boundary(sk_perceptron, X_normalized, y)

7. 感知器在神经网络中的意义

虽然感知器看似简单,但它是现代深度学习的基石。理解感知器的工作原理对学习神经网络至关重要:

  1. 前向传播 :感知器的计算过程就是神经网络中单个神经元的前向传播
  2. 权重更新 :感知器的学习规则是反向传播算法的雏形
  3. 激活函数 :sign函数是最简单的激活函数,现代神经网络使用更复杂的非线性激活

当你学习更复杂的模型时,不妨回想这个简单的感知器实现,你会发现许多概念其实是一脉相承的。

更多推荐