简介:支持向量机(SVM)是机器学习中常用的监督学习模型,可用于分类与回归。该资源为一份面向初学者与进阶学习者的SVM实践压缩包,包含Matlab源码、配套数据集与专题PPT,帮助读者从理论、编码、调参到评估走通完整流程。包内共6个文件,以两个M文件为核心,展示SVM训练与测试代码;3个txt文件提供数据或说明,便于直接运行;PPT则系统讲解SVM原理、核函数选择、C与gamma参数影响等关键内容。压缩包整体大小约1.92MB,轻量易用,适合快速上手。已有256人学习下载。资源价值在于将SVM理论要点与Matlab实现紧密对应,读者可对照代码理解最大间隔、支持向量、核映射等概念,也可基于自带数据集练习交叉验证与准确率、召回率等指标评估,有助于完成课程实验或入门机器学习实践。
1. 从 zip 到能跑的 SVM:这个 Matlab 代码包到底给了你什么
下载到一个“支持向量机 Matlab代码和数据.zip”,你多半正处在这样的状态:课程设计要交,导师让先跑个基线,或者手头正好有一批打好的标签想让 SVM 先试一把。解压之前它是个黑匣子,解压之后你面对一堆 .m 文件和一个 .mat 数据集,反而更不知道该先看哪一行。这篇笔记就把这条完整链路捋一遍——数据怎么读进来、选 fitcsvm 还是 svmtrain、C 和 KernelScale 怎么调、交叉验证怎么做、模型怎么存下来。全程用可复现的最小命令,你只要照着敲,就能从一个 zip 包跑到一张 ROC 曲线。适合没用过 Matlab 机器学习工具箱、或者刚接触 SVM 的从业者和学生。
2. 解压后的第一步:文件识别、数据装载与环境准备
2.1 Zip 里通常有哪些文件:先分清代码和数据再动手
多数这类压缩包的结构并不复杂,常见构成是:两到三个 .m 脚本、一个 .mat 数据文件或 .csv,外加一份 readme。你拿到压缩包的第一件事不要急着运行,而是先在当前工作目录下解压,然后打开 Matlab,把路径切到解压后的文件夹。zip 解压这一步里最值得注意的不是文件数量,而是路径:文件夹名和路径里不要带中文、不要带空格,否则老版本 Matlab 在读取数据时会因为编码问题报出莫名其妙的错误。这是我在帮别人看代码时见过最高频的环境坑。
解压完成后,先用一个小命令把文件夹里的内容列清楚:
% 列出当前文件夹下所有文件及其大小 dir % 如果你只关心 .m 和 .mat/.csv files = dir('*.m'); for i = 1:length(files) fprintf('%s\n', files(i).name); enddir('*.m')会返回一个结构体数组,里面包含文件名、大小、修改日期。跑完这段,你就能确认包里到底有没有训练脚本、数据文件、还有没有 readme。注意看bytes字段:如果数据文件只有几百字节,说明它是演示用的小样本,后面调参数时要把过拟合的警惕心拉满;如果数据文件有几 MB,那说明样本量或特征维度不低,网格搜索时就要控制组合数量。
2.2 数据装载:load、readtable 与标签对齐
数据文件是 .mat 还是 .csv,决定了读取方式。.mat文件用load一下就能读进来,但很多人栽在“不知道 mat 里的变量名叫什么”上。解决办法是先不看文件内容,而是加载后打印变量信息:
% 加载 mat 文件,并显示内部变量结构 loaded = load('data.mat'); disp(fieldnames(loaded)); % 打印变量名 % 假设变量名为 data,查看其尺寸和前几行 X_raw = loaded.data; fprintf('数据尺寸: %d 行, %d 列\n', size(X_raw,1), size(X_raw,2)); disp(X_raw(1:5, :));这段代码的核心在fieldnames:它能告诉你 mat 文件里到底存了哪些变量,省去你一个个猜名字的时间。看到尺寸之后,还需要确认最后几列到底是特征还是标签。常见布局有两种:一是标签放在最后一列,二是标签单独存成另一个变量如label或Y。最靠谱的做法是看前五行数据的值域,特征一般分布是连续的,而标签通常只有两三个离散值。如果前五行的最后一列一直等于 1 或 0,那多半就是标签。
如果是 .csv 文件,就用readtable读取,再用table2array把数据和标签拆开:
% 读取 CSV:假设前 4 列是特征,最后一列是标签 T = readtable('data.csv'); X = table2array(T(:, 1:end-1)); y = table2array(T(:, end)); fprintf('特征维度: %d 列, 标签取值: %s\n', size(X,2), num2str(unique(y)'));这里有一个隐藏的细节:readtable会自动把字符串列识别为cell类型,如果标签列是字符串(例如 'yes'/'no'),直接table2array得到的是 cell,fitcsvm会直接报错。解决办法是用categorical转一下,或者干脆在数据预处理阶段就把标签映射成 +1/-1。我一般倾向映射成 +1 和 -1,因为这样既能用fitcsvm,也能无缝切换到 libsvm 的svmtrain,不会因为换了工具而回头改数据。
2.3 数据归一化:先看分布范围,再决定要不要做
SVM 对特征的量纲非常敏感,这是它和树模型最大的区别。决策树不管特征范围是 0 到 1 还是 0 到 10000,分裂点都是相对排序;SVM 的距离计算则会把数值范围大的特征变成主导,导致核函数的相似度被某几列特征带偏。所以拿到数据后的第二个固定动作是看每列的 min 和 max:
% 打印每个特征的取值范围,判断是否需要归一化 for i = 1:size(X,2) fprintf('特征 %d: [%.4f, %.4f]\n', i, min(X(:,i)), max(X(:,i))); end如果发现某一列的范围是 [0.01, 0.02],另一列是 [0, 5000],那归一化就躲不掉了。常见做法是 z-score 或映射到 [-1,1],我一般推荐先 z-score,因为它能保留离群点的分布信息,而直接线性映射到 [-1,1] 会被离群点压扁正常数据的间距:
% 计算均值与标准差 mu = mean(X); sigma = std(X); % 标准化 X_norm = (X - mu) ./ sigma; % 检查标准化后的范围 fprintf('归一化后每列均值: %.2f\n', mean(X_norm)); fprintf('归一化后每列标准差: %.2f\n', std(X_norm));注意,mu和sigma必须只从训练集计算,预测时用同一组参数对测试集做变换,这是新手最容易忽略的——直接在全部数据上算 norm,会造成训练信息泄露,让交叉验证结果虚高。后面要讲的网格搜索也应该在归一化之后做,否则调出来的参数换一副数据就失灵。
3. fitcsvm 训练代码逐行拆解:从原始矩阵到可预测模型
3.1 该用 fitcsvm 还是 svmtrain:接口差异与选择理由
Matlab 里做 SVM 有两条路线:自带统计工具箱的fitcsvm和 libsvm 的svmtrain。很多人下载的包里如果写的是svmtrain,那说明作者用的是老版本 Matlab 或直接调了 libsvm 的 mex 编译文件。用svmtrain会引入两个麻烦:一是编译环境不匹配,换台电脑就要重编;二是它的输出是一个结构体,预测时还得手动计算决策函数,很繁琐。fitcsvm是官方维护的接口,训练完的对象可以直接调predict,还能配合crossval、fitPosterior做验证和概率输出。
| 对比项 | fitcsvm | libsvm svmtrain |
|---|---|---|
| 适用版本 | R2015a 之后稳定 | 任意,但需编译 |
| 输出对象 | ClassificationSVM,自带 predict | 结构体,需手动做决策 |
| 核函数配置 | KernelFunction + KernelScale | -t 参数 + -g 参数 |
| 交叉验证 | 内置 crossval | 需自己循环 |
| 概率输出 | fitPosterior 一步搞定 | 需要 -b 1 并二次处理 |
如果包里同时给了两种代码,我建议直接忽略svmtrain版本,用fitcsvm重写一遍。原因很实在:它省去了跨平台的编译加载问题,参数名也更接近机器学习常识,遇到问题去查文档能查到更多对应案例。
3.2 最小可用训练代码:从数据矩阵到模型对象
假设你已经把数据装进了X_norm(特征矩阵)和y(+1/-1 标签),下面这段就是最小可用的训练代码:
% 训练 SVM 分类器 mdl = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... % 高斯核,处理线性不可分 'BoxConstraint', 1, ... % 即 C,控制误分类惩罚 'KernelScale', 'auto'); % 自动估计 gamma 的倒数 % 查看模型的基本信息 disp(mdl);这里KernelFunction选'rbf'是保守做法,因为线性核能解决的问题 RBF 核也能覆盖,而反过来不行。BoxConstraint就是软间隔里的 C,取值越大对误分类的惩罚越重,边界越曲折。KernelScale是 RBF 核里gamma的倒数,训练时取'auto'会让 Matlab 用一个启发式值兜底,正式调参时这里是要被网格搜索替代的关键位置。
完成训练后立即做一件事:把训练集预测一下,得到训练集准确率:
% 训练集上的预测与准确率 trainPred = predict(mdl, X_norm); trainAcc = sum(trainPred == y) / length(y); fprintf('训练集准确率: %.2f%%\n', trainAcc * 100);不要因为训练准确率是 100% 就高兴,这一步只是用来确认模型有没有正常收敛。如果训练准确率只有 70%,那要么是标签有噪声、要么是特征没对齐,先回头查数据,不要急着调参。训练集 100% 测试集也 100% 的情况在小样本里很常见,不说明模型好,只说明数据简单或过拟合,真正可靠的评价在后面第四节的交叉验证。
3.3 训练完先别急着用:交叉验证是第一道安检
一个人拿到新包最容易犯的错误是:训练完直接拿全部数据做预测,然后宣布准确率 98%。这种做法的漏洞在于,模型的泛化能力完全没有被检验。正确做法是先用crossval做 K 折交叉验证,用验证误差决定要不要继续调参:
% 5 折交叉验证 cvmdl = crossval(mdl, 'KFold', 5); % 计算验证损失(误分类率) cvLoss = kfoldLoss(cvmdl); fprintf('5折交叉验证误差: %.2f%%\n', cvLoss * 100); fprintf('5折交叉验证准确率: %.2f%%\n', (1 - cvLoss) * 100);crossval会把训练数据分成 5 份,轮流拿其中 4 份训练、1 份验证,最终返回一个带有验证结果的模型对象。kfoldLoss把这个模型的平均误分类率算出来。如果你的训练集准确率 98%,交叉验证准确率只有 60%,这就是标准的过拟合信号,接下来直接跳到网格搜索调参数,而不是尝试更复杂的特征工程。
这里再补充一个容易被忽略的点:数据集如果类别不均衡,比如正样本 900 个、负样本 100 个,直接用准确率评价会虚高。这时候应该看每一类的召回率,或者使用kfoldLoss里的'Mode','individual'查看每一折的表现分布:
% 查看每一折的误差,检查稳定性 foldLoss = kfoldLoss(cvmdl, 'Mode', 'individual'); disp(foldLoss);如果某一折的误差明显高于其他折,说明该折的数据里可能存在离群点或标签噪声。遇到这种情况,不要急着删数据,先用下一章的网格搜索找更平滑的参数组合。
4. 网格搜索 C 和 KernelScale:把验证集误差压低
4.1 参数组合网格搜索:用循环跑出最小的验证误差
SVM 调参的最核心组合就两个:BoxConstraint(C)和KernelScale(对应 RBF 的 gamma)。C 控制你对误分类的容忍度,C 越小边界越平滑、越可能欠拟合;C 越大边界越弯曲、越可能过拟合。KernelScale 控制单个样本的影响范围,值越小 gamma 越大,每个样本只影响很近的邻居,边界容易碎;值越大 gamma 越小,样本影响范围大,边界趋向平滑。这两个参数必须一起调,因为它们互相制约。
常见做法是先用对数网格粗搜,再在最优区间细搜。我一般先把 C 取logspace(-1, 2, 5),即 0.1、1、10、100、1000 五个点,KernelScale 取logspace(-2, 1, 5),即 0.01、0.05、0.3、1.6、10 五档,两两组合共 25 组。每组跑一遍 5 折交叉验证,选出验证误差最小的一组:
% 网格搜索 C 与 KernelScale CList = logspace(-1, 2, 5); KSList = logspace(-2, 1, 5); bestAcc = 0; bestC = CList(1); bestKS = KSList(1); for i = 1:length(CList) for j = 1:length(KSList) % 用当前参数训练并做 5 折交叉验证 mdlTemp = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', CList(i), ... 'KernelScale', KSList(j)); cvTemp = crossval(mdlTemp, 'KFold', 5); lossTemp = kfoldLoss(cvTemp); accTemp = 1 - lossTemp; fprintf('C=%.4f KernelScale=%.4f Acc=%.2f%%\n', ... CList(i), KSList(j), accTemp*100); if accTemp > bestAcc bestAcc = accTemp; bestC = CList(i); bestKS = KSList(j); end end end fprintf('最优: C=%.4f KernelScale=%.4f, Acc=%.2f%%\n', ... bestC, bestKS, bestAcc*100);跑完这一步,你手上就有了第一批有说服力的参数。注意,网格搜索得到的“最优”是相对的,它只说明在这 25 个组合里哪个最好,不代表全局最优。如果你发现最优参数落在搜索边界上,比如 C 取到了 1000 或 KernelScale 取到了 10,那就说明搜索范围没框对,应该把该方向的范围再扩展。
实际跑的时候还有两个经验:第一,fitcsvm内部用 SMO 求解,数据量几千条时一秒钟内能跑完,25 组乘 5 折也就是一百多次训练,耗时完全可接受;第二,如果数据量到了几万条,网格搜索会明显变慢,此时把'KFold'改成 3,或者先随机抽 5000 条做粗搜,再用全量数据在最优邻域细搜,能省很多时间。
4.2 训练完看什么:训练误差、验证误差与决策边界可视化
选出最优参数后,用最优参数重新训练,并对比三组数字:训练集准确率、交叉验证准确率、以及最终测试集准确率。如果训练集和交叉验证准确率差距在 5 个百分点以内,说明参数选得比较安全;如果差距超过 10 个点,这就是过拟合。不要只盯准确率,当数据只有两个特征时,画出决策边界能让你一眼看清样本在特征空间里的分布:
% 用最优参数训练最终模型 mdlFinal = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', bestC, ... 'KernelScale', bestKS); % 生成二维网格并可视化决策边界(仅当特征数为2时) if size(X_norm,2) == 2 [x1Grid, x2Grid] = meshgrid(... linspace(min(X_norm(:,1))-0.5, max(X_norm(:,1))+0.5, 200), ... linspace(min(X_norm(:,2))-0.5, max(X_norm(:,2))+0.5, 200)); gridPred = predict(mdlFinal, [x1Grid(:), x2Grid(:)]); contourf(x1Grid, x2Grid, reshape(gridPred, size(x1Grid))); hold on; gscatter(X_norm(:,1), X_norm(:,2), y, 'rg', 'o', 6); title('SVM 决策边界'); hold off; end可视化这一步的价值不是出图好看,而是能让你快速识别两类问题:一是决策边界特别碎,说明 KernelScale 太小,模型几乎在记样本而不是学规律;二是边界把少量孤立点单独圈出来,说明 C 太大,模型被离群点牵着走。看到这两种形态,直接回网格搜索里把对应方向的范围收紧。
如果是多维特征就看不了二维图,此时替代方案是画混淆矩阵。用confusionchart一行代码就能画出来:
% 计算测试集预测结果与混淆矩阵 testPred = predict(mdlFinal, X_norm); cm = confusionchart(y, testPred);混淆矩阵比准确率更能暴露问题:如果预测结果全部集中在一个类别,说明模型已经退化成了“全猜多数类”,该回去检查类别权重'Prior'或者改用'ClassNames'指定类别顺序。
4.3 数据量大的替代:当网格搜索跑不动时怎么办
网格搜索虽然直观,但在大数据集上会变成灾难。几万样本乘以 25 组参数再乘 5 折,训练时间可能从几分钟膨胀到几小时。这种时候我的处理方式是分两步走:第一步,随机抽样 5000 条做粗搜,把最优区域的中心确定下来;第二步,在全量数据上用粗搜得到的最优邻域做细搜,此时组合数控制在 9 组以内,比如 C 取三个值、KernelScale 取三个值。
另外一个更省时间的做法是贝叶斯优化,Matlab 自带的fitcsvm支持'OptimizeHyperparameters':
% 使用贝叶斯优化自动调参(适合数据量较大的情况) mdlBO = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... 'OptimizeHyperparameters', {'BoxConstraint', 'KernelScale'}, ... 'HyperparameterOptimizationOptions', struct(... 'AcquisitionFunction', 'expected-improvement-plus', ... 'MaxObjectiveEvaluations', 30, ... 'ShowPlots', false, ... 'Verbose', 0));贝叶斯优化的特点是它不是盲目遍历,而是根据已跑的参数组合猜测哪里可能出现更优值,用较少的训练次数逼近较优参数。我自己在数据量超过 1 万条时会优先用这个方案,因为它的时间开销通常是网格搜索的十分之一不到。代价是结果有一定随机性,所以跑完以后记得看一眼mdlBO.HyperparameterOptimizationResults,确认最优参数没有落在搜索边界。
5. 避坑:Matlab 里跑 SVM 常见的 5 个翻车点
5.1 现象:训练集准确率 100%,交叉验证却不到 60%
这个现象在新手手里出现率极高。原因基本两类:一是 C 或 KernelScale 设置得太过激,模型把训练样本连同噪声一起背了下来;二是归一化时用了整个数据集的信息,导致交叉验证的每一折都在“偷看”测试折的分布。解决方法是先检查归一化代码——mu和sigma必须由训练折单独计算,不能提前在全局计算好;然后降低 C 并增大 KernelScale,给边界一个平滑的机会。如果你发现无论怎么调参数交叉验证都上不去,还要回头怀疑标签是否存在错标,此时可以画一下 t-SNE 看看两个类别是否真的可分。
5.2 现象:训练完模型后,保存成 .mat 再加载,predict 报错或极慢
fitcsvm返回的对象默认保留训练用的全部数据,用于交叉验证和后续的resubPredict,所以直接save出来的文件会很大,加载后预测也可能慢。原因是你没有把模型压缩成紧凑形式。解决方法是保存前调用compact:
% 压缩模型后再保存,减少文件体积并去掉训练数据冗余 compactMdl = compact(mdlFinal); save('svm_model.mat', 'compactMdl');加载后predict直接调用即可,不需要重新训练。这个坑在给课程设计或交付代码时特别明显——别人拿到你的 .mat 文件发现几百 MB,第一反应就是代码写错了。
5.3 现象:fitcsvm 直接报错,提示 “Y 必须是数值向量、逻辑向量或分类向量”
这种错误几乎全是标签类型不对。常见来源是readtable读入的标签列是字符串或 cell 类型,而你直接把这一列传给了fitcsvm。解决方法是给标签做一个显式映射:
% 将字符串标签映射为 +1/-1 if iscell(y) || isstring(y) yMap = zeros(length(y), 1); yMap(ismember(y, '正类名')) = 1; yMap(ismember(y, '负类名')) = -1; y = yMap; end如果你懒得写映射,也可以用categorical(y)把字符串变成分类变量,fitcsvm支持分类标签。但需要注意:如果你之后要用fitPosterior做概率输出,数值型 +1/-1 标签仍然是兼容性最好的选择。
5.4 现象:数据文件里存在 NaN,训练没报错但结果明显不对
fitcsvm默认遇到 NaN 会忽略对应样本,如果 NaN 占比高,你实际参与训练的样本数远少于你以为的,模型自然不准。更隐蔽的是,predict阶段遇到 NaN 会自动给最保守的类别,导致预测结果严重偏向某一类。解决方法是训练前检查并清理:
% 检查并删除包含 NaN 的样本行 nanRows = any(isnan(X), 2) | isnan(y); fprintf('发现 %d 行 NaN 数据,已移除\n', sum(nanRows)); X_clean = X(~nanRows, :); y_clean = y(~nanRows, :);如果 NaN 分布在个别特征且占比不高,可以考虑用该列中位数填充而不是直接删行,以保留样本量。但注意中位数填充只适合特征分布较稳定的情况,如果某一列缺失超过 30%,建议直接丢弃该特征。
5.5 现象:负样本只有 30 个,正样本有 3000 个,预测结果几乎全是正类
SVM 在类别极不均衡时,决策边界会被多数类“推”向少数类一侧,得到的模型其实是废的。原因是fitcsvm默认把误分类代价设为两类相等,而多数类犯错的数量天然比少数类多,优化器会倾向于直接全猜多数类。解决办法是给少数类加大误分类权重,通过'Cost'参数指定:
% 给负类更高的误分类惩罚,缓解类别不平衡 costMatrix = [0 1; 3 0]; % 第1行正类,第2行负类;负类误判代价设为3 mdlBalanced = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', 1, ... 'Cost', costMatrix, ... 'ClassNames', [1; -1]);代价矩阵里每一行代表真实类别,每一列代表预测类别,值表示把该真实类别误判成另一类的惩罚力度。负样本只有正样本十分之一时,我会把误判负类的代价设到 3 到 5 倍之间,具体数值通过网格搜索微调。调整之后再看混淆矩阵,你会发现模型开始愿意把边界往多数类压一压,少数类的召回率会明显提升。
6. 收尾技巧:用 ROC 曲线验证泛化能力,用 saveCompactModel 留存模型
训练和调参完成后,千万不要让模型只活在当前 workspace 里。我的固定习惯是:训练脚本末尾写一个“模型存档 + 泛化验证”区块,一箭双雕。存档这一步,前面提过用compact压缩,这里再补充一个上位替代——用saveCompactModel它内部会自动压缩并维护兼容性:
% 先训练最终模型 mdlFinal = fitcsvm(X_norm, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', bestC, ... 'KernelScale', bestKS); % 保存紧凑模型供后续部署 saveCompactModel(mdlFinal, 'svmModel.mat');加载时用loadCompactModel,之后可以直接做预测。这个函数的好处在于它保存的模型对象跨 Matlab 版本更稳定,不会出现换台机器加载后字段丢失的问题。
泛化验证方面,准确率不够直观, ROC 曲线才是判断分类器质量的更完整工具。fitcsvm默认输出的是分类标签而不是概率,要画 ROC 得先给模型加一个概率校准:
% 给 SVM 模型附加概率输出,然后画 ROC 曲线 mdlPro = fitPosterior(mdlFinal); [~, score_scores] = resubPredict(mdlPro); % 得到的是负类与正类概率矩阵,取正类列 [rocX, rocY, ~, auc] = perfcurve(y, score_scores(:,2), 1); plot(rocX, rocY); xlabel('假阳率'); ylabel('真阳率'); title(sprintf('ROC 曲线 (AUC=%.3f)', auc));AUC 在 0.9 以上说明模型真正学到了类别区分度,0.8 到 0.9 算可用的基线,低于 0.8 则需要回炉。我一般把 AUC 低于 0.8 视为“当前特征撑不起这个任务”,此时与其继续调 SVM,不如回到特征工程。
如果你想把模型接到生产流程里,注意fitPosterior教程里得到的概率并不适合直接当置信度,它只是训练集上的校准结果,真实部署时要根据业务场景重新划定阈值。我自己吃过这个亏:训练集上把阈值定在 0.5 看起来挺好,上线后因为正负样本比例和训练集不同,实际分类结果全面偏斜。后来改成在验证集上重新找最优阈值,才把线上效果稳住。每次交付 SVM 代码包,我都会把这个 ROC 曲线和阈值选定步骤写进脚本的尾部,这样别人拿到代码后不会只看到一行训练命令而不知道下一步该做什么。希望帮到你。
本文还有配套的精品资源,点击获取