说在前面: 前几天,公众号不是给大家推送了第二篇关于决策树的文章嘛。阅读过的读者应该会发现,在最后排版已经有点乱套了。真的很抱歉,也不知道咋回事,到了后期Markdown格式文件的内容就解析出现问题了,似乎涉及到Latex就会多多少少排版错乱???暂时也没什么比较好的解决办法,如果有朋友知道的可以联系下Taoye,长时间用Markdown + Latex码文已成习惯了,关于机器学习文章的内容,更好的阅读体验,大家可以跳转至我在Cmd Markdown平台发布的内容,也可前往我的掘金主页,阅读体验都是不错的,PC端食用更佳:
- Taoye的掘金:https://juejin.cn/user/2999123453943021
- SVM——线性篇:https://www.zybuluo.com/tianxingjian/note/1755051
- SVM——优化篇:https://www.zybuluo.com/tianxingjian/note/1756832
- SVM——非线性:https://www.zybuluo.com/tianxingjian/note/1758624
- 决策树——“鬼”东西:https://www.zybuluo.com/tianxingjian/note/1757662
- 决策树——代码实战:https://www.zybuluo.com/tianxingjian/note/1758236
《Machine Learning in Action》—— 懂的都懂,不懂的也能懂。非线性支持向量机
关于支持向量机(SVM),前面我们已经详细讲解了线性SVM的推导过程以及代码实现。之后我们发现,虽然效果还算不错,数据集基本都能够分类正确,模型训练效率的话也还说的过去,但这是基于我们训练样本数据集比较少、迭代次数比较少的前提下。
假如说我们数据集比较大,而且还需要迭代不少次数的话,使用第一篇SVM文章中所提到的SMO算法的效率可就不敢恭维了,训练的速度可堪比龟龟。所以之后也就有了SVM第二篇的内容——优化SMO算法的优化,进一步提高了SVM模型的训练效率。
关于前两篇SVM文章的详细内容,可暂且跳转至:
有了线性SVM所提到的知识做基础,非线性SVM的内容应该不会很难,理解起来也不会有那么大的压力。大家在食用前也不要畏惧它,Taoye一定给你安排的明明白白。原理部分的内容一定要理解清楚、理解透彻,尤其是核技巧的奇妙之处,而代码部分的内容只需要能看懂就行,知道代码所表达的意思即可。所以,一定要加强内功的修炼,才能在之后实战过程中如鱼得水
前面也有讲到,对于一些线性可分的分类问题,线性SVM的确是一种非常有效的方法。
但,现实往往事与愿违。
在实际问题中,我们的原始数据集往往是线性不可分的,也就是说我们无法找到一条直线 | 平面 | 超平面将目标数据集分隔开来,这个时候就要引出我们的非线性SVM了,而其中主要特点就是巧妙的利用了核技巧,既能解决低维空间下分类困难的问题,又能巧妙的避免了高维空间下计算量大的困扰。
为了帮助大家更好的理解非线性SVM对问题的处理方式,我们瞅瞅下面的一个例子。
例子来源:李航——《统计学习方法》第七章
在左图,我们可以看见两类样本,其中圆点代表正样本,叉叉代表负样本。我们根本无法通过一条直线将两类样本分割开来,可以通过一个椭圆才能将两类样本分隔开,也就是说这个原始数据集是线性不可分的。
那咋搞呢?真让人头大!
<img src="https://gitee.com/tianxingjian123/my-images-repository/raw/master/img/naokuo.jpg" width="120">
各位看官还记得,我们最初在介绍线性SVM的时候那个生动形象的例子么?
我们可以这样想象一下:假如我们上面的数据样本点都放在桌面上,此时我们凝神屏气,铁砂掌一拍,此时圆点样本弹起,就在这么一瞬间,我们使用一个平面就可以完美的将两类样本数据分隔开。而非线性SVM所用到的就是这么一种思想,将低维度空间映射到高纬度空间,以此达到方便分类的目的。
这个时候,就引入了一个映射函数的概念。而对于同一个分类问题,我们可以使用不同的映射函数来进行处理。如上右图处理方式虽然使用到了映射函数,但是维度并没有发生改变,却依然能够完美分类,下面我们来看看这个分类的处理过程:
我们不妨将上面的数据样本集表示为,其中表示的是样本的属性特征向量(相信大家都能够反应过来),表示的是样本的分类标签。通过初步目测,我们可以发现这些数据样本集可能满足这么一种关系:负样本的数据集在一个椭圆的内部,而正样本被夹在两个椭圆的之间。
既然是椭圆,我们不妨对原始数据集进行一个映射,将所有的数据样本,映射成。经过这种“平方”映射的结果,我们可以发现,之前的数据样本总体在的四个象限都存在,映射之后就只存在于第一象限,且两类数据样本有明显的分割间距。所有样本映射之后,将原空间中的椭圆:
变换成为新空间中的直线:
即虽然映射之前的数据样本是线性不可分的,但是映射之后的数据样本集是线性可分的。所以,我们可以得到解决非线性问题的两个步骤:
- 找到一个映射函数将原始数据样本映射成一个新的数据样本(简单理解就是数学中的代换)
- 映射之后的数据样本就是一个线性可分的了,之后利用我们前面那篇文章中的方法进行处理,即可实现线性可分
也就是说,在对非线性可分数据集进行分类的时候,我们经常使用到映射函数,不妨令其表示为,对于上述映射来讲,。对此,我们给出核函数的定义:
设是输入空间,又设为映射后的特征空间,如果存在一个从到的映射
使得对所有的(注意是输入空间,也就是说其实是两个样本的属性特征向量),函数K(x,z)满足条件
则称为核函数,为映射函数。
上面是李航——《统计学习方法》中给出核函数的定义,务必要理解清楚。也就是说我们现在的当务之急就是要找出来代替。
在前面,我们已经得到了决策面的表达式:
由于此时我们的数据集是非线性可分的,所以需要对属性特征进行映射,映射之后变成:
干啥子一定要借助核函数来解决最终非线性问题呢,直接通过映射再內积的方式不是照样香么???
对于这个问题,我们可以这样理解:① 先找到映射函数,再进行內积,这是两个步骤。而直接采用核函数就是一步到位,这是省力。② 映射还需要找到映射对应的映射函数才能继续下一步,而且映射之后在內积往往计算量比较的复杂,而采用核函数的计算量就比较的简单,这是省时。
省时又省力的核函数,何乐为不为呢???这种将內积的形式转化成核函数进行处理,就是我们常说的核技巧。
下面我们通过一个简单的例子来进一步理解下核函数的魅力。
<img src="https://gitee.com/tianxingjian123/my-images-repository/raw/master/img/no_svm_2.jpg" width="600">
由上图,我们首先定义了两个二维向量的样本,然后将其通过映射函数进行处理,处理成了一个三维向量的形式。通过计算可以发现,此时,我们令,假设我们事先已经知道了核函数的表达式,此时就能将数据直接代入到核函数中,这样所取得的效果与映射之后的內积计算相等,这样就巧妙的避免了寻找映射函数的麻烦以及计算大的困扰。
当然了,上述仅仅只是其中一种映射的方式,即将2维映射到3维。我们也可以把2维映射成5维,这就涉及到了排列组合了。当我们的维数比较少的时候计算量还尚且可以接受,假如说我们的维度比较大,甚至达到了无穷维的程度,此时的计算量就真的无从下手了。所以,有的时候使用核技巧还是很有必要的。
讲到这里,可能有的读者会有疑问:既然是需要在知道核函数的前提下,才能巧妙的利用核技巧。那么,我怎么知道核函数具体的表达式是什么呢???
其实是这样的,我们一般在解决非线性分类问题的时候,通常都会事先在常用的几个核函数中选择一个核函数来解决问题。常用的核函数主要有以下几个:
- 多项式核函数(次多项式)
- 高斯核函数
还有其他一些像径向基核函数、拉普拉斯核、二次有理核、多元二次核等等,下面我们重点讲讲高斯核函数,这个也是我们在实际进行非线性分类的时候使用比较频繁的一个核函数。
我们先说下高斯核函数中比较重要的两个特性:
- 高斯核函数会将有限维映射到无穷维空间。
- 是用户自定义的用于确定到达率(reach)或者说函数值跌落到0的速度参数,它也叫作高斯核函数的带宽,该值趋近于无穷大的时候,样本与样本之间的距离区分度很小,所以比较难分类。该值趋近于0的时候,样本与样本之间的区分度很大,虽然有利于样本的分类,但是很有可能会出现过拟合的问题。所以,通过调控参数,高斯核函数具有比较高的灵活性。
下面我们具体看看上面高斯核函数的特性。
- 高斯核函数为什么会将有限维映射到无穷维???
这个问题就牵涉到泰勒展开了。
<img src="https://gitee.com/tianxingjian123/my-images-repository/raw/master/img/no_svm_3.jpg" width="500">
如图可见,当时,此时的泰勒展开就相当于指数形式,即高斯核函数其实是一种无穷维映射所得到的结果。
- 为什么参数的取值会影响到样本的分类结果???
<img src="https://gitee.com/tianxingjian123/my-images-repository/raw/master/img/no_svm_4.jpg" width="500">
通过计算两样本之间的“距离”化简分析可知,当趋向于时,此时两样本的区分度很小,很难区分各个样本的分类。而当趋向于时,此时两样本的区分度很大,有可能导致过拟合。
接下来我们通过实际的例子来实现非线性问题的分类吧。训练数据集采用的是testSetRBF.txt
,测试数据集采用的是testSetRBF2.txt
,两个数据集都有100行,三列,其中第一、第二列表示的是数据样本的属性特征,而第三列代表的是数据样本标签。数据下载地址:
部分数据样本如下所示:
为了更加直观的感受下数据样本集的分布,我们不妨通过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
参考资料:
<font style="font-size:13px;opacity:0.7">[1] 《机器学习实战》:Peter Harrington 人民邮电出版社</font>
<font style="font-size:13px;opacity:0.7">[2] 《统计学习方法》:李航 第二版 清华大学出版社</font>
<font style="font-size:13px;opacity:0.7">[3] 《机器学习》:周志华 清华大学出版社</font>
<font style="font-size:13px;opacity:0.7">[4] 支持向量机之非线性SVM:https://cuijiahua.com/blog/2017/11/ml_9_svm_2.html</font>
推荐阅读
《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搭建深度学习环境
网络爬虫之页面花式解析