云计算百科
云计算领域专业知识百科平台

机器学习-算法模型系列文章:04-KNN 不做标准化就跑KNN?你的模型正在被一个特征“独裁“

不做标准化就跑KNN?你的模型正在被一个特征"独裁"

目录

  • 不做标准化就跑KNN?你的模型正在被一个特征"独裁"
    • 一、从"选电影"说起:KNN的朴素直觉
    • 二、距离怎么算:从两点公式到n维空间
    • 三、5瓶葡萄酒,一张草稿纸:手算KNN全过程
    • 四、把酒精含量放大10倍:一个特征的"独裁"现场
    • 五、两种标准化:min-max与Z-score
      • 5.1 min-max标准化(离差标准化)
      • 5.2 Z-score标准化(均值归一化)
      • 5.3 怎么选?一张表说清
    • 六、sklearn实战:wine数据集上的对比实验
    • 七、K值怎么选:GridSearchCV调参曲线
    • 八、KNN的优缺点、适用场景,以及2026年还学它干嘛
    • 九、踩坑实录:新手最容易犯的4个错误
    • 十、参考资料
    • 十一、互动引导

在这里插入图片描述

📌 置顶提示|读完你能带走什么

  • 徒手算一遍KNN:5瓶葡萄酒、一张草稿纸,把"距离排序→投票判类"的完整链条走通,从此看KNN不再是黑盒;
  • 亲眼见证一次特征"独裁"事故:在wine数据集上复现"不标准化准确率0.72、标准化后0.94"的真实对比,并搞懂min-max和Z-score该在什么时候用哪个;
  • 学会用GridSearchCV画K值调参曲线,回答"K到底取几"这个面试高频题。 预计阅读时间:15~18分钟
  • 本文是《机器学习-算法模型系列》第 4 篇。上一篇(本系列第3篇)我们讲了逻辑回归——一个靠学习权重参数来画决策边界的模型。这一篇的主角KNN画风完全不同:它压根不学任何参数,靠"抄邻居作业"就能做分类。但正因为它全靠距离说话,一旦某个特征的数值量级碾压其他特征,整个模型就会被这个特征绑架——这就是标题里说的"独裁"。这篇文章会带你亲眼看到独裁现场,再亲手把它掀翻。

    一、从"选电影"说起:KNN的朴素直觉

    K近邻算法(K-Nearest Neighbors,简称KNN)可能是所有机器学习算法里最符合人类直觉的一个。它的核心思想一句话就能说完:在已有数据中寻找与新样本最相似(离它最近)的K个数据,这K个数据里哪个类别占多数,新样本就判为哪个类别。

    说白了就是"物以类聚":你想知道一个人爱不爱打游戏,看看他最常混在一起的几个朋友就大概知道了。

    拿一个经典的电影分类例子来说。假设平面上散落着两类点:五角星代表爱情片,三角形代表科幻片,每部电影按"打斗镜头数"和"接吻镜头数"两个特征落在坐标系里。现在来了一部新电影(一个正方形的点),它是爱情片还是科幻片?

    KNN的做法是画个圈,看新样本身边最近的K个点:

    • 取K=3:离新样本最近的3个点里,有1个五角星、2个三角形。少数服从多数,三角形赢,新电影判为科幻片;
    • 取K=5:把圈再扩大一点,最近的5个点变成了3个五角星、2个三角形。这回五角星反超,同一部电影又被判成了爱情片。

    同一个样本、同一批数据,K从3变到5,结论直接翻转。这个例子想告诉你两件事:一是KNN的判类机制就是这么朴素的投票;二是K不是随便拍脑袋定的,它直接左右预测结果——这个坑我们在第七节用调参曲线专门解决。

    整个预测流程可以画成一张图:

    #mermaid-svg-NkvybSRxKzJTblrd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NkvybSRxKzJTblrd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NkvybSRxKzJTblrd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NkvybSRxKzJTblrd .error-icon{fill:#552222;}#mermaid-svg-NkvybSRxKzJTblrd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NkvybSRxKzJTblrd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NkvybSRxKzJTblrd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NkvybSRxKzJTblrd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NkvybSRxKzJTblrd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NkvybSRxKzJTblrd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NkvybSRxKzJTblrd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NkvybSRxKzJTblrd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NkvybSRxKzJTblrd .marker.cross{stroke:#333333;}#mermaid-svg-NkvybSRxKzJTblrd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NkvybSRxKzJTblrd p{margin:0;}#mermaid-svg-NkvybSRxKzJTblrd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NkvybSRxKzJTblrd .cluster-label text{fill:#333;}#mermaid-svg-NkvybSRxKzJTblrd .cluster-label span{color:#333;}#mermaid-svg-NkvybSRxKzJTblrd .cluster-label span p{background-color:transparent;}#mermaid-svg-NkvybSRxKzJTblrd .label text,#mermaid-svg-NkvybSRxKzJTblrd span{fill:#333;color:#333;}#mermaid-svg-NkvybSRxKzJTblrd .node rect,#mermaid-svg-NkvybSRxKzJTblrd .node circle,#mermaid-svg-NkvybSRxKzJTblrd .node ellipse,#mermaid-svg-NkvybSRxKzJTblrd .node polygon,#mermaid-svg-NkvybSRxKzJTblrd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NkvybSRxKzJTblrd .rough-node .label text,#mermaid-svg-NkvybSRxKzJTblrd .node .label text,#mermaid-svg-NkvybSRxKzJTblrd .image-shape .label,#mermaid-svg-NkvybSRxKzJTblrd .icon-shape .label{text-anchor:middle;}#mermaid-svg-NkvybSRxKzJTblrd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NkvybSRxKzJTblrd .rough-node .label,#mermaid-svg-NkvybSRxKzJTblrd .node .label,#mermaid-svg-NkvybSRxKzJTblrd .image-shape .label,#mermaid-svg-NkvybSRxKzJTblrd .icon-shape .label{text-align:center;}#mermaid-svg-NkvybSRxKzJTblrd .node.clickable{cursor:pointer;}#mermaid-svg-NkvybSRxKzJTblrd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NkvybSRxKzJTblrd .arrowheadPath{fill:#333333;}#mermaid-svg-NkvybSRxKzJTblrd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NkvybSRxKzJTblrd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NkvybSRxKzJTblrd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NkvybSRxKzJTblrd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NkvybSRxKzJTblrd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NkvybSRxKzJTblrd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NkvybSRxKzJTblrd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NkvybSRxKzJTblrd .cluster text{fill:#333;}#mermaid-svg-NkvybSRxKzJTblrd .cluster span{color:#333;}#mermaid-svg-NkvybSRxKzJTblrd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NkvybSRxKzJTblrd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NkvybSRxKzJTblrd rect.text{fill:none;stroke-width:0;}#mermaid-svg-NkvybSRxKzJTblrd .icon-shape,#mermaid-svg-NkvybSRxKzJTblrd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NkvybSRxKzJTblrd .icon-shape p,#mermaid-svg-NkvybSRxKzJTblrd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NkvybSRxKzJTblrd .icon-shape .label rect,#mermaid-svg-NkvybSRxKzJTblrd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NkvybSRxKzJTblrd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NkvybSRxKzJTblrd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NkvybSRxKzJTblrd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    类别0占多数

    类别1占多数

    前提

    新样本到来

    计算新样本与所有已知样本的距离

    按距离从近到远排序

    取出最近的K个邻居

    K个邻居中哪个类别占多数?

    新样本判为类别0

    新样本判为类别1

    ⚠️ 各特征量纲相近否则距离会被大数值特征独裁

    注意图里那个虚线警告框,它是本文的题眼,第四节会正面撞上它。

    另外提一句:KNN属于惰性学习(lazy learning)。逻辑回归训练完会得到一组权重,训练数据可以扔掉;KNN的"训练"却什么都不算,就是把数据原样存起来,所有计算都推迟到预测那一刻。这个特性决定了它训练飞快、预测偏慢,后面讲优缺点时你会再遇到它。

    二、距离怎么算:从两点公式到n维空间

    “最近的K个邻居”——那"近"怎么定义?KNN里最常用的是欧氏距离,也就是你初中就学过的两点间距离公式。

    二维空间里,样本A的两个特征值为

    (

    X

    1

    ,

    Y

    1

    )

    (X_1, Y_1)

    (X1,Y1),样本B的两个特征值为

    (

    X

    2

    ,

    Y

    2

    )

    (X_2, Y_2)

    (X2,Y2),两个样本的距离是:

    d

    (

    A

    ,

    B

    )

    =

    (

    X

    1

    X

    2

    )

    2

    +

    (

    Y

    1

    Y

    2

    )

    2

    d(A, B) = \\sqrt{(X_1 – X_2)^2 + (Y_1 – Y_2)^2}

    d(A,B)=(X1X2)2+(Y1Y2)2

    几何上看,就是把两个特征的差值当作直角三角形的两条直角边,距离就是斜边长。它只适用于样本恰好有2个特征变量的情况。

    实际业务里特征动辄十几个、几十个,此时把公式推广到n维空间即可:设n维空间内A点坐标为

    (

    X

    1

    ,

    X

    2

    ,

    X

    3

    ,

    ,

    X

    n

    )

    (X_1, X_2, X_3, \\dots, X_n)

    (X1,X2,X3,,Xn),B点坐标为

    (

    Y

    1

    ,

    Y

    2

    ,

    Y

    3

    ,

    ,

    Y

    n

    )

    (Y_1, Y_2, Y_3, \\dots, Y_n)

    (Y1,Y2,Y3,,Yn),则A、B两点间的欧氏距离为:

    d

    (

    A

    ,

    B

    )

    =

    (

    X

    1

    Y

    1

    )

    2

    +

    (

    X

    2

    Y

    2

    )

    2

    +

    +

    (

    X

    n

    Y

    n

    )

    2

    =

    i

    =

    1

    n

    (

    X

    i

    Y

    i

    )

    2

    d(A, B) = \\sqrt{(X_1 – Y_1)^2 + (X_2 – Y_2)^2 + \\cdots + (X_n – Y_n)^2} = \\sqrt{\\sum_{i=1}^{n}(X_i – Y_i)^2}

    d(A,B)=(X1Y1)2+(X2Y2)2++(XnYn)2

    =i=1n(XiYi)2

    公式长得吓人,本质没变:每个特征各自算差值、平方、加起来、开根号。你可以把它想象成在n维空间里拉一根直线绳子量长度。

    盯着这个公式多看两眼,你其实已经能预感到危险了:每个特征的差值是平方后直接相加的。如果某个特征的数值天生比别人大一个数量级,它的平方差就会大两个数量级,求和时其他特征的贡献直接被淹没——这就是"独裁"的数学根源。先按下不表,我们把正常流程走一遍。

    三、5瓶葡萄酒,一张草稿纸:手算KNN全过程

    现在用一个"判断葡萄酒种类"的例子把KNN的计算步骤完整走一遍。商业实战中评判葡萄酒的指标很多,为了能在草稿纸上算完,这里只用"酒精含量"和"苹果酸含量"2个特征变量,把葡萄酒分为2类:分类0代表葡萄酒A,分类1代表葡萄酒B。原始样本只有5组:

    样本酒精含量(%)苹果酸含量(%)分类
    样本1 5 2 0(葡萄酒A)
    样本2 6 1 0(葡萄酒A)
    样本3 8 3 1(葡萄酒B)
    样本4 10 2 1(葡萄酒B)
    样本5 12 1 1(葡萄酒B)

    现在来了一个新样本:酒精含量7%、苹果酸含量1%,它属于葡萄酒A还是葡萄酒B?

    第一步,用欧氏距离公式算新样本和每个已知样本的距离,也就是不同样本间的相似度。以样本1为例:

    d

    =

    (

    7

    5

    )

    2

    +

    (

    1

    2

    )

    2

    =

    4

    +

    1

    =

    5

    2.24

    d = \\sqrt{(7-5)^2 + (1-2)^2} = \\sqrt{4+1} = \\sqrt{5} \\approx 2.24

    d=(75)2+(12)2

    =4+1

    =5

    2.24

    同样的算法套在其余4个样本上,5个距离全部算出来(这些数字我都用numpy验算过,你可以放心跟着草稿纸核对):

    目标计算式距离
    新样本 → 样本1

    (

    7

    5

    )

    2

    +

    (

    1

    2

    )

    2

    =

    5

    \\sqrt{(7-5)^2+(1-2)^2}=\\sqrt{5}

    (75)2+(12)2

    =5

    ≈ 2.24
    新样本 → 样本2

    (

    7

    6

    )

    2

    +

    (

    1

    1

    )

    2

    =

    1

    \\sqrt{(7-6)^2+(1-1)^2}=\\sqrt{1}

    (76)2+(11)2

    =1

    = 1.00
    新样本 → 样本3

    (

    7

    8

    )

    2

    +

    (

    1

    3

    )

    2

    =

    5

    \\sqrt{(7-8)^2+(1-3)^2}=\\sqrt{5}

    (78)2+(13)2

    =5

    ≈ 2.24
    新样本 → 样本4

    (

    7

    10

    )

    2

    +

    (

    1

    2

    )

    2

    =

    10

    \\sqrt{(7-10)^2+(1-2)^2}=\\sqrt{10}

    (710)2+(12)2

    =10

    ≈ 3.16
    新样本 → 样本5

    (

    7

    12

    )

    2

    +

    (

    1

    1

    )

    2

    =

    25

    \\sqrt{(7-12)^2+(1-1)^2}=\\sqrt{25}

    (712)2+(11)2

    =25

    = 5.00

    第二步,按距离由近及远排序:

    名次样本距离该样本的分类
    1 样本2 1.00 0(葡萄酒A)
    2(并列) 样本1 2.24 0(葡萄酒A)
    2(并列) 样本3 2.24 1(葡萄酒B)
    4 样本4 3.16 1(葡萄酒B)
    5 样本5 5.00 1(葡萄酒B)

    (样本1和样本3恰好等距,实际工程里这种并列由排序的先后顺序决定谁排前面,对本例的结论没有影响。)

    第三步,取K个邻居投票:

    • 令K=1:只看离得最近的1个样本,也就是样本2,它的分类是0,所以新样本判为分类0,葡萄酒A;
    • 令K=3:最近的3个样本是样本2、样本1、样本3,分类分别是0、0、1,分类0居多,新样本仍判为分类0,葡萄酒A。

    到这里,KNN的完整计算步骤你已经全部亲手走过一遍了:算距离 → 排序 → 投票。没有梯度下降,没有损失函数,就这么直白。

    用代码验证一下草稿纸上的结果:

    import numpy as np

    # 5个已知样本:[酒精含量, 苹果酸含量],标签0=葡萄酒A,1=葡萄酒B
    X = np.array([[5, 2], [6, 1], [8, 3], [10, 2], [12, 1]])
    y = np.array([0, 0, 1, 1, 1])
    new = np.array([7, 1]) # 待判类的新样本

    d = np.sqrt(((X new) ** 2).sum(axis=1)) # 一行算出全部欧氏距离
    order = np.argsort(d) # 按距离从近到远排出样本下标

    for rank, idx in enumerate(order, 1):
    print(f"第{rank}近: 样本{idx+1} 距离={d[idx]:.2f} 分类={y[idx]}")

    for K in (1, 3):
    votes = y[order[:K]] # 取最近K个邻居的标签
    result = np.bincount(votes).argmax() # 少数服从多数
    print(f"K={K} 时投票结果: 分类{result}")

    # 运行输出:
    # 第1近: 样本2 距离=1.00 分类=0
    # 第2近: 样本1 距离=2.24 分类=0
    # 第3近: 样本3 距离=2.24 分类=1
    # 第4近: 样本4 距离=3.16 分类=1
    # 第5近: 样本5 距离=5.00 分类=1
    # K=1 时投票结果: 分类0
    # K=3 时投票结果: 分类0

    结论和手算完全一致。目前为止一切岁月静好——因为酒精含量和苹果酸含量都是个位数,两个特征的量纲级别相差不大,谁也压不住谁。接下来我们把这份平衡打破。

    四、把酒精含量放大10倍:一个特征的"独裁"现场

    设想一个非常常见的场景:数据换了个统计口径,“酒精含量"的数值都放大为原来的10倍(比如从"百分比"换成了"千分比”),"苹果酸含量"保持不变。数据变成了这样:

    样本酒精含量(‰)苹果酸含量(%)分类
    样本1 50 2 0
    样本2 60 1 0
    样本3 80 3 1
    样本4 100 2 1
    样本5 120 1 1

    两个特征的量纲级别一下子拉开了:一个在50120之间蹦跶,一个还在13之间徘徊。此时如果直接拿去搭KNN模型,会发生什么?

    对应的新样本变成酒精含量70、苹果酸含量1,算一下它与样本1的距离:

    d

    =

    (

    70

    50

    )

    2

    +

    (

    1

    2

    )

    2

    =

    400

    +

    1

    =

    401

    20.02

    d = \\sqrt{(70-50)^2 + (1-2)^2} = \\sqrt{400 + 1} = \\sqrt{401} \\approx 20.02

    d=(7050)2+(12)2

    =400+1

    =401

    20.02

    看清楚这笔账:酒精含量贡献了400,苹果酸含量只贡献了1。这个距离几乎完全由酒精含量主导,苹果酸含量因为量纲级别相差较大,几乎不发挥作用。 你算不算它,距离都是20左右——苹果酸这个特征等于被开除了。“酒精含量"在模型中的重要性远远超过"苹果酸含量”,后者这一特征变量的作用就此丧失,预测结果自然有失偏颇。

    更隐蔽的伤害在排序上。把5个距离都算出来对比一下:

    目标原始数据的距离放大10倍后的距离
    新样本 → 样本1 2.24(并列第2近) 20.02(跌到第3近)
    新样本 → 样本2 1.00(第1近) 10.00(第1近)
    新样本 → 样本3 2.24(并列第2近) 10.20(升到第2近)
    新样本 → 样本4 3.16 30.02
    新样本 → 样本5 5.00 50.00

    原始数据里,样本1和样本3与新样本等距,苹果酸含量的差异是有话语权的;放大之后,样本1(酒精差20)被样本3(酒精差10)远远甩开,邻居的名次完全按酒精含量的差值重排,苹果酸含量彻底失声。本例中投票结果侥幸没变,但只要数据再多一点、类别分布再犬牙交错一点,这种名次重排就会直接改写预测结果。

    这就是"独裁"的完整现场:不是酒精含量这个特征有多重要,而是它的数值量级大,就抢走了全部话语权。模型没报错、代码正常跑、准确率也能输出——只是悄悄变成了一个"只看酒精含量的KNN"。这类静默失效比报错可怕得多。

    如果不同特征变量的量纲级别相差较大、且在建模时相互影响(KNN、K-Means、SVM这类靠距离吃饭的算法全中招),就必须先做数据预处理。这个手段叫数据标准化,也叫数据归一化。

    五、两种标准化:min-max与Z-score

    数据标准化的常见方法有两种:min-max标准化和Z-score标准化。

    5.1 min-max标准化(离差标准化)

    把每个特征线性压缩到

    [

    0

    ,

    1

    ]

    [0, 1]

    [0,1] 区间:

    x

    =

    x

    x

    m

    i

    n

    x

    m

    a

    x

    x

    m

    i

    n

    x' = \\frac{x – x_{min}}{x_{max} – x_{min}}

    x=xmaxxminxxmin

    其中

    x

    m

    i

    n

    x_{min}

    xmin

    x

    m

    a

    x

    x_{max}

    xmax 分别是该特征在训练集里的最小值和最大值。拿放大后的酒精含量列

    [

    50

    ,

    60

    ,

    80

    ,

    100

    ,

    120

    ]

    [50, 60, 80, 100, 120]

    [50,60,80,100,120] 套一下:

    x

    m

    i

    n

    =

    50

    x_{min}=50

    xmin=50

    x

    m

    a

    x

    =

    120

    x_{max}=120

    xmax=120,极差70,变换后得到

    [

    0

    ,

    0.143

    ,

    0.429

    ,

    0.714

    ,

    1

    ]

    [0, 0.143, 0.429, 0.714, 1]

    [0,0.143,0.429,0.714,1]。苹果酸含量列

    [

    2

    ,

    1

    ,

    3

    ,

    2

    ,

    1

    ]

    [2,1,3,2,1]

    [2,1,3,2,1] 同样处理后得到

    [

    0.5

    ,

    0

    ,

    1

    ,

    0.5

    ,

    0

    ]

    [0.5, 0, 1, 0.5, 0]

    [0.5,0,1,0.5,0]。两个特征全部回到0~1的同一起跑线,谁也别想再独裁。

    5.2 Z-score标准化(均值归一化)

    把每个特征变换成均值为0、标准差为1的分布:

    x

    =

    x

    μ

    σ

    x' = \\frac{x – \\mu}{\\sigma}

    x=σxμ

    其中

    μ

    \\mu

    μ 是该特征的均值,

    σ

    \\sigma

    σ 是标准差。还是酒精含量列

    [

    50

    ,

    60

    ,

    80

    ,

    100

    ,

    120

    ]

    [50, 60, 80, 100, 120]

    [50,60,80,100,120]:均值

    μ

    =

    82

    \\mu = 82

    μ=82,标准差

    σ

    25.61

    \\sigma \\approx 25.61

    σ25.61,变换后得到

    [

    1.25

    ,

    0.86

    ,

    0.08

    ,

    0.70

    ,

    1.48

    ]

    [-1.25, -0.86, -0.08, 0.70, 1.48]

    [1.25,0.86,0.08,0.70,1.48]。数值有正有负,含义是"偏离平均水平几个标准差"——考试里的"你超过了全班多少人"就是这个思路。

    5.3 怎么选?一张表说清

    对比维度min-max标准化Z-score标准化
    结果范围 固定压缩到

    [

    0

    ,

    1

    ]

    [0,1]

    [0,1]

    均值0、标准差1,无固定边界
    对离群值 敏感(一个极端值就把其他数据挤成一团) 相对稳健
    适用场景 数据分布有明确边界、无明显离群值 数据近似正态分布或存在离群值
    sklearn实现 MinMaxScaler StandardScaler
    新手默认选 图像像素等天然有界数据 拿不准就用它

    一个工程细节必须在这里敲黑板:标准化的参数(min、max、均值、标准差)只能从训练集上计算,再套用到测试集上。也就是先在训练集上 fit,再对训练集和测试集分别 transform。如果拿全量数据一起算,测试集的信息就提前泄露给了模型,评估结果会虚高。第九节的踩坑实录里有这个坑的完整复现方式。

    六、sklearn实战:wine数据集上的对比实验

    道理讲完了,上真实数据验货。sklearn自带的wine数据集(load_wine())简直是为这篇文章量身定做的:178个葡萄酒样本、13个特征、3个品种类别,正好是第三节手算案例的"工业放大版"。它的特征量纲有多悬殊?"脯氨酸"的取值范围是2781680,而"苹果酸"只有0.745.8,量级差了三个数量级——第四节那个×10的玩具例子,在真实数据里是×300。

    实验设计很简单:同一份数据、同一个KNN模型(K=5),唯一的变量是做不做标准化,比较测试集准确率。

    from sklearn.datasets import load_wine
    from sklearn.model_selection import train_test_split
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.preprocessing import StandardScaler, MinMaxScaler
    from sklearn.metrics import accuracy_score

    X, y = load_wine(return_X_y=True) # 178个样本,13个特征,3个类别
    X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y)

    # 方案一:原始数据直接跑KNN(错误示范)
    knn = KNeighborsClassifier(n_neighbors=5)
    knn.fit(X_train, y_train)
    print("不标准化:", accuracy_score(y_test, knn.predict(X_test)))

    # 方案二:Z-score标准化后再跑(注意只在训练集上fit)
    sc = StandardScaler().fit(X_train) # 均值和标准差只从训练集学
    knn_std = KNeighborsClassifier(n_neighbors=5)
    knn_std.fit(sc.transform(X_train), y_train)
    print("Z-score标准化:",
    accuracy_score(y_test, knn_std.predict(sc.transform(X_test))))

    # 方案三:min-max标准化后再跑
    mm = MinMaxScaler().fit(X_train)
    knn_mm = KNeighborsClassifier(n_neighbors=5)
    knn_mm.fit(mm.transform(X_train), y_train)
    print("min-max标准化:",
    accuracy_score(y_test, knn_mm.predict(mm.transform(X_test))))

    # 运行输出:
    # 不标准化: 0.7222222222222222
    # Z-score标准化: 0.9444444444444444
    # min-max标准化: 0.9629629629629629

    结果汇总(实测数字,sklearn 1.6、random_state=42,你可以逐字复现):

    方案预处理测试集准确率相对提升
    方案一 无(原始数据) 0.7222
    方案二 Z-score标准化 0.9444 +22.2个百分点
    方案三 min-max标准化 0.9630 +24.1个百分点

    同一个模型、同一份数据、同一个K,只是加了两行标准化代码,准确率从0.72跳到0.94以上。反过来读这张表更吓人:不做标准化,你亲手把一个94分的模型开成了72分,而且它不报任何错,你若不做这组对照实验,可能永远不知道自己损失了什么。

    0.72是什么概念?wine数据集三个类别的样本数是59/71/48,全猜最大类也有0.40的准确率。也就是说,13个特征被"脯氨酸"们独裁之后,模型只比瞎猜聪明了一半。

    七、K值怎么选:GridSearchCV调参曲线

    标准化解决了"特征话语权"的问题,还剩下开头挖的那个坑:K取几?第一节的电影例子已经演示过K=3和K=5给出相反结论。K太小,模型跟着个别噪声点起舞,容易过拟合;K太大,连很远的"假邻居"都拉进来投票,决策边界被抹平,容易欠拟合。极端情况下K=样本总数,那所有新样本都会被判成占比最高的类别,模型退化成"报菜名"。

    正经做法是把K当超参数,用交叉验证扫一遍:

    from sklearn.datasets import load_wine
    from sklearn.model_selection import train_test_split, GridSearchCV
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.preprocessing import StandardScaler
    from sklearn.pipeline import make_pipeline

    X, y = load_wine(return_X_y=True)
    X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y)

    # Pipeline把标准化和KNN捆在一起:交叉验证的每一折都自动
    # 只在训练折上fit标准化参数,从机制上杜绝数据泄露
    pipe = make_pipeline(StandardScaler(), KNeighborsClassifier())
    param_grid = {"kneighborsclassifier__n_neighbors": range(1, 21)} # K从1扫到20

    gs = GridSearchCV(pipe, param_grid, cv=5, scoring="accuracy") # 5折交叉验证
    gs.fit(X_train, y_train)

    print("最优K:", gs.best_params_["kneighborsclassifier__n_neighbors"])
    print("交叉验证最高分:", round(gs.best_score_, 4))
    print("测试集分数:", round(gs.score(X_test, y_test), 4))
    for k, s in zip(range(1, 21), gs.cv_results_["mean_test_score"]):
    print(f"K={k:>2d} cv准确率={s:.4f}")

    # 运行输出(节选):
    # 最优K: 12
    # 交叉验证最高分: 0.9677
    # 测试集分数: 0.963
    # K= 1 cv准确率=0.9513
    # K= 2 cv准确率=0.9357
    # K= 5 cv准确率=0.9593
    # K=12 cv准确率=0.9677
    # K=20 cv准确率=0.9513

    把这20个点连起来就是K值调参曲线,形状很典型:K=1时0.9513,每个噪声点都握着一票否决权,分数被拖累;K=2反而跌到全场最低的0.9357——偶数K平票率高,是个经典小坑;K=4以后爬上0.959左右的平台期;K=12摸到峰值0.9677;再往后缓慢下滑,K=20回落到0.9513,欠拟合的苗头出来了。整条曲线是两头低、中间高的倒U形,你在自己的数据上扫出来大概率也长这样,选峰值附近的K即可。

    顺手把KNeighborsClassifier的常用参数整理成速查表:

    参数默认值作用调参建议
    n_neighbors 5 邻居数量K 核心参数,用交叉验证扫1~20,二分类倾向选奇数防平票
    weights 'uniform' 投票权重 'uniform'一人一票;'distance'离得越近票越重,噪声多时可试
    metric 'minkowski' 距离度量 配合p=2即欧氏距离(默认);p=1为曼哈顿距离
    p 2 闵可夫斯基距离的幂 高维稀疏数据可试p=1
    algorithm 'auto' 近邻搜索算法 一般不动,大数据量时'kd_tree'/'ball_tree'加速
    n_jobs None 并行数 预测慢时设-1吃满CPU

    八、KNN的优缺点、适用场景,以及2026年还学它干嘛

    维度内容
    优点 原理直白,几乎零门槛;无需训练过程(惰性学习),新增样本即插即用;天然支持多分类;对数据分布不做任何假设;决策边界可以任意复杂
    缺点 预测时要和全量训练数据算距离,样本量大时又慢又吃内存;对特征量纲极度敏感(本文主题);高维数据下各点距离趋同,效果崩塌(维度灾难);对类别不平衡敏感,多数类容易霸占投票
    适合场景 中小规模、低维、特征物理含义相近的数据;推荐系统里"找相似用户/相似物品"的召回;异常检测(离所有邻居都远的就是异常点);新项目的快速baseline
    不适合场景 百万级以上样本的在线实时预测;几百维以上的稀疏特征(如文本one-hot);特征量纲混乱又没条件做预处理的数据管道

    到2026年的今天还要不要学KNN?要。大模型抢走的是非结构化数据的活儿,而银行风控里的邻域欺诈检测、推荐系统粗排阶段的"相似用户召回"、工业质检里"新工件与历史合格品的相似度比对",本质上都是KNN思想在生产环境的变体——向量数据库里的近似最近邻检索(ANN),就是KNN为了对付海量数据演化出的工程形态。你现在做RAG检索,底层逻辑和本文第三节的手算流程是同一件事:算距离、排序、取Top-K。学透这篇,你学的不是一个老算法,是一整类"相似度检索"问题的地基。

    九、踩坑实录:新手最容易犯的4个错误

    #错误现象原因修正做法
    1 准确率莫名偏低(如wine上只有0.72),换模型也没好转 忘了标准化,距离被大量纲特征独裁 训练前加StandardScaler,KNN/K-Means/SVM一律先标准化
    2 交叉验证分数很高,上线后效果拉胯 在全量数据上fit了Scaler,测试信息泄露 只在训练集上fit,或直接用Pipeline捆绑
    3 二分类任务里预测结果时好时坏、难以复现 K取了偶数,平票时靠邻居顺序裁决 二分类取奇数K,或用weights='distance'
    4 训练"秒完成"于是拿百万级数据直接上线 误以为训练快=预测快,KNN的开销全在预测 大数据量换kd_tree/ball_tree、做ANN近似检索或干脆换模型

    第2个坑展开说两句,因为它最隐蔽。错误写法是scaler.fit(X)之后再切分训练/测试集——测试集的均值方差信息已经混进了标准化参数,模型等于提前偷看了考卷,交叉验证分数虚高,上线见光死。修正代码就是第七节里的make_pipeline(StandardScaler(), KNeighborsClassifier()):Pipeline会保证每一折交叉验证都只用训练折的统计量,想泄露都难。

    第3个坑你在第七节的曲线里已经亲眼见过了:K=2的分数比K=1和K=3都低,不是巧合,是平票裁决在捣乱。

    十、参考资料

  • scikit-learn官方文档·最近邻算法:https://scikit-learn.org/stable/modules/neighbors.html
  • 维基百科·k-nearest neighbors algorithm:https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm
  • 周志华,《机器学习》(西瓜书),清华大学出版社——第10章"降维与度量学习"对k近邻学习器与维度灾难有系统论述。
  • 十一、互动引导

    三个问题,欢迎在评论区聊聊:

  • 你在自己的项目里遇到过"特征独裁"吗?是哪个特征、量级差了多少,标准化之后指标变化了多少?
  • 文中K=2的交叉验证分数低于K=1和K=3,除了平票问题,你觉得还有别的解释吗?
  • 如果两个特征量纲相同、但其中一个其实和标签毫无关系(纯噪声特征),标准化救得了KNN吗?该用什么办法处理?
  • 觉得这篇把"标准化为什么是KNN的救命稻草"讲透了的话,点赞、收藏、关注本专栏三连走一波。下一篇(本系列第5篇)我们进入树模型的世界:《一个数字决定分裂生死:决策树的基尼系数与信息熵对决》——同样是"挑特征",决策树用的是完全不同的一把尺子,不见不散。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 机器学习-算法模型系列文章:04-KNN 不做标准化就跑KNN?你的模型正在被一个特征“独裁“
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!