深度学习-第2周
最近邻规则分类KNN
算法介绍
KNN是一种近朱者赤近墨者黑的监督学习分类算法,是基于实例的学习,属于懒惰学习(没有显式学习过程;在训练阶段不做或做很少的模型构建工作,而是在预测阶段才进行大量计算)
- 为了判断未知实例的类别,以所有已知类别的实例作为参照选择参数K
- 计算未知实例与所有已知实例的距离
- 选择最近K个已知实例
- 根据少数服从多数的投票法则,让未知实例归类为K个最邻近样本中最多数的类别
距离度量
闵可夫斯基距离
$$
有两个k维向量\vec{a}=(x_{11},x_{12},\dots,x_{1k}),\vec{b}=(x_{21},x_{22},\dots,x_{2k}) \
两个向量之间的闵可夫斯基距离为\
d(x,y)=(\sum_{i=1}^{k}|x_{1i}-x_{2i}|^p)^{1/p}
$$
曼哈顿距离
各个维度差值的绝对值的和
$$
闵可夫斯基距离中p=1\
d(x,y) =\sum_{i=1}^{k}|x_{1i}-x_{2i}|
$$
欧几里得距离
接触最多的距离公式
$$
闵可夫斯基距离中p=2\
d(x,y)=\sqrt{\sum_{i=1}^{k}(x_{1i}-x_{2i}})^2
$$
切比雪夫距离
各个维度差值的最大值
$$
闵可夫斯基距离中p->∞ \
d(x,y) = \max\limits_{i}(|x_{1i}-x_{2i}|)
$$
K值
K通常是奇数
为什么?若K是偶数,那么可能出现每类的数量均等这样一个平局的场面,就不好选择到底把未知实例归属到哪一类

由图中我们可以发现:不同的K值对未知实例的归属有影响;K=1时,与未知实例最近的1个点中有1个第一类,0个第二类,此时未知实例归属第一类;而K=5时,与未知实例最近的5个点中有1个第一类,4个第二类,此时未知实例归属第二类
- k太大:会把很远的邻居考虑进去,分类就不准确了(欠拟合)
- k太小:泛化能力差,对于复杂一些的模型,只要改变一点点结果就会变化很大
算法缺点

复杂度高:要计算比较未知实例与所有已知实例之间的距离
样本分布不均衡时(比如有一类样本特别大而且密度大,占主导),新的未知实例容易被归类到这个主导样本中;但实际上未知样本并没有接近目标样本(比如图中的Y,看图其实应该是属于ω1,但是在KNN算法中被分类到了ω2)
预测阶段很慢(懒惰学习,预测时才进行大量计算)
对不相关的功能和数据规模敏感(KNN基于距离进行计算,如果特征中向量中有很多不相关的特征,就相当于引入干扰,导致模型性能下降)
代码实现
例子

绘图
数据定义
1 | # 爱情片 |
绘图的点集
1 | sct1 = plt.scatter(x1, y1) |
绘图
1 | plt.legend( |
绘图结果

算法实现
数据定义
1 | # 已知实例 |
计算点与点的差
方法1:显式转化未知实例矩阵的形状
1 | # 手动将未知实例复制成与已知实例相同的形状,等价于广播 |
1 | diff2:[[ 15 -14] |
方法2:直接利用numpy广播机制隐式转换
1 | diff1 = unknown_x - X |
1 | diff1:[[ 15 -14] |
计算距离
1 | # 点与点的差 |
1 | dis:[ 20.51828453 18.86796226 19.23538406 115.27792503 117.41379817 |
对距离进行排序
1 | # argsort()是获取排序后在原列表中的索引 |
获取到距离从小到大的索引:
1 | [1 2 0 3 4 5] |
对最近邻k个实例进行统计
1 | # 定义一个字典(类似与Java的Map<K,V>)存储最近k个实例中标签(K)与出现次数(V);字典可以直接用索引形式获取K对应的V map['K'] -> V |
1 | map:{'A': 3} |
根据键值对的值进行排序
1 | # 根据KV对的V进行排序 |
1 | map:{'A': 3} |
获取最终分类的标签
1 | # sortedMap[0][0]获取第1个元组的第1个元素(即键) |
1 | ultimate_label:A |
换一组数据,k改为5
1 | X = np.array([ |
1 | def getY(X): |

分类结果:未知实例属于B类
1 | map:{'A': 2, 'B': 3} |
Iris数据集分类

使用到的数据集
1 | from sklearn import datasets |
.load_iris()返回一个bunch对象,类似于字典

里面比较重要的属性
data:存放样本数据

target:3个类别

target_names:类别对应的名字

分割数据集
方法1:直接使用sklearn自带的train_test_spilit
1 | from sklearn import datasets |
方法2:自己实现从打乱数据到分割数据集
1 | def train_test_split(x, y, test_size=0.2): |
KNN代码
自己实现
与前面演示算法实现不同的是,这里未知实例是很多组,需要用循环进行逐组分类
1 | def KNN(unknown, X, y, k=5): |
1 | y_test_classified = [] |
评估分类结果
1 | # from sklearn.metrics import classification_report |
1 | precision recall f1-score support |
整体代码
1 | import operator |
使用sklearn的KNeighborsClassifier实现
1 | import numpy as np |
z1归一化
$$
x_i = \frac{x_i-\mu}{\sigma} \
\mu:均值\
\sigma:标准差\
$$
K值选择
K折交叉验证
将原始数据集均分成K个子集,称为折(Fold),然后模型在K个不同的训练集上进行K次训练和验证;每次训练中,其中一个折被划分为验证集,剩下K-1个折为训练集;这样就产生了K个模型和K个验证分数
通常使用5/10折
为什么?
被认为是 在偏差(bias)和方差(variance)之间取得良好平衡的选择
与其他k对比:
- Loo(留一法):n个样本,每次只留一个验证集,那么进行n次验证,每次只有一个样本作为验证集;泛化能力最强的同时计算开销最大
优点
- 防止过拟合,每个样本都会被验证
- 提高模型泛化能力
缺点
- 计算成本比较大
- 最终验证的分数与验证集和测试集划分有关系,可能出现分数不稳定的情况
GridSearchCV网格搜索
sklearn中一个强大的超参数调优工具
核心功能:
- 自动尝试一组超参数的组合;
- 对每组参数使用交叉验证(如 K 折交叉验证)评估模型性能;
- 找出在验证集上表现最好的参数组合;
- 最终使用最优参数在整个训练集上重新训练模型。
对前面的iris数据集分类案例进行优化
使用cross_val_score实现k折交叉验证
1 | def select_k(X, y): |
整体代码
1 | import operator |
输出结果
第一次
1 | 交叉验证后准确率:0.8866666666666667 |
第二次
1 | 交叉验证后准确率:0.9466666666666667 |
第三次
1 | 交叉验证后准确率:0.9333333333333333 |
使用sklearn的KFold进行k折验证
1 | # KFold实例 |
遍历k值,找到最合适的那个
1 | for k in np.arange(1,22,2): |
整体代码
1 | import numpy as np |
结果
1 | 最优验证分数:0.9666666666666668,最优k=11 |
使用网格搜索
导入网格搜索
1 | # 网格搜索交叉验证 |
核心代码
1 | # 需要搜索的k值,键的名字就是KNeighborsClassifier中参数名 |
整体代码
1 | import numpy as np |
输出
1 | 最佳准确率=0.975 |
优化KNN
前面我们提到,KNN有两个很明显的缺点:
- 距离远近并不影响投票(如果k选的很大就是来者皆是客的情况)
- 计算复杂度高
因此我们可以对KNN进行优化
带权KNN
解决距离远近不影响投票的缺点,给距离带上权值
使用sklearn实现
核心代码
1 | clf = KNeighborsClassifier( |
KD树
解决计算复杂度高的问题
简介
- 一种对k维空间中的实例点进行存储以便于对其进行快速检索的树形数据结构
- kd树是一种二叉树,表示对k维空间的一个划分;构造kd树相当于不断地用垂直于坐标轴的超平面将k维空间切分,构成一系列的k维超矩形区域。kd树的每个结点都对应于一个k维矩形区域
- 利用kd树可以省去对大部分数据点的搜索,从而减少计算量
- 切分维度上,左子树值小于右子树
构造
构造 KD-Tree 的核心思想是递归地选择一个维度进行划分,并将数据点按照该维度的中位数分割为左右两部分,递归构建左右子树
步骤:

- 选择划分维度
- 通常按维度轮流选择,比如依次选择x轴,y轴,z轴
- 或者选择方差最大的维度
- 选择划分点
- 选取当前维度的中位数点作为划分点(index = size/2[整除2])
- 递归构建左右子树
- 小于中位数点的位于左子树
- 大于中位数点的位于右子树
按维度轮流选择代码实现
1 | def build_rr(points, depth=0, d=0): |
每次选择方差最大的维度代码实现
1 | def build_var(points, depth=0): |
搜索
步骤
需要利用切分维度上,左子树值小于右子树这个性质
- 初始化最近距离表(输入k值)
- 根据k值初始化最近距离表
- 距离-默认inf
- 节点-默认None
- 输出最近距离表
- 回溯路径(输入:目标点;KD-Tree)
- 根据kd树的节点的切分维度,比较目标点切分维度值
- 目标点值<节点值,进入左子树
- 目标点值>=节点值,进入右子树
- 回溯路径加入回溯路径表
- 计算最近距离(输入:最近距离表;回溯路径表;k值;目标点)
- 计算目标点与回溯路径表中的节点距离
- 更新最近距离表(更小的距离入表)
- 取距离表中最远的距离作为搜索半径
- 计算节点在划分维度上与目标点的距离,并与搜索半径比较
- 如果搜索半径大于目标点与节点的划分维度距离,那么该节点的左子树或右子树加入回溯半径
例子



计算kd树节点与目标点的欧氏距离,把最大的距离作为搜索半径

比较与节点在划分维度上的距离,划分距离小于搜索半径时,搜索的圆形区域其实会位于节点超平面两侧(与超平面相交),也就是说要考虑另一侧子树


代码实现
构造kd树使用轮流选择维度
1 | def build_rr(points, depth=0, d=0): |
表结构
1 | # 距离表单元 |
1 | def kd_search(root, target, axis=0, k=3, n=0): |





