Skip to content

s01 AI概述 — exercise.py 练习指南

Download exercise.py

练习目标

通过补全感知机的三个核心组件,深入理解感知机的数学原理和代码实现。完成这个练习后,你将能够:

  1. 用一行 NumPy 代码实现阶跃激活函数(理解向量化操作)
  2. 实现感知机的预测方法(理解 wx+bsign 的完整流程)
  3. 掌握感知机权重更新规则(理解"朝正确方向微调参数"的直觉)

预备知识

在开始练习前,确保你已经理解了以下概念(参见 demo.py 代码详解 中对这些概念的详细解释):

  • 感知机的数学模型:y^=sign(wx+b)
  • 阶跃函数的定义:sign(z)=+1 (z0),1 (z<0)
  • 感知机学习规则:ww+ηyixi(仅当误分类时)
  • 向量化运算:np.dot()np.where() 的用法

任务清单

任务1:实现阶跃激活函数 _activation(z)

PerceptronExercise 类的 _activation 方法中,你需要实现阶跃函数。

  • 提示:使用 np.where(condition, value_if_true, value_if_false) 实现向量化判断
  • 期望行为
    • z0 时输出 +1
    • z<0 时输出 1
  • 测试方法:输入 np.array([-1, 0, 1]),期望输出 [-1, 1, 1]

任务2:实现预测方法 predict(X)

PerceptronExercise 类的 predict 方法中,你需要完成两个步骤:

  1. 计算线性组合 z=wX+b(对所有样本批量计算)
  2. z 通过激活函数得到类别标签
  • 提示
    • 使用 np.dot(X, self.w) + self.b 进行批量计算(X(n,d) 矩阵,w(d,) 向量,结果自动广播)
    • 调用 self._activation() 将得分转换为类别
  • 需要调用的函数
    • np.dot():矩阵乘法
    • self._activation():你在任务1中实现的阶跃函数
  • 期望输出:返回形状为 (n_samples,) 的数组,每个元素为 +11

任务3:实现感知机权重更新规则

fit 方法中,找到 TODO 3 标记的位置。当条件 y_i * z <= 0 成立(即样本被误分类)时,实现权重和偏置的更新。

  • 用到的公式
    • ww+ηyixi
    • bb+ηyi
  • 直觉解释
    • 如果 yi=+1 但预测为 1:把 wxi 的正方向推
    • 如果 yi=1 但预测为 +1:把 wxi 的反方向推
  • 需要调用的变量
    • self.learning_rate:学习率 η
    • x_i:当前样本的特征向量
    • y_i:当前样本的真实标签
    • self.w:权重向量(需要就地修改)
    • self.b:偏置标量(需要就地修改)
  • 期望输出:训练完成后,对线性可分数据应达到 100% 准确率,损失曲线(误分类数)单调下降至 0

验证标准

运行 python exercise.py,如果你的实现正确:

  1. 训练过程中的误分类数逐渐减少到 0
  2. 最终打印 训练集准确率: 100.00%
  3. 显示 ✓ 完美!你的感知机实现正确,所有样本分类正确。
  4. 可视化图显示清晰的决策边界将红蓝两类数据完全分开

完整代码

py
# -*- coding: utf-8 -*-
"""
===============================================================================
s01_ai_overview/code/exercise.py — 感知机练习
===============================================================================
本练习文件中,感知机模型的核心部分被替换为了 TODO 注释。
你的任务是完成这些 TODO,使感知机能够正确训练和预测。

练习目标:
  1. 理解感知机的激活函数(阶跃函数)的数学形式
  2. 掌握感知机权重更新的核心规则
  3. 完成预测方法,将训练好的模型用于分类

提示:
  - 感知机的激活函数:sign(z) = 1 (z >= 0) 或 -1 (z < 0)
  - 权重更新规则:w ← w + η * y_i * x_i(当样本被误分类时)
  - 预测:对每个样本 x,计算 sign(w·x + b)

运行方式:
  python exercise.py

如果你正确实现了代码,你应该看到:
  - 训练过程中的误分类数逐渐减少到 0
  - 训练集准确率应为 100%(因为是线性可分数据)
  - 可视化图显示清晰的决策边界
===============================================================================
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib
# 中文字体配置
matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False  # 修复负号显示


def generate_linearly_separable_data(n_samples=100, random_seed=42):
    """
    生成线性可分的二分类数据集。

    参数:
        n_samples: int, 每类样本数
        random_seed: int, 随机种子

    返回:
        X: np.ndarray, 形状 (2*n_samples, 2)
        y: np.ndarray, 形状 (2*n_samples,),标签为 -1 或 +1
    """
    np.random.seed(random_seed)
    # 生成正类数据(均值在 [2, 2],标签 +1)
    X_pos = np.random.randn(n_samples, 2) + np.array([2.0, 2.0])
    y_pos = np.ones(n_samples)
    # 生成负类数据(均值在 [-2, -2],标签 -1)
    X_neg = np.random.randn(n_samples, 2) + np.array([-2.0, -2.0])
    y_neg = -np.ones(n_samples)
    # 合并并打乱
    X = np.vstack([X_pos, X_neg])
    y = np.hstack([y_pos, y_neg])
    shuffle_idx = np.random.permutation(len(y))
    return X[shuffle_idx], y[shuffle_idx]


class PerceptronExercise:
    """
    感知机分类器(练习版)。

    部分关键方法留空,需要你完成 TODO 实现。

    属性:
        w: np.ndarray, 权重向量
        b: float, 偏置
        learning_rate: float, 学习率 η
        max_epochs: int, 最大训练轮数
        losses: list, 每轮的误分类样本数
    """

    def __init__(self, learning_rate=0.01, max_epochs=1000):
        """初始化模型参数。"""
        self.learning_rate = learning_rate
        self.max_epochs = max_epochs
        self.w = None
        self.b = None
        self.losses = []

    # ======================================================================
    # TODO 1: 实现激活函数
    # ======================================================================
    # 感知机使用阶跃函数(Step Function)作为激活函数。
    # 输入 z 是一个数值或数组,你需要返回:
    #   +1 当 z >= 0
    #   -1 当 z < 0
    #
    # 提示:使用 np.where() 可以优雅地实现向量化操作。
    # 示例:np.where(condition, value_if_true, value_if_false)
    # ======================================================================
    def _activation(self, z):
        """
        阶跃激活函数。

        参数:
            z: np.ndarray, 输入值

        返回:
            np.ndarray, 激活后输出(+1 或 -1)
        """
        # TODO: 实现阶跃函数
        # 如果 z >= 0,返回 1;否则返回 -1
        # 请用一行代码完成
        pass  # <-- 替换为你的代码

    # ======================================================================
    # TODO 2: 实现预测方法
    # ======================================================================
    # 预测方法需要做两件事:
    #   1. 计算线性组合 z = w·x + b(对所有样本同时计算)
    #   2. 将 z 通过激活函数得到类别标签
    #
    # 提示:使用 np.dot(X, self.w) + self.b 进行批量计算
    # ======================================================================
    def predict(self, X):
        """
        对样本 X 进行类别预测。

        参数:
            X: np.ndarray, 形状 (n_samples, n_features)

        返回:
            np.ndarray, 形状 (n_samples,),预测类别(+1 或 -1)
        """
        # TODO: 实现预测
        # 步骤 1: 计算线性组合 z = w·X + b
        # 步骤 2: 通过激活函数得到最终类别
        pass  # <-- 替换为你的代码

    # ======================================================================
    # TODO 3: 实现感知机权重更新规则
    # ======================================================================
    # 在下面的 fit() 方法中,找到 TODO 3 标记的位置。
    # 当样本 (x_i, y_i) 被误分类时(即 y_i * (w·x_i + b) <= 0),
    # 你需要按以下规则更新权重和偏置:
    #   self.w ← self.w + η * y_i * x_i
    #   self.b ← self.b + η * y_i
    #
    # 直觉解释:
    #   - 如果 y_i=+1 但预测为 -1:把 w 往 x_i 的正方向推
    #   - 如果 y_i=-1 但预测为 +1:把 w 往 x_i 的反方向推
    # ======================================================================
    def fit(self, X, y):
        """
        训练感知机模型。

        参数:
            X: np.ndarray, 形状 (n_samples, n_features)
            y: np.ndarray, 形状 (n_samples,),标签为 -1 或 +1
        """
        n_samples, n_features = X.shape

        # 初始化权重和偏置
        self.w = np.random.randn(n_features) * 0.01
        self.b = 0.0
        self.losses = []

        for epoch in range(self.max_epochs):
            n_errors = 0

            for i in range(n_samples):
                x_i = X[i]
                y_i = y[i]

                # 计算线性得分 z = w·x_i + b
                z = np.dot(self.w, x_i) + self.b

                # 检查是否误分类
                if y_i * z <= 0:
                    # ==================================================
                    # TODO 3: 在这里实现感知机的权重更新规则
                    # ==================================================
                    # 提示:你需要更新 self.w 和 self.b
                    # 公式:w ← w + η * y_i * x_i
                    #       b ← b + η * y_i
                    # 其中 η 是 self.learning_rate
                    # ==================================================
                    pass  # <-- 替换为你的代码

                    n_errors += 1

            self.losses.append(n_errors)

            if n_errors == 0:
                print(f"感知机在第 {epoch + 1} 轮收敛!所有样本分类正确。")
                break

        print(f"训练完成。共 {epoch + 1} 轮。")
        print(f"最终权重 w = {self.w}, 偏置 b = {self.b:.4f}")

    def decision_function(self, X):
        """计算决策函数值(原始得分 w·x+b)。"""
        return np.dot(X, self.w) + self.b


def plot_results(model, X, y):
    """绘制决策边界和训练损失曲线。"""
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # 子图 1:决策边界
    ax = axes[0]
    ax.scatter(X[y == 1, 0], X[y == 1, 1], c='red', marker='o',
               edgecolors='k', s=60, label='类别 +1', alpha=0.7)
    ax.scatter(X[y == -1, 0], X[y == -1, 1], c='blue', marker='s',
               edgecolors='k', s=60, label='类别 -1', alpha=0.7)

    x_min, x_max = ax.get_xlim()
    w1, w2 = model.w[0], model.w[1]
    b_val = model.b
    x1_line = np.linspace(x_min, x_max, 100)
    if abs(w2) > 1e-10:  # 避免除零
        x2_line = -(w1 / w2) * x1_line - (b_val / w2)
        ax.plot(x1_line, x2_line, 'g-', linewidth=2, label='决策边界')

    ax.set_xlabel('特征 x1', fontsize=12)
    ax.set_ylabel('特征 x2', fontsize=12)
    ax.set_title('感知机决策边界', fontsize=14)
    ax.legend()
    ax.grid(True, alpha=0.3)

    # 子图 2:损失曲线
    ax = axes[1]
    ax.plot(range(1, len(model.losses) + 1), model.losses,
            'b-o', markersize=4, linewidth=1.5)
    ax.set_xlabel('Epoch(训练轮数)', fontsize=12)
    ax.set_ylabel('误分类样本数', fontsize=12)
    ax.set_title('训练过程中的误分类数变化', fontsize=14)
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('perceptron_exercise_results.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("\n图片已保存为 perceptron_exercise_results.png")


def main():
    """主函数:测试你的实现是否正确。"""
    print("=" * 60)
    print("感知机练习 — 请完成代码中的 TODO 标记")
    print("=" * 60)

    # 1. 生成数据
    print("\n[1] 生成线性可分数据...")
    X, y = generate_linearly_separable_data(n_samples=100, random_seed=42)
    print(f"数据形状: X={X.shape}, y={y.shape}")
    print(f"类别分布: +1 有 {np.sum(y == 1)} 个, -1 有 {np.sum(y == -1)} 个")

    # 2. 训练模型
    print("\n[2] 训练感知机...")
    model = PerceptronExercise(learning_rate=0.1, max_epochs=500)
    model.fit(X, y)

    # 3. 评估
    print("\n[3] 评估模型...")
    y_pred = model.predict(X)
    accuracy = np.mean(y_pred == y)
    print(f"训练集准确率: {accuracy:.2%}")

    if accuracy == 1.0:
        print("✓ 完美!你的感知机实现正确,所有样本分类正确。")
    elif accuracy > 0.5:
        print("⚠ 你的感知机已经学到了一些规律,但还没有完全收敛。请检查 TODO 实现。")
    else:
        print("✗ 准确率太低。请检查你的 TODO 实现是否正确。")

    # 4. 可视化
    print("\n[4] 可视化结果...")
    plot_results(model, X, y)

    print("\n" + "=" * 60)
    print("练习结束!")
    print("=" * 60)


if __name__ == '__main__':
    main()