机器学习(八)——K近邻学习(K-Nearest Neighbor, KNN)

1 KNN 原理

K近邻学习是一种常用的监督学习方法,也是“懒惰学习”的代表(因为它没有显式的学习过程)。它的思想很简单:给定测试样本,基于某种距离度量找出训练集中与其最靠近的K个样本,然后基于对这K个“邻居”的投票进行预测。 KNN的建立过程很简单:

  1. 给定测试样本,计算它与训练集中每一个样本的距离;
  2. 找出距离最近的K个训练样本,作为测试样本的K个“邻居”;
  3. 依据这K个近邻归属的类别对测试样本进行投票预测。

例如,我们要决定绿色测试样本的类别归属,如果 K = 3 K=3 K=3,它的3个近邻是实线圆内的3个训练样本,其中红色所占比例为 2 3 \frac{2}{3} 32,因此绿色被判别为红色的一类;如果 K = 5 K=5 K=5,它的5个近邻为虚线圆内的训练样本,其中蓝色占比为 3 5 \frac{3}{5} 53,因此绿色被判别为蓝色的一类。
在这里插入图片描述
那么KNN中需要我们重点关注的两点就是:K值的选择距离度量的计算

1.1 模型优化

1.1.1 K值的选择

K值是该算法模型的一个超参数,一般情况下这个K和数据有很大的关系,都是用交叉验证进行选择的,但是建议 k ∈ [ 2 , 20 ] k\in[2,20] k[2,20]。k值还可以表示模型的复杂度,k值越小意味着模型复杂度越大,更容易过拟合,(用极少数的样例来决定这个预测的结果,很容易产生偏见)。k值越大学习的估计误差越小,但是学习的近似误差就会增大。

1.1.2 距离/相似度度量

我们一般使用 L p L_p Lp距离进行度量,假设两个向量 ( x i , x j ) (x_i,x_j) (xi,xj)来自n维实向量空间 R n R^n Rn,其中 x i = ( x i ( 1 ) , x i ( 2 ) , … , x i ( n ) ) , x j = ( x j ( 1 ) , x j ( 2 ) , … , x j ( n ) ) x_i=(x_i^{(1)},x_i^{(2)},\dots,x_i^{(n)}), x_j=(x_j^{(1)},x_j^{(2)},\dots,x_j^{(n)}) xi=(xi(1),xi(2),,xi(n)),xj=(xj(1),xj(2),,xj(n)),则 x i , x j x_i, x_j xi,xj L p L_p Lp距离定义为:
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(l=1nxi(l)xj(l)p)p1

  • p = 1 p=1 p=1时,称为曼哈顿距离:
    L 1 ( x i , x j ) = ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ L_{1}\left(x_{i}, x_{j}\right)=\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right| L1(xi,xj)=l=1nxi(l)xj(l)
  • p = 2 p=2 p=2时,称为欧氏距离:
    L 2 ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ 2 ) 1 2 L_{2}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{2}\right)^{\frac{1}{2}} L2(xi,xj)=(l=1nxi(l)xj(l)2)21
  • p = ∞ p=\infty p=时,称为极大距离
    L p ( x i , x j ) = max ⁡ l n ∣ x i ( l ) − x j ( l ) ∣ L_{p}\left(x_{i}, x_{j}\right)=\max _{l} n\left|x_{i}^{(l)}-x_{j}^{(l)}\right| Lp(xi,xj)=lmaxnxi(l)xj(l)

1.1.3 投票策略

  • 投票决定,少数服从多数。取类别最多的为测试样本类别。
  • 加权投票法,依据计算得出距离的远近,对近邻的投票进行加权,距离越近则权重越大,设定权重为距离平方的倒数。

1.2 KNN的应用

KNN虽然简单,但是既可以用来做分类,又可以用来做回归,还可以用来做缺失值填充。

  • 在分类任务中常使用“投票法”。
  • 在回归任务中可使用“平均法”,将这个K个近邻的实值输出标记的平均值作为预测结果。还可以使用加权平均。

1.3 泛化性能

假设测试样本维 x x x,其最近邻样本维 z z z,并且在 x x x的任意小范围 δ \delta δ总能找到一个训练样本 z z z,同时令 c ∗ = a r g    m a x c ∈ y P ( c ∣ x ) c^*=arg\;max_{c∈y}P(c|x) c=argmaxcyP(cx)为贝叶斯分类器的最优结果。那么最近了分类器出错的概率就是 x x x z z z的类别标记不同的概率:
P ( e r r ) = 1 − ∑ c ∈ y P ( c ∣ x ) P ( c ∣ z )          ≃ 1 − ∑ c ∈ y P 2 ( c ∣ x ) ≤ 1 − P 2 ( c ∗ ∣ x ) = ( 1 + P 2 ( c ∗ ∣ x ) ) ( 1 − P 2 ( c ∗ ∣ x ) )    ≤ 2 × ( 1 − P 2 ( c ∗ ∣ x ) ) P(err)=1-\sum_{c \in y}P(c|x)P(c|z)\\ \;\;\;\;\simeq 1-\sum_{c\in y}P^2(c|x)\\\leq1-P^2(c^*|x)\\ \qquad\qquad\qquad\quad=(1+P^2(c^*|x))(1-P^2(c^*|x))\\ \;\qquad\leq2\times\left(1-P^2(c^*|x)\right) P(err)=1cyP(cx)P(cz)1cyP2(cx)1P2(cx)=(1+P2(cx))(1P2(cx))2×(1P2(cx))
由此可见最近邻分类器虽然简单,但是其泛化错误率不超过贝叶斯最优分类器错误率的两倍。

2 代码实践

2.1 回归任务

导入所需库和数据集,此处使用sklearn数据集中的波士顿房价数据集,并使用head()函数查看数据集。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline 
plt.style.use("ggplot")      
import seaborn as sns
from sklearn import datasets

boston = datasets.load_boston()  # 返回一个类似于字典的类
X = boston.data                                # 获得特征矩阵
y = boston.target                              # 获得标签
features = boston.feature_names    # 获得特征的名称
boston_data = pd.DataFrame(X,columns=features)
boston_data["Price"] = y

boston_data.head()

在这里插入图片描述
接下来是模型训练和预测可视化
首先我们使用LSTAT列的数据作为要训练的数据,查看它的统计信息

# 统计分析
boston_data["LSTAT"].describe()

count 506.000000
mean 12.653063
std 7.141062
min 1.730000
25% 6.950000
50% 11.360000
75% 16.955000
max 37.970000
Name: LSTAT, dtype: float64
以LSTAT列的数据作为训练集,然后根据该列的数据统计信息,构建测试数据,plot拟合曲线

train = np.reshape(boston_data["LSTAT"].values,(len(boston_data["LSTAT"]),1))
test = np.linspace(0, 38, 506)[:, np.newaxis]

然后分别取 K = 1 , 3 , 5 , 8 , 10 , 40 , 100 , 250 , 506 K=1,3,5,8,10,40,100,250,506 K=1,3,5,8,10,40,100,250,506查看回归效果,KNeighborsRegressor函数官网介绍

from sklearn.neighbors import KNeighborsRegressor
# Fit regression model
# 设置多个k近邻进行比较
n_neighbors = [1, 3, 5, 8, 10, 40,100,250,506]
# 设置图片大小
plt.figure(figsize=(20,10))
for i, k in enumerate(n_neighbors):
    # 默认使用加权平均进行计算predictor
    clf = KNeighborsRegressor(n_neighbors=k, p=2, metric="minkowski")
    # 训练
    clf.fit(T, y)
    # 预测
    y_ = clf.predict(test)
    plt.subplot(3, 3, i + 1)
    plt.scatter(T, y, color='red', label='data')
    plt.plot(test, y_, color='navy', label='prediction')
    plt.axis('tight')
    plt.legend()
    plt.title("KNeighborsRegressor (k = %i)" % (k))

plt.tight_layout()
plt.show()

在这里插入图片描述

K = 1 K=1 K=1时,预测结果只与一个样本有关系,从预测曲线中可以看出当k很小时候很容易发生过拟合。
K = 506 K=506 K=506时,预测的结果和最近的506个样本相关,因为我们只有506个样本,此时是所有样本的平均值,此时所有预测值都是均值,很容易发生欠拟合。
同时也可以看到随着K的增大,拟合曲线越光滑。
一般情况下,使用knn的时候,根据数据规模我们会从[3, 20]之间进行尝试,选择最好的k。

2.2 分类任务

分类任务我们使用熟悉的鸢尾花数据集

# 使用莺尾花数据集的前两维数据,便于数据可视化
iris = datasets.load_iris()
X = iris.data[:, :2]
y = iris.target

并且对 K = 1 , 3 , 5 , 8 , 10 , 15 K=1,3,5,8,10,15 K=1,3,5,8,10,15进行测试,查看其分类结果

k_list = [1, 3, 5, 8, 10, 15]
h = .02
# 创建不同颜色的画布
cmap_light = ListedColormap(['orange', 'cyan', 'cornflowerblue'])
cmap_bold = ListedColormap(['darkorange', 'c', 'darkblue'])

plt.figure(figsize=(15,14))
# 根据不同的k值进行可视化
for ind,k in enumerate(k_list):
    clf = KNeighborsClassifier(k)
    clf.fit(X, y)
    # 画出决策边界
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    # 根据边界填充颜色
    Z = Z.reshape(xx.shape)

    plt.subplot(321+ind)  
    plt.pcolormesh(xx, yy, Z, cmap=cmap_light)
    # 数据点可视化到画布
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold,
                edgecolor='k', s=20)
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.title("3-Class classification (k = %i)"% k)

plt.show()

分类结果如下,不同的颜色代表不同的类别
在这里插入图片描述
K K K较小时,模型分界位置容易受到局部数据的影响,对局部数据敏感。例如 K = 1 K=1 K=1时,浅蓝块中嵌入了很多小块的深蓝。而当 K K K较大时,模型相对来说较鲁棒,预测结果会落入对应的区域中,对局部数据不那么敏感。

2.3 缺失值填充

如果数据集中有空值,空值是不能参与计算的,我们需要进行数据预处理对空值进行填充,此时我们可以使用KNNImputer进行空值填充。下面我们来看其填充原理:
首先我们构建一个有缺失值的数据

X = [[1, 2, np.nan], [3, 4, 3], [np.nan, 6, 5], [8, 8, 7]]

对其进行空值填充时且设定 K = 2 K=2 K=2,对于第一个有空值的样本 [ 1 , 2 , n p . n a n ] [1,2,np.nan] [1,2,np.nan],它的第三维有空值,而与它最近的两个样本是 [ 3 , 4 , 3 ] [3,4,3] [3,4,3] [ n p . n a n , 6 , 5 ] [np.nan,6,5] [np.nan,6,5],它们的第三维的值分别为3和5,用这两个的均值作为填充值,因此填充后为 [ 1 , 2 , 4 ] [1,2,4] [1,2,4]。对于有空值的第二个样本 [ n p . n a n , 6 , 5 ] [np.nan,6,5] [np.nan,6,5],与其距离最近的两个样本是 [ 3 , 4 , 3 ] [3,4,3] [3,4,3] [ 8 , 8 , 7 ] [8,8,7] [8,8,7],第一维缺失值的空值用同样方法填充为 [ 5.5 , 6 , 5 ] [5.5,6,5] [5.5,6,5]
我们来看代码实现:

# kNN数据空值填充
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=2, metric='nan_euclidean')
imputer.fit_transform(X)

array([[1. , 2. , 4. ],
[3. , 4. , 3. ],
[5.5, 6. , 5. ],
[8. , 8. , 7. ]])

有空值的欧式距离又是如何计算的呢?

对于正常样本 [ 3 , 4 , 3 ] [3,4,3] [3,4,3] [ 8 , 8 , 7 ] [8,8,7] [8,8,7],计算其欧式距离为: ( 3 − 8 ) 2 + ( 4 − 8 ) 2 + ( 3 − 7 ) 2 = 33 = 7.55 \sqrt{(3-8)^2+(4-8)^2+(3-7)^2}=\sqrt{33}=7.55 (38)2+(48)2+(37)2 =33 =7.55因此这两个样本的欧氏距离为7.55。
对于有缺失值的样本 [ 1 , 2 , n p . n a n ] [1,2,np.nan] [1,2,np.nan] [ n p . n a n , 6 , 5 ] [np.nan,6,5] [np.nan,6,5],计算欧式距离只计算没有缺失值的维度的值,并按比例增加其余坐标的权重: 3 1 × ( 2 − 6 ) 2 = 48 = 6.93 \sqrt{\frac{3}{1} \times (2-6)^2}=\sqrt{48}=6.93 13×(26)2 =48 =6.93因此这两个样本的欧氏距离为6.93。

用代码计算计算 X X X Y Y Y中每对样本之间的欧式距离,如果 Y = N o n e Y=None Y=None则令 Y = X Y=X Y=X,在计算一对样本之间的距离时,此公式将忽略两个样本中任一值均缺失的要素坐标,并按比例增加其余坐标的权重:

from sklearn.metrics.pairwise import nan_euclidean_distances
X = [[np.nan, 6, 5], [3, 4, 3]]
Y = [[3, 4, 3], [1, 2, np.nan], [8, 8, 7]]
nan_euclidean_distances(X, Y)

array([[3.46410162, 6.92820323, 3.46410162],
[0. , 3.46410162, 7.54983444]])

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值