ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SVM三分类实战:从原理到决策边界可视化

SVM三分类实战:从原理到决策边界可视化

简介:这份资源面向机器学习初学者与需要完成课程实验的学生,聚焦支持向量机从二分类扩展到三分类的完整实现,帮助读者理解一对一策略下多个二分类器的构建与投票预测流程。压缩包共6个文件,以5个m脚本和1个data数据文件为主,脚本分别承担训练、核函数、分类器与可视化绘图等职责,数据文件提供iris样本,整体约4KB,体量轻便便于快速运行与修改。已有6351人学习下载,说明其在教学与练习场景中具有一定参考价值。读者可据此掌握MATLAB中fitcsvm与predict的调用方式,理解核函数选择、正则化参数C对分类边界的影响,并通过可视化脚本直观对比三分类决策结果,为后续调参与多分类扩展打下基础。

1. 从一份三分类 SVM 源码说起:它到底能跑出什么

手上这份资源标题很直白——SVM 实现数据的三分类。很多人第一反应是「SVM 不是二分类器吗」,这恰恰是它值得拆的地方:把二分类的支持向量机推到多分类,中间要处理投票策略、类别不平衡、核函数参数和决策边界可视化几件事,任何一环没对齐,跑出来的三分类结果就会「看着能分、细看全乱」。这份源码包解决的正是这个落地问题:给一份带标签的三类数据,从特征标准化、模型训练、多分类策略选择到决策边界绘制,走通一条可复现的链路。适合刚学完 SVM 公式、想把它真正跑起来的新手,也适合需要快速搭一个三分类基线、再往上叠特征工程的熟手。下面按「先立住原理、再动手复现、最后盯住坑」的顺序拆。

2. 三分类怎么从二分类长出来:策略选型与数据准备

SVM 的原生输出只有正负两类,多分类必须靠组合策略拼出来。这一步选错,后面调参全是白费功夫,所以先把策略和数据这两块地基打牢,再谈代码。

2.1 一对多与一对一:两种组合策略的取舍

常见做法有两类。一对多(One-vs-Rest,OvR)是给每个类别训练一个二分类器,把当前类当正例、其余全部当负例,预测时取决策函数值最大的那个类。一对一(One-vs-One,OvO)是在每两个类别之间训练一个分类器,三分类就是 C(3,2)=3 个分类器,预测时用投票决定归属。

三分类这个规模下,两种策略的计算量差别不大,真正的差别在边界行为。OvR 每个分类器都面对「一类 vs 其余两类」的不平衡问题,某一类样本少的时候容易被压;OvO 每对分类器只比较两类,单次训练样本更均衡,但会出现投票平票的可能,需要额外的决策函数值兜底。我一般三分类优先用 OvO,因为类别数少、投票歧义可控,而且 scikit-learn 的SVC默认就是 OvO,decision_function_shape参数还能在需要时切成 OvR 视角看结果。

策略三分类下分类器数单次训练样本主要风险
OvR3全量,正负比例失衡少数类被多数类淹没
OvO3仅两类,较均衡投票平票需决策值兜底

选型理由说清楚:类别数在 10 以内、样本量中等时,OvO 的训练开销和边界质量更稳;类别数上去以后 OvO 的分类器数量按平方增长,才轮到 OvR 或层次策略出场。这份资源是三分类,OvO 是合理默认。

2.2 特征标准化与标签编码:别让量纲毁掉核函数

SVM 对特征尺度极其敏感,因为核函数算的是样本间距离或内积。一个特征量纲是千级、另一个是零点几级,距离几乎完全由大尺度特征主导,模型等于只学了一半信息。所以训练前必须标准化,常见做法是StandardScaler做零均值单位方差,或者数据有明显异常值时用RobustScaler。

标签也要处理。三分类的原始标签可能是字符串(比如三个类别名),SVM 内部要转成 0/1/2 的整数编码,预测完再映射回去。这一步用LabelEncoder最省事,但要注意训练集和预测集必须用同一个编码器对象,否则类别映射错位,准确率会莫名其妙掉到随机水平。

import numpy as np from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.model_selection import train_test_split # X: 形状 (n_samples, n_features),y: 字符串或整数标签 le = LabelEncoder() y_enc = le.fit_transform(y) # 三类 -> 0/1/2,映射关系保存在 le.classes_ X_train, X_test, y_train, y_test = train_test_split( X, y_enc, test_size=0.25, stratify=y_enc, random_state=42 ) scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) # 只在训练集上 fit X_test_s = scaler.transform(X_test) # 测试集只 transform,防止信息泄漏

这段的关键在最后两行:fit_transform只对训练集做,测试集一律transform。很多人图省事对全量数据先标准化再切分,测试集的均值方差就泄漏进了训练过程,评估结果虚高,上线就翻车。stratify=y_enc保证切分后三类比例一致,三分类样本本来就不多的时候尤其重要。

2.3 核函数与关键参数:三分类场景下的起步配置

三分类数据如果线性可分,linear核足够且可解释;如果边界弯曲,rbf核是默认首选。rbf有两个核心参数:C控制对误分类的惩罚,越大越容易过拟合;gamma控制单个样本的影响半径,越大边界越紧、越容易记住训练点。

起步配置我一般这样给:C=1.0、gamma='scale'(scikit-learn 的默认,等于 1/(n_features * X.var())),先跑通看混淆矩阵,再决定往哪个方向调。三分类的评估不能只看准确率,要看每一类的召回,否则某一类全错、总体准确率还有 66% 的假象。

from sklearn.svm import SVC from sklearn.metrics import classification_report, confusion_matrix clf = SVC( kernel='rbf', C=1.0, gamma='scale', decision_function_shape='ovr', # 训练仍是 OvO,输出 ovr 形状的决策值便于分析 random_state=42 ) clf.fit(X_train_s, y_train) y_pred = clf.predict(X_test_s) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=le.classes_))

decision_function_shape='ovr'这个参数容易被误解:它不改变训练时的 OvO 机制,只改变decision_function返回值的形状,方便你按「一类对其余」的视角看每个类的得分。真正影响分类结果的是核函数和 C、gamma,别把这两件事搞混。

3. 把源码跑通:训练、评估与决策边界可视化

原理立住之后进入动手环节。这一章按「数据进、模型出、边界画出来」的顺序走,每一步都给可抄的代码和参数说明,跑完你应该能拿到一张带三类区域和测试点的决策边界图。

3.1 数据加载与三类样本的构造

资源里的数据可能是 CSV,也可能是脚本内生成的模拟数据。不管哪种,加载后先做三件事:看形状、看每类样本数、看特征范围。三分类最常见的翻车是某类样本只有十几个,训练时被另外两类挤没。

import pandas as pd df = pd.read_csv('three_class_data.csv') # 列名按实际资源调整 print(df.shape) print(df['label'].value_counts()) # 每类样本数,少于 30 要警惕 print(df.describe()) # 特征量纲差异一眼可见 X = df.drop(columns=['label']).values y = df['label'].values

如果资源没有现成数据,用make_classification或make_blobs造三类数据也能复现整条链路,n_classes=3、n_features=2(二维便于画边界)、class_sep控制类间重叠程度。造数据时把class_sep调小一点,能模拟真实场景里边界模糊的情况,比一造就好分的玩具数据更能暴露问题。

3.2 训练与交叉验证:别用单次切分下结论

单次 train/test 切分在三分类小样本上波动很大,换个随机种子准确率能差十几个点。稳妥做法是分层 K 折交叉验证,看每折的分数分布而不是单个数。

from sklearn.model_selection import StratifiedKFold, cross_val_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X_train_s, y_train, cv=cv, scoring='f1_macro') print('每折 f1_macro:', np.round(scores, 3)) print('均值/标准差:', scores.mean().round(3), scores.std().round(3))

这里用f1_macro而不是accuracy,因为 macro 对三类一视同仁,某一类表现差会直接把均值拉下来,比准确率更能暴露类别不平衡的问题。标准差超过 0.05 就说明数据或参数不稳,先别急着调 C,回去看样本分布。

3.3 决策边界可视化:把三分类的「分界线」画出来

二维特征的三分类最适合画决策边界,一眼能看出模型是把三类分成了三块,还是把两类混在一起。做法是在特征空间铺网格、逐点预测、用等高线填色,再把测试点叠上去。

import matplotlib.pyplot as plt # 用前两个特征画二维边界;特征多于两个时先做降维或只取两维 X_plot = X_train_s[:, :2] clf_2d = SVC(kernel='rbf', C=1.0, gamma='scale').fit(X_plot, y_train) x_min, x_max = X_plot[:, 0].min() - 1, X_plot[:, 0].max() + 1 y_min, y_max = X_plot[:, 1].min() - 1, X_plot[:, 1].max() + 1 xx, yy = np.meshgrid(np.linspace(x_min, x_max, 300), np.linspace(y_min, y_max, 300)) Z = clf_2d.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) plt.contourf(xx, yy, Z, alpha=0.3, cmap='viridis') plt.scatter(X_plot[:, 0], X_plot[:, 1], c=y_train, edgecolor='k', cmap='viridis') plt.title('SVM three-class decision boundary (RBF)') plt.show()

网格密度 300 是清晰度和速度的折中,太低边界呈锯齿,太高画图卡顿。注意这里为了可视化只用前两维重新训练了一个模型,它和全特征模型不是同一个,别拿这张图的准确率当全特征模型的评估结果。想看全特征模型的效果,还是回到 3.2 的交叉验证。

3.4 参数扫描:C 与 gamma 的网格怎么设

三分类的调参重点在 C 和 gamma 的配合。经验上先粗后细:C 取 10 的幂次、gamma 取scale附近的数量级,先看哪片区域稳,再在稳的区域内细化。

from sklearn.model_selection import GridSearchCV param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': ['scale', 0.01, 0.1, 1], 'kernel': ['rbf'] } grid = GridSearchCV(SVC(), param_grid, cv=cv, scoring='f1_macro', n_jobs=-1) grid.fit(X_train_s, y_train) print('最优参数:', grid.best_params_) print('最优分数:', round(grid.best_score_, 3))

n_jobs=-1用满 CPU 核,三分类这个规模几秒就能扫完。best_score_是交叉验证均值,不是测试集分数,别拿它当最终指标汇报。扫完把最优参数代回 3.2 的流程,在独立测试集上再验一次,这一步是很多人省掉的后悔药。

4. 避坑与排查:三分类 SVM 最容易翻车的五处

这一章是血泪经验集中区。下面五条都是三分类场景下高频出现的问题,按「现象 → 原因 → 解决」写,遇到对号入座即可。

4.1 准确率虚高但某一类全错

现象:总体准确率 0.8 以上,混淆矩阵里某一类的召回接近 0。原因:三类样本严重不平衡,OvR 或 OvO 的决策都偏向多数类,加上评估只看 accuracy 掩盖了问题。解决:先看value_counts()确认比例,样本少的类做重采样或给class_weight='balanced',评估换成f1_macro和每类召回,别被总数骗了。

4.2 标准化顺序错了导致测试集虚高

现象:交叉验证分数漂亮,换一批新数据预测就崩。原因:对全量数据先fit_transform再切分,测试集的统计量泄漏进训练。解决:严格按 2.2 的顺序,fit只在训练集上做,测试集和新数据一律transform,把这条写进流程检查清单。

4.3 标签编码在训练和预测间不一致

现象:模型训练时准确率正常,预测新样本时类别整体错位,比如把 A 类判成 C 类。原因:预测时重新fit了一个LabelEncoder,类别到整数的映射和训练时不同。解决:编码器对象随模型一起保存(joblib.dump),预测时加载同一个对象做transform和inverse_transform,绝不重新 fit。

4.4 gamma 过大导致边界碎片化

现象:训练集准确率接近 1,测试集明显掉,决策边界图上出现一圈圈孤立的色块。原因:gamma太大,每个样本的影响半径过小,模型把噪声也记住了。解决:把gamma往scale或更小调,配合交叉验证看f1_macro是否回升;同时检查特征是否已标准化,未标准化时gamma='scale'的自动估计也会失真。

4.5 可视化模型和评估模型不是同一个

现象:决策边界图看着分得很干净,但交叉验证分数一般。原因:画图时只取前两维重新训练,评估用的是全特征模型,两者本就不是一个东西。解决:图只用于定性看边界形状,定量结论一律以全特征模型的交叉验证和独立测试集为准,汇报时把两者分开写清楚。

5. 进阶:把三分类 SVM 嵌进真实流程的几个技巧

跑通单份数据只是起点,真实项目里 SVM 三分类往往要和特征工程、模型持久化、阈值调整配合。这一章给几个我实际会用的技巧,都是能直接搬进流程的。

第一个技巧是特征选择前置。SVM 对无关特征不敏感是误解,无关特征会稀释核函数的距离度量。三分类数据特征维度超过二十时,先用方差过滤或基于模型的重要性排序砍一轮,再进 SVM,往往比直接调 C 和 gamma 收益大。常见做法是用SelectKBest配f_classif做粗筛,保留前若干维再训练。

第二个技巧是模型持久化要连预处理一起存。只存SVC对象、不存scaler和LabelEncoder,上线时预处理对不上,预测结果必错。稳妥做法是用Pipeline把标准化和分类器串起来,整体 dump。

from sklearn.pipeline import Pipeline import joblib pipe = Pipeline([ ('scaler', StandardScaler()), ('svc', SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced')) ]) pipe.fit(X_train, y_train) # 注意:Pipeline 内部自己管标准化,传原始特征 joblib.dump({'model': pipe, 'encoder': le}, 'svm_three_class.pkl') # 预测时 bundle = joblib.load('svm_three_class.pkl') y_new = bundle['encoder'].inverse_transform( bundle['model'].predict(X_new_raw) )

用Pipeline之后,标准化参数跟着模型走,新数据进来直接喂原始特征,少一个手工对齐的环节就少一类翻车。class_weight='balanced'在这里顺手加上,三分类不平衡时能兜一层。

第三个技巧是决策值阈值微调。三分类里如果某一类的误判代价明显更高(比如漏检比误检贵),不要只信predict的硬分类,用decision_function拿三类得分,对目标类设一个偏低的相对阈值,牺牲一点总体准确率换目标类召回。这个操作要在验证集上定阈值,再拿到测试集确认,别在测试集上反复试。

第四个技巧是核函数选择的验证方法。linear和rbf谁更好,不要凭感觉,用同一套交叉验证各跑一遍比f1_macro均值和标准差;差距在标准差以内就选linear,可解释性和预测速度都更好。三分类数据线性可分程度高的时候,linear经常不输rbf,还省了调gamma的功夫。

最后说个验证习惯。我每次交付一个三分类 SVM 之前,会强制走一遍「混淆矩阵 + 每类召回 + 独立测试集 + 新样本单条预测」四步,尤其是最后一步——拿一条手工构造的新样本走完整预测链路,确认编码、标准化、预测、反编码四段都对得上。这一步救过我两次,都是编码器没随模型保存导致的类别错位,交叉验证完全看不出来。希望这套流程帮到你,少走我踩过的弯路。

本文还有配套的精品资源,点击获取

返回列表