《神经网络与机器学习》11章 K近邻
神经网络与机器学习
第11章 k近邻算法
§11.1 算法引入
K近邻算法是属于监督学习,不具有显式学习过程,是1968年Cover和Hart提出的[Cover T, Hart P, Nearest neighbor pattern classification, IEEE Trans. On Information Theory, 1967],至今引用4000多次。


KNN算法是一种监督式学习算法,核心思想是用距离最近的k个样本数据的分类来代表目标数据的分类。从原理上来说,存在一个训练样本集,这个数据训练样本的数据集合中的每个样本都包含数据的特征和目标变量(即分类值),输入新的不含目标变量的数据,将该数据的特征与训练样本集中每一个样本进行比较,找到最相似的k个数据,这k个数据出现次数最多的分类,即输入的具有特征值的数据的分类。
举例:训练样本集中包含一系列数据,这个数据包括样本空间位置(特征)和分类信息(即目标变量,属于红色三角形还是蓝色正方形),要对中心的绿色数据的分类。运用kNN算法思想,距离最近的k个样本的分类来代表测试数据的分类,那么:
当k=3时,距离最近的3个样本在实线内,具有2个红色三角和1个蓝色正方形,因此将它归为红色三角。
当k=5时,距离最近的5个样本在虚线内,具有2个红色三角和3个蓝色正方形,因此将它归为蓝色正方形。

§11.2 k近邻模型
K近邻模型实际对应了特征空间的划分,三个要素是:距离度量,k值选择,分类决策。K近邻法(k-nearest neighbors,KNN)是一种很基本的机器学习方法了,在我们平常的生活中也会不自主的应用。比如,我们判断一个人的人品,只需要观察他来往最密切的几个人的人品好坏就可以得出了。这里就运用了KNN的思想。KNN方法既可以做分类,也可以做回归,这点和决策树算法相同。
KNN做回归和分类的主要区别在于最后做预测时候的决策方式不同。KNN做分类预测时,一般是选择多数表决法,即训练集里和预测的样本特征最近的K个样本,预测为里面有最多类别数的类别。而KNN做回归时,一般是选择平均法,即最近的K个样本的样本输出的平均值作为回归预测值。由于两者区别不大,虽然本文主要是讲解KNN的分类方法,但思想对KNN的回归方法也适用。
对于分类决策规则,一般都是使用前面提到的多数表决法。所以我们重点是关注与k值的选择和距离的度量方式。
对于k值的选择,没有一个固定的经验,一般根据样本的分布,选择一个较小的值,可以通过交叉验证选择一个合适的k值。选择较小的k值,就相当于用较小的领域中的训练实例进行预测,训练误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是泛化误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合;
选择较大的k值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少泛化误差,但缺点是训练误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单。

距离度量:K近邻算法的核心在于找到实例点的邻居,这个时候,问题就接踵而至了,如何找到邻居,邻居的判定标准是什么,用什么来度量。这一系列问题便是下面要讲的距离度量。
这是因为特征空间中两个实例点的距离可以反应出两个实例点之间的相似性程度。K近邻模型的特征空间一般是n维实数向量空间,使用的距离可以使欧式距离,也是可以是其它距离。
设特征空间是n维实数欧式空间,空间中两点$\boldsymbol{x}_i=[x_i^1,\cdots,x_i^n]^{\mathrm{T}}$,闵可夫斯基距离(Minkowski Distance),或者叫$L_p$范数,定义为
\[L_p(\boldsymbol{x}_i,\boldsymbol{x}_j)=\Big(\sum_{k=1}^n|x_i^k-x_j^k|^p\Big)^{1/p}\]
当p=2时,就是平常用的欧氏距离,p=1称为Manhattan曼哈顿距离,$p=\infty$就是坐标差的最大值。
不同距离产生的最近邻不一样!例如2维空间三点
\[\boldsymbol{x}_1=[1,1]^{\mathrm{T}}\\
\boldsymbol{x}_2=[5,1]^{\mathrm{T}}\\
\boldsymbol{x}_3=[4,4]^{\mathrm{T}}\]
取欧氏距离
\[L_2(\boldsymbol{x}_1,\boldsymbol{x}_2)=4,L_2(\boldsymbol{x}_1,\boldsymbol{x}_3)=4.24\]
$x_1$,$x_2$最近邻。$p=\infty$时
\[L_{\infty}(\boldsymbol{x}_1,\boldsymbol{x}_2)=4,L_{\infty}(\boldsymbol{x}_1,\boldsymbol{x}_3)=3\]
因此$x_1$,$x_3$最近邻。
除了欧氏距离,还有很多距离,比如以前讲的汉明距离,两个等长字符串s1与s2之间的汉明距离定义为将其中一个变为另外一个所需要作的最小替换次数。例如字符串"1111"与"1001"之间的汉明距离为2。或者定义为异或数目。
马氏距离(Mahalanobis Distance)
\[\sqrt{(\boldsymbol{x}_i-\boldsymbol{x}_j)^{\mathrm{T}}(\sum)^{-1}(\boldsymbol{x}_i-\boldsymbol{x}_j)}\]
$\sum$是数据的协方差矩阵,这样求距离可以消除相关性。
分类决策规则:分类规则经常采用多数表决,由输入k个近邻的多数类决定输入数据的类别。k个训练数据点构成的集合$N_k$,而覆盖此集合的类别是$c_j$,取值0或1,那么误分类概率就是
\[\frac{1}{k}\sum_{\boldsymbol{x}_i\in N_k}P(y_j\neq c_j)=1-\frac{1}{k}\sum_{\boldsymbol{x}_i\in N_k}P(y_j= c_j)\]
因此误分类概率最小就是等价经验风险最小,就要使得多数表决规则$\frac{1}{k}\sum_{\boldsymbol{x}_i\in N_k}P(y_j= c_j)$最大。
§11.3 k近邻的实现:kd树
既然我们要找到k个最近的邻居来做预测,那么我们只需要计算预测样本和所有训练集中的样本的距离,然后计算出最小的k个距离即可,接着多数表决,很容易做出预测。这个方法的确简单直接,在样本量少,样本特征少的时候有效。但是在实际运用中很多时候用不上,为什么呢?因为我们经常碰到样本的特征数有上千以上,样本量有几十万以上,如果我们这要去预测少量的测试集样本,算法的时间效率很成问题。因此,这个方法我们一般称之为蛮力实现。比较适合于少量样本的简单模型的时候用。
KD树算法没有一开始就尝试对测试样本分类,而是先对训练集建模,建立的模型就是KD树,建好了模型再对测试集做预测。所谓的KD树就是K个特征维度的树,注意这里的K和KNN中的K的意思不同。KNN中的K代表最近的K个样本,KD树中的K代表样本特征的维数。为了防止混淆,后面我们称特征维数为n。KD树算法包括三步,第一步是建树,第二部是搜索最近邻,最后一步是预测。
我们首先来看建树的方法。KD树建树采用的是从m个样本的n维特征中,分别计算n个特征的取值的方差,用方差最大的第k维特征$n_k$来作为根节点。对于这个特征,我们选择特征$n_k$的取值的中位数$n_{kv}$对应的样本作为划分点,对于所有第k维特征的取值小于$n_{kv}$的样本,我们划入左子树,对于第k维特征的取值大于等于$n_{kv}$的样本,我们划入右子树,对于左子树和右子树,我们采用和刚才同样的办法来找方差最大的特征来做更节点,递归的生成KD树。
https://blog.csdn.net/john_xia/article/details/107563005
https://www.cnblogs.com/pinard/p/6164214.html
例子:比如我们有二维样本6个,{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},构建kd树的具体步骤为:
1)找到划分的特征。6个数据点在x,y维度上的数据方差分别为6.97,5.37,所以在x轴上方差更大,用第1维特征建树。
2)确定划分点(7,2)。根据x维上的值将数据排序,6个数据的中值(所谓中值,即中间大小的值)为7,所以划分点的数据是(7,2)。这样,该节点的分割超平面就是通过(7,2)并垂直于:划分点维度的直线x=7;
3)确定左子空间和右子空间。分割超平面x=7将整个空间分为两部分:x<=7的部分为左子空间,包含3个节点={(2,3),(5,4),(4,7)};另一部分为右子空间,包含2个节点={(9,6),(8,1)}。
4)用同样的办法划分左子树的节点{(2,3),(5,4),(4,7)}
>> var([2 5 4])
ans =
2.3333
>> var([3 4 7])
ans =
4.3333
和右子树的节点{(9,6),(8,1)}。
>> var([6 1])
ans =
12.5000
最终得到KD树如下:

KD树搜索最近邻
生成KD树以后,就可以去预测测试集里面的样本目标点了。对于一个目标点,我们首先在KD树里面找到包含目标点的叶子节点。以目标点为圆心,以目标点到叶子节点样本实例的距离为半径,得到一个超球体,最近邻的点一定在这个超球体内部。然后返回叶子节点的父节点,检查另一个子节点包含的超矩形体是否和超球体相交,如果相交就到这个子节点寻找是否有更加近的近邻,有的话就更新最近邻。如果不相交那就简单了,我们直接返回父节点的父节点,在另一个子树继续搜索最近邻。当回溯到根节点时,算法结束,此时保存的最近邻节点就是最终的最近邻。
从上面的描述可以看出,KD树划分后可以大大减少无效的最近邻搜索,很多样本点由于所在的超矩形体和超球体不相交,根本不需要计算距离。大大节省了计算时间。
来看对点(2,4.5)找最近邻的过程:

先进行二叉查找,先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,由于查找点为y值为4.5,因此进入右子空间查找到(4,7),形成搜索路径<(7,2),(5,4),(4,7)>,但(4,7)与目标查找点的距离为3.202,而(5,4)与查找点之间的距离为3.041,所以(5,4)为查询点的最近点;以(2,4.5)为圆心,以3.041为半径作圆,如下图所示。
可见该圆和y = 4超平面交割,所以需要进入(5,4)左子空间进行查找,也就是将(2,3)节点加入搜索路径中得<(7,2),(2,3)>;于是接着搜索至(2,3)叶子节点,(2,3)距离(2,4.5)比(5,4)要近,所以最近邻点更新为(2,3),最近距离更新为1.5。回溯查找至(5,4),直到最后回溯到根结点(7,2)的时候,以(2,4.5)为圆心1.5为半径作圆,并不和x = 7分割超平面交割,如下图所示。至此,搜索路径回溯完,返回最近邻点(2,3),最近距离1.5。对应的图如下:

程序:给定数据,建立KD树,然后测试点[6,3.1]的kd树。
clear;
close all;
clc;
% 生成数据
data = [2 3;
5 4;
9 6;
4 7;
8 1;
7 2];
% 给数据标号
for i = 1: size(data,1)
data(i,3) = i;
end
Kd_tree = Kd_tree_create(data) % 建立Kd树
closest = Kd_tree_search_knn(Kd_tree, [6,3.1], 2) % 利用Kd树进行kNN查询
closest =
5 4
2 3
Kd_tree =
包含以下字段的 6×1 struct 数组:
id
node
dim
parent
left
right
子函数
function [tree] = Kd_tree_create(data)
% variance rule
[num, dimension] = size(data);
dimension = dimension - 1;
for i = 1: dimension
data_var(i) = var(data(:,i));
end
[~, choose_dim] = max(data_var);
data = sortrows(data, choose_dim);
tree.id = data(round(num/2),end);
tree.node = data(round(num/2),1:end-1);
tree.dim = choose_dim;
tree.parent = [];
tree.left = [];
tree.right = [];
% kd tree
lefttree = [];
righttree = [];
if round(num/2) > 1
leftdata = data(1:(round(num/2)-1), :);
lefttree = Kd_tree_create(leftdata);
for i = 1: size(lefttree, 1)
if isempty(lefttree(i).parent)
lefttree(i).parent = tree.id;
tree.left = lefttree(i).id;
end
end
end
if round(num/2) < num
rightdata = data((round(num/2)+1):end, :);
righttree = Kd_tree_create(rightdata);
for i = 1: size(righttree, 1)
if isempty(righttree(i).parent)
righttree(i).parent = tree.id;
tree.right = righttree(i).id;
end
end
end
tree = [tree; lefttree];
tree = [tree; righttree];
end
function [closest_point] = Kd_tree_search_knn(Kd_tree, data, n)
% 从根节点开始一直查询到叶节点,找到和data在一个区域的叶节点
closest = Kd_tree(1);
while(1)
if closest.node(closest.dim) >= data(closest.dim) && ~isempty(closest.left)
closest = Kd_tree(find([Kd_tree.id]==closest.left));
elseif closest.node(closest.dim) <= data(closest.dim) && ~isempty(closest.right)
closest = Kd_tree(find([Kd_tree.id]==closest.right));
else
break
end
end
Kd_tree(find([Kd_tree.id]==closest.id)).done = 1;
closest_point = closest.node;
[max_dis, max_idx] = max(sum((closest_point - data).^2, 2));
max_dis = max_dis(1);
max_idx = max_idx(1);
% 从当前节点向上回溯
node_now = closest;
while(1)
% 回溯到根节点就break
if find([Kd_tree.id]==node_now.id) == 1
break
end
% 回溯到父节点,如果父节点的点符合要求就添加进去
node_now = Kd_tree(find([Kd_tree.id]==node_now.parent));
Kd_tree(find([Kd_tree.id]==node_now.id)).done = 1;
if size(closest_point, 1) < n
[max_dis, max_idx] = max(sum((closest_point - data).^2, 2));
closest_point(end+1, :) = node_now.node;
elseif sum((node_now.node-data).^2) < max_dis
closest_point(max_idx, :) = node_now.node;
[max_dis, max_idx] = max(sum((closest_point - data).^2, 2));
max_dis = max_dis(1);
max_idx = max_idx(1);
end
% % 检查data点到父节点的分割线的距离,如果距离小于当前最大距离,则可能在另
%一侧有符合要求的点
closest_temp = [];
if (data(node_now.dim)-node_now.node(node_now.dim))^2 < max_dis
if ~isempty(node_now.left)
node_temp = Kd_tree(find([Kd_tree.id]==node_now.left));
if isempty(node_temp.done)
%% 把左子节点调到第一行,作为根节点,递归调用
Kd_tree_temp = Kd_tree;
Kd_tree_temp(1) = node_temp;
Kd_tree_temp(find([Kd_tree.id]==node_temp.id)) = Kd_tree(1);
closest_temp = [closest_temp; Kd_tree_search_knn(Kd_tree_temp, data, n)];
end
end
if ~isempty(node_now.right)
node_temp = Kd_tree(find([Kd_tree.id]==node_now.right));
if isempty(node_temp.done)
% 把右子节点调到第一行,作为根节点,递归调用
Kd_tree_temp = Kd_tree;
Kd_tree_temp(1) = node_temp;
Kd_tree_temp(find([Kd_tree.id]==node_temp.id)) = Kd_tree(1);
closest_temp = [closest_temp; Kd_tree_search_knn(Kd_tree_temp, data, n)];
end
end
end
if ~isempty(closest_temp)
closest_temp_dis = sum((closest_temp - data).^2, 2);
for i = 1: size(closest_temp_dis, 1)
if closest_temp_dis(i) < max_dis
closest_point(max_idx, :) = closest_temp(i, :);
[max_dis, max_idx] = max(sum((closest_point - data).^2, 2));
max_dis = max_dis(1);
max_idx = max_idx(1);
end
end
end
end
end

浙公网安备 33010602011771号