ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SVM三分类实战:原理、调参与避坑指南

SVM三分类实战:原理、调参与避坑指南 简介一份基于MATLAB的SVM支持向量机三分类实现资源面向机器学习初学者、模式识别研究者及需要在MATLAB中构建多分类模型的工程师既可作为课程设计参考也可作为理解SVM多分类扩展的配套代码。压缩包共6个文件大小仅4KB包含5个m脚本和1个data数据文件脚本分别负责核函数定义、模型训练、分类器构建、主流程执行以及分类结果可视化数据文件为经典的iris鸢尾花数据集可直接运行体验完整分类过程。已有6348人学习资源代码精炼、注释清晰非常适合作SVM三分类入门的动手范例。通过研读代码可以掌握fitcsvm函数在多分类场景下的具体调用方式理解一对一策略如何将三分类问题拆解为多个二分类模型并利用预测投票规则确定最终类别与一对多策略相比一对一在类别数较少时训练开销更低且分类精度更稳定。同时还能对比不同核函数对分类决策边界的影响为进一步处理更复杂数据集或扩展多分类任务打下扎实基础。1. SVM 三分类先搞清楚它不是天生做多分类的做三分类任务时很多人第一反应就是拿 SVM 直接fit(X, y)但 SVC 本质上是个二分类器。它解决三分类靠的是“拆解策略”要么一次训练三个二分类器两两投票OvO要么拿一个类对剩下所有类训练三个分类器OvR。理解了这一点后续调参和看decision_function的 shape 才不会懵。这篇文章围绕“用 SVM 实现数据的三分类”写完整落地路径从数据预处理、核函数、参数搜索到部署验证中途会穿插几个我实际踩过的坑。适合刚接触 SVM 做小样本分类的人也适合已经从逻辑回归切过来、想对比分类边界的同学。先说结论性经验如果你的样本量在几千以内、特征维度几十左右SVM 配 RBF 核通常比逻辑回归和简单神经网络更稳一旦样本量上了十万SVM 的训练时间会明显变长那时才认真考虑 CNN 或树模型。三分类问题里类别不平衡和特征缩放带来的影响远大于核函数本身的选择。下面按我平时做项目的顺序展开。2. 数据准备与预处理三分类能不能做对七成在数据2.1 标签编码与类别不平衡的初步处理SVM 的输入y需要数值标签或者 scikit-learn 能识别的标签形式。常见做法是用LabelEncoder把字符串类别转成 0、1、2。注意LabelEncoder是按字母序编码的不是按你的习惯顺序。如果你后续要看confusion_matrix的坐标轴最好先打印出classes_确认一下对应关系。from sklearn.preprocessing import LabelEncoder y_raw [cat, dog, bird, dog, cat, bird] le LabelEncoder() y le.fit_transform(y_raw) print(le.classes_) # [bird cat dog] print(y) # [1 2 0 2 1 0]这里LabelEncoder把三类映射成 0/1/2但 0 对应 bird 而不是 cat。后续做交叉验证、计算指标时始终以le.classes_为准。如果你希望编码顺序和业务一致用pd.CategoricalDtype(categories[...], orderedTrue)先指定顺序再编码或者直接自己写字典映射。类别不平衡在三分类里很常见。比如故障诊断中的“正常 / 故障A / 故障B”正常样本往往占 90%。SVM 对不平衡敏感因为软间隔的惩罚系数 C 对所有样本一视同仁。最简单的处理是先看类别占比再决定是否用class_weightbalanced。import numpy as np from collections import Counter print(Counter(y)) # {0: 40, 1: 40, 2: 20} # 训练时启用 balanced自动按类别频率反比调整惩罚权重 model svm.SVC(kernelrbf, class_weightbalanced)class_weightbalanced会让少类样本在计算损失时有更大权重避免分类面被多数类“推”着走。我一般会在第一次建模时就开启它因为后续调参时少一个变量。但要注意如果数据极度不平衡比如 1:100单纯靠 class_weight 救不回来需要结合过采样或欠采样。SVM 的决策边界本质是少数支持向量撑起来的极不平衡时支持向量几乎全落在多数类一侧此时样本层面的调整比权重更有效。2.2 特征缩放为什么对 SVM 是生死线SVM 的目标是最大化间隔间隔用欧氏距离计算。如果特征 A 的量纲是 0~1特征 B 是 0~10000那么距离计算会完全被特征 B 主导特征 A 即便有很强的区分能力也被淹没。这是 SVM 和其他基于距离的模型共有的问题但对 SVM 尤其致命因为 RBF 核里的gamma直接依赖样本间距离。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意顺序先切分再fit_transform训练集最后只用transform测试集。如果把scaler.fit放在切分之前测试集的统计量会泄漏到训练过程里导致验证指标虚高。这个坑后面我会在第五节专门讲。StandardScaler会把每个特征变成均值 0、方差 1适合大多数连续特征。如果特征里夹杂着 0/1 的哑变量缩不缩放问题不大但统一缩放也无害。min-max 缩放也可以但对有离群点的特征StandardScaler更稳。我遇到过一个场景某个特征 95% 的样本在 0~100但有 5% 在 10000 以上用 MinMaxScaler 后正常样本被压到 0.01 以下几乎失去区分度。SVM 的 C 和 gamma 在这种数据上很难调出来所以优先 StandardScaler。2.3 划分训练集与验证集时的注意点三分类任务里切分数据时一定要用stratifyy。如果不分层抽样随机切分可能导致验证集里某一类只有几个样本混淆矩阵和 F1 看起来随机波动很大。尤其当某类样本本来就少时stratify 是必须的。X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42, stratifyy ) print(Counter(y_train)) print(Counter(y_test))test_size我习惯在 0.2~0.3 之间。如果总样本不足 200我会用交叉验证代替固定测试集留一法也行但训练成本高。SVM 在小样本上的泛化能力依赖正则化参数 C 和核参数如果数据太少光看测试集指标没有意义至少做 5 折交叉验证取平均值。另外如果你的数据带有时间顺序比如按天采集的振动信号不能随机打乱要按时间切分否则相当于用未来数据预测过去指标再好看也没用。这个和 SVM 本身无关但项目里最容易犯。3. 用 scikit-learn 实现三分类从默认参数到 OvR/OvO 决策3.1 最小可运行代码SVC 默认 OvO 的多分类scikit-learn 的SVC在多分类时默认采用 OvO一对一策略。它会在每两个类别之间训练一个二分类器三个类就有三个分类器预测时每个分类器投票票数最多的类胜出。默认参数下decision_function_shape是ovr但训练方式仍然是 OvO这个名称很容易误导初学者。from sklearn import svm model svm.SVC(kernelrbf, C1.0, gammascale) model.fit(X_train_scaled, y_train) y_pred model.predict(X_test_scaled) print(y_pred) print(model.n_support_) # 每类支持向量数量这段代码是能跑通的最简版本。gammascale表示按1 / (n_features * X.var())自动计算适合没空手动调参时的基线。model.n_support_输出三个数字分别对应每个类作为正类时的支持向量数量如果某个类为 0说明该类样本几乎全被其他类区分规则覆盖模型对它的预测能力很可疑。OvO 的好处是每个二分类器只用两类数据训练样本量减半训练速度快而且类别不平衡的影响被限制在局部。坏处是决策边界可能不一致某个区域出现两个分类器都投了不同票的情况最终靠投票决胜。对于三分类OvO 和 OvR 的精度差异通常不大但 OvR 更直观每个分类器对应“这个类 vs 其他”。3.2 显式指定 decision_function_shape 与 probability 参数如果你需要输出每个类别的“置信度”有两种方式decision_function和predict_proba。默认情况下predict_proba不可用因为 SVC 不支持直接输出概率需要开启probabilityTrue。这个选项会引入 Platt 缩放训练时间明显变长而且概率值不是严格校准的。model svm.SVC(kernelrbf, C1.0, gammascale, decision_function_shapeovr, probabilityTrue) model.fit(X_train_scaled, y_train) # decision_function 形状是 (n_samples, 3) decision model.decision_function(X_test_scaled) print(decision.shape) # predict_proba 也是 (n_samples, 3) proba model.predict_proba(X_test_scaled) print(proba)decision_function_shapeovr只影响decision_function的输出形状它会把 OvO 的分类器得分转换成 OvR 形式的得分。具体做法是每个二分类器输出一个原始距离值再按某种方式合并。一般来说我们直接看predict的标签就够了需要排序或者阈值过滤时才用decision_function。probabilityTrue的坑在于它会用交叉验证拟合 Platt 缩放参数导致同样的random_state下结果也不稳定。如果只是为了排序不建议开启如果业务必须输出概率比如做风控评分那只能开但要注意后续校准。用predict_proba做三分类时三个类别的概率和应为 1但你会发现它不像 softmax 那样置信在小样本上尤其明显。3.3 二分类器组合手动实现 OvR 看决策边界虽然 scikit-learn 直接封装了多分类但为了理解原理手动实现一次 OvR 很有价值。尤其当你想查看某一个二分类器比如“类 0 vs 其他”的决策边界时必须自己把其他类合并成一个类重新训练。from sklearn import svm from sklearn.base import clone # 以类 0 为正类其他类合并为负类 y_binary (y_train 0).astype(int) clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(X_train_scaled, y_binary) # 预测时输出正类得分 scores_0 clf.decision_function(X_test_scaled) print(scores_0)这种做法的意义在于调试。如果最终三分类效果不好你可以分别训练三个二分类器看看是哪个类与其他的区分度太差。常见结论是某两类本来就纠缠在一起合并成 OvR 后分类器的样本分布完全不同决策面也变了。clone保证每次使用相同参数不会因为之前 fit 过的状态残留。手动实现 OvR 的另一个用途是自定义决策规则比如业务要求“只有当类 1 的得分超过 0.8 时才判定为类 1否则判为其他”这时候缺省的多分类逻辑做不到你需要自己保留三个二分类器的原始得分。我在工业项目里经常这么干因为多分类模型的硬分类对“拒绝识别”场景支持不好。4. 核函数与参数调优RBF 的 C 和 gamma 怎么定4.1 线性核还是 RBF 核先看数据量级三分类问题选核函数不能只看流行趋势。线性核kernellinear等价于没做映射只能在原始特征空间找到线性边界适合特征维度很高、样本量也大的情况比如文本分类TF-IDF 特征常常几万维。RBF 核把样本映射到无限维理论容量更大但对参数C和gamma非常敏感调不好容易过拟合。我的经验标准样本量 5000特征维度 100优先 RBF特征维度 1000优先 linear特征维度介于两者间两个都跑一遍交叉验证看哪个验证集得分高。另外如果业务要求可解释性比如需要输出特征权重那么线性核可以用coef_看出每个特征的贡献RBF 核做不到。from sklearn import svm clf_linear svm.SVC(kernellinear, C1.0) clf_linear.fit(X_train_scaled, y_train) print(clf_linear.coef_.shape) # 只有线性核才有 coef_coef_的形状是(n_classes * (n_classes-1) / 2, n_features)三分类就是 3 行。如果想看“类0 vs 其他”的权重需要结合前面手动 OvR 的方法。对于只关心精度的项目直接跳过可解释性用 RBF。4.2 用 GridSearchCV 搜索 C 和 gamma 的实用范围RBF 核有两个核心参数C是误分类惩罚系数越大越不允许训练集出错容易过拟合gamma控制单个样本的影响半径越大决策边界越弯曲容易过拟合。三分类问题调参时很多人直接上GridSearchCV但搜索范围和步长设置不对会导致网格爆炸或者精度虚高。from sklearn.model_selection import GridSearchCV from sklearn import svm param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1], } model svm.SVC(kernelrbf, class_weightbalanced) grid GridSearchCV(model, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train_scaled, y_train) print(grid.best_params_) print(grid.best_score_)C的常用范围是 0.1 到 100gamma常用范围是 0.001 到 1。注意gammascale会根据数据自动调整但网格搜索的起点最好覆盖到1 / n_features附近。我一般先看X_train_scaled.var()的量级如果标准化后方差为 1gamma从 0.01 开始搜就够了。scoringf1_macro比accuracy更适合三分类因为类别不平衡时 accuracy 会骗人。f1_macro计算每一类的 F1 再取平均对少类样本更敏感。如果你觉得GridSearchCV太慢可以先用随机搜索RandomizedSearchCV跑 50 组缩小范围后再精细搜。4.3 交叉验证时类别分布要保持一致GridSearchCV默认使用StratifiedKFold能保证每一折的类别比例和原数据一致。但如果你手动写交叉验证循环一定要用StratifiedKFold别用普通的KFold。from sklearn.model_selection import StratifiedKFold from sklearn import svm from sklearn.metrics import f1_score skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) f1_list [] for train_idx, val_idx in skf.split(X_train_scaled, y_train): X_fold_train X_train_scaled[train_idx] y_fold_train y_train[train_idx] X_fold_val X_train_scaled[val_idx] y_fold_val y_train[val_idx] clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(X_fold_train, y_fold_train) pred clf.predict(X_fold_val) f1_list.append(f1_score(y_fold_val, pred, averagemacro)) print(f1_list)这里shuffleTrue打乱顺序防止原始数据按类别分块导致某一折全是一种样本。random_state固定后结果可复现。交叉验证的目的不是选出一组参数就完事而是确认参数在不同数据划分下都稳定。如果f1_list里最大值和最小值相差超过 0.1说明模型或数据有问题不要急着部署。有一种观点认为 SVM 不需要交叉验证直接用全部训练集拟合就行因为支持向量只占少部分。但那是理想情况实际数据噪声大不用交叉验证很难判断过拟合。三分类任务里每类的支持向量数量都会显示在n_support_中如果某个类支持向量数量占比极高往往意味着该类样本分布离散调参时要格外小心。5. 三分类的 5 个常见坑现象、原因与解决办法5.1 坑一类别标签是字符串导致 fit 报错现象model.fit(X_train, y_train)报错ValueError: Unknown label type: continuous或者直接类型错误。 原因特征是 numpy 数组标签是 Python 字符串列表SVC 内部默认要求标签可比较且类型一致。 解决先用LabelEncoder把字符串转成整数或者确保y_train和y_test都是同一类型。推荐统一用整数 0/1/2后续计算混淆矩阵也方便。# 错误示例 y_bad [低, 中, 高, 低, 高] # 正确做法 from sklearn.preprocessing import LabelEncoder le LabelEncoder() y_good le.fit_transform(y_bad)另外如果y是浮点数比如[0.0, 1.0, 2.0]SVC 能处理但可能被当作回归问题吗不会SVC 分类器只会根据标签的种类数决定工作量。但为了统一建议转成 int。5.2 坑二预测结果全是某一类现象三分类模型跑完y_pred里 95% 是类别 0classification_report中类别 1 和 2 的 F1 接近 0。 原因类别不平衡、C 和 gamma 设置不当、数据特征对少类样本没有区分度三者至少占一个。 解决先看Counter(y_train)确认平衡比然后试class_weightbalanced再对少类样本做可视化比如 PCA 降到 2 维看是否可分。如果 PCA 图上两类完全重叠换核函数或加特征比调参更重要。from sklearn.decomposition import PCA import matplotlib.pyplot as plt pca PCA(n_components2) X_pca pca.fit_transform(X_train_scaled) for label in [0, 1, 2]: mask y_train label plt.scatter(X_pca[mask, 0], X_pca[mask, 1], labelfclass {label}) plt.legend() plt.show()如果类别少且严重不平衡也可以考虑用OneClassSVM做异常检测但那是“这个类 vs 不是这个类”的二元思路和三分类目标不同。我遇到过一次预测结果全是多数类的情况原因不是模型坏了而是因为少类样本只有 15 个正则化后模型发现把少类全部分错带来的损失更小于是妥协了。这种情况加大C或者直接换用RandomForest做分类效果更稳。5.3 坑三decision_function 的 shape 看不懂现象三分类问题中model.decision_function(X_test).shape打印出来是(n_samples, 3)或者(n_samples, 6)不知道哪个维度代表什么。 原因默认decision_function_shapeovr时输出形状是(n_samples, n_classes)如果设置decision_function_shapeovo输出形状是(n_samples, n_classes*(n_classes-1)/2)三分类就是 6。 解决明确你想用哪种形状。绝大多数场景用ovr它把每类的原始得分合并成三个值方便比较。ovo的六列分别代表三个二分类器的输出顺序为 (0,1), (0,2), (1,2)但实际用起来要查文档不直观。model svm.SVC(decision_function_shapeovr) model.fit(X_train_scaled, y_train) dec model.decision_function(X_test_scaled) pred model.classes_[np.argmax(dec, axis1)]np.argmax选得分最高的类别作为预测结果这和predict的投票不一定完全一致因为decision_function的 OvR 得分是内部转换过的不是简单的投票计数。所以如果你要自定义决策规则建议使用手动 OvR 的方法而不是依赖decision_function_shape。5.4 坑四标准化时把测试集的统计量也放进来了现象在切分数据之前直接scaler.fit(X)然后再train_test_split测试集指标比想象中高很多或者调参后验证集分数波动极大。 原因测试集的信息混入了训练过程属于数据泄漏。StandardScaler的均值和方差是全局统计量如果在切分前计算测试集的特征分布已经影响了缩放参数训练时相当于“偷看”了测试集。 解决严格先切分再在训练集上fit_transform之后只对测试集transform。交叉验证里也要注意每个 fold 的 scaler 必须只用训练部分拟合。# 错误示范 scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_test, y_train, y_test train_test_split(X_scaled, y, ...) # 正确示范 X_train, X_test, y_train, y_test train_test_split(X, y, ...) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这个坑不只在 SVM 里出现但在 SVM 中后果更严重因为特征缩放直接改变距离对决策边界影响很大。我用GridSearchCV时会把标准化放进Pipeline这样每一折内部都会正确重算 scaler。5.5 坑五概率校准 predict_proba 和 decision_function 不一致现象predict_proba输出的概率最大值对应的类别和predict输出的类别不一致或者predict_proba的排序和decision_function的排序不一致。 原因probabilityTrue时SVC 先拿到 OvO 的决策值再通过 Platt 缩放转换成概率。Platt 缩放是独立于分类器的逻辑回归拟合过程它可能改变类别之间的相对排序尤其在软间隔 C 较大、决策值分布区间很窄时。 解决如果业务只需要“哪个类概率最高”直接用predict不要拿predict_proba做 argmax如果必须用概率建议用CalibratedClassifierCV重新校准并在验证集上检查校准曲线。from sklearn.calibration import CalibratedClassifierCV base_model svm.SVC(kernelrbf, C1.0, probabilityFalse) calibrated_model CalibratedClassifierCV(base_model, cv3) calibrated_model.fit(X_train_scaled, y_train) prob_calibrated calibrated_model.predict_proba(X_test_scaled)CalibratedClassifierCV会用交叉验证拟合校准器效果比分箱好但训练成本更高。小样本时尤其明显因为校准需要额外数据。我个人的习惯是能不开概率就不开直接输出决策值排序。6. 验证模型与部署教你用混淆矩阵和分类报告收尾6.1 用 classification_report 看每一类的精确率、召回率、F1三分类的评价不能只看准确率必须逐类看。classification_report能直接输出每一类别的精确率、召回率、F1以及宏平均和加权平均。精确率是“预测为该类的样本中多少是对的”召回率是“该类真实样本中多少被找到了”。F1 是两者的调和平均对不平衡数据更有参考意义。from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_test, y_pred, target_namesle.classes_)) print(confusion_matrix(y_test, y_pred))注意target_names要和le.classes_对应否则报告里的行名会错位。如果类别 1 的召回率只有 0.2说明模型对该类识别能力弱优先回去看数据特征而不是调整C或gamma。分类报告里还有macro avg和weighted avg前者更适合三分类均匀场景后者会被多数类主导。6.2 混淆矩阵和三分类的可视化自查混淆矩阵是布局维度的直观检查。三分类矩阵是 3x3对角线是预测正确的数量。看矩阵时不要只看对角线还要看错误集中在哪里。比如[[50, 2, 0], [3, 40, 5], [0, 8, 20]]说明类 1 和类 2 容易被混淆而类 0 非常干净。这种信息在报告中也能看到但矩阵让你一眼定位错例模式。import matplotlib.pyplot as plt import seaborn as sns cm confusion_matrix(y_test, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsle.classes_, yticklabelsle.classes_) plt.xlabel(Predicted) plt.ylabel(True) plt.show()如果发现类 1 总是被误判成类 2可以考虑做特征工程比如组合这两个类的识别特征而不是盲目调参。可视化也会发现数据错误比如某些样本的标签贴错了SVM 硬间隔时会出现离群点软间隔时会用更多支持向量去兜底。6.3 保存模型与加载预测的完整流程模型验证通过后用joblib保存整个 pipeline而不是只保存模型。如果数据标准化放在外面部署时容易忘记同样的 scaler。推荐把StandardScaler和SVC放进Pipeline一起训练、一起保存。import joblib from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (svm, svm.SVC(kernelrbf, C10, gamma0.1, class_weightbalanced)) ]) pipe.fit(X_train, y_train) joblib.dump(pipe, svm_three_class_pipeline.pkl)部署时加载同一个 pipeline输入原始特征直接得到缩放后的预测结果。这避免了我在一个项目里遇到的问题上线时只加载了模型参数忘加载 scaler预测结果几乎全是某个类排查了好久才发现离线测试时是用了X_test_scaled线上却拿原始特征喂给模型。Pipeline是后悔药建议从现在开始就养成习惯。使用 pipeline 还能让你在GridSearchCV时把参数写成svm__C、svm__gamma避免参数名冲突。这也是我最后想强调的一点三分类 SVM 的坑大多不在算法而在工程链路。每一次保存、加载、缩放、切分都对得上模型自然跑得稳。希望帮到你。本文还有配套的精品资源点击获取
返回列表