PS:该系列数据都可以在图灵社区(点击此链接)中随书下载中下载(如下)
支持向量机(Support Vector Machines, SVM)
优点:泛化错误率低,计算开销不大,结果易于解释。
缺点:对参数调节和核函数的选择敏感,原始分类器不加修改仅适用于处理二类问题。
使用数据类型:数值型和标称型数据。
假设给定一个特征空间上的训练数据集
T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x N , y N ) } T=\{(x_1,y_1),(x_2,y_2),...,(x_N,y_N)\} T={(x1,y1),(x2,y2),...,(xN,yN)}
其中, x i ∈ X = R n , y i ∈ Y = { + 1 , − 1 } , i = 1 , 2 , ⋯ , N x_{i} \in \mathcal{X}=\mathbf{R}^{n}, \quad y_{i} \in \mathcal{Y}=\{+1,-1\}, \quad i=1,2, \cdots, N xi∈X=Rn,yi∈Y={+1,−1},i=1,2,⋯,N, x i x_i xi为第 i i i个特征向量,也称为实例, y i y_i yi为 x i x_i xi的类标记,当 y i = + 1 y_i=+1 yi=+1时,称 x i x_i xi为正例;当 y i = − 1 y_i=-1 yi=−1时,称 x i x_i xi为负例, ( x i , y i ) (x_i,y_i) (xi,yi)称为样本点。再假设训练数据集是线性可分的
学习的目标是在特征空间中找到一个分离超平面,能将实例分到不同类,分离超平面对应方程 w ⋅ x + b = 0 w \cdot x+b=0 w⋅x+b=0,它由法向量 w w w和截距 b b b决定,可用 ( w , b ) (w,b) (w,b)来表示。分离超平面将特征空间划分为两部分,一部分是正类,一部分是负类。法向量指向的一侧为正类,另一侧为负类。
一般地,大概训练数据集先行可分时,存在无穷个分离超平面可将两类数据正确分开。感知机利用误分类最小的策略,求得分离超平面,不过这时的解有无穷多个,线性可分支持向量机利用间隔最大化最优分离超平面,这时,解是唯一的。
线性可分支持向量机:给定先行可分训练数据集,通过间隔最大化或等价地求解相应的凸二次规划问题学习得到的分离超平面为 w ∗ ⋅ x + b ∗ = 0 w^{*} \cdot x+b^{*}=0 w∗⋅x+b∗=0
以及相应的分类决策函数 f ( x ) = sign ( w ∗ ⋅ x + b ∗ ) f(x)=\operatorname{sign}\left(w^{*} \cdot x+b^{*}\right) f(x)=sign(w∗⋅x+b∗)
称为线性可分支持向量机。
分隔超平面形式: w T x + b w^Tx+b wTx+b
点A到分隔超平面的距离: ∣ w T + b ∣ ∣ ∣ w ∣ ∣ \frac{|w^T+b|}{||w||} ∣∣w∣∣∣wT+b∣
取类别标签: l a b e l = { − 1 , 1 } \text label=\{-1, 1\} label={−1,1}
我们需要找出分类器中定义的 w w w和 b b b,为此我们必须找到具有最小间隔的数据点,而这些数据点也就是支持向量。以旦找到具有最小间隔的数据点,我们就需要对该间隔最大化。这可以写作:
arg max w , b { min n ( label ⋅ ( w T x + b ) ) ⋅ 1 ∥ w ∥ } \arg \max _{w, b}\left\{\min _{n}\left(\operatorname{label} \cdot\left(w^{\mathrm{T}} x+b\right)\right) \cdot \frac{1}{\|w\|}\right\} argw,bmax{nmin(label⋅(wTx+b))⋅∥w∥1}
直接求解困难,转化为另一种更容易求解的形式。如果令所有支持向量的 label ∗ ( w T + b ) \text {label} * (w^T+b) label∗(wT+b)都为1,那么就可以通过求 ∣ ∣ w ∣ ∣ − 1 ||w||^{-1} ∣∣w∣∣−1的最大值来得到最终解。但并非所有数据点的 label ∗ ( w T + b ) \text {label} * (w^T+b) label∗(wT+b)都等于1,因此给定约束条件 l a b e l ∗ ( w T + b ) ⩾ 1.0 \text label*(w^T+b) \geqslant 1.0 label∗(wT+b)⩾1.0,利用拉格朗日乘子法,优化目标函数最后可以写成: max α [ ∑ i = 1 m α − 1 2 ∑ i , j = 1 m label ( i ) ⋅ label ( j ) ⋅ a i ⋅ a j ⟨ x ( i ) , x ( j ) ⟩ ] \max _{\alpha}\left[\sum_{i=1}^{m} \alpha-\frac{1}{2} \sum_{i, j=1}^{m} \operatorname{label}^{(i)} \cdot \operatorname{label}^{(j)} \cdot a_{i} \cdot a_{j}\left\langle x^{(i)}, x^{(j)}\right\rangle\right] αmax[i=1∑mα−21i,j=1∑mlabel(i)⋅label(j)⋅ai⋅aj⟨x(i),x(j)⟩]
其中,尖括号表示 x ( i ) x^{(i)} x(i)和 x ( j ) x^{(j)} x(j)两个向量的内积。
其约束条件为: α ⩾ 0 , 和 ∑ i − 1 m α i ⋅ label ( i ) = 0 \alpha \geqslant 0, \text和 \sum_{i-1}^{m} \alpha_{i} \cdot \operatorname{label}^{(i)}=0 α⩾0,和i−1∑mαi⋅label(i)=0
上述要求数据100%线性可分,但几乎所有数据都不那么“干净”,因此引入松弛变量(slack variable),来允许有些数据点可以处于分隔面的错误一侧。这样我们的优化目标仍然保持不变,但是新的约束条件变为: C ⩾ α ⩾ 0 , 和 ∑ i = 1 m α i ⋅ label ( i ) = 0 C \geqslant \alpha \geqslant 0, \quad \text {和} \sum_{i=1}^{m} \alpha_{i} \cdot \operatorname{label}^{(i)}=0 C⩾α⩾0,和i=1∑mαi⋅label(i)=0
这里的常数C用于控制“最大化间隔”和“保证大部分点的函数间隔小于1.0”这两个目标的权重。
上面的公式推导需要大量理论知识,强烈建议阅读李航著作《统计学习方法》中关于支持向量机的部分(网上有相应的pdf可以下载),下面再放上l两篇笔者认为写的比较详细的博文链接:机器学习实战(六)——支持向量机和支持向量机(SVM)从入门到放弃再到掌握
1996年,John Platt发布了一个称为SMO的强大算法,用于训练SVM。SMO表示序列最小化(Sequential Minimal Optimization)。
SMO算法的目标是求出一系列alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w并得到分离超平面。
SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的alpha,那么就增大其中一个同时减小另一个。这里所谓的“合适”就是指两个alpha必须要符合一定的条件,条件之一就是这两个alpha必须要在间隔边界之外,第二个条件则是这两个alpha还没有进行过区间化处理或者不在边界上。
Platt SMO算法中的外循环确定要优化的最佳alpha对。而简化版却会跳过这一部分,首先在数据集上遍历每一个alpha,然后在剩下的alpha集合中随机选择另一个alpha,从而构建alpha。我们之所以要同时改变两个alpha值,是因为有一个约束条件: ∑ α i ⋅ label ( i ) = 0 \sum \alpha_i \cdot \operatorname{label}^{(i)}=0 ∑αi⋅label(i)=0
由于改变一个alpha可能会导致该约束条件失效,因此我们总是同时改变两个alpha。
为此,我们将构建一个辅助函数,用于在某个区间范围内随机选择一个整数。同时,也需要另一个辅助函数,用于在数值太大时对其进行调整。创建svmMLiA.py文件,并将数据集中对应的testSet.txt文件复制到svmMLiA.py所在的文件夹中:
import numpy as np
import matplotlib.pyplot as plt
def loadDataSet(fileName):
'''加载数据集'''
dataMat = []; labelMat = []
with open(fileName,'r') as fileObject:
for line in fileObject.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):
'''可视化数据集'''
dataMat1 = []; dataMat2 = []
for i in range(len(dataMat)):
if labelMat[i] > 0.0:
dataMat1.append(dataMat[i])
else:
dataMat2.append(dataMat[i])
data1Arr = np.array(dataMat1)
data2Arr = np.array(dataMat2)
plt.scatter(np.transpose(dataMat1)[0], np.transpose(dataMat1)[1])
plt.scatter(np.transpose(dataMat2)[0], np.transpose(dataMat2)[1])
plt.show()
def selectJrand(i , m):
'''
随机选择另一个alpha下标
i:alpha的下标,m:所有alpha的数目
'''
j = i
while (j == i):
j = int(np.random.uniform(0,m))
return j
def clipAlpha(aj, H, L):
'''用于调整大于H或者小于L的alpha值'''
if aj > H:
aj = H
if L > aj:
aj = L
return aj
创建一个alpha向量并将其初始化为0向量
当迭代次数小于最大迭代次数时(外循环):
对数据集中的每个数据向量(内循环):
如果该数据向量可以被优化:
随机选择另一个数据向量
同时优化这两个向量
如果两个向量都不能被优化,退出内循环
如果所有向量都没被优化,增加迭代数目,继续下一次循环
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
'''
简化版SMO算法
参数说明:dataMatIn:数据矩阵 classLabels:数据标签
C:松弛变量 toler:容错率 maxIter:最大迭代次数
'''
dataMatrix = np.mat(dataMatIn)
labelMat = np.mat(classLabels).transpose()
b = 0
m, n = np.shape(dataMatrix)
alphas = np.mat(np.zeros((m,1)))
iter = 0
while (iter < maxIter):
#记录alpha是否已经优化
alphaPairsChanged = 0
for i in range(m):
#计算fxi和误差Ei
fXi = float(np.multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[i, :].T)) + b
Ei = fXi - float(labelMat[i])
#优化alpha,设定一定的容错率
if ((labelMat[i] * Ei < -toler) and (alphas[i] < C)) or \
((labelMat[i] * Ei > toler) and (alphas[i] > 0)):
#随机选取另一个与alpha_i成对优化的alpha_j
j = selectJrand(i, m)
#计算fxj和误差Ej
fXj = float(np.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
#alpha的最优修改量eta
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: ", iter, " i:", i, ", paris changed ", alphaPairsChanged)
if (alphaPairsChanged == 0):
iter += 1
else:
iter = 0
print("iteration number: ", iter)
return b, alphas
def showClassifer(dataMat, labelMat, alphas, w, b):
'''可视化简化版smo算法的分类结果'''
dataMat1 = []; dataMat2 = []
for i in range(len(dataMat)):
if labelMat[i] > 0:
dataMat1.append(dataMat[i])
else:
dataMat2.append(dataMat[i])
data1Arr = np.array(dataMat1)
data2Arr = np.array(dataMat2)
plt.scatter(np.transpose(dataMat1)[0], np.transpose(dataMat1)[1], s=30,alpha=0.7)
plt.scatter(np.transpose(dataMat2)[0], np.transpose(dataMat2)[1], s=30,alpha=0.7)
# 绘制直线
x1 = max(dataMat)[0]
x2 = min(dataMat)[0]
a1, a2 = w
b = float(b)
a1 = float(a1[0])
a2 = float(a2[0])
y1, y2 = (-b - a1 * x1) / a2, (-b - a1 * x2) / a2
plt.plot([x1, x2], [y1, y2])
#找出支持向量点
for i, alpha in enumerate(alphas):
if abs(alpha) > 0:
x, y = dataMat[i]
plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
plt.show()
def calcWs(alphas, dataArr, classLabels):
'''计算w'''
X = np.mat(dataArr)
labelMat = np.mat(classLabels).transpose()
m, n = np.shape(X)
w = np.zeros((n, 1))
for i in range(m):
w += np.multiply(alphas[i] * labelMat[i], X[i, :].T)
return w
进行如下测试,由于SMO算法的随机性,运行结果可能会与下面结果不一样:
简化版和完整版,实现alpha的更改和代数运算的优化环节一模一样。在优化过程中,唯一的不同就是选择alpha的方式。完整版的Platt SMO算法应用了一些能够提速的启发式方法。
Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓的非边界alpha指的是那些不等于边界0或C的alpha值。对整个数据表的扫描相当容易,而实现非边界值alpha值的扫描时,首先需要建立这些alpha值得列表,然后再对这个表进行遍历。同时,该步骤会跳过那些已知的不会改变的alpha值。
在选择第一个alpha后,算法会通过一个内循环来选择第二个alpha值。在优化过程中,会通过最大化步长的方式获得第二个alpha值。
class OptStruct:
'''保存所有值的数据结构'''
def __init__(self, dataMatIn, classLabels, C, toler):
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)))
self.b = 0
#误差缓存,第一列给出eCache是否有效的标志位,第二列给出实际的E值
self.eCache = np.mat(np.zeros((self.m, 2)))
def calcEk(OS, k):
'''计算第K个数据误差E值'''
fXk = float(np.multiply(OS.alphas, OS.labelMat).T * (OS.X * OS.X[k, :].T)) + OS.b
Ek = fXk - float(OS.labelMat[k])
return Ek
def selectJ(i, OS, Ei):
'''启发式方法选择第二个alpha,选择具有最大步长的j'''
#定义步长maxDeltaE(Ei-Ek)取得最大步长时的k值maxK
maxK = -1; maxDeltaE = 0; Ej =0
OS.eCache[i] = [1, Ei]
validEcacheList = np.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)
#选择具有最大步长的j
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)
#将违反KTT条件的找出来
if ((OS.labelMat[i] * Ei < -OS.tol) and (OS.alphas[i] < OS.C)) or \
((OS.labelMat[i] > 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.X[i, :] * OS.X[j, :].T - OS.X[i, :] * OS.X[i, :].T - \
OS.X[j, :] * OS.X[j, :].T
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])
#更新Ei至误差缓存
updateEk(OS, i)
b1 = OS.b - Ei - OS.labelMat[i] * (OS.alphas[i] - alphaIold) * OS.X[i, :] * OS.X[i, :].T - \
OS.labelMat[j] * (OS.alphas[j] - alphaJold) * OS.X[i, :] * OS.X[j, :].T
b2 = OS.b - Ej - OS.labelMat[i] * (OS.alphas[i] - alphaIold) * OS.X[i, :] * OS.X[j, :].T - \
OS.labelMat[j] * (OS.alphas[j] - alphaJold) * OS.X[j, :] * OS.X[j, :].T
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算法'''
OS = OptStruct(np.mat(dataMatIn), np.mat(classLabels).transpose(), C, toler)
iter = 0
entireSet = True; alphaPairsChanged = 0
#遍历整个数据集alpha都没有更新或者超过最大迭代次数,则退出循环
while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
alphaPairsChanged = 0
#遍历整个数据集
if entireSet:
#首先进行完整遍历,过程和简化版的SMO一样
for i in range(OS.m):
alphaPairsChanged += innerL(i, OS)
print("fullSet, iter: ", iter, " i: ", i, ", pairs changed ", alphaPairsChanged)
iter += 1
else:
#非边界遍历,挑选其中alpha值在0和C之间非边界alpha进行优化
nonBoundIs = np.nonzero((OS.alphas.A > 0) * (OS.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(i, OS)
print("non-bound, iter: ", iter, " i: ", i, ", pairs changed ", alphaPairsChanged)
iter += 1
if entireSet:
entireSet = False
elif alphaPairsChanged == 0:
entireSet = True
print("iteration number: ", iter)
return OS.b, OS.alphas
在python命令行进行如下测试:
会发现优化过的SMO方法更快,并且完整版的支持向量更多。
如果数据线性不可分,就需要使用一种称为核函数(kernel) 的工具将数据转换成易于分类器理解的形式。
我们将数据从一个特征空间转换到另一个特征空间。在新空间下,我们可以很容易利用已有的工具对数据进行处理。这个过程称之为从一个特征空间到另一个特征空间的映射。这个过程可以通过核函数实现,可以把核函数想象成一个包装器(wrapper)或者是接口(interface),它能把数据从某个很难处理的形式转换成为另一个较容易处理的形式。
SVM优化中一个特别好的地方就是,所有的运算都可以写成内积(inner product,也称点积) 的形式。我们可以把内积运算替换成核函数,而不必做简化处理。将内积替换成核函数的方式成为核技巧(kernel trick) 或者核“变电”(kernel substation)。
径向基核函数是SVM中常用的一个核函数。径向基核函数是一个采用向量作为自变量的函数,能够基于向量距离运算输出一个标量。这里使用径向基函数的高斯版本,其具体公式为: k ( x , y ) = exp ( − ∥ x − y ∥ 2 2 σ 2 ) k(x, y)=\exp \left(\frac{-\|x-y\|^{2}}{2 \sigma^{2}}\right) k(x,y)=exp(2σ2−∥x−y∥2)
其中, σ \sigma σ是用户定义的用于确定到达率(reach)或者说函数值跌落到0的速度参数。
对svmMLiA.py文件添加一个函数并对OptStruct类、innerL()函数、calcEk(OS, k)函数进行相应修改,并将testSetRBF.txt和testSetRBF2.txt复制到该文件所在的文件夹中:
class OptStruct:
'''保存所有值的数据结构'''
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)))
self.b = 0
#误差缓存,第一列给出eCache是否有效的标志位,第二列给出实际的E值
self.eCache = np.mat(np.zeros((self.m, 2)))
self.K = np.mat(np.zeros((self.m, self.m)))
for i in range(self.m):
self.K[:, i] = kernelTrans(self.X, self.X[i, :], kTup)
def calcEk(OS, k):
'''计算第K个数据误差E值'''
fXk = float(np.multiply(OS.alphas, OS.labelMat).T * OS.K[:, k] + OS.b)
Ek = fXk - float(OS.labelMat[k])
return Ek
def selectJ(i, OS, Ei):
'''启发式方法选择第二个alpha,选择具有最大步长的j'''
#定义步长maxDeltaE(Ei-Ek)取得最大步长时的k值maxK
maxK = -1; maxDeltaE = 0; Ej =0
OS.eCache[i] = [1, Ei]
validEcacheList = np.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)
#选择具有最大步长的j
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)
#将违反KTT条件的找出来
if ((OS.labelMat[i] * Ei < -OS.tol) and (OS.alphas[i] < OS.C)) or \
((OS.labelMat[i] > 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])
#更新Ei至误差缓存
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[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算法'''
OS = OptStruct(np.mat(dataMatIn), np.mat(classLabels).transpose(), C, toler, kTup)
iter = 0
entireSet = True; alphaPairsChanged = 0
#遍历整个数据集alpha都没有更新或者超过最大迭代次数,则退出循环
while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
alphaPairsChanged = 0
#遍历整个数据集
if entireSet:
#首先进行完整遍历,过程和简化版的SMO一样
for i in range(OS.m):
alphaPairsChanged += innerL(i, OS)
print("fullSet, iter: ", iter, " i: ", i, ", pairs changed ", alphaPairsChanged)
iter += 1
else:
#非边界遍历,挑选其中alpha值在0和C之间非边界alpha进行优化
nonBoundIs = np.nonzero((OS.alphas.A > 0) * (OS.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(i, OS)
print("non-bound, iter: ", iter, " i: ", i, ", pairs changed ", alphaPairsChanged)
iter += 1
if entireSet:
entireSet = False
elif alphaPairsChanged == 0:
entireSet = True
print("iteration number: ", iter)
return OS.b, OS.alphas
def kernelTrans(X, A, kTup):
'''核转换函数'''
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))
else:
raise NameError('Houston We a Problem -- That Kernel is not recognized')
return K
def testRbf(k1=1.3):
'''利用核函数进行分类的径向基测试函数'''
dataArr, labelArr = loadDataSet('testSetRBF.txt')
b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1))
datMat = np.mat(dataArr)
labelMat = np.mat(labelArr).transpose()
#构建支持向量矩阵
svInd = np.nonzero(alphas.A>0)[0]
sVs = datMat[svInd]
labelSV = labelMat[svInd]
print("there are", np.shape(sVs)[0], " Support Vectors")
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
print("the training error rate is: ", (float(errorCount) / m))
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
print("the test error rate is: ", (float(errorCount) / m))
进行测试如下,可以更换不同的k1参数以及观察错误率、训练错误率、支持向量个数随k1的变化情况:
在机器学习实战笔记(一)K近邻算法中使用了KNN方法识别,kNN方法效果确实不错,但是需要保留所有的训练样本。而对于支持向量机而言,其需要保留的样本少了很多(即只保留支持向量),但是能获得可比的效果。将改程序所需要的数据集复制到svmMLiA.py所在的文件夹:
def img2vector(filename):
'''32×32的二进制图像矩阵转换成1×1024的向量'''
returnVect = np.zeros((1, 1024))
with open(filename, 'r') as fileObject:
for i in range(32):
lineStr = fileObject.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 = np.zeros((m, 1024))
for i in range(m):
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0])
if classNumStr == 9: #二分类,数字9标签为-1 其他的标签为+1
hwLabels.append(-1)
else:
hwLabels.append(1)
trainingMat[i, :] = img2vector(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 = np.mat(dataArr)
labelMat = np.mat(labelArr).transpose()
svInd = np.nonzero(alphas.A > 0)[0] #得到支持向量
sVs = datMat[svInd] #支持向量数据
labelSV = labelMat[svInd] #支持向量标签
print("there are ", np.shape(sVs)[0], " Support Vectors")
m, n = np.shape(datMat)
errorCount = 0
for i in range(m):
kernelEval = kernelTrans(sVs,datMat[i,:], kTup) #计算各个点的核
predict = kernelEval.T * np.multiply(labelSV, alphas[svInd]) + b #预测分类平面
if np.sign(predict) != np.sign(labelArr[i]):
errorCount += 1
print("the training error rate is: ", (float(errorCount) / m))
dataArr, labelArr = loadImages('testDigits')
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,:], kTup)
predict = kernelEval.T * np.multiply(labelSV, alphas[svInd]) + b
if np.sign(predict) != np.sign(labelArr[i]):
errorCount += 1
print("the test error rate is: ", (float(errorCount) / m))
进行测试如下,可以尝试不同的 σ \sigma σ值和不同的核函数观察变化:
支持向量机是一种分类器。之所以成为“机”是因为它会产生一个二值决策结果,即它是一种决策“机”。支持向量机的泛化错误率较低,也就是说它具有良好的学习能力,且学到的结果具有很好的推广性。
支持向量机试图通过求解一个二次优化问题来最大化分类间隔。
核方法或者说核技巧会将数据(有时是非线性数据)从一个低维空间映射到一个高维空间,可以将一个在低维空间中非线性问题转换成高维空间下的线性问题来求解。
支持向量机是一个二类分类器。当其解决多类问题时,则需要额外的方法对其进行扩展。SVM的效果也对优化参数和所用核函数的参数敏感。