考虑图6-6给出的数据,这有点像图6-1的方框C中的数据。前面我们用这类数据来描述非线性可分的情况。显而易见,在该数据中存在某种可以识别的模式。其中一个问题就是,我们能否像线性情况一样,利用强大的工具来捕捉数据中的这种模式?显然,答案是肯定的。接下来,我们就要使用一种称为核函数(kernel)的工具将数据转换成易于分类器理解的形式。本节首先解释核函数的概念,并介绍它们在支持向量机中的使用方法。然后,介绍一种称为径向基函数(radial basis function)的最流行的核函数。最后,将该核函数应用于我们前面得到的分类器。
图6-6 这个数据在二维平面中很难用一条直线分隔,不过很明显,这里存在分隔方形点和圆形点的模式
6.5.1 利用核函数将数据映射到高维空间
在图6-6中,数据点处于一个圆中,人类的大脑能够意识到这一点。然而,对于分类器而言,它只能识别分类器的结果是大于0还是小于0。如果只在X和Y轴构成的坐标系中插入直线进行分类的话,我们并不会得到理想的结果。我们或许可以对圆中的数据进行某种形式的转换,从而得到某些新的变量来表示数据。在这种表示情况下,我们就更容易得到大于0或者小于0的测试结果。在这个例子中,我们将数据从一个特征空间转换到另一个特征空间。在新空间下,我们可以很容易利用已有的工具对数据进行处理。数学家们喜欢将这个过程称之为从一个特征空间到另一个特征空间的映射。在通常情况下,这种映射会将低维特征空间映射到高维空间。
这种从某个特征空间到另一个特征空间的映射是通过核函数来实现的。读者可以把核函数想象成一个包装器(wrapper)或者是接口(interface),它能把数据从某个很难处理的形式转换成为另一个较容易处理的形式。如果上述特征空间映射的说法听起来很让人迷糊的话,那么可以将它想象成为另外一种距离计算的方法。前面我们提到过距离计算的方法。距离计算的方法有很多种,不久我们也将看到,核函数一样具有多种类型。经过空间转换之后,我们可以在高维空间中解决线性问题,这也就等价于在低维空间中解决非线性问题。
SVM优化中一个特别好的地方就是,所有的运算都可以写成内积(inner product,也称点积)的形式。向量的内积指的是两个向量相乘,之后得到单个标量或者数值。我们可以把内积运算替换成核函数,而不必做简化处理。将内积替换成核函数的方式被称为核技巧(kernel trick)或者核“变电”(kernel substation)。
核函数并不仅仅应用于支持向量机,很多其他的机器学习算法也都用到核函数。接下来,我们将要来介绍一个流行的核函数,那就是径向基核函数。
6.5.2 径向基核函数
径向基函数是SVM中常用的一个核函数。径向基函数是一个采用向量作为自变量的函数,能够基于向量距离运算输出一个标量。这个距离可以是从<0,0>向量或者其他向量开始计算的距离。接下来,我们将会使用到径向基函数的高斯版本,其具体公式为:
其中,σ
是用户定义的用于确定到达率(reach) 或者说函数值跌落到0的速度参数。
上述高斯核函数将数据从其特征空间映射到更高维的空间,具体来说这里是映射到一个无穷维的空间。关于无穷维空间,读者目前不需要太担心。高斯核函数只是一个常用的核函数,使用者并不需要确切地理解数据到底是如何表现的,而且使用高斯核函数还会得到一个理想的结果。在上面的例子中,数据点基本上都在一个圆内。对于这个例子,我们可以直接检查原始数据,并意识到只要度量数据点到圆心的距离即可。然而,如果碰到了一个不是这种形式的新数据集,那么我们就会陷入困境。在该数据集上,使用高斯核函数可以得到很好的结果。当然,该函数也可以用于许多其他的数据集,并且也能得到低错误率的结果。
如果在svmMLiA.py
文件中添加一个函数并稍做修改,那么我们就能够在已有代码中使用核函数。首先,打开svMLiA.py
代码文件并输入函数kernelTrans
。然后,对optStruct
类进行修改,得到类似如下程序清单6-6的代码。
程序清单6-6 核转换函数
def kernelTrans(X, A, kTup): m,n = shape(X) K = mat(zeros((m,1))) if kTup[0]==/'lin/': K = X * A.T elif kTup[0]==/'rbf/': for j in range(m): deltaRow = X[j,:] - A K[j] = deltaRow*deltaRow.T # ❶ 元素间的除法 K = exp(K /(-1*kTup[1]**2)) else: raise NameError(/'Houston We Have a Problem -- That Kernel is not recognized/') return Kclass optStruct: def __init__(self,dataMatIn, classLabels, C, toler, kTup): self.X = dataMatIn self.labelMat = classLabels self.C = C self.tol = toler self.m = shape(dataMatIn)[0] self.alphas = mat(zeros((self.m,1))) self.b = 0 self.eCache = mat(zeros((self.m,2))) self.K = mat(zeros((self.m,self.m))) for i in range(self.m): self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)
我建议读者最好看一下optStruct
类的新版本。除了引入了一个新变量kTup
之外,该版本和原来的optStruct
一模一样。kTup
是一个包含核函数信息的元组,待会儿我们就能看到它的作用了。在初始化方法结束时,矩阵K
先被构建,然后再通过调用函数kernelTrans
进行填充。全局的K
值只需计算一次。然后,当想要使用核函数时,就可以对它进行调用。这也省去了很多冗余的计算开销。
当计算矩阵K
时,该过程多次调用了函数kernelTrans
。该函数有3个输入参数:2个数值型变量和1个元组。元组kTup
给出的是核函数的信息。元组的第一个参数是描述所用核函数类型的一个字符串,其他2个参数则都是核函数可能需要的可选参数。该函数首先构建出了一个列向量,然后检查元组以确定核函数的类型。这里只给出了2种选择,但是依然可以很容易地通过添加elif
语句来扩展到更多选项。
在线性核函数的情况下,内积计算在“所有数据集”和“数据集中的一行”这两个输入之间展开。在径向基核函数的情况下,在for
循环中对于矩阵的每个元素计算高斯函数的值。而在for
循环结束之后,我们将计算过程应用到整个向量上去。值得一提的是,在NumPy矩阵中,除法符号意味着对矩阵元素展开计算而不像在MATLAB中一样计算矩阵的逆❶。
最后,如果遇到一个无法识别的元组,程序就会抛出异常,因为在这种情况下不希望程序再继续运行,这一点相当重要。
为了使用核函数,先期的两个函数innerL
和calcEk
的代码需要做些修改。修改的结果参见程序清单6-7。本来我并不想这样列出代码,但是重新列出函数的所有代码需要超过90行,我想任何人都不希望这样。读者可以直接从下载的源码中复制代码段,而不必对修改片段进行手工输入。下面列出的就是修改的代码片段。
程序清单6-7 使用核函数时需要对innerL
及calcEk
函数进行的修改
innerL: . . .eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j] . . .b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,i] - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[i,j]b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,j]- oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[j,j] . . .def calcEk(oS, k): fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b) Ek = fXk - float(oS.labelMat[k]) return Ek
你已经了解了如何在训练过程中使用核函数,接下来我们就去了解如何在测试过程中使用核函数。
6.5.3 在测试中使用核函数
接下来我们将构建一个对图6-6中的数据点进行有效分类的分类器,该分类器使用了径向基核函数。前面提到的径向基函数有一个用户定义的输入σ
。首先,我们需要确定它的大小,然后利用该核函数构建出一个分类器。整个测试函数将如程序清单6-8所示。读者也可以打开一个文本编辑器,并且加入函数testRbf
。
程序清单6-8 利用核函数进行分类的径向基测试函数
def testRbf(k1=1.3): dataArr,labelArr = loadDataSet(/'testSetRBF.txt/') b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, (/'rbf/', k1)) 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,:],(/'rbf/', k1)) 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 = loadDataSet(/'testSetRBF2.txt/') errorCount = 0 datMat=mat(dataArr); labelMat = mat(labelArr).transpose m,n = shape(datMat) for i in range(m): kernelEval = kernelTrans(sVs,datMat[i,:],(/'rbf/', k1)) predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b if sign(predict)!=sign(labelArr[i]): errorCount += 1 print /"the test error rate is: %f/" % (float(errorCount)/m)
上述代码只有一个可选的输入参数,该输入参数是高斯径向基函数中的一个用户定义变量。整个代码主要是由以前定义的函数集合构成的。首先,程序从文件中读入数据集,然后在该数据集上运行Platt SMO算法,其中核函数的类型为/'rbf/'
。
优化过程结束后,在后面的矩阵数学运算中建立了数据的矩阵副本,并且找出那些非零的alpha值,从而得到所需要的支持向量;同时,也就得到了这些支持向量和alpha的类别标签值。这些值仅仅是需要分类的值。
整个代码中最重要的是for
循环开始的那两行,它们给出了如何利用核函数进行分类。首先利用结构初始化方法中使用过的kernelTrans
函数,得到转换后的数据。然后,再用其与前面的alpha及类别标签值求积。其中需要特别注意的另一件事是,在这几行代码中,是如何做到只需要支持向量数据就可以进行分类的。除此之外,其他数据都可以直接舍弃。
与第一个for
循环相比,第二个for
循环仅仅只有数据集不同,后者采用的是测试数据集。读者可以比较不同的设置在测试集和训练集上表现出的性能。
为测试程序清单6-8的代码,可以在Python提示符下输入命令:
>>> reload(svmMLiA)<module /'svmMLiA/' from /'svmMLiA.pyc/'>>>> svmMLiA.testRbf . .fullSet, iter: 11 i:497, pairs changed 0fullSet, iter: 11 i:498, pairs changed 0fullSet, iter: 11 i:499, pairs changed 0iteration number: 12there are 27 Support Vectorsthe training error rate is: 0.030000the test error rate is: 0.040000
你可以尝试更换不同的k1
参数以观察测试错误率、训练错误率、支持向量个数随k1
的变化情况。图6-7给出了当k1
非常小(=0.1)时的结果。
图6-7 在用户自定义参数k1 = 0.1
时的径向基函数。该参数此时减少了每个支持向量的影响程度,因此需要更多的支持向量
图6-7中共有100个数据点,其中的85个为支持向量。优化算法发现,必须使用这些支持向量才能对数据进行正确分类。这就可能给了读者径向基函数到达率太小的直觉。我们可以通过增加σ
来观察错误率的变化情况。增加σ
之后得到的另一个结果如图6-8所示。
图6-8 在用户自定义参数k1=1.3
时的径向基函数。这里的支持向量个数少于图6-7的,而这些支持向量在决策边界周围聚集
同图6-7相比,图6-8中只有27个支持向量,其数目少了很多。这时观察一下函数testRbf
的输出结果就会发现,此时的测试错误率也在下降。该数据集在这个设置的某处存在着最优值。如果降低σ
,那么训练错误率就会降低,但是测试错误率却会上升。
支持向量的数目存在一个最优值。SVM的优点在于它能对数据进行高效分类。如果支持向量太少,就可能会得到一个很差的决策边界(下个例子会说明这一点);如果支持向量太多,也就相当于每次都利用整个数据集进行分类,这种分类方法称为k近邻。
我们可以对SMO算法中的其他设置进行随意地修改或者建立新的核函数。接下来,我们将在一个更大的数据上应用支持向量机,并与以前介绍的一个分类器进行对比。