支持向量机
定义:划分数据集的一种方法
1.基本概念
-
线性可分:在一幅二维散点图中很容易就将不同类别的数据分隔开
-
线性不可分:一条线没法分隔,比如种类a的点全部集中在中部形成一个圆,b类的点则包围在他外面,像这种可能需要用
核函数
或软间隔
来处理 -
分隔超平面:用一条直线(一维)能分隔二维数据,一个平面(二维)能分隔三维数据,同理,一个100维的数据需要99维的对象来分隔,用来分隔的对象就是分隔超平面,只是维数不一样(0~N维都可能)。最理想的状态是:用”分隔超平面“分出两个平台,两侧是不同类型的数据。离对象最近的点要与平面保持距离,以尽可能保证新加入的数据点能够进入正确的平台(坏值除外),并不是说找一条能把两类数据分开的线就结束。
像这张图里,对同一个数据集做划分,明显右边的分法会更合理些
-
支持向量:距离分隔超平面最近的那些点,其实决定
分隔超平面
的也就是这几个点了,也是“支持向量机”这个名字的由来
图中红色标注的几个点就是支持向量
-
数据划分:如何预测数据的属性呢?是在边界上方还是下方,可以通过 w T x + b ∣ ∣ w ∣ ∣ \frac{w^Tx+b}{||w||} ∣∣w∣∣wTx+b的大小来判断,以上图为例,若距离大于 d d d,则是黑点,反之为白点
2.支持向量机流程(实验处理的是线性样本)
-
收集、准备数据:测试的数据集有100个样本,形如(1.123,2.321,1),(1.000,2.000,-1)的数值型数据
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
-
分析数据:训练之前可以先看看数据的散点图怎么表示,如果是线性可分隔的话就比较轻松
-
训练算法:第一个函数用来随机挑选alpha;第二个函数则是通过限制alpha的上下限做修剪。
def selectJrand(i, m): j = i # 选择一个不等于i的j while (j == i): j = int(random.uniform(0, m)) return j def clipAlpha(aj, H, L): if aj > H: aj = H if L > aj: aj = L return aj
简化版SMO算法,把数据集转换成矩阵,初始化参数b和alpha。然后开始迭代训练,每次迭代遍历所有样本点,对它们做以下操作:计算每个点的误差 E i E_i Ei,优化alpha,根据容错率选择另一个与alpha_i成对优化的alpha_j;计算误差 E j E_j Ej以及上下界,再计算eta,当它是正数时退出循环,最后更新几个参数值,统计优化次数和更新迭代次数(当优化次数为0时,更新迭代次数,直到达到最大迭代次数为止)
def smoSimple(dataMatIn, classLabels, C, toler, maxIter): # 转换为numpy的mat存储 dataMatrix = np.mat(dataMatIn) labelMat = np.mat(classLabels).transpose() # 初始化b参数,统计dataMatrix的维度 b = 0 m, n = np.shape(dataMatrix) # 初始化alpha参数,设为0 alphas = np.mat(np.zeros((m, 1))) # 初始化迭代次数 iter_num = 0 # 最多迭代matIter次 while iter_num < maxIter: alphaPairsChanged = 0 for i in range(m): # 步骤1:计算误差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) # 步骤1:计算误差Ej fXj = float(np.multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[j, :].T)) + b Ej = fXj - float(labelMat[j]) # 保存更新前的aplpha值,使用深拷贝 alphaIold = alphas[i].copy() alphaJold = alphas[j].copy() # 步骤2:计算上下界L和H 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 # 步骤3:计算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 # 步骤4:更新alpha_j alphas[j] -= labelMat[j] * (Ei - Ej) / eta # 步骤5:修剪alpha_j alphas[j] = clipAlpha(alphas[j], H, L) if (abs(alphas[j] - alphaJold) < 0.00001): print("alpha_j变化太小"); continue # 步骤6:更新alpha_i alphas[i] += labelMat[j] * labelMat[i] * (alphaJold - alphas[j]) # 步骤7:更新b_1和b_2 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 # 步骤8:根据b_1和b_2更新b 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("第%d次迭代 样本:%d, alpha优化次数:%d" % (iter_num, i, alphaPairsChanged)) # 更新迭代次数 if (alphaPairsChanged == 0): iter_num += 1 else: iter_num = 0 print("迭代次数: %d" % iter_num) return b, alphas
-
测试、使用算法,把前面对数据集操作的结果用图像展示出来,画出决策边界并通过法线找到支持向量
def showClassifer(dataMat, w, b): # 绘制样本点 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], s=30, alpha=0.7) # 正样本散点图 plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[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 get_w(dataMat, labelMat, alphas): alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat) w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas) return w.tolist()
-
松弛变量设为0.6,容错率为1%,最大迭代次数定40
if __name__ == '__main__': dataMat, labelMat = loadDataSet('testSet.txt') b, alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40) w = get_w(dataMat, labelMat, alphas) showClassifer(dataMat, w, b)
3.核函数
主要用来解决非线性问题
通过将输入数据从原始空间映射到一个高维特征空间,使得原始数据在新的空间中更容易被线性分类器分离。
核函数的主要作用是通过计算两个样本之间的相似度来衡量它们在特征空间中的距离。
定义
假如给定一个空间X,核函数K(x,y)是一个A×A
的实数域的函数,它需要满足:
- 对于任意的
n×n
矩阵K,以及任意的n维向量a,存在 a T K a ≥ 0 a^T K a≥0 aTKa≥0,也就是说,核函数对应的核矩阵需要是一个半正定矩阵 - 核函数K(x,y)可以表示为两个向量的内积函数。即:存在一个高维特征空间H和一个映射函数φ(x)将输入空间X中的向量映射到特征空间H中,使得K(x, y) = φ(x)^T φ(y)
4.用SVM回顾手写体识别
实验步骤:
- 下载txt文本文件
- 把二值图像 转成 向量形式(比如黑色像素点映射为0,白色映射为1)
- 目测图像代表什么数字
- 训练+测试
代码均来自书本,这里仅解析测试函数testDigits()
的用法
##测试函数是一个用于SVM分类器的测试函数,这里先读入包含核函数信息的元组kTup
def testDigits(kTup=('rbf', 10)):
##从训练集(对象都是由0,1组成的txt文本)里读取数据矩阵和数据标签,后面要转成矩阵的形式再存储
dataArr, labelArr = loadImages('trainingDigits')
##用smoP函数执行“序列最小化”来训练SVM模型,返回SMO算法的b和alaph
b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10, kTup)
##把前面的dataArr, labelArr转成numpy矩阵,分别存储起来
datMat = np.mat(dataArr);
labelMat = np.mat(labelArr).transpose()
##根据前面alphas中的非零元素的索引,获得支持向量的索引,并将其存储于svInd中
##根据svlnd从datMat和labelMat中提取相应的支持向量数据,存储在sVs和labelSV
svInd = np.nonzero(alphas.A > 0)[0]
sVs = datMat[svInd]
labelSV = labelMat[svInd];
print("支持向量个数:%d" % np.shape(sVs)[0])
##获得矩阵datMat的形状,计算分类错误的样本数量
m, n = np.shape(datMat)
errorCount = 0
##每次循环的时候,计算当前样本和支持向量之间的核函数值,用这个值进行预测,不一样的话就error一次
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("训练集错误率: %.2f%%" % (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("测试集错误率: %.2f%%" % (float(errorCount) / m))
5.小结
SMO算法是SVM的优化,传统的SVM在求解时需要同时优化所有的支持向量,效率低下;而SMO每次循环只优化两个alpha,把原问题转化为一个二次规划问题,所以效率要高得多,它的核心思想是选择合适的两个变量进行优化,其他变量则是固定的。