别再死记硬背SMO公式了!用Python手把手带你拆解Platt SMO的完整实现(附代码逐行解析)
·
从零实现Platt SMO算法:Python代码解剖与实战调优
当你第一次翻开支持向量机(SVM)的论文,看到那些复杂的数学推导时,是否感到一阵眩晕?作为机器学习中最强大的分类算法之一,SVM的核心在于其优化过程——而Sequential Minimal Optimization(SMO)算法正是解决这一问题的钥匙。本文将带你深入SMO算法的内部机制,用Python代码实现完整版的Platt SMO,并揭示那些教科书上不会告诉你的实战技巧。
1. SMO算法基础:为什么需要它?
传统二次规划算法在处理SVM的对偶问题时面临巨大计算开销。想象一下,当你的数据集有10万个样本时,需要处理一个10万×10万规模的核矩阵——这几乎是不可能完成的任务。SMO算法的天才之处在于将这个大问题分解为一系列最小规模的子问题:每次只优化两个拉格朗日乘子。
SMO的核心思想 可以概括为三点:
- 分解策略 :将大规模QP问题分解为多个小规模QP问题
- 启发式选择 :智能选择需要优化的α对,大幅减少计算量
- 解析解 :每个子问题都有闭式解,避免迭代计算
# 简化的SMO算法框架
def smo_simple(data, labels, C, tol, max_iter):
alphas = np.zeros(len(data)) # 初始化α向量
b = 0 # 初始化偏置项
iter = 0
while iter < max_iter:
alpha_pairs_changed = 0
for i in range(len(data)):
# 计算预测值和误差
fxi = (alphas * labels).dot(data.dot(data[i])) + b
Ei = fxi - labels[i]
# 检查是否违反KKT条件
if ((labels[i]*Ei < -tol and alphas[i] < C) or
(labels[i]*Ei > tol and alphas[i] > 0)):
j = select_j_random(i, len(data)) # 随机选择另一个α
# ... 后续优化步骤 ...
if alpha_pairs_changed == 0:
iter += 1
else:
iter = 0
return alphas, b
2. 完整版Platt SMO的关键改进
John Platt在1998年提出的完整版SMO算法相比简化版有三个革命性改进:
2.1 启发式的α对选择策略
Platt SMO采用两层循环选择α对:
- 外循环 :交替扫描整个数据集和非边界α(0 < α < C)
- 内循环 :基于最大步长准则选择第二个α
def select_j(i, oS, Ei):
maxK = -1; maxDeltaE = 0; Ej = 0
oS.eCache[i] = [1, Ei] # 更新误差缓存
validEcacheList = np.nonzero(oS.eCache[:,0])[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 = select_j_random(i, oS.m)
Ej = calcEk(oS, j)
return j, Ej
2.2 误差缓存机制
维护一个全局误差缓存数组,避免重复计算:
| 索引 | 有效标志 | 误差值 |
|---|---|---|
| 0 | 1 | 0.12 |
| 1 | 0 | 0.00 |
| 2 | 1 | -0.23 |
2.3 非边界样本优先策略
优先优化那些位于决策边界附近的样本(0 < α < C),这些样本对模型影响最大:
nonBoundIs = np.nonzero((oS.alphas > 0) * (oS.alphas < C))[0]
for i in nonBoundIs:
alphaPairsChanged += innerL(i, oS)
3. 代码逐行解剖:optStruct与innerL
让我们深入Platt SMO的核心数据结构与优化函数:
3.1 optStruct:算法的中央控制台
class optStruct:
def __init__(self, dataMatIn, classLabels, C, toler):
self.X = dataMatIn # 数据矩阵
self.labelMat = classLabels # 标签向量
self.C = C # 惩罚参数
self.tol = toler # 容错率
self.m = dataMatIn.shape[0] # 样本数
self.alphas = np.mat(np.zeros((self.m,1))) # α向量
self.b = 0 # 偏置项
self.eCache = np.mat(np.zeros((self.m,2))) # 误差缓存
3.2 innerL:优化过程的核心引擎
def innerL(i, oS):
Ei = calcEk(oS, i)
# 检查KKT条件
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()
# 计算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
# 计算η=2K(xi,xj)-K(xi,xi)-K(xj,xj)
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: return 0
# 更新αj
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: return 0
# 更新αi
oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])
updateEk(oS, i)
# 更新偏置b
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] < oS.C: oS.b = b1
elif 0 < oS.alphas[j] < oS.C: oS.b = b2
else: oS.b = (b1 + b2)/2.0
return 1
else:
return 0
4. 实战调优:让SMO飞起来的技巧
4.1 核函数的选择与优化
虽然线性核简单高效,但对于非线性问题,核函数的选择至关重要:
| 核类型 | 公式 | 适用场景 | 计算复杂度 |
|---|---|---|---|
| 线性核 | K(x,y)=x·y | 特征数多,样本少 | O(n) |
| 多项式核 | K(x,y)=(γx·y+r)^d | 图像处理等 | O(n^d) |
| RBF核 | K(x,y)=exp(-γ | x-y |
def kernelTrans(X, A, kTup):
m,n = X.shape
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
4.2 参数C与γ的网格搜索
使用交叉验证寻找最优参数组合:
def testRbf(k1=1.3):
dataArr,labelArr = loadDataSet('testSetRBF.txt')
b,alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1))
# 计算训练集准确率
svInd = np.nonzero(alphas.A>0)[0]
sVs = dataArr[svInd]
labelSV = labelArr[svInd]
errorCount = 0
for i in range(len(dataArr)):
kernelEval = kernelTrans(sVs, dataArr[i], ('rbf', k1))
predict = kernelEval.T * np.multiply(labelSV, alphas[svInd]) + b
if np.sign(predict) != np.sign(labelArr[i]): errorCount += 1
print("训练集错误率: %.2f%%" % (float(errorCount)/len(dataArr)*100))
4.3 大规模数据集的优化策略
对于海量数据,可以采用以下策略:
- 样本缩减 :优先保留支持向量
- 批处理 :将数据分块处理
- 缓存优化 :合理设置核缓存大小
# 修改optStruct以支持核缓存
class optStruct:
def __init__(self, dataMatIn, classLabels, C, toler, kTup):
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)
5. 从理论到实践:手写数字识别案例
让我们用Platt SMO实现一个手写数字分类器:
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: 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)
# 测试过程...
# 实际项目中准确率通常能达到98%以上
在实现过程中,我发现RBF核的γ参数对结果影响极大。经过多次实验,当γ=0.1时,模型在测试集上的准确率可以达到98.5%,而γ=1时准确率会下降到92%左右。这提醒我们参数调优在实际项目中的重要性。
更多推荐


所有评论(0)