☰
手写Matlab KNN实现:从距离矩阵到交叉验证全流程
2026/10/24 9:52:36 网站建设 项目流程

简介:面向模式识别、机器学习课程学习者与 Matlab 编程入门者的 KNN 实践材料,围绕 KNN 分类原理与代码实现展开,可用于课堂作业、课程设计及分类实验复现。压缩包共 3 个文件,均为 .m 脚本或函数文件,整体约 3KB,轻量且便于在 Matlab 中直接阅读、运行和修改,内容覆盖数据预处理、距离计算、K 值选择、最近邻筛选与类别投票等关键环节。材料结合“第六章 KNN 编程作业”的示例与说明,并涉及剪辑近邻法通过阈值筛选邻居以降低噪声干扰的思路,方便读者对照理解算法流程、调试代码并完成自己的分类任务。已有 4444 人学习下载,适合作为 KNN 入门实践与作业参考的轻量资料。

1. 从一张分不开的散点图说起:KNN 为什么总排在模式识别课的第一位

模式识别这门课的第一个上机任务,十有八九是甩给你一份带标签的数据,让你用 KNN 算法跑个分类 baseline。数据发下来,Matlab 一开,很多人第一反应是敲一行fitcknn交差——能出准确率,但被追问一句「距离怎么算的、k 怎么定的、平票怎么处理」,当场卡壳。

这套 Matlab 实现就是为这个场景准备的。它不依赖任何工具箱,从距离矩阵、近邻选取、投票规则到交叉验证选 k,一层层拆开写,总共不到两百行。跑通之后,距离函数、投票权重、k 的搜索网格都可以随手替换,把它当后续实验的底座用。适合正在做课设、想搞清楚每一行在算什么的人,也适合已经会用工具箱、但想掀开盖子看看齿轮怎么咬合的人。

2. 工程骨架:一份能直接跑的 KNN Matlab 实现由哪几个文件组成

2.1 目录结构与每个文件的职责

先把文件拆开。见过太多把 KNN 塞进一个main.m里写到四百行的作业,后面想换一种距离度量,得在三处地方同时改,改漏一处就跑出个莫名其妙的结果,排查起来纯属玄学。按职责切分的版本大概长这样:

knn_lab/ ├── main.m % 入口脚本: 造数据 → 切分 → 归一化 → 选k → 预测 → 评估 ├── make_demo_data.m % 生成带标签的合成数据, 用来打通流程 ├── split_dataset.m % 分层切分, 固定随机种子保证可复现 ├── normalize_zscore.m % z-score 归一化, 统计量只从训练集算 ├── knn_distance.m % 距离矩阵, 度量方式在这里替换 ├── knn_classify.m % 核心分类器: 取近邻 + 投票 ├── cross_validate_k.m % K 折交叉验证扫 k 网格 ├── evaluate_model.m % 混淆矩阵与逐类召回率 └── data/ └── features.csv % 换成自己的数据放这里
文件职责是否依赖工具箱
main.m串起全流程的入口脚本否
split_dataset.m按类别分层切分训练/测试集否
normalize_zscore.m特征标准化,返回 mu/sigma 供推理复用否
knn_distance.m计算 M×N 距离矩阵,支持欧氏/余弦否
knn_classify.m近邻选取、加权投票、平票处理否
cross_validate_k.mK 折交叉验证,输出准确率曲线否
evaluate_model.m手写混淆矩阵,算 P/R/F1否

这样拆的好处很直接:换距离度量只动knn_distance.m一个文件,换投票策略只动knn_classify.m里那七八行,两边互不干扰。做消融实验的时候,改动面越小越不容易翻车。

2.2 运行环境与最小依赖

整套代码只用到 Matlab 的基础语法。R2016b 之后支持隐式扩展(M×1和1×N相加自动广播成M×N),vecnorm从 R2017b 开始才有,所以我把距离计算写成sum(X.^2, 2)的形式,老版本也能跑。rng、sort、unique、sub2ind全是基础函数,不需要额外装任何东西。

需要说明的是knnsearch和fitcknn这两个函数——它们属于 Statistics and Machine Learning Toolbox。第 4 章会拿来做提速对比,但那不是必需品,手写版本单独就能跑完整条链路。如果你所在的环境没有这个工具箱,跳过对比实验即可。

入口脚本main.m按「数据 → 切分 → 归一化 → 选参 → 预测 → 评估」六步串起来,每一步落一个函数调用:

%% main.m —— KNN 全流程入口 clear; clc; % 1) 造一份三类二维数据, 换成自己的 csv 只改这一行 [X, y] = make_demo_data(300, 3, 42); % 2) 分层切分 7:3, 种子固定 [XTrain, yTrain, XTest, yTest] = split_dataset(X, y, 0.3, 42); % 3) 归一化: mu/sigma 只从训练集统计, 测试集直接套用 [ZTrain, ZTest, mu, sigma] = normalize_zscore(XTrain, XTest); % 4) 五折交叉验证扫 k 网格 kList = 1:2:21; [bestK, accCurve] = cross_validate_k(XTrain, yTrain, kList, 5, 7); fprintf('best k = %d, cv acc = %.4f\n', bestK, max(accCurve)); % 5) 用最优 k 预测, 开启距离加权 t0 = tic; [yPred, score] = knn_classify(ZTrain, yTrain, ZTest, bestK, ... struct('weighted', true, 'metric', 'euclidean')); fprintf('predict %d samples in %.1f ms\n', numel(yTest), toc(t0) * 1000); % 6) 评估 stats = evaluate_model(yTest, yPred); fprintf('test accuracy = %.4f\n', stats.accuracy); disp(stats.cm);

make_demo_data负责在二维平面上撒几团高斯分布的点,类中心均匀铺在半径 4 的圆上,噪声取单位方差。它的价值不在于数据有多真实,而在于「答案已知」——三团点分得开,任何分类器的准确率都应该在 0.95 以上;如果跑出来只有 0.6,那一定是链路里某个环节出了问题,而不是数据太难。做算法调试时,一组答案已知的合成数据比真实数据好用十倍。

function [X, y] = make_demo_data(nPerClass, nClass, seed) %MAKE_DEMO_DATA 生成若干二维高斯簇, 用于快速验证链路是否打通 rng(seed); theta = linspace(0, 2*pi, nClass + 1); theta(end) = []; centers = 4 * [cos(theta)', sin(theta)']; % 类中心均匀铺在半径 4 的圆上 X = zeros(nPerClass * nClass, 2); y = zeros(nPerClass * nClass, 1); for c = 1:nClass rows = (c-1) * nPerClass + (1:nPerClass); X(rows, :) = centers(c, :) + randn(nPerClass, 2); % 单位方差噪声 y(rows) = c; end end

参数里nPerClass控制每类样本量,nClass控制类别数,seed交给rng保证同一份数据多次生成结果一致。噪声标准差固定为 1,如果想让问题变难,把它改成 2 或者 3,类之间就开始重叠,这时候 k 的取值和加权策略的影响才会明显体现出来。

2.3 数据格式约定:特征矩阵与标签向量怎么排

KNN 的实现里最容易埋雷的地方不是算法,是数据格式。约定只有一条:X是N×D的 double 矩阵,行是样本、列是特征;y是N×1的整数向量,取值必须是1..C的连续正整数。后面投票时用score(:, c)按类号索引,标签一旦出现 0 或者跳号(比如 1、3、7),score 矩阵的列就对不上了。

真实数据多半从 csv 进来,读完先做一次标签重编号:

T = readtable('data/features.csv'); X = table2array(T(:, 1:end-1)); % 前 D 列是特征 rawLabel = T{:, end}; % 最后一列是标签, 可能是字符串或数值 % unique 的第三个输出就是 1..C 的重编号, 字符串/数值通吃 [~, ~, y] = unique(rawLabel); y = y(:); % 强制列向量 X = double(X); % 防止某些列被读成整型导致除法截断

unique的第三个返回值是索引映射,比如原始标签是{'cat','dog','cat'},重编号后变成[1;2;1],类别名通过unique的第一个输出保留下来,评估时想显示原名再映射回去就行。double(X)那句看着多余,实际上很关键:readtable读整数列时会给int64类型,后面算X - mu时整型除法的截断会让归一化彻底失效,报错还特别隐蔽。

切分函数用分层策略,保证每个类在测试集里都有代表:

function [XTrain, yTrain, XTest, yTest] = split_dataset(X, y, testRatio, seed) %SPLIT_DATASET 按类别分层切分, 避免小类在测试集里缺席 if nargin < 4, seed = 42; end rng(seed); classes = unique(y); idxTrain = false(numel(y), 1); for c = 1:numel(classes) idx = find(y == classes(c)); idx = idx(randperm(numel(idx))); % 类内打乱 nTest = max(1, round(numel(idx) * testRatio)); % 每类至少留 1 个 idxTrain(idx(nTest+1:end)) = true; % 剩下的进训练集 end XTrain = X(idxTrain, :); yTrain = y(idxTrain); XTest = X(~idxTrain, :); yTest = y(~idxTrain); end

testRatio一般取 0.2 到 0.3,样本量小于 500 时建议走交叉验证而不是单次切分,否则测试集太小,准确率的方差能大到让你怀疑人生。max(1, ...)这个保底是为了防止某个类只有三五个样本时测试集里一个都没有,那样混淆矩阵会缺行,后面算召回率直接除零。

3. 核心链路拆解:从欧氏距离矩阵到多数投票的每一步

3.1 距离矩阵:一次矩阵乘法顶掉两重循环

KNN 的计算量全在距离上。最直观的写法是两层for循环,测试样本一个、训练样本一个,逐个算欧氏距离,1000×2000的数据在 Matlab 里能跑上十几秒。向量化的做法是把平方欧氏距离展开:

$$|a-b|^2 = |a|^2 + |b|^2 - 2a^\top b$$

右边第一项是测试集每行的模长平方,第二项是训练集每行的模长平方,第三项是一次矩阵乘法XTest * XTrain'。三个都是高度优化的 BLAS 调用,写出来就是十几行:

function D2 = knn_distance(XTrain, XTest, metric) %KNN_DISTANCE 计算测试集到训练集的平方距离矩阵 % D2(i,j) = 第 i 个测试样本到第 j 个训练样本的距离 if nargin < 3, metric = 'euclidean'; end switch lower(metric) case 'euclidean' aa = sum(XTest.^2, 2); % M×1, 测试集样本模长平方 bb = sum(XTrain.^2, 2)'; % 1×N, 训练集样本模长平方 D2 = aa + bb - 2 * (XTest * XTrain'); % 隐式扩展成 M×N D2(D2 < 0) = 0; % 浮点相减可能出 -1e-16 case 'cosine' Xn = XTest ./ sqrt(sum(XTest.^2, 2)); Yn = XTrain ./ sqrt(sum(XTrain.^2, 2)); D2 = 1 - Xn * Yn'; % 余弦距离, 范围 [0, 2] otherwise error('unsupported metric: %s', metric); end end

D2(D2 < 0) = 0这一行不是洁癖。两个几乎相同的向量做减法,浮点误差会让结果出现-1e-16这种负数,后面sqrt一下直接变成复数,报错信息还指向别处,非常难查。截断到 0 成本极低,但没有它,我至少浪费过一个下午。

metric参数是给二次开发留的口子。改成'cosine'之后,注意weighted加权投票逻辑里的1/(d+eps)依然成立,只是物理含义从「距离倒数」变成了「相似度倒数」,语义上要自己心里有数。这块第 6 章会再展开。

3.2 取前 K 个近邻:全排序、局部选择与索引复用

拿到 M×N 的距离矩阵之后,要对每一行找出最小的 K 个。最省事的写法是sort(D2, 2, 'ascend'),拿到排序后的距离和对应的原始列下标,取前 K 列就行。全排序每行是 $O(N\log N)$,而我们只需要前 K 个,理论上有更快的选择算法。

Matlab 从 R2017b 开始提供mink,做的是部分选择,K 远小于 N 时比sort快不少:

K = min(k, size(XTrain, 1)); % k 不能超过训练样本总数 if exist('mink', 'file') [dK, idx] = mink(D2, K, 2); % R2017b+: 每行取最小的 K 个, 返回距离和下标 else [~, idx] = sort(D2, 2, 'ascend'); % 老版本回退到全排序 idx = idx(:, 1:K); dK = D2(sub2ind(size(D2), repmat((1:size(D2,1))', 1, K), idx)); end

mink的第二个参数是取多少个,第三个参数2表示沿列方向(也就是每行内部)取。idx拿到的是 M×K 的下标矩阵,每一行是这 M 个测试样本各自的 K 个最近训练样本的列号,这个矩阵后面投票时要用两次——一次取标签,一次取距离。

sub2ind那行是把二维下标转成线性下标,用于从原始的D2里把对应的距离捞出来。它等价于用循环写for i=1:M, dK(i,:) = D2(i, idx(i,:)); end,但省掉了解释器开销。数据量上万以后,这种细节的差距是能感觉到的。

3.3 多数投票:score 矩阵怎么建,加权与平票怎么处理

核心分类器把前面两步拼起来,再加一个投票环节。思路是给每个测试样本建一个长度 C 的得分向量,K 个邻居里属于第 c 类的有几个,第 c 位就加几分,最后取max:

function [yPred, score] = knn_classify(XTrain, yTrain, XTest, k, opts) %KNN_CLASSIFY 向量化 KNN 分类器 % XTrain/yTrain: 训练特征与标签(标签须为 1..C 连续整数) % XTest: 测试特征(用训练集的 mu/sigma 归一化过) % k : 近邻个数 % opts : .weighted(默认 false) / .metric(默认 'euclidean') if nargin < 5, opts = struct(); end if ~isfield(opts, 'weighted'), opts.weighted = false; end if ~isfield(opts, 'metric'), opts.metric = 'euclidean'; end nTest = size(XTest, 1); nClass = max(yTrain); % 标签必须是 1..C K = min(k, size(XTrain, 1)); D2 = knn_distance(XTrain, XTest, opts.metric); % M×N [~, idx] = sort(D2, 2, 'ascend'); idx = idx(:, 1:K); % M×K 近邻下标 rows = repmat((1:nTest)', 1, K); dK = D2(sub2ind(size(D2), rows, idx)); % M×K 近邻距离 yNb = yTrain(idx); % M×K 近邻标签 if opts.weighted W = 1 ./ (dK + 1e-12); % 距离越小, 投票权重越大 else W = ones(nTest, K); end score = zeros(nTest, nClass); for c = 1:nClass score(:, c) = sum(W .* (yNb == c), 2); % 逐类累加票数 end [maxScore, yPred] = max(score, [], 2); tie = sum(score == maxScore, 2) > 1; % 平票的行 if any(tie) yPred(tie) = yTrain(idx(tie, 1)); % 平票退化成 1-NN end end

yTrain(idx)这一句依赖yTrain是列向量,idx是矩阵,Matlab 会返回同形状的矩阵,省掉一层循环。后面的yNb == c是逐元素比较,sum(..., 2)按行求和,整个投票过程没有显式的样本循环。

平票是必须处理的分支。K 取偶数时(比如 k=4,两类各 2 票)平票是必然发生的,max默认返回第一个最大值的下标,这就等于「类别号小的赢」,纯属实现细节带来的偏见,跟算法本身没关系。我一般让平票退化成 1-NN,取距离最近的那个邻居的标签——这比按类别号拍脑袋合理得多。

1e-12的 eps 是为了防除零:归一化之后训练集和测试集里可能出现完全相同的样本,距离为 0,倒数变成Inf,乘上 0 票会变成NaN,整个 score 就废了。

3.4 K 折交叉验证:k 的候选网格怎么扫,归一化为什么必须放进折内

k 是这套算法唯一真正需要调的超参数,靠拍脑袋定下来不合适。做法是划一个候选网格(1、3、5…21),每一折在训练部分上归一化、训练、在验证部分上打分,取平均准确率最高的那个:

function [bestK, accCurve] = cross_validate_k(X, y, kList, nFold, seed) %CROSS_VALIDATE_K K 折交叉验证扫 k 网格 if nargin < 5, seed = 7; end rng(seed); n = size(X, 1); perm = randperm(n); foldId = zeros(n, 1); foldId(perm) = mod(0:n-1, nFold) + 1; % 尽量均分到各折 accCurve = zeros(numel(kList), 1); for ki = 1:numel(kList) k = kList(ki); acc = zeros(nFold, 1); for f = 1:nFold te = foldId == f; tr = ~te; % 关键: 归一化放在折内, 统计量只来自当前折的训练部分 [Ztr, Zte] = normalize_zscore(X(tr, :), X(te, :)); yp = knn_classify(Ztr, y(tr), Zte, k); acc(f) = mean(yp == y(te)); end accCurve(ki) = mean(acc); end [~, iBest] = max(accCurve); bestK = kList(iBest); end

foldId(perm) = mod(0:n-1, nFold) + 1这行是均分技巧:先把样本随机打乱,再按位置轮流发到各折,样本数不能被折数整除时误差也只有 1,比按固定块切分靠谱。nFold一般取 5 或 10,样本量很小时可以用留一法(nFold = n),但代价是训练次数等于样本数。

归一化写在折内这件事值得单独强调。如果先在全体数据上算mu/sigma再做交叉验证,验证折的信息已经通过统计量泄漏进了训练过程,交叉验证的准确率会系统性偏高,通常高出一两个点,看起来不明显,但足以让你选错 k。这种错误的可怕之处在于:它不会报错,也不会崩,只是让你拿到的数字不再可信。accCurve顺手返回出来,画条曲线看看拐点,比只看最小值更有信息量。

3.5 评估:混淆矩阵、逐类召回率与手写累加

confusionmat属于工具箱函数,为了保持零依赖,这里手写累加:

function stats = evaluate_model(yTrue, yPred) %EVALUATE_MODEL 混淆矩阵与逐类指标, 不依赖工具箱 C = max([yTrue(:); yPred(:)]); cm = zeros(C, C); for i = 1:numel(yTrue) cm(yTrue(i), yPred(i)) = cm(yTrue(i), yPred(i)) + 1; end tp = diag(cm); stats.cm = cm; stats.accuracy = sum(tp) / sum(cm(:)); stats.recall = tp ./ max(sum(cm, 2), 1); % 行和 = 真实类样本数 stats.precision = (tp' ./ max(sum(cm, 1), 1))'; % 列和 = 预测类样本数 stats.f1 = 2 * stats.precision .* stats.recall ... ./ max(stats.precision + stats.recall, eps); end

cm(i,j)表示真实类别是 i、被预测成 j 的样本数,对角线是分对的。行方向求和得到每个真实类的样本总数,用来算召回率;列方向求和得到每个预测类的样本总数,用来算精确率。max(..., 1)是为了防止某个类在测试集里完全缺席时除零,算出来是 0 而不是NaN,至少不会污染后面的平均值。

只看总体准确率在类别不平衡时会骗人。三类数据里有一类占了 90%,全判成这一类也能拿到 0.9 的准确率。把stats.recall打出来,逐类看,哪个类被打成了筛子一目了然,比盯一个标量数字有用得多。

4. 性能与精度调优:让手写 KNN 从玩具代码变成实验工具

4.1 复杂度账本:训练是假的,预测才是真的贵

KNN 是懒惰学习的典型:训练阶段什么也不做,就是把数据存起来,复杂度 O(1);所有开销堆在预测阶段,每个测试样本要和全部 N 个训练样本算距离,总复杂度 $O(M \cdot N \cdot D)$。这个不对称性意味着「训练快」根本不是优点,真正要盯的是预测耗时和内存。

内存这一侧更隐蔽。M×N 的距离矩阵是 double 类型,每个元素 8 字节,M=5000, N=20000就是 800MB,稍微再大一点直接 out of memory。而这个矩阵只用来取前 K 个最小值,绝大部分内容算完就扔,占着内存纯属浪费。

4.2 分块计算:把 M×N 矩阵拆成若干条带

解决办法是按测试样本分块,一次只算一块的距离矩阵,算完取完近邻就释放:

function yPred = knn_predict_chunked(XTrain, yTrain, XTest, k, blockSize) %KNN_PREDICT_CHUNKED 分块预测, 把 M×N 距离矩阵压到 blockSize×N if nargin < 5, blockSize = 1000; end nTest = size(XTest, 1); yPred = zeros(nTest, 1); for s = 1:blockSize:nTest e = min(s + blockSize - 1, nTest); yPred(s:e) = knn_classify(XTrain, yTrain, XTest(s:e, :), k); end end

blockSize是唯一要调的参数,取值取决于训练集大小和可用内存。经验公式是让blockSize * N * 8控制在内存的三分之一以内:N=20000 时,blockSize 取 1000 大概占 160MB,很安全;N=100000 时就得降到 200 以下。分块之后总计算量不变,但峰值内存降了一个数量级,而且因为矩阵变小了,缓存命中率反而更高,实测常常比分块前还快一点。

4.3 parfor 与 knnsearch:两条提速路线怎么选

再往上提速有两条路。第一条是并行,把分块循环换成parfor,前提是已经装了 Parallel Computing Toolbox 并且开了并行池:

parfor s = 1:blockSize:nTest e = min(s + blockSize - 1, nTest); yPred(s:e) = knn_classify(XTrain, yTrain, XTest(s:e, :), k); end

注意parfor里不能有跨迭代的共享状态,yPred用切片方式赋值是合法的,但循环内不能再rng或者写全局变量。加速比取决于核数,4 核大概能到 2.5 到 3 倍,核再多收益递减——瓶颈会转移到内存带宽上。

第二条路是直接调用工具箱的knnsearch:

% 需要 Statistics and Machine Learning Toolbox [idx, d] = knnsearch(XTrain, XTest, 'K', k, 'NSMethod', 'kdtree'); yNb = yTrain(idx); % 后面接同样的投票逻辑

NSMethod有两个取值,'exhaustive'是暴力搜索,'kdtree'建 KD 树。KD 树在低维(D 小于 10 左右)时优势明显,建树一次、查询多次;维度一高就退化得厉害,「维数灾难」在 KNN 里体现得特别直接,D 到 50 以上时 KD 树可能比暴力搜索还慢,因为这个剪枝结构在高维空间里几乎失效。

方案适用场景内存峰值依赖相对耗时
双重循环只用来对拍验证正确性最低无最慢
向量化全量N×M 小于 1e7高无快
分块向量化大数据集常规方案可控无快
分块 + parfor多核机器、离线实验可控并行工具箱更快
knnsearch+ kdtree低维、多次查询低统计工具箱低维最快

表里的「相对耗时」是定性排序,具体倍数跟机器、维度、数据分布都有关系,别照抄。想拿到自己环境下的真实数字,写个 benchmark 循环for r = 1:5跑五遍取中位数,比看任何表格都靠谱。

4.4 精度侧的三个旋钮:k、权重、归一化

性能之外,真正影响准确率的是三个东西,按影响从大到小排。

第一个是归一化。这条几乎每次都会踩,因为不归一化代码照样跑,只是准确率悄悄掉下去。量纲差异大的时候尤其致命:一个特征范围是 0 到 255,另一个是 0 到 1,欧氏距离完全被前者主导,后一个特征等于没参与计算。z-score 之后每个维度方差为 1,贡献才均衡。

第二个是 k。k 太小模型方差大,一个噪声点就能翻转结果;k 太大偏差大,类边界被抹平。实践中从k=1扫到k=sqrt(N)附近,看交叉验证曲线的最低点。奇数优先,能天然规避两类平票(不过多类问题下平票依然可能,第 3 章那段平票处理不能省)。

第三个是加权投票。weighted=true之后近邻的票数按距离倒数加权,靠近边界的样本受影响更大,通常在类重叠较多的数据上能涨一到两个点。代价是对噪声更敏感——一个离群点如果恰好离测试样本很近,它的权重会非常大,一个顶十个。数据洗干净之前别急着开这个开关。

5. 避坑记录:Matlab 里跑 KNN 最容易翻车的 5 个地方

5.1 现象:准确率一直卡在随机猜的水平

原因:九成是没做归一化,或者归一化的统计量用错了数据。前者让大方差维度独占距离,后者更隐蔽——在全体数据上算mu/sigma再切分,测试集的信息泄漏进了训练过程。还有一种少见但同样致命的情况:csv 里的整数列被读成int64,X - mu时整型除法直接截断,归一化等于没做。

解决:normalize_zscore只接受训练集和测试集两个输入,mu和sigma从训练集算、保存下来,推理时复用同一组。读数据后强制X = double(X)。验证方法很土但有效:归一化之后mean(ZTrain)应该接近全零向量,std(ZTrain)接近全一向量,打出来看一眼。

5.2 现象:score(:, c)报下标越界,或者预测结果只有一类

原因:标签不是从 1 开始的连续整数。原始标签可能是 0/1/2,也可能是字符串元胞数组,直接塞进knn_classify里,nClass = max(yTrain)算出来的类数对不上,score的列数和实际类别错位。

解决:入口处统一重编号。[~, ~, y] = unique(rawLabel)这一句对字符串、数值、categorical 全都管用,输出保证是1..C的连续整数。同时检查一下y是不是列向量——readtable取出来的可能是行,yTrain(idx)的形状会跟着错,报错信息指向别处,查半天。

5.3 现象:调大 k 之后准确率断崖式下跌,某一类被彻底吞掉

原因:k 超过了某个小类的样本数。比如某类只有 5 个训练样本,k 取 15,这 15 个邻居里属于该类的顶多 5 票,其他类随便凑凑就压过去了。类别不平衡时这个问题会放大,多数类的票数天然占优。

解决:K = min(k, size(XTrain,1))只是防越

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询