系列第四篇。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−xminx−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+FPTPRecall=TP+FNTPF1=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,符合"评估要暴露短板"的目的。