欢迎光临
我们一直在努力

机器学习学习笔记(四):KNN——距离、特征预处理与交叉验证

系列第四篇。KNN 是思想最简单的算法(“看邻居投票”),但它带出了两个重量级话题:特征预处理(把概述篇欠的归一化/标准化公式账还上)和分类评估体系(混淆矩阵、精确率、召回率、F1),后面所有分类问题都离不开这套东西。

本篇看完要回答的四个面试题:

  • K 值过大或过小会怎样?
  • 归一化和标准化的公式与适用场景?
  • 精确率和召回率的区别?什么场景优先保召回率?
  • F1-score 为什么用调和平均而不是算术平均?

  • 一、KNN:看邻居投票

    K-近邻算法(K Nearest Neighbor)的核心思想一句话:一个样本在特征空间中 k 个最相似的样本,大多数属于某个类别,那它就属于这个类别。

    "相似"用什么衡量?当然是距离。同一任务数据集中,距离越近的样本越相似。距离用欧氏距离:

    d(x,y)=∑i=1n(xi−yi)2d(x, y) = \\sqrt{\\sum_{i=1}^{n}(x_i – y_i)^2}d(x,y)=i=1∑n​(xi​−yi​)2​

    课件的例子很直观:用"搞笑镜头数、拥抱镜头数、打斗镜头数"三个特征给电影分类。《唐人街探案》和《伦敦陷落》的距离:

    d=(2−3)2+(3−3)2+(55−17)2=42.42d = \\sqrt{(2-3)^2 + (3-3)^2 + (55-17)^2} = 42.42d=(2−3)2+(3−3)2+(55−17)2​=42.42

    分别算出它和 9 部已知类型电影的距离,取最近的 K=5 部,里面动作片占多数,就判它是动作片。

    分类和回归的流程只有最后一步不同:

  • 计算未知样本到每一个训练样本的距离
  • 按距离升序排列
  • 取出最近的 K 个样本
  • 分类问题做多数表决(票数最多的类别胜出);回归问题取 K 个样本目标值的均值
  • API 对应两个:分类用 KNeighborsClassifier,回归用 KNeighborsRegressor。

    二、K 值选择:过犹不及

    K 是 KNN 唯一的核心超参数,选择逻辑就是对欠拟合/过拟合的直接应用:

    K 值模型风险
    K 太小 模型变复杂,决策只看极小邻域 容易受异常点影响,过拟合
    K 合适 模型复杂度适配 ——
    K 太大 模型变简单,邻域大到"远亲也来投票" 容易欠拟合
    K = N(全部样本) 无论输入什么,永远预测训练集中样本最多的类别 完全失效,且受样本均衡影响

    K=N 的极端情况最好记:全体邻居投票,永远赢的是多数类,模型的判断力彻底归零。

    实际怎么选 K? 交给交叉验证 + 网格搜索,本文第六节实战。

    三、特征预处理:归一化与标准化(公式还账)

    概述篇给过结论(优先标准化),这篇把公式和实测补全。

    为什么需要预处理? 特征的单位或数值范围相差悬殊时(身高 1.75 米、体重 70 千克、视力 0.2~2.0),数值大的特征会支配距离计算。KNN 尤其敏感,因为它的每一步都建立在距离上。

    3.1 归一化(MinMaxScaler)

    把数据压缩到固定区间 [mi,mx][mi, mx][mi,mx](默认 [0,1][0, 1][0,1]):

    x′=x−xminxmax−xminx' = \\frac{x – x_{min}}{x_{max} – x_{min}}x′=xmax​−xmin​x−xmin​​

    最大值最小值非常容易受异常点影响,鲁棒性差,只适合传统精确小数据场景。

    3.2 标准化(StandardScaler)

    把数据转成均值 0、标准差 1 的分布:

    x′=x−μσx' = \\frac{x – \\mu}{\\sigma}x′=σx−μ​

    其中 μ\\muμ 是特征均值,σ\\sigmaσ 是标准差(方差开根号,方差衡量数据的离散程度)。异常点通过影响均值和标准差间接影响结果,但当样本数量较大时,单点对均值和标准差的影响可以忽略,所以标准化鲁棒得多。

    3.3 异常值实验:一 Compare 见高下

    拿身高/体重/视力数据做实验,先看正常情况下的转换结果,再混入一个体重 300kg 的异常样本,对比两个缩放器的表现。

    import numpy as np
    from sklearn.preprocessing import MinMaxScaler, StandardScaler

    data = np.array([
    [1.70, 67, 1.5],
    [1.71, 80, 0.8],
    [1.75, 70, 1.5],
    [1.76, 68, 1.2],
    ])
    print(np.round(MinMaxScaler().fit_transform(data), 4))
    print(np.round(StandardScaler().fit_transform(data), 4))

    正常运行结果:

    归一化后:
    [[0. 0. 1. ]
    [0.1667 1. 0. ]
    [0.8333 0.2308 1. ]
    [1. 0.0769 0.5714]]

    标准化后:
    [[-1.1767 -0.8227 0.8704]
    [-0.7845 1.6938 -1.5667]
    [ 0.7845 -0.242 0.8704]
    [ 1.1767 -0.6291 -0.1741]]

    归一化后所有值严格落在 [0,1];标准化后围绕 0 波动、量级统一。然后是关键实验:加入体重 300kg 的异常样本:

    data_outlier = np.array([
    [1.70, 67, 1.5],
    [1.71, 80, 0.8],
    [1.75, 70, 1.5],
    [1.76, 68, 1.2],
    [1.72, 300, 1.3],
    ])

    我的运行结果:

    归一化后(正常样本的体重列):
    [0. 0.0558 0.0129 0.0043 1. ]

    标准化后(正常样本的体重列):
    [-0.5458 -0.4039 -0.513 -0.5348 1.9975]

    一目了然:一个异常点把归一化后正常样本的体重列压进了 0.004~0.056 的窄缝里,特征区分度几乎归零;标准化后正常样本仍在 -0.4~-0.55 之间保持原有排序和间隔。这就是"优先标准化"的完整证据链。

    四、案例:鸢尾花分类

    鸢尾花数据集:150 个样本、4 个特征(花萼/花瓣的长宽,单位 cm)、3 个类别(setosa / versicolor / virginica),是 sklearn 自带的入门标准数据集。

    from sklearn.datasets import load_iris
    from sklearn.model_selection import train_test_split, GridSearchCV
    from sklearn.preprocessing import StandardScaler
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.metrics import confusion_matrix, classification_report

    dataset = load_iris()
    x_train, x_test, y_train, y_test = train_test_split(
    dataset.data, dataset.target, test_size=0.2, random_state=22)

    scaler = StandardScaler()
    x_train_s = scaler.fit_transform(x_train)
    x_test_s = scaler.transform(x_test)

    knn = KNeighborsClassifier(n_neighbors=5)
    knn.fit(x_train_s, y_train)
    print(knn.score(x_test_s, y_test))

    我的运行结果(K=5,标准化后):准确率 0.9333,30 个测试样本错 2 个。

    一个诚实的意外发现值得单独说:我在同一划分上跑了不做标准化的对照实验,准确率反而是 0.9667。原因不难解释:鸢尾花 4 个特征本来就是同一量纲(都是 cm)、数值范围接近,距离计算本来就没被支配——标准化不是无脑加分项,它解决的是量纲问题,量纲本来一致时收益有限。真正需要它的是特征单位混杂的数据(比如房价数据的"年收入 30 万"和"房龄 15")。这个结论比"预处理永远要做"的教程套路更接近事实。

    五、交叉验证 + 网格搜索:让模型自己调参

    K 不能拍脑袋选,标准解法是两个工具的组合。

    交叉验证(Cross Validation):把训练集划分为 CV 份,每次拿 1 份做验证集、其余 CV-1 份做训练,轮换 CV 次,取 CV 次评估的平均值作为模型得分。好处是评估不依赖某一次幸运/倒霉的划分,结果更可信。

    网格搜索(Grid Search):把候选超参数排列组合,每组参数都跑一遍交叉验证,返回得分最高的参数组合。它俩在 sklearn 里被封装成一个 API:

    from sklearn.model_selection import GridSearchCV

    param_grid = {"n_neighbors": list(range(1, 21))}
    grid = GridSearchCV(KNeighborsClassifier(), param_grid, cv=5)
    grid.fit(x_train_s, y_train)
    print(grid.best_params_["n_neighbors"])
    print(grid.best_score_)

    我的运行结果:

    网格搜索最优K: 6
    交叉验证最高分: 0.9667
    最优模型在测试集上的准确率: 0.9333

    流程完整走了一遍:K 从 1 到 20 逐个试,每组做 5 折交叉验证(共训练 100 个模型),选出 K=6。注意最后一步,选出最优 K 后,要用全部训练数据重新训练模型,再到测试集上做最终评估(0.9333),交叉验证分数只用来选参数,不能代替测试集表现。

    六、分类评估:混淆矩阵与三大指标

    6.1 准确率的盲区

    准确率(预测对的 / 总数)有一个致命盲区。癌症检测场景:1000 人里只有 10 个患者,模型把所有人全判为健康,准确率 99%,但一个患者都没抓到。准确率高不等于模型有用,所以需要更细的评估体系。

    6.2 混淆矩阵

    以"恶性肿瘤"为正例,四个格子的定义:

    预测为正例预测为反例
    真实为正例 TP 真正例 FN 伪反例
    真实为反例 FP 伪正例 TN 真反例

    记忆技巧:前一个字母(T/F)表示"预测对没对",后一个字母(P/N)表示"预测成了什么"。TP + FN + FP + TN = 总样本数。

    用课件的例子实测:6 个恶性 + 4 个良性样本,两个模型的预测:

    from sklearn.metrics import confusion_matrix, accuracy_score
    from sklearn.metrics import precision_score, recall_score, f1_score

    y_true = ["恶性"] * 6 + ["良性"] * 4
    model_a = ["恶性", "恶性", "恶性", "良性", "良性", "良性",
    "良性", "良性", "良性", "良性"]
    model_b = ["恶性", "恶性", "恶性", "恶性", "恶性", "恶性",
    "良性", "良性", "良性", "恶性"]

    pre = precision_score(y_true, model_a, pos_label="恶性")
    rec = recall_score(y_true, model_a, pos_label="恶性")
    f1 = f1_score(y_true, model_a, pos_label="恶性")

    我的运行结果:

    指标模型 A模型 B
    准确率 0.7000 0.9000
    精确率 1.0000 0.8571
    召回率 0.5000 1.0000
    F1 0.6667 0.9231

    模型 A:判为恶性的 3 个全是真恶性(精确率 100%),但 6 个真恶性里只抓到 3 个(召回率 50%),它很"准"但不"全"。模型 B:6 个患者全抓到(召回率 100%),代价是把 1 个良性误判为恶性(精确率降到 85.7%),它很"全"但牺牲了一点"准"。

    6.3 精确率、召回率、F1

    Precision=TPTP+FPRecall=TPTP+FNF1=2×P×RP+RPrecision = \\frac{TP}{TP + FP} \\qquad Recall = \\frac{TP}{TP + FN} \\qquad F1 = \\frac{2 \\times P \\times R}{P + R}Precision=TP+FPTP​Recall=TP+FNTP​F1=P+R2×P×R​

    • 精确率(查准率):预测为正例的样本里,真对的有多少(“我说的恶性,有多少真是恶性”)
    • 召回率(查全率):真实的正例里,被抓到多少(“所有恶性患者,我抓到了几成”)
    • 优先保谁? 场景:癌症/诈骗检测优先召回率(漏掉一个患者的代价远大于误报);垃圾邮件拦截优先精确率(误杀正常邮件比漏进一封垃圾邮件烦得多)

    F1 为什么用调和平均? 算术平均会被高的一方"兜底":P=1.0、R=0.1 的模型算术平均还有 0.55,看起来凑合;调和平均是 2×1×0.1/1.1=0.18,直接暴露短板。调和平均惩罚偏科,只要有一项接近 0,F1 就接近 0,这符合"评估模型要找明显短板"的本意。

    手动算一遍课件练习:P=0.8、R=0.6 时,F1 = 2×0.48/1.4 ≈ 0.69。

    七、易错点

    • 预处理参数只能来自训练集:scaler.fit_transform(x_train) 之后测试集只能 transform。我把这一步写错过一次(对测试集重新 fit),评估分数直接从 0.93 掉到 0.47,教训是真的
    • 鸢尾花案例里标准化反而不加分:特征量纲本来就一致时,标准化的收益是零,结论要结合数据本身判断
    • 交叉验证分数高 ≠ 测试集分数高:交叉验证用于选参数,最终表现以测试集为准
    • 混淆矩阵四格的命名:T/F 看预测对没对,P/N 看预测成了什么,别背反了
    • precision_score 等默认 pos_label=1:标签是字符串时要显式传 pos_label="恶性",否则报错或算错

    八、知识清单

    • KNN 分类 API:KNeighborsClassifier(n_neighbors=5);回归:KNeighborsRegressor
    • 交叉验证:训练集分 CV 份轮流当验证集,取平均分;网格搜索:超参数排列组合 + 交叉验证,GridSearchCV(model, param_grid, cv=5)
    • 正态分布 N(μ,σ)N(\\mu, \\sigma)N(μ,σ):μ\\muμ 定位置、σ\\sigmaσ 定形状;3σ 法则:μ±3σ 覆盖 99.7% 的数据
    • 归一化:受最大最小值支配,鲁棒性差,适合小数据精确场景;标准化:大样本下对异常值稳健,优先选
    • 评估指标 API:confusion_matrix / accuracy_score / precision_score / recall_score / f1_score
    • F1 是精确率和召回率的调和平均,惩罚偏科

    九、面试高频问答

    Q1:K 值过大或过小会怎样?

    K 太小,模型复杂,决策只看极小邻域,容易被异常点带偏,过拟合;K 太大,模型简单,远处的"非同类"也参与投票,欠拟合;K 等于样本总数时永远预测多数类。实际用交叉验证 + 网格搜索选 K(我实测鸢尾花上 K=6 最优)。

    Q2:归一化和标准化的公式与选择?

    归一化 (x-min)/(max-min) 缩放到 [0,1],受最大最小值支配,一个异常点就能压垮正常样本的区分度;标准化 (x-μ)/σ 转成均值 0 方差 1,大样本下对异常值稳健。有异常值或配合梯度下降时优先标准化,需要固定范围(如图像像素)时用归一化。我在体重数据里混入一个 300kg 异常样本做过对比:归一化后正常样本被压进 0.004~0.056,标准化后依然保持区分。

    Q3:精确率和召回率的区别?什么场景优先召回率?

    精确率 = TP/(TP+FP),管"预测为正例的有多少是真的";召回率 = TP/(TP+FN),管"真实正例抓到了几成"。癌症检测、金融风控漏判代价极高,优先召回率;垃圾邮件拦截误杀代价高,优先精确率。

    Q4:F1 为什么用调和平均?

    调和平均惩罚偏科:P=1.0、R=0.1 的模型,算术平均有 0.55,调和平均只有 0.18。只要有一项接近 0,F1 就接近 0,符合"评估要暴露短板"的目的。

    赞(0)
    未经允许不得转载:171主机测评 » 机器学习学习笔记(四):KNN——距离、特征预处理与交叉验证
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址