KNN可以说是最简单的分类算法之一,同时,它也是最常用的分类算法之一,注意KNN算法是有监督学习中的分类算法,它看起来和另一个机器学习算法Kmeans有点像(Kmeans是无监督学习算法),但却是有本质区别的。那么什么是KNN算法呢,接下来我们就来介绍介绍吧。
KNN是一种即可用于分类又可用于回归的机器学习算法。对于给定测试样本,基于距离度量找出训练集中与其最靠近的K个训练样本,然后基于这K个“邻居”的信息来进行预测。
在分类任务中可使用投票法,选择这K个样本中出现最多的类别标记作为预测结果;在回归任务中可使用平均法,将这K个样本的实值输出标记的平均值作为预测结果。当然还可以基于距离远近程度进行加权平均等方法。
KNN的全称是K Nearest Neighbors,意思是K个最近的邻居,从这个名字我们就能看出一些KNN算法的蛛丝马迹了。K个最近邻居,毫无疑问,K的取值肯定是至关重要的。那么最近的邻居又是怎么回事呢?其实,KNN的原理就是当预测一个新的值x的时候,根据它距离最近的K个点是什么类别来判断x属于哪个类别。听起来有点绕,还是看看图吧。
图中绿色的点就是我们要预测的那个点,假设K=3。那么KNN算法就会找到与它距离最近的三个点(这里用圆圈把它圈起来了),看看哪种类别多一些,比如这个例子中是蓝色三角形多一些,新来的绿色点就归类到蓝三角了。
但是,当K=5的时候,判定就变成不一样了。这次变成红圆多一些,所以新来的绿点被归类成红圆。从这个例子中,我们就能看得出K的取值是很重要的。
下面分析k值过大和过小造成的影响:
对于k值的选择,没有一个固定的经验(sklearn默认为5),一般根据样本的分布,选择一个较小的值,可以通过交叉验证选择一个合适的k值。
KNN是一种非参的,惰性的算法模型。什么是非参,什么是惰性呢?
非参的意思并不是说这个算法不需要参数,而是意味着这个模型不会对数据做出任何的假设,与之相对的是线性回归(我们总会假设线性回归是一条直线)。也就是说KNN建立的模型结构是根据数据来决定的,这也比较符合现实的情况,毕竟在现实中的情况往往与理论上的假设是不相符的。
惰性又是什么意思呢?想想看,同样是分类算法,逻辑回归需要先对数据进行大量训练(tranning),最后才会得到一个算法模型。而KNN算法却不需要,它没有明确的训练数据的过程,或者说这个过程很快。
了解KNN算法的优势和劣势,可以帮助我们在选择学习算法的时候做出更加明智的决定。那我们就来看看KNN算法都有哪些优势以及其缺陷所在!
简单易用,相比其他算法,KNN算是比较简洁明了的算法。即使没有很高的数学基础也能搞清楚它的原理。
模型训练时间快,上面说到KNN算法是惰性的,这里也就不再过多讲述。
预测效果好。
对异常值不敏感
对内存要求较高,因为该算法存储了所有训练数据
预测阶段可能很慢
对不相关的功能和数据规模敏感
至于什么时候应该选择使用KNN算法,sklearn的这张图给了我们一个答案。
线性扫描也叫“暴力搜索”,是计算输入实例与每一个训练实例的距离,并选择前k个最近邻的样本来多数表决。这种实现方法简单,但是当训练集或特征维度很大时(我们经常碰到样本的特征数有上千以上,样本量有几十万以上),如果我们这要去预测少量的测试集样本,算法的时间效率很成问题,计算非常耗时,故这种暴力实现原理是不可行的 。
kd 树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构,构造kd树相当于不断用垂直于坐标轴的超平面将k维空间进行划分,构成一系列的k维超矩形区域,kd树省去了对大部分数据的搜索,大大的较少了计算量。
注意这里的k和KNN中的k的意思不同。KNN中的k代表最近的k个样本,kd树中的k代表样本特征的维数。
KD树算法包括三步,第一步是建树,第二部是搜索最近邻,最后一步是预测。
kd 树的建立。kd树实质是二叉树,其划分思想与CART树一致,切分使样本复杂度降低最多的特征。kd树分别计算k个特征的方差,认为特征方差越大,则该特征的复杂度亦越大,优先对该特征进行切分 ,切分点是所有实例在该特征的中位数。重复该切分步骤,直到切分后无样本则终止切分,终止时的样本为叶节点,形成kd树。
kd树搜索最近邻。生成kd树以后,对于一个目标点以目标点为圆心,以目标点到叶子节点样本实例的距离为半径,得到一个超球体,最近邻的点一定在这个超球体内部。然后返回叶子节点的父节点,检查另一个子节点包含的超矩形体是否和超球体相交,如果相交就到这个子节点寻找是否有更加近的近邻,有的话就更新最近邻。如果不相交直接返回父节点的父节点,在另一个子树继续搜索最近邻。依次下去,当回溯到根节点时,算法结束,此时保存的最近邻节点就是最终的最近邻。
对于kd树来说,划分后可以大大减少无效的最近邻搜索,很多样本点由于所在的超矩形体和超球体不相交,根本不需要计算距离。大大节省了计算时间。
kd树预测。
分类:每一次搜寻与输入样本最近的样本节点,然后忽略该节点,重复同样步骤K次,找到与输入样本最近邻的K个样本 ,投票法确定输出结果。
回归:用K个最近样本的输出的平均值作为回归预测值。
kd树算法虽然提高了KNN搜索的效率,但是在某些时候效率并不高,比如当处理不均匀分布的数据集时,不管是近似方形,还是矩形,甚至正方形,都不是最好的使用形状,因为他们都有角。为了优化超矩形体导致的搜索效率的问题,从而提出了球树实现的方法。其基本思想和kd树类似,就是每个分割块都是超球体,而不是KD树里面的超矩形体。
在scikit-learn 中,与近邻法这一大类相关的类库都在sklearn.neighbors包之中。KNN分类树的类是KNeighborsClassifier,KNN回归树的类KNeighborsRegressor。除此之外,还有KNN的扩展,即限定半径最近邻分类树的类RadiusNeighborsClassifier和限定半径最近邻回归树的类RadiusNeighborsRegressor, 以及最近质心分类算法NearestCentroid。
在这些算法中,KNN分类和回归的类参数完全一样。具体参数如下:
sklearn.neighbors.KNeighborsClassifier(n_neighbors=5, weights=’uniform’,
algorithm=’auto’, leaf_size=30, p=2, metric=’minkowski’, metric_params=None,
n_jobs=None, **kwargs)
限定半径最近邻法分类和回归的类的主要参数也和KNN基本一样。具体参数如下:
sklearn.neighbors.RadiusNeighborsClassifier(radius=1.0, weights=’uniform’,
algorithm=’auto’, leaf_size=30, p=2, metric=’minkowski’, outlier_label=None,
metric_params=None, n_jobs=None, **kwargs)
实现了一个暴力搜索算法基于欧式距离的。
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
class KNearestNeighbor:
def __init__(self):
pass
def fit(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X, k=1):
dists = self.compute_distances(X)
return self.predict_labels(dists, k=k)
def compute_distances(self, X):
num_test = X.shape[0]
num_train = self.X_train.shape[0]
dists = np.zeros((num_test, num_train))
# expand the formula and calculate each term respectively
train_2 = np.tile( np.sum( (self.X_train)**2, axis=1), [num_test, 1])
test_2 = np.tile( np.sum( (X)**2, axis=1), [num_train, 1]).T
test_train = X.dot(self.X_train.T)
dists = train_2 + test_2 - 2*test_train
return dists
def predict_labels(self, dists, k=1):
num_test = dists.shape[0]
y_pred = [[] for _ in range(num_test)]
for i in range(num_test):
# A list of length k storing the labels of the k nearest neighbors to
# the ith test point.
closest_idx = np.argsort(dists[i, :])[:k].tolist()
classCount = {}
for j in range(k):
voteLabel = self.y_train[closest_idx[j]]
###对选取的K个样本所属的类别个数进行统计
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
###选取出现的类别次数最多的类别
maxCount = 0
for key, value in classCount.items():
if value > maxCount:
maxCount = value
y_pred[i] = key
return y_pred
自己写的KNN和Sklearn里的KNN对比
def test_my_KNearestNeighbor():
#X_test = np.array([[1.1, 0.3]])
X_test = np.array([[1.1, 0.3], [2.1, 0.3]])
X_train = np.array([[1.0, 2.0], [1.2, 0.1], [0.1, 1.4], [0.3, 3.5]])
Y_train = ['A', 'A', 'B', 'B']
knn = KNearestNeighbor() # K近邻(K-Nearest Neighbor)分类器
knn.fit(X_train, Y_train) # 进行分类
Y_predict = knn.predict(X_test, k=3)
print(Y_predict)
def test_sklearn_KNeighborsClassifier():
X_test = np.array([[1.1, 0.3], [2.1, 0.3]])
X_train = np.array([[1.0, 2.0], [1.2, 0.1], [0.1, 1.4], [0.3, 3.5]])
Y_train = ['A', 'A', 'B', 'B']
knn = KNeighborsClassifier(n_neighbors =3) # K近邻(K-Nearest Neighbor)分类器
knn.fit(X_train, Y_train) # 进行分类
Y_predict = knn.predict(X_test)
print(Y_predict)
if __name__ == '__main__':
test_my_KNearestNeighbor()
test_sklearn_KNeighborsClassifier()
结果
['A', 'A']
['A' 'A']
参考资料
【1】https://blog.csdn.net/qq_40195360/article/details/86714337
【2】https://www.cnblogs.com/listenfwind/p/10311496.html