ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

K-means辅助KNN的MATLAB实践:原理、调参与避坑指南

K-means辅助KNN的MATLAB实践:原理、调参与避坑指南 简介一份结合K-means与KNNK近邻算法的MATLAB实现面向机器学习初学者与算法实践者可用于分类任务中理解两种经典算法的协同用法。压缩包内共1个文件为MATLAB的m脚本约621B代码虽小但完整演示了利用K-means辅助KNN的流程包括数据预处理、K值选择、距离计算、最近邻确定与多数表决分类。目前已有219人学习下载非常适合对照MATLAB环境边读边调试。通过阅读这份代码可以直观看到K-means簇中心如何作为距离度量参考缓解高维空间中的维度灾难问题同时也能掌握KNN算法从样本距离计算到分类决策的完整实现细节这类结合在数据规模扩大时尤具参考价值。代码结构紧凑便于逐步阅读和实验既能体会K-means预处理对KNN计算复杂度与异常值敏感问题的优化作用也可作为课程设计或算法入门的参考。1. K-means辅助KNN的MATLAB实践这份KNN.rar能帮你省掉多少试错KNN算法在机器学习入门里一直顶着“最简单的那个”的名头但真正动手的人很快会撞上两个现实问题K值怎么选才稳距离怎么算才准。这份KNN.rar里的KNN.m走的是一条挺聪明的路线——先用K-means聚类对数据做一次“摸底”把簇中心当成距离度量的参考点再套上KNN的分类逻辑。适合两类人一类是刚接触机器学习、想在MATLAB里跑通KNN完整流程的学生另一类是拿KNN做股票量化分析之类实测任务、被高维距离失效折磨过的从业者。下面会把原理、复现步骤、参数边界和翻车现场都拆开讲你不仅能跑通代码还能知道哪些参数该调、哪些坑绕不开。2. 原理先行KNN分类与K-means聚类在距离度量上的互补关系2.1 从“近朱者赤”到投票决策KNN的懒惰学习本质KNNK-Nearest Neighbors的核心逻辑一句话就能说清一个样本属于哪个类别看它最近的K个训练样本多数属于哪个类别。这个“近朱者赤近墨者黑”的思路让KNN成了监督学习里最不需要训练的一类算法——它没有显式的模型参数不对数据分布做任何假设预测时才把测试样本和全部训练样本的距离算一遍。这种设计带来的最大优点是简单和透明分类结果可以回溯到具体哪几个邻居投了票不像神经网络那样是个黑匣子。不过“懒惰学习”的代价也很直白。每次分类都要做一次全量距离计算训练样本一多推理时间就线性往上飙。正因如此KNN在实际工程里经常被嫌弃“慢”但它依然是很多场景的基线算法比如手写数字识别、异常检测、以及股票量化分析里的近邻模式匹配。这里的“近邻模式匹配”本质上就是拿历史行情当训练集把当前时刻的特征向量交给KNN找到历史上最像的若干天再按那几天的走势做投票。KNN能在这个领域活下来靠的不是性能而是可解释性。KNN真正需要费心动脑的部分是距离度量。欧氏距离最常用公式是d sqrt(sum((x-y).^2))适用于特征尺度一致、各维度独立的场景曼哈顿距离公式是d sum(abs(x-y))对高维稀疏数据更稳健余弦相似度则只看方向、不看长度常用于文本向量。选错距离度量效果往往比选错K值更致命。这也是K-means辅助KNN这篇代码最值得研究的地方——它其实是在距离度量这一层做文章。分类决策策略也不只有多数表决一种。多数表决是最朴素的做法但在K个邻居中各类别数量接近时平票会频繁出现。工程上常见的变形是加权投票距离越近的邻居权重越大。这个改造只需要把距离的倒数作为投票权重代码上只差两行效果往往比单纯调K更明显。这份KNN.m里作者采用的是多数表决还是加权投票拿到代码后看投票那几行就能确认下面拆解时会按多数表决来演示。2.2 K-means的簇中心如何给KNN当“参照物”K-means是无监督聚类算法目标是把数据集划分成K个簇使同一簇内点到簇中心的距离平方和最小。它的目标函数是J sum_{i1}^{K} sum_{x in C_i} ||x - mu_i||^2其中mu_i是第i个簇的中心。算法通过两步迭代推进先按当前簇中心把每个点分到最近的簇再重新计算每个簇的均值作为新中心直到簇中心的变化小于阈值或达到预设迭代次数。K-means和KNN看起来一个无监督、一个监督但它们的互补点恰恰在距离上。K-means聚类完成后得到的簇中心可以被当作KNN距离度量的“参照物”。具体做法是先用K-means把训练集聚成若干个簇计算测试样本到每个簇中心的距离把最相近的一两个簇里的训练样本挑出来再在这些样本上用KNN做精细分类。这相当于先做一个“粗筛”再做一个“精分”。这种做法的直接收益是能缓解KNN的全量距离计算压力。K-means聚类把数据集压缩成了Kc个代表点测试样本只需要先和Kc个簇中心比较而不是一开始就和全部训练样本比较。在样本量大、维度又高的场景下这个粗筛能把无效距离计算砍掉一大截。间接收益是可解释性增强如果测试样本落在某个簇附近你至少能说出它是“哪一群人”中的异类而不是面对一整张距离表。需要特别说明的是K-means辅助KNN在概念上是一种“半监督式预处理”聚类这个步骤完全不看标签只利用特征的分布结构。所以它不会因为类别标签的噪声而崩塌但反过来如果数据的自然簇结构和类别边界完全不吻合聚类辅助反而会把原本清晰的分界线搞乱。后面避坑章节会专门讲这个坑。2.3 维度灾难下的距离失效K-means预处理的真实作用边界高维空间里有一个反直觉的现象随着维度增加所有样本点之间的距离会逐渐趋于相等最近邻和最远邻的差距变小。这意味着KNN在高维数据上可能根本找不到“真正的邻居”分类就退化成了随机猜测。K-means辅助的价值在于先利用聚类把高维数据压缩成低维的簇结构簇中心之间的距离比原始样本间的距离更有区分度从而减轻维度灾难的影响。但这条优化路线有明确的边界。如果数据本身没有天然的簇结构K-means就是在硬凑K个中心这时候簇中心不但没有提供有效信息反而引入了聚类噪声。另外K-means对初始中心敏感不同随机种子可能收敛到不同的局部最优解。这决定了“K-means辅助KNN”不是万能配方更适合数据分布有明显的聚集特征、或者你只是想快速做一个可解释的基线模型时使用。动手写代码之前可以先做一个快速检查在MATLAB里统计所有训练样本两两距离的标准差。如果标准差相对均值非常小说明距离集中问题存在欧氏距离不可靠。数据量不大的时候用pdist一行就能完成数据量大时抽样计算即可。这个检查能帮你判断K-means预处理在该不该用避免在错误的距离假设上白跑半天实验。注意K-means聚类数Kc和KNN的邻居数K是两个完全不同的参数前者是簇的数量后者是投票邻居数量。在代码里如果共用一个变量名调试起来会非常折磨人。建议代码里一个叫Kc、一个叫K从头到尾不混用。3. 代码落地把KNN.m跑通并读懂每一行3.1 数据准备与归一化这一步决定了欧氏距离是否可信拿到KNN.rar之后第一步不是急着跑KNN.m而是先确认数据集的状态。如果特征是身高、收入、点击次数这种量纲差异巨大的混合变量直接用原始值计算欧氏距离结果是灾难——数值大的特征会主导整个距离数值小的特征几乎不起作用。这是KNN最常见的翻车点没有之一。MATLAB里做标准化有两个方便的选择zscore函数按列做Z-score标准化均值为0、标准差为1适合特征分布接近高斯的情形mapminmax函数把数据线性映射到[-1,1]或[0,1]适合特征有明确上下界的场景。我一般会先看特征的分布形态再选而不是无脑套一个。如果特征里有明显离群点zscore会比mapminmax更稳因为线性映射会被离群点把正常数据压到很小的区间里。% 读入训练数据假设 X_train 是 n×d 矩阵每行一个样本 % y_train 是 n×1 的类别标签向量 % 方式一Z-score 标准化 mu mean(X_train); sigma std(X_train); X_std (X_train - mu) ./ (sigma 1e-8); % 加小常数防止除零 % 方式二mapminmax 映射到 [0,1] X_min min(X_train); X_max max(X_train); X_norm (X_train - X_min) ./ (X_max - X_min 1e-8);这段代码的逻辑很直接先算出训练集的均值和标准差再逐列做减均值除标准差的操作。加1e-8这个常数是防止某一列特征的标准差恰好为0——比如所有样本的某个特征值完全相同直接除零会让结果变成NaN后面KNN一算距离全乱套。同理mapminmax里加1e-8是为了防止某列特征的最大值等于最小值。测试集标准化时有一个关键纪律必须复用训练集的mu和sigma而不是自己单独算。原因是KNN分类时测试样本要和训练样本在同一个特征空间里比较如果两边用各自的均值和标准差尺度就不一致了距离计算等于在做“不同单位之间的加减法”。这份KNN.m如果犯了这个错准确率会莫名奇妙地掉几个百分点具体表现是准确率对数据顺序敏感换个测试顺序结果就变。3.2 KNN.m主循环拆解距离矩阵、排序与多数表决KNN.m最核心的部分是距离计算和投票。在没有工具箱辅助的纯MATLAB实现里通常是用双重循环或者矩阵广播来算欧氏距离矩阵。我拆过很多类似的代码最常见的结构是先计算测试集每个样本到训练集所有样本的距离排序后取前K个再统计这K个邻居里哪个类别最多。function label knn_classify(X_train, y_train, x_test, K) % X_train: n×d 训练特征矩阵 % y_train: n×1 训练标签 % x_test: 1×d 单个测试样本 % K: 邻居数 % 返回: 该测试样本的预测类别 n size(X_train, 1); dists zeros(n, 1); for i 1:n diff X_train(i, :) - x_test; dists(i) sqrt(sum(diff .^ 2)); % 欧氏距离 end [~, idx] sort(dists, ascend); % 按距离升序排序 k_idx idx(1:K); % 取前 K 个邻居索引 k_labels y_train(k_idx); % 取邻居的标签 % 多数表决 [unique_labels, ~, map] unique(k_labels); counts accumarray(map, 1); [~, max_pos] max(counts); label unique_labels(max_pos); end这个函数的逻辑分三步第一步用循环计算测试样本到每个训练样本的欧氏距离diff是当前训练样本和测试样本的差值向量sum(diff.^2)再开方就是距离第二步用sort按距离升序排序取前K个索引第三步用unique和accumarray统计K个标签里各类别的出现次数取频数最高的类别作为预测结果。参数上有几个值得注意的点。sort默认就是升序所以ascend其实可以不写但写出来能让新手一眼看懂意图。K的取值必须小于等于训练样本数n否则idx(1:K)越界报错。accumarray要求map是正整数索引unique返回的第三个输出恰好满足这个条件。如果K个邻居里出现平票max会取第一个最大值的类别这是隐式的随机选择——想避免平票可以强制K取奇数或者改成加权投票。实测中这个函数最大的性能瓶颈是那个for循环。n是几万的时候每个测试样本都要循环几万次MATLAB的循环虽然比早年快了很多但在这种场景下依然吃力。后面第6章会给出一个直接用knnsearch的改造方案那个才是工程版本该用的写法。3.3 与K-means结合聚类结果如何回传到KNN这份KNN.rar的关键卖点是用K-means辅助KNN。常见做法是先用kmeans函数对训练集聚类得到Kc个簇中心和每个簇的样本索引然后分类时先判断测试样本落在哪个簇附近再限定在该簇的训练样本上执行KNN。% 用 K-means 聚类训练集Kc 是聚类数 Kc 5; [cluster_idx, cluster_center] kmeans(X_train, Kc, Replicates, 3); % 分类时先找测试样本最近的簇中心 test_dist_to_center zeros(Kc, 1); for c 1:Kc test_dist_to_center(c) norm(x_test - cluster_center(c, :)); end [~, nearest_cluster] min(test_dist_to_center); % 只在该簇内的训练样本上做 KNN in_cluster (cluster_idx nearest_cluster); X_sub X_train(in_cluster, :); y_sub y_train(in_cluster); label knn_classify(X_sub, y_sub, x_test, K);这段代码里kmeans的第三个参数Replicates很关键。K-means对初始中心敏感Replicates3表示从3个不同的随机初始点出发重复聚类3次最后返回目标函数最小的那次结果。这能在一定程度上缓解聚类不稳定的问题但会增加耗时。数据量大时Replicates可以降到2甚至1再用rng固定种子保底。簇数Kc怎么定没有标准答案。经验上可以先跑一遍kmeans画出簇内距离平方和随Kc变化的曲线找“拐点”。但更实际的做法是直接把它当超参数用交叉验证和K一起搜。要注意的是如果某个簇里的样本数比K还小在这个簇上跑KNN会直接越界所以代码里需要加一个保护判断样本数不足时扩大搜索范围到最近的几个簇。我一般会在KNN函数里加一段保护逻辑判断length(y_sub)是否小于K如果小于就直接把K改成length(y_sub)再分类。虽然这会让“邻居数”名不副实但至少不会让程序崩掉。更稳妥的做法是把最近的几个簇合并成一个候选集再在这个候选集上跑KNN代价是粗筛的加速效果会被稀释。4. 调参与效果验证K值、簇数、距离度量怎么搭配4.1 K值选择交叉验证与“奇数优先”的经验边界K值的选择直接决定KNN分类结果的稳定性。K过小模型对噪声点极其敏感一个异常样本就能带偏投票K过大距离很远的样本也参与投票分类边界被过度平滑小类别模式被淹没。学术界有一种说法是“K取奇数可以避免平票”这只有在二分类的多数表决里才严格成立多分类场景下奇数也照样平票。交叉验证是选K的标准做法。把训练集分成5折轮流拿1折当验证集、剩下4折当训练集统计不同K下的平均准确率选准确率最高的K。这里要注意的是如果训练集本身不大5折的验证结果方差会很大3折或者留一法反而更可靠。我一般会画一条K从1到20的准确率曲线而不是只报一个最高点——曲线的形状能告诉你模型对K的敏感度如果曲线很平那K取哪个都无所谓选个计算量小的就行。场景样本量建议K理由二分类小样本500以下3或5奇数避免平票且不受噪声拖累多分类中等样本5005000515用交叉验证选观察曲线高维稀疏数据任意5以下距离集中邻居再多也无区分度K_candidates 1:2:15; % 只试奇数K兼顾效率和直观性 acc_list zeros(size(K_candidates)); rng(42); % 固定随机种子保证实验可复现 cv_idx crossvalind(Kfold, y_train, 5); for t 1:length(K_candidates) K K_candidates(t); acc 0; for f 1:5 test_mask (cv_idx f); train_mask ~test_mask; % 对验证集每个样本预测 pred arrayfun((i) knn_classify(... X_train(train_mask, :), y_train(train_mask), ... X_train(test_mask, :), K), 1:sum(test_mask)); acc acc mean(pred y_train(test_mask)); end acc_list(t) acc / 5; end [best_acc, best_pos] max(acc_list); best_K K_candidates(best_pos); fprintf(最佳 K%d, 交叉验证准确率%.4f\n, best_K, best_acc);这段代码用crossvalind生成5折索引外层循环遍历候选K内层循环做交叉验证。arrayfun是MATLAB里对数组每个元素执行函数的方式这里对验证集每个样本调用knn_classify。需要留意的是MATLAB的arrayfun在循环次数很大时并不比for循环快纯粹是代码短、可读性好。追求性能的时候改成for循环更明智。参数上rng(42)这一步特别容易被忽略。KNN本身没有随机性但crossvalind的折划分是随机的K-means的初始化也是随机的固定种子才能保证别人复现你的实验时得到一样的结果。这也是写实验代码的底线习惯。另外pred和y_train(test_mask)这两个向量要做成同一形状再比较一个行向量一个列向量时mean会算出一个矩阵而不是一个数这种低级错误能把结果搞成莫名其妙的值。crossvalind需要Statistics and Machine Learning Toolbox如果没有这个工具箱可以用randperm手动划分折。4.2 簇数Kc对KNN性能的影响聚成几类才合理K-means辅助KNN时簇数Kc是一个额外的超参数。Kc太小每个簇里的样本类型混杂粗筛等于没筛Kc太大每个簇的样本太少KNN在簇内找不到足够的邻居甚至越界。它和K值的关系是耦合的Kc决定每个分类任务面对的训练子集规模K决定在这个子集里看多少个邻居。一个比较稳的经验是Kc的大小一般设在类别数的2到5倍之间。如果数据集本身有明确的类别标签比如3类鸢尾花那Kc可以取5到8让每个类别被拆成几个子簇保留类内的结构差异。如果数据是完全无标签的异常检测场景Kc只能靠探查比如画肘部曲线。肘部曲线用MATLAB跑也不复杂循环Kc从1到10每次记录kmeans返回的簇内距离平方和sumd参数然后画出来找拐点。拐点之后增加Kc对距离平方和的改善明显变小那个位置就是聚类数的一个合理估计。这个方法有主观成分但它至少能告诉你Kc取多少是“不过分”的。注意kmeans的输出里sumd默认是每个簇内的距离平方和向量要取总和再画图。Kc和K还有一个联合调优的套路与其分开试不如把两个参数放在同一轮网格搜索里遍历。比如Kc从3取到10K从1取到15跑两层循环记录每一对的交叉验证准确率最后画一个热力图。这样能看到Kc和K是否存在相互依赖。我实测过的一个规律是Kc越大最优K反而可以取得越小因为簇内样本更纯不需要太多邻居来平滑。4.3 三维可视化验证决策边界和簇分布一起看调参的终点不是跑出最高准确率而是确认分类结果是符合直觉的。二维数据可以直接画决策边界但K-means辅助KNN这种两步式方法只画边界不够最好把簇中心和分类区域叠在一起看。MATLAB里用scatter画样本点用contour画边界用hold on叠加。% 假设只有两个特征方便二维可视化 figure; gscatter(X_train(:,1), X_train(:,2), y_train); hold on; % 叠加 K-means 簇中心 plot(cluster_center(:,1), cluster_center(:,2), kx, ... MarkerSize, 12, LineWidth, 2); % 生成网格点逐点预测类别画等高线 [x1g, x2g] meshgrid(linspace(min(X_train(:,1)), max(X_train(:,1)), 200), ... linspace(min(X_train(:,2)), max(X_train(:,2)), 200)); grid_pred zeros(size(x1g)); for i 1:numel(x1g) grid_pred(i) knn_classify(X_train, y_train, [x1g(i), x2g(i)], best_K); end contourf(x1g, x2g, reshape(grid_pred, size(x1g)), LineStyle, none); alpha(0.3);这段代码的思路是在特征空间里生成一个200×200的网格对每个网格点调用knn_classify预测类别再用contourf画类别区域。alpha(0.3)让颜色半透明这样底下的散点和簇中心还能看见。网格分辨率200是个折中——太低边界锯齿明显太高计算量大2000个样本以内不会太慢。如果特征超过两个二维可视化就不够用了。常见替代做法是用pca把数据降维到前两个主成分再画同样的图。但要注意PCA之后的空间和原始特征空间不等价KNN分类应该仍然在原始空间做PCA的图只是给人看的“解释图”不是分类依据。这个区别很多人踩过坑。另一个更贴近K-means的做法是选一个簇中心最多的二维平面投影也就是用簇中心的PCA去做降维这样画出来的簇结构更接近聚类本身的意图。5. 避坑指南KNNK-means组合里的五个典型翻车现场这五个坑是我在复现、移植和修改类似KNN.m代码时真实遇到过的按出现频率排序每一条都值得在跑实验前先自查一遍。5.1 距离度量被特征尺度绑架归一化遗漏导致分类全偏现象跑KNN分类准确率只有一半左右而且无论怎么调K都不见好转看一眼错误样本发现全是数值大的特征在“一票否决”其他特征。原因欧氏距离对特征尺度敏感收入如果是几万、年龄是几十差的量级接近千倍年龄对距离的贡献几乎被淹没。K-means聚类也一样它计算距离时同样被大尺度特征主导。两个算法同时踩进同一个坑症状会被放大。解决标准化必须放在KNN和K-means之前。更严格的做法是用训练集的均值和标准差去标准化测试集不要把测试集单独纳入统计。这个坑我在第一次跑这份KNN.m时也踩过当时偷懒只在训练集上做了标准化测试集忘了套用同样的均值准确率直接掉了8%。要验证是不是尺度问题一个很直接的诊断方法临时把数据标准化后再跑一遍KNN如果准确率明显上升就说明问题出在预处理而不是算法参数上。这份KNN.m如果自带了标准化步骤仔细确认它是对整个输入矩阵做的还是对训练集和测试集分开做的——后者在代码结构上更容易写出数据泄露。5.2 K-means聚类不稳定初始中心选择引发的随机波动现象同一份数据、同一个Kc连续运行两次分类准确率忽高忽低最高和最低能差5个百分点以上。原因K-means的初始中心是随机选择的不同的初始中心可能收敛到不同的局部最优解。如果数据本身簇结构不明显这种波动会更剧烈。解决给kmeans设置Replicates参数多次重复聚类取最优结果同时固定随机种子rng保证实验可复现。如果数据量允许还可以用kmeans初始化方式——MATLAB的kmeans默认就是kmeans但如果你在低版本里用了Start, sample就会退回完全随机选点稳定性更差。判断聚类稳定性有个简单办法把K-means跑10次每次记录聚类目标函数的值如果波动很大说明数据不具备明显的簇结构。这时候强行用K-means辅助KNN反而可能把一个稳定的分类问题搞成随机的结果。5.3 多数表决被类别不平衡扭曲少数类别总是输现象二分类问题中正样本占比只有10%KNN分类结果的准确率看着有九成但混淆矩阵一看正样本几乎全被分错。原因多数表决默认所有类别地位平等。当邻居里负样本远多于正样本时即使测试样本紧挨着几个正样本整体投票结果仍然会被负样本淹没。解决改成加权投票权重取距离的倒数或1/(distanceepsilon)。更系统的方法是调整采样比如对少数类过采样、对多数类欠采样或者给KNN的投票加类别权重。加权投票还有一个额外好处它让K的取值不再那么敏感。因为距离近的邻居权重高、远的邻居权重低即使K选大了几个远处的样本对结果的影响也很小相当于给K值选择增加了一个缓冲。在MATLAB里实现加权投票只需要把knn_classify里的max(counts)改成max(counts .* weight_vec)weight_vec预先按距离算好。5.4 高维稀疏数据上的“距离集中”欧氏距离全面失灵现象数据维度几百上千KNN的准确率逼近随机水平换成K-means辅助也没有明显改善。原因高维空间里任意两点间的欧氏距离都接近相等最近邻失去了区分度。K-means辅助虽然能压缩簇中心但如果原始数据本身是稀疏的簇中心之间同样存在距离集中问题。解决优先换距离度量比如曼哈顿距离或余弦相似度。余弦相似度只关心向量方向不关心长度在文本TF-IDF这种稀疏高维数据上比欧氏距离可靠得多。MATLAB里计算余弦相似度可以自己写一行cos_sim (x * y) / (norm(x) * norm(y))。一个更实用的检查是对所有训练样本两两计算距离统计距离的标准差。如果标准差相对于均值非常小说明距离确实集中了欧氏距离不可靠。这个检查不花多少时间但能在调参之前就定位到问题所在避免在错误的度量上做无意义的网格搜索。5.5 测试集乱入训练流程数据泄露让准确率虚高现象调参时验证集准确率95%一上真实新数据就掉到70%。原因数据预处理、聚类、标准化这些步骤使用了整个数据集的统计信息包括测试集。比如先对全量数据做标准化或聚类再划分训练测试集这属于数据泄露。K-means聚类如果是在划分前的全量数据上跑的测试样本已经被模型“见过”了。解决所有无监督预处理步骤严格限定在训练集上完成测试集只做变换不参与任何统计计算。具体顺序是先划分训练测试集再在训练集上拟合均值和标准差最后用这些参数变换测试集。K-means聚类也只在训练集上跑测试样本分类时才去匹配最近的簇。数据泄露的隐蔽性在于它不报错。准确率虚高的时候很多人会以为是参数调得好直到模型上线才暴露。习惯上我会在代码里把训练、验证、测试三段流程拆成三个函数让数据传递路径一目了然谁用了谁的数据一眼就能查出来。6. 从Demo到工具箱把KNN.m改造成能应对实测数据的版本6.1 用knnsearch替代手写距离循环MATLAB自带近邻检索的提速手写循环在教学上清晰但实测数据一上来就拖后腿。MATLAB自带的knnsearch底层实现了穷举和KD树两种检索策略接口简单一行就能替换掉前面的for循环加sort[idx, dist] knnsearch(X_train, x_test, K, K, Distance, euclidean); k_labels y_train(idx); label mode(k_labels);这里的参数K、Distance和手写版本完全对应。样本数几万、特征维度几十的时候这个替换通常能把单次分类从秒级压到毫秒级。维度超过30左右KD树的检索效率会下降可以显式指定NSMethod, exhaustive让MATLAB改用穷举法再用向量化计算反而更稳。6.2 把K和Kc做成参数对一行命令完成搜索Demo代码里K和Kc是写死的工程版本应该把整个流程包成一个函数输入数据、K、Kc输出模型和验证指标。这样调参时写个双层循环就能自动搜不用每次都改脚本重跑。function acc eval_knn_with_kmeans(X, y, Kc, K, n_fold) rng(42); cv_idx crossvalind(Kfold, y, n_fold); acc 0; for f 1:n_fold tr ~(cv_idx f); te (cv_idx f); % 只在训练集上聚类 [cidx, ccenter] kmeans(X(tr,:), Kc, Replicates, 3); % 测试集样本按最近簇分组后调 knnsearch for i find(te) dc vecnorm(ccenter - X(i,:)); [~, nc] min(dc); sub_tr find(tr cidx nc); pred mode(y_train(knnsearch(X(sub_tr,:), X(i,:), K, K))); acc acc (pred y(i)); end end acc acc / sum(te); end这个函数把交叉验证、K-means粗筛、KNN分类串成了一条流水线Kc和K作为参数暴露在外面。vecnorm一次算出一行向量到所有簇中心的距离比for循环优雅。实际调用时外层再套两个for循环遍历候选Kc和K稳定跑完一轮选准确率最高的组合。6.3 用混淆矩阵验证别只看一个准确率准确率会掩盖类别不平衡的问题。MATLAB的confusionmat函数直接算混淆矩阵一眼能看出哪些类被吞了C confusionmat(y_test, y_pred); disp(C);对角线上的值越大越好非对角线上的值表示“把A类错分成了B类”的数量。我在实测中做过一个很蠢的事只看总体准确率觉得98%已经很好结果混淆矩阵显示某个小类别全部被分到相邻类里去了。所以从那以后我每次跑KNN实验都强制看一遍混淆矩阵和聚类稳定性报告再决定要不要上线这个模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表