KNN原理
基本思想
KNN即K-NearestNeighbor,又叫K近邻;KNN是一种简单的监督学习算法,可以用于分类也可以用于回归;
上图中,共有6个已知样本,类别与颜色对应,蓝色是一类,绿色是一类,现得到一个未知样本,要判断其类别;在图中,即样本点 X 应该属于哪种颜色?是蓝色还是绿色?
根据经验,我可以根据 X 的相邻样本点来判定。例如,和 X 距离最近的三个样本点中绿色占多数,那么 X 就属于为绿色;和 X 距离最近的 5 个样本点中蓝色占多数,那么 X 就属于蓝色。
这种判断方式正是 K 近邻算法的基本思想:根据 K 个近邻样本的类别来判断该未知样本的类别;K 近邻是监督学习中比较简单的一种算法,它既可以解决分类问题,也可以解决回归问题。
现在已经知道 K 近邻算法的基本思想是根据 K 个近邻样本的 y 值来预测自身的 y 值。具体到分类问题中,y 值就是样本类别的取值,一般采用多数表决的原则来对测试样本的类别进行预测。
对于回归问题:
回归问题中预测的 y 值是一个连续值,上图中每个样本点周围的数字代表其 y 值,K近邻是将离 X 最近的 K个样本的 y 值的平均值作为 X 的预测 y 值。
例如:K=3 时,X 的预测输出 y = (1.2+3.4+8.3)/3 = 4.3;K=5 时,X 的预测输出 y = (1.2+3.4+8.3+5.5+4.5)/5 = 4.58
欧氏距离
KNN中样本距离的计算,一般选取的是欧式距离:
a
=
(
a
1
,
a
2
)
,
b
=
(
b
1
,
b
2
)
a=(a_{1},a_{2}),b=(b_{1},b_{2})
a=(a1,a2),b=(b1,b2)
d
=
(
a
1
−
b
1
)
2
+
(
a
2
−
b
2
)
2
d=\sqrt{(a_{1}-b_{1})^{2}+(a_{2}-b_{2})^{2}}
d=(a1−b1)2+(a2−b2)2
a
a
a 和
b
b
b 代表两个样本,更具体一点说是样本的特征向量,每个样本的特征向量维度是2,即:每个样本都有两个特征。延伸到高维空间可得:
a
=
(
a
1
,
a
2
,
.
.
.
,
a
n
)
,
b
=
(
b
1
,
b
2
,
.
.
.
,
b
n
)
a=(a_{1},a_{2},...,a_{n}),b=(b_{1},b_{2},...,b_{n})
a=(a1,a2,...,an),b=(b1,b2,...,bn)
d
=
∑
i
=
1
n
(
a
i
−
b
i
)
2
d=\sqrt{\sum_{i=1}^{n}(a_{i}-b_{i})^{2}}
d=i=1∑n(ai−bi)2
算法流程
K 近邻分类算法的流程:
- 1.准备训练样本集:data = { [ x 1 , x 2 , . . . , x n , y ] [x_{1},x_{2},...,x_{n},y] [x1,x2,...,xn,y],…},其中 x 1 , x 2 , . . . , x n x_{1},x_{2},...,x_{n} x1,x2,...,xn 是样本特征, y y y 是样本类别取值
- 2.输入测试样本 A: [ x 1 , x 2 , . . . , x n ] [x_{1},x_{2},...,x_{n}] [x1,x2,...,xn]
- 3.计算测试样本 A 和所有训练样本的欧式距离
- 4.按照距离递增排序,选取与 A 距离最小的 K 个样本,计算这 K 个样本所在类别的出现频率,返回出现频率最高的类别作为 A 的预测分类
KNN的影响因素
K 近邻中的 K 值是人为设定的参数,在机器学习中的术语叫超参数;回想实例:
如果选择较大的 K 值,则和 X 距离较远的点也会对预测结果产生影响;极端情况下 K 值等于训练样本个数时,无论输入的测试样本是什么,预测结果都将是训练样本中最多的类。在实际应用中,K 值取比较小的数,一般低于训练样本数的平方根;此外,还可以采用交叉验证的方法来选择最优的 K 值:
首先将数据分为训练集和测试集,然后使用不同的 K 值(如:1,3,5,7,…)进行实验,最后选出在测试集上误差最小的 K 值;
除了 K 值外,K 近邻的预测结果还受距离度量和决策规则的影响。距离度量实际上是衡量两个样本的相似程度:距离越小,相似程度越高。常见的相似性度量函数有:欧氏距离、余弦相似性、皮尔逊相关系数。K 近邻中的分类决策规则一般是多数表决,如果采用其他的决策方式,相应的预测结果也会发生变化
KNN电影主题分类
导入numpy和pandas,numpy 和 pandas 是 python 中常见的两个库: numpy 可以用来存储和处理大型矩阵,比 python 自身的嵌套列表结构要高效的多;pandas 是基于 numpy 的一种工具,该工具是为了解决数据分析任务而创建的。
import numpy as np
import pandas as pd
计算欧式距离:
def euclidean_distance(vec1,vec2):
return np.sqrt(np.sum(np.square(vec1 - vec2)))
构造数据集,训练数据使用字典的形式存储,字典的键作为样本索引,值作为样本记录,使用列表存储。列表中的前 3 个值是样本的特征(搞笑镜头、拥抱镜头、打斗镜头),最后一个值是样本的标注(电影类型),测试数据也是使用字典的形式存储,但是没有标记值:
train_data = {'宝贝当家':[45,2,9,'喜剧片'],
'美人鱼':[21,17,5,'喜剧片'],
'澳门风云3':[54,9,11,'喜剧片'],
'功夫熊猫3':[39,0,31,'喜剧片'],
'谍影重重':[5,2,57,'动作片'],
'叶问3':[3,2,65,'动作片'],
'我的特工爷爷':[6,4,21,'动作片'],
'奔爱':[7,46,4,'爱情片'],
'夜孔雀':[9,39,8,'爱情片'],
'代理情人':[9,38,2,'爱情片'],
'新步步惊心':[8,34,17,'爱情片'],
'伦敦陷落':[2,3,55,'动作片']
}
test_data = {'唐人街探案':[23,3,17]}
现在要用KNN预测未知数据test_data的电影类型,为了便于后续的数据分析操作,将训练数据 train_data 转换成 dataframe 类型;
字典 train_data 中的 “键”(电影名称) 在转换为 dataframe 时是对应列名的,所以需要进行转置操作(df.T)将电影名称从 dataframe 的列索引变成行索引:
train_df = pd.DataFrame(train_data).T
设置 train_df 的列索引(列名),前三列是特征列,最后一列是样本标记列:
train_df.columns = ['搞笑镜头','拥抱镜头','打斗镜头','电影类型']
查看dataframe的内容:
train_df.values
"""
array([[9, 38, 2, '爱情片'],
[2, 3, 55, '动作片'],
[39, 0, 31, '喜剧片'],
[3, 2, 65, '动作片'],
[9, 39, 8, '爱情片'],
[7, 46, 4, '爱情片'],
[45, 2, 9, '喜剧片'],
[6, 4, 21, '动作片'],
[8, 34, 17, '爱情片'],
[54, 9, 11, '喜剧片'],
[21, 17, 5, '喜剧片'],
[5, 2, 57, '动作片']], dtype=object)
"""
确定K值和分类的电影:
K = 5
movie = '唐人街探案'
计算欧式距离:
distance_list = []
for train_X in train_df.values[:,:-1]:
test_X = np.array(test_data[movie])
# 计算新电影《唐人街探案》与训练集中所有电影的欧氏距离
distance_list.append(euclidean_distance(train_X,test_X))
distance_list
转为dataframe有:
distance_df = pd.DataFrame({"欧式距离":distance_list},index=train_df.index)
将两个 dataframe 在列维度上进行拼接, 相当于给原来的训练数据增加了一列 “欧式距离” ;然后对所有样本按照欧式距离进行递增排序:
result = pd.concat([train_df,distance_df],axis=1).sort_values(by="欧式距离")
concat参数axis=1
代表在列轴方向操作,即沿列轴方向拼接;
获取分类结果:
print(movie,result.head(K)['电影类型'].max())
# 唐人街探案 喜剧片
扩展:k-dimensional tree
KD树(k-dimensional tree),也可称之为K维树,可以用更高的效率来对空间进行划分,KD树常作为KNN的优化方法,KNN在获得距离当前样本最近的K个样本时,需要浏览整个数据集,效率低,使用KD树,可以在二叉树形成的空间基础上,快速找到最近的K个样本,从而提高KNN的效率;
KD树的构建
首先构造KD树,实例如下,假设在空间
R
2
\mathbb{R}^{2}
R2中生成13个样本,即每个样本有两个特征
x
x
x和
y
y
y,可视化后如下:
KD树的构建过程如下:
- 1.沿着第一个特征划分数据,选择特征 x x x上的中位数对应样本作为根节点 ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50),所有特征 x x x小于 6.27 6.27 6.27的样本作为左子树节点集合,所有特征 x x x大于 6.27 6.27 6.27的样本作为右子树节点集合,如图(a);
- 2.递进到下一个特征
y
y
y,在(a)的两个子集中分别按照特征
y
y
y划分样本;对于(a)的左子树节点,特征
y
y
y的中位数对应样本为
(
1.24
,
−
2.86
)
(1.24,-2.86)
(1.24,−2.86),该样本作为(a)中左子树的根节点,(a)的左子树节点集合中特征
y
y
y小于
−
2.86
-2.86
−2.86的样本成为当前子集的左子树节点集合,特征
y
y
y大于
−
2.86
-2.86
−2.86的样本成为当前子集的右子树节点集合;同理,(a)的右子树中位数样本为
(
17.05
,
−
12.79
)
(17.05,-12.79)
(17.05,−12.79),该子集下又可以得到左子树节点集合和右子树节点集合;如图(b);
现在得到KD树的三个根节点:一个数据集的根节点 ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50),两个子集的根节点 ( 1.24 , − 2.86 ) (1.24,-2.86) (1.24,−2.86)和 ( 17.05 , − 12.79 ) (17.05,-12.79) (17.05,−12.79); - 3.在2中得到了两个子树的根节点,由于数据是二分方式的划分,两个根节点对应4个子集,递进到下一个特征,由于数据只有两个特征,所以该步回到按照第一个特征
x
x
x来划分;即按照特征
x
x
x对4个子集分别进行样本划分;得到4个子集的根节点为
(
−
6.88
,
−
5.40
)
,
(
−
2.96
,
−
0.50
)
,
(
7.75
,
−
22.68
)
,
(
10.80
,
−
5.03
)
(-6.88,-5.40),(-2.96,-0.50),(7.75,-22.68),(10.80,-5.03)
(−6.88,−5.40),(−2.96,−0.50),(7.75,−22.68),(10.80,−5.03);
此时,样本集合被划分成8个子集,如图(c); - 4.当递进到特征 y y y准备继续划分样本时,8个子集内都各自只剩一个或零个节点,已经不能再划分,将8个子集下的节点记入二叉树,得到KD树;基于在3中得到的4个根节点,将8个子集下的节点与对应的根节点比较,特征 x x x小于根节点的特征 x x x,作为左节点,否则作为右节点;
KD树如下,树中所有节点数目为13个,即数据集被分散到二叉树结构中;在一定程度上,树可以反映数据集隐含的空间信息,即减缓了KNN中与数据集样本逐个计算才得到最近样本的问题:
使用KD树获取最近的K个样本
假设现在面对一个测试样本 ( − 1 , − 5 ) (-1,-5) (−1,−5),使用L2距离计算样本之间的距离,目标是在数据集(上面的13个样本)中找到距离该样本最近的前3个样本,即 K = 3 K=3 K=3;
KD树获取样本的过程如下:
- 首先将测试样本与根节点 ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50)在特征 x x x上比较, − 1 < 6.27 -1<6.27 −1<6.27,向左子树搜索;
- 在特征 y y y上与当前树根节点 ( 1.24 , − 2.86 ) (1.24,-2.86) (1.24,−2.86)比较, − 5 < − 2.86 -5<-2.86 −5<−2.86,向当前树的左子树搜索;
- 在特征 x x x上与当前树根节点 ( − 6.88 , − 5.40 ) (-6.88,-5.40) (−6.88,−5.40)比较, − 1 > − 6.88 -1>-6.88 −1>−6.88,向当前树的右子树搜索,但是当前树只有一个子树,所以可以将该树的叶节点 ( − 4.60 , − 10.55 ) (-4.60,-10.55) (−4.60,−10.55)作为找到的第一个最近样本;将节点 ( − 4.60 , − 10.55 ) (-4.60,-10.55) (−4.60,−10.55)标记为"已访问";
- 当前所处的节点不是KD树的根节点,所以要往回走,回到节点 ( − 6.88 , − 5.40 ) (-6.88,-5.40) (−6.88,−5.40),该节点还没被标记过,而且还没找满3个节点,所以现在将该节点作为新增的最近样本,并标记为"已访问";考虑到当前节点的子树可能存在更近节点,应当搜索当前节点下的另一个分支,但是当前节点的分支已经访问过,所以再往回走;
- 回到
(
1.24
,
−
2.86
)
(1.24,-2.86)
(1.24,−2.86),同上一步,由于还没找满3个节点,所以将
(
1.24
,
−
2.86
)
(1.24,-2.86)
(1.24,−2.86)标记为已访问,并作为新增的最近样本;虽然已经找满了3个节点,但目前节点
(
1.24
,
−
2.86
)
(1.24,-2.86)
(1.24,−2.86)下还存在分支节点
(
−
2.96
,
−
0.50
)
(-2.96,-0.50)
(−2.96,−0.50)没被访问,所以要考虑该分支是否会更近;
注意现在3个节点分别为 ( − 4.60 , − 10.55 ) , ( − 6.88 , − 5.40 ) , ( 1.24 , − 2.86 ) (-4.60,-10.55),(-6.88,-5.40),(1.24,-2.86) (−4.60,−10.55),(−6.88,−5.40),(1.24,−2.86),可计算出测试样本到3个节点的距离分别为 6.62 , 5.89 , 3.10 6.62,5.89,3.10 6.62,5.89,3.10,但计算当前节点 ( 1.24 , − 2.86 ) (1.24,-2.86) (1.24,−2.86)下的未访问分支节点 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50)与测试样本距离为 4.90 4.90 4.90,小于目前3个节点与测试样本的最大距离,于是从节点 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50)开始与测试样本比较特征 x x x; - − 1 > − 2.96 -1>-2.96 −1>−2.96,所以搜索 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50)的右子树 ( 1.75 , 12.26 ) (1.75,12.26) (1.75,12.26), ( 1.75 , 12.26 ) (1.75,12.26) (1.75,12.26)已经没有子节点,直接计算测试样本与其的距离为 17.48 17.48 17.48,均大于{ 6.62 , 5.89 , 3.10 6.62,5.89,3.10 6.62,5.89,3.10},所以不记录,往回走,搜索 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50)的左子树 ( − 4.96 , 12.61 ) (-4.96,12.61) (−4.96,12.61),左子树 ( − 4.96 , 12.61 ) (-4.96,12.61) (−4.96,12.61)没有子节点,直接计算距离为 18.04 18.04 18.04,大于{ 6.62 , 5.89 , 3.10 6.62,5.89,3.10 6.62,5.89,3.10}的最大距离 6.62 6.62 6.62,所以往回走,来到 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50),已知与测试样本距离小于{ 6.62 , 5.89 , 3.10 6.62,5.89,3.10 6.62,5.89,3.10}的最大距离 6.62 6.62 6.62,用节点 ( − 2.96 , − 0.50 ) (-2.96,-0.50) (−2.96,−0.50)代替之前找到的 ( − 4.60 , − 10.55 ) (-4.60,-10.55) (−4.60,−10.55),并标记"已访问";
- 往回走到 ( 1.24 , − 2.86 ) (1.24,-2.86) (1.24,−2.86),该节点与该节点的两个子节点均已访问,往回走到 ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50), ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50)与测试样本距离为 12.77 12.77 12.77,均大于{ 4.90 , 5.89 , 3.10 4.90,5.89,3.10 4.90,5.89,3.10},在访问 ( 6.27 , 5.50 ) (6.27,5.50) (6.27,5.50)的右子树前,检测到其已经是根节点,所以可以返回结果;
- 得到三个最近样本 ( − 2.96 , − 0.50 ) , ( − 6.88 , − 5.40 ) , ( 1.24 , − 2.86 ) (-2.96,-0.50),(-6.88,-5.40),(1.24,-2.86) (−2.96,−0.50),(−6.88,−5.40),(1.24,−2.86)