说在前面:前几天,公众号不是给你们推送了第二篇关于决策树的文章嘛。阅读过的读者应该会发现,在最后排版已经有点乱套了。真的很抱歉,也不知道咋回事,到了后期Markdown格式文件的内容就解析出现问题了,彷佛涉及到Latex就会多多少少排版错乱???暂时也没什么比较好的解决办法,若是有朋友知道的能够联系下Taoye,长时间用Markdown + Latex码文已成习惯了,关于机器学习文章的内容,更好的阅读体验,你们能够跳转至我在Cmd Markdown平台发布的内容,也可前往个人掘金主页,阅读体验都是不错的,PC端食用更佳:html
关于支持向量机(SVM),前面咱们已经详细讲解了线性SVM的推导过程以及代码实现。以后咱们发现,虽然效果还算不错,数据集基本都可以分类正确,模型训练效率的话也还说的过去,但这是基于咱们训练样本数据集比较少、迭代次数比较少的前提下。git
假如说咱们数据集比较大,并且还须要迭代很多次数的话,使用第一篇SVM文章中所提到的SMO算法的效率可就不敢恭维了,训练的速度可堪比龟龟。因此以后也就有了SVM第二篇的内容——优化SMO算法的优化,进一步提升了SVM模型的训练效率。算法
关于前两篇SVM文章的详细内容,可暂且跳转至:数据库
有了线性SVM所提到的知识作基础,非线性SVM的内容应该不会很难,理解起来也不会有那么大的压力。你们在食用前也不要畏惧它,Taoye必定给你安排的明明白白。原理部分的内容必定要理解清楚、理解透彻,尤为是核技巧的奇妙之处,而代码部分的内容只须要能看懂就行,知道代码所表达的意思便可。因此,必定要增强内功的修炼,才能在以后实战过程当中如鱼得水数组
前面也有讲到,对于一些线性可分的分类问题,线性SVM的确是一种很是有效的方法。缓存
但,现实每每事与愿违。微信
在实际问题中,咱们的原始数据集每每是线性不可分的,也就是说咱们没法找到一条直线 | 平面 | 超平面将目标数据集分隔开来,这个时候就要引出咱们的非线性SVM了,而其中主要特色就是巧妙的利用了核技巧,既能解决低维空间下分类困难的问题,又能巧妙的避免了高维空间下计算量大的困扰。网络
为了帮助你们更好的理解非线性SVM对问题的处理方式,咱们瞅瞅下面的一个例子。数据结构
例子来源:李航——《统计学习方法》第七章app
在左图,咱们能够看见两类样本,其中圆点表明正样本,叉叉表明负样本。咱们根本没法经过一条直线将两类样本分割开来,能够经过一个椭圆才能将两类样本分隔开,也就是说这个原始数据集是线性不可分的。
那咋搞呢?真让人头大!
各位看官还记得,咱们最初在介绍线性SVM的时候那个生动形象的例子么?
咱们能够这样想象一下:假如咱们上面的数据样本点都放在桌面上,此时咱们凝神屏气,铁砂掌一拍,此时圆点样本弹起,就在这么一瞬间,咱们使用一个平面就能够完美的将两类样本数据分隔开。而非线性SVM所用到的就是这么一种思想,将低维度空间映射到高纬度空间,以此达到方便分类的目的。
这个时候,就引入了一个映射函数的概念。而对于同一个分类问题,咱们可使用不一样的映射函数来进行处理。如上右图处理方式虽然使用到了映射函数,可是维度并无发生改变,却依然可以完美分类,下面咱们来看看这个分类的处理过程:
咱们不妨将上面的数据样本集表示为\(T=\{(x_1,y_1), (x_2,y_2),...,(x_N,y_N)\}\),其中\(x_i\)表示的是样本的属性特征向量(相信你们都可以反应过来),\(y_i\)表示的是样本的分类标签。经过初步目测,咱们能够发现这些数据样本集可能知足这么一种关系:负样本的数据集在一个椭圆的内部,而正样本被夹在两个椭圆的之间。
既然是椭圆,咱们不妨对原始数据集进行一个映射,将全部的数据样本\(x_i=(x_1^{(1)},x_2^{(2)})\),映射成\(z_i=((x_1^{(1)})^2,(x_2^{(2)})^2)\)。通过这种“平方”映射的结果,咱们能够发现,以前的数据样本整体在的四个象限都存在,映射以后就只存在于第一象限,且两类数据样本有明显的分割间距。全部样本映射以后,将原空间中的椭圆:
变换成为新空间中的直线:
即虽然映射以前的数据样本是线性不可分的,可是映射以后的数据样本集是线性可分的。因此,咱们能够获得解决非线性问题的两个步骤:
也就是说,在对非线性可分数据集进行分类的时候,咱们常用到映射函数,不妨令其表示为\(\phi(x)\),对于上述映射来说,\(\phi(x)=((x_1^{(1)})^2,(x_2^{(2)})^2)\)。对此,咱们给出核函数的定义:
设\(X\)是输入空间,又设\(H\)为映射后的特征空间,若是存在一个从\(X\)到\(H\)的映射
使得对全部的\(x,z \in X\)(注意\(X\)是输入空间,也就是说\(x,z\)实际上是两个样本的属性特征向量),函数K(x,z)知足条件
则称\(K(x,z)\)为核函数,\(\phi(x)\)为映射函数。
上面是李航——《统计学习方法》中给出核函数的定义,务必要理解清楚。也就是说咱们如今的当务之急就是要找出\(K(x_1,x_2)\)来代替\(\phi(x_1)\cdot\phi(x_2)\)。
在前面,咱们已经获得了决策面的表达式:
因为此时咱们的数据集是非线性可分的,因此须要对属性特征进行映射,映射以后变成:
干啥子必定要借助核函数来解决最终非线性问题呢,直接经过映射再內积的方式不是照样香么???
对于这个问题,咱们能够这样理解:① 先找到映射函数,再进行內积,这是两个步骤。而直接采用核函数就是一步到位,这是省力。② 映射还须要找到映射对应的映射函数才能继续下一步,并且映射以后在內积每每计算量比较的复杂,而采用核函数的计算量就比较的简单,这是省时。
省时又省力的核函数,何乐为不为呢???这种将內积的形式转化成核函数进行处理,就是咱们常说的核技巧。
下面咱们经过一个简单的例子来进一步理解下核函数的魅力。
由上图,咱们首先定义了两个二维向量的样本\(x_一、x_2\),而后将其经过映射函数进行处理,处理成了一个三维向量的形式。经过计算能够发现,此时\(\phi^T(x_1)\cdot \phi(x_2)=(x_1\cdot x_2)^2\),咱们令\(K(x_1, x_2)=(x_1\cdot x_2)^2\),假设咱们事先已经知道了核函数\(K(x_1,x_2)\)的表达式,此时就能将数据直接代入到核函数中,这样所取得的效果与映射以后的內积计算相等,这样就巧妙的避免了寻找映射函数的麻烦以及计算大的困扰。
固然了,上述仅仅只是其中一种映射的方式,即将2维映射到3维。咱们也能够把2维映射成5维,这就涉及到了排列组合了。当咱们的维数比较少的时候计算量还尚且能够接受,假如说咱们的维度比较大,甚至达到了无穷维的程度,此时的计算量就真的无从下手了。因此,有的时候使用核技巧仍是颇有必要的。
讲到这里,可能有的读者会有疑问:既然是须要在知道核函数的前提下,才能巧妙的利用核技巧。那么,我怎么知道核函数具体的表达式是什么呢???
实际上是这样的,咱们通常在解决非线性分类问题的时候,一般都会事先在经常使用的几个核函数中选择一个核函数来解决问题。经常使用的核函数主要有如下几个:
还有其余一些像径向基核函数、拉普拉斯核、二次有理核、多元二次核等等,下面咱们重点讲讲高斯核函数,这个也是咱们在实际进行非线性分类的时候使用比较频繁的一个核函数。
咱们先说下高斯核函数中比较重要的两个特性:
下面咱们具体看看上面高斯核函数的特性。
这个问题就牵涉到泰勒展开了。
如图可见,当\(n -> \infty\)时,此时的泰勒展开就至关于指数形式,即高斯核函数实际上是一种无穷维映射所获得的结果。
经过计算两样本之间的“距离”\(d=||\phi(x_i)-\phi_j||^2\)化简分析可知,当\(\sigma\)趋向于\(\infty\)时,此时两样本的区分度很小,很难区分各个样本的分类。而当\(\sigma\)趋向于\(0\)时,此时两样本的区分度很大,有可能致使过拟合。
接下来咱们经过实际的例子来实现非线性问题的分类吧。训练数据集采用的是testSetRBF.txt
,测试数据集采用的是testSetRBF2.txt
,两个数据集都有100行,三列,其中第1、第二列表示的是数据样本的属性特征,而第三列表明的是数据样本标签。数据下载地址:
部分数据样本以下所示:
为了更加直观的感觉下数据样本集的分布,咱们不妨经过Matplotlib分别对两个数据样本集进行可视化,可视化代码以下:
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 def showDataSet(dataMat, labelMat): data_plus, data_minus = list(), list() for i in range(len(dataMat)): if labelMat[i] > 0: data_plus.append(dataMat[i]) else: data_minus.append(dataMat[i]) data_plus_np, data_minus_np = np.array(data_plus), np.array(data_minus) plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1]) plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) plt.show()
可视化结果以下:
能够发现,咱们根本没法经过一条直线来将两类数据进行分类。也就说咱们须要经过一个核函数来对数据集进行处理,如下采用的是高斯核函数,大部分代码和前几篇同样,在其基础上进行改进,完整代码以下:
完整代码参考于:《机器学习实战》以及非线性SVM
import matplotlib.pyplot as plt import numpy as np import random class optStruct: """ 数据结构,维护全部须要操做的值 Parameters: dataMatIn - 数据矩阵 classLabels - 数据标签 C - 松弛变量 toler - 容错率 kTup - 包含核函数信息的元组,第一个参数存放核函数类别,第二个参数存放必要的核函数须要用到的参数 """ def __init__(self, dataMatIn, classLabels, C, toler, kTup): self.X = dataMatIn #数据矩阵 self.labelMat = classLabels #数据标签 self.C = C #松弛变量 self.tol = toler #容错率 self.m = np.shape(dataMatIn)[0] #数据矩阵行数 self.alphas = np.mat(np.zeros((self.m,1))) #根据矩阵行数初始化alpha参数为0 self.b = 0 #初始化b参数为0 self.eCache = np.mat(np.zeros((self.m,2))) #根据矩阵行数初始化虎偏差缓存,第一列为是否有效的标志位,第二列为实际的偏差E的值。 self.K = np.mat(np.zeros((self.m,self.m))) #初始化核K for i in range(self.m): #计算全部数据的核K self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup) def kernelTrans(X, A, kTup): """ 经过核函数将数据转换更高维的空间 Parameters: X - 数据矩阵 A - 单个数据的向量 kTup - 包含核函数信息的元组 Returns: K - 计算的核K """ m,n = np.shape(X) K = np.mat(np.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 = np.exp(K/(-1*kTup[1]**2)) #计算高斯核K else: raise NameError('核函数没法识别') return K #返回计算的核K def loadDataSet(fileName): """ 读取数据 Parameters: fileName - 文件名 Returns: dataMat - 数据矩阵 labelMat - 数据标签 """ 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 def calcEk(oS, k): """ 计算偏差 Parameters: oS - 数据结构 k - 标号为k的数据 Returns: Ek - 标号为k的数据偏差 """ fXk = float(np.multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b) Ek = fXk - float(oS.labelMat[k]) return Ek def selectJrand(i, m): """ 函数说明:随机选择alpha_j的索引值 Parameters: i - alpha_i的索引值 m - alpha参数个数 Returns: j - alpha_j的索引值 """ j = i #选择一个不等于i的j while (j == i): j = int(random.uniform(0, m)) return j def selectJ(i, oS, Ei): """ 内循环启发方式2 Parameters: i - 标号为i的数据的索引值 oS - 数据结构 Ei - 标号为i的数据偏差 Returns: j, maxK - 标号为j或maxK的数据的索引值 Ej - 标号为j的数据偏差 """ maxK = -1; maxDeltaE = 0; Ej = 0 #初始化 oS.eCache[i] = [1,Ei] #根据Ei更新偏差缓存 validEcacheList = np.nonzero(oS.eCache[:,0].A)[0] #返回偏差不为0的数据的索引值 if (len(validEcacheList)) > 1: #有不为0的偏差 for k in validEcacheList: #遍历,找到最大的Ek if k == i: continue #不计算i,浪费时间 Ek = calcEk(oS, k) #计算Ek deltaE = abs(Ei - Ek) #计算|Ei-Ek| if (deltaE > maxDeltaE): #找到maxDeltaE maxK = k; maxDeltaE = deltaE; Ej = Ek return maxK, Ej #返回maxK,Ej else: #没有不为0的偏差 j = selectJrand(i, oS.m) #随机选择alpha_j的索引值 Ej = calcEk(oS, j) #计算Ej return j, Ej #j,Ej def updateEk(oS, k): """ 计算Ek,并更新偏差缓存 Parameters: oS - 数据结构 k - 标号为k的数据的索引值 Returns: 无 """ Ek = calcEk(oS, k) #计算Ek oS.eCache[k] = [1,Ek] #更新偏差缓存 def clipAlpha(aj,H,L): """ 修剪alpha_j Parameters: aj - alpha_j的值 H - alpha上限 L - alpha下限 Returns: aj - 修剪后的alpah_j的值 """ if aj > H: aj = H if L > aj: aj = L return aj def innerL(i, oS): """ 优化的SMO算法 Parameters: i - 标号为i的数据的索引值 oS - 数据结构 Returns: 1 - 有任意一对alpha值发生变化 0 - 没有任意一对alpha值发生变化或变化过小 """ #步骤1:计算偏差Ei Ei = calcEk(oS, i) #优化alpha,设定必定的容错率。 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)): #使用内循环启发方式2选择alpha_j,并计算Ej j,Ej = selectJ(i, oS, Ei) #保存更新前的aplpha值,使用深拷贝 alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy(); #步骤2:计算上下界L和H 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: return 0 #步骤3:计算eta eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j] if eta >= 0: return 0 #步骤4:更新alpha_j oS.alphas[j] -= oS.labelMat[j] * (Ei - Ej)/eta #步骤5:修剪alpha_j oS.alphas[j] = clipAlpha(oS.alphas[j],H,L) #更新Ej至偏差缓存 updateEk(oS, j) if (abs(oS.alphas[j] - alphaJold) < 0.00001): return 0 #步骤6:更新alpha_i oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j]) #更新Ei至偏差缓存 updateEk(oS, i) #步骤7:更新b_1和b_2 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] #步骤8:根据b_1和b_2更新b if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]): 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 def smoP(dataMatIn, classLabels, C, toler, maxIter, kTup = ('lin',0)): """ 完整的线性SMO算法 Parameters: dataMatIn - 数据矩阵 classLabels - 数据标签 C - 松弛变量 toler - 容错率 maxIter - 最大迭代次数 kTup - 包含核函数信息的元组 Returns: oS.b - SMO算法计算的b oS.alphas - SMO算法计算的alphas """ oS = optStruct(np.mat(dataMatIn), np.mat(classLabels).transpose(), C, toler, kTup) #初始化数据结构 iter = 0 #初始化当前迭代次数 entireSet = True; alphaPairsChanged = 0 while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)): #遍历整个数据集都alpha也没有更新或者超过最大迭代次数,则退出循环 alphaPairsChanged = 0 if entireSet: #遍历整个数据集 for i in range(oS.m): alphaPairsChanged += innerL(i,oS) #使用优化的SMO算法 iter += 1 else: #遍历非边界值 nonBoundIs = np.nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0] #遍历不在边界0和C的alpha for i in nonBoundIs: alphaPairsChanged += innerL(i,oS) iter += 1 if entireSet: #遍历一次后改成非边界遍历 entireSet = False elif (alphaPairsChanged == 0): #若是alpha没有更新,计算全样本遍历 entireSet = True return oS.b,oS.alphas #返回SMO算法计算的b和alphas def testRbf(k1 = 1.3): """ 测试函数 Parameters: k1 - 使用高斯核函数的时候表示到达率 Returns: 无 """ dataArr,labelArr = loadDataSet('testSetRBF.txt') #加载训练集 b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 100, ('rbf', k1)) #根据训练集计算b和alphas datMat = np.mat(dataArr); labelMat = np.mat(labelArr).transpose() svInd = np.nonzero(alphas.A > 0)[0] #得到支持向量 sVs = datMat[svInd] labelSV = labelMat[svInd]; print("支持向量个数:%d" % np.shape(sVs)[0]) m,n = np.shape(datMat) errorCount = 0 for i in range(m): kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1)) #计算各个点的核 predict = kernelEval.T * np.multiply(labelSV,alphas[svInd]) + b #根据支持向量的点,计算超平面,返回预测结果 if np.sign(predict) != np.sign(labelArr[i]): errorCount += 1 #返回数组中各元素的正负符号,用1和-1表示,并统计错误个数 print("训练集错误率: %.2f%%" % ((1 - float(errorCount)/m)*100)) #打印错误率 dataArr,labelArr = loadDataSet('testSetRBF2.txt') #加载测试集 errorCount = 0 datMat = np.mat(dataArr); labelMat = np.mat(labelArr).transpose() m,n = np.shape(datMat) for i in range(m): kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1)) #计算各个点的核 predict=kernelEval.T * np.multiply(labelSV,alphas[svInd]) + b #根据支持向量的点,计算超平面,返回预测结果 if np.sign(predict) != np.sign(labelArr[i]): errorCount += 1 #返回数组中各元素的正负符号,用1和-1表示,并统计错误个数 print("测试集正确率: %.2f%%" % ((1 - float(errorCount)/m)*100)) #打印错误率 def showDataSet(dataMat, labelMat): """ 数据可视化 Parameters: dataMat - 数据矩阵 labelMat - 数据标签 Returns: 无 """ data_plus = [] #正样本 data_minus = [] #负样本 for i in range(len(dataMat)): if labelMat[i] > 0: data_plus.append(dataMat[i]) else: data_minus.append(dataMat[i]) data_plus_np = np.array(data_plus) #转换为numpy矩阵 data_minus_np = np.array(data_minus) #转换为numpy矩阵 plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1]) #正样本散点图 plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1]) #负样本散点图 plt.show() if __name__ == '__main__': testRbf(k1 = 0.5)
运行结果以下:
由结果,咱们能够发现,此时训练集的正确率为98%,而测试集的正确率为88%,能够看出有可能出现了必定的过拟合,读者可自行调节k1参数进一步观察数据集正确率状况,这东西慢慢体会吧。
以上就是非线性SVM的内容了,重点在于要把核技巧理解透彻。SVM的所有内容就更新到这里了,其余SVM涉及到的相关内容,在从此的文章可能会说起。下期的话应该会更新Knn相关的内容。
我是Taoye,爱专研,爱分享,热衷于各类技术,学习之余喜欢下象棋、听音乐、聊动漫,但愿借此一亩三分地记录本身的成长过程以及生活点滴,也但愿能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder
参考资料:
[1] 《机器学习实战》:Peter Harrington 人民邮电出版社
[2] 《统计学习方法》:李航 第二版 清华大学出版社
[3] 《机器学习》:周志华 清华大学出版社
[4] 支持向量机之非线性SVM:https://cuijiahua.com/blog/2017/11/ml_9_svm_2.html
推荐阅读
《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦
《Machine Learning in Action》—— Taoye给你讲讲决策树究竟是支什么“鬼”
《Machine Learning in Action》—— 剖析支持向量机,优化SMO
《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
print( "Hello,NumPy!" )
干啥啥不行,吃饭第一名
Taoye渗透到一家黑平台总部,背后的真相细思极恐
《大话数据库》-SQL语句执行时,底层究竟作了什么小动做?
那些年,咱们玩过的Git,真香
基于Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度学习环境
网络爬虫之页面花式解析