最近在看《信息检索导论》这本经典著作的同时复习一下经典的机器学习算法,而且想着之前的学习中偷懒并没有对这块内容做过总结,所以趁着这个契机重新过一遍。
K近邻算法(k-nearest neighbor, kNN)由Cover和Hart于1968年提出,它是数据挖掘中分类算法中一种直观、简单但是效果有时又很不错的算法:
- 直观:它的算法原理很符合人的直觉,例如当我们在逛超市看到一个从未见过的商品时,如果不仔细看它的说明如何来判断它是属于什么类型的商品呢?一种直观的方法便是看它周围有什么,如果它的周围是各种水果,那么它大概率是一种水果;如果它旁边是各种饮料,它大概率是一种饮品……
- 简单:不管是用于处理回归问题的线性回归还是用于分类问题的Logistic Regressition,算法的目的都是希望根据数据的特征来找到一个函数 f f f来进行拟合现有的数据或是找到合适的分类边界,因此它们都需要通过数据来进行训练,进而获取到 f f f中假设的参数。KNN是一种基于实例的学习算法(instance-based learning),它并没有显式的学习过程,即它并没有相应的训练阶段,而是待收到新样本时直接进行分类处理,故而也常将其归到懒惰学习(lazy learning)中。
- 有效:KNN算法基于一个基本的假设之上,即如果一个样本在特征空间中的K的相近的样本中的大多数属于同一个类别,那么该样本也就可以被划分到该类别之中。因此,如果假设在成立的前提下,同时数据规模较小且类内间距小、类间间距较大的情况下,KNN的效果还是较好的。
例如,在下图中存在三角、矩形两个类别的数据,如果我们看实线圆内的数据可以得出圆圈大概率时属于三角一类,而如果看虚线圆,那么它又大概率可能是属于矩形一类。
算法描述:
- 计算新样本和训练集中各个数据之间的距离
- 按照距离的递增关系进行排序
- 按照K值的设置选择距离最小的K个点
- 确定K个点中不同类别的频率
- 选择K个点中频率最高的类别作为新样本的预测类别
因此,KNN算法中两个关键的问题便是距离度量方式的选择和K值的选取:
- 距离度量方式:距离反映了两个样本之间的相似程度,KNN中常用的度量方式有曼哈顿距离和欧式距离等。
L
p
(
x
i
,
x
j
)
=
(
∑
l
=
1
n
∣
x
i
l
−
x
j
l
∣
p
)
1
p
L_{p}(x_{i},x_{j})=(\sum_{l=1}^n |x_{i}^l -x_{j}^l|^p)^{\frac{1}{p}}
Lp(xi,xj)=(l=1∑n∣xil−xjl∣p)p1
其中当 p = 1 p=1 p=1时为曼哈顿距离;当 n = 2 n=2 n=2时为欧式距离。
- K的选取:K值的选择决定了在预测类别时需要考虑的训练样本个数,当K值太小时,算法对于噪声数据十分敏感,特别是当 K = 1 K=1 K=1时为最近邻算法,如果邻居恰好为噪声,那么最后的分类结果自然就是错误的;而如果K值选择太大时,虽然可以减少学习时的估计误差,但近似误差同时又会增大。因此,K值的选取依赖于一定的经验和对具体训练数据的特定分析。
优缺点
- 优点:简单、直观且效果通常较好,它既可以用来做分类也可以用来做回归;可用于数值型数据和离散型数据;训练时间复杂度为O(n);对异常值不敏感
- 缺点:计算复杂性高;空间复杂性高;对样本不平衡问题敏感;无法给出数据的内在含义。
KNN算法的实现较为简单,代码主要集中于距离和类别频率的计算。而在实际的使用中我们并不需要自己手敲KNN来进行分类,Scikit-learn中对于KNN已经有了很好的实现,我们需要做的就是将数据处理成它所要求的格式,最后调用相应的API进行分类即可。
sklearn提供了两种用于处理分类问题的KNN算法的实现:KNeighborsClassifier和RadiusNeighborsClassifier。
KNeighborsClassifier:它就是我们通常所使用的KNN,其中参数 k k k通常是用户所设定的一个整数,它主要用于近邻对于新样本预测的影响的权重是一致的情况下。sklearn.neighbors.KNeighborsClassifier主要包含如下的参数:
- n_neighbors:K的选取,默认为5
- weights:设置所有近邻点的权重是否相等,若为uniform则认为是相等的;若为distance则认为距离近的点比距离远的点影响大,默认为uniform
- algorithm:加快KNN的搜索算法,默认为auto,即模型根据实际的数据自行决定,此外还有ball_tree、kd_tree和brute(暴力搜索)
- leaf_size:kd_tree和ball_tree中叶子数目的选择,默认30,它影响了树的构建速度和搜索速度,同时影响存储树所需的内存
- p :距离度量公式 L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i l − x j l ∣ p ) 1 p L_{p}(x_{i},x_{j})=(\sum_{l=1}^n |x_{i}^l -x_{j}^l|^p)^{\frac{1}{p}} Lp(xi,xj)=(∑l=1n∣xil−xjl∣p)p1中 p p p的选取
- metric :独立方式的选择。默认为minkowski,即欧式距离
- …
主要方法:
RadiusNeighborsClassifier主要用于数据不是均匀采样的情况中,它需要用户指定一个浮点数类型的 r r r,它可以在较稀疏的邻近区域中的点使用较少的最近的邻居进行分类。sklearn.neighbors.RadiusNeighborsClassifier中的参数基本上和KNeighborsClassifier是一致的,只是n_neighbors换成了radius(默认为1.0)。
example
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn import neighbors, datasets
n_neighbors = 15
# 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(['orange', 'cyan', 'cornflowerblue'])
cmap_bold = ListedColormap(['darkorange', 'c', 'darkblue'])
for weights in ['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)
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()
sklearn中除了对于分类问题提供了两种实现支持外,它对于KNN在回归问题也提供了 KNeighborsRegressor和RadiusNeighborsRegressor 两种实现。当数据所对应的类别并不是离散值而时连续值时,我们可以使用KNN来进行处理,它的实现原理是将邻居的标签的平均值作为新样本对应的标签。
example:
import numpy as np
import matplotlib.pyplot as plt
from sklearn import neighbors
np.random.seed(0)
X = np.sort(5 * np.random.rand(40, 1), axis=0)
T = np.linspace(0, 5, 500)[:, np.newaxis]
y = np.sin(X).ravel()
# Add noise to targets
y[::5] += 1 * (0.5 - np.random.rand(8))
# #############################################################################
# Fit regression model
n_neighbors = 5
for i, weights in enumerate(['uniform', 'distance']):
knn = neighbors.KNeighborsRegressor(n_neighbors, weights=weights)
y_ = knn.fit(X, y).predict(T)
plt.subplot(2, 1, i + 1)
plt.scatter(X, y, color='darkorange', label='data')
plt.plot(T, y_, color='navy', label='prediction')
plt.axis('tight')
plt.legend()
plt.title("KNeighborsRegressor (k = %i, weights = '%s')" % (n_neighbors,
weights))
plt.tight_layout()
plt.show()
对于KNN算法来说,当数据集的规模较小时,如果K值的选择合理,那么最后模型的分类效果一般都不错。但是在实际的应用场景中,我们使用的数据集的规模通常都很大,此时如果用新样本和数据集中的每一个样本进行距离计算时所需的计算量将会很大,这样就导致计算十分耗时和耗内存,算法的效率和效果也会下降。
为了解决上述的问题,其中一种解决方法便是构建kd树(k-dimentional tree)。kd树一种对k维空间中的实例点进行存储以便对其进行快速搜索的二叉树结构。利用kd树可以省去对大部分数据点的搜索,从而减少搜索的计算量。它的每个节点均为k维数值点的二叉树,其上的每个节点代表一个超平面,该超平面垂直于当前划分维度的坐标轴,并在该维度上将空间划分为两部分,一部分在其左子树,另一部分在其右子树。即若当前节点的划分维度为d,其左子树上所有点在d维的坐标值均小于当前值,右子树上所有点在d维的坐标值均大于等于当前值。
假设当前的数据集中包含
(
2
,
3
)
,
(
5
,
4
)
,
(
9
,
6
)
,
(
4
,
7
)
,
(
8
,
1
)
,
(
7
,
2
)
(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)
(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)这些数据,它对应的kd树的构建过程如图所示。如果将其放到三维空间中,我肯可以看出kd树的构造过程相当于不断的使用垂直于坐标轴的超平面对K维空间进行划分,最后形成一系列的K维超矩形区域
Kd树的构建实现:
# kd-tree每个结点中主要包含的数据结构如下
class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点)
self.split = split # 整数(进行分割维度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree
# 构建kd树
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集exset创建KdNode
if not data_set: # 数据集为空
return None
# key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
# operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
# data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2 # //为Python中的整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k # cycle coordinates
# 递归的创建kd树
return KdNode(median, split,
CreateNode(split_next, data_set[:split_pos]), # 创建左子树
CreateNode(split_next, data_set[split_pos + 1:])) # 创建右子树
self.root = CreateNode(0, data) # 从第0维分量开始构建kd树,返回根节点
# KDTree的前序遍历
def preorder(root):
print(root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
if __name__ == "__main__":
data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
kd = KdTree(data)
preorder(kd.root)
Kd树搜索实现:
# kd-tree每个结点中主要包含的数据结构如下
class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点)
self.split = split # 整数(进行分割维度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree
# 构建kd树
class KdTree(object):
def __init__(self, data):
k = len(data[0]) # 数据维度
def CreateNode(split, data_set): # 按第split维划分数据集exset创建KdNode
if not data_set: # 数据集为空
return None
# key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
# operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
# data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
data_set.sort(key=lambda x: x[split])
split_pos = len(data_set) // 2 # //为Python中的整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % k # cycle coordinates
# 递归的创建kd树
return KdNode(median, split,
CreateNode(split_next, data_set[:split_pos]), # 创建左子树
CreateNode(split_next, data_set[split_pos + 1:])) # 创建右子树
self.root = CreateNode(0, data) # 从第0维分量开始构建kd树,返回根节点
# KDTree的前序遍历
def preorder(root):
print(root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
from math import sqrt
from collections import namedtuple
# 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数
result = namedtuple("Result_tuple", "nearest_point nearest_dist nodes_visited")
def find_nearest(tree, point):
k = len(point) # 数据维度
def travel(kd_node, target, max_dist):
if kd_node is None:
return result([0] * k, float("inf"), 0) # python中用float("inf")和float("-inf")表示正负无穷
nodes_visited = 1
s = kd_node.split # 进行分割的维度
pivot = kd_node.dom_elt # 进行分割的“轴”
if target[s] <= pivot[s]: # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
nearer_node = kd_node.left # 下一个访问节点为左子树根节点
further_node = kd_node.right # 同时记录下右子树
else: # 目标离右子树更近
nearer_node = kd_node.right # 下一个访问节点为右子树根节点
further_node = kd_node.left
temp1 = travel(nearer_node, target, max_dist) # 进行遍历找到包含目标点的区域
nearest = temp1.nearest_point # 以此叶结点作为“当前最近点”
dist = temp1.nearest_dist # 更新最近距离
nodes_visited += temp1.nodes_visited
if dist < max_dist:
max_dist = dist # 最近点将在以目标点为球心,max_dist为半径的超球体内
temp_dist = abs(pivot[s] - target[s]) # 第s维上目标点与分割超平面的距离
if max_dist < temp_dist: # 判断超球体是否与超平面相交
return result(nearest, dist, nodes_visited) # 不相交则可以直接返回,不用继续判断
# ----------------------------------------------------------------------
# 计算目标点与分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(pivot, target)))
if temp_dist < dist: # 如果“更近”
nearest = pivot # 更新最近点
dist = temp_dist # 更新最近距离
max_dist = dist # 更新超球体半径
# 检查另一个子结点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)
nodes_visited += temp2.nodes_visited
if temp2.nearest_dist < dist: # 如果另一个子结点内存在更近距离
nearest = temp2.nearest_point # 更新最近点
dist = temp2.nearest_dist # 更新最近距离
return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点开始递归
from time import clock
from random import random
# 产生一个k维随机向量,每维分量值在0~1之间
def random_point(k):
return [random() for _ in range(k)]
# 产生n个k维随机向量
def random_points(k, n):
return [random_point(k) for _ in range(n)]
if __name__ == "__main__":
data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
kd = KdTree(data)
preorder(kd.root)
ret = find_nearest(kd, [3, 4.5])
print(ret)
N = 400000
t0 = clock()
kd2 = KdTree(random_points(3, N)) # 构建包含四十万个3维空间样本点的kd树
ret2 = find_nearest(kd2, [0.1, 0.5, 0.8]) # 四十万个样本点中寻找离目标最近的点
t1 = clock()
print("time: ", t1 - t0, "s")
print(ret2)
Ball_tree是为了解决 KD 树在高维上效率低下的问题而发明的一种算法,ball tree将在一系列嵌套的超球体上分割数据,即使用超球面而不是超矩形划分区域。虽然在构建数据结构的花费上大过于KDtree,但是在高维甚至很高维的数据上都表现的很高效。
想了解的可浏览:
kNN里面的两种优化的数据结构:kd-tree和ball-tree,在算法实现原理上有什么区别?
scikit-learn–Nearest Neighbors(最近邻)
此外,sklearn在相关的内容中还介绍了有关NCA(Neighborhood Components Analysis)的内容,后续再分开整理叭~