这是《机器学习实战》第二单元k近邻算法的学习笔记,用的是python3实现代码,
对于k近邻这个算法,英文名是k-Nearest Neighbors,即k个最近的邻居,这里的近指的是距离近,所以我们要先计算距离,假如是计算到()的距离,我们用欧氏距离公式即可求得距离。求完距离之后对距离进行排序,离自己最近的k个坐标,分别对他们的标记(label)进行统计投票,得到票数最多那个即为输入向量的分类(class)。
下面是代码实现,
在此之前我们先准备数据,
我们准备四组打好标的二维向量,
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
准备好数据之后,开始写分类器,分类器大致可以分为三部分内容,
- 计算距离,所有的数据距离自己(输入向量inX)的距离
- 对距离进行排序,选择距离最近的k个点,并统计他们的label
- 得到票数最多的那个label
def classify0(inX, dataSet, labels, k):
'''
params:
inX: 输入需要分类的数据
dataSet: 已经打好标的数据
labels: 标记
k: 需要选取的最近邻居的数量
'''
dataSetSize = dataSet.shape[0] # data.shape可返回行和列,[0]即为行
diffMat = tile(inX, (dataSetSize, 1)) - dataSet # 差矩阵,tile()可以理解为复制,里边的参数分别为向下向下复制,这里将复制完的矩阵和原矩阵相减
sqDiffMat = diffMat ** 2 # 将差矩阵的每个值平方(不是矩阵乘矩阵)
sqDistances = sqDiffMat.sum(axis=1) # 将平方之后的值按行相加,.sum(axis=0)为按列相加
distances = sqDistances ** 0.5 # 将和再开方得到距离
sortedDistIndicies = distances.argsort() # 对距离进行排序,但得到的是排好的数据在原列表中的索引
classCount = {} # 分类统计
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]] # 由于是原列表中的索引,所以标记还是原来的顺序,没有被打乱,这样就很容易的得到label
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 # 默认为0,如果get不到就是0,否则在其基础上加一
sortedClassCount = sorted(classCount.items(), # 对classCount进行排序
key=operator.itemgetter(1),
reverse=True)
return sortedClassCount[0][0] # 得到票数最多的那个
做好之后进行简单的测试,我们需要测试的是[0,0]这个点的分类是什么,
分类结果为B,和预想结果没有差别,
接下来书中介绍到对约会数据进行测试,先是从文件中将数据解析出来,文件中数据格式是这样的,
前三列分别代表飞行里程数,玩游戏所占百分比,和每周消耗的冰淇淋数,最后一列为标记,1-3分别代表不喜欢的人,魅力一般的人和极具魅力的人,最后我们输入一组数据,来预测这个人是什么样的人,我们先从文件解析数据,
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 得到所有的行
numberOfLines = len(arrayOLines) # 得到行数
returnMat = zeros((numberOfLines, 3)) # 初始化行数为文件行数,列数为3的0矩阵
classLabelVector = [] # 标记向量
index = 0
for line in arrayOLines:
line = line.strip() # 去除前后空格
listFromLine = line.split('\t') # 以制表符为间隔分割
returnMat[index,:] = listFromLine[0:3] # 将前三个数据给returnMat返回向量
classLabelVector.append(listFromLine[-1]) # 将每行最后一个元素压入标记向量
index += 1
return returnMat, classLabelVector
得到数据之后,由于数据还没有达到最终能用的程度,比如飞行公里数,打游戏的时间,这些是具体的时间,并不是我们想要的,我们想要的是归一化后的数值,最小的为0,最大的为1,newValue = (oldValue - minVal) / (maxValue - minValue),下面的函数为归一化的过程,
def autoNorm(dataSet):
'''
dataSet: 需要归一化的数据
'''
minVals = dataSet.min(0) # min(0)得到每列的最小值,min(1)得到每行的最小值
maxVals = dataSet.max(0) # max(0)得到每行的最大值,max(1)得到每行的最小值
ranges = maxVals - minVals # 差
m = dataSet.shape[0] # 行数
normDataSet = dataSet - tile(minVals, (m, 1)) # (oldValue - minVal)
normDataSet = normDataSet / tile(ranges, (m, 1)) # (oldValue - minValue) / (maxValue - minValue)
return normDataSet, ranges, minVals
分类器还是用之前的分类器,原理也一样,都是算距离,现在从数据集中拿出10%的数据来测试,计算错误率,
def datingClassTest():
hoRation = 0.10 # 拿出10%的数据做测试
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt') # 从文件里加载数据
normMat, ranges, minVals = autoNorm(datingDataMat) # 归一化数据
m = normMat.shape[0] # 得到行数
numTestVecs = int(m * hoRation) # 需要测试的数量
errorCount = 0.0 # 错误数
for i in range(numTestVecs):
# 分类
classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:], datingLabels[numTestVecs:m], 3)
# 分类器得出的结果和原来的结果比较
print("The classifier came back with: %d, the real answer is: %d" % (int(classifierResult), int(datingLabels[i])))
# 不同的记录到errorCount
if(classifierResult != datingLabels[i]):
errorCount += 1.0
# 得出错误率
print("The total error rate is: %f" % (errorCount/float(numTestVecs)))
可得结果为,
现在我们随机输入一组数据来看这个人是不是有魅力的人,
需要注意的是python2里的raw_input()在python3里已改成input()作为输入,输入的数据不是归一化的数据,所以需要处理成分类器需要的数据, (oldValue - minValue) / (maxValue-minValue),
def classifyPerson():
resultList = ['not at all', 'in small doses', 'in large doses'] # 结果的三种可能
percentTats = float(input("percentage of time spent playing video games?")) # 打游戏的时间
ffMiles = float(input("frequent flier miles earned per year?")) # 飞行距离
iceCream = float(input("liters of ice cream cunsumed per year?")) # 消耗的冰淇淋数量
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt') # 从文件里加载数据
normMat, ranges, minVals = autoNorm(datingDataMat) # 归一化数据
inArr = array([ffMiles, percentTats, iceCream]) # 输入向量
classifierResult = classify0((inArr - minVals)/ranges, normMat, datingLabels, 3) # 分类,但在分类之前要处理一下输入矩阵
print("You will probably like this person: ", resultList[int(classifierResult) - 1]) # 得到结果
这就是预测约会概率的例子,下面是测试手写的字的例子,
手写的字肯定有一些偏差,怎么通过k近邻来测试这是不是某个数字呢,将字迹转换为0和1组成的矩阵来表示字迹,每个文件都是由32*32的矩阵组成,其具体格式如下,
我们先准备数据,将数据从文件中解析出来,解析成1行1024列大小的数据,这样每个文件占一行,总共就有文件个数行,
def img2vector(filename):
returnVect = zeros((1, 1024)) # 初始化1行,1024列的图像向量
fr = open(filename) # 打开文件
for i in range(32): # 文件里有32行
lineStr = fr.readline() # 读取每行
for j in range(32):
returnVect[0, 32 * i + j] = int(lineStr[j]) # 将每行读取到的数据一个一个给返回向量
return returnVect
这只是一个文件的解析,
下面将trainingDigits文件夹下所有的文件都解析出来,并对testDigits文件夹里的数字进行测试,得到错误率,
def handwritingClassTest():
hwLabels = [] # 标记向量
trainingFileList = listdir('trainingDigits') # 训练数字
m = len(trainingFileList) # 训练文件的长度
trainingMat = zeros((m, 1024)) # 初始化m * 1024的矩阵
for i in range(m):
fileNameStr = trainingFileList[i] # 0_0.txt
fileStr = fileNameStr.split('.')[0] # 0_0
classNumStr = int(fileStr.split('_')[0]) # 0
hwLabels.append(classNumStr) # 将截取的数字压入标记向量
trainingMat[i,:] = img2vector('trainingDigits/%s' % fileNameStr) # 读取文件里的数据并压入到训练向量里
testFileList = listdir('testDigits') # 测试文件夹
errorCount = 0.0 # 错误数
mTest = len(testFileList) # 测试文件数
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0]) # 得到文件名中的数字
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr) # 得到测试文件中的向量
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) # 分类结果
print("The classifier came back with: %d, the real answer is: %d" % (int(classifierResult), classNumStr)) # 打印预测结果和原有结果
if(int(classifierResult) != classNumStr):
errorCount += 1.0
print("The total error count is: %d" % errorCount) # 总的错误数
print("The total error rate is: %f" % (errorCount/float(mTest))) # 总的错误率
最重要在于分类,然后就是求距离找距离最近的k个距离,然后进行投票,最多的就预测为那个值了。