Python编码

这次我们不将数据直接写在Python代码中,而是放到两个文本文件里:athletesTrainingSet.txt和athletesTestSet.txt。

我会使用第一个文件中的数据来训练分类器,然后使用测试文件里的数据来进行评价。

文件格式大致如下:

Python编码 - 图1

文件中的每一行是一条完整的记录,字段使用制表符分隔。

我要使用运动员的身高体重数据来预测她所从事的运动项目,也就是用第三、四列的数据来预测第二列的数据。

运动员的姓名不会使用到,我们既不能通过运动员的姓名得知她参与的项目,也不会通过身高体重来预测运动员的姓名。

Python编码 - 图2

你好,你有五英尺高,150磅重,莫非你的名字是Clara Coleman?

当然,名字也有它的用处,我们可以用它来解释分类器的预测结果:“我们认为Amelia Pond是一名体操运动员,因为她的身高体重和另一名体操运动员Gabby Douglas很接近。”

为了让我们的Python代码更具一般性,并不只适用于这一种数据集,我会为每一列数据增加一个列名,如:

Python编码 - 图3

所有被标记为comment的列都会被分类器忽略;标记为class的列表示物品所属分类;不定个数的num列则表示物品的特征。

头脑风暴

我们在Python中应该如何表示这些数据呢?以下是一些可能性:

  1. # 1
  2. {'Asuka Termoto': ('Gymnastics', [54, 66]),
  3. 'Brittainey Raven': ('Basketball', [72, 162]), ...}

这种方式使用了运动员的姓名作为键,而我们说过分类器程序根本不会使用到姓名,所以不合理。

  1. # 2
  2. [['Asuka Termoto', 'Gymnastics', 54, 66],
  3. ['Brittainey Raven', 'Basketball', 72, 162], ...]

这种方式看起来不错,它直接反映了文件的格式。由于我们需要遍历文件的数据,所以使用列表类型(list)是合理的。

  1. # 3
  2. [('Gymnastics', [54, 66], ['Asuka Termoto']),
  3. ('Basketball', [72, 162], ['Brittainey Raven']), ...]

这是我最认同的表示方式,因为它将不同类型的数据区别开来了,依次是分类、特征、备注。这里备注可能有多个,所以也用了一个列表来表示。

以下是读取数据文件并转换成上述格式的函数:

  1. class Classifier:
  2. def __init__(self, filename):
  3. self.medianAndDeviation = []
  4. # 读取文件
  5. f = open(filename)
  6. lines = f.readlines()
  7. f.close()
  8. self.format = lines[0].strip().split('\t')
  9. self.data = []
  10. for line in lines[1:]:
  11. fields = line.strip().split('\t')
  12. ignore = []
  13. vector = []
  14. for i in range(len(fields)):
  15. if self.format[i] == 'num':
  16. vector.append(int(fields[i]))
  17. elif self.format[i] == 'comment':
  18. ignore.append(fields[i])
  19. elif self.format[i] == 'class':
  20. classification = fields[i]
  21. self.data.append((classification, vector, ignore))

动手实践

在计算修正的标准分之前,我们需要编写获取中位数和计算绝对偏差的函数,尝试实现这两个函数:

  1. >>> heights = [54, 72, 78, 49, 65, 63, 75, 67, 54]
  2. >>> median = classifier.getMedian(heights)
  3. >>> median
  4. 65
  5. >>> asd = classifier.getAbsoluteStandardDeviation(heights, median)
  6. >>> asd
  7. 8.0

关于断言

通常我们会将一个大的算法拆分成几个小的组件,并为每个组件编写一些单元测试,从而确保它能正常工作。

很多时候,我们会先写单元测试,再写正式的代码。在我提供的模板代码中已经编写了一些单元测试

摘录如下:

  1. def unitTest():
  2. list1 = [54, 72, 78, 49, 65, 63, 75, 67, 54]
  3. classifier = Classifier('athletesTrainingSet.txt')
  4. m1 = classifier.getMedian(list1)
  5. assert(round(m1, 3) == 65)
  6. ...
  7. print("getMedian和getAbsoluteStandardDeviation均能正常工作")

你需要完成的geMedian函数的模板是:

  1. def getMedian(self, alist):
  2. """返回中位数"""
  3. """请在此处编写代码"""
  4. return 0

这个模板函数返回的是0,你需要编写代码来返回列表的中位数。

比如单元测试中我传入了以下列表:

  1. [54, 72, 78, 49, 65, 63, 75, 67, 54]

assert(断言)表示函数的返回值应该是65。如果所有的单元测试都能通过,则报告以下信息:

  1. getMediangetAbsoluteStandardDeviation均能正常工作

否则,则抛出以下异常:

  1. File "testMedianAndASD.py", line 78, in unitTest
  2. assert(round(m1, 3) == 65)
  3. AssertError

断言在单元测试中是很常用的。

将大型代码拆分成一个个小的部分,并为每个部分编写单元测试,这一点是很重要的。如果没有单元测试,你将无法知道自己是否正确完成了所有任务,以及未来的某个修改是否会导致你的程序不可用。—- Peter Norvig

Python编码 - 图4

答案

  1. def getMedian(self, alist):
  2. """返回中位数"""
  3. if alist == []:
  4. return []
  5. blist = sorted(alist)
  6. length = len(alist)
  7. if length % 2 == 1:
  8. # 列表有奇数个元素,返回中间的元素
  9. return blist[int(((length + 1) / 2) - 1)]
  10. else:
  11. # 列表有偶数个元素,返回中间两个元素的均值
  12. v1 = blist[int(length / 2)]
  13. v2 = blist[(int(length / 2) - 1)]
  14. return (v1 + v2) / 2.0
  15. def getAbsoluteStandardDeviation(self, alist, median):
  16. """计算绝对偏差"""
  17. sum = 0
  18. for item in alist:
  19. sum += abs(item - median)
  20. return sum / len(alist)

可以看到,getMedian函数对列表进行了排序,由于数据量并不大,所以这种方式是可以接受的。

如果要对代码进行优化,我们可以使用选择算法

现在,我们已经将数据从athletesTrainingSet.txt读取出来,并保存为以下形式:

  1. [('Gymnastics', [54, 66], ['Asuka Teramoto']),
  2. ('Basketball', [72, 162], ['Brittainey Raven']),
  3. ('Basketball', [78, 204], ['Chen Nan']),
  4. ('Gymnastics', [49, 90], ['Gabby Douglas']), ...]

我们需要对向量中的数据进行标准化,变成以下结果:

  1. [('Gymnastics', [-1.93277, -1.21842], ['Asuka Teramoto']),
  2. ('Basketball', [1.09243, 1.63447], ['Brittainey Raven']),
  3. ('Basketball', [2.10084, 2.88261], ['Chen Nan']),
  4. ('Gymnastics', [-2.7731, -0.50520]),
  5. ('Track', [-0.08403, -0.23774], ['Helalia Johannes']),
  6. ('Track', [-0.42017, -0.02972], ['Irina Miketenko']), ...]

在init方法中,添加标准化过程:

  1. # 获取向量的长度
  2. self.vlen = len(self.data[0][1])
  3. # 标准化
  4. for i in range(self.vlen):
  5. self.normalizeColumn(i)

在for循环中逐列进行标准化,即第一次会标准化身高,第二次标准化体重。

动手实践 下载normalizeColumnTemplate.py文件,编写normalizeColumn方法。

答案

  1. def normalizeColumn(self, columnNumber):
  2. """标准化self.data中的第columnNumber列"""
  3. # 将该列的所有值提取到一个列表中
  4. col = [v[1][columnNumber] for v in self.data]
  5. median = self.getMedian(col)
  6. asd = self.getAbsoluteStandardDeviation(col, median)
  7. #print("Median: %f ASD = %f" % (median, asd))
  8. self.medianAndDeviation.append((median, asd))
  9. for v in self.data:
  10. v[1][columnNumber] = (v[1][columnNumber] - median) / asd

可以看到,我将计算得到的中位数和绝对偏差保存在了medianAndDeviation变量中,因为我们会用它来标准化需要预测的向量。

比如,我要预测Kelly Miller的运动项目,她身高5尺10寸(70英寸),重140磅,即原始向量为[70, 140],需要先进行标准化。

我们计算得到的meanAndDeviation为:

  1. [(65.5, 5.95), (107.0, 33.65)]

它表示向量中第一元素的中位数为65.5,绝对偏差为5.95;第二个元素的中位数为107.0,绝对偏差33.65。

现在我们就利用这组数据将[70, 140]进行标准化。第一个元素的标准分数是:

Python编码 - 图5

第二个元素为:

Python编码 - 图6

以下是实现它的Python代码:

  1. def normalizeVector(self, v):
  2. """我们已保存了每列的中位数和绝对偏差,现用它来标准化向量v"""
  3. vector = list(v)
  4. for i in range(len(vector)):
  5. (median, asd) = self.medianAndDeviation[i]
  6. vector[i] = (vector[i] - median) / asd
  7. return vector

最后,我们要编写分类函数,用来预测运动员的项目:

  1. classifier.classify([70, 140])

在我们的实现中,classify函数只是nearestNeighbor的一层包装:

  1. def classify(self, itemVector):
  2. """预测itemVector的分类"""
  3. return self.nearestNeighbor(self.normalizeVector(itemVector))[1][0]

动手实践 实现nearestNeighbor函数。

答案

  1. def manhattan(self, vector1, vector2):
  2. """计算曼哈顿距离"""
  3. return sum(map(lambda v1, v2: abs(v1 - v2), vector1, vector2))
  4. def nearestNeighbor(self, itemVector):
  5. """返回itemVector的近邻"""
  6. return min([(self.manhattan(itemVector, item[1]), item)
  7. for item in self.data])

好了,我们用200多行代码实现了近邻分类器!

Python编码 - 图7

在完整的示例代码中,我提供了一个test函数,它可以对分类器程序的准确性做一个评价。

比如用它来评价上面实现的分类器:

  1. - Track Aly Raisman Gymnastics 62 115
  2. + Basketball Crystal Langhorne Basketball 74 190
  3. + Basketball Diana Taurasi Basketball 72 163
  4. ...
  5. + Track Xueqin Wang Track 64 110
  6. + Track Zhu Xiaolin Track 67 123
  7. 80.00% correct

可以看到,这个分类器的准确率是80%。它对篮球运动员的预测很准确,但在预测田径和体操运动员时出现了4个失误。

鸢尾花数据集

我们可以用鸢尾花数据集做测试,这个数据集在数据挖掘领域是比较有名的。

它是20世纪30年代Ronald Fisher对三种鸢尾花的50个样本做的测量数据(萼片和花瓣)。

Python编码 - 图8

Ronald Fisher是一名伟大的科学家。他对统计学做出了革命性的改进,Richard Dawkins称他为“继达尔文后最伟大生物学家。”

Python编码 - 图9

鸢尾花数据集可以在这里irisTrainingSetirisTestSet找到,你可以测试你的算法,并问自己一些问题:标准化让结果更正确了吗?训练集中的数据量越多越好吗?用欧几里得距离来算会怎样?

记住 所有的学习过程都是在你自己的脑中进行的,你付出的努力越多,学到的也就越多。

鸢尾花数据集的格式如下,我们要预测的是Species这一列:

Python编码 - 图10

训练集中有120条数据,测试集中有30条,两者没有交集。

测试结果如何呢?

  1. >>> test('irisTrainingSet.data', 'iristestSet.data')
  2. 93.33% correct

这又一次证明我们的分类算法是简单有效的。

有趣的是,如果不对数据进行标准化,它的准确率将达到100%。这个现象我们会在后续的章节中讨论。