Python编码

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

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

文件格式大致如下:

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

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

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

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

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

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

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

头脑风暴

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

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

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

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

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

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

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

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

class Classifier:

    def __init__(self, filename):

        self.medianAndDeviation = []

        # 读取文件
        f = open(filename)
        lines = f.readlines()
        f.close()
        self.format = lines[0].strip().split('\t')
        self.data = []
        for line in lines[1:]:
            fields = line.strip().split('\t')
            ignore = []
            vector = []
            for i in range(len(fields)):
                if self.format[i] == 'num':
                    vector.append(int(fields[i]))
                elif self.format[i] == 'comment':
                    ignore.append(fields[i])
                elif self.format[i] == 'class':
                    classification = fields[i]
            self.data.append((classification, vector, ignore))

动手实践

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

>>> heights = [54, 72, 78, 49, 65, 63, 75, 67, 54]
>>> median = classifier.getMedian(heights)
>>> median
65
>>> asd = classifier.getAbsoluteStandardDeviation(heights, median)
>>> asd
8.0

关于断言

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

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

摘录如下:

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

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

def getMedian(self, alist):
    """返回中位数"""

    """请在此处编写代码"""
    return 0

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

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

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

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

getMedian和getAbsoluteStandardDeviation均能正常工作

否则,则抛出以下异常:

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

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

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

答案

def getMedian(self, alist):
    """返回中位数"""
    if alist == []:
        return []
    blist = sorted(alist)
    length = len(alist)
    if length % 2 == 1:
        # 列表有奇数个元素,返回中间的元素
        return blist[int(((length + 1) / 2) -  1)]
    else:
        # 列表有偶数个元素,返回中间两个元素的均值
        v1 = blist[int(length / 2)]
        v2 = blist[(int(length / 2) - 1)]
        return (v1 + v2) / 2.0

def getAbsoluteStandardDeviation(self, alist, median):
    """计算绝对偏差"""
    sum = 0
    for item in alist:
        sum += abs(item - median)
    return sum / len(alist)

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

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

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

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

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

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

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

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

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

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

答案

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

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

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

我们计算得到的meanAndDeviation为:

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

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

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

第二个元素为:

以下是实现它的Python代码:

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

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

classifier.classify([70, 140])

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

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

动手实践 实现nearestNeighbor函数。

答案

def manhattan(self, vector1, vector2):
    """计算曼哈顿距离"""
    return sum(map(lambda v1, v2: abs(v1 - v2), vector1, vector2))

def nearestNeighbor(self, itemVector):
    """返回itemVector的近邻"""
    return min([(self.manhattan(itemVector, item[1]), item)
                for item in self.data])

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

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

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

-         Track  Aly Raisman  Gymnastics  62  115
+    Basketball  Crystal Langhorne  Basketball  74  190
+    Basketball  Diana Taurasi  Basketball  72  163
...
+         Track  Xueqin Wang  Track 64  110
+         Track  Zhu Xiaolin  Track 67  123

80.00% correct

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

鸢尾花数据集

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

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

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

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

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

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

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

测试结果如何呢?

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

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

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

results matching ""

    No results matching ""