简介:最小二乘支持向量机(LSSVM)的MATLAB实现脚本,面向机器学习与数据挖掘方向的算法学习者、科研人员及工程实践者,主要解决非线性回归与分类问题。该脚本以平方误差最小化为核心,完整实现从模型定义、核函数选择(线性核、多项式核、RBF核)、拉格朗日乘子求解到凸二次规划优化的训练流程,同时集成交叉验证与参数调优功能,用户可修改正则化参数C与核参数γ以提升模型泛化能力。压缩包内共1个m脚本文件,整体体积仅2KB,结构精炼,易于阅读和二次开发。目前已有304人学习浏览,适合作为快速上手LSSVM算法的入门参考资料。通过这份代码,使用者能够直观理解LSSVM与传统SVM在决策边界构建思路上的区别,并能够直接在自有数据集上完成数据读取、训练、评估与预测的全过程,减少从零编码的时间成本。
1. 最小二乘支持向量机:把 SVM 的二次规划换成线性方程组
我在一个回归任务上试过 LSSVM(最小二乘支持向量机),当时特征 12 维、样本 3000 条,sklearn 的 SVR 用 RBF 核跑一次交叉验证要三分钟,换成 LSSVM 后同样的预测精度,训练时间降到几十毫秒。这个差距的来源不是优化库多强,而是 LSSVM 把标准支持向量机的二次规划换成了求解一个线性方程组,免掉了 SMO 的反复迭代。它适合中小规模数据集上的快速原型,也适合做回归、分类和时间序列预测,只要你不介意模型完全失去稀疏性、所有训练样本都成了支持向量。这篇文章我就按自己动手实现的路径,把原理、代码、参数和坑一次讲完。
2. LSSVM 的数学原理:从不等式约束到等式约束,再到只解一个线性方程组
2.1 标准 SVM 与 LSSVM 的区别:松弛变量从一次方变成平方
标准 SVM 分类器找这样一条超平面:最小化 1/2||w||^2 + C Σ ξ_i,同时要求每个样本满足 y_i(w·φ(x_i)+b) ≥ 1 - ξ_i,且 ξ_i ≥ 0。这里的 ξ_i 是铰链损失,允许个别点越过边界,但要让越界量最小。因为约束是不等式,最优点通常落在边界上,那些满足等号的样本就是支持向量,其余样本的 ξ_i=0,对模型没有贡献,这就带来了稀疏性。
LSSVM 把问题改成了:最小化 1/2||w||^2 + (γ/2) Σ e_i^2,约束是 y_i(w·φ(x_i)+b) = 1 - e_i。注意两点变化:第一,不等式变成等式,每个样本必须满足一个等式,偏差由 e_i 体现;第二,误差惩罚从 Σ ξ_i 变成 Σ e_i^2,也就是用平方误差代替铰链损失。这样一来,最优解不再需要迭代寻找边界上的支持向量,而是通过求导直接得到一组线性方程。
为什么这个改动影响巨大?因为平方误差是可导的,等式约束求 KKT 条件后,每个变量都能用拉格朗日乘子表示,最后消元得到一个维度为 N+1 的线性系统。求解它只需要直接法或共轭梯度,没有迭代,所以训练快。具体推导如下:定义核矩阵 Ω,其中 Ω_ij = y_i y_j K(x_i,x_j),K 是核函数。引入拉格朗日乘子 α_i 后,KKT 条件给出 w = Σ α_i y_i φ(x_i)、Σ α_i y_i = 0、α_i = γ e_i。把这些代进约束,得到:
[ 0 Y^T ] [b] [0] [ Y Ω + γ^(-1) I ] [α] = [1]
这里的 I 是单位阵,1 是长度为 N 的全 1 向量。这个线性系统是 LSSVM 分类的求解核心。矩阵大小是 (N+1)*(N+1),直接求解复杂度 O(N^3),所以样本量超过一万时内存和耗时都会明显上涨,这就是 LSSVM 的适用边界。如果你正在调参时遇到速度突然变慢,不用怀疑,问题多半出在这个稠密矩阵上。
2.2 回归形式:把分类的标签乘子去掉,右端换成 y
LSSVM 做回归时,约束改成了 y_i = w·φ(x_i) + b + e_i,e_i 是预测残差,目标函数仍然是 1/2||w||^2 + (γ/2) Σ e_i^2。这里的平方残差直接对应目标函数,所以从原理上就是在做一个带正则化的最小二乘回归,只不过把输入特征用核函数映射到了高维空间。求解系统变成:
[ 0 1^T ] [b] [0] [ 1 Ω + γ^(-1) I ] [α] = [y]
注意 Ω_ij = K(x_i,x_j),不再乘 y_i y_j,右端是 y。这个差异看起来很小,但代码写错就是整体偏移。我当年手写实现时,在这里浪费了大半天,最后发现是把分类版本的 y_i y_j 直接搬到了回归里,导致训练集拟合得很好、测试集预测乱成一团。回归形式实际上可以看作核岭回归的一个变体,只是多了一个偏置 b 的显式求解。
回归问题为什么也愿意用 LSSVM?因为标准 SVR 有 ε-insensitive 损失,也需要迭代求解;LSSVM 回归把损失改成最小二乘,一步出结果。代价是对异常值敏感,一个离群点可能把模型拉偏。如果你的数据里噪声分布比较干净,LSSVM 回归非常顺手;如果离群点多,还是老老实实用 SVR 或随机森林。
2.3 核函数选择与模型存储:RBF 为什么是默认选项,稀疏性去哪了
LSSVM 的常见核函数有线性核、多项式核和 RBF 径向基核。RBF 是默认选择,原因是它只有一个宽度参数 sigma,能平滑地逼近非线性函数;sigma 很大时,RBF 核矩阵接近全 1,模型退化为线性,sigma 很小时,核函数只对极近的样本敏感,模型容易过拟合。所以调参时 sigma 的网格通常跨度很大,从 2^-15 到 2^5 都是常见范围。
关键问题是:LSSVM 没有稀疏性。标准 SVM 中大部分 α_i 为 0,只有边界上的支持向量保留;LSSVM 中 α_i = γ e_i,只要 e_i 不为 0,α_i 就不为 0,而平方损失意味着几乎每个样本都有点残差,所以几乎所有 α_i 都非零。这意味着预测时要算 N 个核函数,模型存储规模是 N。这就是 LSSVM 的最大短板,后面我会单独讲剪枝。
选型理由也很明确:如果你想在几百到几千条样本上快速验证非线性模型,LSSVM 比标准 SVM 省事;如果训练数据几万条且预测速度要求高,标准 SVM 的稀疏性更有优势,或者直接改用随机森林、GBDT 这类不依赖核函数的模型。没有哪个模型是银弹,LSSVM 的好用建立在数据量中等、噪声可控的前提下。
3. 用 Python 从零实现 LSSVM 回归:最小可复现代码
我不用任何 LSSVM 专用库,只用 numpy,因为这样可以看清矩阵在哪一步出问题。完整实现分三块:核矩阵、训练、预测。下面代码基于回归版本,分类版本的差异在 3.3 单独说明。
3.1 核矩阵计算:距离平方的展开别写错
import numpy as np def rbf_kernel(X1, X2, sigma): # X1, X2: 形状分别为 (n1, d) 和 (n2, d) # 返回形状 (n1, n2) 的高斯核矩阵 dist2 = (np.sum(X1**2, axis=1)[:, None] + np.sum(X2**2, axis=1)[None, :] - 2 * np.dot(X1, X2.T)) return np.exp(-dist2 / (2 * sigma**2))逻辑说明:高斯核公式是 exp(-||x_i - x_j||^2 / (2σ^2))。我用了等价展开,把距离平方写成平方和与内积的组合,避免双重循环。这里有一个容易写错的地方:dist2 必须用[:, None]和[None, :]把维度广播开,否则两个一维数组相加会得到错误形状。如果你的特征已经标准化,sigma 的含义更直观;如果不标准化,核函数会被数值较大的特征主导。
参数说明:sigma 越小,核函数只对很近的样本敏感,模型越容易过拟合;sigma 越大,核矩阵越接近全 1,模型趋向线性。初始值可以取样本特征标准差的 1 到 2 倍,再画网格精调。这个函数同样用于预测阶段,所以新样本和训练样本都会调用它。
3.2 训练与预测:解线性方程组
def lssvm_fit(X, y, gamma=1.0, sigma=1.0): # X: (N, d), y: (N,),回归标签是浮点数 N = X.shape[0] K = rbf_kernel(X, X, sigma) # 构造分块矩阵 H,对应 [K + I/gamma, 1; 1^T, 0] H = np.ones((N + 1, N + 1)) H[:N, :N] = K + np.eye(N) / gamma H[:N, N] = 1.0 H[N, :N] = 1.0 H[N, N] = 0.0 rhs = np.concatenate([y, [0.0]]) sol = np.linalg.solve(H, rhs) alpha = sol[:N] b = sol[N] return alpha, b def lssvm_predict(X_new, X_train, alpha, b, sigma=1.0): # 预测新样本,返回连续值 K = rbf_kernel(X_new, X_train, sigma) return np.dot(K, alpha) + b逻辑说明:训练函数把线性方程组写成 H·[alpha; b] = rhs。H 的左上角是核矩阵 K 加上 1/gamma 的单位阵,最后一行和一列是 1,右下角是 0。这个结构对应回归系统的 [1^T] 约束。np.linalg.solve 直接解稠密线性方程组。预测时,新样本要计算与所有训练样本的核函数,再和 alpha 做内积,最后加 b。
参数说明:gamma 是误差惩罚权重,对应目标函数里的 γ。gamma 越大,训练集上的残差越小,但可能过拟合。代码里取 1/gamma 加在对角线上,所以 gamma 无穷大时对角线加的量趋近 0,矩阵更容易奇异,这是后一节避坑的重点。b 的存在让预测多了一步,如果你预测结果整体偏移,先检查 b 有没有加回来。
3.3 分类版本与验证:两处小改动即可切换
def lssvm_fit_clf(X, y, gamma=1.0, sigma=1.0): # y 必须是 ±1,二分类专用 N = X.shape[0] K0 = rbf_kernel(X, X, sigma) K = np.outer(y, y) * K0 # 分类核矩阵要乘标签外积 H = np.ones((N + 1, N + 1)) H[:N, :N] = K + np.eye(N) / gamma H[:N, N] = 1.0 H[N, :N] = 1.0 H[N, N] = 0.0 rhs = np.concatenate([np.ones(N), [0.0]]) sol = np.linalg.solve(H, rhs) alpha = sol[:N] b = sol[N] return alpha, b def lssvm_predict_clf(X_new, X_train, y_train, alpha, b, sigma=1.0): # 返回 ±1 K = rbf_kernel(X_new, X_train, sigma) raw = np.dot(K, alpha * y_train) + b return np.sign(raw)逻辑说明:分类版本只改了两处,一是核矩阵变成 y_i y_j K(x_i,x_j),用 np.outer(y, y) 和核矩阵逐元素相乘;二是右端从 y 变成全 1 向量。预测时内积是 alpha * y_train,不是 alpha,因为原推导里 w 的展开带了 y_i。这个细节我不止一次写错:分类的 w 展开式里带 y_i,所以预测时要乘回去。验证时用一个二维随机数据集,比如 sklearn 的 make_moons,常见做法是画决策边界看分类效果,但至少要对比训练集和测试集准确率。注意 LSSVM 只做二分类,多分类需要自己写一对余(one-vs-rest)或一对一,把每个类的二分类模型组合起来,不要直接在一个多分类数据集上调用上面这个函数。
另外,分类概率需要额外映射。LSSVM 的输出没有概率含义,如果你需要概率,用 Platt scaling 或者拟合一个逻辑回归映射原始输出到 [0,1]。这不是必须的,但在金融风控、医学预测这类场景里很常见。很多新手在这里翻车:拿 LSSVM 的 raw score 直接当概率用,结果阈值怎么调都不对。
4. LSSVM 参数避坑指南:gamma、sigma 与稀疏性缺失
4.1 网格搜索:gamma 和 sigma 的粗扫范围与交叉验证
LSSVM 只有两个核心超参数:gamma 和 sigma。gamma 控制正则化强度,sigma 控制核函数宽度。常见做法是用 2 的幂做等比网格搜索,再在最优附近加密。下面给一个可以直接跑的交叉验证框架:
from sklearn.model_selection import KFold from sklearn.preprocessing import StandardScaler def cv_lssvm(X, y, gamma, sigma, k=5): # 手写 LSSVM 的 K 折交叉验证 rng = np.random.default_rng(0) idx = rng.permutation(len(X)) fold_size = len(X) // k scores = [] for f in range(k): val_idx = idx[f*fold_size:(f+1)*fold_size] train_idx = np.setdiff1d(idx, val_idx) alpha, b = lssvm_fit(X[train_idx], y[train_idx], gamma, sigma) pred = lssvm_predict(X[val_idx], X[train_idx], alpha, b, sigma) scores.append(np.mean((pred - y[val_idx])**2)) return np.mean(scores) # 特征先做标准化,再搜参 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) gamma_list = [2**i for i in range(-5, 16, 2)] sigma_list = [2**i for i in range(-15, 6, 2)] best = None for gamma in gamma_list: for sigma in sigma_list: score = cv_lssvm(X_scaled, y, gamma, sigma) if best is None or score < best[0]: best = (score, gamma, sigma) print("best MSE:", best)逻辑说明:网格用的步长是 2 倍,gamma 范围 2^-5 到 2^15,sigma 范围 2^-15 到 2^5。为什么用 2 的幂?因为核函数和正则化对参数是指数敏感,等比网格比等差网格覆盖更合理。这个循环在 10*10=100 组参数上做 5 折交叉验证,对几千条样本来说完全跑得动。如果 gamma 落在最优范围的边界,继续往大搜;如果 sigma 落在最小边界,说明数据本身接近线性,考虑直接用线性核。
参数说明:交叉验证分数用 MSE,分类就用错误率。注意标准化必须在交叉验证内做,否则会有数据泄露,我见过很多人在外面对全量数据标准化后再折,交叉验证分数虚高。正确做法是对每个训练折单独 fit scaler,再 transform 验证折。上面的简化代码用了全量标准化,仅适合快速粗扫,不要把它当成最终评估流程。
4.2 典型坑一:矩阵奇异或求解失败
现象:np.linalg.solve 抛 LinAlgError: Singular matrix,或者结果 alpha 数值巨大,比如 1e12。原因:当 gamma 很大时,对角线上加的 1/gamma 接近 0;如果样本里存在重复或近似重复的点,核矩阵会接近奇异;数据未标准化时,某些特征尺度太大,核函数计算出的距离超过浮点范围。解决:先标准化,再在对角线上额外加一个小正数 jitter,比如 1e-8;如果还不行,调大 sigma 或调小 gamma。另一个可选做法是用 np.linalg.lstsq 代替 solve,至少在奇异时不崩,但解不稳定,根源还是参数或数据问题。
4.3 典型坑二:预测整体偏移,训练集拟合很好,测试集偏差明显
现象:训练集 R^2 很高,但测试集预测值整体比真实值高或低一个常数。原因:b 的计算或预测时漏加了 b。常见错误是在构造 H 时把最后一维设置错,或者回归版本错误使用了分类版本的核矩阵,导致偏置估计错误。解决:用一个 3 样本的小数据手算一遍,打印 H、rhs、sol,人工验算结果。或者拿 sklearn 的 KernelRidge 做对照,如果你实现的预测结果和它差一个常数,优先怀疑 b 的处理。这个坑隐蔽在代码细节里,调参永远救不了逻辑错误。
4.4 典型坑三:分类标签用 0/1 导致约束失效
现象:训练集准确率 90%,但预测时所有样本都被判负类。原因:LSSVM 分类的约束是 y_i(w·φ(x_i)+b) = 1 - e_i,y 必须取 ±1。如果你把标签设成 0/1,那么 y_i=0 的样本约束变成 0 = 1 - e_i,e_i 永远被拉向 1,模型的超平面被严重扭曲。解决:把标签映射成 [-1, 1],训练完用 np.sign 输出,再在评估时映射回原始标签。这个错误非常低级,但很多人都会踩一次,因为我看到不少代码直接把原始 0/1 标签传进去,然后说 LSSVM 效果不行。
4.5 稀疏性缺失:所有样本都是支持向量怎么办
LSSVM 的内存占用是 O(N^2) 的核矩阵,训练 O(N^3),预测 O(N),N 到一万就很吃力。标准 SVM 只有少量支持向量,LSSVM 几乎全有。缓解办法是剪枝(pruning):训练完看 |alpha_i|,把绝对值小的样本删掉,重新训练。因为 alpha_i 和误差 e_i 成正比,删掉误差小的样本对模型影响小。
def prune_lssvm(X, y, gamma, sigma, keep_ratio=0.5): # 剪掉 50% 的“小支持向量” alpha, b = lssvm_fit(X, y, gamma, sigma) n_keep = int(len(alpha) * keep_ratio) keep_idx = np.argsort(np.abs(alpha))[-n_keep:] # 保留绝对值最大的 n_keep 个 return lssvm_fit(X[keep_idx], y[keep_idx], gamma, sigma)逻辑说明:剪枝后必须重新训练,不能直接保留 alpha 子集,因为 b 和 alpha 的联合分布变了。剪枝到 10% 性能还一样,说明冗余样本多;如果明显下降,说明 LSSVM 的密度支持是你需要的,不如趁早换稀疏方法或随机森林。另一个思路是用增量学习,新样本到来时用 Sherman-Morrison 更新逆矩阵,避免重训全部数据,这放到最后一章讲。
5. 进阶:验证、在线更新与过拟合防御
很多人在小数据集上一上来就用高斯核,我也不例外,翻过一次车。那次样本只有 80 条,LSSVM 训练集拟合得近乎完美,测试集一塌糊涂。后来我先跑了一个线性回归,发现线性模型在测试集上反而更好。原因很简单:数据量太小,核模型容量太大,平方损失又让模型对每个点都在意,结果就是过拟合。所以我现在拿到数据第一步是设一个很大的 sigma,比如 10 倍特征标准差,跑一个线性基线。如果线性已经够好,就不上 RBF;如果线性不够好,再考虑高斯核。这个验证方法成本极低,却能在前期排除大量无效调参。
如果你确实需要在线使用 LSSVM,一个实用技巧是保存训练结束后线性系统的逆矩阵。LSSVM 的训练等价于求解 (H + I/gamma) 的解,当新样本到达时,把原矩阵扩展一行一列,用区块求逆公式做增量更新,不必全量重算。这里要注意数值漂移,连续更新几十次后,建议全量重训一次。我一般会把新样本的贡献做一个衰减系数,让太老的数据权重逐步降低,本质上是给在线学习加遗忘机制。这个做法比每次重训快几个数量级,实现时多花半天,但长期运行稳很多。
LSSVM 值得做,但前提是数据量中等、特征经过标准化、你愿意为快速迭代接受非稀疏模型。我的习惯是:样本几百到几千条做回归,优先 LSSVM;样本过万或者在线预测内存紧张,转标准 SVM 稀疏版本或树模型。参数搜索先粗后细,交叉验证别犯数据泄露,预测时记得加 b,分类标签改成 ±1,这几点做到位,LSSVM 基本不会让你翻车。希望帮到你。
本文还有配套的精品资源,点击获取