k-nearest neighbor(kNN,k近邻算法)理论与实操及KNeighborsClassifier参数详解

1. k-NN算法简介

k近邻法是基本且简单的分类回归方法,这里只讨论分类方法,利用数据集对特征向量空间进行划分,可以进行多分类。如下图:在这里插入图片描述
三角形与矩形分别代表两类数据,标签已知。现要对新输入的为分类点(绿色)进行分类,k-NN的做法是寻找与该绿点相邻最近的k个点(k-NN算法的k的含义,图中的距离为欧式距离),然后通过多数表决的方式把绿点划分到这k个最近点出现频数最高的类。例如如果k取3,则绿点最近的3个点中频数最高为三角形类,所以归为三角形类;若k取5,则距离绿点最近的5个点中频数最高为矩形类,所以归绿点为矩形类。

1.1 模型

输入:训练数据集 T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x N , y N ) } T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\} T={(x1,y1),(x2,y2),...,(xN,yN)}, 实例特征向量 x x x
总共N个样本, x i ∈ X ⊆ R n x_i\in\mathcal{X}\sube R^n xiXRn为实例的特征向量, y i ∈ Y = { c 1 , c 2 , . . . , c K } 为 实 例 的 类 别 , i = 1 , 2 , . . . , N y_i \in \mathcal{Y}=\{c_1,c_2,...,c_K\}为实例的类别, i=1,2,...,N yiY={c1,c2,...,cK},i=1,2,...,N
输出:实例 x x x所属的类 y y y
step1:根据给定的距离度量,在训练集T中找出与 x x x最近的k个点,涵盖这k个点的x的邻域记为 N k ( x ) ; N_k(x); Nk(x);
step2:在 N k ( x ) N_k(x) Nk(x)中根据决策规则(如多数表决)决定 x x x的类别 y y y
y = arg max ⁡ c j ∑ x i ⊆ N k ( x ) I ( y i = c j ) , i = 1 , 2 , . . . , N , j = 1 , 2 , . . . , K ; \large y={\underset {c_j}{\operatorname {arg\,max} }}\sum\limits_{x_i \sube N_k(x)} I(y_i=c_j), i=1,2,...,N, j = 1,2,...,K; y=cjargmaxxiNk(x)I(yi=cj)i=1,2,...,N,j=1,2,...,K
其中I为指示函数

1.2 学习策略

max ⁡ c j ∑ x i ⊆ N k ( x ) I ( y i = c j ) , i = 1 , 2 , . . . , N , j = 1 , 2 , . . . , K ; {\underset {c_j}{\operatorname {max} }}\sum\limits_{x_i \sube N_k(x)} I(y_i=c_j), i=1,2,...,N, j = 1,2,...,K; cjmaxxiNk(x)I(yi=cj)i=1,2,...,N,j=1,2,...,K

1.3 学习算法

学习算法即如何求出以上的学习策略的最大值,用的是多数表决法,即将 c 1 到 c K c_1到c_K c1cK得到的指示函数和的值排序,选择最大值对应的类别 c ∗ c^* c作为输出。假设 c ∗ c^* c为最大值对应的类别,那么得到的最终模型为:
y = c ∗ \large y=c^* y=c

1.4 距离度量

从以上公式可以看出,关键是确定输入实例 x x x的邻域 N k ( x ) N_k(x) Nk(x),而这个邻域又由两个方面决定,一个就是如何计算各点与输入实例 x x x的距离(怎么判断离某些点近不近)?
L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(i=1nxi(i)xj(l)p)p1

  • p = 1 p= 1 p=1 曼哈顿距离
  • p = 2 p= 2 p=2 欧氏距离
  • p = ∞ p= \infty p= 切比雪夫距离
    一般使用欧式距离。

1.5 k值选择

知道了怎么判断点之间离的近不近,那么要确定邻域还得需要知道该邻域包含多少个“最近”点,这就是k值决定的,该邻域会包含k个离输入实例最近的点。k值越小,模型越复杂,过拟合的风险越大,当k=1时成为最近邻模型。而k值越大,模型越简单,最极端情况就是k=N,这时直接把样本中出现频数最高的类当成所有输入实例的类。

距离的度量以及k值作为超参数,可以通过验证集来选择合适的超参数。

2. 实例操作

选取sklearn内置数据库的iris数据集进行实战演练。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# %matplotlib notebook
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal_length', 'sepal_width','petal_length','petal_widthth','label']
df

在这里插入图片描述
总共有4个特征,且分为三类,每一类各50个样本。为了方便画散点图更利于观察,只选取前两个特征’sepal_length’以及’sepal_width’训练模型,

plt.figure()
ax = plt.subplot(1,1,1)
plt.scatter(df[:50]['sepal_length'], df[:50]['sepal_width'], label='0')
plt.scatter(df[50:100]['sepal_length'], df[50:100]['sepal_width'], label='1')
plt.scatter(df[100:150]['sepal_length'], df[100:150]['sepal_width'], label='2')
plt.xlabel('sepal_length')
plt.ylabel('sepal_width')
plt.legend()
plt.show()

如图:
在这里插入图片描述

data = np.array(df.iloc[:, [0, 1, -1]])  # 选取前两个特征
X, y = data[:,:-1], data[:,-1]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) # 划分训练集和测试集

2.1 手写kNN算法

class kNN:
    def __init__(self, X_train, y_train, k_neighbors, p=2):  
        """
        parameter: k_neighbors 所选取的临近点个数
        parameter: p 距离度量,默认p=2为欧式距离
        """
        self.k = k_neighbors
        self.p = p
        self.X_train = X_train
        self.y_train = y_train   # 定义每个模型的私有属性
        
        
    def predict(self,X):
        """
        X:要进行分类的某个输入实例
        """
        knn_list = []  # 取出k个点,组成一个双值子元组列表,元祖包含距离和标签
        
        for i in range(self.k):  # 首先遍历前k个样本
            distance = np.linalg.norm(X - self.X_train[i], ord=self.p) # 计算距离
            knn_list.append((distance, self.y_train[i]))  
        
        for i in range(self.k, len(self.X_train)):  # 再遍历剩下样本,选出最近的k个点
            max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))  # 计算列表中每一次遍历距离最大的点的index
            distance = np.linalg.norm(X - self.X_train[i], ord=self.p)  # 计算剩下点的距离,和列表里最大距离的点相比
            if distance < knn_list[max_index][0] :  # 如果这次遍历的点比较小,那就替换那个距离更大的点
                knn_list[max_index] = (distance, self.y_train[i]) # 最终列表里是距离X最近的k个点
        
        # 下面要统计这最近的k个点中哪个标签出现的次数最多,作为实例X的输出标签
        knn = np.array([int(i[-1]) for i in knn_list]) # 标签数组
        most_lable = np.argmax(np.bincount(knn))  # 出现次数最多的lable
        return most_lable
    
    
    # 下面计算测试集的预测精度
    def score(self, X_test, y_test):
        right_count = 0
        for X, y in zip(X_test, y_test):
            label = self.predict(X)
            if label == y:
                right_count += 1
        return right_count / len(X_test)
trained_knn = kNN(X_train,y_train,5)  # 这就是定义好的模型,不需要fit,可以用来预测测试集了
trained_knn.score(X_test, y_test)  # 预测精度为0.7

用一个测试点来画图:

test_point = [5.0, 3.0]
print(f'Test Point: {trained_knn.predict(test_point)}') #  Test Point: 0
plt.figure()
ax = plt.subplot(1,1,1)
plt.scatter(df[:50]['sepal_length'], df[:50]['sepal_width'], label='0')
plt.scatter(df[50:100]['sepal_length'], df[50:100]['sepal_width'], label='1')
plt.scatter(df[100:150]['sepal_length'], df[100:150]['sepal_width'], label='2')
plt.plot(test_point[0], test_point[1], 'ko', label='test_point')
plt.xlabel('sepal_length')
plt.ylabel('sepal_width')
plt.legend()
plt.show()

可以看出测试点(5,3)明显离0类的大部分点更近,所以输出0。
在这里插入图片描述

kNN算法一般步骤就演示完了,可是存在一个效率问题,因为在寻找最近的k个点时,手写的模型要求计算出所有点到输入实例 x x x的距离,再从中挑选k个最近的点,当样本数量非常大且实例的维度很高时,计算量都会很大,所以为了提高效率,在搜索k个最近值时会用到kd树。

2.2 k近邻法的实现:kd树

k近邻法需要考虑的问题就是如何把邻域找出来。k近邻法最简单的一种实现方法就是线性扫描,计算出输入实例与每个训练实例的距离,然后按距离排序,选择距离最近的k个样本进入邻域,将其标签作为多数表决的标签数据来源。

然而,当数据集很大或者说特征数量很多时,计算非常耗时,通常是不可行的。为了提高邻域的搜索效率,可以考虑用特殊的结构存储训练数据,以减少距离的计算次数。kd树就是一种比较高效实现k近邻法的实现方法。

  • kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。
  • kd树是二叉树,表示对k 维空间的一个划分(partition)。构造kd树相当于不断地用垂直于坐标轴的超平面将 𝑘 维空间切分,构成一系列的k维超矩形区域。kd树的每个结点对应于一个 𝑘维超矩形区域。
  • kd树对k维特征空间划分成很多个超矩形区域,输入的实例 x x x属于其中某一个子区域,然后从那里开始往外搜索最近值,可以减少对距离该区域较远的一些区域的搜索,提高了模型的效率。

2.3 scikit-learn实例

手写只是为了便于理解模型,之后一般应用可以调包。而且其中有个参数algorithm可以选择kd_tree,更加方便。

from sklearn.neighbors import KNeighborsClassifier
# 构造一个kNN分类器,用kd树实现
cnn_sk = KNeighborsClassifier(n_neighbors=5,weights='uniform',
                     algorithm='kd_tree',leaf_size=30,
                     metric='minkowski', p=2,
                     metric_params=None, n_jobs=1)
                     
 # 训练分类器,手写的时候看起来是没有训练模型的,因为用的是线性扫描,输入一个x在进行距离计算,不用训练模型。
 # 但是kd树需要通过训练集学习到一颗包含训练集的树。                    
cnn_sk.fit(X_train, y_train)   
使用训练好的分类器对测试集进行预测,返回准确率
clf_sk.score(X_test, y_test)

2.3.1 KNeighborsClassifier的一些参数

KNeighborsClassifier函数一共有8个参数:

  • n_neighbors: int, default=5,就是k-NN的k的值,选取最近的k个点,默认为5。
  • weights: 默认是uniform,参数可以是uniform、distance,也可以是用户自己定义的函数(直接weights=函数名就行)。uniform是均等的权重,就说所有的邻近点的权重都是相等的。distance是不均等的权重,距离近的点比距离远的点的影响大。用户自定义的函数,接收距离的数组,返回一组维数相同的权重。
  • algorithm: {‘auto’, ‘ball_tree’, ‘kd_tree’, ‘brute’}, default=’auto’。快速k近邻搜索算法,默认参数为auto,可以理解为算法自己决定合适的搜索算法。除此之外,用户也可以自己指定搜索算法ball_tree、kd_tree、brute方法进行搜索,brute是蛮力搜索,也就是线性扫描,当训练集很大时,计算非常耗时。kd_tree,构造kd树存储数据以便对其进行快速检索的树形数据结构,kd树也就是数据结构中的二叉树。以中值切分构造的树,每个结点是一个超矩形,在维数小于20时效率高。ball tree是为了克服kd树高纬失效而发明的,其构造过程是以质心C和半径r分割样本空间,每个节点是一个超球体。
  • leaf_size: int ,default=30。leaf_size:默认是30,这个是构造的kd树和ball树的大小。这个值的设置会影响树构建的速度和搜索速度,同样也影响着存储树所需的内存大小。需要根据问题的性质选择最优的大小。
  • metric: str or callable, default=’minkowski’。用于距离度量,默认度量是minkowski(也就是闵氏距离, L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(i=1nxi(i)xj(l)p)p1),当p=2时为欧氏距离(欧几里德度量)。
  • p: 距离度量公式。即设置metric里闵氏距离的参数p,这个参数默认为2,也就是默认使用欧式距离公式进行距离度量。也可以设置为1,使用曼哈顿距离公式进行距离度量。
  • metric_params: 距离公式的其他关键参数,这个基本不用,使用默认的None即可。
  • n_jobs: 并行处理设置。默认为1,临近点搜索并行的工作数。如果为-1,那么CPU的所有cores都用于并行工作。

2.4总结:

KNN(K-Nearest Neighbor)最邻近分类算法是数据挖掘分类(classification)技术中最简单的算法之一,其指导思想是”近朱者赤,近墨者黑“,即由你的邻居来推断出你的类别。KNN最邻近分类算法的实现原理:为了判断未知样本的类别,以所有已知类别的样本作为参照,计算未知样本与所有已知样本的距离,从中选取与未知样本距离最近的K个已知样本,根据少数服从多数的投票法则(majority-voting),将未知样本与K个最邻近样本中所属类别占比较多的归为一类。

2.4.1优缺点

优点:

  • 简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;
  • 对异常值不敏感

缺点:

  • 最大的缺点是无法给出数据的内在含义。
  • 计算复杂性高;空间复杂性高;
  • 样本不平衡问题(即有些类别的样本数量很多,而其它样本的数量很少);
  • 一般数值很大的时候不用这个,计算量太大。而且k近邻算法最终只用了最近k个点的信息来做决策,而舍弃了其余点。

kNN算法的原理以及实操就演示完了,请大家多多指教。

  • 9
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值