一、K近邻算法基础介绍
K近邻算法也是常说的KNN算法,是一种常见的分类和回归算法,当然我们常将其用于分类。是一种监督算法,该算法的内容其实和名字很像,根据邻居来进行判断。有点近朱者赤近墨者黑的意味。
比如我们常说:某个人的工资一般是与其玩的最好的5个朋友(或者说是N个)工资的平均值。
在K近邻算法中也是如此,某个样本的类别是与其最近的K个样本类别的众数,某个样本的值(回归问题中)是与其最近的K个样本的平均值。
上句话中我将三个地方标黑了,这也是K近邻算法中最重要的三个地方,也是KNN算法的三要素:
(1):k值的选择
(2):距离的度量(即何为最近)
(3):决策规则(为什么分类用众数,回归用平均值)
算法介绍:
以使用KNN算法分类为例,给定一个数据集:
其中 是一个k维向量,
为数据
所属的类别。一般𝑘<<𝑛
预测:
给定一个𝑥输出其所属的类别。
该算法采用的方法是:在给定的距离度量基础上,在训练集𝑇中找出与𝑥最接近的k个点,将其记录为 。然后在
中根据分类决策规则决定𝑥的类别𝑦
用公式表示就是:
𝐼是指示函数,当 时为1,否则为0。
当𝑘==1时,可以称之为最近邻算法,此时𝑥的预测值为直接距离𝑥最近的训练集中的样本 对应得
根据上面的介绍,我们可以发现KNN算法好像并不需要对样本𝑇进行训练,当预测样本来的时候我们直接查找与预测样本最近,然后做一些判断就ok了。事实也是如此,该算法没有显示学习的过程,并且是是一个无参算法。
无参算法对应的就是有参数算法,故名思义,无参就是没有参数!我们不需要去优化某个参数,然后生成由参数和结构组成的模型。有参算法就是需要训练生成参数,然后根据生成的参数和模型对预测样本进行预测。
二、K近邻算法三要素和源码
上文说到k近邻算法的三要素。下面我们来详细介绍一下:
1:k值
k值就是选取邻居的数量。到达到达根据几个朋友的工资来估算某个人的工资呢?如果k的取值太小的话,相应的误差就会比较大。反之如果k的取值太大,可能会选取大不那么近的邻居,导致误差也偏大。极端情况下,当k等于样本总和N时,无论预测样本是什么都会选取训练集中最多的类别(分类问题)作为预测结构。
k值的选取我们可以根据交叉检验的方法来选取。比如:我们将训练集拆成2份,一部分作为训练集,一部分作为验证集。选取几个k值的候选值,然后预测验证集在该k值情况下的分类误差。然后可以选取分类误差最小的一个k值。当然也可以采用五折验证的方法。
2:距离度量
距离度量就是衡量两个样本距离的方法,常见的度量方法有:欧式距离,曼哈顿距离,或者是更为一般的 距离。
假设特征空间是n维的向量空间 。
,
。
的
距离为:
其中𝑞>=1,可以发现,当𝑝=2时,称之为欧式距离:
当𝑝=1时,称之为曼哈顿距离,即:
3:决策规则
就是决策方法,假设一个分类问题,我们现在已经获取了预测样本最近的K个训练样本的类别,怎样去进行预测?一般情况下我们可以使用直接表决的方法,也就是取众数,在回归问题中我们采用平均数。当然有时候我们也要考虑到距离因素,给距离越近的样本加点权重。
下面我们用代码来表示从五折检验选取最优的k值,然后通过k值来进行预测。
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.model_selection import StratifiedKFold
X = datasets.load_iris()['data']
Y = datasets.load_iris()['target']
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.4, stratify=Y,random_state=100)
class KNN:
def __init__(self, train_x, train_y, test_x, test_y):
"""
KNN初始话
:param train_x:训练集X
:param train_y: 训练集Y
:param test_x: 预测集X
:param test_y: 预测集Y
"""
self.train_x = train_x
self.train_y = train_y
self.test_x = test_x
self.test_y = test_y
self.k = None
def euclidean_dis(self, x1, x2):
"""
返回x1与x2的距离(x1,x2均为二维矩阵).x1.shape=(N1*M),x2.shape=(N2*M2),返回结果为(N1*N2)
:param x1:
:param x2:
:return:
"""
n1, m1 = x1.shape
n2, m2 = x2.shape
if m1 != m2:
raise ("两个向量维度不相等")
x1x2 = np.dot(x1, x2.T) # (n1,n2)
y1 = np.repeat(np.reshape(np.sum(np.multiply(x1, x1), axis=1), (n1, 1)), repeats=n2, axis=1)
y2 = np.repeat(np.reshape(np.sum(np.multiply(x2, x2), axis=1), (n2, 1)), repeats=n1, axis=1).T
dis = y1 + y2 - 2 * x1x2
return dis
def predict(self, train_x, y, test, k):
"""
返回根据KNN预测的结果
:param train_x: 训练集x
:param y: 训练集y
:param test: 预测集
:return: 返回test预测的结果
"""
dis = self.euclidean_dis(test, train_x)
k_neighbor = np.argsort(dis, axis=1)[:, :k]
k_neighbor_value = y[k_neighbor]
n = test.shape[0] # 预测结果的个数
pred = np.zeros(n)
for i in range(n):
pred[i] = np.argmax(np.bincount(k_neighbor_value[i]))
return pred
def KFlod(self, k):
folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1996)
oof = np.zeros(self.train_x.shape[0])
for fold_, (train_index, test_index) in enumerate(folds.split(self.train_x, self.train_y)):
train_x, test_x, train_y, test_y = self.train_x[train_index], self.train_x[test_index], \
self.train_y[
train_index], self.train_y[test_index]
pred = self.predict(train_x, train_y, test_x, k)
oof[test_index] = pred
return np.sum(oof == self.train_y)
def selectK(self):
ks = [2,3, 4, 5, 6,7,8,9,10,11,12,13,14,15]
value = 0
for k in ks:
value_tem = self.KFlod(k)
print("当前K的值为:", k, "预测得分为:", value_tem)
if value_tem > value:
self.k = k
value = value_tem
def trainAndPredic(self):
self.selectK()
print("选择的k为:", self.k)
preds = self.predict(self.train_x, self.train_y, self.test_x, self.k)
print("预测结果的正确个数为:", np.sum(preds == self.test_y))
print("预测结果的错误个数为:", np.sum(preds != self.test_y))
model = KNN(X_train, y_train, X_test, y_test)
model.trainAndPredic()
'''
当前K的值为: 2 预测得分为: 89
当前K的值为: 3 预测得分为: 89
当前K的值为: 4 预测得分为: 89
当前K的值为: 5 预测得分为: 89
当前K的值为: 6 预测得分为: 88
当前K的值为: 7 预测得分为: 89
当前K的值为: 8 预测得分为: 89
当前K的值为: 9 预测得分为: 89
当前K的值为: 10 预测得分为: 89
当前K的值为: 11 预测得分为: 89
当前K的值为: 12 预测得分为: 89
当前K的值为: 13 预测得分为: 90
当前K的值为: 14 预测得分为: 88
当前K的值为: 15 预测得分为: 89
选择的k为: 13
预测结果的正确个数为: 56
预测结果的错误个数为: 4
'''
三、K近邻算法的优化
假设我的样本空间是𝑁𝑋𝑀,那么我每次预测一个样本花费的时间是𝑂(𝑁𝑀)+𝑂(𝐾𝑁)。(前者为计算距离的时间,后者为选出K个最近邻居的时间)。在很多大型的场景中𝑁的值会非常的大,采用该算法进行预测花费的时间会非常的长。比较常见的优化方法有:KD树。在实际的应用中我们会比较常用Annoy等信息检索中常用的方法。
KD树在很多关于K近邻算法的介绍中都有所提及,并且相比与Annoy相比,Annoy算法其实更加常用。
Annoy也是采用二叉树的方法,在各种工业系统中使用的比较多,算法基础我们不说了,直接看下面的例子:
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
X=datasets.load_iris()['data']
Y=datasets.load_iris()['target']
Y[Y>1]=1
X_train,X_test,y_train,y_test=train_test_split(X,Y,test_size=0.4,stratify=Y)
from annoy import AnnoyIndex
# pip install annoy 安装Annoy
f指的是向量的维度,metric 表示度量公式。在这里,Annoy 支持的度量公式包括:"angular", "euclidean", "manhattan", "hamming", "dot"
metric="euclidean"
f=4
a = AnnoyIndex(f, metric)
'''
a.add_item(i, v):i 是一个非负数,表示 v 是第 i 个向量
'''
for i in range(90):
a.add_item(i, X_train[i])
'''
trees表示树的数量
'''
# 树的数量
trees=10
a.build(trees) # 10 trees
# True
'''
储存索引成文件
'''
a.save("train.ann")
# True
'''
读取存储好的索引文件
'''
u = AnnoyIndex(f, 'euclidean')
u.load("train.ann")
# True
'''
查找距离某个向量最近的n个样本。include_distances=False时仅输出索引,True输出索引和距离值。
'''
a.get_nns_by_vector(X_test[0], n=10, search_k=-1, include_distances=True)
'''
([13, 5, 42, 89, 33, 45, 55, 71, 63, 47],
[0.20000028610229492,
0.331662654876709,
0.34640997648239136,
0.38729819655418396,
0.4358895421028137,
0.4582575857639313,
0.47958314418792725,
0.49999988079071045,
0.5099020004272461,
0.5567763447761536])
'''