1. k近邻法
1.1 简介与定义
k k k近邻法是一种基本的分类与回归方法,其输入为实例的特征向量,对应于特征空间中的点;输出为实例的类别,可以取多个类别。 k k k近邻法包括 k k k值的选择、距离度量及分类决策规则三部分。
k近邻法
输入 训练数据集 T = { ( x 1 , y 2 ) , ( x 2 , y 2 ) , . . . , ( x N , y N ) } T=\{(x_1,y_2),(x_2,y_2),...,(x_N,y_N)\} T={(x1,y2),(x2,y2),...,(xN,yN)}
其中 x i ∈ X ⊆ R n x_i\in\mathcal {X}\subseteq{\bold R}^n xi∈X⊆Rn为实例的特征向量, y i ∈ Y = { c 1 , c 2 , . . . , c K } y_i\in\mathcal {Y}=\{c_1,c_2,...,c_K\} yi∈Y={c1,c2,...,cK}为实例的类别, i = 1 , 2 , . . . , N i=1,2,...,N i=1,2,...,N;实例特征向量 x x x;
输出 实例 x x x所属的类 y y y。
(1)根据给定的距离度量,在训练集 T T T中找出与 x x x最邻近的 k k k个点,涵盖这 k k k个点的 x x x的邻域记作 N k ( x ) N_k(x) Nk(x);
(2)在 N k ( x ) N_k(x) Nk(x)中根据分类决策规则(如多数表决)决定 x x x的类别 y y y: y = arg max c j ∑ x i ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , . . . , N ; j = 1 , 2 , . . . , K . y=\argmax_{c_j}\sum_{x_i\in N_k(x)}I(y_i=c_j), \ \ i=1,2,...,N;j=1,2,...,K. y=cjargmaxxi∈Nk(x)∑I(yi=cj), i=1,2,...,N;j=1,2,...,K.
1.2 距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反映。 k k k近邻模型的特征空间一般是 n n n维实数向量空间 R n \bold R^n Rn。使用距离是欧式距离,但也可以使用其他距离,如更一般的 L p L_p Lp距离或 M i n k o w s k i {\rm Minkowski} Minkowski距离。
设特征空间 X \mathcal X X是 n n n维实数向量空间 R n \bold R^n Rn, x i , x j ∈ X x_i,x_j\in \mathcal X xi,xj∈X, x i = ( x i ( 1 ) , x i ( 2 ) , . . . , x i ( n ) ) T x_i=(x_i^{(1)},x_i^{(2)},...,x_i^{(n)})^{\rm T} xi=(xi(1),xi(2),...,xi(n))T, x j = ( x j ( 1 ) , x j ( 2 ) , . . . , x j ( n ) ) T x_j=(x_j^{(1)},x_j^{(2)},...,x_j^{(n)})^{\rm T} xj=(xj(1),xj(2),...,xj(n))T, 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(x_i,x_j)=\left(\sum_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^p\right)^\frac{1}{p} Lp(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣p)p1
这里 p ≥ 1 p\geq1 p≥1。当 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(x_i,x_j)=\left(\sum_{l=1}^n|x_i^{(l)}-x_j^{(l)}|^2\right)^\frac{1}{2} L2(xi,xj)=(l=1∑n∣xi(l)−xj(l)∣2)21
当
p
=
1
p=1
p=1时,称为曼哈顿距离,即
L
1
(
x
i
,
x
j
)
=
∑
l
=
1
n
∣
x
i
(
l
)
−
x
j
(
l
)
∣
L_1(x_i,x_j)=\sum_{l=1}^n|x_i^{(l)}-x_j^{(l)}|
L1(xi,xj)=l=1∑n∣xi(l)−xj(l)∣
当
p
=
∞
p=\infty
p=∞时,它是各个坐标距离的最大值,即
L
∞
(
x
i
,
x
j
)
=
max
l
∣
x
i
(
l
)
−
x
j
(
l
)
∣
.
L_\infty(x_i,x_j)=\max_{l}|x_i^{(l)}-x_j^{(l)}|.
L∞(xi,xj)=lmax∣xi(l)−xj(l)∣.
1.3 k值的选择
k k k值的选择会对 k k k近邻法的结果产生重大的影响。如果选择较小的 k k k值,就相当于用较小的邻域中的训练实例进行预测,学习的近似误差会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是学习的估计误差会增大,预测结果会对近邻的结果非常敏感。并且如果近邻恰好是噪声,预测就会出错。即k值的减小就意味着整体模型变得复杂,容易发生过拟合。如果选择较大的 k k k值,就相当于用较大的邻域中的训练实例进行预测。其优点是可以减小学习误差,但缺点是学习的近似误差会增大。这时与输入实例较远(不相似的)训练实例也会对预测起作用,使预测发生错误。k值的减大就意味着整体模型变得简单。通常在实际应用中, k k k值一般取一个比较小的值,然后采用交叉验证法来选取最后的 k k k值。
1.4 分类决策规则
k k k近邻法中的分类决策规则往往是多数表决,即由输入实例的 k k k个近邻的训练实例中的多数类决定输入实例的类。多数表决规则有如下解释:如果分类的损失函数为0-1损失函数,分类函数为 f : R n → { c 1 , c 2 , . . . , c K } f:\bold R^n\rightarrow\{c_1,c_2,...,c_K\} f:Rn→{c1,c2,...,cK}
那么误分类的概率是 P ( Y ≠ f ( X ) ) = 1 − P ( Y = f ( X ) ) P(Y\neq f(X))=1-P(Y=f(X)) P(Y=f(X))=1−P(Y=f(X))
对于给定实例 x ∈ X x\in\mathcal {X} x∈X,其最近邻的 k k k个训练实例点构成集合 N k ( x ) N_k(x) Nk(x)。如果涵盖 N k ( x ) N_k(x) Nk(x)的区域的类别是 c j c_j cj,那么误分类概率是 1 k ∑ x i ∈ N k ( x ) I ( y i ≠ c j ) = 1 − 1 k ∑ x i ∈ N k ( x ) I ( y i = c j ) \frac{1}{k}\sum_{x_i\in N_k(x)}I(y_i\neq c_j)=1-\frac{1}{k}\sum_{x_i\in N_k(x)}I(y_i=c_j) k1xi∈Nk(x)∑I(yi=cj)=1−k1xi∈Nk(x)∑I(yi=cj)
要使误分类率最小即经验风险最小,就要使 ∑ x i ∈ N k ( x ) I ( y i = c j ) \sum \limits_{x_i\in N_k(x)}I(y_i=c_j) xi∈Nk(x)∑I(yi=cj)最大,所以多数表决规则等价于经验风险最小化。
1.5 k近邻法的实现:kd树
实现 k k k近邻法时,主要考虑的问题时如何对训练数据进行快速 k k k近邻搜索,这点在特征空间的维数大及训练数据容量大时尤其必要。 k k k近邻法最简单的实现方法是线性扫描。这时要计算输入实例与每个训练实例间的距离。而当训练数据集很大时,计算非常耗时。为了提高 k k k近邻法搜索的效率,可以考虑使用特殊的结构存储训练数据,以减少计算距离的次数。这里介绍一种常用的存储 k k k近邻法中数据集的结构, k d kd kd树。
k d kd kd树是一棵二叉树,表示对 k k k维空间的一个划分。构造 k d kd kd树相当于不断地用垂直于坐标轴的超平面将 k k k维空间切分,构成一系列的 k k k维超矩形区域。 k d kd kd树的每个结点对应于一个 k k k维超矩形区域。
构造 k d kd kd树的方法如下:构造根结点,使根结点对应于 k k k维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对 k k k维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域(子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。通常,以此选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数为切分点,这样得到的 k d kd kd树是平衡的。
构建平衡kd树
输入 k k k维空间数据集 T = { ( x 1 , y 2 ) , ( x 2 , y 2 ) , . . . , ( x N , y N ) } T=\{(x_1,y_2),(x_2,y_2),...,(x_N,y_N)\} T={(x1,y2),(x2,y2),...,(xN,yN)},其中 x j = ( x j ( 1 ) , x j ( 2 ) , . . . , x j ( n ) ) T x_j=(x_j^{(1)},x_j^{(2)},...,x_j^{(n)})^{\rm T} xj=(xj(1),xj(2),...,xj(n))T, i = 1 , 2 , . . . , N i=1,2,...,N i=1,2,...,N;
输出 k d kd kd树。
(1)开始:构造根结点,根结点对应于包含 T T T的 k k k维空间的超矩形区域。选择 x ( 1 ) x^{(1)} x(1)为坐标轴,以 T T T的所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并于坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。这时,由根结点生成深度为 1 1 1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
(2)重复:对深度为 j j j的结点,选择 x ( l ) x^{(l)} x(l)为切分的坐标轴, l = j ( m o d k ) + 1 l=j({\rm mod}\ k)+1 l=j(mod k)+1,以该结点的区域中所有实例的 x ( l ) x^{(l)} x(l)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并于坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。
并由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。将落在切分超平面上的实例点保存在该结点。
(3)直到两个子区域没有实例存在时停止,从而形成 k d kd kd树的区域划分。
搜索kd树
以最近邻为例,介绍如何使用 k d kd kd树的特点完成快速搜索。给定一个目标点,搜索其最近邻。首先找到包含目标点的叶结点;然后从叶结点出发,依次回退到父结点;不断查找与目标点最近邻的结点,当确定不可能存在更近的结点时终止。这样的搜索被限制在空间的局部区域上,效率大为提高。
包含目标点的叶结点对应包含目标点的最小超矩形区域。以此叶结点的实例点作为当前最近点。目标点的最近邻一定在以目标点为中心并通过当前最近点的超球体的内部。然后返回当前结点的父结点,如果父结点的另一子结点的超矩形区域与超球体相交,那么在相交的区域内寻找与目标点更接近的实例点。如果存在这样的点,将此点作为新的当前最近点。算法转到更上一级的父结点,继续上述过程。如果父结点的另一子结点的超矩形区域与超球体不相交,或不存在比当前最近点更近的点,则停止搜索。
用kd树的最近邻搜索
输入 已构建的 k d kd kd树,目标点 x x x。
输出 x x x的最近邻。
(1)在 k d kd kd树中找到包含目标点 x x x的叶结点:从根结点出发,递归地向下访问 k d kd kd树。若目标点 x x x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止;
(2)以此叶结点为当前最近点;
(3)递归地向上回退,在每个结点进行一下操作:
- 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为当前最近点;
- 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为中心,以目标点与当前最近点间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,向上回退。
(4)当回退到根结点时,搜索结束。最后的当前最近点即为目标点 x x x的最近邻点。
2. Python实现k近邻算法
数据处理等部分上一文已经提到,这里只给出关键部分的代码。 k k k近邻法,首先将数据集转化成算法输入格式:
def load_data(file):
# 定义空列表
dataArr = []
labelArr = []
# 打开并处理文件
with open(file, "r") as f:
lines = f.readlines()
for line in lines:
# 针对csv文件格式,使用','分割数据
curLine = line.strip().split(',')
# k近邻法是一个二分类算法,定义0-4为-1、5-9为+1
# csv文件第一列存放着具体类别
if int(curLine[0]) < 5:
labelArr.append(-1)
else:
labelArr.append(1)
# 处理具体数据,除以255归一化
dataArr.append([int(num) / 255 for num in curLine[1:]])
# 返回
return dataArr, labelArr
这里使用欧式距离来衡量两个实例点之间的距离:
def calculate_dist(x1, x2):
# 计算两个实例点间的欧式距离
return np.sqrt(np.sum(np.square(x1 - x2)))
k k k近邻法的主体函数:
def get_closest(dataArr, labelArr, x, top_k):
# top_k即为k近邻算法的k值
# 建立一个存放距离的列表,distList[i]为实例点x距离实例点i的距离
# distList=[0, 0, 0, 0,..., 0]
distList = [0] * len(dataArr)
# 计算距离
for i in range(len(dataArr)):
# 计算欧式距离
distList[i] = calculate_dist(x, dataArr[i])
# 调用排序函数对距离排序,并返回排序后各项的索引,默认为升序
# 并取前k个最短距离
top_k_list = np.argsort(np.array(distList))[:top_k]
# 存放类别的变量以实现计数功能
labelList = [0] * 10
# 遍历前k个距离,开始投票
for i in top_k_list:
# i对应的数据索引
data = labelArr[i]
# 对应类别元素加一
labelList[int(data)] += 1
# 找到labelList中的最大值
mx = max(labelList)
# 返回该最大值对应的索引以及相应的类别
return labelList.index(mx)
测试集上的表现:
def test(trainDataArr, trainLabelArr, testDataArr, testLabelArr, top_k):
# 格式转换
trainDataMat = np.mat(trainDataArr)
trainLabelMat = np.mat(trainLabelArr).T
testDataMat = np.mat(testDataArr)
testLabelMat = np.mat(testLabelArr).T
# 记录错误个数
errorCnt = 0
# 遍历测试集
for i in range(len(testDataMat)):
print("test {}/{}".format(i, len(testDataMat)))
# 当前索引对应的测试数据
x = testDataMat[i]
# 获取对应的最近邻数据
y = get_closest(trainDataMat, trainLabelMat, x, top_k)
# 错误数加一
if y != testLabelMat[i]:
errorCnt += 1
# 计算正确率
return 1 - (errorCnt / len(testDataMat))
3. k近邻法的总结
k k k近邻法是基本且简单的分类与回归方法,其包含三要素:距离度量, k k k值选取和分类决策规则。常用到的距离度量方式为欧式距离; k k k值小时,模型更加复杂, k k k值大时,模型更加简单;分类决策规则通常使用多数表决,对应于经验风险最小化。 k k k近邻法由Cover和Hart提出。
参考
- 统计学习方法/李航著。—2版。—北京:清华大学出版社,2019(2019.6重印).
- https://github.com/Dod-o/Statistical-Learning-Method_Code(k近邻法代码).