s01 AI概述 — exercise.py 练习指南
练习目标
通过补全感知机的三个核心组件,深入理解感知机的数学原理和代码实现。完成这个练习后,你将能够:
- 用一行 NumPy 代码实现阶跃激活函数(理解向量化操作)
- 实现感知机的预测方法(理解
的完整流程) - 掌握感知机权重更新规则(理解"朝正确方向微调参数"的直觉)
预备知识
在开始练习前,确保你已经理解了以下概念(参见 demo.py 代码详解 中对这些概念的详细解释):
- 感知机的数学模型:
- 阶跃函数的定义:
- 感知机学习规则:
(仅当误分类时) - 向量化运算:
np.dot()和np.where()的用法
任务清单
任务1:实现阶跃激活函数 _activation(z)
在 PerceptronExercise 类的 _activation 方法中,你需要实现阶跃函数。
- 提示:使用
np.where(condition, value_if_true, value_if_false)实现向量化判断 - 期望行为:
时输出 时输出
- 测试方法:输入
np.array([-1, 0, 1]),期望输出[-1, 1, 1]
任务2:实现预测方法 predict(X)
在 PerceptronExercise 类的 predict 方法中,你需要完成两个步骤:
- 计算线性组合
(对所有样本批量计算) - 将
通过激活函数得到类别标签
- 提示:
- 使用
np.dot(X, self.w) + self.b进行批量计算(是 矩阵, 是 向量,结果自动广播) - 调用
self._activation()将得分转换为类别
- 使用
- 需要调用的函数:
np.dot():矩阵乘法self._activation():你在任务1中实现的阶跃函数
- 期望输出:返回形状为
的数组,每个元素为 或
任务3:实现感知机权重更新规则
在 fit 方法中,找到 TODO 3 标记的位置。当条件 y_i * z <= 0 成立(即样本被误分类)时,实现权重和偏置的更新。
- 用到的公式:
- 直觉解释:
- 如果
但预测为 :把 往 的正方向推 - 如果
但预测为 :把 往 的反方向推
- 如果
- 需要调用的变量:
self.learning_rate:学习率x_i:当前样本的特征向量y_i:当前样本的真实标签self.w:权重向量(需要就地修改)self.b:偏置标量(需要就地修改)
- 期望输出:训练完成后,对线性可分数据应达到 100% 准确率,损失曲线(误分类数)单调下降至 0
验证标准
运行 python exercise.py,如果你的实现正确:
- 训练过程中的误分类数逐渐减少到 0
- 最终打印
训练集准确率: 100.00% - 显示
✓ 完美!你的感知机实现正确,所有样本分类正确。 - 可视化图显示清晰的决策边界将红蓝两类数据完全分开
完整代码
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()