前言:机器学习算法的第一次实验,用k-近邻算法来设计一个有关于集美大学的程序,对于刚了解机器学习以及接触python不深的我来说,只能先将教科书上的例子吃透在继续深入
课本源代码使用的是python2版本,照书本写代码后发现在自己的Visual Studio Code上报了一些错误,在上网查阅资料后发现,python3是不向下兼容python2的,虽然其中很多组件和扩展都是python2的,但部分代码还是有些不同,基于自己所用的是python3版本的,所以决定用python3来理解书上的案例
目录
一、理解k-近邻算法含义以及代码实现
二、从文本文件中解析数据
三、用Matplotlib进行图形化数据
四、将数据进行归一化处理
五、算法测试
六、实现算法
k-近邻算法的代码实现原理:
在一个样本集中存在对应特征和标签,例如海伦约会网站案例中一个样本有四个值(每年获得的飞行常客里程数、玩视频游戏所消耗时间百分比、每周消费的冰淇淋公升数、喜爱程度),前三个值为特征,最后一个值为标签,这样便形成了一个样本数据和其分类的对应关系,当一个没有标签的新数据被输进来时,我们需要提取与新数据(k个)最相近的特征的分类标签,选择其中出现次数最多的标签,作为新数据的分类标签。
那么,怎么求特征间的距离呢?
书本上使用的是欧氏距离,即 :
这个公式是中学数学中常见的计算两点间距离的公式,该公式还可拓展至三维:
代码实现:
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndicies = distances.argsort()
classCount = {}
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]
代码解读:
新数据特征集 到 距离集 的演变
训练样本:
[[x1, y1, z1],
[x2, y2, z2],
[x3, y3, z3],
.
.
[xn, yn, zn]]
sum(axis=1)**0.5
新数据样本:——————>sqDiffMat:———————————> sqDistances:
[x0, y0, z0] [[(x0-x1)**2, (y0-y1)**2, (z0-z1)**2], [d1,d2,d3,d4........dn]
[(x0-x2)**2, (y0-y2)**2, (z0-z2)**2],
[(x0-x3)**2, (y0-y3)**2, (z0-z3)**2],
.
[(x0-xn)**2, (y0-yn)**2, (z0-zn)**2]]
其中需要注意的点是array.sum()中参数axis的取值,为此我写了段测试代码
可知当axis=0时,sum()先将每一列相加,得到的数给结果矩阵第一个值,以此类推;当axis=1时,sum()将每一行的值相加,得到的数给结果矩阵第一个值。
将得到的距离集排序,循环k次,将这k次中的距离新数据点最近的k个点的类别标签出现的次数统计并存放在字典集classCount{}中
最后一行将字典集中的值按降序排序,函数返回字典集第一个值,既类别标签次数最多的类别,如(1,3)的返回值为1即k次中类别标签为1的出现次数最多,为3次,由此可得新数据样本的类别标签最贴近的应该是1
首先观察数据样本集合中的内容:
实际上样本中第一个数据和第二个数据中间是有\t为分隔符的,但是不知道为什么还是挤在一起了,但是当第一项数据较小时,我们可以看到中间有分隔。
代码实现:
def file2matrix(filename):
fr = open("D:/datingTestSet2.txt")
arrayOLines = fr.readlines()
numberOfLines = len(arrayOLines)
returnMat = zeros((numberOfLines,3))
classLabelVector = []
index = 0
for line in arrayOLines:
line = line.strip()
listFromLine = line.split('\t')
returnMat[index,:] = listFromLine[0:3]
classLabelVector.append(int(listFromLine[-1]))
index+=1
return returnMat,classLabelVector
读取文件的代码比较简单,先设一个数据全为0的形状和样本一样大小的returnMat,之后从第0行到最后一行把txt文本中每一个数据特征都赋值给returnMat即可,其中需要注意有两个返回值,returnMat中存储的是特征集,classLabelVector存储的是标签集
图形化数据是直观观察样本的一个重要步骤,虽然对测试没有影响,但是可以反映一个样本集合的好坏,一个分类明确的样本肯定比鱼龙混杂的样本要好
代码实现:
import matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,0], datingDataMat[:,1],15.0*array(datingLabels), 15.0*array(datingLabels) )
plt.show()
matplotlib是耳熟能详的好工具了,以前主要是应用画函数图像,第一次用来画散点图对其中参数做了一定学习,值得一提的是该代码段对散点颜色和大小的分类十分巧妙,应用不同样本点的不同类别标签来以此设置不同散点的颜色和大小。(虽然样本有三个特征,但是只是提取了其中两个特征来画二维图像)
我们可以发现,在求解样本间距离的过程中,飞行里程数对计算结果的影响远大于玩视频游戏耗时百分比和每周消费的冰淇淋公升数这两个特征值,我们应认为这三种特征是同等重要的,在处理这种不同取值范围的特征值时,我们通常采用的方法时数值归一化,公式如下:
代码实现:
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(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
代码十分简洁,就是将每一项样本的值按照公式进行处理,最后返回的normDataSet即是经过归一化后每一项值都在0-1之间的样本集合
现在来测试算法的正确率,通常对训练样本和测试样本的区分是有一定要求的,但是在海伦约会网站配对的这个案例中,收集的数据本身就是随机的,所以我们可以随意选择10%的数据进行测试
代码实现:
def datingClassTest():
hoRatio = 0.10
datingDataMat, datingLabels = file2matrix('D:/datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0]
numTestVecs = int(m*hoRatio)
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" % (classifierResult, datingLabels[i]))
if (classifierResult != datingLabels[i]): errorCount += 1.0
print("the total error rate is: %f" % (errorCount / float(numTestVecs)))
print(errorCount)
可以看到100次测验中出现了五次错误预估,所以该算法的效果还是合格的
最后的最后,终于要实现整个算法了,话不多说,直接贴代码
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 consumed per year?"))
datingDataMat, datingLabels = file2matrix('D:/datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = np.array([ffMiles, percentTats, iceCream, ])
classifierResult = classify0((inArr - \
minVals)/ranges, normMat, datingLabels, 3)
print("ffMile: ",ffMiles,"\npercentTats: ",percentTats, "\niceCream: " ,iceCream, "\nYou will probably like this person: %s" % resultList[classifierResult - 1])
这段代码段中的代码大部分在前面都见过,是程序段的结合,加了input()函数来提示输入信息,书上使用的python2中输入的代码时raw_input(),这个函数在python3中被废弃了,改为了input(),而且python2中的print后面直接跟" ",而python3中print需要加() ,值得一提的是,使用jupyter这个工具后的input()是不会显示在底下的,所以在print输出中我加了数据特征的参数让结果更直观化,让我们看看运行结果如何
可以看到,我所输入的新数据与数据样本中第一个数据是相近的,类别标签也是十分贴合第一个数据样本。
总结:理解和调试了一天的代码,总算把k-近邻算法的大致弄懂,这是我首次对矩阵数据类型进行如此大规模的操作,回想起之前听过的,万物皆可矩阵化,把数据做成矩阵进行运算让我大开了眼界。下一篇我将通过这次书本上的案例自己写k-近邻算法来设计一个有关集美大学的程序