SVM 支持向量机代码改进报告
一、概述
本次改进针对 src/chap03_SVM/svm.py 中的线性 SVM 实现,修复了一个关键 Bug 并增加了四项训练优化功能。改进保持了原有算法(hinge loss + L2 正则化 + 梯度下降)不变,仅在工程实现层面进行优化。
核心改进:
- 修复 predict() 崩溃 Bug — 原代码调用
train()后再调用predict()会因y_train_unique未定义而报错 - 学习率指数衰减 — 训练初期快速下降,后期精细调优
- 训练损失监控 — 每隔固定轮数打印 loss、accuracy、lr,便于观察收敛过程
- 早停机制 — 损失不再下降时自动停止,节省计算资源
- 默认开启正则化 —
reg_lambda从 0.0 改为 0.001,防止过拟合
二、原代码问题分析
2.1 predict() 方法存在崩溃 Bug
原代码:
def predict(self, x_raw):
x = self.scaler.transform(x_raw)
score = np.dot(x, self.w) + self.b
return np.where(score >= 0, 1, -1) if -1 in self.y_train_unique else np.where(score >= 0, 1, 0)
def train_with_label_tracking(self, data_train):
self.y_train_unique = np.unique(data_train[:, 2]) # 只在这里赋值
self.train(data_train)
问题:
- predict() 依赖 self.y_train_unique 属性
- 该属性只在 train_with_label_tracking() 中赋值
- 如果直接调用 train() 后再调用 predict(),会抛出 AttributeError: 'SVM' object has no attribute 'y_train_unique'
- 需要一个专门的 wrapper 方法来避免这个 Bug,说明封装存在缺陷
2.2 训练过程无反馈
原代码:
for epoch in range(self.max_iter):
# ... 梯度计算和更新 ...
# 无任何输出
问题: - 默认 20000 次迭代,全程无输出 - 无法判断模型是否收敛、收敛速度如何 - 调试和调参非常困难
2.3 固定学习率
原代码:
self.learning_rate = learning_rate # 始终不变
问题: - 训练初期需要大学习率快速收敛 - 训练后期需要小学习率精细调优 - 固定学习率无法同时满足两者,容易在最优点附近震荡
2.4 无早停机制
问题:
- 无论是否已收敛,都跑满 max_iter 轮
- 浪费计算资源
- 过多的迭代可能导致过拟合
2.5 正则化默认关闭
原代码:
def __init__(self, learning_rate=0.1, reg_lambda=0.0, max_iter=20000):
问题:
- reg_lambda=0.0 意味着没有正则化
- SVM 的核心优势之一就是通过正则化控制模型复杂度
- 默认关闭正则化容易导致过拟合
三、改进内容详解
3.1 修复 predict() Bug
改进代码:
def train(self, data_train):
X_raw = data_train[:, :2]
y_raw = data_train[:, 2]
# 在 train() 中直接记录标签空间
self.label_set = np.unique(y_raw)
# ...
def predict(self, x_raw):
x = self.scaler.transform(x_raw)
score = np.dot(x, self.w) + self.b
# 使用 train() 中记录的 self.label_set
if -1 in self.label_set:
return np.where(score >= 0, 1, -1)
else:
return np.where(score >= 0, 1, 0)
改进点:
- 将标签记录逻辑从 train_with_label_tracking() 移入 train() 本身
- 删除了多余的 train_with_label_tracking() 方法
- train() + predict() 可以直接配合使用,无需 wrapper
3.2 学习率指数衰减
改进代码:
def __init__(self, ..., lr_decay=0.9995, ...):
self.lr_decay = lr_decay
def train(self, data_train):
lr = self.learning_rate
for epoch in range(self.max_iter):
# ... 使用 lr 进行梯度更新 ...
# 学习率衰减
lr = self.learning_rate * (self.lr_decay ** epoch)
原理:
- 学习率按 lr = lr_0 × decay^epoch 指数衰减
- 默认 decay=0.9995,即每轮衰减 0.05%
- 20000 轮后学习率衰减至初始值的约 0.004 倍
效果: - 线性数据集:lr 从 0.1 衰减至 0.000033 - 非线性数据集:lr 从 0.1 衰减至 0.004977(6000 轮时早停)
3.3 训练损失监控
改进代码:
def _compute_hinge_loss(self, X, y):
"""计算 hinge loss + L2 正则化损失"""
score = np.dot(X, self.w) + self.b
hinge = np.maximum(0, 1 - y * score)
return np.mean(hinge) + self.reg_lambda * np.dot(self.w, self.w)
def train(self, data_train):
for epoch in range(self.max_iter):
# ... 训练逻辑 ...
if (epoch + 1) % self.print_interval == 0 or epoch == 0:
loss = self._compute_hinge_loss(X, y)
pred = np.where(np.dot(X, self.w) + self.b >= 0, 1, -1)
acc = np.mean(pred == y)
print(f" epoch {epoch+1:>6d} | loss: {loss:.4f} | acc: {acc*100:.1f}% | lr: {lr:.6f}")
效果: - 每 2000 轮打印一次训练状态 - 显示当前 epoch、hinge loss、训练准确率、学习率 - 直观观察收敛过程
3.4 早停机制
改进代码:
best_loss = float('inf')
no_improve_count = 0
for epoch in range(self.max_iter):
# ... 训练逻辑 ...
if (epoch + 1) % self.print_interval == 0:
loss = self._compute_hinge_loss(X, y)
if loss < best_loss - 1e-6:
best_loss = loss
no_improve_count = 0
else:
no_improve_count += self.print_interval
if no_improve_count >= self.patience:
print(f" 早停触发于 epoch {epoch+1},最佳损失: {best_loss:.4f}")
break
原理:
- 每 print_interval 轮检查一次损失
- 如果损失连续 patience 轮没有显著下降(< 1e-6),则停止训练
- 默认 patience=2000,即连续 2000 轮无改善则早停
效果(实测):
| 数据集 | 原始轮数 | 改进后轮数 | 节省 |
|---|---|---|---|
| 线性数据 | 20000 | 16000 | 20% |
| 非线性数据 | 20000 | 6000 | 70% |
3.5 默认开启正则化
改进代码:
def __init__(self, learning_rate=0.1, reg_lambda=0.001, ...):
效果:
- reg_lambda 从 0.0 改为 0.001
- 提供适度的 L2 正则化,限制权重大小
- 防止过拟合,提高泛化能力
四、结果对比
线性数据集
| 指标 | 改进前 | 改进后 |
|---|---|---|
| 训练准确率 | 95.5% | 95.5% |
| 测试准确率 | 97.5% | 97.5% |
| 训练轮数 | 20000 | 16000(早停) |
| 训练过程 | 无输出 | 每 2000 轮打印 loss/acc/lr |
非线性数据集
| 指标 | 改进前 | 改进后 |
|---|---|---|
| 训练准确率 | 81.5% | 81.5% |
| 测试准确率 | 81.0% | 81.0% |
| 训练轮数 | 20000 | 6000(早停) |
| 训练过程 | 无输出 | 每 2000 轮打印 loss/acc/lr |
训练过程日志(线性数据集)
epoch 1 | loss: 0.8834 | acc: 96.0% | lr: 0.100000
epoch 2000 | loss: 0.1116 | acc: 95.5% | lr: 0.036797
epoch 4000 | loss: 0.1109 | acc: 95.5% | lr: 0.013534
epoch 6000 | loss: 0.1107 | acc: 95.5% | lr: 0.004977
epoch 8000 | loss: 0.1106 | acc: 95.5% | lr: 0.001831
epoch 10000 | loss: 0.1106 | acc: 95.5% | lr: 0.000673
epoch 12000 | loss: 0.1106 | acc: 95.5% | lr: 0.000248
epoch 14000 | loss: 0.1106 | acc: 95.5% | lr: 0.000091
epoch 16000 | loss: 0.1106 | acc: 95.5% | lr: 0.000033
早停触发于 epoch 16000,最佳损失: 0.1106
train accuracy: 95.5%
test accuracy: 97.5%
训练过程日志(非线性数据集)
epoch 1 | loss: 0.9460 | acc: 81.5% | lr: 0.100000
epoch 2000 | loss: 0.3947 | acc: 81.0% | lr: 0.036797
epoch 4000 | loss: 0.3947 | acc: 81.5% | lr: 0.013534
epoch 6000 | loss: 0.3947 | acc: 81.5% | lr: 0.004977
早停触发于 epoch 6000,最佳损失: 0.3947
train accuracy: 81.5%
test accuracy: 81.0%
五、新增命令行参数
| 参数 | 默认值 | 说明 |
|---|---|---|
--learning-rate |
0.1 | 初始学习率 |
--reg-lambda |
0.001 | L2 正则化系数(原默认 0.0) |
--max-iter |
20000 | 最大迭代次数 |
--lr-decay |
0.9995 | 学习率衰减系数(新增) |
--patience |
2000 | 早停耐心值(新增) |
六、总结
改进总结
本次对 svm.py 的改进聚焦于工程实践层面,不改变核心算法:
- 修复了 predict() 的属性未定义 Bug — 消除了对
train_with_label_tracking()的依赖 - 添加学习率指数衰减 — 兼顾初期快速收敛和后期精细调优
- 添加训练过程监控 — 实时显示 loss、accuracy、lr,便于调试
- 添加早停机制 — 线性数据节省 20% 迭代,非线性数据节省 70%
- 默认开启 L2 正则化 — 提高泛化能力
可继续改进的方向
- Mini-batch SGD:用小批量梯度下降替代全批量,加速大数据集训练
- 核函数扩展:引入 RBF 核处理非线性数据
- 交叉验证:自动选择最优超参数组合
- 可视化:绘制决策边界和训练 loss 曲线
七、使用方式
cd src/chap03_SVM
# 使用默认参数运行(线性数据集)
python svm.py
# 自定义参数运行
python svm.py --learning-rate 0.05 --reg-lambda 0.01 --lr-decay 0.999 --patience 3000
# 使用非线性数据集
python svm.py --train-file data/train_kernel.txt --test-file data/test_kernel.txt