简介一份基于MATLAB的SVM三分类完整实现面向机器学习初学者、算法研究者以及需要在不平衡或多类别数据上快速验证分类效果的工程人员。压缩包仅有6个文件包括5个m脚本与1份iris.data标准鸢尾花数据集其中训练、分类、核函数、绘图和主程序模块分工明确整体仅约4KB适合直接阅读与二次修改。实现采用一对一策略把三分类拆解成多个二分类SVM模型配合fitcsvm、predict等函数覆盖数据准备、核函数选择、训练预测和结果可视化整个流程描述中还给出了多个模型融合判断类别的思路。已有6348人学习下载。读者可对照代码理解最优超平面、支持向量、线性与高斯核等关键概念也能直接运行Iris数据复现效果替换自有特征与标签后用于其他三分类场景。整体轻量、结构清晰是入门SVM多分类的实用范本。1. SVM 做三分类难的不是算法而是多类决策SVM(支持向量机)实现数据的三分类听起来像是一个算法选型问题但真正上手做过的人都会告诉你SVM 天生是二分类器所谓三分类核心是「怎么把多个二分类结果汇总成一个可信的三分类预测」。这个问题解决不好训练集上 0.9 的准确率换一批数据就掉到 0.55混淆矩阵里第三类几乎全军覆没。我见过很多人拿默认参数的 SVC 直接跑三分类发现结果不如预期就开始怀疑核函数、怀疑数据质量其实根因通常不在模型本身而在于多分类策略、特征尺度和类别权重这三件事没有摆对位置。本文会先讲清 OvO 和 OvR 两条路线再给出最小可跑代码、数据预处理、参数调优和几条可以直接照抄的排查记录。这三部分内容适合谁手里有几千条以内、特征维度不高、类别边界还算清晰的数据集想快速得到一个可解释且能上线的三分类模型的人。如果你手上已经是十万级样本或超高维文本特征SVM 的核矩阵计算会逼你换模型但就算到时候本文第 5 章的排查思路依然能帮你定位问题。2. 三分类的最小可跑代码一对多与一对一的取舍2.1 为什么 SVC 一次只能输出二分类SVM 的优化目标是找最大间隔超平面它的数学形式只回答「正类还是负类」这一个问题。三个类别互斥的分类任务必须被拆成多个二分类子问题再把子问题的结果汇总。常见的拆法有两种一对多One-vs-Rest简称 OvR和一对一One-vs-One简称 OvO。OvR 的做法是每次拿一个类别当正样本其余两个类别的样本合并当负样本三分类时共训练 3 个分类器。OvO 则是把三个类别两两配对共 C(3,2)3 个子分类器。三分类时两者子模型数量相同但训练样本的组成方式完全不同——OvR 里每个子问题都要拿全部样本参与而 OvO 每个子问题只用两个类别的样本训练更轻但总共要维护的模型数量在类别数变多时会按平方增长。预测阶段两者的汇总方式也不同。OvR 直接取三个分类器得分最高的类别解释起来很直观OvO 则是六个两两分类结果投票票数相同时会产生平票区间需要额外规则处理。三分类场景下我一般优先选 OvR理由很简单决策函数可解释排查和调参都更直接。2.2 一份最小可跑的三分类代码先给你一段可以直接复制运行的代码数据用模拟的三分类样本重点看 SVC 的配置方式。from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.metrics import classification_report # 生成三分类模拟数据300个样本、4个特征、3个类别 X, y make_classification( n_samples300, n_features4, n_informative3, n_redundant0, n_classes3, n_clusters_per_class1, random_state42, ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42, ) model SVC( kernelrbf, C1.0, gammascale, decision_function_shapeovr ) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))这段代码里有几个关键参数需要特别说明。decision_function_shapeovr作用在预测阶段它把底层的二分类器输出转换成每个类别的得分预测时取得分最高者这也是三分类中最常用的配置。stratifyy保证三个类别在训练集和测试集里的比例一致避免某个类别在测试集里占比过大或过小导致评估结果失真。gammascale是 RBF 核宽度的自动估计方式scikit-learn 会按 1/(特征数 × 特征方差) 计算初始值对特征尺度差异较大的数据比gammaauto稳得多。C1.0是误分类惩罚的倒数这里先用默认值第 4 章会专门讲怎么调。还需要知道一个底层细节SVC 训练时背后的 libsvm 实现其实按一对一方式训练多个二分类器decision_function_shape只改变输出形式和预测投票逻辑。这一点不影响上面的代码运行但如果你想用decision_function做置信度分析必须明白你拿到的分数到底是怎么来的。2.3 三分类时选 OvO 还是 OvR三个类别时两种策略的子模型数相同但选型依然有讲究。给你一张对比表方便直接照着判断对比项OvR一对多OvO一对一子分类器数量3 个3 个每个子问题的样本量全部样本只用两个类别的样本预测方式取得分最高类别两两投票可能平票单模型训练成本较高较低decision_function 输出维度3 列每类一列3 列每对类别一列推荐场景类别数少、需要可解释性类别多、样本不均匀三分类任务上我一般直接用 OvR因为三个分类器的输出就是「属于第 0 类 / 第 1 类 / 第 2 类」的得分一目了然。OvO 的优势在类别数超过 10 时才明显比如 20 个类别时 OvO 每个子问题只用很小一部分样本训练快很多但投票机制在平票时怎么处理又是一个需要额外代码的麻烦。3. 让 RBF 核正常工作数据标准化与流水线的三个细节3.1 量纲差异如何破坏三分类边界SVM 的决策边界由样本之间的距离计算决定RBF 核内部算的是 exp(-gamma × ||x-x||²)也就是高斯核下的欧氏距离。假设一个特征量级是几千另一个特征量级是零点几欧氏距离会被前一个特征完全主导后一个特征对边界的贡献趋近于零。三分类比二分类更敏感因为同一批样本要被多个子分类器复用。某个特征的尺度偏差不是只影响一个决策边界而是同时污染三对二分类问题最终表现为某些类别之间分得开、另一些类别彻底混在一起。常见的解决办法是 Z-score 标准化把每个特征缩放到均值 0、方差 1而不是用 MinMaxScaler——SVM 对中心化尤其敏感RBF 核的样本点之间需要围绕原点均匀分布效果才稳定。3.2 用 Pipeline 把标准化和训练绑在一起很多人在标准化这里踩过一个隐蔽的坑先在全量数据上调用fit_transform再切 train/test最后发现调参时验证分数虚高上线后回不到那个水平。原因很简单——标准化器已经提前看见了测试集的信息这属于信息泄露。避免这个问题的标准做法是把 StandardScaler 和 SVC 一起装进 Pipeline让交叉验证的每一折都独立完成「先算训练折的均值和方差再转换训练折和验证折」。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe Pipeline([ (scaler, StandardScaler()), (svm, SVC(kernelrbf, C1.0, gammascale, class_weightbalanced)), ]) pipe.fit(X_train, y_train) print(pipe.score(X_test, y_test))Pipeline 的意义不只是代码整洁它把「标准化参数只来自训练集」这件事变成了结构上的保证。网格搜索时这一点更重要GridSearchCV(pipe, ...)在每一折都会重新拟合整个流水线标准化器拿到的均值方差只来自该折的训练子集验证子集的样本不会以任何形式参与计算。换一种写法先用scaler.fit_transform(X)处理全量数据再切分结果看起来可能更漂亮但那不是模型真正的泛化能力只是泄露后的乐观估计。三分类的类别多、样本容易被核函数重叠这种泄露对调参的误导比二分类更明显。3.3 类别不平衡时 class_weight 怎么设三分类数据里三个类别占比接近时class_weight可以不设默认为每个样本相同权重。但当你发现某一类只占全部样本的 8%模型预测结果里完全没有这一类时问题基本不在核参数而在类别权重。class_weightbalanced会让每个类别的权重与该类别样本数成反比样本数越少的类别单个样本的损失贡献越大。这样做会带来一个很实际的现象训练集整体准确率会下降因为模型不再无脑偏向多数类但混淆矩阵里小类的召回率会明显回升。判断该不该用class_weight的标准很简单先跑一次默认配置打印混淆矩阵观察对角线以外的小类有没有大批量被预测成其他类。如果有加上class_weightbalanced再看。注意这个参数和C的作用是叠加的调完class_weight后网格搜索的范围需要重新确认否则最好的参数组合可能落在搜索区间的边界上。4. 网格搜索与验证曲线把 C 和 gamma 调出稳定边界4.1 C 和 gamma 在这条流水线里的真实角色C是正则化参数控制间隔最大化与误分类惩罚之间的权衡。C越大模型越不愿意在训练集上犯错决策边界越贴近训练样本过拟合风险随之上升C越小边界越平滑但可能出现欠拟合。gamma控制 RBF 核的宽度gamma越小每个训练样本的影响范围越大、边界越平滑gamma越大影响范围越小边界越绕容易逐点贴合训练数据。三分类时这两个参数会同时作用到多个子分类器上。假设三个类别里有两类边界很近gamma偏大时子分类器为了拟合这两个类别的细微差异会把边界绕得很复杂而第三个类别又因为远离其他两类被孤立地划分出来——最终整体准确率看起来还行但新样本稍微偏移一点就翻车。所以三分类调参的任务不是让单个参数最优而是让一组参数在三个子问题上都保持稳定。我一般会先画验证曲线定位合理区间再做小范围网格搜索两个方法配合使用。4.2 用验证曲线观察过拟合拐点验证曲线的思路很直接固定一个参数比如gamma让它从很小到很大扫一组值同时记录训练集和交叉验证集的准确率观察两条曲线的变化关系。import numpy as np from sklearn.model_selection import validation_curve param_range np.logspace(-2, 1, 10) train_scores, valid_scores validation_curve( pipe, X_train, y_train, param_namesvm__gamma, param_rangeparam_range, cv5, scoringaccuracy, ) for gamma, tr_mean, va_mean in zip( param_range, train_scores.mean(axis1), valid_scores.mean(axis1) ): print(fgamma{gamma:.3f} train{tr_mean:.4f} valid{va_mean:.4f})注意param_namesvm__gamma这个写法Pipeline 里子组件的参数要用双下划线穿透只写gamma会报错。输出结果里你会看到典型的趋势gamma很小时训练集和验证集分数都低这是欠拟合gamma增大后训练集分数持续上涨但验证集分数涨到某个点开始回落这个回落点就是过拟合的开始也是参数应该取的位置。三分类时如果验证曲线显示三个类别中某两个特别难分可以参考单类别的f1_score而不是只看整体准确率。验证曲线不会直接告诉你类别间的问题但能帮你判断每个子分类器的复杂度是否已经超出数据能支撑的范围。4.3 网格搜索和它的两个细节gamma区间确定后用GridSearchCV把C和gamma一起搜一遍比分别调要高效得多因为两者的组合效应经常是耦合的。from sklearn.model_selection import GridSearchCV param_grid { svm__C: [0.1, 1, 10, 100], svm__gamma: [0.01, 0.1, 1, 10], } grid GridSearchCV( pipe, param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best cv score:, grid.best_score_)这里有两个细节是我每次都会检查的。第一看最优参数是否落在搜索范围的边界上——如果C100是当前网格的最大值说明这个方向还没探到头需要扩大搜索范围重跑否则你拿到的只是一个被截断的「局部最优」。第二best_score_是训练集内部的交叉验证结果不是测试集分数测试集只能用来做最终一次性评估不能拿它反复筛选参数。三分类的网格搜索还有一个容易被忽视的点scoringaccuracy对类别不平衡不敏感如果数据存在明显的不平衡建议把scoring换成f1_macro让网格搜索同时兼顾三个类别的表现而不是只奖励多数类。调参阶段常用参数汇总如下方便你快速配置参数名作用调参建议C误分类惩罚的倒数越大越容易过拟合从 0.1 到 100 按 10 倍步进搜索gammaRBF 核宽度越小越平滑从 0.01 到 10 配合验证曲线定位class_weight类别权重balanced 适合不平衡数据数据不平衡时优先开启decision_function_shapeovr 或 ovo影响决策输出三分类默认 ovr5. 三分类实战避坑五条让模型翻车的典型案例5.1 坑一不标准化训练集 0.85测试集 0.55现象RBF 核下训练集准确率 0.85测试集只有 0.55换数据分布稍微偏移一点分数掉得更夸张。原因四个特征里有一个量级是千位数其余都在零点几浮动。欧氏距离被大规模特征独裁三个子分类器的边界都建立在同一根「拐杖」上。这根拐杖在训练集里表现正常测试集一换就暴露了。解决把 StandardScaler 放进 Pipeline让每个特征缩放到均值 0、方差 1。改完之后再看混淆矩阵你会发现原来混乱的类别对会明显分开。这一条是所有 SVM 实战笔记里出现频率最高的血泪经验三分类尤其不能跳过。5.2 坑二类别不平衡时总正确率 0.92 但小类全挂现象三个类别占比约 85%、10%、5%模型整体 accuracy 0.92 看起来很健康但第 3 类的召回率是 0——预测结果里完全没有第 3 类。原因软间隔的损失函数被多数类样本淹没少数类即使全被分错损失总和也不足以让决策边界移动。更隐蔽的是整体准确率因为多数类占比高而显得很好掩盖了小类全灭的事实。解决把class_weight设为balanced再看分类报告里第 3 类的召回率是否回升。调完之后整体准确率大概率会掉几个点这是正常的代价你需要的不是高 accuracy而是三个类别都能用的模型。5.3 坑三gamma 拉满训练集全对、验证集全错现象gamma设为 100训练集准确率直接 100%验证集掉到 0.6决策边界几乎绕着每个训练样本转圈。原因核宽度过窄RBF 的影响范围缩到样本点附近。模型记住了每个训练样本的位置没有学到类别间的泛化规律。三分类时这个问题会在多个子分类器上同时出现验证曲线的分数落差会比二分类更夸张。解决用验证曲线从gamma0.01扫到gamma10找到验证集分数开始下降的拐点。记住一个规律训练集分数一路上涨不是好消息验证集分数停止上涨的位置才是最优区间。5.4 坑四predict_proba 与 decision_function 排序不一致现象SVC 默认probabilityFalse时predict_proba不可用打开probabilityTrue后得到的概率值与decision_function的得分排名对不上。原因probabilityTrue会额外做 Platt 缩放通过交叉验证拟合一个 sigmoid 映射把决策函数得分转换成概率。样本量小、类别不平衡时这个映射会被少数类干扰输出概率不再忠实反映得分排序。解决三分类的置信度排序直接用decision_function不要依赖predict_proba。第 6 章会给出具体做法——拿 OvR 模式下每个类别的得分做排序和筛选比概率值更稳定。5.5 坑五先全量标准化再切 train/test现象交叉验证分数 0.93测试集评估也正常但部署到真实数据后效果明显下滑排查半天找不出原因。原因标准化器先对全量数据fit_transform再切 train/test均值方差里已经混入了测试集的信息。交叉验证时每个验证折都可能泄漏到训练过程中分数虚高属于信息泄露的一种隐蔽形式。解决把 StandardScaler 放进 Pipeline让它在每一折交叉验证中只对训练折计算均值方差验证折只做转换。改完后再跑一遍交叉验证分数会变低但那个低一点的分数才是你上线后能期待的真实水平。6. 用 decision_function 给三分类结果排序置信度筛选与误判热力图模型上线后产品方最常问的不是准确率而是「这条样本你有多大把握」。predict只返回类别标签不提供任何把握程度的信号。要回答这个问题需要从decision_function里拿三类得分再计算 Top1 与 Top2 之间的差值作为置信度。scores grid.best_estimator_.decision_function(X_test) order np.argsort(scores, axis1)[:, ::-1] top1 order[:, 0] confidence ( scores[np.arange(len(scores)), top1] - scores[np.arange(len(scores)), order[:, 1]] )这段代码在 OvR 模式下拿到的是每个类别的独立得分三列得分相互之间没有归一化关系。argsort按行对得分降序排列confidence表示最高分与次高分之间的差距——差值越大模型对这次预测越有把握。拿到置信度后可以做两件事。第一设置阈值做拒识差值低于 0.3 的样本不进入业务决策转入人工处理或二次模型。阈值的选择要看你业务上能接受多少误判建议先在测试集上画出置信度分布观察误分类样本是不是集中在低置信区间如果是这个筛选方法就是有效的。第二做误判热力图对top1的预测结果和y_test计算混淆矩阵把误判集中的类别对找出来。三分类里最常见的误判模式是相邻类别互认画一次热力图比盯着 accuracy 数字有效得多。我个人的习惯是每次跑完新数据先把置信度差值的分布图存下来看一眼低置信尾巴有多长。尾巴越长模型离下一次翻车越近。这个习惯帮我避过不少无声退化的坑也建议你试试看希望帮到你。本文还有配套的精品资源点击获取