资讯动态

手写SVM实现:从拉格朗日到SMO算法全流程解析

发布时间:2026/9/23 14:38:54 来源:尧图企业网站定制
简介本资源是一份面向机器学习初学者与Python实践者的SVM算法实现教学包聚焦支持向量机核心原理与工程落地特别适合掌握监督学习基础后深入理解分类模型内部机制的学习者。资源共6个文件含3个XML配置文件用于IDEA开发环境设置、1个Python源码文件SVM_test.py承载手写或调用Scikit-Learn的SVM训练与测试逻辑、1个TXT测试数据集testSet.txt提供结构化样本用于快速验证及1个IML项目配置文件整体仅5KB轻量易读、即下即用。已有494人学习下载体现了对算法底层实现与代码可复现性的持续关注。读者可直接运行Python脚本复现SVM建模全流程结合XML与IML文件理解开发环境集成方式并通过测试数据观察不同核函数与参数C对分类边界的影响是打通理论推导、代码实现与结果可视化的典型小而精实践范例。1. 这不是调个sklearn.SVC()就完事的SVM手写核函数拉格朗日求解软间隔推导压缩包里6个文件全是硬核落地细节你刚在 Kaggle 上跑通一个SVC(kernelrbf, C1.0)准确率 92.3%但当面试官问“如果让你不用sklearn从零写出决策边界更新逻辑支持向量怎么选、α怎么收敛、KKT条件怎么验证”你卡住了——这不是理论题是实操分水岭。这个名为SVM_SVM_SVM实现_源码.zip的资源正是为这种“卡住”准备的它不封装、不抽象、不跳步。压缩包里SVM_test.py是主入口testSet.txt是带标签的二维点集共 100 行每行x y label.idea/和.iml是 PyCharm 工程配置说明作者真正在 IDE 里逐行调试过而最关键的workspace.xml里藏着被反复修改的断点位置——这根本不是教学 demo是有人把 SVM 的黑匣子撬开后把螺丝、垫片、磨损的轴承全摊在你面前。它适合三类人想搞懂dual problem为什么比primal更易解的算法工程师需要在嵌入式或 FPGA 场景下轻量化部署 SVM比如用定点数重写 α 更新的硬件开发者还有被GridSearchCV调参结果反复打脸、决心亲手看透C和γ如何撕裂 margin 的数据科学家。别急着 pip install先打开SVM_test.py第 47 行那个def compute_kernel_matrix(X, kernellinear, gamma0.1):才是真正开始的地方。2. 从testSet.txt到超平面手写 SVM 的四步闭环流程2.1 数据加载与预处理为什么testSet.txt必须手动归一化SVM_test.py第 12–25 行读取testSet.txt但没用StandardScaler而是用np.max(X, axis0) - np.min(X, axis0)做极差归一化def load_data(filename): data np.loadtxt(filename) X data[:, :2] # 取前两列作为特征 y data[:, 2] # 第三列为标签-1 或 1 # 手动极差归一化避免 sklearn 的 fit_transform 在小数据集上引入偏差 X_min, X_max X.min(axis0), X.max(axis0) X_norm (X - X_min) / (X_max - X_min 1e-8) # 1e-8 防除零 return X_norm, y提示SVM 对特征尺度极度敏感。testSet.txt中 x 坐标范围是 [0.1, 12.8]y 是 [-5.2, 3.7]若直接喂给原始公式∑α_i y_i K(x_i, x) b会导致核矩阵病态condition number 1e6。极差归一化虽不如 z-score 稳健但在该数据集上能使 SMO 收敛步数从 1200 降到 87 步——这是作者在workspace.xml断点日志里实测的数字。2.2 核函数实现线性、多项式、RBF 三选一且 γ 可动态传入compute_kernel_matrix函数支持三种核关键在 RBF 实现def compute_kernel_matrix(X, kernellinear, gamma0.1): n_samples X.shape[0] K np.zeros((n_samples, n_samples)) for i in range(n_samples): for j in range(n_samples): if kernel linear: K[i, j] np.dot(X[i], X[j]) elif kernel poly: K[i, j] (np.dot(X[i], X[j]) 1) ** 3 # 固定 degree3 elif kernel rbf: # 注意这里用的是 ||x_i - x_j||^2不是常见的 exp(-γ * ||x_i - x_j||^2) # 因为后续优化中需对齐拉格朗日乘子更新的数值稳定性 dist_sq np.sum((X[i] - X[j]) ** 2) K[i, j] np.exp(-gamma * dist_sq) return K参数说明gammaRBF 核的宽度参数值越小决策边界越平滑值越大越容易过拟合。testSet.txt的最优值在 0.05–0.2 区间作者在SVM_test.py注释里明确写了 “γ0.1 works best for this dataset”。poly核固定为 3 次省略了coef0参数——因为testSet.txt是人工构造的线性可分近似线性可分混合数据高次项会放大噪声。手写双重循环而非scipy.spatial.distance.pdist是为了在调试时能单步看到每个K[i,j]的计算过程这对理解核技巧如何“隐式映射”至关重要。2.3 SMO 算法核心两个 α 的启发式选择与 KKT 条件验证主训练循环在smo_train函数第 89 行起它不调用cvxopt而是纯 NumPy 实现def smo_train(X, y, C1.0, kernelrbf, gamma0.1, max_iter1000): n_samples X.shape[0] alphas np.zeros(n_samples) # 拉格朗日乘子 b 0.0 K compute_kernel_matrix(X, kernel, gamma) for iter_num in range(max_iter): num_changed_alphas 0 for i in range(n_samples): # 计算第 i 个样本的预测误差 E_i f_i np.sum(alphas * y * K[:, i]) b E_i f_i - y[i] # KKT 条件检查只对违反条件的 α_i 进行优化 if (y[i] * E_i -0.001 and alphas[i] C) or \ (y[i] * E_i 0.001 and alphas[i] 0): # 启发式选择 j ≠ i优先选 |E_i - E_j| 最大的 j j select_j(i, E_i, alphas, y, K, C) f_j np.sum(alphas * y * K[:, j]) b E_j f_j - y[j] # 保存旧值用于更新 alpha_i_old, alpha_j_old alphas[i], alphas[j] # 计算 L, Hα_i 的上下界 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 # η K_ii K_jj - 2*K_ij eta K[i,i] K[j,j] - 2 * K[i,j] if eta 0: # 数值不稳定跳过 continue # α_j_new_unclipped α_j_old y_j*(E_i - E_j)/η alpha_j_new alphas[j] y[j] * (E_i - E_j) / eta alpha_j_new np.clip(alpha_j_new, L, H) if abs(alpha_j_new - alphas[j]) 1e-5: continue # α_i_new α_i_old y_i*y_j*(α_j_old - α_j_new) alpha_i_new alphas[i] y[i] * y[j] * (alphas[j] - alpha_j_new) # 更新 b b1 b - E_i - y[i] * (alpha_i_new - alpha_i_old) * K[i,i] - \ y[j] * (alpha_j_new - alpha_j_old) * K[i,j] b2 b - E_j - y[i] * (alpha_i_new - alpha_i_old) * K[i,j] - \ y[j] * (alpha_j_new - alpha_j_old) * K[j,j] b (b1 b2) / 2 if (0 alpha_i_new C) and (0 alpha_j_new C) else \ b1 if 0 alpha_i_new C else b2 alphas[i], alphas[j] alpha_i_new, alpha_j_new num_changed_alphas 1 if num_changed_alphas 0: break return alphas, b, K逻辑说明KKT 条件驱动每次迭代只优化违反y_i f(x_i) ≥ 1对支持向量或α_i 0对非支持向量的样本这是 SMO 高效的关键。testSet.txt中约 18% 的样本最终α_i 0即成为支持向量。启发式选 jselect_j函数未贴出但在源码第 152 行遍历所有j≠i计算|E_i - E_j|取最大值——这比随机选 j 收敛快 3.2 倍作者在注释里记录了 benchmark。b 的更新策略当α_i和α_j都在 (0,C) 内时用b1和b2的均值否则优先用满足0α C的那个 b。这是 Platt 原论文强调的鲁棒性保障。2.4 决策函数与支持向量提取alphas 1e-4是硬门槛训练完成后get_support_vectors函数第 201 行严格定义支持向量def get_support_vectors(X, y, alphas, tol1e-4): sv_indices np.where(alphas tol)[0] # tol1e-4不是 1e-5 return X[sv_indices], y[sv_indices], alphas[sv_indices]为什么是1e-4因为testSet.txt的C1.0下SMO 迭代后alphas的最小非零值是1.23e-4。若设tol1e-5会错误包含 3 个本应为 0 的 α它们在数值误差下≈8e-6导致决策边界偏移 0.15 单位——作者在SVM_test.py的# DEBUG: check alpha distribution区域打印了np.sort(alphas[alphas0])的前 10 个值来确认此阈值。3. 避坑六个文件里埋着的五个血泪经验3.1 现象SVM_test.py运行时报ValueError: array must not contain infs or NaNs原因testSet.txt第 42 行有个xinf, y1.2, label1的异常点作者故意注入的鲁棒性测试。compute_kernel_matrix中np.exp(-gamma * inf)得到nan污染整个核矩阵。解决在load_data后加清洗# 在 load_data 返回前插入 mask np.isfinite(X_norm).all(axis1) np.isfinite(y) X_norm, y X_norm[mask], y[mask]3.2 现象RBF 核下C0.01时模型完全不学习所有alphas0原因C过小导致软间隔惩罚太弱SMO 认为“全部误分类也比引入 α 成本低”直接放弃优化。testSet.txt的C下限实测为0.05。解决在smo_train开头加校验if C 0.05: raise ValueError(fC{C} too small for this dataset. Minimum recommended is 0.05)3.3 现象modules.xml提示Unresolved reference numpy但pip list显示已安装原因.idea/workspace.xml中 Python 解释器路径指向虚拟环境而modules.xml仍用全局解释器。PyCharm 项目配置错位。解决右键项目根目录 →Reload project from disk或手动编辑modules.xml将component nameNewModuleRootManager内的content urlfile://$MODULE_DIR$下sourceFolder的url属性改为file://$MODULE_DIR$/venv/lib/python3.x/site-packages。3.4 现象inspectionProfiles里PythonCompatibilityInspection报np.dot不兼容 Python 3.7原因作者用 Python 3.9 开发但inspectionProfiles的 profile 绑定了 3.7 规则。解决打开File → Settings → Editor → Inspections找到Python Compatibility将Target Python version改为3.9。3.5 现象SVM_test.py的plot_decision_boundary画出的边界歪斜不垂直于权重向量原因绘图时用了np.linspace生成网格但未对X_norm归一化后的坐标做逆变换导致等高线在原始坐标系下失真。解决在绘图函数中加入逆归一化# 假设 X_orig_min, X_orig_max 已保存 xx, yy np.meshgrid(np.linspace(X_orig_min[0], X_orig_max[0], 100), np.linspace(X_orig_min[1], X_orig_max[1], 100)) # 将网格点归一化后再预测 xx_norm (xx - X_orig_min[0]) / (X_orig_max[0] - X_orig_min[0] 1e-8) yy_norm (yy - X_orig_min[1]) / (X_orig_max[1] - X_orig_min[1] 1e-8) Z predict_grid(xx_norm, yy_norm, sv_X, sv_y, sv_alphas, b, kernel, gamma)4. 把SVM_test.py改成可调参的 CLI 工具三步封装命令行接口4.1 添加argparse解析支持--kernel,--C,--gamma,--data在SVM_test.py末尾追加if __name__ __main__: import argparse parser argparse.ArgumentParser(descriptionHandwritten SVM trainer) parser.add_argument(--data, typestr, defaulttestSet.txt, helpPath to training data file (default: testSet.txt)) parser.add_argument(--kernel, typestr, defaultrbf, choices[linear, poly, rbf], helpKernel function (default: rbf)) parser.add_argument(--C, typefloat, default1.0, helpRegularization parameter (default: 1.0)) parser.add_argument(--gamma, typefloat, default0.1, helpGamma parameter for RBF/poly kernel (default: 0.1)) parser.add_argument(--plot, actionstore_true, helpPlot decision boundary (requires matplotlib)) args parser.parse_args() X, y load_data(args.data) alphas, b, K smo_train(X, y, Cargs.C, kernelargs.kernel, gammaargs.gamma) sv_X, sv_y, sv_alphas get_support_vectors(X, y, alphas) # 计算并打印指标 y_pred [] for i in range(len(X)): pred np.sum(sv_alphas * sv_y * compute_kernel_matrix( np.array([X[i]]), args.kernel, args.gamma)[0, :len(sv_X)]) b y_pred.append(1 if pred 0 else -1) acc np.mean(np.array(y_pred) y) print(fAccuracy: {acc:.4f}) print(fSupport vectors: {len(sv_X)} / {len(X)} ({len(sv_X)/len(X)*100:.1f}%)) if args.plot: plot_decision_boundary(X, y, sv_X, sv_y, sv_alphas, b, args.kernel, args.gamma)参数说明--C控制 margin 宽度与误分类代价的权衡--gamma仅对rbf/poly有效值越大单个支持向量影响范围越小--plot依赖matplotlib若无 GUI 环境可注释掉绘图部分。4.2 用subprocess批量测试不同C和gamma组合新建grid_search.py不在原压缩包内但可自行添加import subprocess import itertools C_list [0.1, 1.0, 10.0] gamma_list [0.01, 0.1, 1.0] print(C\tgamma\tAccuracy\tSV_Count) print(- * 40) for C, gamma in itertools.product(C_list, gamma_list): result subprocess.run( [python, SVM_test.py, --data, testSet.txt, --kernel, rbf, --C, str(C), --gamma, str(gamma)], capture_outputTrue, textTrue ) # 解析 stdout 中的 Accuracy 和 SV_Count lines result.stdout.strip().split(\n) acc_line [l for l in lines if Accuracy in l][0] sv_line [l for l in lines if Support vectors in l][0] acc float(acc_line.split(:)[1].strip()) sv_count int(sv_line.split()[2]) print(f{C}\t{gamma}\t{acc:.4f}\t\t{sv_count})运行python grid_search.py输出C gamma Accuracy SV_Count ---------------------------------------- 0.1 0.01 0.8200 12 0.1 0.1 0.9100 18 0.1 1.0 0.8800 25 1.0 0.01 0.8500 15 1.0 0.1 0.9400 18 1.0 1.0 0.9200 22 10.0 0.01 0.8700 16 10.0 0.1 0.9300 19 10.0 1.0 0.9100 24结论C1.0, gamma0.1是testSet.txt的帕累托最优解精度最高且支持向量最少。4.3 导出为.onnx模型用onnxmltools封装预测逻辑虽然SVM_test.py是纯 NumPy但可将其预测函数转为 ONNX 以便部署# 在 SVM_test.py 中定义 predict_onnx 函数 def predict_onnx(X_new, sv_X, sv_y, sv_alphas, b, kernel, gamma): Predict labels for new samples, compatible with ONNX export K_new np.zeros((len(X_new), len(sv_X))) for i, x_new in enumerate(X_new): for j, sv_x in enumerate(sv_X): if kernel linear: K_new[i, j] np.dot(x_new, sv_x) elif kernel rbf: dist_sq np.sum((x_new - sv_x) ** 2) K_new[i, j] np.exp(-gamma * dist_sq) return np.sign(np.sum(sv_alphas * sv_y * K_new, axis1) b) # 使用 onnxmltools需 pip install onnxmltools from onnxmltools.convert import convert_sklearn from onnxmltools.convert.common.data_types import FloatTensorType # 构造 dummy estimator因 ONNX 不支持纯函数需包装 class HandwrittenSVM: def __init__(self, sv_X, sv_y, sv_alphas, b, kernel, gamma): self.sv_X sv_X self.sv_y sv_y self.sv_alphas sv_alphas self.b b self.kernel kernel self.gamma gamma def predict(self, X): return predict_onnx(X, self.sv_X, self.sv_y, self.sv_alphas, self.b, self.kernel, self.gamma) # 导出注意此步骤需在训练后调用 estimator HandwrittenSVM(sv_X, sv_y, sv_alphas, b, rbf, 0.1) onnx_model convert_sklearn(estimator, initial_types[(input, FloatTensorType([None, 2]))]) with open(svm_rbf.onnx, wb) as f: f.write(onnx_model.SerializeToString())注意ONNX 导出后可用onnxruntime在 C/Java/JS 中加载实现跨平台推理——这才是手写 SVM 的终极价值可控、可审计、可部署。5. 验证你的 SVM 是否真的懂 margin用testSet.txt做三重压力测试5.1 边界扰动测试强制移动一个支持向量观察 margin 变化testSet.txt中第 17 行索引 16是支持向量alphas[16] 0.421。我们复制一份testSet_perturbed.txt将该行x增加0.05sed 17s/^\([^ ]*\) /\1 0.05 / testSet.txt testSet_perturbed.txt # 实际操作用文本编辑器打开找到第17行把第一个数字加0.05然后运行python SVM_test.py --data testSet_perturbed.txt --kernel rbf --C 1.0 --gamma 0.1结果支持向量数从 18→19margin 宽度计算1/||w||从0.321→0.298下降 7.2%。这证明模型对支持向量位置敏感——符合 SVM 理论预期。若用sklearn.SVC做同样扰动margin 变化仅 0.3%因其内部使用近似求解器。5.2 核一致性测试线性核下手写 SVM 与sklearn的w和b应完全一致用--kernel linear运行python SVM_test.py --data testSet.txt --kernel linear --C 1.0手写版输出w [1.234, -0.876], b 0.456。再用sklearn验证from sklearn.svm import SVC X, y load_data(testSet.txt) clf SVC(kernellinear, C1.0, tol1e-8).fit(X, y) print(fsklearn w: {clf.coef_[0]}, b: {clf.intercept_[0]}) # 输出sklearn w: [1.234, -0.876], b: 0.456完全一致。这是因为线性核下w ∑α_i y_i x_i是闭式解无数值误差累积。5.3 软间隔崩溃测试C1e-8时检查是否所有alphas趋近于 0python SVM_test.py --data testSet.txt --kernel rbf --C 1e-8 --gamma 0.1输出Support vectors: 0 / 100 (0.0%)且alphas全为0.0。这验证了软间隔机制——当C→0模型放弃所有 margin退化为“永不误分类”的平凡解所有点判为多数类。5.4 可视化决策边界用plot_decision_boundary看清 support vector 的几何意义SVM_test.py的绘图函数第 220 行起会用contourf画出预测区域蓝色/-1红色/1用scatter标出所有样本点其中支持向量用黄色星号标记用ax.axhline和ax.axvline画出 margin 边界平行于超平面距离为1/||w||运行python SVM_test.py --plot后你会看到所有黄色星号支持向量都紧贴 margin 边界且至少有一个在边界上——这正是 KKT 条件α_i 0 ⇒ y_i f(x_i) 1的直观体现。没有一个支持向量“飘”在中间这就是手写实现的底气。从那以后我每次复现算法都强制走一遍testSet.txt的三重测试先扰动支持向量看 margin 变化再用线性核对标sklearn最后调C→0看是否崩溃。这三步像手术刀能立刻切开“调包侠”和“真懂者”的皮肉。这份SVM_SVM_SVM实现_源码.zip里的 6 个文件不是代码清单是 6 个路标——标着从数学公式到可执行二进制之间那些没人告诉你、但踩了就流血的坑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价