简介:面向数学建模、科学计算与科研数据分析场景,这份MATLAB实现K近邻(KNN)算法的压缩包,定位于帮助初学者快速上手监督学习中的经典分类与回归方法。KNN以“物以类聚”为核心思想,无需训练过程,原理直观,该资源恰好以精简代码呈现这一过程。包体仅含1个MATLAB脚本文件(m文件),压缩包大小约1KB,代码涵盖邻域搜索、距离度量与分类决策等关键环节,虽小巧却具备完整的参考价值。目前已有202人学习下载,简洁的结构既适合刚接触算法的本科生/研究生对照运行,也适合有经验者快速搭建基线版本。通过脚本中的示例,读者可以掌握欧氏距离等度量方式的MATLAB实现,并进一步将KNN迁移到模式识别、异常检测或时间序列等自身课题中,提升算法落地效率,整体是一份小而精的算法学习素材。
1. K近邻为什么总在数学建模的“保底算法”名单里
不管是全国大学生数学建模竞赛还是华为杯研究生数学建模竞赛,拿到赛题后先做数据探索,几乎都会试一遍分类器。K近邻(KNN)通常是第一个被拉出来跑通流程的算法:不需要训练模型、不用假设数据分布、代码二十行就能出分类结果。在数学建模和科学计算场景里,它还是处理小样本分类、异常检测、缺失值填补的通用工具,甚至比决策树、支持向量机更好解释——评委问你“为什么这么分”,你只需要说“离它最近的几个样本这么分,所以它分到这一类”。
但K近邻能上榜,靠的不是深奥的统计理论,而是它的执行逻辑足够直白:一个样本的类别,由它周围最近的k个样本投票决定。这个逻辑既是优点也是陷阱。k取多少、距离怎么算、特征要不要标准化、数据维度高了以后邻居还准不准,每一个环节都会让准确率剧烈波动。用MATLAB实现K近邻算法时,可以直接调fitcknn,也可以手写一个循环版本跑通整个流程。这篇内容会从两种实现路径讲起,再结合数学建模案例和参数调优,把这套算法在MATLAB里的细节拆开,方便你在赛场上快速复现。
2. 从欧氏距离到MATLAB函数:K近邻的两个实现层次
2.1 手写K近邻的核心步骤
2.1.1 距离计算:先理解KNN的决策基础
K近邻的所有决策都建立在“距离”上。最常见的欧氏距离在MATLAB里用pdist2一行就能算,但自己写一遍能看清楚计算过程,更方便排查量纲问题。
% 构造两类二维样本 rng(42); data1 = mvnrnd([0 0], eye(2)*0.3, 30); data2 = mvnrnd([2 2], eye(2)*0.5, 30); X = [data1; data2]; % 60x2 的特征矩阵 y = [ones(30,1); -ones(30,1)]; % 类别标签 +1 / -1 % 待预测样本 xq = [1.0 1.2]; % 方案一:用矩阵运算手写欧氏距离 diff = X - xq; % 每个样本与查询点的坐标差(60x2) dist = sqrt(sum(diff.^2, 2)); % 按行平方求和再开方,得到60x1距离向量上面这段代码里,diff记录了每个样本与查询点在各维度上的差值,sum(..., 2)表示沿第二个维度(列方向)求和,得到每个样本的平方距离汇总,最后开方得到距离。手写的好处是,如果中途需要换成带权重的距离,可以直接在diff上乘一个权重向量。
2.1.2 投票与回归:从距离到决策只有三步
拿到距离向量后,找出距离最近的k个样本,分类问题看票数,回归问题看均值:
k = 5; [~, idx] = sort(dist); % 距离从小到大排序 neighborIdx = idx(1:k); % 前k个邻居的位置 neighborLabels = y(neighborIdx); % 分类:多数投票 predClass = sign(sum(neighborLabels)); % 两类,用符号函数统计 % 回归:邻居均值 predValue = mean(neighborLabels);sort(dist)返回排序结果和原始索引,idx(1:k)就是最近k个邻居在原始样本矩阵中的行号。分类这里用sum(neighborLabels)配合sign,适用于±1编码的两分类问题;如果面对多分类,改用mode(neighborLabels)取众数就可以了。回归场景里连续量的预测值直接取邻居均值,经验做法是以距离倒数为权重做加权平均,距离近的样本对结果影响更大。
手写流程对数学建模比赛的意义在于:当自带工具箱不可用时,这套逻辑迁到Python、R或者Excel里也能成立。它同时也暴露了KNN的关键瓶颈——每预测一个样本都要计算它与全部训练样本的距离,数据量大时计算开销成倍增长。
2.2 用fitcknn快速建模范式
2.2.1 fitcknn与predict的最小闭环
MATLAB的Statistics and Machine Learning Toolbox提供了封装好的fitcknn,它能自动处理距离计算、邻居搜索策略和投票规则。对数学建模赛题来说,优先使用这个函数能省下大量调循环的时间。
% 加载自带数据:鸢尾花 load fisheriris X = meas; % 150x4 花瓣与花萼尺寸 Y = species; % 150x1 花的类别(字符串元胞) % 切分训练集与测试集 cv = cvpartition(length(Y), 'HoldOut', 0.3); trainIdx = training(cv); testIdx = test(cv); % 训练KNN分类器,k默认取与类别数不相关的经验值5 mdl = fitcknn(X(trainIdx,:), Y(trainIdx), 'NumNeighbors', 5); % 测试集预测 yPred = predict(mdl, X(testIdx,:)); accuracy = sum(yPred == Y(testIdx)) / numel(yPred); fprintf('准确率: %.2f%%\n', accuracy*100);fitcknn实际是创建一个分类器对象ClassificationKNN,predict方法负责对新样本做预测。cvpartition是MATLAB内置的交叉验证工具,上面用的'HoldOut'把70%样本划给训练集,30%用于测试。NumNeighbors是k值,它是KNN最重要的超参数,后面会专门讲怎么选。Y是字符串元胞数组,MATLAB对分类变量的支持直接体现在这里,不需要手动编码成数字。
2.2.2 让模型同时输出概率与邻居索引
比赛里写论文时需要“可解释性”材料,比如“这个样本被判为A类的依据是什么”。predict只能返回类别,但fitcknn对象还有另一个方法predict后接多个输出参数,或者用resubPredict拿邻居信息。
[yPred, score, cost] = predict(mdl, X(testIdx(1:5),:)); % yPred: 预测类别 % score: 各类别得分,KNN得分是训练集中邻居类别的比例 % cost: 按误分类成本计算后的期望成本 % 另一个常用工具:resubPredict,返回训练集自身的邻居信息 resubPred = resubPredict(mdl);score矩阵里每一行是每个测试样本被分到各个类别的概率估计。KNN的得分不是真正意义上经过校准的概率,它只是邻居类别比例的归一化结果,但在数学建模论文里足够表达不确定性。用score还能观察两个类别得分接近的样本,这些往往是分类边界附近的“危险样本”,值得在答辩时拿出来分析。
3. 数学建模里的三组K近邻实战案例
3.1 案例一:分类建模中用交叉验证评估KNN
3.1.1 用鸢尾花数据跑通完整分类流程
很多数学建模赛题本质上是多分类问题,例如根据土壤成分判断地质类型、根据光学特征判断目标类别。鸢尾花数据集结构简单、特征和类别都清晰,适合作为KNN分类建模的沙盘。
load fisheriris X = meas; Y = species; % 标准化:数学建模必做的一步 mu = mean(X); sigma = std(X); X_scaled = (X - mu) ./ sigma; % 五折交叉验证 mdl = fitcknn(X_scaled, Y, 'NumNeighbors', 7, 'Standardize', false); cvmdl = crossval(mdl, 'KFold', 5); loss = kfoldLoss(cvmdl); % 损失函数,默认是误分率 acc = 1 - loss; fprintf('五折交叉验证准确率: %.2f%%\n', acc*100);上面代码先手动标准化再训练,fitcknn里的'Standardize'参数没有重复开启。原因是我把标准化的主动权握在自己手里,这样在后续做特征重要性分析时,每个特征的实际尺度是已知的,不会被fitcknn内部的标准化步骤遮挡。五折交叉验证用crossval实现,kfoldLoss返回的是平均损失,用1减去得到准确率,这个值比单次随机划分训练集和测试集得到的准确率更稳定。
3.1.2 不同k值下的分类效果对比
固定其他条件,把k从1变到15,观察准确率变化。这一步在数学建模中属于“算法稳健性分析”,很多评委喜欢看这类参数敏感性图表。
kList = 1:2:15; accList = zeros(size(kList)); for i = 1:numel(kList) cvmdl = crossval(fitcknn(X_scaled, Y, 'NumNeighbors', kList(i)), 'KFold', 5); accList(i) = 1 - kfoldLoss(cvmdl); end table(kList', accList', 'VariableNames', {'k值', '交叉验证准确率'})循环里每次都重建并交叉验证一个KNN模型,数据量只有150个样本,计算代价很低。输出表格可以看到,当k太小时模型对噪声敏感,k太大时不同类别的样本混入投票,准确率下降。对鸢尾花来说,k在5到9之间通常表现稳定。
3.2 案例二:回归预测连续变量
3.2.1 用KNN做回归的固定套路
KNN的分类和回归在MATLAB里是两条线:fitcknn管分类,fitrknn管回归。后者的预测值来自邻居标签的均值。
rng(0); x = linspace(0, 4*pi, 200)'; y = sin(x) + 0.2*randn(200, 1); % 带噪声的正弦曲线 % 训练KNN回归模型 mdlR = fitrknn(x, y, 'NumNeighbors', 5, 'Standardize', true); % 生成更密的预测网格 xTest = linspace(0, 4*pi, 500)'; yPred = predict(mdlR, xTest); figure; scatter(x, y, 10, 'filled'); hold on; plot(xTest, yPred, 'r-', 'LineWidth', 1.5); legend('含噪观测', 'KNN回归预测');fitrknn的调用方式与分类版几乎一致,但底层用的距离搜索和投票规则不同。回归KNN在曲线光滑度上远不如样条插值,但在数据波动大、无明确函数形式的赛题中,它不需要任何先验假设。注意特征只有x一个维度时,标准化与否影响不大;一旦回归输入变成多个特征,比如预测土壤含水量时同时用温度、湿度、光照,标准化就是必需的前置步骤。
3.2.2 用KDTree加速回归预测
数学建模中如果训练样本达到十万级,朴素的KNN每次预测都要全量扫一遍样本,速度会拖慢计算。fitrknn里可以指定搜索方法。
mdlR2 = fitrknn(x, y, 'NumNeighbors', 5, ... 'NSMethod', 'kdtree', 'Distance', 'euclidean');'NSMethod'设为'kdtree'会构建KDTree来加速邻居搜索,适合特征维度较低(一般少于20维)的情况。特征维度超过20后,KDTree的检索优势会减弱,此时用默认的'exhaustive'暴力搜索可能更稳定。数学建模赛题的数据维度通常在几十到几百之间,选择哪一档需要看实际维度,这条经验在后续调参时非常关键。
3.3 案例三:用KNN填补缺失值
3.3.1 为什么数学建模数据清洗需要KNN
赛题附件里经常出现缺失值,常见处理是删行或填均值。删行浪费信息,均值填充会改变数据分布。KNN填补的思路是:找到目标样本在完整特征空间里的k个近邻,用这些邻居在该缺失特征上的均值(或中位数)来填充。
data = [ 1.0 2.0 NaN; 1.1 2.1 9.0; 0.9 1.9 8.8; 5.0 6.0 NaN; 4.8 5.9 12.0; ]; % 找到含缺失值的行 missIdx = find(any(isnan(data), 2)); for i = 1:numel(missIdx) m = missIdx(i); % 先暂时删掉该行,避免用不完整样本作邻居 tmp = data; tmp(m,:) = []; colMiss = isnan(data(m,:)); % 只使用没有缺失的特征计算距离 validCols = ~colMiss; distToAll = sqrt(sum((tmp(:,validCols) - data(m,validCols)).^2, 2)); [~, order] = sort(distToAll); k = 2; nearVals = tmp(order(1:k), colMiss); % 用邻居均值填充缺失列 data(m, colMiss) = mean(nearVals); end这段代码把KNN填补的逻辑压缩在了一个循环里:对每个缺失样本,先找到在非缺失维度上距离最近的k个完整样本,再取这k个样本在缺失维度上的均值作为填充值。它的隐患是如果某个样本同时丢失多列特征,距离计算只基于少数完整维度,误差会放大。数学建模中更稳妥的做法是先用均值填充跑通流程,再对比不同填充方式对最终模型准确率的影响。
4. K近邻的关键参数怎么调:k值、距离度量与特征标准化
4.1 网格搜索寻找最优k值
4.1.1 k的取值边界与常见误区
k值太小时模型容易过拟合,训练集准确率接近100%,但测试集表现不稳定;k太大时决策边界过于平滑,小类别容易被大类别“淹没”。数学建模里一个反直觉的经验是,k的取值不必追求奇偶数对称,而应该与数据量和类别分布挂钩。当样本量不足100时,k取3或5即可;当样本量超过1000,k取10到20比较合理。
rng(123); X = randn(200, 5); Y = double(sum(X, 2) > 0); % 人工构造二分类标签 kRange = 1:2:31; cvAcc = zeros(size(kRange)); for i = 1:numel(kRange) mdl = fitcknn(X, Y, 'NumNeighbors', kRange(i), 'Distance', 'euclidean'); cvmdl = crossval(mdl, 'KFold', 10); cvAcc(i) = 1 - kfoldLoss(cvmdl); end [bestAcc, bestIdx] = max(cvAcc); fprintf('最优k=%d, 交叉验证准确率=%.3f\n', kRange(bestIdx), bestAcc);这段网格搜索覆盖了k从1到31的所有奇数。选择奇数不是绝对要求,而是为了避免二分类投票出现平票。实际赛题里我一般会把搜索范围扩大到偶数,观察平票是否真实存在,如果评分函数里写了平票时的处理逻辑,奇偶就不再重要。crossval内部每次都会重随机分组,所以同一k值在不同运行中会有一点点浮动,严谨的做法是外层再套一层重复交叉验证,取多次运行的平均值。
4.1.2 用损失曲线判断k的敏感区间
网格搜索的结果可以直接画成曲线观察“k值的稳定平台期”。如果准确率在k=5到k=15之间都维持在高位,说明数据本身分类边界清晰,对k不敏感;如果准确率只在某个单点最高,两侧快速下降,说明数据噪声较大,需要额外关注其他参数。
4.2 距离度量的选择与标准化
4.2.1 标准化是KNN的生命线
KNN对特征尺度极其敏感。比如两个特征分别是身高180厘米和体重70公斤,欧氏距离的计算中身高数值会完全压制体重的贡献。即使只是预测一个类别,量纲不一致也会让邻居选择被高量纲特征主导。下列代码展示了标准化前后的差异。
% 未标准化:特征2的量纲远大于特征1 X1 = [1 100; 2 200; 3 300; 100 500]; Y1 = [0; 0; 1; 1]; mdlRaw = fitcknn(X1, Y1, 'NumNeighbors', 3); % 标准化 X1S = zscore(X1); mdlS = fitcknn(X1S, Y1, 'NumNeighbors', 3);zscore按列做零均值单位方差标准化,计算方式是每列减去均值再除以标准差。很多新手会在fitcknn里同时开启'Standardize', true,又在外面对数据手动标准化,造成重复中心化,虽然结果通常不差,但会让代码难以解释。建议固定一个策略:要么全部用fitcknn内部标准化,要么全部由自己手动完成。
4.2.2 不同距离度量在MATLAB中的写法
除了欧氏距离,数学建模中还常用到曼哈顿距离、切比雪夫距离和余弦距离。fitcknn通过'Distance'参数切换。
| Distance参数 | 对应公式 | 适用场景 |
|---|---|---|
'euclidean' | P2范数 | 连续数值特征,量纲一致时 |
'cityblock' | 各维度绝对差之和 | 高维稀疏特征,对离群点更鲁棒 |
'chebychev' | 各维度绝对差最大值 | 强调最大差异的场景 |
'cosine' | 1减去向量夹角余弦 | 文本向量、方向敏感数据 |
mdlRaw = fitcknn(X_train, Y_train, 'NumNeighbors', 5, 'Distance', 'cityblock');余弦距离在高维文本特征中效果好于欧氏距离,因为文本向量的模长受文档长度影响很大,只关心方向不关心长度。但在数值型赛题里余弦距离并不多见,需要特征本身具有方向语义才用得上。数学建模里最稳妥的路线还是先标准化,再用欧氏距离跑一版,然后用cityblock对比,看准确率是否明显提升。
4.3 维度灾难与特征选择
4.3.1 高维度下邻居不再“近”
当特征维度超过30,欧氏距离的区分能力会迅速下降,所有点之间的距离都变得差不多,KNN的邻居选择几乎失效。数学建模赛题中如果遇到高维特征,常见做法是先做特征选择,保留与目标相关性高的特征。
% 用fscmrmr做最小冗余最大相关性特征选择 [idx, scores] = fscmrmr(X, Y); % idx返回特征索引,按重要性从大到小排序 % 取前15个特征重新训练 X_sel = X(:, idx(1:15)); mdl = fitcknn(X_sel, Y, 'NumNeighbors', 7, 'Standardize', true);fscmrmr是MATLAB自带的多分类特征选择函数,它基于最小冗余最大相关性准则选择特征,速度快且稳。特征选择之后KNN的准确率经常不降反升,“少即是多”在高维KNN场景里几乎是定论。比赛时如果特征数上百,不要急着调k,先砍特征,这个顺序错了会事倍功半。
4.3.2 当数据维度不高但样本不平衡时
样本不平衡在高维分类里会放大k值的影响。多数类样本在空间中占据更大面积,少数类样本容易被淹没。解决思路不是强行调小k,而是用fitcknn的'Prior'参数给少数类更高先验权重,这比调k更直接。
5. 验证模型、加速计算与不平衡数据的处理技巧
数学建模论文中,光给出一个“预测准确率”是不够的,评审更关心这个准确率是怎么算出来的。用crossval做k折交叉验证算是基本操作,但KNN还有一个容易被忽视的验证陷阱:交叉验证划分时必须保持类别比例。cvpartition(Y, 'KFold', 10, 'Stratify', true)这个写法在MATLAB新版本中并不总是连续可用的,更稳的做法是先手工分层采样,再用cvpartition的'Stratify'选项,或者在crossval里传入类别信息让MATLAB自动处理。
对于样本量达到万级的赛题,KNN训练本身几乎不耗时,但预测阶段会慢得明显。fitcknn提供两种加速手段,一是'NSMethod','kdtree',二是并行预测。后者用predict(mdl, Xtest, 'UseParallel', true)即可,但要先开启并行池parpool,否则选项会被忽略。KDTree适合特征维度小于20的情况,超过这个阈值建议还是用exhaustive,或者先做PCA降维再进KNN。
不平衡数据的处理,除了调'Prior',另一个技巧是改投票规则。MATLAB的fitcknn自带'ScoreTransform'参数,可以配合'LossFun'设计代价敏感的权重。比如把少数类的误判代价设为多数类的5倍,模型会把决策边界往多数类方向推,提升少数类召回率。这个操作在fitcknn里的写法是创建分类器后修改cost属性:
mdl = fitcknn(X_train, Y_train, 'NumNeighbors', 7); mdl.Cost = [0 1; 5 0]; % 假设类别是二分类 mdl = fitcknn(X_train, Y_train, 'NumNeighbors', 7, 'Cost', mdl.Cost);这里Cost矩阵的行表示真实类别,列表示预测类别,Cost(2,1)=5意味着把第2类预测成第1类要承担5倍代价。修改后再做交叉验证,观察少数类召回率和总体准确率之间的取舍,这是数学建模论文中“模型改进”部分非常实用的素材。实际比赛中,先用默认参数跑通流程,再集中调k值、标准化和特征维数,最后用混淆矩阵和交叉验证把结果写进论文。如果手写版本和fitcknn版本结果不一致,优先检查训练集和测试集的划分种子是否固定,KNN对样本顺序并不敏感,但对划分方式很敏感。
本文还有配套的精品资源,点击获取