这次我来介绍一下k近邻法(k-nearest neighbor, KNN)的基本原理以及在scikit-learn中的应用。这是一种看起来结构和原理都挺简单的机器学习算法,主要的数据结构就是kd树的构造和搜索,在scikit-learn中的例子也比较少。
k近邻的原理很简单,给定一个数据集,对于新来的数据,我们首先定义一个数k,然后找到离这个新加入的数据点最近的k个点,然后找出这k个点里面最多的类别,然后以这个类别作为新加入的点的类。
其中的一种特例就是k等于1的情况,这种情况叫做最近邻,就是以离新加入数据点最近的点的类别作为新的点的类别。
可以看出KNN有几个重要的特色,首先它很简单,基本不需要训练,其实只是用一个数据结构存储一下。它是“懒惰学习”的著名代表。
第二,这个值k对结果的影响很大
上图来自于维基百科
对于新加入的点(绿色),它的类别应该是什么?如果以实线这个圈来算的话,应该是红色的,但是以虚线的这个圈来算的话又应该是蓝色的。所以实际中对于k应该仔细选取。
找出最近的K个点,最简单的算法就是线性搜索,但是这个的代价对于大数据集来说太大了,所以我们有了kd树。
kd树是一种二叉树,表示对k维空间的划分,构成kd树相当于不断地递归地用超平面划分空间,直到实例点划分完毕。最后每个节点都代表一个k维空间的超矩形。
算法
数据集包含N个样本点:
(1)选择x1为坐标轴, 所有样本点的x1分量的中位数为切分点,将全空间分成两个部分。
(2)对子区间递归地进行切分,对深度为j的节点,选择xl为切分的坐标轴,其中l=j(modk)+1,对子区间的xl坐标的中位数做为切分点。重复(2)直到没有样本未被分过。
下面是一个例子,数据集为:
其实在我看来kd-树的构成最主要的就是一方面递归地划分,另一方面每次使用的维度是按顺序循环的
kd树也是一种二叉树,它的搜索也和二叉树有一些相似的地方,之所以能够加速查找就在于树里面的数据相互之间具有一定的有序关系,那么我们有时候就能够根据根节点和所要查找数据之间的关系直接略过一整棵子树。
对应于具体的二叉树问题,也是类似的,我们这里考虑最近邻,也就是k=1的情况。
(1)首先我们通过不断地比较查询点各个维度的数值,沿着二叉树下降直到叶节点。
(2)这个叶节点并不一定是最近邻,只是我们假设这个点为最近邻,以相互之间的距离为半径画一个超球,然后往上到父节点。如果父节点所代表的超平面不和这个超球相交,那么向上回退
(3)如果相交的话还要到父节点的另一个子节点去搜索,如果碰到更近的点,就更新最近邻点
(4)当回退到根节点的时候搜索结束。
下面介绍一下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,和一组数据点,它的密度估计为
下面是例子:
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()
最后生成的结果是
可以看到下面生成的数据跟原数据还是很像的,不过我没有看到这种方法的原理,也不知道它的作用大不大,以后有机会了可以研究一下。