文章目录
4.K近邻法的算法原理
分类问题的K近邻法算法原理本质其实很简明,就是计算与预测点“距离”最近K个已知点(训练样本),通过统筹这些点的类别,决定预测点的应该分到哪一类中去。
用数学语言来定义,即如下所述。
K近邻算法:
输入:训练样本
T
(
X
,
Z
)
=
{
(
X
(
1
)
,
Z
(
1
)
)
,
(
X
(
2
)
,
Z
(
2
)
)
,
.
.
.
,
(
X
(
n
)
,
Z
(
n
)
)
}
T(X, Z)=\{(X^{(1)},Z^{(1)}),(X^{(2)},Z^{(2)}),...,(X^{(n)},Z^{(n)})\}
T(X,Z)={(X(1),Z(1)),(X(2),Z(2)),...,(X(n),Z(n))}
其中特征向量
X
(
i
)
=
(
x
1
(
i
)
,
x
2
(
i
)
,
.
.
.
,
x
l
(
i
)
)
X^{(i)}=(x_1^{(i)},x_2^{(i)},...,x_l^{(i)})
X(i)=(x1(i),x2(i),...,xl(i)),
Z
Z
Z是分类标签。
待预测点
X
^
=
(
x
^
1
,
x
^
2
,
.
.
.
,
x
^
l
)
\hat X=(\hat x_1,\hat x_2,...,\hat x_l)
X^=(x^1,x^2,...,x^l)。
过程:① 计算待预测点
X
^
\hat X
X^与训练样本点的“距离”,选择最近的K个训练样本点;
② 统筹考虑K个训练样本点的分类类别,决定待预测点应该分属于哪一类。
输出:待预测点
X
^
\hat X
X^的分类
Z
^
\hat Z
Z^
综上所述K近邻法针对的多分类问题,而且最重要的一点是K近邻法不需要训练模型,几乎是直接拿到训练样本,在上面定义距离和设定K值就可以对待预测点进行分类预测了,这是K近邻法对比其他分类方法最大的不同之处(也是最大的优点)。
当然我这里用了几乎,主要是由于在K近邻法中为了减少距离计算的遍历,需要对训练样本建立树结构的索引机制,所以实际应用中还是会基于训练样本进行预先“训练”(虽然严格意义上不是训练,应该说是构建训练样本的索引数据结构)。
4.1 K近邻法的“距离”
K近邻算法中得先定义距离算法,才能开展接下来的工作。
最常见的距离是欧式距离,定义为特征向量差的2-范数(实际意义就是n维空间的长度度量),如下所示:
∣
∣
X
(
i
)
−
X
(
j
)
∣
∣
2
=
∑
k
=
1
l
(
x
k
(
i
)
−
x
k
(
j
)
)
2
||X^{(i)}-X^{(j)}||_2=\sqrt {\sum_{k=1}^l{(x_k^{(i)}-x_k^{(j)}})^2}
∣∣X(i)−X(j)∣∣2=k=1∑l(xk(i)−xk(j))2
当然还有其他的距离定义,如曼哈顿距离,特征向量差的1-范数:
∣
∣
X
(
i
)
−
X
(
j
)
∣
∣
1
=
∑
k
=
1
l
∣
x
k
(
i
)
−
x
k
(
j
)
∣
||X^{(i)}-X^{(j)}||_1=\sum_{k=1}^l{|x_k^{(i)}-x_k^{(j)}}|
∣∣X(i)−X(j)∣∣1=k=1∑l∣xk(i)−xk(j)∣
还有具有更一般形式的闵科夫斯基距离,特征向量差的p-范数:
∣
∣
X
(
i
)
−
X
(
j
)
∣
∣
2
=
(
∑
k
=
1
l
(
x
k
(
i
)
−
x
k
(
j
)
)
p
)
1
/
p
||X^{(i)}-X^{(j)}||_2=(\sum_{k=1}^l{(x_k^{(i)}-x_k^{(j)}})^p)^{1/p}
∣∣X(i)−X(j)∣∣2=(k=1∑l(xk(i)−xk(j))p)1/p
当然你会发现欧式距离和曼哈顿距离就是闵科夫斯基距离在p=2和p=1的具体形式。
还有
inf
\inf
inf范数,
∣
∣
X
(
i
)
−
X
(
j
)
∣
∣
1
=
max
k
=
1
,
.
.
.
,
l
∣
x
k
(
i
)
−
x
k
(
j
)
∣
||X^{(i)}-X^{(j)}||_1=\max_{k=1,...,l}{|x_k^{(i)}-x_k^{(j)}}|
∣∣X(i)−X(j)∣∣1=k=1,...,lmax∣xk(i)−xk(j)∣
不同的距离定义对最后的分类结果有一定影响,比如
4.2 K近邻法的K值
在K近邻算法中,唯一需要使用到的参数是K值。K值也是对分类结果产生最重要影响的参数。
k的取值是一项case by case的事,没有规定应该具体取什么值,k的取值都是跟具体的分析对象和训练样本等有关。
当然也不是说k值就随便取,也有几个规律性的结果可以遵循:
① k值取越小,在训练样本上面表现的误差越小;
② k值取过小值,容易变得过拟合,训练样本中的噪声点对结果影响变得很大,对于预测不利
③ k值取过大值,容易变得欠拟合,容易损失数据特征,对预测同样不利
所以说k值得选取一般还需要调试,《统计学习方法》中建议采用交叉验证法来优化k的取值。
4.3 K近邻法的统筹方法(分类决策原则)
当在k近邻算法进行完距离计算,找到k个最近训练样本点之后需要根据这些样本的类别属性统筹,决定预测点应该分到哪个类别中。
统筹方法或者分类决策原则一般是多数表决机制,即选取k个样本点中最多的类别作为待预测点的类别。这个合理性是显而易见可以理解的。
从数学上也是可以解释的。实际上对于最近邻的k各训练样本点组成的集合
N
N
N,类别为
z
i
z_i
zi的发生概率可以用先验概率估计
P
(
Z
=
z
i
)
=
1
k
∑
I
(
Z
=
z
i
)
P(Z=z_i)=\frac{1}{k}\sum I(Z=z_i)
P(Z=zi)=k1∑I(Z=zi)
那么产生误分类的风险如下所示:
1
−
P
(
Z
=
z
i
)
=
1
−
1
k
∑
I
(
Z
=
z
i
)
1-P(Z=z_i)=1-\frac{1}{k}\sum I(Z=z_i)
1−P(Z=zi)=1−k1∑I(Z=zi)
显然当
P
(
Z
=
z
i
)
P(Z=z_i)
P(Z=zi)最大的时候,误分类风险最小。
4.4 kd树
k近邻算法中主要的计算消耗在于遍历计算距离选取k个最近的训练样本点这个过程。对于训练样本很大的情况,想想都是很大的计算消耗,kd树是减少遍历计算的一种索引数据结构。
kd树的本质是,建立一种二叉树形式的索引数据结构,在计算距离时优先计算叶节点样本点上的距离,向根节点方向遍历,以一定的规则判断并摒弃没有可能成为k邻近域内的样本点,仅计算一部分满足规则的样本点,与叶节点计算的距离组成集合,比较选取k个值。
显然上面的话读着很拗口,很难理解(其实我也不知道怎么简单表述)。还是以实际的过程为例。
① 构建kd树
输入:训练样本
T
(
X
,
Z
)
=
{
(
X
(
1
)
,
Z
(
1
)
)
,
(
X
(
2
)
,
Z
(
2
)
)
,
.
.
.
,
(
X
(
n
)
,
Z
(
n
)
)
}
T(X, Z)=\{(X^{(1)},Z^{(1)}),(X^{(2)},Z^{(2)}),...,(X^{(n)},Z^{(n)})\}
T(X,Z)={(X(1),Z(1)),(X(2),Z(2)),...,(X(n),Z(n))}
其中特征向量
X
(
i
)
=
(
x
1
(
i
)
,
x
2
(
i
)
,
.
.
.
,
x
l
(
i
)
)
X^{(i)}=(x_1^{(i)},x_2^{(i)},...,x_l^{(i)})
X(i)=(x1(i),x2(i),...,xl(i)),
Z
Z
Z是分类标签。
过程:
(1) 对于训练样本的特征向量
X
(
i
)
X^{(i)}
X(i)按照
x
1
x_1
x1为轴,将训练样本按序排列,取中位数(奇数个取中位数,偶数个取中间树靠左或者靠右第一个数)为根节点,在中位数左侧的数据放入左子树,将中位数右侧的数据放入右子树;
(2)在左子树(右子树同样)中按照
x
2
x_2
x2作为轴,将左子树中的训练样本按序排列(注意这里排序是按照
x
2
x_2
x2作为轴),取中位数作为根节点,将中位数左侧数据放入左子树,右侧数据放入右子树;
(3)改变排序轴,重复开展上述二叉树构建步骤,直到子树中只包含一个数据,即为叶节点(如下图所示)。
输出:kd树结构
显然这是一个递归算法的典型范例。
② 搜索kd树
在建立kd树结构后,计算距离就变成搜索kd树中与待遇测点距离最小的k个值得过程了。先以k近邻算法为例说明这个算法步骤。
k近邻搜索算法:
输入:已建立的kd树,待预测点特征向量;
过程:
(1) 从根节点出发,递归查找待预测点特征向量归属的叶节点,即如果待预测点在根节点左侧,就移动到左子树,如果待预测点在根节点右侧,就移动到右子树,重复此步,知道移动到叶节点为止;
(2) 计算叶节点与待预测点的距离为k近邻值之一;
(3) 退到上一级根节点,并计算该根节点距离,对比现存k近邻值列表,当满足一下两点,则更新k近邻列表:a) 当k近邻列表中有未定义值,b)当根节点距离小于k近邻列表中最小值;
(4) 计算(3)根节点中的另一个子节点,对比现存k近邻值列表,当满足一下两点,则更新k近邻列表:a) 当k近邻列表中有未定义值,b)当该子节点距离小于k近邻列表中最小值;
(5)重复(3)(4),直到最终根节点。如下图所示。
4.5 最近邻算法的Python代码实现
输入:训练样本如下,训练样本特征向量为前两列,第三列是类别标签,第四列是为了说明kd树结构的辅助序列号,仅仅为了说明而已。
# x1 x2 label order
4.45925637575900 8.22541838354701 -1 #1
0.0432761720122110 6.30740040001402 -1 #2
6.99716180262699 9.31339338579386 -1 #3
4.75483224215432 9.26037784240288 -1 #4
0.640487340762152 2.96504627163533 -1 #5
2.56324459286653 7.83286351946551 -1 #6
5.42032358874179 8.77024851395462 -1 #7
7.09749399121559 4.84058301823207 1 #8
4.15244831176753 1.44597290703838 1 #9
9.55986996363196 1.13832040773527 1 #10
1.63276516895206 0.446783742774178 1 #11
9.38532498107474 0.913169554364942 1 #12
6.08012336783280 3.21848321902105 1 #13
5.48289786541560 0.267286639106428 1 #14
以字典结构为例,建立kd树
def kdtree(dataset, loop):
subtree = {}
# figure out if it is leaf node
if dataset.shape[0] == 1:
subtree['node'] = list(dataset.iloc[0])
subtree['End'] = 1
return subtree
if dataset.empty:
return {}
# loop in feature list, and sort dataset
columnslist = dataset.columns.values.tolist()
loopi = loop % (dataset.shape[1] - 1)
dataset1 = dataset.sort_values(by=columnslist[loopi], axis=0, ascending=True)
# make the middle value as root node
index = int(dataset.shape[0] / 2)
node = list(dataset1.iloc[index])
subtree['axis'] = columnslist[loopi]
subtree['node'] = node
# left sub tree
loop += 1
left = dataset1[dataset1[columnslist[loopi]] < node[loopi]]
right = dataset1[dataset1[columnslist[loopi]] > node[loopi]]
print(left)
subtree1 = kdtree(left, loop)
subtree2 = kdtree(right, loop)
if len(subtree1)>0:
subtree['leftsubtree'] = subtree1
if len(subtree2)>0:
subtree['rightsubtree'] = subtree2
return subtree
建立的kd树如下所示
距离定义:这里采用欧式距离。
def distance(data1, data2):
dis = 0
for loopi in range(len(data1)):
dis += (data1[loopi]-data2[loopi])**2
dis = math.sqrt(dis)
return dis
k近邻搜索kd树
def searchnode(dataset, tree, k):
# the function is to figure out a leaf node
# dataset is the target to predict, just feature vector.
# searchdir is a list of the dir to reach leaf node
axis_label = dataset.columns.values.tolist()
axis_label.extend(['label', 'distance'])
knode = pd.DataFrame(np.zeros((k, len(axis_label))), columns=axis_label)
subtree = tree
searchdir = []
while 'End' not in subtree:
#print(subtree)
axis = subtree['axis']
index = axis_label.index(axis)
# if value(in axis)<root node or child node just leftnode, move leftsubtree
if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
subtree = subtree['leftsubtree']
searchdir.append('leftsubtree')
# print(subtree)
else:
subtree = subtree['rightsubtree']
searchdir.append('rightsubtree')
# define the leaf node as k neighbour node
leafnode = []
leafnode[:] = subtree['node']
dis = distance(leafnode[:len(leafnode)-1], dataset.iloc[0])
leafnode.append(dis)
knode.loc[0:2, axis_label] = leafnode
# search back the kd tree
data = list(dataset.iloc[0].values)
knode = searchback(data, knode, tree, searchdir, k)
return knode
def searchback(data, knode, tree, searchdir, k):
# this function is to search back the kd tree
subtree = tree
tempnode = []
axis_label = knode.columns.values.tolist()
for loopi in searchdir[:len(searchdir)-1]:
subtree = subtree[loopi]
# check father node if it is closer
tempnode[:] = subtree['node']
dis = distance(tempnode[:len(tempnode)-1], data)
if dis<knode.distance.max():
tempnode.append(dis)
knode.loc[knode.distance.idxmax()] = tempnode
# check sister node if it is closer
if searchdir[len(searchdir)-1] == 'leftsubtree' and ('rightsubtree' in subtree):
subtree2 = subtree['rightsubtree']
tempnode[:] = subtree2['node']
dis2 = distance(tempnode[:len(tempnode)-1], data)
if dis2<knode.distance.max():
tempnode.append(dis2)
knode.loc[knode.distance.idxmax()] = tempnode
elif searchdir[len(searchdir)-1] == 'rightsubtree' and ('leftsubtree' in subtree):
subtree2 = subtree['leftsubtree']
tempnode[:] = subtree2['node']
dis2 = distance(tempnode[:len(tempnode)-1], data)
if dis2<knode.distance.max():
tempnode.append(dis2)
knode.loc[knode.distance.idxmax()] = tempnode
if len(searchdir) > 1:
searchdir2 = searchdir[:len(searchdir)-1]
searchback(data, knode, tree, searchdir2, k)
return knode
统筹方法-分类决策原则
def judge(knode):
if knode['label'].sum()>0:
return 1
else:
return -1
# return 1 if knode.label.sum()>0 else -1
待预测点分类预测
def predict(node, tree, k):
label = []
for loopi, data in node.iterrows():
data = pd.DataFrame(data)
knode = searchnode(data.T, tree, 3)
templabel = judge(knode)
label.append(templabel)
print(tree)
return label
最近邻法就没有统筹方法这一节的内容了。
附录:k近邻法Python原代码
import pandas as pd
import numpy as np
import math
def kdtree(dataset, loop):
subtree = {}
# figure out if it is leaf node
if dataset.shape[0] == 1:
subtree['node'] = list(dataset.iloc[0])
subtree['End'] = 1
return subtree
if dataset.empty:
return {}
# loop in feature list, and sort dataset
columnslist = dataset.columns.values.tolist()
loopi = loop % (dataset.shape[1] - 1)
dataset1 = dataset.sort_values(by=columnslist[loopi], axis=0, ascending=True)
# make the middle value as root node
index = int(dataset.shape[0] / 2)
node = list(dataset1.iloc[index])
subtree['axis'] = columnslist[loopi]
subtree['node'] = node
# left sub tree
loop += 1
left = dataset1[dataset1[columnslist[loopi]] < node[loopi]]
right = dataset1[dataset1[columnslist[loopi]] > node[loopi]]
print(left)
subtree1 = kdtree(left, loop)
subtree2 = kdtree(right, loop)
if len(subtree1)>0:
subtree['leftsubtree'] = subtree1
if len(subtree2)>0:
subtree['rightsubtree'] = subtree2
return subtree
def searchnode(dataset, tree, k):
# the function is to figure out a leaf node of target
axis_label = dataset.columns.values.tolist()
axis_label.extend(['label', 'distance'])
knode = pd.DataFrame(np.zeros((k, len(axis_label))), columns=axis_label)
subtree = tree
searchdir = []
while 'End' not in subtree:
print(subtree)
axis = subtree['axis']
index = axis_label.index(axis)
if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
subtree = subtree['leftsubtree']
searchdir.append('leftsubtree')
print(subtree)
else:
subtree = subtree['rightsubtree']
searchdir.append('rightsubtree')
leafnode = []
leafnode[:] = subtree['node']
dis = distance(leafnode[:len(leafnode)-1], dataset.iloc[0])
leafnode.append(dis)
knode.loc[0:2, axis_label] = leafnode
data = list(dataset.iloc[0].values)
knode = searchback(data, knode, tree, searchdir, k)
return knode
def judge(knode):
'''if knode['label'].sum()>0:
return 1
else:
return -1'''
return 1 if knode.label.sum() > 0 else -1
def searchback(data, knode, tree, searchdir, k):
subtree = tree
tempnode = []
axis_label = knode.columns.values.tolist()
for loopi in searchdir[:len(searchdir)-1]:
subtree = subtree[loopi]
# father node check
tempnode[:] = subtree['node']
dis = distance(tempnode[:len(tempnode)-1], data)
if dis<knode.distance.max():
tempnode.append(dis)
print(knode)
knode.loc[knode.distance.idxmax()] = tempnode
print(knode)
# sister node check
if searchdir[len(searchdir)-1] == 'leftsubtree' and ('rightsubtree' in subtree):
subtree2 = subtree['rightsubtree']
tempnode[:] = subtree2['node']
dis2 = distance(tempnode[:len(tempnode)-1], data)
if dis2<knode.distance.max():
tempnode.append(dis2)
print(knode)
knode.loc[knode.distance.idxmax()] = tempnode
print(knode)
elif searchdir[len(searchdir)-1] == 'rightsubtree' and ('leftsubtree' in subtree):
subtree2 = subtree['leftsubtree']
tempnode[:] = subtree2['node']
dis2 = distance(tempnode[:len(tempnode)-1], data)
if dis2<knode.distance.max():
tempnode.append(dis2)
print(knode)
knode.loc[knode.distance.idxmax()] = tempnode
print(knode)
if len(searchdir) > 1:
searchdir2 = searchdir[:len(searchdir)-1]
searchback(data, knode, tree, searchdir2, k)
return knode
def distance(data1, data2):
dis = 0
for loopi in range(len(data1)):
dis += (data1[loopi]-data2[loopi])**2
dis = math.sqrt(dis)
return dis
def searchleafnode(dataset, tree, k):
axis_label = dataset.columns.values.tolist()
while 'End' not in subtree:
axis = subtree['axis']
index = axis_label.index(axis)
if dataset.iloc[0][axis] < subtree['node'][index] or ('rightsubtree' not in subtree):
subtree = subtree['leftsubtree']
else:
subtree = subtree['rightsubtree']
return subtree['node']
def predict(node, tree, k):
label = []
for loopi, data in node.iterrows():
data = pd.DataFrame(data)
knode = searchnode(data.T, tree, 3)
templabel = judge(knode)
label.append(templabel)
print(tree)
return label
if __name__ =='__main__':
temp = []
with open(r'XX\knn\train_data.txt') as f:
for loopi in f.readlines():
line = loopi.strip().split('\t')
temp.append([float(line[0]), float(line[1]), float(line[2])])
data_set=pd.DataFrame(temp, columns=['x','y','label'])
vector = data_set[['x','y']]
tree = kdtree(data_set, 0)
print(tree)
# node is a target data to be predicted, dataframe structrue
node = vector.iloc[6:8] + 1
label = predict(node, tree, 3)
print(label)
文章导引列表:
机器学习
- 小瓜讲机器学习——分类算法(一)logistic regression(逻辑回归)算法原理详解
- 小瓜讲机器学习——分类算法(二)支持向量机(SVM)算法原理详解
- 小瓜讲机器学习——分类算法(三)朴素贝叶斯法(naive Bayes)
- 小瓜讲机器学习——分类算法(四)K近邻法算法原理及Python代码实现
- 小瓜讲机器学习——分类算法(五)决策树算法原理及Python代码实现
- 小瓜讲机器学习——聚类算法(一)K-Means算法原理Python代码实现
- 小瓜讲机器学习——聚类算法(二)Mean Shift算法原理及Python代码实现
- 小瓜讲机器学习——聚类算法(三)DBSCAN算法原理及Python代码实现
数据分析
- 小呆学数据分析——使用pandas中的merge函数进行数据集合并
- 小呆学数据分析——使用pandas中的concat函数进行数据集堆叠
- 小呆学数据分析——pandas中的层次化索引
- 小呆学数据分析——使用pandas的pivot进行数据重塑
- 小呆学数据分析——用duplicated/drop_duplicates方法进行重复项处理
- 小呆学数据分析——缺失值处理(一)
- 小呆学数据分析——异常值判定与处理(一)
- 小瓜讲数据分析——数据清洗