提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、基于最大间隔分隔数据
线性模型
在二维空间中,当存在两类点,并且它们可以被一条直线完全分开时,我们称其为线性可分。如下图所示,在二维坐标系中,我们可以找到一条直线,将不同类别的样本点分开。这条直线可以作为线性模型的一种表示形式。
上述将数据集分隔开来的直线称为分隔超平面,即。
超平面
当数据点在二维平面上时,分隔超平面就是一条直线。但是,如果数据集是三维的,那么分隔数据的是一个平面。更高维的情况可以依此类推。例如,如果数据集是1000维的,则需要使用一个999维的对象来对数据进行分隔。当数据集是N维时,需要一个N-1维的对象来对数据进行分隔。N-1维的该对象被称为超平面,也就是分类的决策边界。超平面将数据空间分成两个部分,每个部分都对应于不同的类别。超平面可以用数学表达式表示为:w1x1 + w2x2 + ... + wNxN + b = 0,其中x1, x2, ..., xN是数据点的N个特征,w1, w2, ..., wN是超平面的法向量,b是偏移量。超平面的法向量与数据点之间的距离称为间隔。SVM的目标是找到一个最大间隔的超平面,即使得支持向量到超平面的距离最大化。支持向量是指距离超平面最近的数据点。通过最大化支持向量到超平面的距离,SVM可以得到一个具有较好泛化性能的分类模型。
支持向量
如下图,支持向量(support vector)就是离分隔超平面最近的那些点。
超平面方程:
import numpy as np
# 随机选择alpha
def selectJrand(i, m):
j = i
while j == i:
j = int(np.random.uniform(0, m))
return j
# 修剪alpha
def clipAlpha(aj, H, L):
if aj > H:
aj = H
if L > aj:
aj = L
return aj
# 类
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
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 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('无法识别的核函数类型')
return K
# 计算误差
def calcEk(oS, k):
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):
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)
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
# 计算Ek并更新误差缓存
def updateEk(oS, k):
Ek = calcEk(oS, k)
oS.eCache[k] = [1, Ek]
# 优化的SMO算法
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.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])
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
# 完整的线性SMO算法
def smoP(dataMatIn, classLabels, C, toler, maxIter, kTup=('lin', 0)):
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)):
alphaPairsChanged = 0
if entireSet:
for i in range(oS.m):
alphaPairsChanged += innerL(i, oS)
print("全样本遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
else:
nonBoundIs = np.nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(i, oS)
print("非边界遍历,第%d次迭代 样本:%d, alpha优化次数:%d" % (iter, i, alphaPairsChanged))
iter += 1
if entireSet:
entireSet = False
elif alphaPairsChanged == 0:
entireSet = True
print("迭代次数: %d" % iter)
return oS.b, oS.alphas
# 图像转换为向量
def img2vector(filename):
returnVect = np.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):
import os
hwLabels = []
trainingFileList = os.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:
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 = 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, :], kTup)
predict = kernelEval.T * np.multiply(labelSV, alphas[svInd]) + b
if np.sign(predict) != np.sign(labelArr[i]):
errorCount += 1
print("训练集错误率: %f" % (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("测试错误率: %f" % (float(errorCount) / m))
总结
SVM的关键思想是最大间隔,即找到将两个类别分开的最优超平面,使得该超平面对于新样本具有很好的泛化性能。为了处理非线性数据,SVM使用核函数将数据映射到高维空间中,从而使数据线性可分。
SVM的训练过程可以通过求解一个二次规划问题实现,但是当数据集非常大时,这种方法会变得非常耗时。为了加快训练速度,SMO算法被提出,并成为SVM的经典算法之一。该算法通过将二次规划问题分解为若干小的子问题,并在每个子问题上进行优化,从而实现了对大规模数据集的高效训练。
总之,SVM是一种强大的分类器,它不仅可以处理线性数据,还可以处理非线性数据,并且具有良好的泛化性能。它的基本思想是基于最大间隔原则,通过将数据映射到高维空间,并在该空间中找到最优超平面实现分类。