第三课.K近邻算法

KNN原理

基本思想

KNN即K-NearestNeighbor,又叫K近邻;KNN是一种简单的监督学习算法,可以用于分类也可以用于回归;
fig1
上图中,共有6个已知样本,类别与颜色对应,蓝色是一类,绿色是一类,现得到一个未知样本,要判断其类别;在图中,即样本点 X 应该属于哪种颜色?是蓝色还是绿色?
根据经验,我可以根据 X 的相邻样本点来判定。例如,和 X 距离最近的三个样本点中绿色占多数,那么 X 就属于为绿色;和 X 距离最近的 5 个样本点中蓝色占多数,那么 X 就属于蓝色。
这种判断方式正是 K 近邻算法的基本思想:根据 K 个近邻样本的类别来判断该未知样本的类别;K 近邻是监督学习中比较简单的一种算法,它既可以解决分类问题,也可以解决回归问题。
现在已经知道 K 近邻算法的基本思想是根据 K 个近邻样本的 y 值来预测自身的 y 值。具体到分类问题中,y 值就是样本类别的取值,一般采用多数表决的原则来对测试样本的类别进行预测。
对于回归问题:
fig2
回归问题中预测的 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=(a1b1)2+(a2b2)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=1n(aibi)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 值是人为设定的参数,在机器学习中的术语叫超参数;回想实例:
fig3

如果选择较大的 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

fig4
设置 train_df 的列索引(列名),前三列是特征列,最后一列是样本标记列:

train_df.columns = ['搞笑镜头','拥抱镜头','打斗镜头','电影类型']

fig5
查看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)

fig6

将两个 dataframe 在列维度上进行拼接, 相当于给原来的训练数据增加了一列 “欧式距离” ;然后对所有样本按照欧式距离进行递增排序:

result = pd.concat([train_df,distance_df],axis=1).sort_values(by="欧式距离")

fig7
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,可视化后如下:
fig8

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,作为左节点,否则作为右节点;

fig9
KD树如下,树中所有节点数目为13个,即数据集被分散到二叉树结构中;在一定程度上,树可以反映数据集隐含的空间信息,即减缓了KNN中与数据集样本逐个计算才得到最近样本的问题:
fig10

使用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.960.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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值