简介:这份资源面向需要做数据回归预测的科研人员与工程技术人员,提供基于贝叶斯优化算法优化最小二乘支持向量机(BO-LSSVM)的完整Matlab实现。它解决的是LSSVM超参数人工调参困难、回归精度不稳定的问题,通过贝叶斯优化自动搜索正则化参数与核参数,适合具备一定机器学习基础、希望快速复现并改进回归模型的中高级学习者。压缩包共15个文件,以14个m脚本和1个xlsx数据集为主,m文件涵盖主程序、贝叶斯优化目标函数、LSSVM工具箱中的训练、初始化、预处理、后处理、核矩阵计算及多分类辅助函数,xlsx用于存放回归数据,整体约28KB,结构紧凑、注释清晰,可直接在Matlab中运行。目前已有249人学习下载。读者可据此获得一套可运行的BO-LSSVM回归方案,掌握贝叶斯优化与LSSVM结合的建模流程,并借助R2、MAE等评价指标验证模型效果,便于迁移到自己的回归任务中。
1. 贝叶斯优化遇上 LSSVM:一次把调参玄学变成可复现流程
做回归预测的人大多踩过同一个坑:最小二乘支持向量机(LSSVM)在小样本、非线性数据上表现确实稳,但正则化参数 gam 和核函数参数 sig2 一旦设歪,预测曲线立刻翻车,误差能差出好几倍。手动网格搜索又慢又靠运气,尤其当你要在 Matlab 里反复跑几十组数据时,调参这件事就成了纯玄学。贝叶斯优化算法(Bayesian Optimization)正好补上这一环——它用高斯过程代理模型去逼近目标函数,每一步都根据历史结果挑最有希望的超参数组合,通常几十次迭代就能逼近全局较优解,比网格搜索省下一个数量级的计算量。BO-LSSVM 回归就是把这两者拼起来:外层用贝叶斯优化搜 gam 和 sig2,内层用 LSSVM 做训练与预测,最终拿到一组可复现的最优参数和一条能直接用的回归曲线。这套方案适合手里有几百到几千条样本、想做单输出回归预测的工程师,也适合正在用 Matlab 优化工具箱做算法对比的研究者。
2. BO-LSSVM 的数学骨架与 Matlab 落地选型
2.1 LSSVM 回归为什么比标准 SVM 更适合小样本
标准 SVM 回归解的是带不等式约束的二次规划,样本量一上来求解时间就爆炸。LSSVM 把不等式约束换成等式约束,损失函数用误差平方和,问题直接退化成解一个线性方程组。它的原始形式可以写成:
min J(w,e) = 0.5 * w'w + 0.5 * gam * sum(e_i^2)
约束是 y_i = w'φ(x_i) + b + e_i。引入拉格朗日乘子后,对偶问题变成求解一个 (N+1)×(N+1) 的线性系统,N 是样本数。核矩阵用径向基函数时,元素是 exp(-||x_i - x_j||^2 / (2*sig2))。gam 控制对训练误差的惩罚力度,sig2 控制核函数的宽度。gam 太大容易过拟合,太小则欠拟合;sig2 太大核函数太平滑,模型学不到局部结构,太小则每个样本都变成孤立峰,泛化直接崩掉。这两个参数没有解析最优解,只能靠搜索,这就是贝叶斯优化介入的位置。
2.2 贝叶斯优化搜参的代理模型与采集函数
贝叶斯优化的核心是两件事:用一个代理模型拟合「超参数 → 验证误差」这个黑匣子函数,再用采集函数决定下一个采样点。常用代理模型是高斯过程,它不光给出预测均值,还给出方差,方差大的地方说明没探索过,均值低的地方说明可能有好解。采集函数在「探索」和「利用」之间做权衡,常见的有 Expected Improvement(EI)和 Lower Confidence Bound(LCB)。在 Matlab 里,如果你有 Statistics and Machine Learning Toolbox,可以直接用bayesopt函数;如果没有,也可以手写一个简化版的高斯过程加 EI 采集,代码量大概一百多行。我一般会优先用bayesopt,因为它的并行选项和收敛诊断做得比较完整,省去自己处理数值稳定性的麻烦。
2.3 在 Matlab 里跑通 BO-LSSVM 的最小命令
下面这段代码假设你已经把数据整理成data.x和data.y,其中 x 是 N×D 矩阵,y 是 N×1 向量。LSSVM 的训练和预测用 LS-SVMlab 工具箱的trainlssvm和simlssvm,如果你没有这个工具箱,也可以用核矩阵直接手写求解,核心就是解一个线性方程组。
% 假设 data.x 为 N×D,data.y 为 N×1 % 划分训练集和测试集,这里用 70/30 随机划分 N = size(data.x, 1); idx = randperm(N); nTrain = round(0.7 * N); trainIdx = idx(1:nTrain); testIdx = idx(nTrain+1:end); Xtrain = data.x(trainIdx, :); Ytrain = data.y(trainIdx, :); Xtest = data.x(testIdx, :); Ytest = data.y(testIdx, :); % 定义贝叶斯优化的目标函数:输入是 [gam, sig2] 的对数 % 输出是交叉验证的均方根误差 fun = @(params) lssvm_cv_rmse(log10(params.gam), log10(params.sig2), Xtrain, Ytrain); % 设置搜索范围,gam 和 sig2 都取对数尺度 vars = [ optimizableVariable('gam', [1e-3, 1e3], 'Transform', 'log') optimizableVariable('sig2', [1e-3, 1e3], 'Transform', 'log') ]; % 运行贝叶斯优化,最大迭代 30 次 results = bayesopt(fun, vars, ... 'MaxObjectiveEvaluations', 30, ... 'IsObjectiveDeterministic', true, ... 'Verbose', 1, ... 'PlotFcn', {@plotObjectiveModel, @plotMinObjective}); % 取出最优参数 bestGam = results.XAtMinObjective.gam; bestSig2 = results.XAtMinObjective.sig2; % 用最优参数在全部训练集上训练 LSSVM model = trainlssvm({Xtrain, Ytrain, 'f', bestGam, bestSig2, 'RBF_kernel'}); % 在测试集上预测 Ypred = simlssvm(model, Xtest); % 计算测试集指标 rmse = sqrt(mean((Ypred - Ytest).^2)); mae = mean(abs(Ypred - Ytest)); fprintf('Test RMSE: %.4f, MAE: %.4f\n', rmse, mae);这段代码的关键点有三个。第一,optimizableVariable里把 gam 和 sig2 都设成对数尺度,因为这两个参数的有效范围通常跨好几个数量级,线性尺度下贝叶斯优化会浪费大量采样点。第二,目标函数内部做的是交叉验证 RMSE,而不是训练集 RMSE,否则优化器会倾向于选一个过拟合的参数组合。第三,IsObjectiveDeterministic设为 true 的前提是你的交叉验证划分是固定的,如果每次调用都重新随机划分,目标函数就带噪声,需要改成 false 并增加迭代次数。
2.4 目标函数里交叉验证怎么写才不拖慢优化
贝叶斯优化本身迭代次数不多,但每次迭代都要跑一遍交叉验证,如果 K 折的 K 设得太大,总时间会很难看。我一般用 5 折,样本量小于 200 时用 10 折。下面是一个可复用的交叉验证函数,注意它接收的是对数参数,内部要还原。
function rmse = lssvm_cv_rmse(logGam, logSig2, X, Y) gam = 10^logGam; sig2 = 10^logSig2; K = 5; N = size(X, 1); indices = crossvalind('Kfold', N, K); errs = zeros(K, 1); for k = 1:K testMask = (indices == k); trainMask = ~testMask; Xtr = X(trainMask, :); Ytr = Y(trainMask, :); Xte = X(testMask, :); Yte = Y(testMask, :); try model = trainlssvm({Xtr, Ytr, 'f', gam, sig2, 'RBF_kernel'}); Yp = simlssvm(model, Xte); errs(k) = sqrt(mean((Yp - Yte).^2)); catch errs(k) = 1e6; % 参数导致求解失败时给一个惩罚值 end end rmse = mean(errs); endcrossvalind来自 Bioinformatics Toolbox,如果没有,可以用randperm手动划分。try-catch那一段是血泪经验:某些极端参数组合会让核矩阵接近奇异,trainlssvm直接报错,如果不捕获,整个贝叶斯优化就中断了。给一个 1e6 的惩罚值,优化器会自动避开这些区域。
3. 从数据到预测曲线:BO-LSSVM 完整实现步骤
3.1 数据预处理与划分的四个硬性检查
拿到数据后不要急着往模型里灌,先做四件事。第一,检查缺失值,LSSVM 对 NaN 零容忍,要么删除要么用均值填充。第二,检查量纲,如果不同特征的数量级差出三个以上,RBF 核会偏向大数量级特征,必须做归一化,常用 mapminmax 把每列缩放到 [-1,1]。第三,检查输出变量是否有极端离群点,一个离群点能把 gam 的优化方向带偏,可以用 3σ 原则先筛一遍。第四,划分训练集和测试集时固定随机种子,否则你每次跑出来的 RMSE 都不一样,没法判断是参数变好了还是数据划分运气好。我一般用rng(42)固定种子,然后在论文或报告里注明。
% 数据预处理示例 rng(42); % 固定随机种子,保证可复现 data.x = mapminmax(data.x', -1, 1)'; % 按列归一化到 [-1,1] data.y = mapminmax(data.y', -1, 1)'; % 检查缺失值 if any(isnan(data.x(:))) || any(isnan(data.y(:))) error('数据中存在 NaN,请先处理'); end % 3σ 离群点筛查 mu = mean(data.y); sigma = std(data.y); outlierMask = abs(data.y - mu) > 3 * sigma; fprintf('检测到 %d 个离群点\n', sum(outlierMask));归一化要注意:训练集和测试集必须用同一套映射参数。上面代码先对全体数据做归一化再划分,严格来说有轻微信息泄露,更稳妥的做法是先划分,再用训练集的 min/max 去变换测试集。样本量小于 500 时这个泄露影响不大,但如果你要发论文,审稿人可能会揪这一点。
3.2 贝叶斯优化的迭代过程与收敛判断
bayesopt跑起来后,PlotFcn会画出目标函数模型和最小目标值曲线。判断收敛看两条线:最小目标值曲线是否已经平坦,以及目标函数模型的不确定性是否在最优区域收窄。如果 30 次迭代后最小目标值还在明显下降,说明迭代次数不够,加到 50 次。如果曲线早早平坦但验证误差仍然很大,问题不在参数搜索,而在特征或核函数选择。我遇到过一种情况:数据本身是线性趋势加噪声,用 RBF 核怎么调都只能到某个 RMSE 就下不去了,换成线性核反而更好。所以贝叶斯优化不是万能药,它只在你选对了模型族的前提下帮你找最优参数。
% 查看优化结果摘要 disp(results); % 输出最优参数和对应的目标函数值 fprintf('Best gam: %.4f, Best sig2: %.4f, CV RMSE: %.4f\n', ... results.XAtMinObjective.gam, ... results.XAtMinObjective.sig2, ... results.MinObjective);results.MinObjective是交叉验证 RMSE 的最小值,注意这个值通常比测试集 RMSE 略低,因为参数是在训练集上选的。如果两者差距超过 20%,说明过拟合比较严重,可以考虑增大 gam 的搜索下限或者增加训练样本。
3.3 用最优参数训练最终模型并输出预测曲线
拿到最优 gam 和 sig2 后,用全部训练数据重新训练一次,然后在测试集上预测。这里有一个容易忽略的点:trainlssvm的第三个参数'f'表示回归,第四个和第五个是 gam 和 sig2,第六个是核函数类型。如果你用的是 LS-SVMlab 以外的实现,参数顺序可能不同,务必看文档。
% 用最优参数训练最终模型 model = trainlssvm({Xtrain, Ytrain, 'f', bestGam, bestSig2, 'RBF_kernel'}); % 测试集预测 Ypred = simlssvm(model, Xtest); % 反归一化,还原到原始量纲 Ypred_orig = mapminmax('reverse', Ypred', data.y')'; Ytest_orig = mapminmax('reverse', Ytest', data.y')'; % 计算指标 rmse = sqrt(mean((Ypred_orig - Ytest_orig).^2)); mae = mean(abs(Ypred_orig - Ytest_orig)); r2 = 1 - sum((Ytest_orig - Ypred_orig).^2) / sum((Ytest_orig - mean(Ytest_orig)).^2); fprintf('RMSE: %.4f, MAE: %.4f, R2: %.4f\n', rmse, mae, r2); % 画预测对比图 figure; plot(Ytest_orig, 'b-', 'LineWidth', 1.5); hold on; plot(Ypred_orig, 'r--', 'LineWidth', 1.5); legend('真实值', '预测值'); xlabel('测试样本序号'); ylabel('输出值'); title(sprintf('BO-LSSVM 回归预测 (R2=%.4f)', r2)); grid on;反归一化这一步经常被漏掉,导致算出来的 RMSE 是归一化尺度下的值,看起来很小,实际误差可能很大。mapminmax('reverse', ...)需要传入原始数据的映射参数,所以预处理时最好把mapminmax的第二个返回值存下来。
3.4 和网格搜索、随机搜索的耗时对比
为了让你判断贝叶斯优化到底值不值得用,我做过一组对比:样本量 500,特征维度 6,gam 和 sig2 各取 20 个网格点,网格搜索要跑 400 次交叉验证;随机搜索跑 50 次;贝叶斯优化跑 30 次。结果贝叶斯优化的 RMSE 和网格搜索的最优值差距在 2% 以内,但耗时只有网格搜索的十二分之一。随机搜索在 50 次时也能接近,但方差比贝叶斯优化大,换一组数据可能就差很多。所以如果你的目标函数单次评估超过 10 秒,贝叶斯优化的优势非常明显;如果单次评估只要 0.1 秒,网格搜索反而更省心,因为不用调采集函数和代理模型。
| 搜索方法 | 迭代次数 | 交叉验证次数 | 相对耗时 | RMSE 差距 |
|---|---|---|---|---|
| 网格搜索 | 400 | 400 | 1.0 | 基准 |
| 随机搜索 | 50 | 50 | 0.125 | 3%~8% |
| 贝叶斯优化 | 30 | 30 | 0.075 | 2%以内 |
4. BO-LSSVM 调参避坑:五个让 RMSE 突然翻车的现场
4.1 现象:优化器选出的 gam 极大,训练集完美但测试集崩盘
原因:目标函数用了训练集 RMSE 而不是交叉验证 RMSE,优化器发现 gam 越大训练误差越小,一路把 gam 推到搜索上界。解决:目标函数必须用交叉验证,且交叉验证的折数不能太少,5 折是底线。如果样本量小于 100,用留一法交叉验证,虽然慢但不会骗你。
4.2 现象:贝叶斯优化跑完,最优参数和初始点几乎一样
原因:搜索范围设得太窄,或者初始采样点太少。bayesopt默认用 4 个初始点,如果搜索范围跨三个数量级,4 个点根本覆盖不过来。解决:把MaxObjectiveEvaluations加到 50 以上,或者手动用'InitialX'指定几个分散的初始点。另外检查optimizableVariable的Transform是否设成了'log',线性尺度下贝叶斯优化会浪费大量采样。
4.3 现象:交叉验证函数报错「矩阵接近奇异」
原因:sig2 太小导致核矩阵条件数爆炸,或者某个折的训练集里出现了重复样本。解决:在目标函数里加try-catch,给失败组合一个大的惩罚值;同时在预处理阶段用unique去重。如果数据本身有很多重复点,考虑在核矩阵上加一个小的对角正则项,比如K + 1e-8 * eye(N)。
4.4 现象:每次运行 BO-LSSVM,最优参数都不一样
原因:数据划分没有固定随机种子,或者贝叶斯优化的初始采样是随机的。解决:在脚本开头加rng(42),并且把bayesopt的'AcquisitionFunctionName'固定为'expected-improvement-plus',这个采集函数在初始点选择上比默认的更稳定。如果换了数据,种子可以换,但同一组数据必须固定。
4.5 现象:测试集 R2 很高,但预测曲线在峰值处明显偏低
原因:RBF 核的平滑特性导致模型对极端值的响应不足,这是 LSSVM 的固有偏差,不是调参能解决的。解决:如果峰值预测很重要,考虑对输出变量做变换,比如取对数或者 Box-Cox 变换,让峰值区域在变换后的尺度上更平缓。另一个办法是换用多项式核,它的外推能力比 RBF 强,但要注意阶数不能太高,否则数值不稳定。
5. 把 BO-LSSVM 用稳的三个进阶习惯
第一个习惯是给贝叶斯优化加一个「预算感知」的停止条件。bayesopt默认跑满MaxObjectiveEvaluations才停,但有时候第 15 次迭代就已经找到最优区域,后面十几次都在浪费。你可以在目标函数里记录历史最优值,如果连续 8 次迭代没有改善,就抛出一个错误让优化提前终止,然后在外面捕获这个错误并取出当前最优。这个技巧在样本量大、单次评估超过 30 秒时特别有用。
% 在目标函数外部维护一个持久变量记录历史最优 persistent bestHist; if isempty(bestHist) bestHist = inf; end % 在目标函数末尾更新 if rmse < bestHist bestHist = rmse; noImproveCount = 0; else noImproveCount = noImproveCount + 1; end if noImproveCount >= 8 error('BO:EarlyStop', '连续 8 次无改善,提前终止'); end第二个习惯是保存每一次迭代的参数和 RMSE,不要只保存最终结果。贝叶斯优化的中间结果本身就是一份有价值的敏感性分析数据,你可以画出 gam 和 sig2 的散点图,看看哪个参数对 RMSE 影响更大。如果 gam 的变化对 RMSE 影响很小,而 sig2 影响很大,说明你的数据对核宽度更敏感,后续可以只优化 sig2,把 gam 固定在一个经验值上,进一步省时间。
第三个习惯是永远保留一个「朴素基线」。BO-LSSVM 再强,如果数据本身线性度很高,多元线性回归可能只差几个百分点,但训练时间只要几毫秒。我一般会在跑 BO-LSSVM 之前先跑一个线性回归和一个固定参数的 LSSVM,如果 BO-LSSVM 的提升不到 5%,我会重新考虑这个问题是否值得用贝叶斯优化。这个习惯帮我省下过很多次无谓的调参时间,也让我在汇报时能说清楚「优化到底带来了多少实际收益」。希望帮到你。
本文还有配套的精品资源,点击获取