
刚把西瓜书第六章啃到支持向量机这一章时我一度怀疑自己是不是数学没入门。对偶问题、KKT条件、核函数这些概念书上推导一行接一行但心里始终缺一个“哦原来跑起来是这样”的画面。后来强迫自己放下课本先把课后题6.2用LIBSVM跑通一遍再回头看公式很多含糊的地方就突然通了。这篇笔记就是基于我自己实操后的复盘用Python调用LIBSVM实现SVM从数据准备、训练、参数设置到结果解读完整走一遍课后题6.2顺便把踩过的坑和调参经验一起写下来。如果你也正在看西瓜书第六章或者刚接触SVM、想把第一个SVM实验跑起来这篇应该能帮你少走不少弯路。1. 课后题6.2到底让做什么1.1 先别急着写代码把题目要求拆开看课后题6.2的原题核心意思是让你用LIBSVM这个现成工具在西瓜数据集3.0上训练SVM模型然后比较不同核函数下的表现。这里有个容易被忽略的点题目没有让你从零实现SMO算法而是希望你用成熟库去获得感性认识。这样设计是有道理的因为在第六章前面已经详细推导了SVM的数学原理后面还会有手动实现的题所以这一题的意义在于“先把工具用熟把结果看懂”。西瓜数据集3.0是书里自带的一个小型二分类数据集特征主要是“密度”和“含糖率”标签是“好瓜”和“坏瓜”。为什么用这个数据集因为它只有两个特征方便可视化也方便观察支持向量在二维平面上的分布。数据规模不大训练极快很适合新手反复调整参数看效果。1.2 为什么偏偏是LIBSVM而不是自己写个梯度下降LIBSVM是支持向量机领域最常用的开源库之一底层用C实现接口简单输出信息详细。它支持线性核、多项式核、RBF核、sigmoid核也支持分类、回归、交叉验证等功能。对新手来说最直观的价值在于训练完成时它会在终端打印一串诊断信息比如迭代次数、目标函数值、支持向量个数等。这些信息恰好能帮助你对应书上那些抽象概念。熟悉Python的同学可能会问scikit-learn里的SVC不也能用吗确实能而且sklearn的SVC底层也包含LIBSVM的实现接口更Pythonic。但既然题目点名LIBSVM我建议还是老老实实体验一下原始库的输出格式等你理解了那些指标再用sklearn会更有底气。这篇笔记里我会两种方式都提到让你可以按自己的环境选择。1.3 一句话总结课后题6.2的核心目标题目的训练过程可以拆成四步准备符合LIBSVM格式的数据选择合适的核函数和参数训练模型读取并理解训练日志对比不同核函数下的支持向量数量、准确率等指标。做完之后再想想为什么不同核函数的结果会不一样为什么有的模型复杂但准确率并没那么高这些问题想通了才算真正吃透这一题。2. 环境准备与LIBSVM的两种打开方式2.1 小白首选直接用scikit-learn的SVC接口如果你只是想先把实验跑通最省事的方法是用sklearn。安装很简单pip install scikit-learn然后在Python里导入from sklearn import svm clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(X_train, y_train)为什么建议这个作为第一种方式因为sklearn的SVC接口把LIBSVM的细节封装得很干净fit之后直接拿模型去预测代码量最少。但它不会在终端打印LIBSVM那种详细日志所以很多人跑完sklearn之后对“支持向量个数”没有直观概念。2.2 想看到原始训练日志就装原版LIBSVM的Python接口原版LIBSVM在官网下载后里面有一个python目录里面有svmutil.py和svm.py两个关键文件。你只需要把python目录放到你的项目里然后import即可import sys sys.path.append(libsvm/python) # 换成你的实际路径 from svmutil import *如果你不确定路径也可以在命令行里进入libsvm/python目录直接运行Python。这种方式相比sklearn麻烦一点但优点是训练时能看到如下日志optimization finished, #iter 45 nu 0.215038 obj -1.234567, rho -0.345678 nSV 12, nBSV 3 Total nSV 12这一串信息对理解SVM原理非常有帮助后文我会逐行解释。2.3 准备数据把西瓜数据集变成LIBSVM格式LIBSVM的数据格式是这样的label index1:value1 index2:value2 ...每行是一个样本开头是标签后面是特征对。比如西瓜数据集3.0里的一个样本可能是1 1:0.697 2:0.460意思就是标签为1好瓜第1个特征密度为0.697第2个特征含糖率为0.460。如果标签是坏瓜可以写成-1。一般来说我们会把原始CSV转成这种格式。假设你手上有一个watermelon.csv列名是density, sugar, labellabel为1或0那么转换代码可以这样写def csv_to_libsvm(input_file, output_file): with open(input_file, r, encodingutf-8) as fin, \ open(output_file, w, encodingutf-8) as fout: for line in fin: parts line.strip().split(,) label 1 if parts[2] 1 else -1 density float(parts[0]) sugar float(parts[1]) fout.write(f{label} 1:{density} 2:{sugar}\n) csv_to_libsvm(watermelon.csv, watermelon.libsvm)实际操作中你还需要切分训练集和测试集。最简单的方法是把数据按比例随机打乱后分成两部分这个后面在代码里会演示。如果你实在找不到西瓜数据集也可以用LIBSVM自带的heart_scale文件来练手它是一个标准的心跳疾病数据集格式已经符合要求直接调用svm_read_problem(heart_scale)即可。3. 手把手训练SVM线性核、多项式核、RBF核3.1 训练一个模型的核心代码用原版LIBSVM的Python接口核心代码非常短from svmutil import * y, x svm_read_problem(watermelon.libsvm) # 假设前70%做训练集后30%做测试集 train_count int(len(y) * 0.7) y_train, x_train y[:train_count], x[:train_count] y_test, x_test y[train_count:], x[train_count:] param svm_parameter(-t 2 -c 1.0 -g 0.5) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model)这里最关键的是svm_parameter里的参数。-t 2表示用RBF核-c 1.0是惩罚系数C-g 0.5是gamma。如果你想用线性核改成-t 0用多项式核改成-t 1同时还要指定-d次数和-rcoef0。3.2 svm_parameter里的每个参数到底是什么很多小白直接抄参数却完全不懂含义这样一旦结果不对就两眼一抹黑。我把常用参数整理成一张表参数命令行写法含义典型值核函数类型-t0线性、1多项式、2RBF、3sigmoid2惩罚系数C-c对误分类样本的容忍度越大越严格1.0gamma-g核函数的宽度影响模型复杂度1/特征维度多项式核次数-d仅在多项式核时有效3coef0-r多项式/sigmoid核的常数项0是否输出概率-b1表示输出概率0不输出0C是SVM里最核心的惩罚项。你可以把它理解成“班主任查纪律的严格程度”C越大模型就越不愿意放过任何一个训练集里的错误点结果可能是训练集准确率很高但测试集反而变差也就是过拟合。C太小模型又会太“佛系”连明显的分类错误都容忍导致欠拟合。所以C不是越大越好要和数据情况匹配。gamma只对RBF核有决定性影响。它定义了单个样本的影响半径gamma越大决策边界越曲折模型越复杂gamma越小边界越平滑。很多人上来就调C却忽略gamma结果折腾半天还不如先固定C从不同量级的gamma里找找感觉。3.3 三种核函数放到循环里对比既然课后题6.2要求比较不同核函数最直接的办法是写一个循环把线性核、多项式核、RBF核都训练一遍然后打印各自的准确率和支持向量数量kernels [ (Linear, -t 0 -c 1.0), (Poly, -t 1 -d 3 -r 0 -c 1.0), (RBF, -t 2 -c 1.0 -g 0.5), ] for name, opt in kernels: param svm_parameter(opt) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model) nsv model.get_nr_sv() print(f{name}: accuracy{p_acc[0]}%, nSV{nsv})这样跑下来你会发现线性核在这个二维小数据集上可能就已经不错了RBF核则有可能取得更高的测试准确率但支持向量数量也会变多。这正好呼应了书上“核函数的选择会影响模型复杂度”这句话。3.4 为什么训练前一定要做标准化SVM相关算法对特征的尺度非常敏感。因为核函数内部要算样本之间的距离或内积如果一个特征值范围是0到1另一个是100到1000那么大尺度的特征就会主导距离计算另一个特征几乎白费。西瓜数据集的密度和含糖率都在0到1附近量级差异不大所以很多教程里不标准化也能跑出不错结果。但真实项目中特征量级差异往往非常大。标准化最常用的是Z-score归一化让每个特征均值为0、标准差为1。在sklearn里是这样写的from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意fit_transform要用在训练集上transform用在测试集上千万不要对整个数据集直接fit_transform否则会把测试集的信息泄露到训练过程中导致结果虚高。这个错误我在第一次做实验时就犯过后面你会看到后果。4. 结果怎么看支持向量、准确率、决策边界4.1 训练日志里的每一行都是什么意思用原版LIBSVM运行后终端会输出一段很“唬人”的日志。我拿一次实际训练的输出举例optimization finished, #iter 45 nu 0.215038 obj -1.234567, rho -0.345678 nSV 12, nBSV 3 Total nSV 12逐行拆解optimization finished, #iter 45SMO算法迭代了45次后收敛。迭代次数多不一定是坏事但有可能是C或者gamma设置得太极端。nu和C相关的一个参数用于控制支持向量的比例。数值越小说明模型越“简单”。obj目标函数的最优值。对SVM来说这是在约束条件下求解出来的对偶目标函数值可以粗略理解为模型训练结束时的损失。rho决策边界的偏置项也就是超平面方程里的b。nSV 12支持向量的个数。这是理解SVM的关键指标。训练完成后模型只保留这些支持向量用于预测其他样本都可以丢弃。nBSV 3边界支持向量的个数。边界支持向量通常位于间隔内部或者分类错误的一侧它们的数量偏多往往意味着数据噪声较大或者C设置过大。对新手来说最需要盯住的是nSV。如果一个模型训练准确率很高但nSV占了训练集的一大半那这个模型基本处于过拟合状态泛化能力堪忧。相反如果nSV非常少说明模型只靠很少几个关键点就能划分数据通常泛化比较好。4.2 用准确率判断但别只看准确率svm_predict运行结束后会返回三个结果预测标签p_label、准确率p_acc、决策值或概率p_val。其中p_acc本身是一个数组格式如下Accuracy 92.8571% (13/14) (classification) Mean squared error 0.0714 (regression) Squared correlation coefficient 0.9231 (regression)分类任务里我们最关心Accuracy 92.8571%但只看这个数字是有风险的。比如西瓜数据集里好瓜坏瓜比例如果接近1:1准确率还有参考价值一旦出现七八成都是好瓜模型就算把所有样本都预测成好瓜准确率也会高达七八成这显然不是我们想要的结果。所以我会习惯性打印混淆矩阵。用sklearn可以一行搞定from sklearn.metrics import confusion_matrix cm confusion_matrix(y_test, p_label) print(cm)混淆矩阵的四格分别代表真正例、假正例、真负例、假负例。看完之后你才能判断模型是把好瓜全认出来了但误伤了很多坏瓜还是反过来。这个信息在调参时特别有用。4.3 把决策边界画出来一切变得直观西瓜数据集只有两个特征这给了我们一个绝佳的可视化机会。在二维平面上画网格点让训练好的模型逐点预测再按预测类别给网格上色就能看到决策边界长什么样子。代码如下import numpy as np import matplotlib.pyplot as plt def plot_decision_boundary(model, X_train, y_train): h 0.01 x_min, x_max X_train[:, 0].min() - 0.1, X_train[:, 0].max() 0.1 y_min, y_max X_train[:, 1].min() - 0.1, X_train[:, 1].max() 0.1 xx, yy np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # 需要把网格坐标转成LIBSVM格式 grid_samples [] for px, py in zip(xx.ravel(), yy.ravel()): grid_samples.append({1: px, 2: py}) p_label, _, _ svm_predict([0]*len(grid_samples), grid_samples, model) p_label np.array(p_label).reshape(xx.shape) plt.contourf(xx, yy, p_label, alpha0.6, cmapplt.cm.coolwarm) plt.scatter(X_train[:, 0], X_train[:, 1], cy_train, edgecolorsk, cmapplt.cm.coolwarm) plt.xlabel(density) plt.ylabel(sugar) plt.show()注意这里svm_predict需要为每个网格点传一个标签但因为只是预测标签值填什么都不会影响结果。画出来之后你能清晰看到线性核的边界是直线RBF核的边界则是弯曲的支持向量往往就贴在这些边界附近。看到这个图再回头看书上的KKT条件你会对“支持向量是距离超平面最近的点”这句话有完全不同的感受。5. 参数调优的实战心得5.1 核函数选型不要迷信RBF很多教程一上来就说RBF核最万能直接用它。但对小数据集、特征维度不高的时候线性核往往已经够用而且泛化更稳。核函数选型可以简单按下面的经验判断特征维度非常高比如文本分类的几万维优先线性核因为高维空间里线性超平面通常已经足够。特征维度低样本量适中且数据分布有明显非线性时优先RBF。数据中有一定的特征交叉关系多项式核可能更直观但次数太高容易过拟合。需要模型可解释、能直接提取权重时线性核唯一可选。课后题6.2里三种核都试一遍是必要的但试完以后不要只选测试集准确率最高的那个还要结合模型复杂度一起考虑。5.2 C和gamma怎么配合在RBF核下C和gamma是两个需要联合调优的参数。最古典的方法是网格搜索C取2^-5到2^15gamma取2^-15到2^3每一个组合都做交叉验证。LIBSVM自带交叉验证功能参数里加-v 5表示做5折交叉验证param svm_parameter(-t 2 -c 2 -g 0.5 -v 5) svm_train(y_train, x_train, param)注意加了-v之后svm_train返回的是交叉验证准确率而不是一个模型对象。这是新手最容易看代码跑完却拿不到模型来预测的原因。之前我自己做小实验时粗略网格搜索的脚本大概是这样的best_acc 0 best_param None for c in [0.1, 1, 10, 100]: for g in [0.01, 0.1, 1, 10]: param svm_parameter(f-t 2 -c {c} -g {g} -v 5) acc svm_train(y_train, x_train, param) if acc best_acc: best_acc acc best_param f-t 2 -c {c} -g {g}最后用best_param重新训练模型并测试。这样虽然笨但在小数据上很快也能让你直观看到C和gamma对准确率的影响。5.3 标准化和交叉验证一起做才靠谱我自己的经验是先标准化再调参否则网格搜索选出来的参数会被特征的量级干扰。标准化以后RBF核的gamma才有一个稳定的参考范围。如果数据不标准化同样的C和gamma在不同特征量级下表现会千差万别你甚至会得到一个看起来“准确率接近100%”的结果但那只是被大尺度特征带偏的假象。交叉验证也千万别省。尤其在数据量少的时候只用一次随机划分的测试集来判断参数好坏偶然性很大。用5折或10折交叉验证选出参数后再用独立的测试集做最终评估这个流程会让你的结果可信得多。6. 踩坑记录与结果解读要点6.1 数据标准化前后结果差很多我第一次直接在原始西瓜数据上跑RBF核准确率大概在85%左右。后来顺手做了一次标准化同样的参数组合直接跳到95%以上。原因就是密度和含糖率虽然量级都小于1但分布范围不完全一致RBF核本身对特征尺度很敏感。这个坑在课后题里不一定会暴露因为数据集太干净但在任何真实项目里标准化应该成为SVM流程的默认动作。记住对SVM来说标准化不是可选项而是必选项。6.2 类别不平衡会让准确率失真西瓜数据集做成了类别基本均衡的样子但真实数据经常不是。一个很典型的现象就是训练集里好瓜90个、坏瓜10个模型为了降低整体损失会把所有样本都预测成好瓜最终准确率90%听起来很漂亮但坏瓜一个都没找出来。这时候准确率就是骗人的指标。解决办法有三个一是对少数类样本提高惩罚权重在原版LIBSVM里可以用-w1和-w-1分别设置正类和负类的C权重二是对少数类进行过采样三是改用F1分数或召回率来评估不要只看准确率。实践中最立竿见影的是第一种方法。6.3 LIBSVM输出概率的坑如果你想用SVM输出每个样本属于正类的概率而不是只给分类标签需要在训练参数里加上-b 1param svm_parameter(-t 2 -c 1.0 -g 0.5 -b 1) model svm_train(y_train, x_train, param) p_label, p_acc, p_val svm_predict(y_test, x_test, model)加了-b 1之后p_val才是概率值。如果不加p_val返回的是决策值而且很多情况下是空的。这个坑很隐蔽因为你不看文档的话很难意识到p_val为什么空了。还有一点要注意加了-b 1之后训练会变慢因为需要额外做Platt缩放来拟合概率。在样本量大的时候先评估是否需要概率输出再决定加不加-b。6.4 一个容易被忽略的点随机划分会带来方差如果你每次跑代码的结果都不一样先检查一下训练集和测试集划分的时候有没有设置随机种子。在划分前加上random_state或seed可以保证结果可复现。当然即使设置随机种子网格搜索得到的“最优参数”也只代表在这一份数据划分下的最优换一份数据划分结果可能不同。所以我在做对比实验时会用cross_val_score多做几次交叉验证并看平均分数而不是只看一次测试集结果。这组实验做完我最大的感受是SVM的数学推导虽然难啃但它落地后的运行逻辑其实非常简洁。模型训练完只保留支持向量预测过程就是计算新样本和支持向量之间的关系核函数然后加权投票。有了LIBSVM输出日志里那一串数字再回去翻书上的KKT条件和拉格朗日对偶很多符号就不是“空中楼阁”了。如果你刚做完6.2建议再往前走一步尝试用Python手写一个简单的SMO实现不需要高性能只要能在同样的西瓜数据上跑出类似的支持向量这个过程会对SVM的理解有一个质的提升。我已经开始动手了等做完再写一篇笔记分享出来。