
简介一份基于MATLAB的K近邻算法实现融入K均值聚类预处理策略既可用于分类实验也适合机器学习初学者与算法研究者对照学习。压缩包内共包含一个M文件即K近邻分类主程序整个资源仅六百余字节结构异常精简便于逐行阅读。目前已有两百一十九人学习下载。程序完整演示了数据标准化、距离矩阵计算、K值选择、最近邻筛选及多数表决分类等K近邻算法核心流程同时利用K均值聚类得到的簇中心作为距离度量参考改善高维空间中的计算效果缓解维度灾难的影响。借助这份代码学习者可自行调整K值、更换训练样本观察分类边界与准确率的变化从而直观理解K近邻与K均值两种算法的协作方式并为进一步学习其他监督学习算法建立基础。1. 从KNN.rar说起为什么K-means和KNN总是打包出现KNN.rar这类标题常驻搜索结果是件很自然的事K-means和KNN在课程作业、论文复现和量化分析里总是成对出现前者是无监督聚类后者是监督分类底层又共用同一套距离度量所以打包上传时总被放在同一个压缩包里。但很多人把包下载下来跑main脚本就翻车不是报Undefined function就是结果和说明对不上。下面把这两个算法在MATLAB里的落地路径拆开怎么分辨它们、怎么用fitcknn和kmeans跑通最小代码、K值怎么定、哪些坑最常见。适合正在做机器学习课程作业、用MATLAB做图像分类或股票量化验证的工程师照着代码改数据就能跑出自己的结果。2. KNN和K-means的边界监督分类与无监督聚类不只是差一个标签KNN和K-means经常被放在同一个压缩包里但它们是两个方向完全不同的算法。这一章先用最直观的判据把两者分开再并排跑一遍最小代码后面所有参数讨论都基于这个边界。2.1 一个判断快速分辨训练数据里有没有y拿到KNN.rar这类包第一步不是急着解压跑main而是先分辨脚本到底在解决哪类问题。最直接的判断标准就一句话训练数据里有没有明确的y标签。有标签KNN分类没有标签K-means聚类。这句话听起来很浅但实际判断时经常被绕进去比如有的课程设计把K-means的聚类结果当伪标签再接KNN做分类形成“先聚类后分类”的流程代码里kmeans和fitcknn同时出现初学者很容易把两者的参数搞混。KNN的数学动作可以概括为投票新样本进来到训练集里找出距离最近的K个已知样本让这K个邻居按类别投票票数最多的类别作为预测结果。这里K代表邻居数算法对数据分布不做全局假设决策边界贴得很近。K-means的数学动作是均值迭代先随机放K个簇中心每个样本归到距离最近的中心然后把中心更新成所属样本的均值重复这两步直到中心位置稳定。这里的K代表簇数和KNN里的K完全是两个概念。两者的差异可以收敛成一张小表对比项KNNK-means学习类型监督分类无监督聚类训练数据是否需要y需要不需要K的含义投票邻居数聚类簇数输出类别标签簇编号核心操作找最近邻并投票迭代更新簇均值MATLAB常用函数fitcknn / predictkmeans从落地角度两者还有一个容易被忽略的关系K-means聚类得到的簇结构可以给KNN提供伪标签来训练初始模型这在标注成本高的场景里很常见。这也是为什么资源包里两个算法总被一起打包。2.2 并排跑通最小代码fitcknn和kmeans各管一段先造一份带标签的二维数据两类簇中心分别在左右两侧这样最小代码跑完后可以直接画图验证不用去看一堆数字。% 生成两类高斯分布数据每类50个样本二维特征 rng(1); X1 randn(50, 2) [2 2]; % 第一类簇中心在 (2,2) X2 randn(50, 2) [-2 -2]; % 第二类簇中心在 (-2,-2) X [X1; X2]; % 合并成100x2的样本矩阵 y [ones(50,1); 2*ones(50,1)]; % 标签1类和2类 % 随机打乱后按7:3切分训练集和测试集 idx_all randperm(100); X_tr X(idx_all(1:70), :); % 训练特征 y_tr y(idx_all(1:70)); % 训练标签 X_te X(idx_all(71:end), :); % 测试特征 y_te y(idx_all(71:end)); % 测试标签 % KNN训练分类器并预测 mdl fitcknn(X_tr, y_tr, NumNeighbors, 5, Standardize, true); pred predict(mdl, X_te); fprintf(KNN测试集准确率: %.2f%%\n, sum(pred y_te) / numel(y_te) * 100);逻辑说明rng(1)固定随机种子保证每次生成的数据一致X1和X2合成100x2矩阵标签为1和2randperm打乱顺序后按7:3切分这样训练集和测试集都包含两类样本避免只拿一类做测试。fitcknn返回的是ClassificationKNN对象predict用这个对象对测试样本做预测。Standardize, true会在训练前自动做zscore标准化这是KNN最容易忽略的一步后面避坑章节会重点展开。接着跑K-means注意这里完全不用标签% K-means不知道标签只按特征聚成2类 [idx, C] kmeans(X, 2, Replicates, 10); % 画聚类结果不同颜色代表不同簇黑色x标记簇中心 gscatter(X(:,1), X(:,2), idx); hold on; plot(C(:,1), C(:,2), kx, MarkerSize, 12, LineWidth, 2); title(K-means 聚类结果);逻辑说明kmeans输入样本矩阵X和簇数2返回每个样本的簇编号idx和簇中心矩阵CC的大小是2x2每一行对应一个簇中心坐标。Replicates设为10表示算法用10组不同的随机初始中心各跑一遍取误差平方和最小的结果返回解决随机初始化不稳定的问题。gscatter按idx的类别自动配色聚类完成后如果没有背景真值判断好坏的唯一方式是可视化看簇之间是否有重叠或是否出现空簇。3. 把KNN在MATLAB里跑起来从knnclassify到fitcknn的路解压KNN.rar后最常见的问题是脚本跑不起来。这一章从版本更迭讲起再给一份能直接保存运行的手写KNN最后落到三个必调参数。3.1 旧函数更迭为什么解压后直接跑会报错很多从课程网站流出的KNN MATLAB脚本函数名还是knnclassify、knnreg这一代。R2012a之后MATLAB逐步标记这些接口为将要移除R2014a引入fitcknn后续又给出ClassificationKNN的完整类体系。新版MATLAB里执行旧脚本会看到这样的报错Undefined function knnclassify for input arguments of type double.这个报错在国内论坛被问到的频率极高因为资源包互相转存新用户拿到手根本不知道是版本问题。解决方式是把旧调用转成fitcknn加predict结构。旧代码常见的写法% 旧代码2012年之前可用新版MATLAB已经移除 pred knnclassify(X_test, X_train, y_train, 5);对应的新写法% 新代码fitcknn训练predict预测 mdl fitcknn(X_train, y_train, NumNeighbors, 5); pred predict(mdl, X_test);注意旧版knnclassify的第五个参数可以传距离度量名比如euclidean或cityblock新写法要把距离度量放到fitcknn的Distance参数里邻居数和距离度量分成两个参数指定位置参数改成名称-值配对。改动量不大但没有意识到是版本更迭的话就会在函数名上卡很久。3.2 手写KNN分类器代码拆解与老版本兼容fitcknn在数据量大时性能很好但课程作业经常要求手写实现。手写一遍的价值在于把黑匣子拆开知道距离怎么算、排序怎么取、投票怎么做后面调参和定位问题才有手感。下面这份代码可以直接存成knn_predict.m使用function pred knn_predict(X_train, y_train, X_test, K) % 手写KNN分类器用最近邻投票预测测试集标签 % 输入 % X_train 训练样本每行一个样本大小为 n_train x d % y_train 训练标签大小为 n_train x 1 % X_test 测试样本每行一个样本大小为 n_test x d % K 最近邻个数推荐取奇数防止平票 % 输出 % pred 测试集预测标签大小为 n_test x 1 n_test size(X_test, 1); pred zeros(n_test, 1); for i 1:n_test % 计算测试样本到所有训练样本的欧氏距离 dist sqrt(sum((X_train - X_test(i, :)).^2, 2)); % 按距离升序排序取前K个邻居的标签 [~, idx] sort(dist); neighbors y_train(idx(1:K)); % 少数服从多数mode返回出现次数最多的类别 pred(i) mode(neighbors); end end逻辑说明外层循环逐样本预测内层dist是n_train×1的列向量用隐式扩展计算差值平方和再开方得到欧氏距离。sort返回排序后的下标idx的前K个就是最近邻在训练集里的原始位置。mode对K个邻居的标签投票平票时MATLAB会返回数值最小的类别所以K取奇数更稳。这个实现的复杂度是O(n_test×n_train×d)样本量一旦上万循环会明显变慢生产环境直接改用fitcknn或knnsearch。参数说明K是唯一必调的模型参数默认取5。要扩展距离度量也不难把dist那行替换成曼哈顿距离sum(abs(X_train - X_test(i, :)), 2)或余弦距离即可。测试集进入预测前应该用训练集统计量做标准化这一步在调用函数前完成函数内部不做标准化保持职责单一。注意如果样本量上万手写KNN的循环会明显变慢优先改用fitcknn或knnsearch。一个老版本兼容坑R2016b之前的MATLAB不支持隐式扩展X_train - X_test(i, :)会报矩阵维度不一致需要改成bsxfun(minus, X_train, X_test(i, :))。如果手写代码要在旧环境跑记得加上这个兼容写法。3.3 三个必调参数K值、距离度量与标准化fitcknn的参数面板看着吓人真正要调的只有三项NumNeighbors、Distance、Standardize。其余参数如Prior、Cost在多数分类场景不需要动。K值的选法有点玄学但有一个经验边界K太小决策边界贴着训练样本走测试集泛化差K太大远处样本也参与投票把局部结构磨平了。一般从1到sqrt(n_train)的范围里候选用交叉验证挑稳定值。分类别记得选奇数降低平票概率。距离度量按数据形态选欧氏距离适合连续特征且量纲接近曼哈顿距离对离群点不敏感余弦距离适合文本向量和方向性特征马氏距离能处理特征相关性但样本量小时协方差估计不稳容易翻车。课程作业和量化分析里欧氏距离够用换别的度量前先确认特征分布是否符合这种距离的假设。标准化是KNN里最不能省的一步。想象一个股票特征矩阵价格在几百到几千成交量在几万到几百万欧氏距离会被成交量完全主导KNN等价于只在看一个维度。fitcknn设Standardize, true就直接做zscore。手写版本要自己算% 手写KNN时的标准做法用训练集统计量标准化训练集和测试集 mu mean(X_train, 1); sigma std(X_train, 0, 1); X_tr_norm (X_train - mu) ./ sigma; X_te_norm (X_test - mu) ./ sigma; % 注意统计量来自训练集这里有个细节测试集标准化必须复用训练集的均值和标准差不能拿测试集自己算。否则测试集的信息通过统计量渗进模型交叉验证结果会虚高。这个坑在量化回测里尤其容易踩第五章会专门展开。4. 用K-means做聚类MATLAB的kmeans函数与聚类数K的确定K-means在MATLAB里一行就能出结果但聚类数K和初始化的坑都在这一行后面。这一章从函数参数讲到手写实现最后用肘部法则把K确定下来。4.1 kmeans函数的输入输出与常用参数怎么设MATLAB自带的kmeans是标准实现调用一行就能出结果[idx, C, sumd] kmeans(X, K);X是n×d样本矩阵K是簇数。返回值有三个idx是每个样本的簇编号C是K×d的簇中心矩阵sumd是每个样本到所属簇中心的距离和。sumd累加起来就是SSE后面肘部法则直接用。实际使用中参数远比上面这行复杂我最常用的是这一组[idx, C] kmeans(X, K, ... Distance, sqeuclidean, ... Replicates, 10, ... MaxIter, 500, ... Start, plus);参数说明Distance默认sqeuclidean和欧氏距离等价聚类效果一样但计算更快cityblock适合对离群点鲁棒的需求cosine适合归一化向量。Replicates控制随机初始化重复次数默认1意味着只跑一次很容易落在局部最优一般设10到20每次用不同初始中心跑完整迭代最终返回SSE最小的那次结果。MaxIter默认100数据量大或中心移动慢时加大到500。Start支持plusk-means初始化、sample随机采样或自定义矩阵聚类数多时建议用plus。选型理由默认sqeuclidean加Replicates是最稳定的组合先这样跑通再调别的。特征稀疏时换成cosine距离计算不再被绝对数值主导但中心均值更新的公式也随之变化MATLAB内部会自动处理。4.2 手写K-means初始化与迭代的细节手写K-means是课程作业的保留项目。这份代码覆盖了分配、更新、收敛、空簇四个关键点function [idx, C] custom_kmeans(X, k) % 手写K-means聚类 % X: n x d 样本矩阵 % k: 簇数 % 返回每个样本的簇编号idx和最终簇中心C [n, d] size(X); % 随机选k个样本作为初始中心保证初始中心来自真实数据 init_idx randsample(n, k); C X(init_idx, :); idx zeros(n, 1); max_iter 100; for iter 1:max_iter % 分配每个样本归到距离最近的中心 for i 1:n dist sqrt(sum((X(i, :) - C).^2, 2)); [~, idx(i)] min(dist); end % 更新每类中心取本簇样本均值 newC zeros(k, d); for j 1:k if sum(idx j) 0 newC(j, :) mean(X(idx j, :), 1); else % 空簇处理保留旧中心避免mean出现NaN newC(j, :) C(j, :); end end % 收敛判断中心矩阵变化量足够小就提前退出 if norm(newC - C) 1e-6 break; end C newC; end end逻辑说明初始化用randsample从样本里随机抽k个当初始中心这比随机生成坐标更稳妥中心一定落在数据范围内。分配阶段对每个样本求到所有中心的欧氏距离取最小距离对应的簇号。更新阶段对每个簇求样本均值这里有个容易被忽略的边界如果某簇没有任何样本mean(X(idxj,:),1)会得到NaN后续所有距离计算全部污染。处理方式是保留旧中心本次迭代继续跑通常下一次就有样本归进来。收敛条件norm(newC - C)小于1e-6用的是中心矩阵的Frobenius范数数值上很稳定。手写版没有Replicates机制所以随机初始化差异会直接反映在结果里运行两次聚类图可能完全不同。要复现就在调用前加rng(固定种子)。如果希望更接近kmeans的稳定性可以外层包一层循环跑多次取SSE最小的结果。4.3 肘部法则确定聚类数SSE曲线怎么读真实数据里聚成几类没有标准答案。最常见的方法是肘部法则K增大SSE必然下降但下降速度会在某个点突变这个拐点就是合适的聚类数。% 计算K1到8的SSE画肘部曲线 rng(1); sse zeros(8, 1); for k 1:8 [idx_k, C_k] kmeans(X, k, Replicates, 10, MaxIter, 500); d zeros(size(X, 1), 1); for i 1:size(X, 1) d(i) sum((X(i, :) - C_k(idx_k(i), :)).^2); end sse(k) sum(d); end plot(1:8, sse, -o, LineWidth, 1.5); xlabel(聚类数 K); ylabel(SSE); grid on;逻辑说明kmeans返回的idx_k是每个样本的簇编号C_k是对应簇中心循环里逐个样本计算到所属中心的平方距离累加得到该K下的SSE。把rng(1)放在循环前让整个实验可以复现曲线不会因为随机性抖动。读图方法SSE曲线总体递减前段陡峭说明增加簇数明显降低内部离散度后段平缓说明簇已经足够多再增加收益很小拐点就是K的合适取值。如果曲线从头到尾平滑没有明显拐点说明数据没有天然簇结构这时候硬聚类出来的结果不能当作业务结论只能作为特征工程的一种手段。想更严格可以用evalclusters(X,kmeans,Silhouette)看轮廓系数但肘部法则足够给出一个可信的起点。5. K值、距离、数据泄漏KNN落地避坑与常见问题排查KNN和K-means原理都不复杂麻烦全在细节上。下面五条是我在这类算法落地时反复遇到的坑按现象到原因到解决的方式记录看的时候可以对号入座。5.1 现象K值一改测试准确率剧烈波动现象同一份数据K1时准确率93%K3掉到84%K5又涨回89%改一个数字结果忽上忽下。原因是K太小决策边界贴着训练样本走测试样本附近只要有一个离群点就能改变投票结果K太大远处类别被拉进来局部结构被磨平。另一个常见但隐蔽的原因是没有标准化某个大数值特征主导了邻居选取K值变化只是放大了不平衡。解决先标准化再调K。K用交叉验证在13579里挑不要只看一次测试集的结果。如果K1和K3差距特别大优先检查数据里是否存在离群样本考虑先做离群点剔除或改用曼哈顿距离。5.2 现象标准化前后准确率和聚类结果完全变了现象KNN不加标准化准确率只有60%加完变92%K-means聚类图从横着切变成竖着切。原因是特征量纲不一致价格在几百到几千成交量在几万到几百万欧氏距离实际只被成交量一个维度主导其他特征形同虚设。解决KNN训练前用zscore注意测试集必须复用训练集的均值和标准差不能用测试集自己的统计量。K-means同样要在聚类前标准化这是数据预处理的一部分不算作弊。标准化之后如果结果仍然不合理再看特征本身是否包含噪声或缺失值。5.3 现象报错Undefined function or variable knnclassify现象解压KNN.rar里的脚本运行第一行main就报Undefined function or variable knnclassify。原因是脚本基于2012年以前的MATLAB编写旧接口knnclassify在新版本被移除用fitcknn替代。解决把旧调用改成fitcknn加predict两行结构对应的代码迁移如下% 旧代码新版MATLAB不可用 % pred knnclassify(X_test, X_train, y_train, 5); % 新代码 mdl fitcknn(X_train, y_train, NumNeighbors, 5, Distance, euclidean); pred predict(mdl, X_test);旧代码的参数是位置传参新代码是名称-值配对距离度量要单独指定为Distance。如果脚本里还出现classregtree、svmclassify这类旧函数也是同一批被移除的接口都要换成新体系的对应函数。5.4 现象K-means每次运行结果不一样现象同一份数据、同一个K上午跑和下午跑聚类图不一样SSE也不一样。原因是kmeans默认随机初始化默认Replicates为1每次都从不同起点出发落到不同的局部最优解。解决设Replicates, 10到20让算法多次重启返回SSE最小的那次结果。要完全复现就在调用前加rng(固定种子)。手写K-means没有Replicates机制更要靠rng控制随机性或者在外部包一层多次运行的循环比较SSE后保留最优。5.5 现象交叉验证或量化回测的准确率虚高现象交叉验证95%准确率放到新数据上一测只有60%股票回测看起来每年翻倍实盘亏钱。原因是数据泄漏最常见的是标准化时用了全量数据的均值和标准差相当于测试集的分布信息提前进了训练流程。时间序列场景更严重随机划分交叉验证会让训练集混入未来数据模型无形中看见了答案。解决时间序列只用按时间顺序切分训练集在前测试集在后切分点之后的数据绝不进入训练。标准化统计量只从训练集计算测试集用同一套统计量变换。量化场景更严格的做法是rolling window训练窗口定期滑动每次重新拟合模型。回测和实盘的差距如果仍然很大再去怀疑特征本身是否包含了未来信息比如用了当天的收盘价预测当天的涨跌。6. 把K值用交叉验证定下来股票量化场景可直接套用的脚本K值的选择不能靠感觉交叉验证是最通用的办法。这里给出两段能直接改数据用的脚本分别对应非时序数据和时序数据。6.1 五折交叉验证网格搜索K的通用脚本非时序数据直接用cvpartition做分层五折rng(42); cvp cvpartition(y, KFold, 5); Ks 1:2:15; acc zeros(length(Ks), 1); for i 1:length(Ks) acc_fold zeros(cvp.NumTestSets, 1); for f 1:cvp.NumTestSets trIdx cvp.training(f); teIdx cvp.test(f); mdl fitcknn(X(trIdx,:), y(trIdx), NumNeighbors, Ks(i)); acc_fold(f) sum(predict(mdl, X(teIdx,:)) y(teIdx)) / sum(teIdx); end acc(i) mean(acc_fold); end [~, best_i] max(acc); fprintf(最优K%d, 交叉验证准确率%.2f%%\n, Ks(best_i), acc(best_i)*100);逻辑说明cvpartition按分层抽样划分五折每一折都保证类别比例和全量一致。内层循环在每一折上临时训练一个fitcknn测试集上算准确率五折平均得到该K的得分。Ks用1:2:15的奇数序列覆盖从小到大的合理范围。6.2 时序场景训练集只含历史数据标准化禁用全量统计量股票量化里KNN常见的做法是特征取过去几日的涨幅、成交量变化率标签是次日涨跌。时序数据不能随机打乱切分必须按时间顺序% 按时间顺序切分前80%训练后20%测试 cut round(n * 0.8); X_tr X(1:cut, :); y_tr y(1:cut); X_te X(cut1:end, :); y_te y(cut1:end); % 标准化统计量只从训练集计算 mu mean(X_tr, 1); sigma std(X_tr, 0, 1); X_tr (X_tr - mu) ./ sigma; X_te (X_te - mu) ./ sigma; % 已经手动标准化fitcknn里不要再设Standardize mdl fitcknn(X_tr, y_tr, NumNeighbors, 11, Standardize, false); pred predict(mdl, X_te);参数说明cut取样本量80%X_tr和X_te严格按时间顺序切测试集永远不会出现在训练集之前的位置。标准化统计量mu和sigma只从训练集计算测试集变换只用这两个量。fitcknn的Standardize要设false避免做二次标准化。这段代码跑通后你会看到KNN在量化上的表现大概率只比随机略好。这是正常现象因为KNN捕捉的是“历史相似形态 → 未来同样走向”的局部规律股票市场这类低信噪比场景里它的价值是作为基线模型而不是盈利武器。我自己近年做回测的习惯是先用KNN把特征和标签的代码链跑通验证没有数据泄漏再替换成更复杂的模型。这个流程能帮我在换模型时快速定位是特征的问题还是算法的问题。希望帮到你。本文还有配套的精品资源点击获取