简介:SVM(支持向量机)本质上面向二分类,但借助一对一策略也能高效完成三分类任务。资源包内含一套基于MATLAB的SVM三分类实现方案,面向机器学习初学者或需要快速上手多分类实战的开发者,覆盖数据加载、核函数定义、分类器训练、结果可视化等完整链路。压缩包共6个文件,包含5个.m脚本与1个iris.data数据集,整体仅4KB,代码结构清晰,模块化程度高,适合直接运行、逐行研读与二次改造。iris.data是经典鸢尾花数据集,包含三类样本,便于开箱验证算法效果。已有6349人学习下载,说明其在实际学习场景中被广泛验证。读者可通过源码掌握一对一多分类的建模思路,包括如何为三对类别分别训练二分类SVM、如何利用predict投票确定最终类别,以及fitcsvm的核函数与正则化参数调整技巧。无论用于课程作业、毕业设计还是SVM多分类入门,这套精简示例都能提供从原理到代码的直观参照。
1. SVM 三分类:一次把支持向量机从训练跑到评估的完整实践
某开发者在做一个用户分群需求时,用业务规则写了七八条硬编码阈值,分类准确率一直卡在 85% 上不去;后来把样本喂给支持向量机(SVM)做三分类,准确率直接拉到 98%,原来那堆 if-else 规则全部删掉。这个项目做的就是这样一件事:基于 SVM 实现数据的三分类,覆盖数据预处理、模型训练、混淆矩阵评估、超参数搜索的完整工程链路,代码量不大,但每一步都是能直接照搬的落地写法。
适合两类人:一是刚接触分类算法、想把 SVM 用在自己数据集上的开发者,二是准备面试时想讲清楚多分类策略(OvR/OvO)和核函数选择的人。读完这个项目,你不仅能跑通三分类,还能在二次开发时快速切换核函数、调整类别权重,甚至把模型封装成一个预测接口。
2. 技术选型与工程结构:为什么用 scikit-learn 而不是手写 SMO
2.1 技术栈:四个依赖就够
项目基于 Python 3 开发,核心依赖只有 numpy、pandas、scikit-learn、matplotlib 四个。三分类的 SVM 实现直接使用 scikit-learn 的SVC类,最直接的理由是它内部封装了成熟的 LIBSVM 数值求解器,稳定性和收敛速度比自己手写 SMO 算法可靠得多。
很多同学为了搞懂原理,从零实现 SMO 做三分类,2000 个样本要训练几分钟,还经常遇到数值溢出、alpha 更新不收敛的问题。这个项目的定位是“把三分类这件事跑通、跑准”,所以优先用现成的成熟实现。如果你想深入研究原理,可以对照 SMO 论文去读迭代细节,但不影响先把项目跑起来。
2.2 项目目录结构:入口和核心逻辑分离
代码按照“入口脚本 + 核心模块”的方式组织,对二次开发很友好,新增一个模型对比脚本不需要改动原有逻辑:
svm_three_class/ ├── README.md ├── requirements.txt ├── data/ │ └── sample_data.csv # 三分类样本:3 个特征 + 1 个标签列 ├── src/ │ ├── __init__.py │ ├── preprocess.py # 数据清洗、缺失值填充、归一化 │ ├── train.py # 划分数据集、训练 SVC、保存模型 │ ├── evaluate.py # 准确率、混淆矩阵、分类报告 │ └── predict.py # 加载模型,对单条样本做预测 └── main.py # 一键串联整个流程每个文件职责单一,main.py里只需要按顺序调用preprocess → train → evaluate三个函数即可。requirements.txt对应依赖如下:
| 依赖 | 版本建议 | 用途 |
|---|---|---|
| numpy | 1.24+ | 数值计算与数组操作 |
| pandas | 2.0+ | 读取 CSV、处理缺失值 |
| scikit-learn | 1.3+ | SVC、数据划分、评估指标 |
| matplotlib | 3.7+ | 绘制混淆矩阵热力图(可选) |
2.3 三分类策略:OvR 与 OvO 的取舍
SVM 天然是二分类器,做三分类必须做策略拆分。scikit-learn 的SVC通过decision_function_shape参数切换两种策略:
ovr(一对多,默认):为每个类别训练一个“该类别 vs 其余类别”的二分类器,三分类下总共 3 个分类器,预测时取决策函数得分最高的类别。ovo(一对一):三分类下训练 3 个二分类器(类别 0 vs 1、0 vs 2、1 vs 2),预测时两两投票,得票最多的类别胜出。
从计算量上看,三分类时两者差异不大;但类别数增加到 10 类时,OvR 只需要 10 个分类器,OvO 需要 C(10,2)=45 个分类器,训练耗时差距一下就出来了。这个项目默认用ovr,理由是它的分类器数量少、训练更快,且对类别不均衡更友好。
提示:
decision_function_shape只影响decision_function的输出形状和内部训练方式,最终predict结果在多数场景下两者差异很小,不必过度纠结。
3. 核心模块拆解:从 CSV 读到归一化再到模型产出的完整链路
3.1 数据加载与预处理:归一化是 SVM 的命门
preprocess.py负责把原始 CSV 读进来、处理缺失值、完成特征归一化:
import pandas as pd from sklearn.preprocessing import MinMaxScaler def load_and_preprocess(path="data/sample_data.csv"): df = pd.read_csv(path) # 特征列存在缺失值,用中位数填充;标签缺失则直接丢弃该行 if df.isnull().sum().sum() > 0: df = df.fillna(df.median(numeric_only=True)) df = df.dropna(subset=["label"]) X = df.drop("label", axis=1).values y = df["label"].astype(int).values # RBF 核依赖样本间的距离度量,量纲不统一会毁掉决策边界 scaler = MinMaxScaler(feature_range=(0, 1)) X_scaled = scaler.fit_transform(X) return X_scaled, y, scaler这段代码里有两个关键点。第一,缺失值用中位数而不是均值填充,因为中位数对离群点不敏感,更适合 SVM 这种依赖距离度量的模型。第二,MinMaxScaler把所有特征压缩到 [0,1] 区间,这是 SVM 项目里最常见的归一化手段。
我见过不少翻车案例:某个特征取值范围是 0 到 10000,另一个是 0 到 1,不归一化直接训练,RBF 核计算距离时小数值特征完全被淹没,准确率比随机猜好不了多少。归一化是 SVM 的命门,这一步省掉,后面调参全白费。
3.2 划分训练集和测试集:stratify 保证类别比例
train.py里用train_test_split做数据划分,这里有个容易被忽略的参数stratify:
from sklearn.model_selection import train_test_split from sklearn.svm import SVC X, y, scaler = load_and_preprocess() X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y # 按类别比例分层抽样 )如果不传stratify,默认是随机打乱划分。三分类数据里如果某一类样本很少,随机划分很可能让测试集里这一类的样本数量为 0,训练出的模型在评估时会对该类完全没有区分能力。
stratify=y的作用是保证训练集和测试集中三个类别的比例与原始数据一致,比如原始数据三类比例是 6:3:1,划分后两边都保持这个比例。random_state=42固定随机种子,保证每次运行结果可复现,这是做对比实验的基本素养。
3.3 模型训练:RBF 核 + 参数初始化
核心训练代码只有几行:
model = SVC( kernel="rbf", # 高斯径向基核函数,适合非线性可分数据 C=1.0, # 正则化系数,越大越强调正确分类,越小越容忍误分 gamma="scale", # RBF 核宽度,scale 表示按 1/(n_features * X.var()) 自动计算 decision_function_shape="ovr", # 一对多三分类策略 probability=True, # 启用概率估计,方便看置信度 random_state=42, ) model.fit(X_train, y_train)sample_data.csv里的三类样本不是线性可分的,线性核效果明显偏差,所以项目默认选 RBF 核。关于参数:
C是误分类惩罚系数。C越大,模型越努力把训练样本分对,容易过拟合;C越小,决策边界越平滑,但可能欠拟合。gamma控制 RBF 核的影响半径。gamma越大,单个样本的影响范围越小,决策边界越复杂;gamma越小,边界越平滑。probability=True会启用 Platt 缩放,让predict_proba能输出伪概率,但会增加训练耗时。不需要置信度时建议关掉。
3.4 评估:别只看准确率,混淆矩阵才是三分类的照妖镜
evaluate.py里同时输出分类报告和混淆矩阵:
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix import matplotlib.pyplot as plt import numpy as np y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) print(f"准确率: {acc:.4f}") print(classification_report( y_test, y_pred, target_names=["类0", "类1", "类2"] )) cm = confusion_matrix(y_test, y_pred) print(cm)三分类只报一个准确率是不够的。假设数据里类 0 占 90%,模型全预测成类 0 也有 90% 准确率,看起来很高,实际完全没用。混淆矩阵能看出每个类别的具体表现:对角线是正确预测的样本数,非对角线是混淆情况。
# 可视化混淆矩阵(可选) plt.imshow(cm, interpolation="nearest", cmap="Blues") plt.colorbar() plt.xlabel("预测类别") plt.ylabel("真实类别") plt.show()如果classification_report里某一类的 recall 很低,优先检查是不是类别不均衡,再考虑是否要调整class_weight参数,这个坑在第 4 章单独展开。
4. 避坑指南:SVM 三分类最常见的四个翻车现场
4.1 特征未归一化,准确率只有 55%
现象:训练完模型后,测试集准确率只有 55% 左右,和随机猜测差不多。查阅决策边界后发现,边界几乎完全由某个取值范围大的特征决定。
原因:RBF 核计算样本距离时,大数值特征在欧氏距离中占绝对主导地位,小数值特征的信息被压缩掉了。SVM 不像树模型那样对特征尺度不敏感,这是它最典型的“玄学”之一。
解决:在训练前强制做归一化,项目里用MinMaxScaler(feature_range=(0,1)),也可以用StandardScaler做标准化。从那以后我每次新建 SVM 项目,第一件事就是先检查特征取值范围,再看别的。
4.2 类别不均衡,小类别 recall 直接为 0
现象:分类报告里类 2 的 recall 是 0.00,precision 也是 0.00,但整体准确率有 91%,看起来好像还行。
原因:三分类数据里类 2 只占 5%,SVM 的软间隔优化不会自动调整类别权重,模型为了最小化整体误分率,直接放弃了数量少的类。
解决:在SVC里加上class_weight="balanced",让模型按类别样本数的反比自动加权;如果效果还不够,可以配合sample_weight在fit时手动给少数类更大权重。这是处理不均衡三分类最直接的手段。
4.3 gamma 调得过大,训练集 100% 测试集 70%
现象:训练集准确率接近 100%,测试集只有 70% 左右,典型的过拟合表象。画出决策边界后发现每个样本周围都形成一个“孤岛”。
原因:gamma过大时,RBF 核的影响半径非常小,模型把训练样本逐个圈起来,决策边界极度扭曲,对新样本完全失去泛化能力。
解决:把gamma调到0.1或更小,或者用第 5 章的GridSearchCV做网格搜索。经验值是gamma在0.01到1之间扫描,配合C一起调。
4.4 开了 probability=True,训练时间翻倍
现象:同样 5000 个样本,不开probability训练 3 秒,开了之后训练 8 秒,而且predict_proba出来的概率值在多数结果上偏向 0.5。
原因:probability=True会启用 Platt 缩放,内部要做额外的交叉验证来拟合 sigmoid 参数,训练开销明显增加。这个过程中得到的概率并不是真正的类别概率,而是决策函数值的二次映射,不同类别间可比性有限。
解决:不需要概率输出时保持probability=False;需要输出置信度时,可以只用decision_function的得分做排序,或者接受训练时间的增加。
5. 实验结果与调参路径:从默认参数到最优组合
5.1 基线结果:核函数之间的差距
固定C=1.0、gamma="scale",只切换核函数,在测试集上的表现如下:
| 核函数 | 准确率 | 训练耗时 | 说明 |
|---|---|---|---|
| linear | 0.913 | 0.08s | 线性边界,欠拟合 |
| poly(degree=3) | 0.947 | 0.32s | 多项式边界,中等表现 |
| rbf | 0.983 | 0.15s | 默认核,本数据表现最好 |
这说明样本边界是非线性的,线性核容量不够,RBF 核能拟合更复杂的边界。如果你的数据特征维度很高、样本量很大,可以优先试线性核,训练快且不容易过拟合。
5.2 网格搜索:C 和 gamma 的组合才是上限
RBF 核的最终上限取决于C和gamma的组合,GridSearchCV可以自动扫描:
from sklearn.model_selection import GridSearchCV param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.01, 0.1, 1, "scale"], "kernel": ["rbf"], } grid = GridSearchCV( SVC(decision_function_shape="ovr" ), param_grid, cv=5, # 五折交叉验证,减少过拟合 scoring="accuracy", n_jobs=-1, # 并行跑全部组合 ) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_)参数说明:cv=5表示每个参数组合都做五折交叉验证,用 5 次的平均准确率作为该组参数的得分,能有效避免单次划分带来的偶然性;n_jobs=-1表示使用所有 CPU 核心并行计算,4 组 C × 4 组 gamma × 5 折 = 80 次训练会被大幅压缩。
这个项目样例上的最优组合是C=10, gamma=0.1,准确率从基线 0.983 提升到 0.992。值得注意的是,C增大到 100 后准确率反而下降,说明正则化太弱导致过拟合,这验证了第 4.3 节的判断。
5.3 多分类策略对比:OvR 与 OvO 的实际差异
固定最优参数,对比两种策略:
| 策略 | 分类器数 | 准确率 | 训练耗时 |
|---|---|---|---|
| ovr | 3 | 0.992 | 0.21s |
| ovo | 3 | 0.990 | 0.24s |
三分类场景下两者差别很小,ovr略微占优。但如果把类别数扩展到 10 类,ovr始终是 10 个分类器,ovo则变成 45 个分类器,训练耗时差距会拉开到接近 4 倍。项目里默认ovr的原因就在这里:保证精度不降的前提下,给后续类别扩展留足空间。
6. 二次开发:从三分类走向多分类与接口化部署
6.1 从 3 类扩展到 10 类:只需要改一个参数
把三分类推广到更多类别时,代码改动非常小:重新准备包含更多类别标签的数据,把SVC的class_weight设为"balanced",然后重跑训练即可。注意两点:第一,类别数增加后ovo的分类器数量呈平方级增长,类别超过 8 个时建议继续用ovr;第二,stratifify=y在类别极不均衡时可能报错,此时需要手动检查每个类别的最小样本数是否大于test_size对应的样本量。
6.2 模型持久化与预测接口封装
训练好的模型用 joblib 保存,预测脚本直接加载,这套写法在二次开发里非常常用:
import joblib # 训练完成后保存 joblib.dump(model, "model/svm_model.pkl") joblib.dump(scaler, "model/scaler.pkl")注意保存时要同时保存scaler,因为预测阶段的新样本必须经过和训练时完全相同的归一化处理,否则模型输入分布不一致,预测结果会偏移。加载后封装成预测接口,可以快速接进业务系统里:
def predict_one(features): model = joblib.load("model/svm_model.pkl") scaler = joblib.load("model/scaler.pkl") x = scaler.transform([features]) proba = model.predict_proba(x)[0] label = int(model.predict(x)[0]) return {"label": label, "confidence": float(max(proba))}这段代码把原始输入、归一化、预测、置信度输出串成一条链,返回的结果可以直接给到上层服务展示。
我最初做这个项目时只保存了模型没保存 scaler,上线后预测结果乱成一团,排查了半天才发现是新样本没有归一化。从那以后我每次做分类项目,都会把预处理器和模型放进同一个持久化目录里,加载时强制同时读出来,再跑一遍单测确认边界样本的输出稳定。这个习惯帮我避开了后面好几个项目的同类问题,希望帮到你。
本文还有配套的精品资源,点击获取