K近邻学习是一种常用的监督学习方法,也是“懒惰学习”的代表(因为它没有显式的学习过程)。它的思想很简单:给定测试样本,基于某种距离度量找出训练集中与其最靠近的K个样本,然后基于对这K个“邻居”的投票进行预测。 KNN的建立过程很简单:
例如,我们要决定绿色测试样本的类别归属,如果 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值的选择和距离度量的计算。
K值是该算法模型的一个超参数,一般情况下这个K和数据有很大的关系,都是用交叉验证进行选择的,但是建议 k ∈ [ 2 , 20 ] k\in[2,20] k∈[2,20]。k值还可以表示模型的复杂度,k值越小意味着模型复杂度越大,更容易过拟合,(用极少数的样例来决定这个预测的结果,很容易产生偏见)。k值越大学习的估计误差越小,但是学习的近似误差就会增大。
我们一般使用 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=1∑n∣∣∣xi(l)−xj(l)∣∣∣p)p1
KNN虽然简单,但是既可以用来做分类,又可以用来做回归,还可以用来做缺失值填充。
假设测试样本维 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∗=argmaxc∈yP(c∣x)为贝叶斯分类器的最优结果。那么最近了分类器出错的概率就是 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)=1−c∈y∑P(c∣x)P(c∣z)≃1−c∈y∑P2(c∣x)≤1−P2(c∗∣x)=(1+P2(c∗∣x))(1−P2(c∗∣x))≤2×(1−P2(c∗∣x))
由此可见最近邻分类器虽然简单,但是其泛化错误率不超过贝叶斯最优分类器错误率的两倍。
导入所需库和数据集,此处使用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。
分类任务我们使用熟悉的鸢尾花数据集
# 使用莺尾花数据集的前两维数据,便于数据可视化
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较大时,模型相对来说较鲁棒,预测结果会落入对应的区域中,对局部数据不那么敏感。
如果数据集中有空值,空值是不能参与计算的,我们需要进行数据预处理对空值进行填充,此时我们可以使用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 (3−8)2+(4−8)2+(3−7)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×(2−6)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]])