
简介基于半监督支持向量机算法实现代码包面向机器学习与数据挖掘方向的开发者、研究人员也适合用于课程设计和论文复现。该方法能同时利用少量有标签样本和大量无标签样本在最大化分类间隔的同时优化决策边界得到全局最优解与仅为标记数据设计的传统支持向量机相比它在标注成本较高的场景中更具优势可有效提升模型泛化能力。资源共包含6个文件其中5个为MATLAB脚本覆盖核函数计算、双月数据集生成、可视化绘图与TSVM优化求解等核心模块另有1个Readme说明文档压缩包整体仅5KB结构精简便于阅读、修改和迁移。目前已有154人学习代码可直接用于复现半监督支持向量机实验也可作为改进算法或工程应用的基础框架。通过阅读源码可以理解拉格朗日乘子求解、无标签样本约束处理以及全局最优解构造等关键步骤测试脚本还能帮助在双月数据集上快速验证效果为后续算法比较、参数调优或实际业务落地提供参考。另一方面代码依赖较少适合在此基础上二次开发。1. 半监督支持向量机的“全局最优解”到底指什么假设你手上只有 20 个有标签样本但硬盘里躺着 5000 条未标注数据。直接丢给标准 SVM分类面会被这 20 个点带偏只做聚类又浪费了来之不易的标签。半监督支持向量机就是为这种场景设计的把未标记样本的空间分布也并进训练目标让分类面同时尊重标注信息和数据本身的几何结构。但“半监督 SVM 能得到全局最优解”这句话经常被误会——直推式 TSVM 因为对未标记样本使用了非凸的对称铰链损失目标函数本身就不是凸的普通梯度优化很容易停在局部极小真正能拍着胸脯说“全局最优”的是另一条以 Laplacian SVMLapSVM为代表的凸模型之路。它用流形正则项把图的平滑性写进目标整个问题仍是一个凸二次规划求出来的 α 在数学意义上是唯一的全局最优。这篇文章把这条凸路径的原理、最小实现和调参验证方式一次讲透适合已经会用 SVM、正准备把半监督思想落到代码里的工程师。2. 从TSVM到LapSVM为什么有的半监督SVM不保证全局最优2.1 S3VM 的目标函数是怎么变“非凸”的标准 SVM 是个凸二次规划目标函数和解都是全局最优的。但半监督 SVM 希望未标记样本尽量远离分类间隔于是要对每个未标记样本 x_j 增加一项惩罚。常见写法是在目标里加一个对称的 hinge 形式loss_unsup(f(x_j)) max(0, 1 - |f(x_j)|)。这个函数当 f 的绝对值小于 1 时为正绝对值大于等于 1 时为 0中间形成两个对称的凹陷整体是非凸的。考虑一个简单的一维情形只有一个未标记点 x0。分类面 w 和偏置 b 可以把间隔放在 0 的左边也可以放在右边。两个位置都能让这个未标记点的损失降到 0但它们对应 w 空间里两个不同的山谷。整个目标函数在这些山谷之间还有局部鞍点和极小值。用经典的凹凸过程CCCP求解每轮迭代只保证目标值单调下降最终收敛到哪个极小完全取决于初值。对同一个数据集换一个初始化就得到另一条分类面这在生产环境里是没法接受的。标准 S3VM 的另一个问题是未标记样本的惩罚权重 C_u 需要人工反复调。调小了未标记信息不起作用调大了分类面会被少数几个间隔内的点强行拉走。即使固定了核参数和权重目标函数仍然是非凸的任何一个凸优化包都不能保证全局最优。许多论文里的“半监督 SVM 好用”往往只在小规模、低密度分隔假设明显的数据上成立换一个分布就可能翻车。2.2 图正则化从非凸回到凸另一条路线完全不用非凸损失转而引入流形假设如果两个样本在图上相邻它们的决策函数值应该接近。这个假设用图拉普拉斯矩阵 L 来表达LapSVM 的目标函数写作min_{f} 1/2 ||f||K^2 C Σ{i1}^l (y_i - f(x_i))^2 λ/2 f^T L f第一项是核希尔伯特空间里的范数正则保证解有界第二项是监督平方损失替代了一般 SVM 的 hinge loss第三项是流形正则项f^T L f 可以被理解为所有相连样本之间决策值差异的加权平方和。这三项相加关于 f 是二次的而且核范数项使得目标强凸。用表示定理把 f 写成核函数的线性组合即 f(x) Σ_i α_i K(x_i, x)代入后得到关于 α 的二次函数。其海森矩阵为 K 2C K J K λ K L K其中 K 是正定核矩阵J 是只在前 l 个对角位置置 1 的标注选择矩阵L 是半正定的归一化拉普拉斯矩阵。三项都是半正定或正定矩阵相加因此整个目标函数是严格凸的。严格凸意味着存在唯一的最优 α无论你用什么线性系统求解器只要数值稳定得到的就是同一个全局最优解。这里我把 C 记作监督损失权重λ 记作流形正则强度。有些资料里写作 γ_A 和 γ_I名字不同物理意义一样。关键点是LapSVM 没有放弃 SVM 的核心思想——它仍然在核特征空间里构造线性分类器只是把“最大间隔”换成了“小平方损失 图平滑”。从优化角度说它比原始 S3VM 更安全更适合作为默认方案。2.3 全局最优不是“万能”模型假设要先于优化需要说清楚的是凸性能保证的是优化层面上的全局最优而不是泛化层面上的“准确率最高”。LapSVM 的全局最优是在流形假设成立的前提下才有意义。如果数据的类别边界和流形结构不一致比如两类样本在局部区域互相穿插强推销的图平滑项反而会把决策边界磨得太圆测试效果还不如标准 SVM。所以选择一个好的模型假设比追求全局最优更靠前。直推式 TSVM 相信低密度分隔LapSVM 相信标签在流形上平滑变化。下面这张表是我在选型时经常对比的基本区别维度直推式TSVM归纳式LapSVM未标记损失形式对称hinge/ramp loss图拉普拉斯二次项目标函数凸性非凸凸全局最优解不保证保证对新样本预测可用训练出的分类面直接可预测主要假设低密度分隔流形平滑求解方法CCCP、SMO变种Cholesky/共轭梯度如果你的业务场景里类别之间边界清晰、空隙大TSVM 可能很准但你要承担多次初始化找最优的代价。如果你只是想快速得到一个稳定、可解释、能上线验证的效果LapSVM 更合适。3. 用Python实现凸半监督SVM的最小可运行版本3.1 核矩阵和归一化拉普拉斯矩阵我不用任何现成的半监督库只用 numpy 和 scipy 把 LapSVM 从零写出来。第一步是高斯核矩阵以及基于 k 近邻图的归一化拉普拉斯矩阵。核矩阵用于把样本映射到高维空间图拉普拉斯用于捕捉流形结构。import numpy as np from scipy.spatial.distance import cdist from scipy.linalg import cho_factor, cho_solve def rbf_kernel(X1, X2, gamma1.0): D2 cdist(X1, X2, sqeuclidean) return np.exp(-gamma * D2) def center_kernel(K): 中心化核矩阵等价于在特征空间里减去均值 n K.shape[0] col_mean K.mean(axis1) # 每个训练点的平均核值 total_mean K.mean() K_c K - col_mean[:, None] - col_mean[None, :] total_mean return K_c, col_mean, total_mean def center_test_kernel(K_new, col_mean, total_mean): 用训练集的中心化参数处理新样本的核向量 return K_new - K_new.mean(axis1)[:, None] - col_mean[None, :] total_mean def graph_laplacian(X, k5, sigma1.0): 构造对称 k 近邻图的归一化拉普拉斯矩阵 n X.shape[0] D2 cdist(X, X, sqeuclidean) adj np.zeros((n, n)) for i in range(n): idx np.argsort(D2[i])[1:k1] # 排除自己 adj[i, idx] 1.0 adj np.maximum(adj, adj.T) # 对称化 W adj * np.exp(-D2 / (2 * sigma**2)) D W.sum(axis1) 1e-12 L np.diag(D) - W D_sqrt_inv np.diag(1.0 / np.sqrt(D)) return D_sqrt_inv L D_sqrt_invrbf_kernel用成对平方距离计算 RBF 核。center_kernel做特征空间均值减除目的是让决策函数里不用显式写偏置项返回的col_mean和total_mean在预测新样本时还要用保证训练和推理的中心化方式一致。graph_laplacian用对称 k 近邻构造邻接矩阵再加高斯权重最后做归一化。归一化的好处是不同连通区域的样本度数差异不会在优化时造成尺度失衡。如果你更习惯看推导可以简单把这里看成核矩阵规定了样本之间的相似度拉普拉斯矩阵规定了决策函数在相邻样本上的平滑性。两个矩阵最终都会进入那个线性系统。3.2 训练和预测的闭式解LapSVM 的目标函数求导后可以得到一个对称正定的线性系统直接用 Cholesky 分解求解。相比求逆它更快也更稳。def lap_svm_train(X_l, y_l, X_u, gamma1.0, C1.0, lam1.0, k5, sigma1.0): X_all np.vstack([X_l, X_u]) n_all X_all.shape[0] l X_l.shape[0] K rbf_kernel(X_all, X_all, gamma) K_c, col_mean, total_mean center_kernel(K) L graph_laplacian(X_all, kk, sigmasigma) # J 选出有标签位置Y 中未标记样本填 0 J np.diag(np.concatenate([np.ones(l), np.zeros(n_all - l)])) Y np.concatenate([y_l, np.zeros(n_all - l)]) # 海森矩阵和梯度项 A K_c 2 * C * (K_c J K_c) lam * (K_c L K_c) b 2 * C * (K_c (J Y)) c, low cho_factor(A, lowerTrue) alpha cho_solve((c, low), b) return alpha, X_all, col_mean, total_mean def lap_svm_predict(X_new, X_all, alpha, gamma, col_mean, total_mean): K_new rbf_kernel(X_new, X_all, gamma) K_new_c center_test_kernel(K_new, col_mean, total_mean) return K_new_c alphaA的构造是核心。第一项K_c来自核范数的梯度第二项2 C K_c J K_c来自监督平方损失第三项λ K_c L K_c来自流形正则。因为K_c、J、L都是对称矩阵K_c J K_c和K_c L K_c也是对称的所以A必然对称正定。b里J Y其实就是前 l 个标签组成的向量因为未标记位置的 Y 本来就是 0。训练时y_l必须为 ±1。预测时lap_svm_predict的返回值符号为正则判为正类符号为负则判为负类。整个流程不迭代不依赖随机数同一份数据得到的结果是严格可复现的。3.3 最小参数速查我给这份代码配了一张参数速查表方便刚开始改代码时快速定位参数默认值作用常见调整范围gamma1.0RBF核带宽控制特征空间尺度用距离中位数估计C1.0有标签样本的损失权重0.1 ~ 10lam1.0流形正则强度0.05 ~ 5k5k近邻图的邻居数3 ~ 10sigma1.0图边权重的高斯带宽0.1 ~ 54. 半监督SVM调参核参数、正则系数与图邻接的取舍4.1 核带宽 gamma用距离中位数做起点RBF 核的输出由 γ||x_i - x_j||^2 决定。γ 太小核矩阵中所有值都趋于 1样本在特征空间里挤成一团区分度差γ 太大核矩阵接近单位阵每个样本只影响自己图正则化等于没做。我一般用所有训练样本之间欧氏距离平方的中位数来设置 gamma写成代码就是def median_gamma(X): D2 cdist(X, X, sqeuclidean) triu_ind np.triu_indices_from(D2, 1) med np.median(D2[triu_ind]) return 1.0 / (2 * med 1e-12)这样得到的 gamma 对特征尺度不敏感作为初值基本安全。如果你的数据已经做了标准化中位数启发式依然有效因为距离已经归一。数据集更大的时候可以对随机抽样的子集计算中位数结果差别不大。4.2 图邻接构造k 与 sigma 的配合图拉普拉斯的质量直接决定流形正则项的有效性。k 是邻居个数它影响图的连通性。k2 时图可能分裂成多个孤立分量半监督信息传不过去k20 时远距离点也会被强行连边流形结构被破坏。对二维数据k 取 5 到 10 一般够了对高维数据可以适度增大因为距离在高维空间更有随机性。sigma 控制边的权重衰减速度。我常用的启发式是先算每个样本的 k 近邻距离取这些距离的均值作为 sigma。这样近邻边的权重分布更合理。如果 sigma 过小边权重几乎都一样图退化成纯 kNN 二值图sigma 过大所有边权重都接近 1距离信息就浪费了。4.3 正则权重 C 与 lam 的博弈C 控制有标签样本的拟合程度。C 小模型允许训练标签犯错分类面更平滑C 大模型会尽量正确分类每个有标签样本。lam 控制流形正则强度lam 越大分类面越贴近图结构。两者不是独立的因为目标函数中监督损失项和图正则项在同一个数量级上。一个实用的起点是 C1.0lam0.1C。如果未标记样本非常多可以把 lam 提高到 0.5C 甚至与 C 同量级如果标签质量不高C 要调小。我经常用下面的网格搜索逻辑快速找参数数据量小的时候可以在几分钟内跑完best_score -1 best_params {} for C in [0.1, 1.0, 10]: for lam in [0.05, 0.1, 0.5, 1.0]: alpha, X_all, col_mean, total_mean lap_svm_train( X_l, y_l, X_u, gammagamma, CC, lamlam, k5, sigma1.0) pred lap_svm_predict(X_val, X_all, alpha, gamma, col_mean, total_mean) score np.mean((pred 0) (y_val 0)) if score best_score: best_score score best_params (C, lam)这段代码在验证集上选择最优的 C 和 lam。注意验证集也必须是有标签的否则无法算准。如果数据量太少可以用嵌套交叉验证或者把这组参数直接放回训练集看分类面是否合理。网格搜索不是银弹但至少比瞎猜强。4.4 未标记样本规模对“全局最优”的实际限制要清楚一点线性系统解的复杂度是 O(n^3)n 是全部样本有标记未标记的数量。几千个样本还能接受上万个样本时每次 Cholesky 分解会吃光内存。这时候常见做法是用 Nyström 方法近似核矩阵或者用随机傅里叶特征把核函数近似成显式的低维特征但这样目标函数不再是严格的原始问题全局最优解也变成了近似解。所以“半监督SVM能得到全局最优”这句话成立的前提是样本规模可以承受稠密核矩阵的计算。我的建议是2000 个样本以内放心用5000 个以上先做聚类抽样或者换用线性核。5. KKT条件校验确认你得到的确实是全局最优解5.1 梯度残差检查既然目标是强凸二次函数全局最优的充要条件就是梯度为零。训练完成之后我习惯立刻计算线性系统的残差确认没有数值问题residual np.linalg.norm(A alpha - b) / (np.linalg.norm(b) 1e-12) print(relative residual:, residual)如果残差小于 1e-8说明 Cholesky 分解对应线性系统解得足够精确α 是全局最优。如果残差很大优先检查K_c是否加了足够大的正则。K_c在样本数多、gamma 大的时候可能接近奇异此时可以给A的旋钮上加一个很小的 jitter比如A 1e-8 * np.eye(n)但不要加太大否则会偏离原问题。5.2 不同初始点的收敛一致性凸问题的另一特征是解与初值无关。你可以拿一个优化器直接对原始目标函数做数值优化分别从随机初始 α 出发看最终目标值是否一致from scipy.optimize import minimize def lap_svm_objective(alpha, K_c, J, Y, L, C, lam): return 0.5 * alpha K_c alpha \ C * (Y - K_c alpha) J (Y - K_c alpha) \ 0.5 * lam * alpha K_c L K_c alpha for seed in range(5): rng np.random.default_rng(seed) x0 rng.normal(0, 1, sizen_all) res minimize(lambda a: lap_svm_objective(a, K_c, J, Y, L, C, lam), x0, methodL-BFGS-B) print(res.fun)几次运行的目标值应该几乎完全相同。如果差别明显要么目标函数写错了要么数值精度不足以在凸问题上收敛。这个方法不需要额外依赖是一个能快速验证“全局最优”的数值证据。5.3 一个实用的验证技巧检查分类面的可解释性除了数值验证我还会直接可视化小数据集上的分类面和未标记样本的位置。LapSVM 的分类面应该穿过图中边权重较低的区域而不是斜着劈开密集簇。如果分类面看起来像被某个局部结构拽弯说明拉普拉斯矩阵构造出了问题最常见的是sigma设置过大或者没有对距离矩阵做缩放。这时候优先检查graph_laplacian的D2是否与gamma使用同一套距离度量。把这一步想清楚整个模型离上线就不远了。本文还有配套的精品资源点击获取