《机器学习实战》——支持向量机SVM

一、前言  

      这应该是我第一次写博客,写的不好,还请各位大神多多指教……

       学习机器学习已经有一段时间了,已经稍稍入了点门了,但是总感觉对数据的处理理解不到位,苏安然跟着书把代码实现了,但怎样实现的还是不清楚,希望继续学习下去,可以把这一点搞清楚吧。

二、基本概念

       下面进入正题,学习支持向量机,首先得理解支持向量机的基本概念。在线性可分(对于一个数据集合能否画一条直线将两组数据点分开)的数据中,将数据集分隔开来的直线成为分隔超平面。对于二维平面来说,分隔超平面就是一条直线,但对于三维及以上数据来说,该对象就是超平面,也就是分类的决策边界。如下图所示:

                                                     

       我们希望采用一种方式来构建分类器,即如果数据点离决策边界越远,那么其最后的预测结果也就越可信,我们希望找到离分隔超平面最近的点,确保它们离分隔面的距离尽可能远,这里点到分隔面的距离被称为间隔,支持向量(support vector)就是离分隔超平面最近的那些点。最大化支持向量到分隔面的距离,就需要找到优化求解方法来寻找最大间隔。

       支持向量是一种二类分类器。之所以称为“机”,是因为它会产生一个二值决策结果,即它是一种决策“机”。

三、SMO高效优化算法

       SMO表示序列最小优化,就是将大优化问题分解成多个小优化问题来求解,这些小优化问题往往很容易求解,并且对它们进行顺序求解的结果与将它们作为整体来求解的结果完全一致,但SMO算法的求解时间短很多。

       SMO算法的目标是求出一系列的alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w并得到分隔超平面。

       SMO算法的工作原理:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的alpha,那么就增大其中一个同事减小另一个。所谓“合适”是指两个alpha必须要符合一定的条件,条件之一是这两个alpha必须要在间隔边界之外,第二个条件则是这两个alpha还没有进行过区间化处理或者不在边界上。

1、简化版SMO算法处理小规模数据集

       

#loadDataSet()函数:打开文件并对其进行逐行解析
#从而得到每行的类标签和整个数据矩阵
def loadDataSet(fileName):
	dataMat = []; labelMat = []
	fr = open(fileName)
	for line in fr.readlines():
		lineArr = line.strip().split('\t')
		dataMat.append([float(lineArr[0]), float(lineArr[1])])
		labelMat.append(float(lineArr[2]))
	return dataMat,labelMat
#构建辅助函数1:在某个区间范围内随机选择一个整数,只要函数值不等于输入值i,函数就会进行随机选择。
#i是第一个alpha的下标,m是alpha的数目
def selectJrand(i,m):
	j = i
	while (j==i):
		j = int(random.uniform(0,m))
	return j
#构建辅助函数2:对于数值不在范围内时对其进行调整,调整大于H或小于L的alpha值。
def clipAlpha(aj,H,L):
	if aj > H:
		aj = H
	if L > aj:
		aj = L
	return aj

SMO函数的伪代码如下:

#创建一个alpha向量并将其初始化为0向量
#当迭代次数小于最大迭代次数时(外循环)
    #对数据集中的每个数据向量(内循环):
        #如果该数据向量可以被优化:
            #随机选择另外一个数据向量
            #同时优化这两个向量
            #如果两个向量都不能被优化,退出内循环
    #如果所有向量都没有被优化,增加迭代数目,继续下一次循环

简化版SMO算法:

#dataMat    :数据列表
#classLabels:标签列表
#C          :权衡因子(增加松弛因子而在目标优化函数中引入了惩罚项)
#toler      :容错率
#maxIter    :最大迭代次数
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
	dataMatrix = mat(dataMatIn); labelMat = mat(classLabels).transpose()
	b = 0; m,n = shape(dataMatrix)
	alphas = mat(zeros((m,1)))
	iter = 0
	while (iter < maxIter):
		alphaPairsChanged = 0
		for i in range(m):
			fXi = float(multiply(alphas,labelMat).T*\
			(dataMatrix*dataMatrix[i,:].T)) + b
			Ei = fXi - float(labelMat[i])
			if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or \
			((labelMat[i]*Ei > toler) and (alphas[i] > 0)):
				j = selectJrand(i,m)
				fXj = float(multiply(alphas,labelMat).T*\
					(dataMatrix*dataMatrix[j,:].T)) + b
				Ej = fXj - float(labelMat[j])
				alphaIold = alphas[i].copy();
				alphaJold = alphas[j].copy();
				if (labelMat[i] != labelMat[j]):
					L = max(0, alphas[j] - alphas[i])
					H = min(C, C + alphas[j] - alphas[i])
				else:
					L = max(0, alphas[j] + alphas[i] - C)
					H = min(C, alphas[j] + alphas[i])
				if L==H: print("L==H");continue
				eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - \
					dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*\
					dataMatrix[j,:].T
				if eta >= 0: print("eta>=0"); continue
				alphas[j] -= labelMat[j]*(Ei - Ej)/eta
				alphas[j] = clipAlpha(alphas[j],H,L)
				if (abs(alphas[j] - alphaJold) < 0.00001):
					print("j not moving enough"); continue
				alphas[i] += labelMat[j]*labelMat[i] * \
				(alphaJold - alphas[j])
				b1 = b - Ei - labelMat[i] * (alphas[i] - \
					alphaIold)*dataMatrix[i,:] * dataMatrix[i,:].T - \
					labelMat[j] * (alphas[j] - alphaJold) * \
					dataMatrix[i,:] * dataMatrix[j,:].T
				b2 = b - Ej - labelMat[i] * (alphas[i] - \
					alphaIold)*dataMatrix[i,:] * dataMatrix[j,:].T - \
					labelMat[j] * (alphas[j] - alphaJold) * \
					dataMatrix[j,:] * dataMatrix[j,:].T
				if (0 < alphas[i]) and (C > alphas[i]): b = b1
				elif (0 < alphas[j]) and (C > alphas[j]): b = b2
				else:b = (b1 + b2)/2.0
				alphaPairsChanged += 1
				print("iter: %d i:%d, pairs changed %d" % 
						(iter,i,alphaPairsChanged))
		if (alphaPairsChanged == 0): iter += 1
		else:iter = 0
		print("iteration number: %d" % iter)
	return b,alphas

2、完整版Platt SMO算法加速优化

       在简化版和完整版中,实现alpha的更改和代数运算的优化环节一模一样,不同的是选择alpha的方式。完整版的Platt SMO 算法应用了一些能够提速的启发方法:是通过一个外循环来选择第一个alpha值的,其选择过程会在两种方式之间进行交替,一种方式是在所有的数据集上进行单遍扫描,另一种方式则是在非边界alpha(非边界alpha指的是那些不等于边界0或C的alpha值)中实现单遍扫描。

       在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。在优化过程中会通过最大化步长的方式来获得第二个alpha值,所以我们会建立一个全局的缓存用于保存误差值,并从中选择使得步长或者说Ei - Ej最大的alpha值。

下面的程序包含1个用于清理代码的数据结构和3个用于对E进行缓存的辅助函数

class 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)
"""calcEK()能够计算E值并返回,单独写出来方便调用"""	
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
	
def selectJ(i, oS, Ei):
	maxK = -1; maxDeltaE = 0; Ej = 0
	oS.eCache[i] = [1,Ei]
	validEcacheList = nonzero(oS.eCache[:,0].A)[0]
	if (len(validEcacheList)) > 1:
		for k in validEcacheList:
			if k == i:continue
			Ek = calcEk(oS,k)
			deltaE = abs(Ei - Ek)
			if (deltaE > maxDeltaE):
				maxK = k; maxDeltaE = deltaE; Ej = Ek
		return maxK, Ej
	else:
		j = selectJrand(i, oS.m)
		Ej = calcEk(oS, j)
	return j, Ej
	
def updateEk(oS, k):
	Ek = calcEk(oS, k)
	oS.eCache[k] = [1, Ek]

寻找决策边界的优化历程:

def innerL(i,oS):
	Ei = calcEk(oS,i)
	if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or\
	((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):
		j,Ej = selectJ(i, oS, Ei)
		alphaIold = oS.alphas[i].copy();
		alphaJold = oS.alphas[j].copy();
		if (oS.labelMat[i] != oS.labelMat[j]):
			L = max(0, oS.alphas[j] - oS.alphas[i])
			H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
		else:
			L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
			H = min(oS.C, oS.alphas[j] + oS.alphas[i])
		if L ==H:print("L==H"); return 0
		eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j]
		if eta >= 0: print("eta >= 0"); return 0
		oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta
		oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)
		updateEk(oS, j)
		if (abs(oS.alphas[j] - alphaJold) < 0.00001):
			print("j not moving enough"); return 0
		oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*\
						(alphaJold - oS.alphas[j])
		updateEk(oS,i)
		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]
		if (0 < oS.alphas[i]) and (oS.C > oS.alphas[j]): oS.b = b1
		elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]): oS.b = b2
		else: oS.b = (b1 + b2)/2.0
		return 1
	else: return 0

SMO的外循环代码:

def smoP(dataMatIn, classLabels, C, toler, maxIter, kTup = ('lin', 0)):
	oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler,kTup)
	iter = 0
	entireSet = True; alphaPairsChanged = 0
	while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
		alphapairsChanged = 0
		if entireSet:
			for i in range(oS.m):
				alphaPairsChanged += innerL(i,oS)
			print("fullSet, iter: %d I:%d, pairs changed %d" %\
			(iter,i,alphaPairsChanged))
			iter += 1
		else:
			nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
			for i in nonBoundIs:
				alphaPairsChanged += innerL(i,oS)
				print("non-bound, iter: %d i:%d, pairs changed %d" % \
				(iter,i,alphaPairsChanged))
			iter += 1
		if entireSet: entireSet = False
		elif (alphaPairsChanged == 0): entireSet = True
		print("iteration number: %d" % iter)
	return oS.b,oS.alphas

四、在复杂数据上应用核函数

1、利用核函数将数据映射到高维空间

                                             

       如果数据集是这种非线性可分的情况,我们就会用核函数(kernel)的工具将数据转换成易于分类器理解的形式。数据点处于一个圆中,人类的大脑能够意识到这一点,而对于分类器而言,它只能识别分类器的结果是大于0还是小于0.如果只在x和y轴构成的坐标系中插入直线进行分类的话,我们并不会得到理想的结果。我们或许可以对圆中的数据进行某种形式的转换,从而得到某些新的变量来表示数据。将数据从一个特征空间转换到另一个特征空间,在新空间下,我们可以很容易利用已有的工具对数据进行处理,这个过程叫做从一个特征空间到另一个特征空间的映射。通常情况下,映射会将低维特征空间映射到高维特征空间。而这种从某个特征空间到另一个特征空间的映射是通过核函数来实现的。我们可以把核函数想象成一个包装器(wrapper)或者是接口(interface),它能把数据从某个很难处理的形式转换成为另一个较容易的处理的形式。如果上述特征空间映射的说法听起来很让人迷糊的话,那么可以将它想象成为另外一种距离计算的方法。经过空间转换之后,我们可以在高维空间中解决线性问题,也就等价于在低维空间中解决非线性问题。

       SVM优化中一个特别好的地方就是,所有的运算都可以写成内积(inner product,也称点积)的形式。向量的内积使之两个向量相乘,之后得到单个标量或者数值。我们可以把内积运算替换成核函数,而不必做简化处理。将内积替换成核函数的方式被称为核技巧(kernel trick)或者核“变电”(kernel substation)。

2、径向基核函数

       径向基函数是SVM中常用的一个核函数。径向基函数是一个采用向量作为自变量的函数,能够基于向量距离运算输出一个标量。这个距离可以使从<0,0>向量或者其他向量开始计算的距离。

                                                                                

       \sigma是用户定义的用于确定到达率或者说函数值跌落到0的速度参数。高斯核函数将数据从其特征空间映射到更高维的空间,具体来说就是这里是映射到一个无穷维的空间。对前面的函数进行修改并输入函数kernelTran,代码如下:

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 K

class 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)

       使用核函数时需要对innerL()及calcEk()函数进行修改,在之前的代码中已经是修改过的版本,接下来在测试中使用核函数,利用核函数进行分类的径向基测试函数代码如下:

"""代码只有一个可选的输入参数,该输入参数时高斯径向基函数中的一个用户定义变量
核函数类型为'rbf
'"""
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))

 五、示例:手写识别问题回顾

基于SVM的数字识别,步骤如下:

(1)收集数据:提供的文本文件

(2)准备数据:基于二值图像构造向量

(3)分析数据:对图像向量进行目测

(4)训练算法:采用两种不同的核函数,并对径向基核函数曹勇不同的设置来运行SMO算法

(5)测试算法:编写一个函数来测试不同的核函数并计算错误率

(6)使用算法:一个图像识别的完整应用还需要一些图像处理的知识,这里并不打算深入介绍

基于SVM的手写数字识别

def img2vector(filename):
	returnVect = zeros((1,1024))
	fr = open(filename)
	for i in range(32):
		lineStr = fr.readline()
		for j in range(32):
			returnVect[0,32*i+j] = int(lineStr[j])
	return returnVect

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, hwLabels

def 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 eroor 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 += 1
	print("the test error rate is: %f" % (float(errorCount)/m))

 七、在算法实现的过程中遇到的一些错误:

1、name‘mat’is  not defined

       一直有name‘mat’is  not defined 错误,把程序重新核对了两遍,找到了一些其他错误,但是还是‘mat’未定义,一直找不到怎么错了,从网上查了一下突然意识到mat是numpy里面的,所以导入了numpy数据包,然后将mat改为np.mat,结果就运行出来了。

2、无意间发现了一个小技巧,按住Shift+字母,可以使字母大写,不用按Caps Lock来回的切换。

3、缩进错误:

       缩进格式错误。

4、对象不接受参数

原因是__init__中少打了一个i,打成了int

5:各种书写错误:

6,修改完以后,就出现92行multiply is not defined,怎么都找不到错误,后来把numpy里面的全部方法都导入到程序里面,from numpy import *就运行成功了。

       在测试核函数的时候一定要细心细心再细心,代码要检查检查再检查,不然一万多个数据跑了三遍真是伤不起,跑一遍至少15分钟。

7.找不到文件

        原因是%s和%s之间少了一个/,还是强调细心细心再细心

最后结果如下:

错误率在1%,结果还是比较理想的,比kNN算法的错误率要低很多!

猜你喜欢

转载自blog.csdn.net/weixin_41965380/article/details/81623956