传送门
《统计学习方法》读书笔记——机器学习常用评价指标
《统计学习方法》读书笔记——感知机(原理+代码实现)
《统计学习方法》读书笔记——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=1∑n∣xi(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