从零实现SVM:Python源码拆解SMO与核技巧

发布时间:2026/10/21 21:09:07
从零实现SVM:Python源码拆解SMO与核技巧 简介这份资源是支持向量机SVM的Python实现代码包面向正在学习机器学习算法、希望从零理解SVM原理并动手实践的学生与开发者。包内共6个文件以3个Python源码文件为核心分别承担SVM主体算法实现、测试脚本与辅助处理功能另附1个编译缓存文件、1份Markdown说明文档和1份txt格式的数据集整体压缩包约9KB体量轻巧便于快速阅读与本地运行。资源围绕SVM分类器的训练与测试展开读者可借助源码理清核函数、超平面求解与样本分类的代码逻辑配合测试数据验证模型效果并通过说明文档了解项目结构与使用方式。目前已有1942人学习下载适合作为机器学习入门阶段对照理论、调试代码的实践素材。1. 从一份手写 SVM 源码说起为什么我还要看 Python 实现很多人第一次接触支持向量机都是在某门课上被拉格朗日乘子和对偶问题绕得云里雾里最后靠调包sklearn.svm.SVC蒙混过关。但真到了要改核函数、要看支持向量到底长什么样、要排查为什么模型在小样本上表现玄学的时候黑箱就不好使了。这份 SVM 的 Python 实现就是拿来拆黑箱的它用纯 Python 加 NumPy 把最大间隔分类器、软间隔、核技巧这几件事从头写了一遍适合已经会用 sklearn 但想搞懂内部机理的开发者也适合拿它当二次开发的底座比如换核函数、加多分类策略、接自己的数据管道。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲。2. 项目定位与技术选型它到底实现了哪些 SVM 能力2.1 从功能边界看这份实现覆盖了什么拿到一份源码我习惯先看它到底实现了哪些能力而不是急着跑。这份 SVM 实现的核心功能可以归成四块线性可分情况下的硬间隔分类、带松弛变量的软间隔分类、核函数支持至少覆盖线性核和 RBF 核、以及基于 SMO 的求解过程。换句话说它不是一个只会跑鸢尾花数据集的玩具而是一个把 SVM 从原始问题到对偶问题再到数值求解这条链路走通的完整实现。它没有做的事也很明确没有做多分类的 one-vs-one 或 one-vs-rest 封装没有做概率输出Platt scaling也没有做大规模数据的分布式求解。这些恰恰是二次开发最自然的切入点。理解了这个边界你就知道这份代码适合拿来学原理和做中小规模实验不适合直接上生产环境跑百万级样本。从技术选型上看它只依赖 NumPy 做矩阵运算没有引入 scipy.optimize 之类的现成优化器说明作者是刻意要把 SMO 的迭代逻辑暴露出来。这个选择对学习者非常友好因为你能看到每一次乘子更新、每一次误差缓存是怎么发生的。常见做法是用 scipy 的 minimize 直接解对偶问题代码会短很多但 SMO 的工程细节就全被藏起来了。2.2 目录结构与模块职责一份结构清晰的 SVM 实现目录通常长这样svm/ ├── svm.py # 核心类fit/predict 入口 ├── kernel.py # 核函数实现与缓存 ├── smo.py # SMO 求解器 ├── utils.py # 数据加载、标准化、切分 └── examples/ └── demo.py # 最小可运行示例svm.py里定义主类对外暴露fit(X, y)和predict(X)内部持有alphas、b、支持向量索引这些状态。kernel.py负责把原始特征映射成核矩阵RBF 核里那个 gamma 参数就在这里生效。smo.py是整份代码最值得读的部分它实现了外层循环选第一个乘子、内层循环选第二个乘子的启发式策略。utils.py做数据预处理因为 SVM 对特征尺度非常敏感标准化几乎是必须的。我一般会先读svm.py的fit方法看它怎么调用 SMO再看smo.py的主循环最后回头看kernel.py。这个顺序能让你先建立整体数据流再钻细节不至于一上来就陷进公式里。2.3 关键参数与它们对结果的影响在动手跑之前有几个参数必须先搞清楚否则调出来的结果会让你怀疑人生参数含义典型取值影响C惩罚系数0.1 ~ 100越大越不容忍误分类容易过拟合kernel核类型linear / rbf决定决策边界形状gammaRBF 核宽度0.001 ~ 10越大边界越复杂容易过拟合tol收敛容差1e-3越小迭代越久结果越精确max_iter最大迭代100 ~ 1000防止死循环C 和 gamma 是 SVM 里最玄学的两个参数网格搜索基本是标配。tol 和 max_iter 则决定了你等结果要等多久。我一般先把 tol 设成 1e-3 跑通流程确认逻辑没问题后再收紧到 1e-4 看收敛曲线。3. 把项目跑起来环境、数据与最小可运行示例3.1 环境准备与依赖安装这份实现依赖很轻Python 3.8 以上加 NumPy 就够了。如果你还想画决策边界再装个 matplotlib。我一般用虚拟环境隔离避免和系统里的包打架python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy matplotlib装完之后先别急着跑 demo用python -c import numpy; print(numpy.__version__)确认 NumPy 能正常导入。这一步看着多余但我踩过坑某些环境下 NumPy 装是装了但 BLAS 库没链接好矩阵运算会报奇怪的错提前确认能省很多排查时间。3.2 数据准备与预处理SVM 对特征尺度敏感这是血泪经验。如果一列特征范围是 0 到 1另一列是 0 到 10000那第二列会主导整个距离计算模型基本学不到第一列的信息。所以标准化是跑之前必须做的一步import numpy as np def standardize(X): # 按列计算均值和标准差 mu X.mean(axis0) sigma X.std(axis0) # 防止除零标准差为 0 的列保持原样 sigma[sigma 0] 1.0 return (X - mu) / sigma # 假设 X 是 (n_samples, n_features) 的数组 X np.array([[1.0, 200.0], [2.0, 300.0], [3.0, 150.0]]) X_std standardize(X) print(X_std)这段代码按列做 z-score 标准化mu和sigma都是长度为特征数的向量。注意sigma[sigma 0] 1.0这行如果某列所有值相同标准差是 0直接除会得到 nan把它设成 1 就安全了。标准化参数必须从训练集算出来然后应用到测试集不能各算各的否则就是数据泄露。3.3 训练与预测的最小示例环境好了、数据也标准化了接下来就是最小可运行示例。我一般用两类线性可分的数据先验证逻辑import numpy as np from svm import SVM # 构造一个简单的二分类数据集 X np.array([[1, 2], [2, 3], [3, 3], [2, 1], [3, 2], [6, 7], [7, 8], [8, 8], [7, 6], [8, 7]]) y np.array([-1, -1, -1, -1, -1, 1, 1, 1, 1, 1]) # 初始化 SVM先用线性核 model SVM(C1.0, kernellinear, tol1e-3, max_iter100) model.fit(X, y) # 预测 preds model.predict(X) acc (preds y).mean() print(f训练集准确率: {acc:.4f}) print(f支持向量个数: {len(model.support_vectors)})这段代码里SVM(C1.0, kernellinear)构造了一个软间隔线性分类器fit内部会调用 SMO 求解alphas和b。predict用决策函数sign(w·x b)输出标签。跑通之后你会看到准确率是 1.0支持向量个数大概是 2 到 4 个这符合线性可分数据的预期。如果换成kernelrbf和gamma0.5再跑一遍你会发现支持向量个数变多决策边界也更弯曲。这就是核技巧的作用把数据映射到高维空间让原本线性不可分的数据变得可分。提示第一次跑如果准确率很低先检查标签是不是 -1 和 1而不是 0 和 1。很多 SVM 实现内部用 sign 函数标签必须是 ±1。4. 核心模块拆解SMO 求解器与核函数怎么写的4.1 SMO 主循环两个乘子怎么选、怎么更新SMO 是这份代码的心脏。它的思路是每次只优化两个拉格朗日乘子把其余固定这样对偶问题就退化成一个可以解析求解的二元问题。外层循环选第一个违反 KKT 条件最严重的乘子内层循环选让目标函数下降最快的第二个乘子def smo_loop(X, y, C, tol, max_iter, kernel): n X.shape[0] alphas np.zeros(n) b 0.0 # 预计算核矩阵避免重复计算 K kernel(X, X) for it in range(max_iter): num_changed 0 for i in range(n): # 计算第 i 个样本的误差 Ei np.dot(alphas * y, K[:, i]) b - y[i] # 检查是否违反 KKT 条件 if (y[i] * Ei -tol and alphas[i] C) or \ (y[i] * Ei tol and alphas[i] 0): # 内层循环选第二个乘子 j j select_j(i, n) Ej np.dot(alphas * y, K[:, j]) b - y[j] # 保存旧值 alpha_i_old, alpha_j_old alphas[i], alphas[j] # 计算上下界 L 和 H if y[i] ! y[j]: L max(0, alphas[j] - alphas[i]) H min(C, C alphas[j] - alphas[i]) else: L max(0, alphas[i] alphas[j] - C) H min(C, alphas[i] alphas[j]) if L H: continue # 计算 eta即二阶导数 eta 2 * K[i, j] - K[i, i] - K[j, j] if eta 0: continue # 更新 alpha_j alphas[j] alpha_j_old - y[j] * (Ei - Ej) / eta alphas[j] np.clip(alphas[j], L, H) if abs(alphas[j] - alpha_j_old) 1e-5: continue # 更新 alpha_i alphas[i] alpha_i_old y[i] * y[j] * (alpha_j_old - alphas[j]) # 更新 b b1 b - Ei - y[i] * (alphas[i] - alpha_i_old) * K[i, i] \ - y[j] * (alphas[j] - alpha_j_old) * K[i, j] b2 b - Ej - y[i] * (alphas[i] - alpha_i_old) * K[i, j] \ - y[j] * (alphas[j] - alpha_j_old) * K[j, j] if 0 alphas[i] C: b b1 elif 0 alphas[j] C: b b2 else: b (b1 b2) / 2 num_changed 1 # 如果一轮下来没有任何乘子被更新说明收敛 if num_changed 0: break return alphas, b这段代码有几个关键点。Ei是第 i 个样本的预测误差y[i] * Ei用来判断是否违反 KKT 条件如果y[i]*Ei -tol且alphas[i] C说明这个样本被正确分类但还有优化空间如果y[i]*Ei tol且alphas[i] 0说明它被错误分类或者落在间隔内。eta是目标函数沿更新方向的二阶导数如果eta 0说明这个方向不是下降方向直接跳过。np.clip把alpha_j限制在[L, H]内保证满足约束。b 的更新分三种情况如果alpha_i在 (0, C) 内用 b1如果alpha_j在 (0, C) 内用 b2否则取平均。select_j是内层循环的启发式常见做法是选误差差最大的那个也就是max(|Ei - Ej|)。这份实现里我一般会加一个随机兜底避免在误差相等时死循环。4.2 核函数实现线性核与 RBF 核的矩阵化写法核函数决定了 SVM 能处理多复杂的边界。线性核就是内积RBF 核是高斯函数。矩阵化写法能避免 Python 循环速度差几十倍def linear_kernel(X, Y): # 线性核直接内积 return np.dot(X, Y.T) def rbf_kernel(X, Y, gamma0.5): # RBF 核exp(-gamma * ||x - y||^2) # 用 (a-b)^2 a^2 b^2 - 2ab 展开避免逐样本循环 X_norm np.sum(X ** 2, axis1).reshape(-1, 1) Y_norm np.sum(Y ** 2, axis1).reshape(1, -1) sq_dist X_norm Y_norm - 2 * np.dot(X, Y.T) # 数值误差可能让 sq_dist 出现极小负值截断到 0 sq_dist np.maximum(sq_dist, 0) return np.exp(-gamma * sq_dist)linear_kernel直接返回X和Y的内积矩阵形状是(n_X, n_Y)。rbf_kernel用平方展开把逐样本的距离计算变成矩阵运算X_norm是每个样本的模平方Y_norm同理np.dot(X, Y.T)是交叉项。np.maximum(sq_dist, 0)这行很重要浮点误差可能让理论上为 0 的距离变成 -1e-12开根号或取 exp 前必须截断否则会得到 nan。gamma 参数控制核的宽度gamma 越大单个样本的影响范围越小边界越复杂gamma 越小边界越平滑。我一般从1 / n_features开始试再按倍数上下搜索。4.3 预测阶段支持向量与决策函数训练完之后真正参与预测的只有支持向量也就是alpha 0的那些样本。预测时计算新样本和支持向量的核函数值加权求和再加 bdef predict(self, X): # 找出支持向量 sv_idx self.alphas 1e-5 sv self.X_train[sv_idx] sv_y self.y_train[sv_idx] sv_alpha self.alphas[sv_idx] # 计算核矩阵 K self.kernel(X, sv) # 决策函数sum(alpha_i * y_i * K(x, sv_i)) b decision np.dot(K, sv_alpha * sv_y) self.b return np.sign(decision)sv_idx用alphas 1e-5而不是 0是为了过滤掉数值误差产生的极小值。decision是每个样本的决策值正数判为正类负数判为负类np.sign直接输出 ±1。这里没有做概率输出如果你需要概率得额外做 Platt scaling那是二次开发的一个方向。注意预测时用的核函数必须和训练时完全一致包括 gamma 参数。我见过有人训练用 RBF、预测用线性核结果准确率掉到 0.5排查了半天才发现是配置没对齐。5. 避坑与排查跑 SVM 时最容易翻车的几个地方5.1 准确率一直是 0.5 左右现象模型训练完了预测结果和随机猜差不多。原因通常是标签格式不对。很多 SVM 实现内部用sign函数要求标签是 -1 和 1如果你传的是 0 和 1sign(0)会得到 0整个决策就乱了。解决办法是在fit入口加一行校验把 0 映射成 -1或者直接抛异常提示用户。我一般会在utils.py里写个check_labels函数统一处理。5.2 迭代不收敛跑了几万轮还在循环现象max_iter设了 1000跑完还没收敛支持向量个数异常多。原因多半是 C 设得太大或者 gamma 太大导致优化问题病态。解决办法是先固定 C1.0、gamma0.1 跑一遍确认能收敛后再逐步放大。另外 tol 别设太小1e-3 通常够用1e-6 会让迭代次数爆炸。如果数据本身有重叠软间隔的 C 也不能太大否则就是在逼模型硬分。5.3 核矩阵计算爆内存现象样本量到几千rbf_kernel一算就 MemoryError。原因是核矩阵是(n, n)的n10000 就是 800MB 的 float64。解决办法是小规模用完整核矩阵大规模改用分块计算或者用sklearn.metrics.pairwise.rbf_kernel的chunk_size参数。这份实现面向教学没做分块二次开发时如果数据量大这是必须补的一环。5.4 标准化参数在训练集和测试集上不一致现象训练集准确率 0.95测试集 0.6。原因是你对训练集和测试集分别做了标准化两者的均值和标准差不同导致特征空间错位。解决办法是把训练集的mu和sigma存下来测试集直接用这两个值做变换。我一般把标准化封装成一个类fit存参数transform用参数避免手滑。5.5 支持向量个数等于样本总数现象训练完发现所有样本的alpha都大于 0。原因通常是 C 太大或者 gamma 太大模型把每个样本都当成了支持向量。这在 RBF 核上尤其常见。解决办法是降低 C 和 gamma让模型有容错空间。如果数据本身噪声大硬间隔本来就不合适软间隔的 C 要调小。6. 二次开发与改造从能跑到好用要补的几件事6.1 加多分类支持这份实现是二分类的要扩展到多分类最直接的是 one-vs-rest对每个类别训练一个二分类器预测时取决策值最大的那个。改造点在svm.py外面包一层MultiSVM类内部维护一个分类器列表。训练时把当前类标为 1其余标为 -1预测时收集所有分类器的decision_function输出取 argmax。这个改造大概几十行代码但要注意类别不平衡时给每个分类器加权。6.2 换核函数与自定义核kernel.py里的核函数是硬编码的要加新核比如多项式核、sigmoid 核最干净的做法是把核函数做成可传入的参数。SVM.__init__接收一个kernel_fn默认用 RBF用户也可以传自己的函数。自定义核必须满足 Mercer 条件也就是核矩阵半正定否则 SMO 可能不收敛。我一般会加一个check_kernel函数随机采样几个点算核矩阵的特征值有负值就警告。6.3 加早停与收敛曲线现在的smo_loop只在num_changed 0时停没有中间状态输出。二次开发时我建议加一个history列表每轮记录num_changed和目标函数值跑完画出来。这样你能直观看到收敛过程调参时也有依据。目标函数就是对偶问题的值sum(alphas) - 0.5 * sum(alphas_i * alphas_j * y_i * y_j * K_ij)每轮算一次开销不大。6.4 用 sklearn 做交叉验证对比改完之后怎么验证对不对最省事的办法是和 sklearn 的SVC对比。同一份数据、同样的 C 和 gamma两边的支持向量个数和准确率应该接近。如果差很多说明你的实现有问题。我一般会写个脚本跑 5 折交叉验证把两边的准确率打出来对比。注意 sklearn 的gamma默认是1 / n_features而这份实现默认是 0.5对比时要显式对齐。6.5 一个我常犯的错误早期我改核函数的时候直接在rbf_kernel里把 gamma 写死了结果调参时怎么改SVM(gamma...)都不生效排查了半天才发现是核函数没接收这个参数。从那以后我每次加新核函数都强制走一遍「传参 → 打印 → 对比」的流程确认参数真的透传到了计算层。希望这个习惯能帮到你少走点我走过的弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询