什么是SVM
SVM(全称Support Vector Machine)中文名支持向量机。SVM是一种监督机器学习算法,是一种二分类模型,它的目的是寻找一个超平面来对样本进行分割,分割的原则是间隔最大化,最终转化为一个凸二次规划问题来求解。可用于分类或回归挑战。然而,它主要用于分类问题。
最大间隔与分类
如果一个线性函数能够将样本分开,称这些数据样本是线性可分的。那么什么是线性函数呢?其实很简单,在二维空间中就是一条直线,在三维空间中就是一个平面,以此类推,如果不考虑空间维数,这样的线性函数统称为超平面。我们看一个简单的二维空间的例子,+代表正类,-代表负类,样本是线性可分的,但是很显然不只有这一条直线可以将样本分开,而是有无数条,我们所说的线性可分支持向量机就对应着能将数据正确划分并且间隔最大的直线。
在样本空间中寻找一个超平面, 将不同类别的样本分开。
最大化间隔: 寻找参数w和b , 使得下述公式最大:
最大间隔问题的拉格朗日乘法
第一步:引入拉格朗日乘子ai≥ 0得到拉格朗日函数
第二步:令L(w,b,a)对w和b的偏导为零
第三步:w, b回代到第一步
对偶问题
1.等式约束
2.不等式约束的KKT条件
3.KKT
SMO高效优化算法
SMO算法的目标是求出一系列alpha和b,一旦求出这些alpha,就很容易算出权重向量w并得到分割超平面。
SMO算法工作原理:每次循环中选择两个alpha进行优化处理。一旦找到一对合适的alpha,那么就增大其中一个,减小另外一个。
算法流程:每次选取两个a进行更新
编程求解线性SVM
通过简约版线性SMO算法进行支持向量机分类:
部分数据集展示:
运行代码
`# -- coding:UTF-8 --
import matplotlib.pyplot as plt
import numpy as np
import random
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 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
def showDataSet(dataMat, labelMat):
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()
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(dataMatrixdataMatrix[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(dataMatrixdataMatrix[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 LH: print("LH"); 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- a1x1)/a2, (-b - a1x2)/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()
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)
`
运行结果:
可以看到红色圈圈圈起来点是支持向量点。
通过SMO-SVM实现对鸢尾花数据集的二分类
`
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
鸢尾花(iris)数据集
数据集内包含 3 类共 150 条记录,每类各 50 个数据,
每条记录都有 4 项特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度,
可以通过这4个特征预测鸢尾花卉属于(iris-setosa, iris-versicolour, iris-virginica)中的哪一品种。
这里只取前100条记录,两项特征,两个类别。
def create_data():
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
data = np.array(df.iloc[:100, [0, 1, -1]])
for i in range(len(data)):
if data[i,-1] == 0:
data[i,-1] = -1
return data[:,:2], data[:,-1]
使用RBF(Radial basis function)核函数处理
def K(x,z,sigma=1.5):
return np.exp(np.dot((x-z),(x-z).T)/-(2sigma**2))
对应课本147页的g(x_i),该函数助于验证KKT条件
def g(i,x,y,alpha,b):
sum=b
for j in range(len(y)):
sum+=alpha[j]y[j]K(x[i],x[j])
return sum
验证第i个样本点是否满足KKT条件
def isKKT(alpha,i,x,y,b,C):
if alpha[i]==0 and y[i]g(i,x,y,alpha,b)>=1:
return True
elif alpha[i]C and y[i]g(i,x,y,alpha,b)<=1:
return True
elif alpha[i]>0 and alpha[i]<C and y[i]g(i,x,y,alpha,b)1:
return True
else:
return False
验证第i个样本点违反KKT条件的程度。由于KKT条件和y_ig(x_i)与1的不等式有关
因此计算y_ig(x_i)与1之间差值的绝对值作为衡量违反程度的标准
def vioKKT(alpha,i,x,y,b):
return abs(y[i]g(i,x,y,alpha,b)-1)
在数组x中找到值为a的元素第一次出现的位置
def findindex(x,a):
for i in range(len(x)):
if x[i]==a:
return i
某个样本点分类误差函数
def E(w,b,x_k,y_k):
predi_k=int(np.sign(np.dot(w,x_k.T)+b))
return predi_k-y_k
计算样本点与分割直线的距离
def distance_count(x,w,b):
return abs(w[0]x[0]+w[1]*x[1]+b) / np.sqrt(w[0]2 + w[1]2)
from functions import *
from numpy import *
from random import *
from matplotlib import pyplot as plt
def train(C=1.0):
#获得数据集
x,y=create_data()
#设定迭代次数为100次
iter=100
#样本容量也就是标签的个数
N=len(y)
#alpha的初始值取全0
alpha=zeros(len(y))
#设置i,j的初始值(对应alpha1和alpha2)
i,j=randint(0,N-1),randint(0,N-1)
#保证i≠j
while ij:
i=randint(0,N-1)
for k in range(iter):
#x的尺寸为一个1×2行向量
x_i,x_j=x[i],x[j]
#y的取值为+1或-1
y_i,y_j=y[i],y[j]
#计算ita,为计算a2_newunc做准备
ita=K(x_i,x_i)+K(x_j,x_j)-2*K(x_i,x_j)
if ita0:
continue
#计算分割平面参数w与b
#x:100×2矩阵,w:1×2矩阵
#由于y-dot(w,x.T)是个与y等长的行向量,取其各元素平均值
w=dot(alphay,x)
b=mean(y-dot(w,x.T))
#计算误差E1和E2
E_i=E(w,b,x_i,y_i)
E_j=E(w,b,x_j,y_j)
#计算a2_ewunc
a1_old=alpha[i]
a2_old=alpha[j]
a2_newunc=a2_old+y_j(E_i-E_j)/ita
#计算L与H
L,H=0.0,0.0
if y_i!=y_j:
L=max(0,a2_old-a1_old)
H=min(C,C+a2_old-a1_old)
elif y_iy_j:
L=max(0,a2_old+a1_old-C)
H=min(C,a2_old+a1_old)
#计算剪辑后a2_new与a1_new的值
a2_new=max(L,min(H,a2_newunc))
a1_new=a1_old+y_iy_j(a2_old-a2_new)
#更新alpha
alpha[i],alpha[j]=a1_new,a2_new
#violation表示每个元素违反KKT条件的程度
violation=zeros(N)
#对每一个样本点检验KKT条件,在violation内记录每个样本点违反KKT的程度
for k in range(N):
if isKKT(alpha,k,x,y,b,C)False:
violation[k]=float(vioKKT(alpha,k,x,y,b))
#如果没有违反KKT条件,则违反程度是0
else:
violation[k]=0.0
#找到violation中违反程度最大的点,设定为i,对应alpha_1
i=findindex(violation,max(violation))
#这里设置j(对应alpha_2)为不等于i的随机数。
#原本alpha_2的选取应该是令abs(E_i-E_k)最大的k值对应的alpha点
#经过测试,在大多数情况下,abs(E_i-E_k)(1×100向量)的所有元素都是0
#即预测每个元素都准确,每个元素的分类误差都是0,误差的差值也是0
#只有少数情况下,会有一个误差差值不等于0
#对于前一种情况,无所谓“最大的误差差值”(因为都是0),因此只能设置j为随机数
#对于后一种情况,由于出现的次数少,并且那一个不为0的差值的元素出现的位置具有随机性
#因此总是将j设定为随机数
j=randint(0,N-1)
while ji:
j = randint(0, N - 1)
#计算最终(迭代100次)分割平面参数
w = dot(alpha * y, x)
b = mean(y - dot(w, x.T))
draw_x, draw_y, draw_label = [], [], []
#在散点图上标记样本点的位置,样本点第一个元素作为x坐标,第二个元素作为y坐标
for p in x:
draw_x.append(p[0])
draw_y.append(p[1])
#画散点图,其中支持向量呈现绿色,正类呈现红色,负类呈现蓝色
#样本点离分割直线最近的为支持向量
distance=zeros(len(y))
for i in range(len(y)):
distance[i]=distance_count(x[i],w,b)
vector=findindex(distance,min(distance))
for i in range(len(y)):
if ivector:
draw_label.append('g')
else:
if y[i] > 0:
draw_label.append('r')
else:
draw_label.append('b')
plt.scatter(draw_x, draw_y, color=draw_label)
plain_x = range(4, 8, 1)
plain_y = []
for i in plain_x:
temp = double(-(w[0] * i + b) / w[1])
plain_y.append(temp)
plt.plot(plain_x, plain_y)
#最终绘图
plt.savefig('SMO.jpg')
plt.show()
if name == 'main':
train()
`
总结
SVM优点
1、解决小样本下机器学习问题。
2、解决非线性问题。
3、无局部极小值问题。(相对于神经网络等算法)
4、可以很好的处理高维数据集。
5、泛化能力比较强。
SVM缺点
1、对于核函数的高维映射解释力不强,尤其是径向基函数。
2、对缺失数据敏感。
3、调参麻烦,核函数难选难调