文章目录
k-近邻法
代码、数据、相关书籍打包下载
链接:https://pan.baidu.com/s/1lHmuSWChCWKSMhdz-TSipg
提取码:yhly
或者查看https://github.com/apachecn/AiLearning
一、KNN 概述
k-近邻(k-NN, k-Nearest Neighbor)算法是一种 基本分类与回归方法 ,我们这里只讨论分类问题中的 k-近邻算法。
一句话总结: 近朱者赤近墨者黑!
k 近邻算法的输入为实例的特征向量,对应于特征空间的点;输出为实例的类别,可以取多类。k 近邻算法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其 k 个最近邻的训练实例的类别,通过多数表决等方式进行预测。因此,k近邻算法不具有显式的学习过程。
k 近邻算法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。
k值的选择、距离度量以及分类决策规则是k近邻算法的三个基本要素。
二、kNN 场景
电影可以按照题材分类,那么如何区分 动作片
和 爱情片
呢?
- 动作片: 打斗次数更多
- 爱情片: 亲吻次数更多
基于电影中的亲吻、打斗出现的次数,使用 k-近邻算法构造程序,就可以自动划分电影的题材类型。
现在根据上面我们得到的样本集中所有电影与未知电影的距离,按照距离递增排序,可以找到 k 个距离最近的电影。
假定 k=3,则三个最靠近的电影依次是, He’s Not Really into Dudes 、 Beautiful Woman 和 California Man。
knn 算法按照距离最近的三部电影的类型,决定未知电影的类型,而这三部电影全是爱情片,因此我们判定未知电影是爱情片。
三、k 近邻算法
近邻算法简单、直观:给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的 k 个实例,这个实例的多数属于某个类,就把该输入实例分为这个类,下面先叙述 k 近邻算法,然后再讨论其细节。
算法3.1(k近邻法)
输入:训练数据集
近邻法的特殊情况是 k=1 的情形,称为最近邻算法,对于输入的实例点〈特征向量)x ,最近邻法将训练数据集中与 x 最邻近点的类作为 x 的类.
四、k 近邻模型
近邻法使用的模型实际上对应于对特征空间的划分.模型由三个基本要素一一距离度量、k 值的选择和分类决策规则决定.
1、模型
k近邻法中,当训练集、距离度量(如欧氏距离)、k值及分类决策规则(如多数表决)确定后,对于任何一个新的输入实例,它所属的类唯一地确定.这相当于根据上述要素将特征空间划分为一些子空间,确定子空间里的每个点所属的类.这一事实从最近邻算法中可以看得很清楚.
特征空间中,对每个训练实例点 x i x_i xi ,距离该点比其他点更近的所有点组成一个区域,叫作单元(cell)。每个训练实例点拥有一个单元,所有训练实例点的单元构成对特征空间的一个划分.最近邻法将实例 x i x_i xi 的类 y i y_i yi 作为其单元中所有点的类标记(class label)。这样,每个单元的实例点的类别是确定的。
图3.1是二维特征空间划分的一个例子,
2、距离度量
特征空间中两个实例点的距离是两个实例点相似程度的反映.k 近邻模型的特征空间一般是 n 维实数向量空间
R
n
R^n
Rn .使用的距离是欧氏距离,但也可以是其他距离,如更一般的闵可夫斯基距离(Minkowski distance):
3、k 值的选择
k值的选择会对近邻法的结果产生重大影响
如果选择较小的值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用.但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感就如果邻近的实例点恰巧是噪声,预测就会出错.换句话说,k 值的减小就意味着整体模型变得复杂,容易发生过拟合.
如果选择较大的值,就相当于用较大邻域中的训练实例进行预测,其优点是可以减少学习的估计误差.但缺点是学习的近似误差会增大,这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误.k 值的增大就意味着整体的模型变得简单.
如果 k=N,那么无论输入实例是什么,都将简单地预测它属于在训练实例中最多的类.这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的.
在应用中,k 值一般取一个比较小的数值.通常采用交叉验证法来选取最优的值.
近似误差
:可以理解为对现有训练集的训练误差。
估计误差
:可以理解为对测试集的测试误差。
4、分类决策规则
近邻法中的分类决策规则往往是多数表决,即由输入实例的 k 个邻近的训练实例中的多数类决定输入实例的类.
多数表决规则(majorityvotingrule)有如下解释:如果分类的损失函数为 0-1 损失函数,分类函数为
五、项目案例
1、简单应用——上述动作片和爱情片
import numpy as np
import operator
import collections
"""
函数说明:创建数据集
Parameters:
无
Returns:
group - 数据集
labels - 分类标签
"""
def createDataSet():
# 四组二维特征
group = np.array([[1, 101], [5, 89], [108, 5], [115, 8]])
# 四组特征的标签
labels = ['爱情片', '爱情片', '动作片', '动作片']
return group, labels
"""
函数说明:kNN算法,分类器
Parameters:
inx - 用于分类的数据(测试集)
dataset - 用于训练的数据(训练集)
labes - 分类标签
k - kNN算法参数,选择距离最小的 k 个点
Returns:
sortedClassCount[0][0] - 分类结果
"""
def classify0(inx, dataset, labels, k):
# 计算距离
dist = np.sum((inx - dataset)**2, axis=1)**0.5
# k个最近的标签
k_labels = [labels[index]
for index in dist.argsort()[0: k]] # 返回数组值从小到大的索引值
# 出现次数最多的标签即为最终类别
label = collections.Counter(k_labels).most_common(1)[0][0]
return label
2、项目案例1: 优化约会网站的配对效果
项目概述
海伦使用约会网站寻找约会对象。经过一段时间之后,她发现曾交往过三种类型的人:
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
她希望:
- 工作日与魅力一般的人约会
- 周末与极具魅力的人约会
- 不喜欢的人则直接排除掉
现在她收集到了一些约会网站未曾记录的数据信息,这更有助于匹配对象的归类。
收集数据: 提供文本文件
海伦把这些约会对象的数据存放在文本文件 datingTestSet2.txt 中,总共有 1000 行。海伦约会的对象主要包含以下 3 种特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消费的冰淇淋公升数
文本文件数据格式如下:
40920 8.326976 0.953952 3
14488 7.153469 1.673904 2
26052 1.441871 0.805124 1
75136 13.147394 0.428964 1
38344 1.669788 0.134296 1
准备数据: 使用 Python 解析文本文件
from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import numpy as np
import operator
"""
函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
Parameters:
filename - 文件名
Returns:
returnMat - 特征矩阵
classLabelVector - 分类Label向量
Modify:
2017-03-24
"""
def file2matrix(filename):
#打开文件,此次应指定编码,
fr = open(filename,'r',encoding = 'utf-8')
#读取文件所有内容
arrayOLines = fr.readlines()
#针对有BOM的UTF-8文本,应该去掉BOM,否则后面会引发错误。
arrayOLines[0]=arrayOLines[0].lstrip('\ufeff')
#得到文件行数
numberOfLines = len(arrayOLines)
#返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
returnMat = np.zeros((numberOfLines,3))
#返回的分类标签向量
classLabelVector = []
#行的索引值
index = 0
for line in arrayOLines:
#s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
line = line.strip()
#使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
listFromLine = line.split('\t')
#将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
returnMat[index,:] = listFromLine[0:3]
#根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
# 对于datingTestSet2.txt 最后的标签是已经经过处理的 标签已经改为了1, 2, 3
if listFromLine[-1] == 'didntLike':
classLabelVector.append(1)
elif listFromLine[-1] == 'smallDoses':
classLabelVector.append(2)
elif listFromLine[-1] == 'largeDoses':
classLabelVector.append(3)
index += 1
return returnMat, classLabelVector
分析数据: 使用 Matplotlib 画二维散点图
"""
函数说明:可视化数据
Parameters:
datingDataMat - 特征矩阵
datingLabels - 分类Label
Returns:
无
Modify:
2017-03-24
"""
def showdatas(datingDataMat, datingLabels):
plt.rcParams['font.sans-serif']=['SimHei'] #显示中文标签
plt.rcParams['axes.unicode_minus']=False
#将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
#当nrow=2,nclos=2时,代表fig画布被分为四个区域,axs[0][0]表示第一行第一个区域
fig, axs = plt.subplots(nrows=2, ncols=2,sharex=False, sharey=False, figsize=(13,8))
numberOfLabels = len(datingLabels)
LabelsColors = []
for i in datingLabels:
if i == 1:
LabelsColors.append('black')
if i == 2:
LabelsColors.append('orange')
if i == 3:
LabelsColors.append('red')
#画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,散点大小为15,透明度为0.5
axs[0][0].scatter(x=datingDataMat[:,0], y=datingDataMat[:,1], color=LabelsColors,s=15, alpha=.5)
#设置标题,x轴label,y轴label
axs0_title_text = axs[0][0].set_title(u'每年获得的飞行常客里程数与玩视频游戏所消耗时间占比')
axs0_xlabel_text = axs[0][0].set_xlabel(u'每年获得的飞行常客里程数')
axs0_ylabel_text = axs[0][0].set_ylabel(u'玩视频游戏所消耗时间占比')
plt.setp(axs0_title_text, size=9, weight='bold', color='red')
plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
#画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
axs[0][1].scatter(x=datingDataMat[:,0], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
#设置标题,x轴label,y轴label
axs1_title_text = axs[0][1].set_title(u'每年获得的飞行常客里程数与每周消费的冰激淋公升数')
axs1_xlabel_text = axs[0][1].set_xlabel(u'每年获得的飞行常客里程数')
axs1_ylabel_text = axs[0][1].set_ylabel(u'每周消费的冰激淋公升数')
plt.setp(axs1_title_text, size=9, weight='bold', color='red')
plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
#画出散点图,以datingDataMat矩阵的第二(玩游戏)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
axs[1][0].scatter(x=datingDataMat[:,1], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
#设置标题,x轴label,y轴label
axs2_title_text = axs[1][0].set_title(u'玩视频游戏所消耗时间占比与每周消费的冰激淋公升数')
axs2_xlabel_text = axs[1][0].set_xlabel(u'玩视频游戏所消耗时间占比')
axs2_ylabel_text = axs[1][0].set_ylabel(u'每周消费的冰激淋公升数')
plt.setp(axs2_title_text, size=9, weight='bold', color='red')
plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
#设置图例
didntLike = mlines.Line2D([], [], color='black', marker='.',
markersize=6, label='didntLike')
smallDoses = mlines.Line2D([], [], color='orange', marker='.',
markersize=6, label='smallDoses')
largeDoses = mlines.Line2D([], [], color='red', marker='.',
markersize=6, label='largeDoses')
#添加图例
axs[0][0].legend(handles=[didntLike,smallDoses,largeDoses])
axs[0][1].legend(handles=[didntLike,smallDoses,largeDoses])
axs[1][0].legend(handles=[didntLike,smallDoses,largeDoses])
#显示图片
plt.show()
归一化数据,(归一化是一个让权重变为统一的过程,更多细节请参考:https://www.zhihu.com/question/19951858 )
样本3和样本4的距离:
(
0
−
67
)
2
+
(
20000
−
32000
)
2
+
(
1.1
−
0.1
)
2
\sqrt{(0-67)^2 + (20000-32000)^2 + (1.1-0.1)^2 }
(0−67)2+(20000−32000)2+(1.1−0.1)2
归一化特征值,消除特征之间量级不同导致的影响
归一化定义: 我是这样认为的,归一化就是要把你需要处理的数据经过处理后(通过某种算法)限制在你需要的一定范围内。首先归一化是为了后面数据处理的方便,其次是保正程序运行时收敛加快。 方法有如下:
-
线性函数转换,表达式如下:
y=(x-MinValue)/(MaxValue-MinValue)
说明: x、y分别为转换前、后的值,MaxValue、MinValue分别为样本的最大值和最小值。
-
对数函数转换,表达式如下:
y=log10(x)
说明: 以10为底的对数函数转换。
如图:
-
反余切函数转换,表达式如下:
y=arctan(x)*2/PI
如图:
"""
函数说明:对数据进行归一化
Parameters:
dataSet - 特征矩阵
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
Modify:
2017-03-24
"""
def autoNorm(dataSet):
#获得数据的最小值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
#最大值和最小值的范围
ranges = maxVals - minVals
#shape(dataSet)返回dataSet的矩阵行列数
normDataSet = np.zeros(np.shape(dataSet))
#返回dataSet的行数
m = dataSet.shape[0]
#原始值减去最小值
normDataSet = dataSet - np.tile(minVals, (m, 1))
#除以最大和最小值的差,得到归一化数据
normDataSet = normDataSet / np.tile(ranges, (m, 1))
#返回归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
训练算法: 此步骤不适用于 k-近邻算法
因为测试数据每一次都要与全量的训练数据进行比较,所以这个过程是没有必要的。
kNN 算法伪代码:
对于每一个在数据集中的数据点:
计算目标的数据点(需要分类的数据点)与该数据点的距离
将距离排序: 从小到大
选取前K个最短距离
选取这K个中最多的分类类别
返回该类别来作为目标数据点的预测值
"""
函数说明:kNN算法,分类器
Parameters:
inX - 用于分类的数据(测试集)
dataSet - 用于训练的数据(训练集)
labes - 分类标签
k - kNN算法参数,选择距离最小的k个点
Returns:
sortedClassCount[0][0] - 分类结果
Modify:
2017-03-24
"""
def classify0(inX, dataSet, labels, k):
#numpy函数shape[0]返回dataSet的行数
dataSetSize = dataSet.shape[0]
#在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
#二维特征相减后平方
sqDiffMat = diffMat**2
#sum()所有元素相加,sum(0)行相加,sum(1)列相加
sqDistances = sqDiffMat.sum(axis=1)
#开方,计算出距离
distances = sqDistances**0.5
#返回distances中元素从小到大排序后的索引值
sortedDistIndices = distances.argsort()
#定一个记录类别次数的字典
classCount = {}
for i in range(k):
#取出前k个元素的类别
voteIlabel = labels[sortedDistIndices[i]]
#dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
#计算类别次数
classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
#python3中用items()替换python2中的iteritems()
#key=operator.itemgetter(1)根据字典的值进行排序
#key=operator.itemgetter(0)根据字典的键进行排序
#reverse降序排序字典
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
print(sortedClassCount)
#返回次数最多的类别,即所要分类的类别
return sortedClassCount[0][0]
测试算法: 使用海伦提供的部分数据作为测试样本。
如果预测分类与实际类别不同,则标记为一个错误。
"""
函数说明:分类器测试函数
取百分之十的数据作为测试数据,检测分类器的正确性
Parameters:
无
Returns:
无
Modify:
2017-03-24
"""
def datingClassTest():
#打开的文件名
filename = "./data/datingTestSet.txt"
#将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
#取所有数据的百分之十
hoRatio = 0.10
#数据归一化,返回归一化后的矩阵,数据范围,数据最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
#获得normMat的行数
m = normMat.shape[0]
#百分之十的测试数据的个数
numTestVecs = int(m * hoRatio)
#分类错误计数
errorCount = 0.0
for i in range(numTestVecs):
#前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:],
datingLabels[numTestVecs:m], 4)
print("分类结果:%s\t真实类别:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))
使用算法: 产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
约会网站预测函数
"""
函数说明:通过输入一个人的三维特征,进行分类输出
Parameters:
无
Returns:
无
Modify:
2017-03-24
"""
def classifyPerson():
#输出结果
resultList = ['讨厌','有些喜欢','非常喜欢']
#三维特征用户输入
precentTats = float(input("玩视频游戏所耗时间百分比:"))
ffMiles = float(input("每年获得的飞行常客里程数:"))
iceCream = float(input("每周消费的冰激淋公升数:"))
#打开的文件名
filename = "./data/datingTestSet.txt"
#打开并处理数据
datingDataMat, datingLabels = file2matrix(filename)
#训练集归一化
normMat, ranges, minVals = autoNorm(datingDataMat)
#生成NumPy数组,测试集
inArr = np.array([ffMiles, precentTats, iceCream])
#测试集归一化
norminArr = (inArr - minVals) / ranges
#返回分类结果
classifierResult = classify0(norminArr, normMat, datingLabels, 3)
#打印结果
print("你可能%s这个人" % (resultList[classifierResult-1]))
调包实现:
import sklearn.neighbors
def sklearnKnnTest():
"""1. Load the dataset(通过调包载入数据)"""
#打开的文件名
filename = "./data/datingTestSet.txt"
#将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
# datingDataMat是一个矩阵,有1000行3列,即有1000个样本,每个样本都具有3种特征
"""2. Split the data(划分数据集,分成训练集和测试集)"""
#取所有数据的百分之十
hoRatio = 0.10
#数据归一化,返回归一化后的矩阵,数据范围,数据最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
#获得normMat的行数
m = normMat.shape[0]
#百分之十的测试数据的个数
numTestVecs = int(m * hoRatio)
"""3. Indicate the training set"""
# n_neighbors即KNN算法中的K值,此处设为4
tempClassifier = sklearn.neighbors.KNeighborsClassifier(n_neighbors=4)
tempClassifier.fit(normMat[numTestVecs:,:], datingLabels[numTestVecs:])
"""4. Test"""
tempScore = tempClassifier.score(normMat[:numTestVecs,:], datingLabels[:numTestVecs])
print("The score is: ", tempScore)
3、项目案例2: 手写数字识别系统
项目概述
构造一个能识别数字 0 到 9 的基于 KNN 分类器的手写数字识别系统。
需要识别的数字是存储在文本文件中的具有相同的色彩和大小: 宽高是 32 像素 * 32 像素的黑白图像。
收集数据: 提供文本文件
目录 trainingDigits 中包含了大约 2000 个例子,每个例子内容如下图所示,每个数字大约有 200 个样本;目录 testDigits 中包含了大约 900 个测试数据。
准备数据: 编写函数 img2vector(), 将图像文本数据转换为分类器使用的向量
将图像文本数据转换为向量
"""
函数说明:将32x32的二进制图像转换为1x1024向量。
Parameters:
filename - 文件名
Returns:
returnVect - 返回的二进制图像的1x1024向量
Modify:
2017-03-25
"""
def img2vector(filename):
#创建1x1024零向量
returnVect = np.zeros((1, 1024))
#打开文件
fr = open(filename)
#按行读取
for i in range(32):
#读一行数据
lineStr = fr.readline()
#每一行的前32个元素依次添加到returnVect中
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
#返回转换后的1x1024向量
return returnVect
分析数据: 在 Python 命令提示符中检查数据,确保它符合要求
在 Python 命令行中输入下列命令测试 img2vector 函数,然后与文本编辑器打开的文件进行比较:
训练算法: 此步骤不适用于 KNN
因为测试数据每一次都要与全量的训练数据进行比较,所以这个过程是没有必要的。
测试算法: 编写函数使用提供的部分数据集作为测试样本,如果预测分类与实际类别不同,则标记为一个错误
from os import listdir
"""
函数说明:手写数字分类测试
Parameters:
无
Returns:
无
Modify:
2017-03-25
"""
def handwritingClassTest():
#测试集的Labels
hwLabels = []
#返回trainingDigits目录下的文件名
trainingFileList = listdir('./data/trainingDigits')
#返回文件夹下文件的个数
m = len(trainingFileList)
#初始化训练的Mat矩阵,测试集
trainingMat = np.zeros((m, 1024))
#从文件名中解析出训练集的类别
for i in range(m):
#获得文件的名字
fileNameStr = trainingFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
#将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('./data/trainingDigits/%s' % (fileNameStr))
#返回testDigits目录下的文件名
testFileList = listdir('./data/testDigits')
#错误检测计数
errorCount = 0.0
#测试数据的数量
mTest = len(testFileList)
#从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
#获得文件的名字
fileNameStr = testFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#获得测试集的1x1024向量,用于训练
vectorUnderTest = img2vector('./data/testDigits/%s' % (fileNameStr))
#获得预测结果
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest))
六、KNN 小结
KNN 是什么?定义: 监督学习? 非监督学习?
KNN 是一个简单的无显示学习过程,非泛化学习的监督学习模型。在分类和回归中均有应用。
1、基本原理
简单来说: 通过距离度量来计算查询点(query point)与每个训练数据点的距离,然后选出与查询点(query point)相近的K个最邻点(K nearest neighbors),使用分类决策来选出对应的标签来作为该查询点的标签。
2、KNN 三要素
K, K的取值
-
对查询点标签影响显著(效果拔群)。k值小的时候 近似误差小,估计误差大。 k值大 近似误差大,估计误差小。
-
如果选择较小的 k 值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。换句话说,k 值的减小就意味着整体模型变得复杂,容易发生过拟合。
-
如果选择较大的 k 值,就相当于用较大的邻域中的训练实例进行预测。其优点是可以减少学习的估计误差。但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。 k 值的增大就意味着整体的模型变得简单。
-
太大太小都不太好,可以用交叉验证(cross validation)来选取适合的k值。
-
近似误差和估计误差,请看这里: https://www.zhihu.com/question/60793482
距离度量 Metric/Distance Measure
- 距离度量 通常为 欧式距离(Euclidean distance),还可以是 闵可夫斯基 距离 或者 曼哈顿距离。也可以是 地理空间中的一些距离公式。(更多细节可以参看 sklearn 中 valid_metric 部分)
分类决策 (decision rule)
- 分类决策 在 分类问题中 通常为通过少数服从多数 来选取票数最多的标签,在回归问题中通常为 K个最邻点的标签的平均值。
3、算法: (sklearn 上有三种)
-
Brute Force 暴力计算/线性扫描
-
KD Tree 使用二叉树根据数据维度来平分参数空间。
-
Ball Tree 使用一系列的超球体来平分训练数据集。
-
树结构的算法都有建树和查询两个过程。Brute Force 没有建树的过程。
算法特点:
优点:精度高、对异常值不敏感、无数据输入假定。
缺点:计算复杂度高、空间复杂度高。
适用数据范围:数值型和标称型。
相似同源产物:
radius neighbors 根据制定的半径来找寻邻点
影响算法因素:
N 数据集样本数量(number of samples), D 数据维度 (number of features)
总消耗:
- Brute Force: O[ D N 2 DN^2 DN2]
此处考虑的是最蠢的方法: 把所有训练的点之间的距离都算一遍。当然有更快的实现方式, 比如 O(ND + kN) 和 O(NDK) , 最快的是 O[DN] 。感兴趣的可以阅读这个链接: k-NN computational complexity https://stats.stackexchange.com/questions/219655/k-nn-computational-complexity
-
KD Tree: O[ D N l o g ( N ) DN log(N) DNlog(N)]
-
Ball Tree: O[ D N l o g ( N ) DN log(N) DNlog(N)] 跟 KD Tree 处于相同的数量级,虽然建树时间会比 KD Tree 久一点,但是在高结构的数据,甚至是高纬度的数据中,查询速度有很大的提升。
查询所需消耗:
-
Brute Force: O[ D N DN DN]
-
KD Tree: 当维度比较小的时候, 比如 D<20, O[ D l o g ( N ) Dlog(N) Dlog(N)] 。相反,将会趋向于 O[DN]
-
Ball Tree: O[ D l o g ( N ) Dlog(N) Dlog(N)]
当数据集比较小的时候,比如 N<30的时候,Brute Force 更有优势。
Intrinsic Dimensionality(本征维数) 和 Sparsity(稀疏度)
数据的 intrinsic dimensionality 是指数据所在的流形的维数 d < D , 在参数空间可以是线性或非线性的。稀疏度指的是数据填充参数空间的程度(这与“稀疏”矩阵中使用的概念不同, 数据矩阵可能没有零项, 但是从这个意义上来讲,它的结构 仍然是 “稀疏” 的)。
Brute Force 的查询时间不受影响。
对于 KD Tree 和 Ball Tree的查询时间, 较小本征维数且更稀疏的数据集的查询时间更快。KD Tree 的改善由于通过坐标轴来平分参数空间的自身特性 没有Ball Tree 显著。
k的取值 (k 个邻点)
Brute Force 的查询时间基本不受影响。
但是对于 KD Tree 和 Ball Tree , k越大,查询时间越慢。
k 在N的占比较大的时候,使用 Brute Force 比较好。
Number of Query Points (查询点数量, 即测试数据的数量)
查询点较少的时候用Brute Force。查询点较多的时候可以使用树结构算法。
关于 sklearn 中模型的一些额外干货:
如果KD Tree,Ball Tree 和Brute Force 应用场景傻傻分不清楚,可以直接使用 含有algorithm='auto’的模组。 algorithm=‘auto’ 自动为您选择最优算法。 有 regressor 和 classifier 可以来选择。
metric/distance measure 可以选择。 另外距离 可以通过weight 来加权。
leaf size 对KD Tree 和 Ball Tree 的影响
建树时间: leaf size 比较大的时候,建树时间也就快点。
查询时间: leaf size 太大太小都不太好。如果leaf size 趋向于 N(训练数据的样本数量),算法其实就是 brute force了。如果leaf size 太小了,趋向于1,那查询的时候 遍历树的时间就会大大增加。leaf size 建议的数值是 30,也就是默认值。
内存: leaf size 变大,存树结构的内存变小。
Nearest Centroid Classifier
分类决策是哪个标签的质心与测试点最近,就选哪个标签。
该模型假设在所有维度中方差相同。 是一个很好的base line。
进阶版: Nearest Shrunken Centroid
可以通过shrink_threshold来设置。
作用: 可以移除某些影响分类的特征,例如移除噪音特征的影响
七、参考文献
ApacheCN GitHub地址: https://github.com/apachecn/AiLearning
2012.李航.统计学习方法