《机器学习实战》kNN学习笔记
概述
k-近邻算法采用测量不同特征值之间的距离方法进行分类
优缺点
优点:精度高、对异常值不敏感、唔数据输入假定
缺点:计算复杂度高、空间复杂度高。
适用数据范围:数值型和标称型
标称型:标称型目标变量的结果只在有限目标集中取值,如真与假(标称型目标变量主要用于分类)标称型:标称型目标变量的结果只在有限目标集中取值,如真与假(标称型目标变量主要用于分类)
数值型:数值型目标变量则可以从无限的数值集合中取值,如0.100,42.001等 (数值型目标变量主要用于回归分析)
k-近邻算法的一般流程
收集数据:可以使用任何方法.
准备数据:距离计算所需要的数值,最好是结构化的数据格式.
分析数据:可以使用任何方法.
训练算法:此步骤不适用于 k-近邻算法.
测试算法:计算错误率.
使用算法:首先需要输入样本数据和结构化的输出结果,然后运行 k-近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理.
简单案例kNN.py
from numpy import *
import operator
from importlib import reload
# k-近邻算法
def createDataSet():
group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
labels = ['A','A','B','B']
return group , labels
def classify(inX,dataSet,labels,k):
# 计算距离
dataSetSize = dataSet.shape[0] #读取矩阵第一行的长度 4
diffMat = tile(inX,(dataSetSize,1)) - dataSet #tile 重复inx的各个维度1次 相减
sqDiffMat = diffMat ** 2
sqDistances = sqDiffMat.sum(axis = 1)
distances = sqDistances**0.5
sortedDistIndicies = distances.argsort()
classCount={}
# 选择距离最小的k个点
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0)+1
# 排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
然后在python交互式开发环境下
import kNN
kNN.classify([0,0],group,labels,3)
输出结果应该是B
可以改变输入【0,0】的值,查看运行结果
在约会网站上使用k-近邻算法
首先,把约会数据存放在文本文件datingTestSet中,样本主要包括以下三个特征
- 每年获得的飞行常客里程数
- 玩视频游戏所耗时间百分比
- 每周消耗的冰激凌公升数
在kNN.py中创建名为file2matrix的函数,以此来处理输入格式问题。该函数的输入为文件名字符串,输出为训练样本矩阵和类标签向量
def file2matrix(filename):
# 打开文件
fr = open(filename)
# 读取文件所有内容
arrayOlines = fr.readlines()
# 得到文件行数
numberOfLines = len(arrayOlines)
# 返回的NumPy矩阵numberOfLines行,3列
returnMat = np.zeros((numberOfLines, 3))
# 创建分类标签向量
classLabelVector = []
# 行的索引值
index = 0
# 读取每一行
for line in arrayOlines:
# 去掉每一行首尾的空白符,例如'\n','\r','\t',' '
line = line.strip()
# 将每一行内容根据'\t'符进行切片,本例中一共有4列
listFromLine = line.split('\t')
# 将数据的前3列进行提取保存在returnMat矩阵中,也就是特征矩阵
returnMat[index,:] = listFromLine[0:3]
# 根据文本内容进行分类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
在python命令提示符下输入下内容
>>> from numpy import *
>>> import kNN
>>> from importlib import reload
>>> reload(kNN)
<module 'kNN' from 'D:\\pythonworkspace\\Machine-Learning-in-Action-Python3-master\\kNN_Project1\\kNN.py'>
>>> datingDataMat,datingLabels = kNN.file2matrix('datingTestSet.txt')
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>>> ax.scatter(datingDataMat[:,1],datingDataMat[:,2])
<matplotlib.collections.PathCollection object at 0x000002B8A857A788>
>>> plt.show()
>>>
我们使用Matplotlib创建散点图
散点图使用大厅DataMat矩阵的第二第三列数据,分别表示特征值玩‘视频游戏所耗时间百分比’和’每周消耗的冰激凌公升数‘
如图所示
上图很难得到一些需要的数据信息,所以用色彩来标记不同的样本分类
Matplotlib库提供的scatter函数支持个性化标记散点图上的点。重新输入上面代码
调用scatter函数时使用下列参数
>>> ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15*array(datingLabels),15*array(datingLabels))
<matplotlib.collections.PathCollection object at 0x000002B8A857A788>
>>> plt.show()
结果如下图所示
归一化特征值
在处理不同取值范围的特征值时,我们通常采用的方法时讲数值归一化,例如将取值范围处理为0-1之间。
下面的公式可以将特征值转化为0-1区间内的值:
newValue = (oldValue - min)/(max - min)
其中min和max分别时数据集中最小特征值和最大特征值。虽然改变取值范围增加了分类器的复杂度,但能提高准确度。
在kNN.py中添加一个函数,autoNorm(),该函数可以将数字特征值转化为0-1的区间
def autoNorm(dataSet):
# 获取数据的最小值
minVals = dataSet.min(0)
# 获取数据的最大值
maxVals = dataSet.max(0)
# 最大值和最小值的范围
ranges = maxVals - minVals
# shape(dataSet)返回dataSet的矩阵行列数
normDataSet = np.zeros(np.shape(dataSet))
# numpy函数shape[0]返回dataSet的行数
m = dataSet.shape[0]
# 原始值减去最小值(x-xmin)
normDataSet = dataSet - np.tile(minVals, (m, 1))
# 差值处以最大值和最小值的差值(x-xmin)/(xmax-xmin)
normDataSet = normDataSet / np.tile(ranges, (m, 1))
# 归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
然后再python命令提示符下输入
>>> reload(kNN)
<module 'kNN' from 'D:\\pythonworkspace\\Machine-Learning-in-Action-Python3-master\\kNN_Project1\\kNN.py'>
>>> normMat,ranges,minVals = kNN.autoNorm(datingDataMat)
>>> normMat
array([[0.44832535, 0.39805139, 0.56233353],
[0.15873259, 0.34195467, 0.98724416],
[0.28542943, 0.06892523, 0.47449629],
...,
[0.29115949, 0.50910294, 0.51079493],
[0.52711097, 0.43665451, 0.4290048 ],
[0.47940793, 0.3768091 , 0.78571804]])
>>> ranges
array([9.1273000e+04, 2.0919349e+01, 1.6943610e+00])
>>> minVals
array([0. , 0. , 0.001156])
>>>
测试代码
def datingClassTest():
# 打开文件名
filename = "datingTestSet.txt"
# 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
# 取所有数据的10% hoRatio越小,错误率越低
hoRatio = 0.10
# 数据归一化,返回归一化数据结果,数据范围,最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
# 获取normMat的行数
m = normMat.shape[0]
# 10%的测试数据的个数
numTestVecs = int(m * hoRatio)
# 分类错误计数
errorCount = 0.0
for i in range(numTestVecs):
# 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
# k选择label数+1(结果比较好)
classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:],\
datingLabels[numTestVecs:m], 4)
print("分类结果:%d\t真实类别:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%" % (errorCount/float(numTestVecs)*100))
它首先使用了file2matrix和autoNorm()函数从文件读取数据并把它转换为归一化特征值。接着计算测试向量的数量,决定normMat向量中哪些为测试集,哪些为训练集,然后将这两部分数据输入到classify()函数中,最后计算错误率并输出结果
预测函数
def classifyPerson():
# 输出结果
resultList = ['讨厌', '有些喜欢', '非常喜欢']
# 三维特征用户输入
percentTats = float(input("玩视频游戏所消耗时间百分比:"))
ffMiles = float(input("每年获得的飞行常客里程数:"))
iceCream = float(input("每周消费的冰淇淋公升数:"))
# 打开的文件名
filename = "datingTestSet.txt"
# 打开并处理数据
datingDataMat, datingLabels = file2matrix(filename)
# 训练集归一化
normMat, ranges, minVals = autoNorm(datingDataMat)
# 生成NumPy数组,测试集
inArr = np.array([percentTats, ffMiles, iceCream])
# 测试集归一化
norminArr = (inArr - minVals) / ranges
# 返回分类结果
classifierResult = classify0(norminArr, normMat, datingLabels, 4)
# 打印结果
print("你可能%s这个人" % (resultList[classifierResult - 1]))
新加入函数raw_input(),该函数允许用户输入文本行命令并返回用户所输入的命令
使用k-近邻算法识别手写数字
首先,我们必须把图像格式化处理为一个向量。我们把一个3232的二进制图像矩阵转化为11024的向量,这样之前的分类棋就可以处理数字图像信息了
import numpy as np
import operator
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as kNN
"""
函数说明:将32*32的二进制图像转换为1*1024向量
Parameters:
filename - 文件名
Returns:
returnVect - 返回二进制图像的1*1024向量
def img2vector(filename):
# 创建1*1024零向量
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])
# 返回转换后的1*1024向量
return returnVect
该函数创建一个1*1024的numpy数组,然后打开指定的文件,读出文件的前32行,并将每行的前32个字符储存在numpy数组中,然后返回数组
"""
函数说明:手写数字分类测试
def handwritingClassTest():
# 测试集的Labels
hwLabels = []
# 返回trainingDigits目录下的文件名
trainingFilesList = listdir('trainingDigits')
# 返回文件夹下文件的个数
m = len(trainingFilesList)
# 初始化训练的Mat矩阵(全零阵),测试集
trainingMat = np.zeros((m, 1024))
# 从文件名中解析出训练集的类别
for i in range(m):
# 获得文件的名字
fileNameStr = trainingFilesList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
# 将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('trainingDigits/%s' % (fileNameStr))
# 构造kNN分类器
neigh = kNN(n_neighbors=3, algorithm='auto')
# 拟合模型,trainingMat为测试矩阵,hwLabels为对应标签
neigh.fit(trainingMat, hwLabels)
# 返回testDigits目录下的文件列表
testFileList = listdir('testDigits')
# 错误检测计数
errorCount = 0.0
# 测试数据的数量
mTest = len(testFileList)
# 从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
# 获得文件名字
fileNameStr = testFileList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 获得测试集的1*1024向量,用于训练
vectorUnderTest = img2vector('testDigits/%s' % (fileNameStr))
# 获得预测结果
classifierResult = neigh.predict(vectorUnderTest)
print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest * 100))
def main():
handwritingClassTest()
if __name__ == '__main__':
main()
在python输入kNN.handwritingClassTest()
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
分类返回结果为9 真实结果为9
总共错了12个数据
错误率为1.268499%
通过改变k的值,修改函数handwritingClassTest随机选取训练样本,改变训练样本的数目,都会影响错误率