SVM三分类实战:OvO/OvR策略、决策边界与Pipeline避坑

发布时间:2026/11/2 11:28:48
SVM三分类实战:OvO/OvR策略、决策边界与Pipeline避坑 简介SVM多分类入门实践资料面向已了解监督学习基础、希望在MATLAB中完成三分类实验的读者围绕支持向量机从二分类扩展到三分类的建模流程展开。压缩包共6个文件约4KB以5个m脚本和1个data数据文件为主脚本覆盖训练、核函数、分类器与结果绘图等环节data文件提供可直接载入的鸢尾花样本便于把特征矩阵、类别标签与模型预测串联起来。内容聚焦一对一策略下的多个二分类SVM构建、fitcsvm参数设置与predict预测也可据此观察线性核等选择对分类边界的影响。已有6351人学习适合课程实验、算法作业和自学练手时对照脚本梳理数据预处理、模型训练、结果可视化与简单调参思路快速搭建可运行的三分类SVM小案例。1. 三分类的坑不在 SVM而在你怎么把二分类器拼起来有人拿 SVM 做三分类训练集准确率刷到 0.98混淆矩阵一打出来中间那一类被两边啃掉大半。问题不在 SVM 本身——它从诞生起就是个二分类器三分类是靠多个二分类器组合出来的组合方式不同切出来的决策面就不同翻车的方式也不一样。这份资源把数据构造、多分类策略、核函数选型、决策边界可视化这几段完整链路摊开适合两类人刚学完对偶形式和 KKT 条件、想看看代码里边界到底怎么落地以及手上有个三五类任务、逻辑回归和树模型效果平平、想换 SVM 试试水。核心就一句让你看清三分类的决策面是怎么被切出来的以及动哪个参数它就会变形。2. 工程骨架与技术选型目录结构、依赖清单和三条主线拿到一个分类 demo最怕的是一上来就from sklearn import svm三行跑完后面想改就无从下手。这个项目按「造数 → 训练 → 评估 → 画图」拆成四条可独立运行的脚本先把骨架立住再往里面填算法。2.1 目录结构每个文件只负责一件事svm-tri-class/ ├── src/ │ ├── config.py # 全局配置随机种子、路径、类别数 │ ├── make_dataset.py # 合成三簇数据控制重叠度 │ ├── train.py # 训练 网格搜索 模型落盘 │ ├── evaluate.py # 混淆矩阵、macro F1、分类报告 │ └── plot_boundary.py # 网格采样画决策边界 ├── outputs/ │ ├── svm_pipe.joblib # 训练好的 Pipeline │ └── figures/ │ └── boundary.png ├── requirements.txt └── README.md这个拆法不是摆样子。config.py单独拎出来是因为随机种子、类别数、测试集比例在四个脚本里都要用散落在各处的话改一次centers3得翻四个文件改漏一处就会出现「训练用三分类、评估按四分类算」这种低级事故。train.py只负责产出模型evaluate.py只负责读模型出指标想换评估口径不用重训几秒钟跑完。plot_boundary.py独立出来还有一个隐性好处画图是重活meshgrid步长 0.02 时一张图要预测几万个网格点跟训练流程耦合在一起会拖慢整个迭代节奏。outputs/目录在.gitignore里排除但保留目录结构和.gitkeep。模型文件、图片这些产物提交进仓库几次迭代下来仓库体积就能到几十兆回滚还容易覆盖掉别人正在用的模型。2.2 技术选型为什么是 scikit-learn 的 SVC 而不是手写 SMO层次选型选它的理由数值计算numpy网格采样、投票统计、核矩阵手算都靠它模型sklearn.svm.SVC底层封装 libsvm多分类、核函数、软间隔参数齐全预处理StandardScalerSVM 对特征尺度极敏感标准化几乎算硬性前置调参GridSearchCV交叉验证 网格搜索一体比手写 for 循环可靠评估sklearn.metrics混淆矩阵、macro F1、分类报告都是现成的绘图matplotlibcontourf画决策面比 seaborn 直接持久化joblib比 pickle 对 numpy 数组更友好这里最容易被质疑的是「为什么用现成的 SVC」。教学场景下自己实现一版 SMO 当然更有收获但要处理工作集选择、违反 KKT 条件的判定、启发式更新这些细节代码量几百行起步而且不规则的数据上收不收敛全靠调。实际工程里SVC背后是 libsvm二十多年打磨过的实现稳定性和速度都不是临时手写能比的。真正需要自己动手的是多分类的组合逻辑和评估口径这两块 sklearn 做了封装但没藏住细节值得拆。另一个选型细节是核函数默认用rbf而不是linear。三簇数据如果是线性可分的linear核足够且可解释但真实的分类任务里类别边界往往弯曲rbf映射到高维后能切出闭合的曲面代价是多一个gamma要调。项目里的网格搜索把两种核都放进了候选让数据说话。2.3 数据构造为什么用二维、为什么能控制重叠# src/make_dataset.py import numpy as np from sklearn.datasets import make_blobs from sklearn.model_selection import train_test_split from config import RANDOM_STATE def build(n_samples600, centers3, n_features2, cluster_std1.2): X, y make_blobs( n_samplesn_samples, centerscenters, # 类别数改这里就能扩到 N 分类 n_featuresn_features, # 二维便于可视化真实任务改大 cluster_stdcluster_std, # 越大簇越松散、重叠越多难度旋钮 center_box(-8.0, 8.0), # 簇心分布范围避免全挤在原点附近 random_stateRANDOM_STATE, ) return train_test_split(X, y, test_size0.3, stratifyy, random_stateRANDOM_STATE)n_features2不是偷懒是为了可视化。决策边界只有在二维平面上才画得出来三维就得切面再高只能靠投影直观性断崖式下降。业界常见做法是「二维调通、高维照搬」因为 SVM 的行为跟维度关系不大跟特征尺度和核参数关系很大二维完全能把这些规律摸清楚。cluster_std是整个项目里最重要的难度旋钮。设成 0.5 时三簇分得很开用linear核都接近 100%设成 2.0 时簇边缘互相渗透准确率掉到 0.7 左右这时候才能看出C和gamma的调参价值。建议初跑者先用 0.8 拿到一个漂亮结果建立信心再往上加观察混淆矩阵里哪两类先混在一起——这比盯着准确率数字有用得多。stratifyy也值得说一句。数据量 600 时三类的样本数大致均衡不加分层问题不大但如果你把n_samples调到 150随机切分很可能出现某类在测试集只有个位数样本指标剧烈抖动。分层切分保证训练集和测试集的类别比例一致属于写一次省一堆事的操作。3. 多分类策略拆解OvO 投票和 OvR 的决策值聚合SVM 原生的输出只有 ±1要处理 K 类就得拆。拆法主流两种一对一One-vs-OneOvO和一对其余One-vs-RestOvR。两者的分类器数量、训练成本、决策面形状都不同而 sklearn 在这里埋了一个非常容易踩的语义差异。3.1 OvO 的投票机制K(K-1)/2 个分类器怎么表决OvO 的思路是两两配对三分类就配出 (0,1)、(0,2)、(1,2) 三个二分类器每个只用对应的两类样本训练。预测时新样本过三个分类器每个分类器投一票给它认为的类最后票数最多的类胜出。K 类时分类器数量是 K(K-1)/2三分类是 3 个十分类是 45 个——增长速度是平方级类别一多训练成本就压不住。但 OvO 有个被低估的优点每个分类器只需要看两类样本单次训练的数据量小而且两两之间的边界通常比「一类对其余」更容易找。当类别数在 10 以内、每类样本不太多时OvO 往往是更优解。下面这段手写实现把投票过程完整暴露出来就是为了让你看清「票数怎么数」# 手写 OvO训练 K(K-1)/2 个二分类器并投票 import numpy as np from itertools import combinations from sklearn.svm import SVC def fit_ovo(X, y, **svc_kwargs): classes np.unique(y) models {} for i, j in combinations(classes, 2): mask (y i) | (y j) # 只取这两类的样本 clf SVC(**svc_kwargs).fit(X[mask], y[mask]) models[(i, j)] clf return classes, models def predict_ovo(classes, models, X): votes np.zeros((X.shape[0], len(classes)), dtypeint) margins np.zeros_like(votes, dtypefloat) # 累计决策值用于打平兜底 for (i, j), clf in models.items(): pred clf.predict(X) dec clf.decision_function(X) # 正负对应 i / j for k in range(X.shape[0]): votes[k, pred[k]] 1 margins[k, pred[k]] abs(dec[k]) # 票数相同时比较累计间隔间隔大的类胜出 return classes[np.lexsort((margins, votes), axis1)[:, -1]]mask (y i) | (y j)是 OvO 的核心取数逻辑漏掉这一步就会把三类样本一起喂给二分类器SVM 内部会把其中两类合并成负例训出来的模型语义全错。decision_function返回的是到超平面的有符号距离正负号对应两类的归属绝对值大小反映置信度——票数打平时用它兜底比随机选更合理。最后的np.lexsort先按margins升序、再按votes升序排取最后一列就是票数最高且间隔最大的类。3.2 sklearn 的实际行为训练永远是 OvO输出形状才受参数控制这一段是整篇最容易翻车的地方。很多人看到SVC(decision_function_shapeovr)就以为模型按 OvR 训练了其实sklearn 的 SVC 在多分类时底层始终用 OvO 训练decision_function_shape只影响decision_function()返回矩阵的形状不影响训练出来的模型也不影响predict()的结果。参数名起得有误导性血泪经验。from sklearn.svm import SVC # 两种设置下predict 的结果完全一致 clf_ovr SVC(kernelrbf, decision_function_shapeovr).fit(X_tr, y_tr) clf_ovo SVC(kernelrbf, decision_function_shapeovo).fit(X_tr, y_tr) print((clf_ovr.predict(X_te) clf_ovo.predict(X_te)).all()) # True # 差别只在 decision_function 的列数 print(clf_ovr.decision_function(X_te).shape) # (n_samples, 3) 每类一个聚合值 print(clf_ovo.decision_function(X_te).shape) # (n_samples, 3) 两两组合的原始值三分类时两种形状恰好都是 3 列看起来没区别但含义完全不同ovr模式下每一列是「该类对其余所有类」的聚合决策值列顺序严格对应clf.classes_ovo模式下每一列对应一个两两组合三分类时列的顺序是 (0,1)、(0,2)、(1,2)四分类时变成 6 列。拿ovo的输出直接喂给需要「每类一个分数」的下游逻辑比如多分类的阈值调整会静默错位而且不报错。真要用 OvR 训练得手动封装对每个类训练一个「该类 vs 其余」的二分类器预测时取decision_function值最大的类。它的好处是分类器只有 K 个、训练快坏处是每个分类器都面对类别不平衡一类对 K-1 类需要配class_weightbalanced而且各类的决策值尺度不一致直接比大小并不公平。三分类这种小规模场景我一般直接吃 OvO 的默认行为省事且效果稳。3.3 决策值、概率和预测结果的对齐关系还有一个隐藏的对齐问题predict()返回的是类别标签比如 0/1/2decision_function()返回的列顺序对应classes_升序排列后的唯一值而predict_proba()的列顺序也跟classes_一致但需要probabilityTrue才可用。三者必须对齐否则你看着概率最高的类跟预测标签对不上会怀疑人生。clf SVC(kernelrbf, probabilityTrue, random_state42).fit(X_tr, y_tr) print(clf.classes_) # [0 1 2] proba clf.predict_proba(X_te) # 列 0/1/2 分别对应 classes_[0..2] pred clf.predict(X_te) print((proba.argmax(axis1) pred).mean()) # 通常接近 1但不保证 100%最后一句不保证 100% 是实话probabilityTrue走的是 Platt 缩放在 SVM 输出上额外拟合一层 sigmoid得到的概率和decision_function的排序可能在边界样本上不一致。做业务决策时如果两个都要用最好统一口径。另外probabilityTrue会触发内部 5 折交叉验证来拟合概率参数训练时间大概翻几倍三分类 600 样本感知不明显上万样本时就是分钟级的差别。4. 训练主链路标准化、网格搜索与决策边界可视化前面把策略讲清了这一章落到能跑的代码。整条链路是「Pipeline 组装 → 网格搜索 → 模型落盘 → 混淆矩阵 → 边界图」每一段都有个容易忽视的细节。4.1 标准化为什么必须放进 Pipeline 而不是提前做SVM 的本质是在特征空间里找最大间隔超平面间隔用欧氏距离度量。如果两个特征的量纲差两个数量级比如一个在 0~1一个在 0~10000距离几乎完全由大尺度特征决定小尺度特征等于没参与。rbf核更甚gamma直接乘在距离平方上尺度不统一时模型基本学不到东西。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC pipe Pipeline([ (scaler, StandardScaler()), # 均值 0、方差 1 (svc, SVC(kernelrbf, C1.0, gammascale, class_weightbalanced, random_state42)), ])标准化写进 Pipeline 而不是在train_test_split之后手动fit_transform原因只有一个交叉验证时防止数据泄漏。手动提前标准化的写法是用全量训练数据算均值和方差这均值方差里已经包含了验证折的信息CV 分数会虚高等上线换真实测试集就露馅。Pipeline 保证每一折只用自己那部分训练数据拟合 scaler评估才可信。gammascale是 sklearn 的默认值等于1 / (n_features * X.var())随特征数自动缩放比写死的 0.1 稳。特征数从 2 改到 20 时scale会自动跟着变小不用重新调。4.2 网格搜索线性核和 RBF 核分开搜两种核的超参空间不同混在一起搜会把网格撑得很大。分开写成列表搜索量小一半from sklearn.model_selection import GridSearchCV, StratifiedKFold param_grid [ {svc__kernel: [linear], svc__C: [0.1, 1, 10, 100]}, {svc__kernel: [rbf], svc__C: [0.1, 1, 10, 100], svc__gamma: [scale, 0.01, 0.05, 0.1, 1.0]}, ] cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) gs GridSearchCV(pipe, param_grid, cvcv, scoringf1_macro, # 三分类用 macro避免被大类带偏 n_jobs-1, refitTrue) gs.fit(X_train, y_train) print(gs.best_params_, gs.best_score_)svc__kernel里的双下划线是 Pipeline 传参的固定语法前段是步骤名(svc, SVC(...))里的svc后段是参数名。写成kernel或svm__kernel都会报ValueError: Invalid parameter新手最常卡在这。scoringf1_macro而不是默认的accuracy是因为三分类里只要有一类样本偏少准确率就会被另外两类拉高macro F1 对每一类一视同仁更能暴露问题。配合class_weightbalanced使用小类的召回率会明显改善。结合起来linear分支 4 组、rbf分支 20 组5 折交叉验证共 120 次训练600 样本的规模在普通笔记本上几秒钟出结果。4.3 画决策边界网格点必须走同一个 Pipeline这一步是踩坑重灾区也是最值得抄走的一段import numpy as np import matplotlib.pyplot as plt def plot_boundary(pipe, X, y, step0.02, axNone): ax ax or plt.gca() x_min, x_max X[:, 0].min() - 1, X[:, 0].max() 1 y_min, y_max X[:, 1].min() - 1, X[:, 1].max() 1 xx, yy np.meshgrid(np.arange(x_min, x_max, step), np.arange(y_min, y_max, step)) grid np.c_[xx.ravel(), yy.ravel()] # 展平成 (N, 2) # 关键用 pipe.predictscaler 会自动作用在网格点上 Z pipe.predict(grid).reshape(xx.shape) ax.contourf(xx, yy, Z, alpha0.25, levels[-0.5, 0.5, 1.5, 2.5]) # 三类的分界层级 ax.scatter(X[:, 0], X[:, 1], cy, edgecolorsk, s18) ax.set_xlabel(feature 1); ax.set_ylabel(feature 2) return axpipe.predict(grid)和pipe.named_steps[svc].predict(grid)看着差不多结果差很远。前者会把StandardScaler作用到网格点上后者跳过标准化用原始尺度的网格点去问一个在标准化空间里训出来的模型画出来的边界会整体偏移、形状变形你还以为是核参数调坏了。这个 bug 我调过不止一次。levels[-0.5, 0.5, 1.5, 2.5]是因为contourf按数值分层填色类别标签是 0/1/2边界要落在相邻整数中点所以取 ±0.5、1.5、2.5。少写一层就会有一类不被填充图上看不出它被切到哪去了。step0.02是精度和速度的折中。网格范围大约 18×18步长 0.02 意味着 81 万个预测点rbf核下要几十秒步长 0.1 只要 3 万个点一秒出图但边缘会有明显锯齿。调参阶段用 0.1 快速看趋势定稿再用 0.02这是我一般的做法。4.4 混淆矩阵比准确率信息量大得多from sklearn.metrics import confusion_matrix, classification_report y_pred gs.predict(X_test) print(classification_report(y_test, y_pred, digits3)) print(confusion_matrix(y_test, y_pred))classification_report给出每类的 precision/recall/F1 和支持数。三分类里如果某类 recall 明显偏低说明它被另外两类吞了去混淆矩阵里看行真实类哪一列预测类数大就知道是哪两个类在打架。这时候别急着调C先回头看数据这两类在特征空间里是不是本来就重叠cluster_std调大一点重叠就是无法消除的噪声硬调参数只会过拟合训练集。一个健康的训练结果大致是这样训练集和测试集准确率差距在 5% 以内每类 F1 都在 0.85 以上混淆矩阵对角占绝对优势。如果训练集 1.0、测试集 0.75典型的过拟合往下压C、减小gamma如果两边都 0.6欠拟合往反方向调或者换rbf核。5. 避坑与排查五条踩过的真坑算法本身不难难的是各种静默错误——不报错结果就是不对。下面五条按「现象 → 原因 → 解决」记录都是复现过的。5.1 训练集准确率 1.0测试集掉到 0.7现象gs.best_score_显示 0.99predict在测试集上只有 0.7混淆矩阵里三类互相乱窜。原因C设得过大比如 1000加上gamma很大模型把每个训练点都当支持向量间隔被压到极窄等于把训练集背下来了。SVM 的软间隔本来就是用来容忍噪声的C越大惩罚越重容错空间越小。解决先把C拉回 0.1~10 的区间重搜gamma从scale起步。同时确认标准化有没有漏——没标准化时模型为了拟合大尺度特征会疯狂扭曲边界表现也是过拟合。诊断手段是画学习曲线训练分数高、验证分数低且两者不收敛就是过拟合无疑。5.2 网格搜索分数漂亮实际预测一塌糊涂现象GridSearchCV报best_score_ 0.95把best_estimator_拿去预测新数据效果 0.6 都不到。原因在进GridSearchCV之前对全量数据做了StandardScaler().fit_transform(X)然后才切分。scaler 的均值方差里包含了验证折的信息CV 评估的是「见过答案」的模型。解决所有预处理必须放进 Pipeline让GridSearchCV在每一折内部单独拟合。判断方法很简单看代码里fit_transform是不是出现在train_test_split之前或者出现在GridSearchCV之外。5.3 决策边界图整体偏移跟散点对不上现象散点明明是三类分明的簇画出来的彩色区域却整体往一个方向偏边缘还有一条直直的切痕。原因画网格时用了svc.predict(grid)而不是pipe.predict(grid)网格点没经过StandardScaler落在了和训练数据不同的尺度空间里。那条直切痕通常是网格范围边界上的外推行为。解决统一走pipe.predict。手头只有裸模型没有 Pipeline 时必须显式scaler.transform(grid)再预测并且这个 scaler 必须是训练时用的那个实例不能重新fit。5.4 某一类召回率极低但整体准确率还行现象三分类准确率 0.85看着能交差但分类报告里第 2 类的 recall 只有 0.4。原因类别不平衡。合成数据时如果某一簇的样本天然偏少或者center_box让某一簇被挤到边缘SVM 会倾向于把边界往小类那边推——间隔最大化在不平衡数据上偏向多数类。解决SVC(class_weightbalanced)会按类别频率反比加权代价是可能牺牲一点整体准确率换取小类召回。评估口径也同步换成f1_macro或balanced_accuracy别只看 accuracy 自欺欺人。5.5probabilityTrue之后训练慢了一个数量级现象加了个probabilityTrue想输出置信度训练时间从 3 秒变成 40 秒数据量越大差距越夸张。原因sklearn 为了给出校准后的概率内部要对 SVM 的决策值再做一次 Platt 缩放而 Platt 缩放需要 5 折交叉验证重新训练模型。等于训练成本乘以 6 倍。解决只需要类别标签时坚决不开probability用decision_function拿间隔值就够。真需要概率考虑把probability关掉、下游用 softmax 对decision_function做归一化虽然校准性差一些但速度快得多或者接受成本在最终模型上才开调参阶段一律关掉。6. 二次开发从三分类扩到 N 分类与自定义核这个项目的价值不在跑出一个 0.95 的分数在于它把可改的点都留出来了。接手之后最常做的两件事换类别数、换核函数。6.1 类别数改成 5看哪些地方会崩make_dataset.build(centers5)一行就能扩到五分类但代码里有两处硬编码需要跟着改。第一处是画图的levels[-0.5, 0.5, 1.5, 2.5]五分类得生成 6 个层级import numpy as np n_classes len(np.unique(y)) levels np.arange(-0.5, n_classes, 1.0) # [-0.5, 0.5, ..., 4.5] ax.contourf(xx, yy, Z, alpha0.25, levelslevels)第二处是decision_function的输出列数。三分类 OvO 是 3 列五分类变成 10 列任何假设「列数等于类别数」的下游代码都会静默出错。最稳妥的写法是永远通过clf.classes_和clf.decision_function_shape反推含义别写死列索引。6.2 自定义核函数换成拉普拉斯核试试SVC接受任意 callable 作为kernel签名要求f(X, Y) - (n_samples_X, n_samples_Y)的核矩阵import numpy as np from sklearn.svm import SVC def laplacian_kernel(X, Y, sigma1.0): # 拉普拉斯核exp(-||x-y||_1 / sigma)比 rbf 更抗重尾噪声 diff np.abs(X[:, None, :] - Y[None, :, :]).sum(axis2) return np.exp(-diff / sigma) svc SVC(kernellaplacian_kernel, C1.0).fit(X_train, y_train)X[:, None, :] - Y[None, :, :]用广播一次性算出所有样本对的差得到(n_X, n_Y, n_features)再沿最后一维求和变成曼哈顿距离。这个写法的内存占用是 O(n_X·n_Y·d)样本上千就会吃掉几个 G实际用的时候要么分块、要么直接算距离矩阵传进去。sigma的作用类似gamma越小边界越弯曲。换成拉普拉斯核后在cluster_std较大的数据上通常能看到对噪声更鲁棒的表现代价是核矩阵计算更慢而且没法用gammascale自动缩放得自己估。6.3 验证改动的三条硬标准改完别急着看准确率。我一般强制走三步一是固定三个随机种子42、7、2024各跑一遍看指标方差是否在 0.02 以内方差大说明改动对数据划分太敏感二是画学习曲线横轴训练样本数、纵轴f1_macro训练和验证曲线的间隙决定了还能不能靠加数据提升三是用混淆矩阵确认没有某一类被系统性牺牲三分类里出现某类 recall 低于 0.7 就必须回头查类别平衡和核参数。这三步是我在几个分类任务上反复吃过亏之后固化的习惯——有次只改了一行gamma单次跑分从 0.91 涨到 0.94高兴了半小时换个种子跑直接掉到 0.82白忙一场。从那以后每次改超参或换核函数我都强制跑一遍多种子 学习曲线宁可多花二十分钟也不拿一次运气好的结果当结论。希望这份拆解帮到你把三分类这条链路跑通之后扩类、换核、接真实数据都是顺理成章的事。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询