简介:一套面向机器学习初学者与金融风控建模人员的实用资源,围绕SVM-KNN组合分类模型,给出MATLAB环境下的核心算法实现与配套实验数据,适用于课程设计、课题研究或小型项目实践。压缩包共4个文件,由MATLAB脚本、CSV数据集、CAJ论文文献与ZIP辅助工具包组成,整体仅1.28MB,便于快速下载和本地复现。目前已有320人学习,内容兼顾代码实践与理论补充:MATLAB脚本覆盖数据预处理、SVM训练、KNN预测校正等关键步骤,可直接运行并调整参数;CSV数据用于验证分类效果;CAJ文献帮助理解SVM与KNN融合在信用风险场景中的建模逻辑;ZIP工具包可扩展支持向量机相关操作。对希望掌握两种经典算法组合应用、提升分类模型泛化能力的读者,这套资源能提供从数据准备到模型评估的完整参考。
1. 当 KNN 遇上 SVM:组合模型并不是简单二选一
做分类任务时,我见过太多人在 KNN 和 SVM 之间反复横跳:KNN 在边界样本上犹豫不决,SVM 在小样本上又容易过拟合。其实这两者并不互斥,SVM 擅长在大间距下找全局最优超平面,KNN 擅长在局部密度上捕捉流形结构,它们的错误模式恰好错开。把这两个模型组合起来,往往比任何一个单模型都稳定——尤其在手写数字识别这类中等规模数据集上,用对组合方式,准确率能再往上顶一个百分点。
这篇笔记针对 svm-knn.rar 这类 SVM+KNN 组合模型的落地场景,讲清楚三种常见的组合方式、关键参数怎么调、以及我踩过的几个坑,最后给出一套能稳定复现的验证流程。适合做中小型分类任务、精度卡在瓶颈上又不想直接上深度学习的团队参考。
2. 组合模型的三种常见结构:串行、并行、预处理嵌入
2.1 为什么 SVM 和 KNN 适合做组合
单模型的短板是组合模型存在的根本理由。SVM 的核心思想是找到一个最大间隔的超平面,把不同类别的样本分开。在高维空间里,它靠核函数把样本映射上去,但对局部区域的几何结构不敏感——离决策边界很远的样本,SVM 给它的置信度往往是趋同的,区分不出“确定属于 A 类”和“勉强属于 A 类”的区别。而 KNN 恰好相反,它不建模全局分布,只看测试样本周围 k 个近邻的类别投票。在数据密度不均匀的区域,KNN 能捕捉到局部结构,但它对特征缩放极度敏感,维度一高,距离度量就开始失真——这个在特征维度超过 50 的时候尤其明显。
如果两个模型犯错的样本高度重叠,组合就没有意义;如果它们的错误模式互补,组合才有价值。手写数字这种数据集上,你可以做一个很简单的实验:分别用 RBF 核 SVM 和 k=5 的 KNN 训练一遍,把预测错误的样本挑出来对比,重合率通常在 30%~50% 之间。也就是说,有超过一半的错判样本,另一个模型其实能判对。这就是组合的逻辑基础。
2.2 串行组合:先粗分后精分
串行结构是指先用一个模型做初筛,把置信度高的样本直接输出,置信度低的样本交给另一个模型做二次判断。常见做法是先跑 KNN,因为 KNN 的决策速度在中小数据集上很快,而且能给出一个非常直观的“近邻一致性”指标——k 个近邻中,最多票类的得票比例。这个比例天然就是置信度。设定一个阈值 t,得票率超过 t 就直接输出,低于 t 的送进 SVM。
实现代码如下:
import numpy as np from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split X, y = load_digits(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) # 第一阶段:KNN 粗分类 knn = KNeighborsClassifier(n_neighbors=5, weights='distance') knn.fit(X_train, y_train) # 第一阶段:SVM 精分类,RBF 核 + C=5.0,gamma 用 scale 自动计算 svm = SVC(C=5.0, gamma='scale', kernel='rbf', probability=False) svm.fit(X_train, y_train) # 串行策略:KNN 近邻一致性置信度低于 0.8 的样本交给 SVM THRESHOLD = 0.8 # 用 kneighbors 拿到测试样本的近邻索引 distances, indices = knn.kneighbors(X_test) # 手工计算每个样本的近邻标签投票比例 knn_conf = [] knn_preds = [] for i in range(X_test.shape[0]): neighbor_labels = y_train[indices[i]] # np.bincount 统计票数,argmax 取最多票类 votes = np.bincount(neighbor_labels, minlength=10) pred = np.argmax(votes) conf = votes[pred] / len(neighbor_labels) knn_conf.append(conf) knn_preds.append(pred) knn_preds = np.array(knn_preds) knn_conf = np.array(knn_conf) # 低置信度样本交给 SVM svm_preds = svm.predict(X_test) final_preds = np.where(knn_conf >= THRESHOLD, knn_preds, svm_preds) acc = np.mean(final_preds == y_test) print(f"组合模型准确率: {acc:.4f}")这段代码里有一个关键参数是weights='distance',意思是近邻投票时按距离倒数加权,距离近的样本说话权更大。在串行结构里这个设置比默认的uniform更合理,因为一旦进入低置信度分支,说明近邻本身就不一致,此时距离更近的邻居的标签更可信。另一个重点是np.bincount的用法,它在统计离散标签票数时比collections.Counter快得多。
THRESHOLD 这个参数不建议拍脑袋定。我一般会在验证集上把阈值从 0.5 到 0.95 扫一遍,画出准确率曲线,找拐点。通常 0.75~0.85 之间会有一个明显的平台区,低于这个区间,大量本应正确的样本被白白踢给 SVM,浪费计算资源;高于这个区间,SVM 又被迫承担太多它并不擅长的“近邻稠密但标签混乱”的区域。
2.3 并行组合:概率融合与投票
并行结构是指 KNN 和 SVM 同时独立预测,再把结果融合。融合方式有两种:硬投票直接数票数,但意义不大,因为两个模型各一票,平局时没法处理;更稳的是软融合,把两个模型的输出统一到概率空间上加权求和。
SVM 的输出要变成概率,得用 Platt Scaling。SVC里设置probability=True时会自动用交叉验证拟合一个逻辑回归做校准。代价是训练时间大约多耗 20%~30%,但这是值得的:
from sklearn.calibration import CalibratedClassifierCV X_train_sub, X_val, y_train_sub, y_val = train_test_split( X_train, y_train, test_size=0.2, random_state=5 ) # 用校准过的 SVM 输出概率 svm_prob = CalibratedClassifierCV( SVC(C=5.0, gamma='scale', kernel='rbf'), method='sigmoid', cv=3 ) svm_prob.fit(X_train_sub, y_train_sub) svm_proba = svm_prob.predict_proba(X_test) # KNN 自带概率就是近邻投票比例 knn_proba = knn.predict_proba(X_test) # 软融合:权重系数 alpha,SVM 权重 0.6,KNN 权重 0.4 ALPHA = 0.6 combined_proba = ALPHA * svm_proba + (1 - ALPHA) * knn_proba final_pred = np.argmax(combined_proba, axis=1) acc_soft = np.mean(final_pred == y_test) print(f"软融合准确率: {acc_soft:.4f}")这里关键参数是融合权重 ALPHA。它不应该是拍脑袋的 0.6,应该用验证集做网格搜索。我见过不少团队在这个权重上翻车——直接取 0.5,结果把性能本来更好的那个模型拖到了另一个模型的水平。一个务实的做法是先在验证集上分别记录两个模型的单模型准确率,再按准确率占比做加权初始化,比如 SVM 单模型 98.2%,KNN 单模型 97.5%,那 ALPHA 初值就设在 0.55~0.6 之间,然后以 0.05 为步长微调。
2.4 预处理嵌入:把 KNN 距离作为 SVM 的新特征
第三种结构思路不太一样,不再做预测层的融合,而是把 KNN 的“距离”作为特征拼进样本里,再训练 SVM。本质上是让 SVM 感知到样本在局部密度中的位置。做法并不复杂:对每个训练样本,提前计算它到所有训练样本的距离,取前 k 个近邻的距离均值或最大值作为一个新特征维度。
from sklearn.neighbors import NearestNeighbors # 对训练集做 PCA 降维到 30 维,缓解原始 64 维特征的距离失真 from sklearn.decomposition import PCA pca = PCA(n_components=30) X_train_pca = pca.fit_transform(X_train) X_test_pca = pca.transform(X_test) # 近邻距离作为新特征 nn = NearestNeighbors(n_neighbors=7, metric='minkowski', p=2) nn.fit(X_train_pca) # 测试样本到训练集前 7 个近邻的距离 dist_to_train, _ = nn.kneighbors(X_test_pca) radius_feature = np.mean(dist_to_train, axis=1).reshape(-1, 1) # 拼接到原始特征 X_test_combined = np.hstack([X_test_pca, radius_feature]) X_train_radius = np.mean(nn.kneighbors(X_train_pca)[0], axis=1).reshape(-1, 1) X_train_combined = np.hstack([X_train_pca, X_train_radius]) svm_enhanced = SVC(C=5.0, gamma='scale', kernel='rbf') svm_enhanced.fit(X_train_combined, y_train) acc_enhanced = svm_enhanced.score(X_test_combined, y_test) print(f"嵌入距离特征后准确率: {acc_enhanced:.4f}")这种方案在特征维度不高的场合有效,但注意一个陷阱:新增的距离特征和原始特征在尺度上相差可能很大。PCA 后的特征值大概在 -10 到 10 之间,而距离均值可能是 20 到 50。如果直接用不标准化,SVM 的 RBF 核计算时,距离特征会主导核函数的值,原始特征的信息就被淹没了。我一般在这个方案里把新特征做一次StandardScaler,或者等价地把近邻距离除以训练集距离均值,让量级对齐。
三种结构各有适用场景:串行适合推理资源紧张、想控制平均耗时的场景,因为大部分样本只在 KNN 里跑了一遍;软融合适合精度优先、能接受 20%~30% 训练时间损耗的场景;预处理嵌入适合特征本身噪声大、希望从几何结构里多捞一点信息的场景。手写数字这种数据集密度较高,我实测下来软融合的上限最高。
3. 把 svm-knn 组合落到数据集上:加载、切分与评估基线
3.1 数据集的选择与加载方式
手写数字分类的最常用数据集是 sklearn 内置的load_digits,每张图 8x8 像素,展开就是 64 维特征向量,样本量 1797,类别 10 个。这个数据规模对于测试 SVM+KNN 组合模型非常合适:不大不小,既能看出两个模型的差异,又不会让训练等太久。也可以用 MNIST 的子集,但从本地.rar包组织代码的角度,直接用sklearn.datasets往往更省事:
from sklearn.datasets import fetch_openml import pandas as pd # load_digits 是 sklearn 自带的,fetch_openml 需要联网且可能较慢 # 推荐本地已有 mnist_784 文件时用 fetch_openml 的 as_frame 参数读取我一般优先推荐load_digits,因为fetch_openml需要联网,且下载回来的数据格式是 DataFrame,很多团队在读取时容易踩到pandas版本兼容的坑。如果本地就有数据文件,也可以用np.loadtxt直接读。重点不在数据源,而在后续的预处理:把像素值从 0-16 或 0-255 缩放到 [-1, 1] 区间,这个看似普通的操作对 SVM 的影响非常大。
load_digits的像素值是 0-16 的整数,而 RBF 核的 SVM 依赖样本间的欧氏距离。像素值不缩放时,gamma 的默认计算方式(1 / (n_features * X.var()))会被像素量级带偏,导致决策边界过于窄或过于宽。我习惯把所有特征按(X - 8) / 8的方式压到 -1 到 1 之间。
3.2 划分策略与基线评估
任何组合模型都必须先建立基线。也就是分别记录单模型 KNN 和单模型 SVM 在同一个测试集上的准确率与运行时间。没有基线,组合模型的提升就没有说服力,更没法定位是组合带来的收益还是数据泄漏造成的假象。
import time from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline X_scaled = (X - 8.0) / 8.0 # 手工缩放更可控 X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.3, random_state=42, stratify=y ) # 基线1: KNN knn_base = KNeighborsClassifier(n_neighbors=5, weights='distance') t0 = time.time() knn_base.fit(X_train, y_train) acc_knn = knn_base.score(X_test, y_test) t_knn = time.time() - t0 # 基线2: SVM RBF svm_base = SVC(C=5.0, gamma='scale', kernel='rbf') t0 = time.time() svm_base.fit(X_train, y_train) acc_svm = svm_base.score(X_test, y_test) t_svm = time.time() - t0 print(f"KNN 基线: {acc_knn:.4f} 训练耗时 {t_knn:.2f}s") print(f"SVM 基线: {acc_svm:.4f} 训练耗时 {t_svm:.2f}s")这个基线代码里用了stratify=y做分层切分,保证训练集和测试集的类别比例基本一致。手写数字数据里各类别样本数略有差异,不做分层切分的话,可能某类数字在测试集偏多,造成准确率的波动被误判为模型差异。
在load_digits上,KNN(k=5, distance 加权) 的基线准确率通常在 98% 左右,RBF SVM(C=5, gamma=scale) 的基线大约在 98.2%~98.5%。两模型误差本身就不多,组合后的提升也许只有 0.3~0.8 个百分点。这个提升幅度看起来小,但在工程上是有意义的——分类系统到了 98% 以上,每提升 0.5 个百分点往往意味着边界样本的错误类型在变,比如把 9 识别成 7 和把 7 识别成 9 的错误分布发生偏移。后续做业务规则时,这 0.5% 决定了很多人工复核的成本。
3.3 特征缩放与降维对组合模型的影响
特征缩放不仅对 SVM 重要,对 KNN 同样关键。KNN 的距离计算里,如果某个维度的数值范围天然比其他维度大,那这个维度就主导了整个距离。在load_digits这类像素数据里,某个像素位置在不同数字上的灰度分布本身就不同,某些位置方差大、某些位置方差小,如果不统一尺度,KNN 实际上是在“用少数几个高方差像素区分所有数字”,丢失了其它像素的判别力。
我常用的组合模型预处理顺序是:先做全局缩放,再按需 PCA。PCA 在组合模型里有两面性:降维能去掉噪声维度、加速 KNN 的距离计算,也会抹掉一部分对区分某些相似数字有用的细节信息。在load_digits上,PCA 保留 30 维通常能保住 98% 以上的方差,对 SVM 影响不大,对 KNN 反而有正面效果,因为它让距离度量更稳定。但如果保留维度低于 20,部分类别的混淆率会明显上升,尤其是 3/8、7/9 这两对经典难分数字。
4. 让 SVM 在组合里发挥全力:核函数与关键参数
4.1 核函数选型对组合模型的影响
组合模型里 SVM 的角色是“兜底”和“纠偏”,它应该比单用时更适合处理硬样本。核函数的选择直接决定 SVM 的决策边界形态。线性核在load_digits这种 64 维数据上效果已经不错,准确率能到 97% 左右,但它在组合结构中往往不够用——因为进入 SVM 分支的样本大概率是非线性的边界区域,线性核兜不住。多项式核的 degree 参数不好调,高了容易数值溢出,低了又退化成线性。最稳妥的始终是 RBF 核,它只有一个 gamma 参数,调参空间集中在两个数上,落地成本最低。
RBF 核的本质是把样本映射到无穷维空间,gamma 控制的是每个训练样本“影响力”的半径。gamma 越大,每个样本只影响它周围的极小区域,决策边界就越曲折,容易过拟合;gamma 越小,样本影响力越远,边界越平滑,欠拟合风险上升。在组合模型里,因为 KNN 已经处理掉了局部细节,SVM 侧的 gamma 反而可以比单用时略微调小一点,更注重全局形状。
4.2 组合场景下 SVM 的 C 值调法
C 是误分类惩罚系数,它控制“允许训练集上出错多少”和“决策边界复杂度”之间的权衡。常规参数网格里,C 取 0.1、1、10、100 四档。但专注 optdigits 手写数字分类中 SVM 核函数与参数的影响研究时,你会发现一个现象:在load_digits上精度先随 C 上升,到 C=5 附近进入平台期,再大会引入过拟合,验证集准确率反而掉头向下。
在组合模型里,C 的选择要考虑到 KNN 已经滤掉了一部分低质量样本。此时 SVM 面对的是相对干净的硬样本集,合适的 C 值往往比单模型的峰值 C 略高一点,因为硬样本的分布可能更复杂,需要更强的拟合能力。我用网格搜索配合验证集微调,通常组合里的 SVM 取 C=5~10 就足够。
4.3 错误诊断:从混淆矩阵看组合该补哪里
组合模型的收益并不是均匀分布的,它只对某几类样本起作用。我习惯每次跑完组合后立刻打印混淆矩阵对比分析:
from sklearn.metrics import confusion_matrix import itertools def report_confusion(y_true, y_pred, title): cm = confusion_matrix(y_true, y_pred) errors = [] for i, j in itertools.product(range(10), repeat=2): if i != j and cm[i][j] > 0: errors.append((i, j, cm[i][j])) errors.sort(key=lambda x: -x[2]) print(f"{title} 混淆最多的三对类别: {errors[:3]}") report_confusion(y_test, knn_preds, "KNN") report_confusion(y_test, svm_preds, "SVM") report_confusion(y_test, final_preds, "组合模型")排序后立刻可以看出组合模型到底在修复哪些错误。最常见的模式是:KNN 把 4 错判成 9,而 SVM 把 4 错判成 2,组合后 4 的判错率下降,因为两个模型的错误不是同一方向。反过来,如果发现某个类别组合后错误增多了,那就是融合权重或阈值设置在了错误的位置,需要回退调整。
5. SVM+KNN 组合练手:三大配置与四个避坑笔记
5.1 组合模型必须关注三大配置
第一个配置是数据划分后要保持类别均衡。如果业务数据本身不平衡,组合模型里的 KNN 会在近邻投票时天然偏向多数类,SVM 的软间隔也会向多数类倾斜。处理办法要么先对少数类做上采样,要么在 KNN 里用weights='distance'并配合验证集人工复核。
第二个配置是交叉验证的折数必须和最终评价方式对齐。SVM 的probability=True本身会在内部做 3 折交叉验证来校准概率。如果在外部又套一层 5 折交叉验证选参数,校准时看到的概率分布和最终推理时的分布会同源但不同构,容易造成阈值的轻微偏差。我一般把外部验证折数定为 4,内部校准折数定为 3,这样层间数据重叠控制在合理范围内。
第三个配置是在融合层做归一化。并行软融合时,SVM 输出的 Platt 概率和 KNN 输出的投票比例虽然在 [0,1] 区间,但分布形状完全不同。KNN 的近邻投票比例往往是 0.6、0.8、1.0 这样离散跃迁的;SVM 的概率则更连续。直接把两者加权相加,KNN 的概率集中在高值区,会在融合里变相获得比预期更高的权重。解决方法是先各自做一次最大最小值归一化,再套加权融合,这样组合才公平。
5.2 避坑笔记一:KNN 近邻数 k 与 distance 权重组合不当
现象:加了weights='distance'后准确率反而低于默认的uniform。原因很简单,k 值取太小(比如 k=3)时,距离加权会让最近邻的决定权被无限放大,等效于模型退化成“单近邻分类”,对噪声点极其敏感,可解释性和鲁棒性都被破坏了。解决方法是 k 值适当加大,比如在load_digits这种 1797 样本规模下取 k=7~11,并且检查近邻列表中前三个邻居的标签是否一致,一致率低于 60% 的样本才适合被降权处理。
5.3 避坑笔记二:SVC probability=True 训练时间失控
现象:设置probability=True后训练时间从几十秒暴涨到几分钟。原因是 Platt Scaling 需要额外的交叉验证来拟合校准曲线。在load_digits这种小数据上勉强能接受,换成 5000 条以上的数据就直接卡到不可用。解决思路是:如果不需要精确概率值,只是想要“哪个类更可能”的相对排名,可以直接用decision_function输出的的距离值做 Min-Max 归一化替代概率。我用这一步替换后,训练时间从 3 分钟降到 15 秒,融合准确率掉了 0.1% 以内。
5.4 避坑笔记三:跳过分层切分导致组合模型虚高
现象:组合模型在测试集上准确率比单模型高出一大截,高得反常。最后定位到问题是随机切分时某类数字恰好全落在训练集或全落在测试集。load_digits里类别样本数最多 183、最少 174,看起来均衡,但 30% 测试集只有约 54 个样本每类,随机切分一棵树就有几个类别的测试样本数被抽偏。解决方法是固定random_state且使用stratify=y,并把最终的模型评价放在至少 3 个不同随机种子上的平均值,而不是一锤定音。
5.5 避坑笔记四:把准确率当唯一指标,漏掉推理时延
现象:组合模型的离线准确率提升了,但线上推理平均耗时从单模型的 56 微秒涨到 1800 微秒,因为每个样本都同时跑了两轮近邻搜索和 SVM 预测。其实组合模型的初衷就是“大部分样本用便宜的分类器覆盖,小部分难样本才用贵分类器”。正确做法是先测一次 KNN 的置信度分布,统计低于阈值的样本占比是多少。在load_digits这类高可分数据集上,占比通常只有 10%~20%,这样推断时延的涨幅是可控的;如果占比超过 50%,说明 KNN 单独效果太差,应该先优化 KNN 本身的配置,而不是强行组合上去。
6. 组合模型的验证技巧:用 Bootstrap 观察方差而不是看一次结果
做组合模型最稳妥的验证方式不是把数据切一次就跑,而是走一遍带自助采样的方差分析。训练集里随机有放回地抽取和原样本相同数量的样本,做一个新的训练集,重复至少 30 轮;每轮里都重新执行一次完整的 KNN+SVM 组合训练与预测脚本,最后看准确率分布的均值和标准差。这个方法不需要额外数据,而且在组合模型这种多阶段流水线上特别有用——每阶段的小方差经过叠加,可能放大为最终结果的不可控波动。
实现时可以只在序列化组合方案上跑快速评估:固定 SVM 参数不变,每轮重新采样后同时重训 KNN 和 SVM。观察得到准确率落在什么范围。如果标准差大于 0.5 个百分点,说明数据划分对最终结果的影响太大了,应该考虑改用更复杂的交叉验证策略来选阈值和权重。如果标准差、均值、混淆矩阵在小幅变动中都能保持稳定,才说明组合策略真的在起正向作用。
我自己用 Bootstrap 验证过阈值和融合权重。初始用 0.8 的 KNN 置信度阈值时,30 轮自助采样得到的标准差是 0.21,说明这个阈值在数据量变化时的行为比较稳定;但当阈值升到 0.9 时标准差跳到 0.38,原因是更多样本被踢给了 SVM,而 SVM 的分支行为对训练集的采样更敏感。后来我把阈值设在 0.82,融合权重固定为 0.6,组合模型性能才在多次采样中稳住了。
回到标题里的 svm-knn.rar 这类资源,落地时也不必追求把串行、并行、特征融合全部实现,选一种最符合自己资源约束的方案,把阈值、C、gamma、k 四个参数在验证集上做一遍系统扫描,记录下准确率-阈值曲线,效果通常就能扎实地落在基线之上。机器学习模型的最终形态未必需要多新奇,稳定可复现的组合策略,结合清晰的错误分析和合理的验证流程,就已经能解决这个场景里大部分精度瓶颈的问题。希望帮到你。
本文还有配套的精品资源,点击获取