ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Matlab SVM预测实战:从数据划分到交叉验证调参全流程

Matlab SVM预测实战:从数据划分到交叉验证调参全流程

简介:这套Matlab版SVM预测代码面向需要在分类与回归任务中应用支持向量机的机器学习学习者和科研工程人员,覆盖从模型训练到结果预测的完整闭环。压缩包共7个文件,含5个m脚本和1个txt数据文件,整体仅12KB;m脚本分别实现SVM训练、核函数构造、SVR回归主程序与仿真演示,txt文件为可直接读取的测试数据集,便于边运行边核对效果。目前已有2164人学习下载,适合希望在较短时间内掌握SVM具体落地的入门与进阶用户。这份代码完整展示了从线性核、RBF核等核函数选择,到惩罚参数C与gamma调优,再到fitcsvm训练与predict预测的主要环节;模块划分明确、注释清晰,既可用于课程实验与论文复现,也可作为工程中SVM预测模块的轻量参考实现。

1. SVM 预测在 Matlab 里的真面目:它不是一个模型,是一套要自己配的流程

SVM(支持向量机)在 Matlab 里做预测,难点从来不在“调用哪个函数”,而在“你手上的数据到底能不能被喂进去”。很多人下了一个 svm 支持向量机 matlab 代码包,打开一看,里面是 libsvm 的 mex 文件、一堆 .m 脚本和一个 README,跑起来才发现:数据格式不对、标签不是 ±1、没做归一化、kernel 参数是默认值,结果出来一塌糊涂。这其实是 SVM 落地的常态——它不像神经网络那样“喂进去就能训练”,它要求你先回答三个问题:样本够不够、特征怎么表示、正负样本边界在哪。这篇笔记我用自己的习惯讲一遍:怎么在 Matlab 里把 SVM 预测做成一个可复现、可调参、敢拿去跑结果的流程。适合刚接触 SVM、手头有数据但不知道从哪开始的人,也适合被 libsvm 和 fitcsvm 折腾过、想理清边界的人。

2. 在 Matlab 里实现 SVM 预测:选 fitcsvm 还是 libsvm,五步落地路径

2.1 两条技术路线怎么选:工具箱函数和 libsvm 不是一回事

Matlab 里做 SVM 预测,常见做法是二选一:用 Statistics and Machine Learning Toolbox 自带的 fitcsvm 和 predict,或者用台湾大学林智仁老师的 libsvm 工具箱。这两条路线我都用过,先说结论:如果你的样本量在几千到几万、特征是连续值、主要做二分类,fitcsvm 就够了,代码量少,和 Matlab 的表格数据类型、交叉验证函数配合得最好;如果你要处理多分类、要用自定义核函数、或者要从 Python 那边迁过来沿用同一套参数经验,libsvm 的接口更通用,但你要自己处理数据格式转换。

fitcsvm 的核心语法是mdl = fitcsvm(X, y, 'KernelFunction', 'rbf', 'Standardize', true),其中 X 是样本矩阵,每行一个样本,y 是标签向量,取值必须是二值(比如 1 和 -1,或者 true/false)。它默认用的是线性核,但实际预测里用得多的是 rbf 核,也就是高斯核,因为线性核解决不了非线性边界。rbf 核有两个必调参数:BoxConstraint(记作 C)和 KernelScale(记作 gamma)。C 控制误分类的惩罚力度,太大容易过拟合,太小欠拟合;gamma 控制单个样本的影响半径,越大边界越复杂,越小边界越平滑。

libsvm 那边走的是另一套接口:model = svmtrain(y, X, '-c 2 -g 0.5'),注意参数是字符串传进去的,y 在前、X 在后,这和 fitcsvm 正好相反。初用 libsvm 的人最容易在这翻车:把 X 和 y 的位置写反,或者忘了把标签转成 double 类型。libsvm 的优势是它对稀疏数据、大规模样本的支持更成熟,预测速度也快,但它不直接支持 Matlab 的 table 数据类型,你得先把表拆成矩阵。

我一般这样选:如果数据已经整理成 Excel 或 CSV,要快速出一个预测结果,用 fitcsvm,因为它能直接吃 table 的列;如果要做研究对比、要调 C 和 gamma 直到出图,用 libsvm,因为它的网格搜索脚本比较成熟。两条路线的参数含义是相通的,C 和 gamma 在两边都存在,只是名字略有差别。

对比项fitcsvmlibsvm
输入格式X 矩阵 + y 向量,支持 tableX 矩阵 + y 向量,标签转 double
核函数linear、rbf、polynomial、自定义linear、rbf、polynomial、sigmoid
多分类需要 fitcecoc 封装自带多分类策略
参数方式可选参数名值对字符串简洁指定
交叉验证crossval 无缝集成自己写循环或网格脚本
典型样本量万级以内十万级更稳

2.2 五步落地路径:从数据表到预测结果的标准动作

拿 fitcsvm 这条线做例子,我通常按五个步骤走,每一步都有一句明确的命令可抄。

第一步,读数据并构造训练集。无论数据来自 Excel 还是 CSV,先读成 table,再拆成特征矩阵和标签向量。注意标签必须是 categorical 或 logical/double 二值,不能是字符串。如果标签是字符串(比如 '正常' 和 '故障'),用categorical转一下,或者映射成 1 和 -1。

data = readtable('train_data.csv'); X = data{:, 1:end-1}; % 特征列,假设最后一列是标签 y = data{:, end}; % 原始标签 y_num = double(categorical(y)); % 转成 1、2 之类的数值 % 再把数值标签映射为 1 / -1 y_bin = 2 * (y_num == max(y_num)) - 1;

这段代码的逻辑是:先用花括号从 table 里取数值矩阵,最后一列分离出来当标签;categorical会把字符串变成类别序数,所以 y_num 会是 1、2、3 这种整数;最后把较大的那一类映射为 1,另一类映射为 -1。这里有个隐藏问题:如果原本的类别不是两类,比如三类,这段代码会把中间那类也归到 -1 去,所以要在拆数据之前先确认unique(y)恰好是两个值。

第二步,划分训练集和测试集。常见做法是留出法,按 7:3 或 8:2 切分。不要用随机索引硬切,因为可能把一类样本全切到测试集里。这里我习惯用cvpartition做分层划分。

cv = cvpartition(y_bin, 'HoldOut', 0.3); X_train = X(training(cv), :); y_train = y_bin(training(cv), :); X_test = X(test(cv), :); y_test = y_bin(test(cv), :);

cvpartition的第二个参数指定留出比例,0.3 表示测试集占 30%。它默认做分层抽样,也就是训练集和测试集里正负样本的比例基本一致,这对类别不平衡的情况比手写 randperm 稳定得多。切完先别急着训练,先看一眼y_train里两类样本的数量,如果一边只有几个样本,模型基本是废的。

第三步,训练模型。这里先不上调参,用默认参数跑一遍,目的是建立基线。

mdl = fitcsvm(X_train, y_train, ... 'KernelFunction', 'rbf', ... 'Standardize', true);

Standardize设为 true 是必须的,因为 rbf 核函数计算的是样本间距离,特征的量纲不一致时,数值大的列会直接压过数值小的列。这一步的默认 C 和 gamma 是 Matlab 根据数据自动选的,通常不是最优,但能给你一个可对比的起点。

第四步,预测并评估。用训练好的模型对测试集做预测,然后计算准确率、混淆矩阵、F1 这几个指标。

y_pred = predict(mdl, X_test); acc = mean(y_pred == y_test); confmat = confusionmat(y_test, y_pred); tp = confmat(1, 1); fp = confmat(1, 2); fn = confmat(2, 1); tn = confmat(2, 2); precision = tp / (tp + fp); recall = tp / (tp + fn); f1 = 2 * precision * recall / (precision + recall);

predict返回的是和训练标签同类型的值,所以y_pred == y_test能直接比较。confusionmat返回 2×2 矩阵,我用行代表真实、列代表预测,tp和fp取第一行,这样后续调参时能直接看精确率和召回率的变化。基线模型跑到这一步,如果准确率不到 0.7,问题多半不在模型,而在特征或数据划分,先去检查前两步。

第五步,保存模型并做单样本预测。训练完不是终点,真正的预测场景是来了一个新样本要打标签。保存用saveLearnerForCoder,或者直接save成 .mat 文件。

saveLearnerForCoder(mdl, 'svm_model'); new_sample = X_test(1, :); % 模拟一条新数据 y_new = predict(mdl, new_sample);

saveLearnerForCoder生成的模型文件可以用 Matlab Coder 转成 C/C++ 代码,适合部署到嵌入式设备或现场机器。如果只是本地用,直接save('svm_model.mat', 'mdl')就行,但要注意:加载模型时对应的特征列顺序、标准化参数都封装在模型对象里,不会丢。这条链路跑通后,你的 SVM 预测就具备了拿到新数据直接出结果的能力。

2.3 特征矩阵的常见坑:单个样本怎么变成一行

新手最容易卡在把一条原始数据变成一行特征向量的环节。SVM 不管你的原始数据是波形、图像还是一串日志,它只认一行固定长度的数值向量。所以特征提取是 SVM 预测里最耗时、也最影响上限的一步。

常见做法是手工设计特征:对时间序列算均值、方差、峰值、能量;对图像算灰度直方图、纹理特征;对传感器数据算频域特征。这一步没有银弹,但有一个原则可循:特征要有区分度。区分度怎么检查?在训练前,分别算正类和负类每个特征的均值,如果某个特征在两边的均值几乎相同,它对预测就没有贡献,可以考虑删掉。

我自己常用的脚本是这样:先按标签分组算均值,再按特征列比较差异,输出一个简单排名,用来决定要不要删列。

pos_mean = mean(X_train(y_train == 1, :)); neg_mean = mean(X_train(y_train == -1, :)); diff_ratio = abs(pos_mean - neg_mean) ./ std(X_train, 0, 1); [~, feat_order] = sort(diff_ratio, 'descend'); % feat_order 前若干列即为区分度最高的特征

这段代码的逻辑是:每一列特征在正类和负类之间的均值差,除以该列的标准差,得到一个类似信噪比的指标。比率越大,说明该特征对分类的贡献越明显。sort排序后,feat_order就是按区分度从高到低排列的特征列编号。你可以保留前 10 个或前 20 个,舍掉尾部区分度接近 0 的列,这样训练更快、过拟合风险也更低。这里要注意,std(X_train, 0, 1)是按列算标准差,如果某一列的标准差为 0 也就是常数列,这个比率会是 NaN,训练前要先把常数列删掉。

3. 把数据切成训练集和测试集:避免泄露,才能拿到真实预测精度

3.1 数据划分是预测可信度的第一道闸门

很多 SVM 预测项目跑出的准确率高得离谱,但落地就废,原因不在模型,在数据划分时“泄露”了。泄露出现在三种常见操作里:一是先做归一化或标准化,再划分训练测试集,导致测试集的信息混进了训练过程;二是随机划分时没有分层,类别极不平衡导致测试集全是多数类;三是时序数据没有按时间顺序切,直接随机打乱,让训练集“偷看”了未来信息。

正确的顺序是先划分、后标准化。fitcsvm 里Standardize参数之所以要在训练时设定,就是因为它只会在训练集上计算均值和标准差,再把这个变换保存进模型,预测时自动把测试集按同一组参数变换。如果你自己手动先zscore整个矩阵再划分,就等于测试集的均值混入了训练集的变换参数,在学术实验里这叫数据泄露,在工程里这就叫结果虚高。

我自己固定用的流程是:读数据 → 分层划分 → 交给 fitcsvm 训练。手动做标准化的情况我只在一种状态下用——当我要把特征可视化、输出到报告里看数的时候。也就是说,分标准化和训练标准化是两码事,但都必须在划分之后做。

3.2 你该用留出法还是 K 折交叉验证

留出法适合一眼看结果、快速验证流程通不通;K 折交叉验证适合样本量不大、你想要一个更稳健的精度估计。fitcsvm 的交叉验证跑法很顺,直接调crossval。

cvmdl = crossval(mdl, 'KFold', 5); loss = kfoldLoss(cvmdl); acc_cv = 1 - loss;

这段代码的意思是:在已经训练好的mdl的同一批参数上,把训练集再切 5 份,轮流用 4 份训练、1 份验证,最后把 5 次验证的平均损失返回。kfoldLoss默认返回的是误分类率,所以1 - loss就是交叉验证准确率。注意这里crossval接收的mdl本身已经在原训练集上训练过,但交叉验证内部会重新训练出 K 个子模型,不是拿mdl去验证,理解这一点才不会把kfoldLoss的结果误当成测试集精度。

K 折里 K 的取值常见是 5 或 10。K 太小,验证集样本少、方差大;K 太大,训练 K 个模型的成本高、耗时翻倍。对样本量几千的这个量级,5 折够用,10 折更稳,但配合网格调参时一般先用 5 折,选完参数再用 10 折复核,这样能把调参时间砍一半。

cvpartition也支持 K 折划分,写法略有不同。我一般只在需要手动控制循环时用它,fitcsvm 加 crossval 的组合在多数场景更省事。两者选一个就好,别混着用。

3.3 类别不平衡时的切法:分层切和权重惩罚

如果正类样本有 500 个,负类只有 50 个,直接训练出的 SVM 会倾向于把所有样本预测成多数类,宏观准确率看着不低,但少数类一个都抓不出来。两类样本数量差超过 10 倍时,一定要处理。常见手法有两个:分层划分保比例,以及用fitcsvm的Prior或Cost参数做类别惩罚。

分层划分用cvpartition即可,它默认就会保持训练测试集里的类别比例。如果划分后测试集中的少数类样本只剩个位数,属于正常现象,因为测试集太小,不代表划分错了。此时应该考虑增大测试集比例,或者改用交叉验证而不是单次留出。

类别惩罚是另一个有效手段:训练时告诉模型“分错少数类的代价更高”。fitcsvm 里可以直接设置Cost矩阵,把少数类的误分类代价调高。

cost = [0 1; 3 0]; % 第1类预测为第2类代价1,第2类预测为第1类代价3 mdl_balanced = fitcsvm(X_train, y_train, ... 'KernelFunction', 'rbf', ... 'Standardize', true, ... 'Cost', cost);

这里的Cost矩阵含义是:Cost(i, j)表示把第 i 类预测为第 j 类的代价。对角元固定为 0。代价高的含义是模型宁可牺牲多数类的准确率也要保住少数类。代价倍数怎么调?我一般从 2 倍试到 10 倍,观测少数类的召回率从多少涨到多少,同时盯着总体准确率的下降幅度,选一个平衡点。必须看confusionmat的结果,不要只看准确率。

4. 手写一个交叉验证调参脚本:网格搜索 C 与 gamma 的标准姿势

4.1 C 和 gamma 的取值范围与步长怎么定

调参的第一步是把范围定住。C 常见取2.^-5到2.^5,即 0.03125 到 32;gamma 常见取2.^-5到2.^5。这个范围覆盖了绝大多数中小规模数据集的合理区间,超过这个范围的两端通常已经落入明显的欠拟合或过拟合区。步长上,log2 空间每次走 1 格,也就是 0.5、1、2、4 这样翻倍。先粗搜找趋势,再在趋势周围细搜半步,比如 0.75、1.5 这种。

fitcsvm 的KernelScale参数和 libsvm 的gamma有个换算关系:KernelScale = 1 / sqrt(gamma)。你用 fitcsvm 时调的是 KernelScale,调参方向和 gamma 是相反的——KernelScale 越大,边界越平滑;gamma 越大,边界越复杂。习惯上我按 gamma 来思考,然后转换出 KernelScale 来设参。这个换算常被忽略,是两套代码对不上结果的常见原因。

4.2 网格搜索的 Matlab 实现:从两层循环到热力图

网格搜索的本质是把 C 和 gamma 的候选值排成一张表,逐个组合跑交叉验证,然后找交叉验证准确率最高的组合。实现上用一个两层循环,外层滚 C,内层滚 gamma,记录每次的损失。

C_list = 2 .^ (-5:5); gamma_list = 2 .^ (-5:5); cv_acc = zeros(length(C_list), length(gamma_list)); for i = 1:length(C_list) for j = 1:length(gamma_list) mdl_tmp = fitcsvm(X_train, y_train, ... 'KernelFunction', 'rbf', ... 'Standardize', true, ... 'BoxConstraint', C_list(i), ... 'KernelScale', 1 / sqrt(gamma_list(j))); cvmdl_tmp = crossval(mdl_tmp, 'KFold', 5); cv_acc(i, j) = 1 - kfoldLoss(cvmdl_tmp); end end [best_acc, idx] = max(cv_acc(:)); [best_i, best_j] = ind2sub(size(cv_acc), idx); best_C = C_list(best_i); best_gamma = gamma_list(best_j);

这段循环里有个细节:crossval每次都会重新训练一组子模型,所以整个网格是 11×11 共 121 组参数,每组做 5 折训练,总共要训练 605 个模型。样本量几千时一般一分钟内跑完,样本量上万后会明显变慢。此时可以先把候选集从 11 缩到 7(比如2.^(-3:3)),粗搜后再精调。ind2sub把线性索引转回行列坐标,这样就能拿到对应的 C 和 gamma。跑网格搜索时建议把cv_acc存下来直接surf画出来,一眼能看到参数高原区而不是孤立尖峰——孤立尖峰往往是过拟合信号。

figure; surf(log2(gamma_list), log2(C_list), cv_acc); xlabel('log2(gamma)'); ylabel('log2(C)'); zlabel('5-fold CV Accuracy');

这里 x 和 y 取 log2 是为了让坐标轴跨度均匀,否则参数在指数尺度上会挤成一团。观察曲面:如果准确率在某个区域形成一片平坦的高原,说明参数鲁棒性好;如果只有一个孤立尖峰,周围下降很快,那个点的泛化能力存疑,建议舍掉尖峰,选高原边缘的组合。

4.3 调完参数后务必做的一件事:用独立测试集复测

网格搜索结果出来之后,不要直接拿最佳参数重新训练再测试集上预测,一次就完事。正确的收尾动作是:用最佳 C 和 gamma 重新训练整个训练集,然后仅在之前留出的测试集上预测一次,记录精度。此时训练过程已经完全见不到测试集,结果才是可信的。

mdl_best = fitcsvm(X_train, y_train, ... 'KernelFunction', 'rbf', ... 'Standardize', true, ... 'BoxConstraint', best_C, ... 'KernelScale', 1 / sqrt(best_gamma)); y_final = predict(mdl_best, X_test); final_acc = mean(y_final == y_test);

这一步和网格搜索里的cv_acc是两个数字,cv_acc是训练集内部的交叉验证估计,final_acc是独立测试集上的真实表现。两者差值在 5% 以内属于正常,差距超过 10% 说明模型过拟合,需要回头检查特征是不是太稀疏、样本是不是太少、C 是不是调得过大。

我一般把这三个数字一起记录:基线默认参数在测试集上的精度、交叉验证最好精度、复测精度。三个数字放到一张表里,对比着看就能判断这个数据集的 SVM 预测是否真的靠谱。

参数基线默认网格最佳复测
C自动best_Cbest_C
KernelScale自动1/sqrt(best_gamma)1/sqrt(best_gamma)
测试集精度0.72—0.81
交叉验证精度—0.83—

4. 避坑与常见问题:SVM 预测最容易翻车的 4 个场景

4.1 标签不是 ±1,predict 结果对不上

现象:训练和预测没报错,但准确率永远是 0.5 上下或者对不上号。原因多半是fitcsvm里用了 0/1 标签,而 SVM 的决策边界只输出二值判定,导致预测值和标签映射不一致。解决:训练前把标签统一映射为 1 / -1,或者在 fitcsvm 里指定ClassNames。

mdl = fitcsvm(X, y, 'ClassNames', [-1 1]);

ClassNames的顺序决定了 predict 输出两类的对应关系,测试时y_pred就会和y_test对齐。我有个习惯:训练和测试的标签向量都打印一遍unique,确认两边类别集合完全一致再训练。如果测试集里冒出一个训练时没见过的标签,SVM 是预测不出来的,这种场景实际生产中常见,比如故障种类新增了,此时模型必须重训。

4.2 特征量纲不一致,精度直接拉开 10%

现象:直接训练,准确率 0.7;特征归一化后,准确率 0.85。原因:rbf 核的距离度量对量纲敏感,量级大的列主导了整个距离计算,量级小但同样重要的特征被淹没。解决:在 fitcsvm 里设Standardize, true,或者手动按列 zscore。注意手动 zscore 必须在划分训练测试集之后、只对训练集计算均值和方差,再用同一组参数变换测试集。fitcsvm 的Standardize参数把这一步封装了,省得自己写两遍。如果特征里混有类别型变量比如 1、2、3 的人为编码,不能直接当连续值喂进去,要先做 one-hot 或 ordinal 编码,否则距离计算会失真。

4.3 libsvm 的坐标顺序:y 在前、X 在后,还有 label 千万别传错

现象:沿用 fitcsvm 的思维写svmtrain(X, y, ...),Matlab 不报错,但训练出来的模型预测结果全是同一类。原因:libsvm 的接口是svmtrain(label, instance, options),label 必须在前,X 在后;同时 label 的类型必须是 double,不能是 logical 或 categorical。解决:训练前强制转换。

y = double(y_bin); model = svmtrain(y, X_train, '-c 8 -g 0.5'); % 预测 [y_pred, acc, ~] = svmpredict(y_test, X_test, model);

svmpredict的返回值第一项是预测标签,第二项acc是一个 1×3 的向量,包含准确率。不要用predict去调用 libsvm 的 model,那不是 Matlab 自带的预测函数,写成svmpredict才对。这个坑我翻过一次车,调了一下午才发现是函数名和参数顺序双重错误。

4.4 网格搜索跑得太久,先缩小范围再细搜

现象:11×11 网格配 5 折,加一个上万样本的数据集,跑了几十分钟没出结果。原因:模型训练次数太多,参数范围又偏宽。解决:第一轮粗搜用2.^(-3:3)共 7 个候选值,找到最优区域后,在最优 C 和 gamma 的附近各取左右 2 步细搜,候选集缩到 5×5。另外可以把KFold从 5 临时降到 3,粗搜阶段更快,找到位置后再恢复 5 折复核。

C_range = best_C * 2 .^ (-2:2); gamma_range = best_gamma * 2 .^ (-2:2);

这段是粗搜之后精调的标准写法:以best_C为中心,左右各乘 2 的 -2 次方到 2 次方,细搜范围是原来的四分之一步长。跑完精搜多半能再涨 1% 到 2%,如果浮动小于 0.5%,说明第一次粗搜的步长已经足够,不必再细化寸进。

5. 用一个简单技巧验证 SVM 预测是否被“暗坑”拖累:对比线性核与 RBF 核的边界

调参结束后,我建议你做最后一道验证:用同一个数据集分别训练线性核和 RBF 核,对比两者的测试精度和训练精度。如果 RBF 核的训练精度远高于线性核、但测试精度只高一点点,说明数据本身没有复杂的非线性边界,你的特征已经线性可分,用线性核就够了。反之,如果 RBF 核训练精度也明显更高,这条路才值得继续调 C 和 gamma。

mdl_linear = fitcsvm(X_train, y_train, ... 'KernelFunction', 'linear', ... 'Standardize', true); y_lin = predict(mdl_linear, X_test); acc_lin = mean(y_lin == y_test);

线性核只有 C 一个参数,训练快,解释性强。很多实际场景比如传感器阈值判断、简单故障分型,线性核已经够用。多调一个 gamma 不是免费的,它在小样本上尤其容易让模型记住噪声。我自己的判断流程是:先跑线性,测试精度上 0.85 就不再碰 RBF;线性到顶还是 0.75 左右,再上 RBF 调参。这样能把调参时间花在真正有价值的数据上。

最后再谈一个位置很尴尬但经常被问的问题:predict报错说特征数量不一致。SVM 模型对输入特征的列数是有记忆的,训练时用了 12 列特征,预测新样本也得是 12 列。会犯这个错的情况,往往是训练时用了整个 table,预测时取了data{:, 1:end-2},少取了一列。我的习惯是在保存模型前,把特征列的名字也存一份到.mat里,下次加载模型时先用isfield检查一遍列数和列名,再往上喂预测。这不算什么高深技巧,但它能救回你一次毫无头绪的报错排查时间。

如果你照着上面的流程跑了一遍,依然感觉精度上不去,建议回头再看一眼特征提取那边:SVM 预测的上限基本上在特征那里定型了,调参只是把模型拉近那个上限,而不是提高上限。我自己的经验是,花在特征提取上的时间至少要是调参的两倍,整体曲线才最划算。希望这套流程能帮你在 Matlab 里把 SVM 预测这条路走通。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进