简单来说,k-紧邻算法采用测量不同特征值之间的距离方法进行分类。
优点:精度高、对异常值不敏感,无数据输入假定。
缺点 :计算复杂度高、空间复杂度高。
使用数据范围 : 数值型和标称型(标称型目标变量的结果只在有限目标集中取值,如真与假(标称型目标变量主要用于分类)
工作原理:给定一个样本数据集(训练样本集),并且样本集中每个数据都存在标签,即我们知道样本集中每个数据与所属分类的对应关系。对新输入没有标签的实例,在训练数据集中找到与该实例最邻近的 k 个实例,这 k 个实例的多数属于某个类,就把该输入实例分为这个类。
项目案例1:优化约会网站的配对效果
项目概述
海伦使用约会网站寻找约会对象。经过一段时间之后,她发现曾交往过三种类型的人:
- 1:不喜欢的人
- 2:魅力一般的人
- 3:极具魅力的人
她希望:
- 不喜欢的人则直接排除掉
- 工作日与魅力一般的人约会
- 周末与极具魅力的人约会
现在她收集到了一些约会网站未曾记录的数据信息,这更有助于匹配对象的归类。
实验流程:
Step1:收集数据
海伦把这些约会对象的数据存放在文本文件 datingTestSet2.txt 中,总共有 1000 行。海伦约会的对象主要包含以下 3 种特征:
- Col1:每年获得的飞行常客里程数
- Col2:玩视频游戏所耗时间百分比
- Col3:每周消费的冰淇淋公升数
Step2:准备数据
使用 Python 解析文本文件。创建file2metrix函数,以此来处理输入格式问题。该函数的输入文本是 文件名字符串,输出为训练样本矩阵和类标签向量。
将文本记录转换为 NumPy 的解析程序如下所示:
import numpy as np
def file2matrix(filename):
"""
Desc:
导入训练数据
parameters:
filename: 数据文件路径
return:
数据矩阵 returnMat 和对应的类别 classLabelVector
"""
fr = open(filename)
# 获得文件中的数据行的行数
lines = fr.readlines()
numberOfLines = len(lines) # type: int
# 生成对应的空矩阵
# 例如:zeros(2,3)就是生成一个 2*3的矩阵,各个位置上全是 0
returnMat = np.zeros((numberOfLines, 3)) # prepare matrix to return
classLabelVector = [] # prepare labels return
index = 0
for line in lines:
# str.strip([chars]) --返回已移除字符串头尾指定字符所生成的新字符串
line = line.strip()#截取掉所有的回车字符
# 以 '\t' 切割字符串
listFromLine = line.split('\t')
# 每列的属性数据
returnMat[index, :] = listFromLine[0:3]
# 每列的类别数据,就是 label 标签数据
classLabelVector.append(int(listFromLine[-1]))
index += 1
# 返回数据矩阵returnMat和对应的类别classLabelVector
return returnMat, classLabelVector
Step3:分析数据
使用 Matplotlib 画二维散点图。
if __name__ == '__main__':
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
color = ['r', 'g', 'b']
fig = plt.figure()
ax = fig.add_subplot(311)#将画布分割成3行4列,图像画在从左到右从上到下的第9块
#那第十块怎么办,3410是不行的,可以用另一种方式(3,4,10)
for i in range(1, 4):
index = np.where(np.array(datingLabels) == i)
ax.scatter(datingDataMat[index, 0], datingDataMat[index, 1], c=color[i - 1], label=i)
plt.xlabel('Col.0')
plt.ylabel('Col.1')
plt.legend()
bx = fig.add_subplot(312)
for i in range(1, 4):
index = np.where(np.array(datingLabels) == i)
bx.scatter(datingDataMat[index, 0], datingDataMat[index, 2], c=color[i - 1], label=i)
plt.xlabel('Col.0')
plt.ylabel('Col.2')
plt.legend()
cx = fig.add_subplot(313)
for i in range(1, 4):
index = np.where(np.array(datingLabels) == i)
cx.scatter(datingDataMat[index, 1], datingDataMat[index, 2], c=color[i - 1], label=i)
plt.xlabel('Col.1')
plt.ylabel('Col.2')
plt.legend()
plt.show()
归一化特征值,消除特征之间量级不同导致的影响。
def autoNorm(dataSet):
"""
Desc:
归一化特征值,消除特征之间量级不同导致的影响
parameter:
dataSet: 数据集
return:
归一化后的数据集 normDataSet.ranges和minVals即最小值与范围,并没有用到
归一化公式:
Y = (X-Xmin)/(Xmax-Xmin)
其中的 min 和 max 分别是数据集中的最小特征值和最大特征值。该函数可以自动将数字特征值转化为0到1的区间。
"""
# 计算每种属性的最大值、最小值、范围
minVals = np.min(dataSet, axis=0)
maxVals = np.max(dataSet, axis=0)
# 极差
ranges = maxVals - minVals
#返回dataSet矩阵的行数(读取矩阵第一维的长度)
m = dataSet.shape[0]
# 生成与最小值之差组成的矩阵
normDataSet = dataSet - np.tile(minVals, (m, 1))
# 将最小值之差除以范围组成矩阵
normDataSet = normDataSet / np.tile(ranges, (m, 1)) # element wise divide
return normDataSet, ranges, minVals
Step4:训练算法
此步骤不适用于 k-近邻算法。因为测试数据每一次都要与全部的训练数据进行比较,所以这个过程是没有必要的。
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
# 距离度量 度量公式为欧氏距离
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat ** 2
sqDistances = np.sum(sqDiffMat, axis=1)
distances = sqDistances ** 0.5
# 将距离排序:从小到大
sortedDistIndicies = distances.argsort()
# 选取前K个最短距离, 选取这K个中最多的分类类别
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1#次数加1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)#对label出现的次数从大到小进行排序
return sortedClassCount[0][0]# 返回出现次数最大的label
Step5:测试算法
计算错误率,使用海伦提供的部分数据作为测试样本。如果预测分类与实际类别不同,则标记为一个错误。
def datingClassTest():
"""
Desc:
对约会网站的测试方法
parameters:
none
return:
错误数
"""
# 设置测试数据的的一个比例
hoRatio = 0.1 # 测试范围,一部分测试一部分作为样本
# 从文件中加载数据
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt') # load data setfrom file
# 归一化数据
normMat, ranges, minVals = autoNorm(datingDataMat)
# m 表示数据的行数,即矩阵的第一维
m = normMat.shape[0]
# 设置测试的样本数量, numTestVecs:m表示训练样本的数量
numTestVecs = int(m * hoRatio)
print('numTestVecs=', numTestVecs)
errorCount = 0.0
for i in range(numTestVecs):
# 对数据测试
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 3)
print("分类器返回结果: %d, 实际结果: %d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率: %f" % (errorCount / float(numTestVecs)))
print(errorCount)
Step6:使用算法
产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
约会网站预测函数如下:
def classifyPerson():
resultList = ['不喜欢的人', '魅力一般的人', '极具魅力的人']
ffMiles = float(input("每年获得的飞行常客里程数?"))
percentTats = float(input("玩视频游戏所耗时间百分比?"))
iceCream = float(input("每周消费的冰淇淋公升数?"))
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = np.array([ffMiles, percentTats, iceCream])
intX = (inArr - minVals) / ranges
classifierResult = classify0(intX, normMat, datingLabels, 3)
print("这个人属于: ", resultList[classifierResult - 1])
'''
每年获得的飞行常客里程数? 10000
玩视频游戏所耗时间百分比? 10
每周消费的冰淇淋公升数? 0.5
这个人属于: 魅力一般的人
'''
总结:
KNN算法是最简单有效的分类算法,简单且容易实现。当训练数据集很大时,需要大量的存储空间,而且需要计算待测样本和训练数据集中所有样本的距离,所以非常耗时。
KNN对于随机分布的数据集分类效果较差,对于类内间距小,类间间距大的数据集分类效果好,而且对于边界不规则的数据效果好于线性分类器。
KNN对于样本不均衡的数据效果不好,需要进行改进。改进的方法时对k个近邻数据赋予权重,比如距离测试样本越近,权重越大。
KNN很耗时,时间复杂度为O(n),一般适用于样本数较少的数据集,当数据量大时,可以将数据以树的形式呈现,能提高速度,常用的有kd-tree和ball-tree。