K近邻(KNN)算法原理与实践

这次我来介绍一下k近邻法(k-nearest neighbor, KNN)的基本原理以及在scikit-learn中的应用。这是一种看起来结构和原理都挺简单的机器学习算法,主要的数据结构就是kd树的构造和搜索,在scikit-learn中的例子也比较少。

K近邻算法的原理

k近邻的原理很简单,给定一个数据集,对于新来的数据,我们首先定义一个数k,然后找到离这个新加入的数据点最近的k个点,然后找出这k个点里面最多的类别,然后以这个类别作为新加入的点的类。

其中的一种特例就是k等于1的情况,这种情况叫做最近邻,就是以离新加入数据点最近的点的类别作为新的点的类别。

可以看出KNN有几个重要的特色,首先它很简单,基本不需要训练,其实只是用一个数据结构存储一下。它是“懒惰学习”的著名代表。
第二,这个值k对结果的影响很大
这里写图片描述

上图来自于维基百科
对于新加入的点(绿色),它的类别应该是什么?如果以实线这个圈来算的话,应该是红色的,但是以虚线的这个圈来算的话又应该是蓝色的。所以实际中对于k应该仔细选取。

KNN实现原理:kd树

找出最近的K个点,最简单的算法就是线性搜索,但是这个的代价对于大数据集来说太大了,所以我们有了kd树。

kd树是一种二叉树,表示对k维空间的划分,构成kd树相当于不断地递归地用超平面划分空间,直到实例点划分完毕。最后每个节点都代表一个k维空间的超矩形。

算法
数据集包含N个样本点:

D={x1,x2,,xN}

每个样本都是k维的:
xi=(x1i,x22,,xki)

(1)选择 x1 为坐标轴, 所有样本点的 x1 分量的中位数为切分点,将全空间分成两个部分。
(2)对子区间递归地进行切分,对深度为 j 的节点,选择xl为切分的坐标轴,其中 l=j(modk)+1 ,对子区间的 xl 坐标的中位数做为切分点。重复(2)直到没有样本未被分过。

下面是一个例子,数据集为:

D={(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}

最后生成的划分如下图
这里写图片描述
(图片来自百度百科)

其实在我看来kd-树的构成最主要的就是一方面递归地划分,另一方面每次使用的维度是按顺序循环的

kd树的搜索

kd树也是一种二叉树,它的搜索也和二叉树有一些相似的地方,之所以能够加速查找就在于树里面的数据相互之间具有一定的有序关系,那么我们有时候就能够根据根节点和所要查找数据之间的关系直接略过一整棵子树。

对应于具体的二叉树问题,也是类似的,我们这里考虑最近邻,也就是k=1的情况。
(1)首先我们通过不断地比较查询点各个维度的数值,沿着二叉树下降直到叶节点。
(2)这个叶节点并不一定是最近邻,只是我们假设这个点为最近邻,以相互之间的距离为半径画一个超球,然后往上到父节点。如果父节点所代表的超平面不和这个超球相交,那么向上回退
(3)如果相交的话还要到父节点的另一个子节点去搜索,如果碰到更近的点,就更新最近邻点
(4)当回退到根节点的时候搜索结束。

scikit-learn中KNN的使用

下面介绍一下scikit-learn 中KNN的使用,KNN的使用相对来说很简单,没有太多需要讲的的,只是在其中出现了一种叫做Kernel Density Estimation 的模型,感觉挺有意思的,在这里也介绍一下。

例子一

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn import neighbors, datasets

n_neighbors = 15
#确定最近邻的个数,也就是k的值
# import some data to play with
iris = datasets.load_iris()

# we only take the first two features. We could avoid this ugly
# slicing by using a two-dim dataset
X = iris.data[:, :2]
y = iris.target
#输入数据只取前两个特征
h = .02 # step size in the mesh

# Create color maps
cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA', '#AAAAFF'])
cmap_bold = ListedColormap(['#FF0000', '#00FF00', '#0000FF'])

for weights in ['uniform', 'distance']:
#这里的'uniform'和'distance'代表两种权重函数,'uniform'代表各个点之间的权重是相同的
#'distance'代表权重是距离的倒数,这样距离近的点权重就更大
# we create an instance of Neighbours Classifier and fit the data.  
    clf = neighbors.KNeighborsClassifier(n_neighbors, weights=weights)
    clf.fit(X, y)

# Plot the decision boundary. For that, we will assign a color to each
# point in the mesh [x_min, x_max]x[y_min, y_max].
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
    np.arange(y_min, y_max, h))
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])

# Put the result into a color plot
    Z = Z.reshape(xx.shape)
    #上面这些代码都是很常见的如果有不懂的可以看我上一篇讲SVM的应用的博客

    plt.figure()
    plt.pcolormesh(xx, yy, Z, cmap=cmap_light)

# Plot also the training points
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold,
edgecolor='k', s=20)
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.title("3-Class classification (k = %i, weights = '%s')"
% (n_neighbors, weights))


plt.show()

可以看到代码本身是很简单的,核心的代码就是那两行训练的代码,搞懂那两个权重是什么也就行了。关于画图方面看不懂的话可以看看我的上一篇博客,支持向量机原理与实践(二):scikit-learn中SVM的使用,上面有些介绍,更好的是去相关官网上查看。

例子二

这个例子是关于核密度估计(Kernel Density Estimation )的,KDE是一种生成模型,训练完成后,我们可以自己从其中生成数据。这种算法也是使用kd树或者BallTree实现的。
核密度估计也有一个重要的概念“核函数”,但这个“核函数”应该和SVM中的不一样(我没看到推导过程,所以不是很清楚)。对于一个核函数 K(x;h) ,一个重要的参数是 h 另一个就是核函数本身的形式,常用的核函数有
这里写图片描述
他们的图像为
这里写图片描述

那么有了核函数,模型是什么样的?
对于给定的点y,和一组数据点,它的密度估计为

ρK(y)=i=1NK((yxi)/h)

对于参数 h <script id="MathJax-Element-94" type="math/tex">h</script>,它的名称是带宽(badnwidth),较大的带宽可以使分布比较平滑,但是偏差较大,较小的带宽使分布不光滑,但是方差较大。

下面是例子:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_digits
from sklearn.neighbors import KernelDensity
from sklearn.decomposition import PCA
from sklearn.model_selection import GridSearchCV

# load the data
digits = load_digits()
data = digits.data

# project the 64-dimensional data to a lower dimension
pca = PCA(n_components=15, whiten=False)
data = pca.fit_transform(digits.data)
#上面利用PCA降维,将64维的图片向量降为15维
# use grid search cross-validation to optimize the bandwidth
params = {'bandwidth': np.logspace(-1, 1, 20)}
grid = GridSearchCV(KernelDensity(), params)
grid.fit(data)
#利用GridSearcnCV寻找最优参数,这个在SVM的相关博客中我也介绍过了。

print("best bandwidth: {0}".format(grid.best_estimator_.bandwidth))

# use the best estimator to compute the kernel density estimate
kde = grid.best_estimator_
#kde就是找出的最优的estimator
# sample 44 new points from the data
new_data = kde.sample(44, random_state=0)
#从模型中随机生成44个点,这个目前只在使用gaussian 和 tophat的核函数中使用
new_data = pca.inverse_transform(new_data)
#转换成原来的维度

# turn data into a 4x11 grid
new_data = new_data.reshape((4, 11, -1))
real_data = digits.data[:44].reshape((4, 11, -1))

# plot real digits and resampled digits
fig, ax = plt.subplots(9, 11, subplot_kw=dict(xticks=[], yticks=[]))
for j in range(11):
    ax[4, j].set_visible(False)
    for i in range(4):
        im = ax[i, j].imshow(real_data[i, j].reshape((8, 8)),
        cmap=plt.cm.binary, interpolation='nearest')
        im.set_clim(0, 16)
        im = ax[i + 5, j].imshow(new_data[i, j].reshape((8, 8)),
        cmap=plt.cm.binary, interpolation='nearest')
        im.set_clim(0, 16)

ax[0, 5].set_title('Selection from the input data')
ax[5, 5].set_title('"New" digits drawn from the kernel density model')
#上面就是画图了
plt.show()

最后生成的结果是
这里写图片描述
可以看到下面生成的数据跟原数据还是很像的,不过我没有看到这种方法的原理,也不知道它的作用大不大,以后有机会了可以研究一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值