《统计学习方法》读书笔记——K近邻法(原理+代码实现)

传送门

《统计学习方法》读书笔记——机器学习常用评价指标
《统计学习方法》读书笔记——感知机(原理+代码实现)
《统计学习方法》读书笔记——K近邻法(原理+代码实现)
《统计学习方法》读书笔记——朴素贝叶斯法(公式推导+代码实现)


一、K近邻法

K近邻法((k-nearest neighbor),简称KNN,是一种基本的分类算法。其描述如下:
给定一个数据集与待分类样本 x x x,分类时,在数据集中找出 K个 x x x 最近邻的样本。根据这K个样本的类别,通过 多数表决等方式进行预测 x x x的类别。

注:K近邻法没有显式的学习过程

二、K近邻法模型

k 近邻法使用的模型实际上对应于对特征空间的划分。K近邻法模型有三个基本要素:距离度量K值的选择分类决策规则
特征空间中,对每个训练样本 x i x_i xi,距离该点比其他点更近的所有点组成一个区域,叫作单元( cell) 。每个训练实例点拥有一个单元,所有训练实例点的单元构成对特征空间的一个划分。

2.1 距离度量

使用 L p L_p Lp距离可以对特征空间中的两个实例点的相近邻程度进行度量, L p L_p Lp的定义如下:
假设 x i x_i xi x j x_j xj n n n维度特征空间中的两个实例点,其中 x i = ( x i ( 1 ) , x i ( 2 ) , . . . , x i ( n ) ) T x_i = (x_i^{(1)},x_i^{(2)},...,x_i^{(n)})^T xi=(xi(1),xi(2),...,xi(n))T x j = ( x j ( 1 ) , x j ( 2 ) , . . . , x j ( n ) ) T x_j = (x_j^{(1)},x_j^{(2)},...,x_j^{(n)})^T xj=(xj(1),xj(2),...,xj(n))T,则
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i ( l ) − x j ( l ) ∣ p ) 1 p L_p(x_i,x_j) = (\sum_{l=1}^{n}|x_i^{(l)} - x_j^{(l)}|^p )^{\frac{1}{p}} Lp(xi,xj)=(l=1nxi(l)xj(l)p)p1
在上式中,若 p = 2 p=2 p=2即为欧式距离,在K近邻法中常用欧式距离做相近邻程度的度量。

2.2 K值的选择

  • K值较小的时候,预测结果对近邻的样本数据非常敏感,易使模型发生过拟合
  • K值过大时,意味着整体的模型变得简单,易使模型发生欠拟合。若 k = N k = N k=N,即K值为数据集的样本总数,则不管输入什么数据,得到的结果永远是数据集中数量最多的类别
  • 在应用中,K值一般取一个比较小的数值。通常采用交叉验证法来选取最优的K值。

2.3 决策规则

K近邻法中的分类决策规则往往是多数表决。即假设K=10,在选得10个近邻的样本后,哪个类别最多,数据结果就是那个类别!

三、K近邻法的一种实现:kd树

实现K近邻法时,主要考虑的问题是如何对训练数据进行快速K近邻搜索。最简单的实现时对数据集进行线性扫描,但过于耗时,其时间复杂度为 O ( n ) \mathcal{O(n)} O(n)。因此可使用特殊的数据结构存储数据集,以便在搜索时快速找到最近邻样本。

3.1 构造kd树

构造kd树相当于不断用垂直于坐标轴的超平面将k维空间作划分。

kd树构造算法
输入 k k k维空间数据集 T = { x 1 , x 2 , . . . , x N } T = \{x_1,x_2,...,x_N\} T={x1,x2,...,xN},其中 x i = ( x i ( 1 ) , x i ( 2 ) , . . . , x i ( k ) ) T , i = 1 , 2 , . . . , N x_i = (x_i^{(1)},x_i^{(2)},...,x_i^{(k)})^T,i = 1,2,...,N xi=(xi(1),xi(2),...,xi(k))T,i=1,2,...,N
输出:kd树
(1) 开始:构造根结点,根结点即为全部数据集对应的k维超矩形区域。
选择 x ( 1 ) x^{(1)} x(1)为坐标轴,以数据集中所有样本的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点(即 x 1 ( 1 ) , x 2 ( 1 ) , . . . , x N ( 1 ) x_1^{(1)},x_2^{(1)},...,x_N^{(1)} x1(1),x2(1),...,xN(1)),将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。
由根结点生成深度为1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应于坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
将落在切分超平面上的实例点保存在根结点。
(2) 重复:对深度为j的结点,选择 x ( l ) x^{(l)} x(l)为切分的坐标轴, l = j m o d    k + 1 l=j \mod k+1 l=jmodk+1,以该结点的区域中所有实例的 x ( l ) x^{(l)} x(l)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( l ) x^{(l)} x(l)垂直的超平面实现。
由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:左子结点对应坐标 x ( l ) x^{(l)} x(l)小于切分点的子区域,右子结点对应坐标 x ( l ) x^{(l)} x(l)大于切分点的子区域。
将落在切分超平面上的实例点保存在该结点。
(3) 直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

3.2 搜索kd树

kd树搜索算法
输入:己构造的kd树,目标点 x x x
输出:X 的最近邻。
(1) 在kd树中找出包含目标点x的叶结点:从根结点出发,递归地向下访问kd树。若目标点x当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止。
(2) 以此叶结点为“当前最近点”。
(3) 递归地向上回退,在每个结点进行以下操作:
  (a) 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。
  (b)当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。
如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,向上回退。
(4) 当回退到根结点时,搜索结束。最后的“当前最近点”即为x的最近邻点。

如果实例点是随机分布的,kd树搜索的平均计算复杂度是 O ( log ⁡ N ) \mathcal{O}(\log N) O(logN)

四、代码实现

没有实现kd树的构造和搜索,对数据集的搜索只是线性扫描,所以搜索时间很长。

# coding=utf-8
import numpy as np
import time
def loadData(fileName):
    print('start to read data:' + fileName)
    dataArr = []
    labelArr = []
    fr = open(fileName, 'r')
    # 将文件按行读取
    for line in fr.readlines():
        # 对每一行数据按切割符','进行切割,返回字段列表
        curLine = line.strip().split(',')
        dataArr.append([int(num) for num in curLine[1:]])
        labelArr.append(int(curLine[0]))
    # 返回data和label
    return dataArr, labelArr


def calcDist(x1, x2):
    # 计算欧式距离
    return np.sqrt(np.sum(np.square(x1 - x2)))


def getClosest(trainDataArr, trainLabelArr, x, topK):
    '''
    :param trainDataArr:训练集数据集
    :param trainLabelArr:训练集标签集
    :param x:要预测的样本x
    :param topK:选择参考最邻近样本的数目
    :return:预测的标记
    '''
    # x与训练集中样本的距离
    distList = [0] * len(trainLabelArr)
    # 遍历训练集中所有的样本点,计算与x的欧式距离
    for i in range(len(trainDataArr)):
        distList[i] = calcDist(trainDataArr[i], x)
    # 找出**欧式距离**最近的k个样本
    topKList = np.argsort(np.array(distList))[:topK]
    # "投票箱"
    labelList = [0] * 10
    # 对topK个索引进行遍历
    for index in topKList:
        labelList[int(trainLabelArr[index])] += 1
    # 返回“投票箱”中票数最多的label
    return labelList.index(max(labelList))


def model_test(trainData, trainLabel, testData, testLabel, topK):
    '''
    测试正确率
    :param trainData:训练集数据集
    :param trainLabel: 训练集标记
    :param testData: 测试集数据集
    :param testLabel: 测试集标记
    :param topK: 选择多少个邻近点参考
    :return: 正确率
    '''
    print('start test')
    trainDataArr = np.array(trainData)
    trainLabelArr = np.array(trainLabel).T
    testDataArr = np.array(testData)
    testLabelArr = np.array(testLabel).T

    # 错误数量
    errorCnt = 0
    for i in range(100):
        print('test %d:%d' % (i, 100))
        # 获取测试数据x的最近邻
        y = getClosest(trainDataArr, trainLabelArr, x=testDataArr[i], topK=topK)
        if y != testLabelArr[i]:
            errorCnt += 1
    # 返回正确率
    return 1 - (errorCnt / 100)


if __name__ == "__main__":
    start = time.time()
    # 获取训练集、测试集
    trainData, trainLabel = loadData('./mnist/mnist_train.csv')
    testData, testLabel = loadData('./mnist/mnist_test.csv')
    # 计算测试集正确率
    accur = model_test(trainData, trainLabel, testData, testLabel, 25)
    print('accur is:%d' % (accur * 100), '%')
    print('time span:', time.time() - start)

运行结果:

start to read data:./mnist/mnist_train.csv
start to read data:./mnist/mnist_test.csv
start test
test 0:100
test 1:100
test 2:100
....(笔者手动折叠)
test 99:100
accur is:98 %
time span: 92.45222759246826

参考

原理:《统计学习方法》
代码: https://github.com/Dod-o/Statistical-Learning-Method_Code

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值