首页 » 机器学习实战 » 机器学习实战全文在线阅读

《机器学习实战》6.6 示例:手写识别问题回顾

关灯直达底部

考虑这样一个假想的场景。你的老板过来对你说:“你写的那个手写体识别程序非常好,但是它占用的内存太大了。顾客不能通过无线的方式下载我们的应用(在写本书时,无线下载的限制容量为10MB,可以肯定,这将来会成为笑料的。)我们必须在保持其性能不变的同时,使用更少的内存。我呢,告诉了CEO,你会在一周内准备好,但你到底还得多长时间才能搞定这件事?”我不确定你到底会如何回答,但是如果想要满足他们的需求,你可以考虑使用支持向量机。尽管第2章所使用的kNN方法效果不错,但是需要保留所有的训练样本。而对于支持向量机而言,其需要保留的样本少了很多(即只保留支持向量),但是能获得可比的效果。

示例:基于SVM的数字识别

  1. 收集数据:提供的文本文件。
  2. 准备数据:基于二值图像构造向量。
  3. 分析数据:对图像向量进行目测。
  4. 训练算法:采用两种不同的核函数,并对径向基核函数采用不同的设置来运行SMO算法 。
  5. 测试算法:编写一个函数来测试不同的核函数并计算错误率。
  6. 使用算法:一个图像识别的完整应用还需要一些图像处理的知识,这里并不打算深入介绍。

使用第2章中的一些代码和SMO算法,可以构建一个系统去测试手写数字上的分类器。打开svmMLiA.py并将第2章中knn.py中的img2vector函数复制过来。然后,加入程序清单6-9中的代码。

程序清单6-9 基于SVM的手写数字识别

def loadImages(dirName):    from os import listdir    hwLabels =     trainingFileList = listdir(dirName)    m = len(trainingFileList)    trainingMat = zeros((m,1024))    for i in range(m):        fileNameStr = trainingFileList[i]        fileStr = fileNameStr.split(/'./')[0]        classNumStr = int(fileStr.split(/'_/')[0])        if classNumStr == 9: hwLabels.append(-1)        else: hwLabels.append(1)        trainingMat[i,:] = img2vector(/'%s/%s/' % (dirName, fileNameStr))    return trainingMat, hwLabelsdef testDigits(kTup=(/'rbf/', 10)):    dataArr,labelArr = loadImages(/'trainingDigits/')    b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup)    datMat=mat(dataArr); labelMat = mat(labelArr).transpose    svInd=nonzero(alphas.A>0)[0]    sVs=datMat[svInd]    labelSV = labelMat[svInd];    print /"there are %d Support Vectors/" % shape(sVs)[0]    m,n = shape(datMat)    errorCount = 0    for i in range(m):        kernelEval = kernelTrans(sVs,datMat[i,:],kTup)        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b        if sign(predict)!=sign(labelArr[i]): errorCount += 1    print /"the training error rate is: %f/" % (float(errorCount)/m)    dataArr,labelArr = loadImages(/'testDigits/')    errorCount = 0    datMat=mat(dataArr); labelMat = mat(labelArr).transpose    m,n = shape(datMat)    for i in range(m):        kernelEval = kernelTrans(sVs,datMat[i,:],kTup)        predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b        if sign(predict)!=sign(labelArr[i]): errorCount += 1print /"the test error rate is: %f/" % (float(errorCount)/m)  

函数loadImages是作为前面kNN.py中的handwritingClassTest的一部分出现的。它已经被重构为自身的一个函数。其中仅有的一个大区别在于,在kNN.py中代码直接应用类别标签,而同支持向量机一起使用时,类别标签为-1或者+1。因此,一旦碰到数字9,则输出类别标签-1,否则输出+1。本质上,支持向量机是一个二类分类器,其分类结果不是+1就是-1。基于SVM构建多类分类器已有很多研究和对比了,如果读者感兴趣,建议阅读C. W. Huset等人发表的一篇论文“A Comparison of Methods for Multiclass Support Vector Machines”1。由于这里我们只做二类分类,因此除了1和9之外的数字都被去掉了。

1. C. W. Hus, and C. J. Lin, “A Comparison of Methods for Multiclass Support Vector Machines,” IEEE Transactions on Neural Networks 13, no. 2 (March 2002), 415–25.

下一个函数testDigits并不是全新的函数,它和testRbf的代码几乎一样,唯一的大区别就是它调用了loadImages函数来获得类别标签和数据。另一个细小的不同是现在这里的函数元组kTup是输入参数,而在testRbf中默认的就是使用rbf核函数。如果对于函数testDigits不增加任何输入参数的话,那么kTup的默认值就是(/'rbf/' ,10)。

输入程序清单6-9中的代码之后,将之保存为svmMLiA.py并输入如下命令:

>>> svmMLiA.testDigits((/'rbf/', 20))                    .                    .L==HfullSet, iter: 3 i:401, pairs changed 0iteration number: 4there are 43 Support Vectorsthe training error rate is: 0.017413the test error rate is: 0.032258 

我尝试了不同的σ值,并尝试了线性核函数,总结得到的结果如表6-1所示。

表6-1 不同σ值的手写数字识别性能

内核,设置训练错误率(%)测试错误率(%)支持向量数RBF, 0.1052402RBF, 503.2402RBF, 1000.599RBF, 500.22.241RBF, 100 4.5 4.3 26Linear2.72.238

表6-1给出的结果表明,当径向基核函数中的参数σ取10左右时,就可以得到最小的测试错误率。该参数值比前面例子中的取值大得多,而前面的测试错误率在1.3左右。为什么差距如此之大?原因就在于数据的不同。在手写识别的数据中,有1024个特征,而这些特征的值有可能高达1.0。而在6.5节的例子中,所有数据从-1到1变化,但是只有2个特征。如何才能知道该怎么设置呢?说老实话,在写这个例子时我也不知道。我只是对不同的设置进行了多次尝试。C的设置也会影响到分类的结果。当然,存在另外的SVM形式,它们把C同时考虑到了优化过程中,例如v-SVM。有关v-SVM的一个较好的讨论可以参考本书第3章介绍过的Sergios Theodoridis和Konstantinos Koutroumbas撰写的Pattern Recognition2。

2 .Sergios Theodoridis and Konstantinos Koutroumbas, Pattern Recognition, 4th ed. (Academic Press, 2009), 133.

你可能注意到了一个有趣的现象,即最小的训练错误率并不对应于最小的支持向量数目。另一个值得注意的就是,线性核函数的效果并不是特别的糟糕。可以以牺牲线性核函数的错误率来换取分类速度的提高。尽管这一点在实际中是可以接受的,但是还得取决于具体的应用。