简介:遗传算法与支持向量机结合的Matlab实现资源,面向本科、硕士阶段机器学习或智能优化课程学习者,适用于基础教程、算法对比实验及教研参考。压缩包共48个文件,以35个m源码文件为主,涵盖主程序、数据训练与测试脚本,配套5个mat数据文件,另有mexglx、dll等编译辅助文件,整体仅1.62MB,轻量易用。内含GA_SVM核心代码及完整运行结果,可在Matlab2019a直接运行,帮助读者理解遗传算法特征选择/参数优化与SVM分类器的协同流程。已有946人学习下载,按教程步骤可快速复现结果,适合作为课程作业或论文实验的参考实现。虽然文件结构紧凑,但代码注释和运行数据已覆盖算法验证关键环节,有利于初学者对照排错并进一步拓展。
1. 遗传算法优化支持向量机的核心痛点与适用场景
用RBF核的支持向量机做分类,调参体验通常比建模更煎熬。C和gamma对精度曲线的影响很陡,同样的训练集,核宽度从一个数量级跳到另一个数量级,交叉验证分数能相差十几到二十个百分点;对着网格搜索扫一遍,等把参数面覆盖完,半天就过去了,数据一换又得重来。遗传算法解决的是这个问题:把C和gamma当作一个二维决策变量,让一组候选参数组成一个种群,经过选择、交叉、变异几十代后自动收敛到验证误差低的区域。这个过程不需要预设精细步长,也不要求你理解SVM对偶求解的内部细节。适合想快速确定支持向量机超参、看懂网上遗传算法优化支持向量机MATLAB代码包里每一步在干什么的工程师;后续换libsvm还是Python的sklearn,思路也都能平移。
2. 为什么调C和gamma值得交给遗传算法:网格搜索的边界与GA的套路
2.1 C和gamma在RBF-SVM里各自管什么
先对准参数名。在libsvm里,两个要调的超参叫C和gamma;在MATLAB自带的fitcsvm里,对应的是BoxConstraint和KernelScale。BoxConstraint是误分类损失的惩罚系数,C越大,模型越不愿意让训练样本犯错,决策边界跟着变复杂,训练误差低,但边界把噪声也一并记了下来;C越小,边界越平滑,但也可能欠拟合。KernelScale是RBF核的宽度参数,表示单个支持向量的影响半径;KernelScale越小,模型可以拟合更尖锐的局部模式,KernelScale越大,边界就越接近线性。
这两个参数还经常互相牵制,C偏大同时KernelScale偏小时,模型会把每个训练点都“背”下来,典型表现是训练集准确率接近满分而测试集分数很难看。反过来,C很小且KernelScale很大时,所有样本的影响范围重叠在一起,边界退化成一条近似直线,模型丢失了非线性分类能力。所以在实际工程里,很少有人单独调一个参数,而是把C和KernelScale放同一个搜索空间里一起找。
有个换算关系经常把人卡住:libsvm的gamma和fitcsvm的KernelScale不是同一个东西。libsvm中gamma等于1除以2倍的sigma平方,fitcsvm中的KernelScale就是sigma。网上下的代码包里这两个工具箱混着用,不先把参数单位对齐,后面所有搜索结果都是错的。
2.2 网格搜索的死角与GA的搜索逻辑
C和gamma都连续取值,网格搜索能枚举的只是参数面上有限个离散点。比如C取10个候选值,KernelScale取10个候选值,一轮就是100次SVM训练;数据量上千、单次训练耗时较长时,这个代价已经不小,可最佳位置可能正好落在两个网格点之间。随机搜索更省事,但它在参数面上没有导向性,只能靠运气撞出好区域。
遗传算法把搜索过程变成一条向低误差区域收敛的进化路径。种群里的每个个体是一个二维坐标,对应一组C和KernelScale;每一代都让所有个体分别做一次交叉验证评估,错误率低的个体有更高概率进入下一代,再通过交叉和变异产生新候选。它不需要目标函数可导,SVM交叉验证错误率这种黑箱输出也能直接作为适应度。三者取舍可以看这张表。
| 寻优方式 | 评估策略 | 全局搜索能力 | 典型代价 | 适用场景 |
|---|---|---|---|---|
| 网格搜索 | 枚举候选集 | 受步长限制,易漏掉格子间的较好解 | 评估次数等于C候选数与核候选数之积 | 参数少、每次训练很快 |
| 随机搜索 | 范围内随机采样 | 覆盖较广,但不会定向收敛 | 评估次数不确定,只能碰运气 | 参数面宽、先找大致区域 |
| 遗传算法 | 种群逐代进化 | 通过选择、交叉、变异定向收敛 | 单次评估慢,但通常几十代能到达可用区域 | SVM超参寻优、黑箱目标函数 |
GA并不是在所有场景都压过另外两种。当候选区间已知且训练一次小于0.1秒,网格搜索仍然非常直接;但SVM训练通常不满足这个条件,GA用几十次评估逐步收敛到好区域,每次换代还能看到整体分布如何收窄。遗传算法优化支持向量机的适用面,正是这种“一次评估贵、参数面连续、对全局最优没有解析解”的组合。
2.3 选型:自带fitcsvm加ga工具箱,比libsvm更适合从零跑通
MATLAB生态里做这件事通常有两套搭配。第一套是libsvm的matlab接口加自写GA循环;第二套是fitcsvm配合Global Optimization Toolbox的ga函数。前者好处是模型训练快、社区资料多,但需要额外编译mex;后者全部在MATLAB原生环境里跑,参数名统一,交叉验证直接在fitcsvm里用'CrossVal','on'完成,代码量更少。我一般推荐从后者起步。
% libsvm 与 fitcsvm 的核参数换算 % 已知 libsvm 的 gamma,算 fitcsvm 的 KernelScale sigma = 1 / sqrt(2 * gamma_libsvm); % 反向换算 gamma_libsvm = 1 / (2 * KernelScale^2);这两行代码解决两个工具箱交叉使用时的最大坑。网上流传的遗传算法优化支持向量机matlab代码,有的基于libsvm实现,有的基于fitcsvm,参数名不统一时直接对照换算,能省很多排错时间。决定用fitcsvm之后,剩下的核心工作就是把适应度函数写清楚,然后交给ga。
3. 遗传算法优化支持向量机的最小闭环:在MATLAB里把适应度函数与ga跑通
3.1 先准备数据与适应度函数骨架
用内置的鸢尾花数据来演示,它刚好是三分类,样本量150,适合验证流程。实验前先把训练集和测试集分开,测试集在GA之外完全不参与,否则后面算出来的测试成绩是虚高的。适应度函数是GA和目标模型的唯一接口:GA给进来一组二维参数C和KernelScale,适应度函数在训练集上做交叉验证,把错误率返回给GA。错误率越低,个体被保留的概率越高。
% svm_cv_error.m % 输入参数向量 p = [C, KernelScale],返回 5 折交叉验证错误率 function err = svm_cv_error(X, y, p) C = p(1); ks = p(2); mdl = fitcsvm(X, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', C, ... 'KernelScale', ks, ... 'Standardize', true, ... 'CrossVal', 'on', ... 'KFold', 5); err = kfoldLoss(mdl); end这里的关键选择是把'Standardize', true放到fitcsvm里,而不是预先在外部做一次zscore。两种写法的区别在于:放在fitcsvm里时,模型在每折训练时独立计算均值和方差,再对对应验证折做同样变换,天然避免信息泄漏;如果自己先在整个数据集上做标准化,训练集和测试集的统计量就混在一起了。代码里的KFold=5表示把训练集分成5份,模型会训练5次,适应度函数的耗时几乎完全由这5次训练决定。
提示:第一次跑通流程时,把种群规模和迭代代数都调小,确认代码没有报错,再逐步加大。
3.2 调用ga完成寻优并输出最优参数
主脚本分成三步:固定随机种子、划分完全隔离的测试集、调用ga。固定随机种子是为了让同一份数据在每次运行时得到可复现的结果,这在报告实验数据时几乎是必须的。cvpartition用'HoldOut', 0.3随机抽出三成样本当测试集,剩下的七成只用于GA和最终训练。
%% 加载并划分数据 load fisheriris X = meas; % 150 x 4 y = species; % 三个类别标签 rng(2024); cvp = cvpartition(y, 'HoldOut', 0.3); Xtr = X(training(cvp), :); ytr = y(training(cvp), :); Xte = X(test(cvp), :); yte = y(test(cvp), :); %% 绑定适应度函数 fitfcn = @(p) svm_cv_error(Xtr, ytr, p); %% 参数搜索边界 lb = [0.1, 0.1]; % [C_min, KernelScale_min] ub = [100, 10]; % [C_max, KernelScale_max] %% 设置GA选项 options = optimoptions('ga', ... 'PopulationSize', 20, ... 'MaxGenerations', 50, ... 'Display', 'iter', ... 'PlotFcn', @gaplotbestf, ... 'UseParallel', false); %% 运行遗传算法 [bestP, bestErr] = ga(fitfcn, 2, [], [], [], [], lb, ub, [], options); %% 用最优参数重训并测试 finalMdl = fitcsvm(Xtr, ytr, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', bestP(1), ... 'KernelScale', bestP(2), ... 'Standardize', true); testAcc = 1 - loss(finalMdl, Xte, yte);ga函数的第二个输入是决策变量个数,这里就是2;lb和ub控制搜索框;中间4个空数组对应线性不等式、线性等式、空约束和空非线性约束,MATLAB语法要求占位。'Display', 'iter'会在命令行打印每一代的平均分和最优分,这是观察收敛是否正常的第一手信息。'PlotFcn', @gaplotbestf画出最优分数的下降曲线。'UseParallel'在参数评估耗时很长的任务里再开,第一次跑通时先关掉更稳定。
ga函数本身来自Global Optimization Toolbox,也就是常说的MATLAB优化工具箱。它这里执行的完整流程是:生成初始种群、逐个调用fitfcn计算适应度、选择精英、交叉变异、进入下一代。bestErr是GA停止时找到的最低交叉验证错误率,bestP是对应的最优参数。
3.3 结果怎么读:exitflag、收敛曲线与最终训练
运行结束后,exitflag是额外的判断信息。exitflag=0表示因为达到MaxGenerations上限而停止,这是最常见的情况;exitflag=1表示提前满足停止条件,说明最优解收敛得很好。这两种情况都不需要额外处理,但如果你写自动化脚本,可以顺手记录到日志里。
bestErr是最优个体对应的5折交叉验证错误率,它反映的是GA寻优过程中验证集的平均表现,不是测试集分数。真正对外汇报的模型能力,要用finalMdl在Xte和yte上算出来的testAcc。loss函数返回的是分类错误率,用1减掉再转成准确率。
由于鸢尾花分类本身太过容易,GA往往在十几代内就能找到接近满分的参数,实际工程价值更多体现在维度更高、样本量更大、类别更不平衡的分类任务上。跑通这个最小闭环之后,后面真正花时间的部分,是适应度函数设计和参数范围的选择。
4. 不要把GA当成调参神器:适应度设计、搜索范围与三类翻车现场
4.1 适应度函数里的交叉验证如何避免信息泄漏
很多人写这类代码时会犯一个隐蔽的错误:先把全部数据标准化,再划分训练测试集。这样一来,测试集的均值和方差已经参与了训练数据的变换,最终测试准确率多少带点水分。fitcsvm的'Standardize'参数绕开了这个问题,但如果你把svm实现换成libsvm,或者自己写训练预测循环,就很容易走回老路。一个稳妥的做法是在脚本开头一次性生成确定好的数据划分,适应度函数里永远只接收Xtr和ytr,完全不接触Xte。
rng(2024); cvp = cvpartition(y, 'HoldOut', 0.3); Xtr = X(training(cvp), :); ytr = y(training(cvp), :); Xte = X(test(cvp), :); yte = y(test(cvp), :);适应度函数内部还有一层划分。fitcsvm配合'CrossVal','on','KFold',5,会在训练集内部再做5折交叉验证。这里的随机性同样会影响结果:同一组参数跑两次,可能因为划分不同而得到略微不同的错误率。GA对这种噪音并不敏感,因为它的比较对象是同一代里的其他个体,大家承受的分配误差方向是一致的。但如果数据量很小,5折交叉验证的方差会变大,这时可以把KFold降到3,或者换成留一法。
注意:适应度函数里不要设置新的随机种子。让每一折划分保持随机,反而能让GA对参数的评价更接近真实分布。
4.2 参数上界下界与GA种群参数的经验表
C和KernelScale的范围设大了搜索慢,设小了错过好区域。我一般会先按数量级给范围,跑一轮粗搜,再根据GA输出的最优参数收缩边界做第二轮。常见经验值如下,多数分类任务都能落在这个范围内。
| 参数 | 建议范围 | 说明 |
|---|---|---|
| BoxConstraint(C) | 0.1 ~ 100 | 数据标准化后从1附近的量级开始覆盖 |
| KernelScale | 0.1 ~ 10 | 对应libsvm中gamma约为0.005 ~ 50 |
| PopulationSize | 20 ~ 50 | 数据量大时从20起步,避免单代评估过慢 |
| MaxGenerations | 30 ~ 100 | 依据收敛曲线决定,曲线平了就可以停 |
| KFold | 5 | 数据量大保持5,小样本可设3或使用留一法 |
PopulationSize比想象中更影响最终效果。种群太小,比如只有10个个体,交叉时可选余地小,容易陷入局部最优;种群太大,比如100个,每代的交叉验证耗时直接翻倍,收敛速度也不会成比例变快。我通常从20或30开始,跑一次看收敛曲线再调整。
MaxGenerations并不是越大越好。SVM训练本身偏重,50代乘以20个个体就是1000次SVM训练。如果数据量再大一点,这个耗时可能从分钟级涨到小时级。实际经验是,GA前期收敛很快,后20代的改进幅度经常不到千分之一,所以先用小代数验证流程,再用适量代数拿正式结果。
4.3 适应度不降、过拟合、类别不平衡:三个高频问题
在真实项目里,以及在很多在线练习平台的分类任务作业里,GA不出效果的原因往往不是算法本身,而是适应度函数或数据处理环节出了问题。下面三类现象我几乎每次都遇到,对应排查思路如下。
| 现象 | 优先检查 | 修正方法 |
|---|---|---|
| Best分数几十代不降 | 数据有没有标准化;适应度函数内部是否不小心使用了全量数据 | 在fitcsvm里打开'Standardize';检查函数签名和传入数据 |
| 训练误差接近0但测试误差高 | C和KernelScale的范围是不是太冲 | 收紧上界,比如C降到10以内,KernelScale抬到1以上 |
| 少数类几乎全部被分错 | 类别分布严重不平衡 | 在fitcsvm中显式设置'Prior', 'empirical'或'Cost'矩阵 |
第一类问题最常见。如果训练数据里特征量纲差异极大,比如一列是0到1的归一化分数,另一列是几十万的数值,SVM的边界形状会被大数值特征主导,GA怎么调C和gamma都救不回来。这是标准化要解决的问题,不是GA要解决的问题。
第二类问题往往被误判成“GA搜失败了”。实际上GA找到了一个在训练集上表现优异的参数组合,但C过大加上KernelScale过小,让模型把噪声也学习进去了。解决办法不一定需要换算法,把搜索边界改窄,重新跑一轮就能看到测试集分数明显回升。
第三类问题在信用评分、故障诊断这类真实分类任务里特别明显。少数类样本占比可能只有5%,适应度函数返回的交叉验证错误率会被多数类主导,GA会把“全部判成多数类”当成一个优秀解。这时适应度函数不能只看整体错误率,要在fitcsvm里设置类别先验概率或代价矩阵,让少数类的分错代价更高。
5. 把GA优化支持向量机的搜索再收紧一步:适应度缓存与早停判断
GA运行到后期,种群会全部聚集在最优解附近,这时几乎每一代都在评估非常相近的C和KernelScale。同一个参数组合被重复训练很多次,是纯浪费。解决思路是在适应度函数里加一层缓存,已经算过的组合直接返回历史结果。
% svm_cv_error_cached.m function err = svm_cv_error_cached(X, y, p) persistent cache; if isempty(cache) cache = containers.Map('KeyType', 'char', 'ValueType', 'double'); end key = sprintf('%.6f_%.6f', p(1), p(2)); if cache.isKey(key) err = cache(key); return; end mdl = fitcsvm(X, y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', p(1), ... 'KernelScale', p(2), ... 'Standardize', true, ... 'CrossVal', 'on', ... 'KFold', 5); err = kfoldLoss(mdl); cache(key) = err; endpersistent变量在MATLAB里可以跨函数调用保存数据。containers.Map用字符串作为键,键里保留6位小数精度,防止GA传来的浮点参数因为微小差异绕过缓存。一个注意事项:如果把'UseParallel'打开,并行worker各自持有独立的persistent变量,缓存不会共享,代码依然正确,只是命中率下降。
第二个技巧是让GA提前停掉没有意义的迭代。ga的OutputFcn回调提供了一个检查种群状态的入口,连续几十代最优分数保持不变时,说明种群已经进入平台期。
function [state, options, optchanged] = ga_plateau_stop(options, state, flag) optchanged = false; n = numel(state.Best); if n > 15 && state.Best(n) == state.Best(n - 10) state.StopFlag = 'plateau'; % 非空字符串即触发提前终止 end end把这个函数挂到optimoptions('ga', ..., 'OutputFcn', @ga_plateau_stop)上,ga会在最优分数连续10代完全不变时提前结束。state.Best记录每一代的最优适应度值,判断条件要求至少有16个历史代,避免前期偶然相同导致误停。
最后一步是把GA给出的bestP用于最终模型训练,并在完全没参与选参的测试集上重新计算一次准确率。bestErr只代表交叉验证错误率,testAcc才是对外汇报的结果。把这两个数字分开记录在实验报告里,对方一眼就能看出参数是搜出来的还是过拟合出来的。
本文还有配套的精品资源,点击获取