保留初心,砥砺前行
k-nearest neighbor, k-NN是一种可以用于多分类和回归的方法。knn是一个很简单的分类方法。
k近邻法的输入是实例的特征向量,也就是特征空间中的点;
k近邻法的输出是实例的类别,可以是多个类。
接下来本文将从:
- k近邻算法
- k近邻法的模型
- kd树
三个方面进行阐述。
1. k近邻算法
本文的第一句话,就已经非常明确地告诉了你,k-NN是用来做什么的。机器学习无非就是做这几件事而已,k-NN一下子就可以分类和回归,是不是很牛。
然后介绍了它的输入输出,这个时候你可能就一头雾水了——等等,我连它是什么都不清楚,怎么就输入输出了呢。别急,接下来我就开始介绍k近邻算法。
k近邻算法简单、直观:给定一个训练数据集(其中的实例类别已定),对新的输入实例,在训练数据集中找到与该实例最接近的k个实例,这k个实例的多数属于某个类,就把该输入实例分为这个类。
通过以上的话已经足够理解k近邻,如果说的再具象一些:
我们需要输入一个这样的数据集
T = {(x1,y1),(x2,y2),(x3,y3),···,(xN,yN)}
其中xi是实例的特征向量,yi是xi这个实例对应的类别,有可能是c1,c2,c3,··· , cK。
然后我们要再输入一个需要被分类的实例的特征向量x。根据给定的某种距离度量,在 T 中找到与x最接近的k个点。
在k个点中根据某种分类的决策规则(例如,哪个类别多就是哪个类别)决定x的类别y。
输出x的类别y。
从以上的叙述可以看出, k近邻是没有显式的学习过程的。
当 k近邻的k = 1时,成为最近邻算法。顾名思义,相信聪明的你肯定知道是什么意思了。
2. k近邻法的模型
在前边的介绍中的第2个步骤中,可以看到“某种距离度量”;第3步中有“某种决策规则”;还有k近邻的k究竟是多少。
以上这些都是在了解了 k近邻算法后我们会产生的问题。
没错,这三个问题就是k近邻法的3个基本要素。
下面,我们就从这3个基本要素,探究k近邻法的模型。
2.1. 距离度量
根据k近邻法的思路,我们输入的x与数据集中的某个xi的相似程度,需要通过两个点的距离来反应。
一般使用的是欧氏距离,推广到更一般的情况是Lp距离。
Lp距离的定义是:
Lp(xi,xj) = (∑l=1n|xi(l) - xj(l)|p)1/p
其中xi,xj为特征空间中的两个点。上式就代表了xi和xj间的Lp距离。
当p = 2时Lp距离就称为欧氏距离。
欧氏距离简单说就是两点的每一个维度的元素分别做差,再求平方和,最后开根号,是不是很容易。
2.2. k值的选择
算法名叫 k近邻,因此显而易见k值对于该算法的重要性。
先说结论: K值一般取一个较小的值,通常采用交叉验证法来选取最优的k值。
K值的选择会对k近邻法的结果产生较大的影响。
当k较小时:
优点:学习的近似误差会减小。
缺点:预测结果会对近邻的实例点非常敏感(我个人的理解是:例如,如果k = 2, 邻近的2个点恰好的不同类的情况下, 预测结果就会出错),k值的减小就意味着整体模型变得复杂,容易发生过拟合。当k较大时:
优点:减小学习的估计误差。
缺点:学习的近似误差会增大,k值的增大意味着整体的模型变得简单,过于简单会完全忽略训练实例中的大量有用信息。
从上图可以看出,k = 3 与 k = 5 时的分类结果是不同的。然而k值并不会在学习过程中自动优化,而是在模型初始化是人为约定好的。因此 k值对于 k近邻至关重要,需要给予更多的关注。
2.3. 分类决策规则
一般是多数表决,就是说k个实例中哪个类别多,输入的x就是哪一类。
3. kd树
重头戏来了,在实现 k近邻时,需要计算输入x与数据集中的每一个xi的距离,从而选择距离最小的k个。当训练集很大或维数很大时,计算会非常耗时。
因此为了提高 k近邻的效率,下面介绍kd树方法来存储训练数据,减少计算次数。
kd树是对k维空间中的实例点进行存储以便对其快速检索的二叉树数据结构,表示对k维空间的一种划分。
对构造kd树的语言描述晦涩难懂,因此直接上算法,再加上一个低维空间的例子,就可以搞懂它到底是怎么回事。
(以下算法描述只为了清晰易懂,并不是严禁的说法)
- 构造算法:
输入:k维空间数据集T = {x1, x2, ..., xN},
其中xi = (xi(1), xi(2), ..., xi(k)), i = 1, 2, 3, ..., N
输出: kd树
- 将所有实例点视作根节点。也就是说现在的根节点(为了叙述方便,起个别名叫做节点A)包含了整个k维空间。
- 选择一条坐标轴x(1),l = j mod k + 1,其中j为节点的深度,k为空间的维度。
将节点A中的所有的点的 x(1)轴的中位数找出来,给它起个名字叫做切分点(选择中位数作为切分点叫做平衡kd树)。并且通过切分点画一个与x(1)轴垂直的超平面,将节点A所表示的那个k维空间1分为2。 - 把那些被划分到两个子空间的实例点放在2个子节点(节点B和节点C)上(坐标x(1)小于切分点的点放在左子节点,大于的放在右子节点),那些恰巧落在切分超平面上的点,还保留在节点A中。
- 上述的2和3步骤,也就是选择坐标轴,选择切分点,划分超平面的动作循环执行下去... ...
直到,某次划分的两个子区域没有实例点存在,就停止算法。至此,kd树形成。
- 例题:
这个例题从低维出发,便于读者想象思考,高维同理。
使用如下2维空间数据集:
T = {(2, 3), (5, 4), (9, 6), (4, 7), (8, 1), (7, 2)}
和上述算法构造一个平衡kd树。
对应上述算法的第1步
将根节点1分为2,恰好落在超平面上的点保留在根节点,剩下的点按照划分进入左右子节点
继续上图的划分,在左右两个子节点的基础上继续划分,落在超平面上的点留下,分到两边的点进入下一层的左右子节点
最后一次划分,左右子节点已经没有实例点的存在了,算法结束
- 搜索算法
输入:经过上边的构造算法构造出来的kd树,目标点x
输出:x的最近邻点
上图是构造kd树算法的输出结果,红点代表目标点x。也就是说我们要寻找红色目标点的最近邻。
首先在kd树中找到红色园点所在的子节点,也就是(4,7)那个点所在的叶子节点。
因此把(4,7)作为最邻近。并且以红色目标点为圆心,经过(4,7)画圆。从图中可以看出的是,真正的最近邻一定在这个圆的内部。
你可能会说,从这个图一下不就看出来内部有一个点吗。等等等等,这是你从上帝视角看的图,而真实的数据结构是一棵二叉树,并不是这张图,因此从树上是无法做出这种判断的。
接下来我们就要寻找比(4,7)距离红点更近的点。
根据kd树找到(4,7)父节点的另一个子节点,如果这个子节点的区域与圆不相交,这个子节点的区域就不会存在最近邻点,这种情况下就退回(4,7)父节点的父节点,继续以上的步骤。而从图-5可以看出(4,7)的兄弟节点是(2,3),就在圆内,因此最近邻节点由(4,7)变成(2,3)。因此就可以得到红色圆点的最近邻为(2,3)。
这是低维,少量数据的情况,只是为了清楚地解释搜索算法。可以推广到高维大量数据的情况,实相同的道理。
OK,理论结束,开始实践。
经过上述的讲解,我们已经知道,k近邻算法是没有显示的学习过程的,整个分类过程就是构建kd树和搜索kd树的过程。
k近邻实验,我们使用sklearn中内置的Iris数据集。它由150个实例组成,平均分为3个种类,每个种类50个实例。每个实例分别有4个属性描述。
下面从sklearn中导入Iris数据集的下载器,使用下载器下载数据存入变量iris中。
from sklearn.datasets import load_iris
iris = load_iris()
print iris.data.shape
(150, 4)
print iris.data #打印数据集中的数据
print iris.target #打印实例对应的类别
[[ 5.1 3.5 1.4 0.2]
[ 4.9 3. 1.4 0.2]
[ 4.7 3.2 1.3 0.2]
[ 4.6 3.1 1.5 0.2]
... ...
... ...
篇幅原因,中间省略
一共150个实例
... ...
... ...
[ 6.8 3.2 5.9 2.3]
[ 6.7 3.3 5.7 2.5]
[ 6.7 3. 5.2 2.3]
[ 6.3 2.5 5. 1.9]
[ 6.5 3. 5.2 2. ]
[ 6.2 3.4 5.4 2.3]
[ 5.9 3. 5.1 1.8]]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2]
接下来对数据集进行分割,需要强调的一点是,无论什么数据集,在分割训练集和测试集的时候一定要做到随机采样,避免相同特征的数据聚在一起的情况。
from sklearn.cross_validation import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(iris.data, iris.target, test_size=0.25, random_state=33)
print 'X_train: ', X_train
print 'X_test: ', X_test
print 'Y_train: ', Y_train
print 'Y_test: ', Y_test
X_train: [[ 5. 2.3 3.3 1. ]
[ 4.9 3.1 1.5 0.1]
[ 6.3 2.3 4.4 1.3]
[ 5.8 2.6 4. 1.2]
... ...
... ...
... ...
[ 5.6 3. 4.5 1.5]
[ 7.7 3. 6.1 2.3]
[ 5.4 3.4 1.7 0.2]]
X_test: [[ 5.7 2.9 4.2 1.3]
[ 6.7 3.1 4.4 1.4]
[ 4.7 3.2 1.6 0.2]
[ 6.5 2.8 4.6 1.5]
... ...
[ 5.8 2.7 5.1 1.9]
[ 5.7 3. 4.2 1.2]]
Y_train: [1 0 1 1 1 0 0 1 0 2 0 0 1 2 0 1 2 2 1 1 0 0 2 0 0 2 1 1 2 2 2 2 0 0 1 1 0
1 2 1 2 0 2 0 1 0 2 1 0 2 2 0 0 2 0 0 0 2 2 0 1 0 1 0 1 1 1 1 1 0 1 0 1 2
0 0 0 0 2 2 0 1 1 2 1 0 0 1 1 1 0 1 1 0 2 2 2 1 2 0 1 0 0 0 2 1 2 1 2 1 2
0]
Y_test: [1 1 0 1 2 2 0 0 2 2 2 0 2 1 2 1 2 0 1 2 0 0 2 0 2 2 1 1 2 2 1 1 2 2 2 2 2
1]
最后就是使用sklearn的k近邻分类器,对测试集进行分类测试:
KNeighborsClassifier函数默认k为5,经过测试,对于这个数据集,当k=7时效果最好。
# 导入数据标准化模块
from sklearn.preprocessing import StandardScaler
# 导入k近邻分类器
from sklearn.neighbors import KNeighborsClassifier
# 标准化训练集
ss = StandardScaler()
X_train = ss.fit_transform(X_train)
# 标准化测试集
X_test = ss.fit_transform(X_test)
# 将knn分类器赋给变量knn,并且参数中选择上文中讲的kd树
knn = KNeighborsClassifier(algorithm='kd_tree')
#knn = KNeighborsClassifier(n_neighbors=7, algorithm='kd_tree')
# Fit the model using X as training data and y as target values
knn.fit(X_train, Y_train)
# 输入测试集进行预测
y_predict = knn.predict(X_test)
print Y_test
print y_predict
第一行打印的是正确的结果
第二行打印的是knn模型预测的结果
[1 1 0 1 2 2 0 0 2 2 2 0 2 1 2 1 2 0 1 2 0 0 2 0 2 2 1 1 2 2 1 1 2 2 2 2 2 1]
[1 1 0 1 1 2 0 0 1 2 2 0 2 1 2 1 1 0 1 1 0 0 2 0 1 1 1 1 1 1 1 1 1 1 2 2 1 1]
错误率:e = 1/m ∑i=1mI(f(xi)≠yi)
由以上结果可以算出e = 10 / 36
模型精度1 - e
如果你也喜欢机器学习,并且也像我一样在ML之路上努力,请关注我,我会进行不定期更新,总有一些可以帮到你。
所有图片均来自网络