KNN 算法

1, 关于 KNN

KNN (K-Nearest Neighbor), 即 K 近邻算法, K 个最近的邻居. 当需要预测一个未知样本的时候, 就由与该样本最近的 K 个邻居来决定

KNN 既可以用于分类, 也可用于回归. 用来分类时, 使用 K 个邻居中, 类别数量最多(或加权最多)者, 作为预测结果; 当用来回归分析时, 使用 K 个邻居的均值(或加权均值), 作为预测结果

KNN 算法的原理是: 样本映射到多维空间时, 相似度较高的样本, 距离也会较接近, “近朱者赤近墨者黑”

2, K 值

KNN 算法的 K 值是一个模型训练前就要人为指定的参数 超参数 , 不同于模型内部通过训练数据计算得到的参数. KNN 的超参数, 需要通常通过 交叉验证 的方式来选择最合适的参数组合

K 值的选择非常重要, K 值较小时, 模型预测依赖附近的邻居, 敏感性高, 稳定性低, 容易导致过拟合; 反之, K 值较大, 敏感性低, 稳定性高, 容易欠拟合

K 值在数据量小时, 可以通过遍历所有样本(穷举)的方式找出最近的 K 个邻居, 当数据量庞大时, 穷举耗费大量时间, 此时可以采用 KD树 来找 K 个邻居

3, 交叉验证

KNN 的网格搜索交叉验证: 取不同的 K, 选择不同的距离或权重计算方式等, 将数据分为多个组, 一个组作为测试集, 其他部分作为训练集, 不断循环训练和测试, 对模型进行循环验证, 找出最佳参数组合

4, 距离的度量方式

闵可夫斯基距离:

设 n 维空间中两个点位 X 和 Y:

$X=\left(x_{1}, x_{2}, \ldots \ldots, x_{n}\right)$ $Y=\left(y_{1}, y_{2}, \ldots \ldots, y_{n}\right)$

则阁可夫斯基距离为:

$D(X, Y)=\left(\sum_{i=1}^{n}\left|x_{i}-y_{i}\right|^{p}\right)^{1 / p}$

当 p 为 1 时, 又称 曼哈顿距离 ; 当 p 为 2 时, 称 欧几里得距离

5, 权重

统一权重: K 个邻居权重相同, 不管近远都是 1/K

距离加权权重: K 个邻居的权重, 与他们各自和待测样本的距离成反比, 同时要保证权重之和为 1. 例如 3 个邻居 a, b, c 距离待测样本的距离分别为 a, b 和 c, 则 a 的权重为:

$$\frac{\frac{1}{a}}{\frac{1}{a}+\frac{1}{b}+\frac{1}{c}}=\frac{b c}{b c+a c+a b}$$

b 和 c 同理

6, 数据标准化

样本中的特征通常非常多,由于各特征的性质不同,通常具有不同的量纲(数量级). 当各特征间的量纲相差很大时,如果直接用原始特征值进行分析,就会突出数值较高的特征在综合分析中的作用,相对削弱数值较低特征的作用, 因此需要通过数据标准化, 将量纲统一, 才能客观地描述各个特征对模型的影响程度

线性回归和逻辑回归, 都是通过每个特征与其权重的乘积相加来进行计算, 不进行数据标准化(不考虑正则化), 对每个特征的权重影响较大, 但对结果不会造成影响, 而 KNN 是基于距离计算的, 如果特征的量纲不同, 量纲较大的特征会占据主导地位, 导致忽略量纲较小的特征, 从而对模型性能造成较大影响

7, 算法实现步骤

a, 确定超参数
确定 K
确定距离度量方式
确定权重计算方式
其他超参数

b, 从训练集中选择距离待测样本最近的 K 个样本

c, 根据 K 个样本对待测样本进行预测, 如果遇到多个样本距离相同的情况, 默认选取训练集中靠前的

8, 流水线 Pipline

流水线可以将每个评估器视为一个步骤, 然后将多个步骤作为整体依次执行. 例如数据处理工作较多时, 可能涉及更多步骤, 例如多项式扩展, One-Hot 编码, 特征选择, 数据标准化, 交叉验证等, 分别执行过于繁琐, 我们可以将数据处理与模型训练各个步骤作为一个整体来执行

流水线具有最后一个评估器的所有方法:

a, 当流水线对象调用 fit 方法时, 会从第一个评估器依次调用 fit_transform 方法, 然后到最后一个评估器调用 fit 方法

b, 当流水线对象调用 其他 方法时, 会从第一个评估器依次调用 transform 方法, 然后到最后一个评估器调用 其他 方法

9, 以鸢尾花为例, 对逻辑回归和 KNN 进行比较:

from sklearn.model_selection import train_test_split  
from sklearn.neighbors import KNeighborsClassifier  
from sklearn.metrics import classification_report  
from sklearn.datasets import load_iris  
from sklearn.preprocessing import StandardScaler  
from sklearn.linear_model import LogisticRegression  
import matplotlib as mpl  
import matplotlib.pyplot as plt  
import warnings  
warnings.filterwarnings("ignore")  
import numpy as np  

mpl.rcParams["font.family"] = "SimHei"  
mpl.rcParams["axes.unicode_minus"] = False  

iris = load_iris()  
X = iris.data[:, :2]  
y = iris.target  
X_train, X_test, y_train, y_test = train_test_split(X, y,   
                        test_size=0.25, random_state=0)  

# 数据标准化: StandardScaler 均值标准差标准化, MinMaxScaler 最大最小值标准化  
ss = StandardScaler()  
X_train = ss.fit_transform(X_train)  
X_test = ss.transform(X_test)  

# 逻辑回归训练  
lr = LogisticRegression()  
lr.fit(X_train,y_train)  

# KNN 训练  
# n_neighbors: 邻居的数量  
# weights:权重计算方式, 可选值为 uniform 统一权重, 与 distance 加权权重  
knn = KNeighborsClassifier(n_neighbors=3, weights="uniform")  
knn.fit(X_train, y_train)  

# 比较 AUC  
from sklearn.metrics import roc_curve, auc,roc_auc_score  

lr_fpr, lr_tpr, lr_thresholds = roc_curve(y_test,  
                lr.predict_proba(X_test)[:,1], pos_label=1)  
lr_auc = auc(lr_fpr, lr_tpr)  
print('Logistic 算法: AUC = %.3f' % lr_auc)  

knn_fpr, knn_tpr, knn_thresholds = roc_curve(y_test,  
                knn.predict_proba(X_test)[:,1], pos_label=1)  
knn_auc = auc(knn_fpr, knn_tpr)  
print('KNN 算法: AUC = %.3f' % knn_auc) 
Logistic 算法: AUC = 0.835
KNN 算法: AUC = 0.794
# 将 KNN 算法参数进行调优再来比较  
from sklearn.model_selection import GridSearchCV  

# K 值取 1~10, 并定义需要的参数组合  
knn = KNeighborsClassifier()  
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}  

# 网格搜索交叉验证  
# param_grid:需要检验的超参数组合  
# scoring:模型评估标准, accuracy 正确率  
# n_jobs:并发数量  
# cv:交叉验证折数  
# verbose:输出冗余信息  
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='accuracy',  
                  n_jobs=-1, cv=5, verbose=0)  
gs.fit(X_train, y_train)  
gs_fpr, gs_tpr, gs_thresholds = roc_curve(y_test,  
                gs.predict_proba(X_test)[:,1], pos_label=1)  
gs_auc = auc(gs_fpr, gs_tpr)  
print('KNN 算法: AUC = %.3f' % gs_auc) 
KNN 算法: AUC = 0.855

10, 以波士顿房价为例, 对线性回归和 KNN 进行比较:

from sklearn.datasets import load_boston  
from sklearn.neighbors import KNeighborsRegressor   
from sklearn.linear_model import LinearRegression  

X, y = load_boston(return_X_y=True)  
X_train, X_test, y_train, y_test = train_test_split(X, y,  
                        test_size=0.25, random_state=0)  
knn = KNeighborsRegressor(n_neighbors=3, weights="distance")   
knn.fit(X_train, y_train)  
print("KNN 算法 R²:", knn.score(X_test, y_test))  
lr = LinearRegression()  
lr.fit(X_train, y_train)  
print("线性回归算法 R²:", lr.score(X_test, y_test))  
KNN 算法 R²: 0.5158073940789912
线性回归算法 R²: 0.6354638433202129
# 对 KNN 数据标准化和参数调优之后再来比较  
knn = KNeighborsRegressor()  
grid = {'n_neighbors': range(1,11,1), 'weights': ['uniform','distance']}  
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring='r2',  
                  n_jobs=-1, cv=5, verbose=0)   

# 利用流水线处理  
from sklearn.pipeline import Pipeline  

# 定义流水线的步骤: 类型为一个列表, 列表中的每个元素是元组类型  
# 格式为:[(步骤名1,评估器1), (步骤名2, 评估器2), ……, (步骤名n, 评估器n)  
knn_steps = [("scaler", StandardScaler()), ("knn", gs)]  
knn_p = Pipeline(knn_steps)  

# 可以设置流水线的参数. 所有可用的参数可以通过 get_params 获取  
# 设置格式如下: (步骤名__参数)  
# p.set_params(knn__n_neighbors=3, knn__weights="uniform")  
knn_p.fit(X_train, y_train)  
print("KNN 算法 R²:", knn_p.score(X_test, y_test))  

# 线性回归数据标准化  
lr_steps = [("scaler", StandardScaler()), ("lr", LinearRegression())]  
lr_p = Pipeline(lr_steps)  
lr_p.fit(X_train, y_train)  
print("线性回归算法 R²:", lr_p.score(X_test, y_test))  
KNN 算法 R²: 0.6441485149216897
线性回归算法 R²: 0.6354638433202131