news 2026/10/1 18:22:30

Matlab中KNN与K-means区别及实战:手写分类器与加速优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Matlab中KNN与K-means区别及实战:手写分类器与加速优化

简介:压缩包内为一份MATLAB实现的KNN算法示例代码,面向机器学习入门者,演示如何借助K均值聚类辅助完成K近邻分类任务,适合需要快速上手KNN或对比两种经典算法的学习者。代码仅一个M文件,压缩包大小六百二十一字节,虽小巧但完整覆盖KNN核心流程,包括数据标准化或归一化预处理、K值选取、距离计算(欧氏距离、曼哈顿距离等)与多数表决等步骤。K均值在此扮演数据预处理角色,其簇中心可作为一种距离参考,帮助缓解高维空间中的维度灾难,这一思路值得深入学习。目前已有二百一十九人浏览学习,可结合簇中心理解KNN中距离度量的改进,也可将MATLAB实现直接移植到自有数据集上进行验证。对于希望理解两种算法内在联系并动手实践的用户,这份代码提供了简洁实用的参照。

1. KNN算法在Matlab里落地:先别急着调fitcknn,分清KNN和K-means再动手

用Matlab做分类,很多人第一反应是调fitcknn,一行代码出结果。但真到了项目里——比如股票量化分析中给行情状态打标签,或者图像特征分类——你很快就会撞上两个问题:预测结果时好时坏,样本量上来之后计算慢得离谱。网上流传的“KNN.rar”这类压缩包,往往把K-means和KNN两种算法打包在一起,名字像亲兄弟,实际一个做无监督聚类,一个做有监督分类,K的含义也完全不同。这篇文章不讨论怎么解压那个rar,而是把K-means与KNN在Matlab里的正确关系讲透,给你一套能复现、能调参、能绕开常见坑的落地代码。

2. 先看透KNN与K-means的边界:监督、懒惰学习与迭代聚类的差异

2.1 KNN的“训练”是存数据:懒惰学习的代价与收益

KNN算法在Matlab里的“训练”阶段几乎什么都不做。fitcknn返回的模型对象,本质上保存了训练样本矩阵和标签,外加你指定的邻居数、距离度量这些超参数。我常说它是懒惰学习,不是贬义,而是说它的计算全部发生在预测那一刻:新样本进来,和所有历史样本算一遍距离,取最近的K个邻居投票。

% 以内置的fisheriris为例,训练一个5近邻KNN模型 mdl = fitcknn(meas, species, 'NumNeighbors', 5); % 模型对象里实际存的就是训练数据和标签 X = mdl.X; Y = mdl.Y; disp(size(X)); disp(unique(Y));

这段代码用来验证KNN的懒惰本质:fitcknn执行后没有迭代,没有梯度,只是把X和Y原样挂到模型对象上。参数'NumNeighbors'就是K值,决定每次投票让多少个邻居参与。

这种设计的收益是简单、稳定,对非线性边界不需要假设函数形式;代价是预测阶段的时间和存储成本都随训练集规模线性上涨。十万条样本、每条样本算一次距离,再排序取前K个,单次预测的延迟就按毫秒到几十毫秒走。所以KNN适合中小样本、特征维度几十到几百的场景,不适合动不动就千万级的在线推理。

2.2 K-means的迭代本质:K是簇数不是邻居数

K-means是另一种“K”,目标是给无标签数据划分成K个簇。它做的事情是反复迭代:随机初始化K个质心,把每个样本分给最近的质心,再根据每个簇里的样本均值更新质心位置,直到质心不再明显移动。

% 用kmeans把样本聚成5簇 % idx: 每个样本所属簇编号, C: 每个簇的质心坐标 [idx, C] = kmeans(meas, 5, 'Replicates', 10, 'MaxIter', 500); % 查看每个簇的样本数量 histcounts(idx);

这里的'Replicates'是我每次必设的参数。kmeans的初始质心是随机的,一次运行可能陷进局部最优,重复10次能保留划分误差最小的一次结果。'MaxIter'控制单次迭代上限,默认值偏小,数据量大时不跑到收敛就提前停了。

K-means聚出的每个簇没有语义标签,只有编号。真正用它做分类辅助时,需要人工给簇定义含义,或者统计每个簇里已知类别的占比,按多数类别给簇贴标签。

维度KNNK-means
学习方式有监督分类无监督聚类
K的含义邻居个数簇个数
训练阶段仅存储样本迭代更新质心
典型用途分类、回归分组、数据压缩、异常发现
Matlab常用函数fitcknn、手写距离计算kmeans

这张表是我给团队讲这两个算法时的固定开场。KNN的K是投票人数,K-means的K是分组数,如果混着叫,调参时必然乱套。

2.3 为什么KNN和K-means总被放在同一个包里:三种常见配合

网上的Matlab算法包里经常同时出现K-means和KNN,不是因为它们像,而是因为它们互补。我实际用过的配合至少三种。

第一种是伪标签。把无标签数据先用kmeans聚类,把簇编号当成类别,喂给KNN做后续样本的归类。这在业务冷启动时很有用,比如刚接入一批不带标注的设备状态数据,先用聚类粗分,再让KNN对新数据做实时归类。

% 先用K-means生成伪标签 [idx, ~] = kmeans(unlabeledData, 5, 'Replicates', 10); % 把伪标签当训练标签,训练KNN mdl = fitcknn(unlabeledData, idx, 'NumNeighbors', 3);

注意这里的伪标签只是工程手段,不代表真实客观类别。用它训练KNN前,最好抽一批样本人工确认簇的语义,否则模型学到的边界可能跟业务真实边界对不上。

第二种是原型选择。用kmeans把大量训练样本压缩成K个质心,再用质心作为训练集喂给KNN,训练集从十万级降到百级,预测速度大幅提升。后面第4章展开讲。

第三种是状态匹配。在股票量化分析里,先用K-means把历史行情切成几个状态簇,比如震荡、上涨、下跌,再用KNN判断当前K线特征接近哪个状态簇。这种做法的好处是状态划分可解释,坏处是K-means的簇边界本身是线性的,状态切换瞬间容易误判,属于典型的“聚类粗筛+近邻细分类”场景。

3. 用Matlab手写一个KNN分类器:距离度量、K值选择与加权投票全放进去

3.1 最小可跑的myknn函数:从pdist2到投票判决

fitcknn虽然方便,但黑匣子味道太重。我建议至少手写一版KNN,理解每个环节之后再回到内置函数也不迟。先上一个最小实现,只用Matlab基础函数。

function pred = myknn(trainData, trainLabel, testData, K) % 手写KNN分类器 % trainData: 训练样本矩阵,每行一个样本 % trainLabel: 训练标签列向量 % testData: 测试样本矩阵,每行一个待预测样本 % K: 邻居个数 [nt, ~] = size(testData); pred = zeros(nt, 1); for i = 1:nt % 计算当前测试样本与所有训练样本的欧氏距离 diff = trainData - testData(i, :); dist = sqrt(sum(diff .^ 2, 2)); % 按距离升序排序,取前K个邻居 [~, idx] = sort(dist); idx = idx(1:K); % 邻居标签投票,mode取出现次数最多的值 labels = trainLabel(idx); pred(i) = mode(labels); end end

这段代码的核心逻辑就三步:广播计算差向量、按行求平方和再开方、排序取前K投票。需要注意训练数据和测试数据必须保证维度一致,否则diff会报维度错误。trainLabel建议用数值型列向量,mode函数对数值最友好,如果标签是字符串或categorical,最好先grp2idx转成数值编码。

K值的设定直接影响过拟合。K=1时模型完全信任距离最近的单个样本,噪声点附近会跟着错;K太大时投票会把远处样本也拉进来,边界变平滑但也变模糊。通常先取奇数,二分类时能避免平票。

3.2 距离度量不该写死:用pdist2支持欧氏、曼哈顿和余弦

上面的程序写死了欧氏距离,真实项目里往往需要换距离。Matlab的pdist2函数能一次算成对的任意距离,同样一个循环体,只需要传一个距离指标参数。

function pred = myknn2(trainData, trainLabel, testData, K, distMetric) % distMetric 支持 'euclidean' / 'cityblock' / 'cosine' [nt, ~] = size(testData); pred = zeros(nt, 1); for i = 1:nt % pdist2返回当前测试样本与所有训练样本的距离向量 dist = pdist2(testData(i, :), trainData, distMetric); [~, idx] = sort(dist); idx = idx(1:K); pred(i) = mode(trainLabel(idx)); end end

pdist2第一个参数是观测点,第二个参数是训练集,返回一行列向量。换成'cityblock'就是曼哈顿距离,对异常值比欧氏稳健一些。换成'cosine'就是余弦相似度,适合文本特征或高维稀疏向量。

距离度量适用场景注意事项
euclidean连续数值特征、量纲已统一特征尺度不一致时会被大数值特征主导
cityblock含噪声或异常值较多的特征对维度之间的补偿关系更稳健
cosine文本、稀疏高维向量只关心方向,不关心长度

选距离不是玄学:先看特征语义,再看数据分布。如果所有特征都是同一物理量,比如温度曲线各点,欧氏距离最自然;如果是用户行为特征,不同维度量纲跨度大,优先标准化后再用欧氏。

3.3 加权投票:距离倒数作为权重,避免被远处样本带偏

多数投票的一个隐患是:5个邻居里,近的3个投类别A,但离得较远的2个投类别B,A赢得很勉强。更合理的做法是让距离近的样本拥有更大话语权。

function pred = myknn_weighted(trainData, trainLabel, testData, K) [nt, ~] = size(testData); pred = zeros(nt, 1); for i = 1:nt dist = pdist2(testData(i, :), trainData, 'euclidean'); [d, idx] = sort(dist); d = d(1:K); idx = idx(1:K); labels = trainLabel(idx); % 距离倒数作为权重,距离为0时加eps防止除零 w = 1 ./ (d + eps); % 按类别累加权重 classes = unique(trainLabel); score = zeros(length(classes), 1); for c = 1:length(classes) score(c) = sum(w(labels == classes(c))); end [~, best] = max(score); pred(i) = classes(best); end end

这里的核心改动是w = 1 ./ (d + eps),距离越近权重越大。eps是一个极小的正数,专门防止某个邻居与测试样本完全重合时距离为0导致除零。

加权投票能明显改善边界样本的预测稳定性,代价是要多维护一个类别列表,并对每个类别累加权重。如果样本量很大、类别又多,这段内层循环会成为瓶颈,可以改用accumarray按类别向量化累加。

4. 用K-means给KNN提速:聚类索引、原型选择与边界代价

4.1 KNN的预测延迟:当训练集到十万级时,全量距离算不动

KNN的惰性学习在训练集规模上来之后非常尴尬。一个十万行、五十维特征的训练集,单次预测要算十万次距离,加起来是五百万次浮点运算,再排序取前K。Matlab里用循环实现,单条预测可能要几十毫秒,放到实时股票量化分析里,每秒处理几十个新样本就会卡顿。

我最早做这类优化时,第一反应是上KDTreeSearcher。Matlab确实提供了这个封装,构建树之后检索复杂度能降不少,但KD树在高维空间里效率衰减很快。另一种思路更直观:先用K-means把训练数据粗分成几十个簇,预测时先定位最近的簇,再只在簇内做KNN。这本质是“粗筛加细分类”,用聚类中心做索引。

4.2 方法一:聚类中心粗筛,簇内再做KNN

下面这个函数把K-means和KNN串起来:训练阶段先对训练集聚类,预测阶段先找最近的簇中心,再只把该簇内的训练样本作为近邻候选池。

function pred = knn_with_km(trainData, trainLabel, testData, K, numCluster) % 先用K-means对训练集聚类 [idx, center] = kmeans(trainData, numCluster, 'Replicates', 5); % 提前算好每个测试样本最近的簇中心编号 [~, testCluster] = pdist2(center, testData, 'euclidean', 'Smallest', 1); [nt, ~] = size(testData); pred = zeros(nt, 1); for i = 1:nt ci = testCluster(i); memberIdx = find(idx == ci); % 簇内样本太少时回退到全量训练集 if length(memberIdx) < K subData = trainData; subLabel = trainLabel; else subData = trainData(memberIdx, :); subLabel = trainLabel(memberIdx, :); end dist = pdist2(testData(i, :), subData, 'euclidean'); [~, sortIdx] = sort(dist); pred(i) = mode(subLabel(sortIdx(1:K))); end end

代码里pdist2(center, testData, 'Smallest', 1)返回的是一个索引向量,每一列对应一个测试样本最近的簇中心编号。这个提前计算可以避免在循环里反复对中心矩阵做距离计算。

numCluster选多少是门学问。经验上是取训练样本数的1%到5%,或者接近sqrt(N)。簇数太少,每个候选池仍然很大,加速不明显;簇数太多,单个簇样本量少,旁边的真实邻居容易被排除掉。

这个方案的代价在簇边界:如果测试样本恰好落在两个簇的交界处,只选最近一个簇就会漏掉真正属于邻近簇的邻居。我的处理方式是扩充候选范围,把“最近1个簇”换成“最近2到3个簇”。

% 取最近的3个簇作为候选池 [~, candidateIdx] = pdist2(center, testData(i, :), 'euclidean', 'Smallest', 3); memberIdx = find(ismember(idx, candidateIdx));

ismember会把属于这三个簇的所有训练样本都拉进候选池。候选池一般还是远小于全量训练集,速度依然比全量KNN快一个数量级,而准确率损失大幅下降。

4.3 方法二:用K-means做原型选择,减少训练集规模

另一个思路是直接给训练集“减肥”:对每个类别分别做K-means,把得到的簇中心作为新的训练样本,簇的标签按该簇多数类决定。训练集从N降到K,KNN计算量随之下降。

function [protoData, protoLabel] = prototype_selection(trainData, trainLabel, perClassCluster) % perClassCluster: 每个类别保留多少个原型簇中心 classes = unique(trainLabel); protoData = []; protoLabel = []; for c = 1:length(classes) classData = trainData(trainLabel == classes(c), :); kc = min(perClassCluster, size(classData, 1)); [~, center] = kmeans(classData, kc, 'Replicates', 3); protoData = [protoData; center]; protoLabel = [protoLabel; repmat(classes(c), kc, 1)]; end end

这段代码先按类别把训练集拆开,再对每个类别单独聚类。perClassCluster通常设为每个类别样本量的1%到5%,如果原始样本只有几十个,就取原始样本数,不再压缩。

原型选择是典型的有损压缩。簇中心的分布能反映类别大致的中心区域,但会丢掉类边界附近的重要样本。我一般会在压缩前后各跑一遍交叉验证,对比错误率,如果准确率掉得超过两个百分点,就调大perClassCluster,或者保留一部分边界样本。

5. KNN与K-means混用避坑指南:五个我后悔没早知道的坑

5.1 量纲不一致,欧氏距离被大数值特征主导

现象:用KNN分类时,模型几乎只关心数值范围最大的那一两列特征,其他特征变成了摆色,交叉验证准确率怎么调都上不去。

原因:欧氏距离对量纲非常敏感。比如一个特征取值范围是0到1,另一个特征是0到10000,距离计算时后者稍微波动一点,就把前者的差异淹没了。

解决:训练前先做标准化,最好用zscore,而且必须用训练集算出来的均值和标准差去归一化测试集,不能把测试集混进来一起算,否则会造成信息泄漏。

mu = mean(trainData); sigma = std(trainData); trainData = (trainData - mu) ./ sigma; testData = (testData - mu) ./ sigma;

这段代码是KNN项目里的第一步,几乎所有后续调参都建立在标准化的基础上。fitcknn函数里有'Standardize', true这个参数,效果类似,但手写方案时千万别漏。

5.2 K值取偶数导致平票,mode的隐式行为坑人

现象:二分类任务里K设为2,预测结果时对时错,单独看某条样本会发现两个邻居类别各一票,投票结果像随机选的。

原因:mode函数在出现多个最高频值时,会返回数值最小的那个类别。如果类别标签0代表“正常”,1代表“故障”,平票时永远偏向0,看起来就是偶发性漏报。

解决:最简单是把K设为奇数。但多分类任务中,奇数K也可能平票,更稳妥的做法是显式处理平票:统计每个候选类别的邻居数量,如果最高票并列,再比较并列类别的平均距离,距离和更小的一方胜出。实质上就是加权投票的思路,所以我在手写版本里干脆都启用加权。

5.3 K-means初始质心随机,聚类结果不稳定

现象:同一份数据,同样设K为30,跑两次kmeans,得到的簇编号完全不一样,导致后续KNN的预测结果也跟着变。

原因:kmeans的初始质心是随机选择的,不同的起点可能收敛到不同的局部最优。簇变了,原型的选取也变了,KNN的候选池自然就不稳定。

解决:用随机种子把kmeans固定下来,同时加大Replicates次数。

rng(42); % 固定随机种子,让结果可复现 [idx, center] = kmeans(trainData, 30, 'Replicates', 20, ... 'Options', statset('UseParallel', true));

rng是调试后悔药,不设seed的聚类结果今天一个样明天一个样。Replicates设为20意味着重复20次聚类,取误差最小的结果。UseParallel需要并行工具箱,没有的话去掉也行,只是慢一点。

5.4 只用最近簇中心粗筛,在簇边界翻车

现象:用K-means加速KNN后,整体准确率比全量KNN低了三五个点,检查错误样本发现集中在两个簇的交界区域。

原因:测试样本真正最近的邻居分布在邻近簇里,但簇中心粗筛只选了最近一个簇,把真实邻居排除在候选池外。

解决:把候选簇从1个扩展到3个左右,前面4.2节已经给出ismember的写法。这里的关键权衡是候选簇数量越大,越接近全量KNN,加速效果越小。建议用一小部分验证集试跑,观察准确率和延迟的拐点。

5.5 fitcknn不调参就上路,预测结果像摇骰子

现象:直接用fitcknn默认参数训练,做了几轮预测,曲线看着波动很大,换一批测试数据结果就对不上了。

原因:默认参数下K往往很小,相当于只看最近的一两个样本,对噪声和离群点几乎没有抵抗力。

解决:先把K通过交叉验证选出来,再显式传给fitcknn,同时打开标准化选项。

mdl = fitcknn(trainData, trainLabel, ... 'NumNeighbors', 7, ... 'Distance', 'euclidean', ... 'Standardize', true);

不要以为封装函数会自己优化这些超参数,fitcknn的默认值只是保证“能跑”,不保证“跑得好”。我见过太多人把fitcknn当黑匣子,出问题就换函数,其实他们缺的就是手动选K这一步。

6. 用交叉验证选K值:把错误率曲线画出来再上路

6.1 用K折交叉验证找出稳定K值

选K值最可靠的办法不是拍脑袋,而是画一条交叉验证错误率随K变化的曲线。下面这段代码用fitcknn做5折交叉验证,K从1扫到15,步长为2,把每个K的验证错误率画出来。

rng(42); cv = cvpartition(species, 'KFold', 5); KList = 1:2:15; err = zeros(length(KList), 1); for ki = 1:length(KList) foldErr = zeros(cv.NumTestSets, 1); for f = 1:cv.NumTestSets trIdx = cv.training(f); teIdx = cv.test(f); mdl = fitcknn(meas(trIdx,:), species(trIdx), ... 'NumNeighbors', KList(ki), 'Standardize', true); pred = predict(mdl, meas(teIdx,:)); foldErr(f) = 1 - mean(pred == species(teIdx)); end err(ki) = mean(foldErr); end plot(KList, err, 'o-'); xlabel('K'); ylabel('5-Fold Cross-Validation Error');

cvpartition把数据均匀分成5份,每次用4份训练、1份验证。外层循环扫K,内层循环跑完5折取平均,这一条曲线比任何经验公式都直观。选K时不要只盯最低点,看整条曲线:如果K=5和K=9的错误率只差0.1%,我一般选5,因为更小的K意味着更快的预测和更简单的边界。

如果用的是自己手写的myknn,在交叉验证前必须记得对每一折都用训练折的均值和标准差去标准化验证折,不能用全量数据的统计量。fitcknn的'Standardize'参数内部会自动按训练折处理,这也是我推荐新手直接用fitcknn跑交叉验证的原因。

我自己做KNN项目,已经形成习惯:先标准化全数据,再跑一遍这条错误率曲线,确认错误率平台区,然后选择平台区左侧的K值。很多调参翻车其实不是算法问题,而是连一张这样的曲线都没画过。KNN不是一个复杂的模型,但它的效果高度依赖K、距离度量和数据尺度这三样东西,每一项都值得用交叉验证去检验。希望这套流程能帮你的Matlab KNN项目少走点弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 18:22:18

VMware虚拟化引擎三开关详解:VT-x、性能计数器与IOMMU

1. 为什么VMware会给你三个“虚拟化”开关&#xff1f;先搞懂底层逻辑很多人第一次打开VMware Workstation的“虚拟机设置 -> 处理器”&#xff0c;看到“虚拟化引擎”下面那一串选项&#xff0c;第一反应是&#xff1a;这不是都选上拉倒吗&#xff1f;其实不行。这三个开关…

作者头像 李华
网站建设 2026/10/1 18:21:55

后见之明:从心理学偏差到AI复盘的实用方法

"hindsight"最近被刷到的频率有点高。我在投资理财、项目管理、体育评论和一些科技社区的讨论里反复看见它&#xff0c;语境惊人地一致&#xff1a;事情出了结果之后&#xff0c;有人站出来说一句"其实早就该看出来""这不就是明摆着的吗"。翻译过…

作者头像 李华
网站建设 2026/10/1 18:21:51

hindsight:深入解析Chrome浏览器历史记录的取证工具

1. 先从名字说起&#xff1a;hindsight 为什么叫"后见之明" 第一次在 GitHub 上看到 hindsight 这个名字时&#xff0c;我以为是某个讲认知心理学的项目——毕竟 hindsight&#xff08;后见之明&#xff09;在书里最常见的解释是"事后看来一切都清清楚楚"。…

作者头像 李华
网站建设 2026/10/1 18:21:09

从零构建推理模型:三个月吃透AI工程核心路线图

如果你跟我一样是写代码出身&#xff0c;这几年一定被问过无数次&#xff1a;“现在大模型都这么强了&#xff0c;还有必要从零开始学AI工程吗&#xff1f;”我自己的体会是&#xff0c;这个问题的答案取决于你到底想当“用户”还是“工程师”。用API、搭Agent、做RAG&#xff…

作者头像 李华
网站建设 2026/10/1 18:21:07

Java面试刷题的本质与系统化备战:从信号博弈到结构化表达

1. 面试不是考试&#xff0c;是一场信号博弈 我自己既做过求职者&#xff0c;也坐在面试官的位置上聊过不少候选人。先说一个可能有点反直觉的结论&#xff1a;Java程序员面试之前刷题&#xff0c;核心目的从来不是为了"押中面试题"&#xff0c;而是为了对抗面试这场…

作者头像 李华
网站建设 2026/10/1 18:20:54

Verdi 断言波形调试:从失败日志到正确采样沿

凌晨一点半&#xff0c;回归脚本刷出一行红字&#xff1a;"tb_top.u_dma.a_hs_done": started at 128450ns failed at 132450ns。我把valid、ready、done三根线拉进 Verdi 的 nWave&#xff0c;来回放大到 128450ns 附近&#xff0c;盯着看&#xff1a;沿是干净的&am…

作者头像 李华