文章目录
一、基本概念
KNN是一种基本分类和回归方法。
K近邻算法,即是给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的K个实例,这K个实例的多数属于某个类,就把该输入实例分类到这个类中。(这就类似于现实生活中少数服从多数的思想)
如上图所示,有两类不同的样本数据,分别用蓝色的小正方形和红色的小三角形表示,而图正中间的那个绿色的圆所标示的数据则是待分类的数据。
-
如果K=3,绿色圆点的最邻近的3个点是2个红色小三角形和1个蓝色小正方形,少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于红色的三角形一类。
-
如果K=5,绿色圆点的最邻近的5个邻居是2个红色三角形和3个蓝色的正方形,还是少数从属于多数,基于统计的方法,判定绿色的这个待分类点属于蓝色的正方形一类。
二、KNN算法中K的选取以及特征归一化的重要性
1、K值的选取及其影响
-
K值较小,整体模型变得复杂,容易发生过拟合。
例:选取K=1这个极端情况
则判定待分类点是黑色的圆点,明显错误。
所谓的过拟合就是在训练集上准确率非常高,而在测试集上准确率低,经过上例,我们可以得到k太小会导致过拟合,很容易将一些噪声(如上图离五边形很近的黑色圆点)学习到模型中,而忽略了数据真实的分布!
-
K值较大,整体模型变得简单,容易忽略训练数据实例中的大量有用信息。
例:选取K=N这个极端情况:
则判定待分类点是黑色的圆点,明显错误。
如果我们选取较大的k值,就相当于用较大邻域中的训练数据进行预测,这时与输入实例较远的(不相似)训练实例也会对预测起作用,使预测发生错误,k值的增大意味着整体模型变得简单。
-
K值的选取
一般选取一个较小的数值,通常采取交叉验证法来选取最优的k值。(也就是说,选取k值很重要的关键是实验调参,类似于神经网络选取多少层这种,通过调整超参数来得到一个较好的结果)
2、距离的度量
K近邻算法是在训练数据集中找到与该实例最邻近的K个实例,而最邻近是如何度量的呢?
我们有以下几种度量方法:
-
切比雪夫距离(p=∞)
以数学的观点来看,切比雪夫距离是由一致范数(uniform norm)(或称为上确界范数)所衍生的度量,也是超凸度量(injective metric space)的一种。
玩过国际象棋的朋友或许知道,国王走一步能够移动到相邻的8个方格中的任意一个。那么国王从格子(x1,y1)走到格子(x2,y2)最少需要多少步?。你会发现最少步数总是max( | x2-x1 | , | y2-y1 | ) 步 。有一种类似的一种距离度量方法叫切比雪夫距离。
3、特征归一化的必要性
由于各个特征量纲的不同,应该让每个特征都是同等重要的,归一化公式如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IEqz9W0h-1626602032959)(C:\Users\86130\AppData\Roaming\Typora\typora-user-images\image-20210718123724737.png)]
三、KD树
1、KD树的结构
kd树是一个二叉树结构,它的每一个节点记载了【特征坐标,切分轴,指向左枝的指针,指向右枝的指针】。
- 特征坐标是线性空间 R n R^n Rn 中的一个点 ( x 1 , x 2 , . . . , x n ) (x_1,x_2,...,x_n) (x1,x2,...,xn);
- 切分轴由一个整数 r r r 表示,这里 1 < r < n 1<r<n 1<r<n,是我们在 n n n 维空间中沿第 r r r 维进行一次分割;
- 节点的左枝和右枝分别都是 kd 树,并且满足:如果 y y y 是左枝的一个特征坐标,那么 y x ≤ x r y_x \leq x_r yx≤xr;并且如果 z z z 是右枝的一个特征坐标,那么 z r ≥ x r z_r \geq x_r zr≥xr。
给定一个数据样本集 S ⊆ R n S \subseteq R^n S⊆Rn 和切分轴 r r r,以下递归算法将构建一个基于该数据集的 kd 树,每一次循环制作一个节点:
-
如果 ∣ S ∣ = 1 |S|=1 ∣S∣=1|,记录 S S S 中唯一的一个点为当前节点的特征数据,并且不设左枝和右枝。( ∣ S ∣ |S| ∣S∣ 指集合 S S S 中元素的数量)
-
如果 ∣ S ∣ > 1 |S|>1 ∣S∣>1:
① 将 S S S 内所有点按照第 r r r 个坐标的大小进行排序;
② 选出该排列后的中位元素(如果一共有偶数个元素,则选择中位左边或右边的元素,左或右并无影响),作为当前节点的特征坐标,并且记录切分轴 r r r;
③ 将 S L S_L SL 设为在 S S S 中所有排列在中位元素之前的元素, S R S_R SR 设为在 S S S 中所有排列在中位元素后的元素;
④ 当前节点的左枝设为以 S L S_L SL 为数据集并且 r r r 为切分轴制作出的 kd 树,当前节点的右枝设为以 S R S_R SR 为数据集并且 r r r 为切分轴制作出 的 kd 树,再设 r ← ( r + 1 ) % n r \leftarrow (r+1) \% n r←(r+1)%n。(这里,我们想轮流沿着每一个维度进行分割, % n \%n %n 是因为一共有 n n n 个维度,在沿着最后一个维度进行分割之后再重新回到第一个维度)
构造KD树的例子:
2、KD树上的KNN算法
给定一个构建与一个样本集的 KD 树,下面算法可以寻找距离某个点 p p p 最近的 k k k 个样本:
(1) 设 L L L 为一个有 k k k 个空位的列表,用于保存已搜寻到的最近点;
(2) 根据 p p p 的坐标值和每个节点的切分值向下搜索(也就是说,如果树的节点是按照 x r = a x_r=a xr=a 进行切分,并且 p p p 的 r r r 坐标小于 a a a,则向左枝进行搜索;反之则走右枝);
(3) 当达到一个底部节点时,将其标记为访问过。如果 L L L 里不足 k k k 个点,则将当前节点的特征坐标加入 L L L ;如果 L L L 不为空并且当前节点的特征与 p p p 的距离小于 L L L 里最长的距离,则用当前特征替换掉 L L L 中离 p p p 最远的点;
(4) 如果当前节点不是整棵树最顶端节点,则向上爬一个节点;反之,输出 L L L,算法完成。
-
如果当前(向上爬之后的)节点未曾被访问过,将其标记为被访问过,然后执行 ① 和 ②;如果当前节点被访问过,再次向上爬一个节点。
① 如果此时 L L L 里不足 k k k 个点,则将节点特征加入 L L L;如果 L L L 中已满 k k k 个点,且当前节点与 p p p 的距离小于 L L L 里最长的距离,则用节点特征替换掉 L L L 中离最远的点;
② 计算 p p p 和当前节点切分线的距离。如果该距离大于等于 L L L 中距离 p p p 最远的距离并且 L L L 中已有 k k k 个点,则在切分线另一边不会有更近的点,执行 (4);如果该距离小于 L L L 中最远的距离或者 L L L 中不足 k k k 个点,则切分线另一边可能有更近的点,因此在当前节点的另一个枝从 (2) 开始执行。
四、scikit-learn 实现 KNN 分类
1、模型建立
导入包。
import random
from sklearn import neighbors
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
随机生成 6 组 200 个正态分布的数据。
x1 = random.normal(50, 6, 200)
y1 = random.normal(5, 0.5, 200)
x2 = random.normal(30,6,200)
y2 = random.normal(4,0.5,200)
x3 = random.normal(45,6,200)
y3 = random.normal(2.5, 0.5, 200)
x 1 、 x 2 、 x 3 x_1、x_2、x_3 x1、x2、x3 作为 x x x 坐标, y 1 、 y 2 、 y 3 y_1、y_2、y_3 y1、y2、y3 作为 y y y 坐标,两两配对; ( x 1 , y 1 ) (x_1,y_1) (x1,y1) 标为 1 类, ( x 2 , y 2 ) (x_2, y_2) (x2,y2) 标为 2 类, ( x 3 , y 3 ) (x_3, y_3) (x3,y3) 标为 3 类,将它们画出得到下图,1 类是蓝色,2 类红色,3 类绿色。
plt.scatter(x1,y1,c='b',marker='s',s=50,alpha=0.8)
plt.scatter(x2,y2,c='r', marker='^', s=50, alpha=0.8)
plt.scatter(x3,y3, c='g', s=50, alpha=0.8)
把所有的 x x x 坐标和 y y y 坐标放在一起。
x_val = np.concatenate((x1,x2,x3))
y_val = np.concatenate((y1,y2,y3))
计算 x x x 值的最大差和 y y y 值的最大差,将坐标除以这个差得到距离的归一化,再将 x x x 和 y y y 值两两配对。
x_diff = max(x_val)-min(x_val)
y_diff = max(y_val)-min(y_val)
x_normalized = [x/(x_diff) for x in x_val]
y_normalized = [y/(y_diff) for y in y_val]
xy_normalized = zip(x_normalized,y_normalized)
生成相应的分类标签:生成一个长度600的list,前200个是1,中间200个是2,最后200个是3,对应三种标签。
labels = [1]*200+[2]*200+[3]*200
创建k邻分类器,设置 n_neighbors 为 30,其他默认。
clf = neighbors.KNeighborsClassifier(30)
'''
neighbors.KNeighborsClassifier(n_neighbors=5, weights=’uniform’, algorithm=’auto’, leaf_size=30, p=2, metric=’minkowski’, metric_params=None, n-jobs=1)`
n_neighbors: KNN 里的 k.
weights: 进行分类判断时给最近邻附上的加权,默认的 'uniform' 是等权加权,还有 'distance' 选项是按照距离的倒数进行加权,也可以 使用用户自己设置的其他加权方法,权重功能的选项应该视应用的场景而定.
algorithm: 分类时采取的算法,有 'brute'、'kd_tree' 和 'ball_tree'。kd_tree 的算法在 kd 树文章中有详细介绍,而 ball_tree 是另一种基于树状结构的 kNN 算法,brute 则是最直接的蛮力计算. 根据样本量的大小和特征的维度数量,不同的 算法有各自的优势. 默认的 'auto' 选项会在学习时自动选择最合适的算法,所以一般来讲选择 auto 就可以.
leaf_size: kd_tree 或 ball_tree 生成的树的叶子结点的大小.
metric & p: 距离函数的选项,默认的 metric='minkowski'(默认)和 p=2(默认).
n_jobs: 并行计算的线程数量,默认是 1,输入 -1 则设为 CPU 的内核数.
'''
进行拟合。(在创建了 KNeighborsClassifier 后,要给它数据来进行学习)
clf.fit(xy_normalized, labels)
2、模型预测
① k 最近邻
首先,我们想知道 (50,5) 和 (30,3) 两个点附近最近的 5 个样本分别都是什么,坐标别忘了除以 x_diff 和 y_diff 来归一化。
nearests = clf.kneighbors([(50/x_diff, 5/y_diff),(30/x_diff, 3/y_diff)], 10, False)
nearests
得到:
array([[ 97, 134, 177, 144, 10], [278, 569, 242, 324, 504]])
表示训练数据中的第 97、134、177、144、10 个离 (50,5) 最近,第 278、569、242、324、504 个离 (30,3) 最近。
② 预测
还是上面那两个点,我们通过 30NN 来判断它们属于什么类别。
prediction = clf.predict([(50/x_diff, 5/y_diff),(30/x_diff, 3/y_diff)])
prediction
得到:
array([1, 2])
表示 (50,5) 判断为 1 类,而 (30,3) 是 2 类。
③ 概率预测
那么这两个点的分类的概率都是多少呢?
prediction_proba = clf.predict_proba([(50/x_diff, 5/y_diff),(30/x_diff, 3/y_diff)])
prediction_proba
得到:
array([[ 1. , 0. , 0. ], [ 0. , 0.8, 0.2]])
表示(50, 5) 有 100% 的可能性是 1 类,而 (30,3) 有 80% 是 2 类,20% 是3类。
④ 准确率打分
我们再用同样的均值和标准差生成一些正态分布点,以此检测预测的准确性。
x1_test = random.normal(50, 6, 100)
y1_test = random.normal(5, 0.5, 100)
x2_test = random.normal(30,6,100)
y2_test = random.normal(4,0.5,100)
x3_test = random.normal(45,6,100)
y3_test = random.normal(2.5, 0.5, 100)
xy_test_normalized = zip(np.concatenate((x1_test,x2_test,x3_test))/x_diff,\
np.concatenate((y1_test,y2_test,y3_test))/y_diff)
labels_test = [1]*100+[2]*100+[3]*100
测试数据生成完毕,下面进行测试。
score = clf.score(xy_test_normalized, labels_test)
score
得到预测的正确率是 97% 还是很不错的。
再看一下,如果使用 1NN 分类,会出现过拟合的现象,那么准确率的评分就变为:
clf1 = neighbors.KNeighborsClassifier(1)
clf1.fit(xy_normalized, labels)
clf1.score(xy_test_normalized, labels_test)
得到 95%,的确是降低了。我们还应该注意,这里的预测准确率很高是因为训练和测试的数据都是人为按照正态分布生成的,在实际使用的很多场景中(比如,涨跌预测)是很难达到这个精度的。