用libsvm对比线性核与高斯核SVM支持向量数
课前我把第六章翻了整整三遍对偶问题推到一半差点扔书。后来一个老师跟我说别光对着公式死磕去把代码跑起来让支持向量自己跳出来给你看。这一跑就通了。今天记录的就是课后题6.2用libsvm在西瓜数据集3.0α上分别训练线性核和高斯核SVM比较支持向量数的差别。这篇笔记适合正在啃西瓜书、对SVM原理半懂不懂的小白读者也适合那些想用Python完整跑一遍SVM、搞清楚支持向量到底是什么的人。我会把工具选择、数据准备、训练过程、结果解读和常见坑一次讲完你可以直接照着动手。1. 这道课后题到底想让你练什么1.1 题目拆开之后的三个考点先还原一下题目本身给定西瓜数据集3.0α使用线性核和高斯核分别训练一个SVM然后比较二者的支持向量数量。表面看是一道工具操作题但它并不只是让你学会敲两行代码。拆开之后我觉得它真正想练的是三件事。理解支持向量的定义知道支持向量在模型里处于什么位置、起到什么作用。理解核函数的选择为什么会带来不同几何效果线性核和高斯核各自的边界形态是什么。明白支持向量数这个指标不能用来直接判断模型好坏它只是一个观察窗口背后还有正则化、泛化这些概念在起作用。很多同学把代码跑完看到两个数字就开始写报告线性核支持向量多高斯核支持向量少。这个结论本身没有错但如果只停在数字层面题目就白做了。你要回答的不是“差多少”而是“为什么差、参数变了之后又会怎样变化”。这篇笔记后面很大篇幅都在解释这件事。1.2 支持向量到底是谁在二维平面上解释SVM最直观。假设现在有两类点一类是好瓜一类是坏瓜。SVM要做的是找一条直线把它们尽量分开。这条直线两侧各有一条虚线边界两条虚线之间的区域叫做间隔。落在虚线边界上的那些样本点决定了间隔能有多宽它们就是支持向量。换一种说法支持向量是距离决策边界最近的那些训练样本也是真正把间隔“撑开”的点。其他样本离边界比较远对间隔宽度没有贡献对模型形状也没有贡献。训练完成之后你打印模型的support_vectors_看到的正是这些真正参与构建边界的点。这个定义在代码里是能直接对上的。看sklearn里训练好的模型clf.support_vectors_返回的就是支持向量样本clf.n_support_返回每一类各有多少个支持向量。有了这个对应关系课本里的“间隔”“支持向量”就不再是抽象符号了。1.3 为什么要拿两种核函数对比核函数是SVM处理非线性数据的核心工具。线性核直接计算原始特征的内积决策边界在原始空间就是一条直线高斯核也叫RBF核它计算的是样本之间的高斯距离相似度。从数学上说高斯核可以把数据映射到高维甚至无穷维空间但实际计算时并不需要真的做这个映射这就是“核技巧”。用两种核跑同一个数据集最直观的差异就是边界形状线性核只能划直线高斯核可以划出各种弯曲的边界。边界形态不同两类样本里哪些点会成为支持向量自然也会不同。线性核遇到线性不可分的数据时只能通过软间隔妥协会有很多样本落在边界附近高斯核能构造更贴合数据形状的边界边界附近需要支撑的点往往更集中所以支持向量数经常比线性核少。这句话的意思是“总体趋势”不是严格定理。具体数字和参数设置强相关第4部分会展开讲。2. 工具准备和数据集处理2.1 到底选libsvm还是scikit-learn题目写的是libsvm但很多小白折腾原生libsvm的Python接口时会被编译问题卡住。这里先给一个明确建议如果机器上已经装好scikit-learn直接用svm.SVC就够了因为它的SVM实现核心就是libsvm两者在模型原理和主要参数上是一致的。如果你想严格按libsvm的原始工具链来我也把用法写出来。原生libsvm的使用套路是先从官网下载源码在本地编译然后调用svmutil。训练代码大致是这样from svmutil import * # y: 1 / -1 标签列表 # x: 每个样本特征用字典表示键从1开始 y [1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1] x [ {1: 0.697, 2: 0.460}, {1: 0.774, 2: 0.376}, # 继续填入其余样本 ] param svm_parameter(-t 0 -c 1) # t0 线性核, C1.0 prob svm_problem(y, x) model svm_train(prob, param) sv_index model.get_sv_indices() print(支持向量个数:, len(sv_index))原生接口有两个需要注意的点x里的字典键必须从1开始和libsvm数据文件的稀疏格式保持一致标签必须是1和-1不能用字符串。如果不习惯折腾源码编译直接用sklearn是最省事的方案。下面我用sklearn为主来演示你跑出来的结果同样能对应libsvm的核心逻辑。2.2 西瓜数据集3.0α的组织方式西瓜数据集3.0α在书里给的是17条样本每条样本保留密度和含糖率两个连续属性类别标记为好瓜和坏瓜。我在代码里把它们组织成两个数组方便复制运行。import numpy as np # 特征列顺序: [密度, 含糖率] X np.array([ [0.697, 0.460], [0.774, 0.376], [0.634, 0.264], [0.608, 0.318], [0.556, 0.215], [0.403, 0.237], [0.481, 0.149], [0.437, 0.211], [0.666, 0.091], [0.243, 0.267], [0.245, 0.057], [0.343, 0.099], [0.639, 0.161], [0.657, 0.198], [0.360, 0.370], [0.593, 0.042], [0.719, 0.103], ]) # 1 表示好瓜, -1 表示坏瓜 y np.array([1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1])这组数据里前8个样本是好瓜后9个是坏瓜特征数值都在0到1之间。量纲一致意味着后面不做标准化也不会出大问题。但你要记住这只是这个数据集的特例换一个数据集之后特征尺度差异会把高斯核的距离计算带偏。2.3 两个最容易踩的格式坑第一个坑是标签格式。libsvm要求二分类标签必须是整数形式的1和-1不能是“是”“否”或任何字符串。sklearn对标签类型相对宽松但为了和libsvm的结果对照我还是建议统一用1/-1。第二个坑是特征编号。用原生libsvm数据文件时一行数据长这样1 1:0.697 2:0.460冒号前面的数字是特征编号从1开始。如果你从0开始编号程序不会报错但训练出来的模型在语义上已经错位了结果没有意义。sklearn里不用关心这个编号但理解这种格式仍然有用因为网上流传的很多SVM格式数据都用这套表示法。3. 实操线性核和高斯核的训练过程3.1 线性核SVM跑一遍进入正式训练。先用线性核C取1.0。C表示对误分类样本的惩罚力度C越大模型越不愿意放过任何误分点。这里先按最常见的默认值来。from sklearn.svm import SVC clf_linear SVC(kernellinear, C1.0) clf_linear.fit(X, y) print(线性核支持向量索引:, clf_linear.support_) print(线性核支持向量个数:, len(clf_linear.support_vectors_)) print(线性核每类支持向量数:, clf_linear.n_support_) print(线性核训练集准确率:, clf_linear.score(X, y))我在这组数据上跑出来的结果是支持向量总共11个每类分别6个和5个训练集准确率约0.8235。也就是说17个样本中有3个样本被线性分类器误分。这个误分结果其实很正常。好瓜和坏瓜在“密度-含糖率”平面上并不是被一条直线完美分开的两类点的分布有交叉。线性核唯一的选择是接受一部分误分点或者把C调大换一个更复杂的硬间隔但那样支持向量数会继续增加。打印clf_linear.support_vectors_这些点会集中在两类样本交界的区域说明决定边界的确实是那些最容易被混淆的样本。3.2 高斯核SVM跑一遍高斯核多了一个重要参数gamma。gamma控制单个样本的影响半径gamma越大影响半径越小边界越弯曲gamma越小影响半径越大边界越平滑。为了和线性核公平对比我先固定C1.0gamma1.0。clf_rbf SVC(kernelrbf, C1.0, gamma1.0) clf_rbf.fit(X, y) print(高斯核支持向量个数:, len(clf_rbf.support_vectors_)) print(高斯核每类支持向量数:, clf_rbf.n_support_) print(高斯核训练集准确率:, clf_rbf.score(X, y))我跑出来的结果是支持向量总数6个每类各3个训练集准确率1.0。也就是说高斯核在训练数据上把17个样本全部分对了而且只用了一半左右的支持向量。看到这个结果第一反应可能是“高斯核更强”。先别急着下结论。训练集100%只能说明这个模型在当前数据上记忆得很好新样本能不能分对还得看泛化能力。尤其当gamma继续调大时模型的边界会变得异常曲折每个点都有可能变成支持向量那时候训练集依然100%但新样本的预测会非常不稳定。3.3 支持向量数对比表整理成一个表格后面解读时直接引用核函数参数设置支持向量总数每类支持向量数训练集准确率线性核C1.0116 / 582.35%RBF高斯核C1.0, gamma1.063 / 3100%这个表只是特定参数下的一次快照。SVM训练本身是确定性的不会因为随机种子改变结果但如果有人用sklearn不写gamma默认是gammascale系统会根据数据方差自动算一个值结果可能和我这里差不少。做实验时把参数显式写出来是一切对比的前提。4. 结果解读支持向量数差异背后的原理4.1 线性核为什么会有误分线性核SVM的本质是在原始特征空间里找一个超平面。二维情况下就是一条直线目标是让两侧的样本尽可能被分开。问题在于西瓜数据集3.0α的两类样本不是线性可分的。好瓜和坏瓜在平面上有交叉比如某几个坏瓜点周围紧挨着好瓜再怎么调整直线的位置和角度都不可能让所有点落在正确的一侧。这就是几何上的“线性不可分”。软间隔机制给了模型一条出路允许一部分样本出现在间隔内部甚至暂时站在误分类的一侧代价由C参数控制。C1.0表示惩罚比较温和线性核接受3个误分点换来一条相对平直的边界。而为了维持这个边界大量处于交界地带的样本都被纳入了支持向量所以数量偏多。支持向量数量偏多在这个场景里意味着模型正在靠多个样本的“妥协”来维持一条稳定的直线。这不是缺陷而是线性模型面对非线性数据时必然会出现的现象。4.2 高斯核的局部性如何改变支持向量再来看高斯核的表达式K(x, z) exp(-gamma * ||x - z||^2)它衡量的是两个样本在特征空间里的接近程度。距离近核函数值接近1距离远核值迅速衰减到0。高斯核在做决策时更多依赖局部样本信息而不是像线性核那样追求全局的一条直线。这种局部性直接影响支持向量的分布。上面实验中RBF核用6个支持向量就把两类分开了。原因是在弯曲边界的每一段只需要少数几个处于类别交界处的样本把边界“顶”起来离边界较远的样本对决策没有贡献。高斯核等于在“该弯曲的地方弯曲”不需要所有交界点都上场。不过要提醒一句支持向量越少不代表一定越好。支持向量数只是模型复杂度的侧面指标。高斯核可以拟合非常复杂的边界复杂度越高过拟合风险越大。如果gamma很大每个样本的影响范围变得很小决策边界就需要更多“钉子”才能撑起来支持向量数会不减反增。4.3 参数C和gamma的影响方向我直接用控制变量法说结论只把C从0.01增加到100支持向量数通常先减少后增加只把gamma从0.01增加到10支持向量数也会经历先减少后增加的过程。为什么不是单调变化C太小的时候模型对误分基本不在乎间隔被放宽大量点都可能落在边界附近C增大到合适区间模型开始认真分类支持向量集中在真正的间隔边界上数量下降C太大会导致硬间隔为了排除所有误分点边界剧烈扭曲支持向量数又涨回来。gamma的影响方向类似。gamma很小时高斯核的影响半径太大边界被“磨平”支持向量不少gamma适中边界贴合类别分布支持向量数降低gamma过大每个点都只被自己的小邻域影响边界变成很多小鼓包几乎所有样本点都可能变成支持向量。面对这个规律与其背结论不如自己动手画一条“gamma-支持向量数”曲线跑10组参数立刻能感受到模型复杂度在变化。这也是比较实验的正确姿势每次只变动一个参数其他全部固定。4.4 小样本上别急着下结论最后必须强调一个被忽略的事实这个数据集只有17条样本。在这么小的数据上无论训练集准确率是82%还是100%都不能证明哪种核函数更好。你拿k近邻、决策树甚至随机猜几个标签都可能得到不错的训练集分数。课后题的真正价值在于帮你理解SVM的运行机制而不是让你用17条数据给算法排名。如果非要评估泛化能力17个样本建议用留一法交叉验证每次都拿16条训练、1条测试最后把所有正确率平均from sklearn.model_selection import cross_val_score, LeaveOneOut clf_linear2 SVC(kernellinear, C1.0) clf_rbf2 SVC(kernelrbf, C1.0, gamma1.0) scores_linear cross_val_score(clf_linear2, X, y, cvLeaveOneOut()) scores_rbf cross_val_score(clf_rbf2, X, y, cvLeaveOneOut()) print(线性核留一法正确率:, scores_linear.mean()) print(高斯核留一法正确率:, scores_rbf.mean())实际跑下来两个核函数在留一法下的差距往往会比训练集上小甚至可能出现线性核反超的情况。把训练集分数和交叉验证分数放在一起对比才是一份完整的实验记录。5. 常见问题与避坑心得5.1 不同工具跑出的结果对不齐有同学会遇到这种情况拿libsvm原生接口跑出来支持向量是8个拿sklearn跑出来却是6个于是怀疑自己代码有错。这不一定是你错了。libsvm默认的gamma是1除以特征数两个特征就是0.5sklearn新版默认是gammascale按数据方差自动计算。两个默认值不同支持向量数自然不同。C的默认值虽然是1.0但不同版本或二次封装里也可能有差异。解决方案是一句老话把参数显式写出来。kernel、C、gamma三个参数全部写清楚然后才谈得上对比。5.2 标签、维度、特征顺序的坑标签格式是最常见的报错点。原生libsvm要求1/-1sklearn虽然允许字符串标签但你从csv读取“是”“否”之后最好还是做一个映射y np.array([1 if label 是 else -1 for label in y_raw])第二个坑是维度。X必须是二维数组哪怕只有一个特征也要reshape成(n, 1)。sklearn遇到一维数组会直接报错提示你shape不对。第三个坑是特征顺序。密度和含糖率不要写反不然训练出来的模型虽然能跑但边界形状、支持向量位置都和教材对不上。建议把数据打印出来人工检查一次再喂给模型。5.3 特征标准化该不该做西瓜数据集3.0α的两个特征都在0到1不做标准化也没有大问题。高斯核依赖样本间的距离如果换成身高、体重这种量纲差异很大的数据距离会被大特征主导小特征的影响力几乎被淹没这时候就必须做标准化。标准化代码很简单from sklearn.preprocessing import StandardScaler X_scaled StandardScaler().fit_transform(X)注意标准化之后再跑线性核决策边界的系数含义会变化但分类效果一般不会差太多。对高斯核来说标准化几乎变成必选项。我个人的习惯是无论什么数据集只要用RBF核先做标准化再说。5.4 支持向量数不等于模型好坏最后一句话送给自己也送给你支持向量数是模型复杂度的观察窗口但不是唯一窗口更不能直接当模型优劣的评判标准。小样本下尤其如此。如果你实在想通过支持向量数去理解模型可以按这个顺序排查先固定C用一组对数网格搜索gamma找到边界形态比较合理的区间再在这个区间里微调C最后用交叉验证确认。不要同时盲目调两个参数不然你会得到一组“看起来很好看但解释不出为什么”的数字。写到这里这道题目的实验链路已经完整了准备17条数据用线性核和高斯核分别训练观察支持向量数差异再把差异归因到核函数的几何特性和参数影响。我实际操作中的一个体会是做完这次实验再回去看第六章的KKT条件和支持向量定义明显轻松很多。代码里的support_vectors_就是课本里反复强调的支持向量decision_function的正负对应样本落在超平面的哪一侧概念一旦和代码对应上就不再抽象了。如果你还想继续往前走建议画一张决策边界图把17个样本点、支持向量、间隔线全部画出来。看到支持向量真的像“钉子”一样钉在边界上推导里的符号会瞬间变成画面。这一步留在你手里自己跑效果比看任何截图都好。