【统计学习方法】K近邻法

K近邻法

概念简述

训练一个KNN模型,简单来说就是对于一个新的输入点,其周围最近的(如欧氏距离)K个点,哪种点最多,这个新输入点就被定义为哪种点。

其数学表示如下:

而对于上述的最近距离的度量方式,有以下几种:

在这里插入图片描述
通常来说,选择不同的距离度量方式,所得到的结果是不同的,
p = 1,2时,要考虑每一个分量;
p = ∞时,指的是一个向量(即每个点的数据)里边绝对值最大的元素对应的绝对值即无穷范数,也就是希望输入变量相差最大的分量比较小

K值选择

实践时发现K是先选定几个值,然后分别计算输出一下,看哪个K值的结果更好,也就是下边要说的分类决策规则,而K值选择有两个注意的点:

① K值太小的话,容易造成过拟合,增加模型的复杂度,简单理解就是,新输入的点周围可以用来判断的信息太少了,其周围最近的少量的点拥有了太大的话语权,如果这些点里包含了噪声点,那么对判断结果就会有很大的影响了;

② K值太大的话,容易使得预测结果不准确,简单来说就是把距离新输入的点很远的点也纳入考虑范围了,可能这两者之间就没什么太大关系,反而会影响到结果的准确判断。

K值的选择一般是采用交叉验证的方式,把训练集中的一部分拿出来当验证集来用,以提升K值选择的准确性。

分类决策规则

这个其实就是一个计算代价函数的过程,用上边说的交叉验证的方式,拿一部分(m个)点进来测试,如果第i个点判断对了,就记为0,判断错了就记为1,然后求和,也就是求出判断错误的点的数量,最后再除以m,就是误分类率了,其数学表示如下:
在这里插入图片描述

K近邻法实现:Kd树

这一部分并没有讲解,所以我在之前了解过的基础上,结合书上的知识说明如下:
实现KNN的原始方法是线性扫描(遍历),也就是把给定点和所有点算一遍距离,肯定是更加耗费时间的,所以才有了kd树搜索法。

kd树简述

这里以二维平面中有限个点,谈一下我的理解:

首先要选择一个根节点,一般是把所有点的X值排序,取中位数所在的点作为根节点,中位数的左右的点集,再用同样的方式去分,直到分到最后的子节点是叶节点(即单个点,不是点集),完成kd树构造,如下图所示:

在二维平面上的理解就是,选中根点之后,对这个二维平面沿着根节点竖着砍一刀,分成两个小平面,再沿着左右选中的子节点在各自的小平面横着砍一刀(竖一刀横一刀依次),各分成两个更小的平面,直到砍到区域内只剩下叶节点了,那么这个二维平面就分好了,如下图所示:

用kd树的最邻近搜索

先把书上的标准流程放在这,下边会写我自己的理解:

这个是书上的一道例题,我看了答案之后写下我的理解如下:
对于给定的新输入s点,然后执行以下步骤:
① 划定一个圆域,圆心是S,半径是S到D的距离;
② S是包含在节点D所在区域的,最近点设定为D(算法3.3的第1,2步);
③ 返回D的父节点B,看B和D谁离S近,这里是D更近,所以最近点还是D(算法3.3的第3(a)步);
④ 去到B的另一个叶子节点F,搜索最近邻,发现F的区域和S的圆域不相交,则回退到B的父节点,也就是根节点A(算法3.3的第3(b)步);
⑤ 查看A的另一个子节点C,发现S的圆域和C的区域有相交(整个矩形的上半部分都属于C),所以在C中再执行最近邻搜索(算法3.3的第3步);
⑥ 检查C的左右子节点E,G,发现E所在区域和S的圆域有交集,看E和D谁离S近,发现E更近,所以E是新的最近点(算法3.3的第1步);
⑦ 一直回退,退到A,无法再退,搜索结束(算法3.3的第4步)。

程序实现

自编程实现

用到的库:

import numpy as np
from collections import Counter
from draw import draw

其中draw是自行编写的一个画图的程序(draw.py)

KNN函数(类):

class KNN:
    def __init__(self, X_train, y_train, k=3):
        # 所需参数初始化
        self.k = k  # 所取k值
        self.X_train = X_train
        self.y_train = y_train

这里设定的k,如果没有输入的话,默认为3。

	def predict(self, X_new):
	    # 计算欧氏距离
	    dist_list = [(np.linalg.norm(X_new - self.X_train[i], ord=2), self.y_train[i])
	                 for i in range(self.X_train.shape[0])]
	    # 上述语句计算结果的形式:[(d0,-1),(d1,1)...]
	    # 对所有距离进行排序
	    dist_list.sort(key=lambda x: x[0])
	    # 取前k个最小距离对应的类别(也就是y值)
	    y_list = [dist_list[i][-1] for i in range(self.k)]
	    # 上述语句计算结果的形式:[-1,1,1,-1...]
	    # 对上述k个点的分类进行统计
	    y_count = Counter(y_list).most_common()
	    # 上述语句计算结果的形式:[(-1, 3), (1, 2)]
	    return y_count[0][0]

上述代码中,计算欧式距离时涉及到了以下问题:
① np.linalg.norm()是一个求范数的函数,对于参数ord描述如下:
在这里插入图片描述
其实这个ord参数就是上边提到的参数p,p=2就是欧式距离;

② self.X_train.shape[0]
输出self.X_train.shape的话会返回两个值,第一个(shape[0])是X的行数,第二个(shape[1])是X的列数;

③ 这个计算后输出的结果就是输入的点(X_new),到每一个已知的样本点的欧氏距离,以及该样本点对应的标签(self.y_train[i]);

注:这里记录一下我之前没有见过的写循环的方式,比如这句话

y_list = [dist_list[i][-1] for i in range(self.k)]

如果之前我写的话可能要用for写一行,再计算每一个dist_list[i][-1],再把这些数组合起来,但是用上边这种方法,一行代码就解决了。

而最后统计时,涉及的代码:
Counter(y_list).most_common()是一个统计用的函数,返回的是一个列表,里边元素组成是[标签名+出现次数],排列顺序是出现次数从高到低排的,所以最后返回排头的数(y_count[0][0])即可。

draw函数:

import matplotlib.pyplot as plt
import numpy as np


def draw(X_train, y_train, X_new):
    # 正负实例点初始化
    X_po = np.zeros(X_train.shape[1])
    X_ne = np.zeros(X_train.shape[1])
    # 区分正、负实例点
    for i in range(y_train.shape[0]):
        if y_train[i] == 1:
            X_po = np.vstack((X_po, X_train[i]))
        else:
            X_ne = np.vstack((X_ne, X_train[i]))
    # 实例点绘图
    plt.plot(X_po[1:, 0], X_po[1:, 1], "g*", label="1")
    plt.plot(X_ne[1:, 0], X_ne[1:, 1], "rx", label="-1")
    plt.plot(X_new[:, 0], X_new[:, 1], "bo", label="test_points")
    # 测试点坐标值标注
    for xy in zip(X_new[:, 0], X_new[:, 1]):
        plt.annotate("test{}".format(xy), xy)
    # 设置坐标轴
    plt.axis([0, 10, 0, 10])
    plt.xlabel("x1")
    plt.ylabel("x2")
    # 显示图例
    plt.legend()
    # 显示图像
    plt.show()

主函数:

def main():
    # 训练数据
    X_train = np.array([[5, 4],
                        [9, 6],
                        [4, 7],
                        [2, 3],
                        [8, 1],
                        [7, 2]])
    y_train = np.array([1, 1, 1, -1, -1, -1])
    # 测试数据
    X_new = np.array([[5, 3]])
    # 绘图
    draw(X_train, y_train, X_new)
    # 不同的k(取奇数)对分类结果的影响
    for k in range(1, 6, 2):
        # 构建KNN实例
        clf = KNN(X_train, y_train, k=k)
        # 对测试数据进行分类预测
        y_predict = clf.predict(X_new)
        print("k={},被分类为:{}".format(k, y_predict))

画图结果如下:
在这里插入图片描述

sklearn实现

用到的库:

import numpy as np
from sklearn.neighbors import KNeighborsClassifier
def main():
    # 训练数据
    X_train = np.array([[5, 4],
                        [9, 6],
                        [4, 7],
                        [2, 3],
                        [8, 1],
                        [7, 2]])
    y_train = np.array([1, 1, 1, -1, -1, -1])
    # 待预测数据
    X_new = np.array([[5, 3]])
    # 不同k值对结果的影响
    for k in range(1, 6, 2):
        # 构建实例
        clf = KNeighborsClassifier(n_neighbors=k, n_jobs=-1)
        # 选择合适算法
        clf.fit(X_train, y_train)
        # 预测
        y_predict = clf.predict(X_new)
        print("预测正确率:{:.0%}".format(clf.score([[5, 3]], [[1]])))
        print("k={},被分类为:{}".format(k, y_predict))

其中涉及到的 KNeighborsClassifier(),参数如下:

其中,第一个参数n_neighbors就是k值,另外KNeighborsClassifier涉及到的子方法如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值