在上个版本的基础上修订,改正了一些错误,优化了一些代码结构,修改了一些注释。
不厌其烦的详细注释,对入门来说还是挺友好的哈,欢迎指正!
测试数据是吴恩达机器学习课程svm章节的作业。
分别用高斯核函数与线性核函数进行测试,可以发现高斯核函数的稳定性和准确率明显较线性核函数好。
import numpy as np
import pandas as pd
from scipy.io import loadmat
class SVM():
def __init__(self, C=1, toler=0.001, maxIter=500, kernel_option=('', 1)):
'''
初始化参数
:param C: 正则化参数
:param toler: 容错率
:param maxIter: 最大迭代次数
:param kenel_option: 选择的核函数
'''
self.C = C
self.toler = toler
self.maxIter = maxIter
self.kernel_opt = kernel_option
def cal_kernel_value(self, X, X_i, kernel_option):
'''
计算所有样本跟第i个样本的核函数的值,返回m*1的矩阵. 使用np的矩阵运算, 格式都为np.matrix
:param X: 特征集m*n,m为样本数,n为特征数
:param X_i: 第i个样本1*n
:param kernel_option: 所选择的核函数(高斯核函数或线性核函数)
:return: 第i个样本与所有样本的核函数的值,m*1
'''
m = X.shape[0]
kenel_value = np.mat(np.zeros((m, 1))) # 定义返回矩阵
if kernel_option[0] == 'rbf': # 计算高斯核函数
sigma = kernel_option[1] # 高斯函数的参数值,默认为1
for i in range(m): # 根据高斯函数的公式,逐个更新
diff = X[i, :] - X_i
kenel_value[i] = np.exp(np.dot(diff, diff.T) / (-2 * sigma**2))
else: # 计算线性核函数
kenel_value = np.dot(X, X_i.T) # 一次性更新
return kenel_value
def cal_kernel(self, X, kernel_option):
'''
计算所有样本相互之间的核函数的值.返回一个m*m的矩阵M,第i个样本和第j个样本的值=M[i,j]=M[j,i],0<=i,j<m
:param X: 特征集m*n,m为样本数,n为特征数
:param kenel_option: 所选择的核函数(高斯核函数或线性核函数)
:return: m*m的矩阵M,储存个样本间的核函数的值
'''
m = X.shape[0]
kernel_matrix = np.mat(np.zeros((m, m))) # 定义返回矩阵
for i in range(m): # 逐列更新
kernel_matrix[:, i] = self.cal_kernel_value(X, X[i, :], kernel_option)
return kernel_matrix
def training(self, X, y):
'''
训练模型,得到拉格朗日乘子alpha和偏差b.使用矩阵计算.
:param X: 特征集m*n,m为样本数,n为特征数
:param y: 目标集m*1
:return: 无返回值,通过更新类变量来得到训练好的模型参数
'''
self.X = np.mat(X) # 特征集
self.y = np.mat(y) # 目标集
self.m = self.X.shape[0] # 样本数
self.alpha = np.mat(np.zeros((self.m, 1))) # 拉格朗日乘子
self.b = 0 # 偏差
self.Ecache = np.mat(np.zeros((self.m, 2))) # 存储E的矩阵.E为预测值与目标值的差,Ei=h(xi)-yi. 分2列,第一列作为标记(默认为0,如果更新过,则设值为1),第二列存储值
self.kernel_matrix = self.cal_kernel(self.X, self.kernel_opt) # 计算并储存核函数矩阵
switch = True # 用于控制全遍历或局部遍历的开关,局部遍历指,只遍历支持向量.True全遍历,False局部遍历
alpha_changed = 0 # 用于记录所有alpha在本次迭代中,是否有所改变,若值为0,说明都无改变,若值大于0,说明存在改变,执行下一次迭代
iter = 0 # 用于记录迭代次数
while iter < self.maxIter and (alpha_changed > 0 or switch): # 当迭代轮次超过最大值或者 遍历全集后alpha值无变化, 则跳出外循环,训练结束
alpha_changed = 0 # 每次迭代,重置为0
if switch: # 全遍历,验证每个样本
for i in range(self.m):
alpha_changed += self.innerL(i) # innerL返回0或1,分别表示无变化和有变化.只要有一个alpha发生过变化,则认为整个alpha集发生变化
iter += 1 # 一次更新完毕,迭代次数+1
else: # 全遍历后,再遍历所有支持向量,直到所有支持向量的alpha无变化,再进行全遍历.如果此次全遍历,整个alpha集都无变化,则训练结束,否则再次遍历支持向量,如此循环.
bound_alpha = [i for i, a in enumerate(self.alpha) if 0 < a < self.C] # 获取所有支持向量的索引
for i in bound_alpha:
alpha_changed += self.innerL(i)
iter += 1
if switch: # 全遍历后,进入支持向量的遍历
switch = False
elif alpha_changed == 0: # 支持向量遍历后,如果所有支持向量的alpha无变化,则进行全遍历
switch = True
print('Total Iter:', iter)
return
def Jrand(self, i): # 获取与i不同的随机的索引值
j = i
while j == i:
j = np.random.randint(0, self.m)
return j
def clip_alpha(self, alpha, L, H): # 裁剪alpha
if alpha > H: return H
if alpha < L: return L
return alpha
def cal_E(self, i): # 计算Ei=h(xi)-yi
hxi = float(np.dot(self.kernel_matrix[i, :], np.multiply(self.alpha, self.y)) + self.b)
return hxi - float(self.y[i])
def update_E(self, i): # 更新Ecache中Ei的值和标识
Ei = self.cal_E(i)
self.Ecache[i] = [1, Ei]
return
def select_second_alpha(self, i, Ei): # 选取第二个alpha变量
j, Ej, maxsteps = 0, 0, 0
self.Ecache[i] = [1, Ei] # 更新Ecache
validE = np.nonzero(self.Ecache[:, 0])[0] # 获取所有更新过值的E的索引
if len(validE) > 1:
for k in validE:
if k == i: continue
Ek = self.cal_E(k)
deltaE = abs(Ek - Ei)
if deltaE > maxsteps: # 获取abs(Ej-Ei)最大的Ej,这样可以加快迭代的幅度,以尽快抵达终点
j = k
Ej = Ek
maxsteps = deltaE
else: # 第一次遍历,随机选取一个不同与i的索引
j = self.Jrand(i)
Ej = self.cal_E(j)
return j, Ej
def innerL(self, i): # 内部循环,判断i样本是否满足KKT条件,满足则返回0,若不满足,则更新alpha[i]和alpha[j],更新成功返回1,更新失败返回0
Ei = self.cal_E(i)
r = self.y[i] * Ei # 拆开来,等价于 y(wx+b)-1
if (self.alpha[i] < self.C and r < -self.toler) or (self.alpha[i] > 0 and r > self.toler): # 如果没有容错率,则分别是 r<0 和 r>0. 容错率的意思是,在(-toler,toler)之间的点,就当做是满足KKT条件了,而放过不做优化
j, Ej = self.select_second_alpha(i, Ei) # 选取第二个alpha变量
alphaIold = self.alpha[i].copy() # 定义改变前的alpha
alphaJold = self.alpha[j].copy()
# 根据0<=alpha<=C,计算alpha的上下边界
if self.y[i] == self.y[j]: # 分目标值相等或不等两种情况
L = max(0, alphaIold + alphaJold - self.C)
H = min(self.C, alphaIold + alphaJold)
else:
L = max(0, alphaJold - alphaIold)
H = min(self.C, self.C + alphaJold - alphaIold)
if L == H: # 这种情况,意味着alpha不会再改变,直接返回0
return 0
eta = self.kernel_matrix[i, i] + self.kernel_matrix[j, j] - 2 * self.kernel_matrix[i, j] # alphaj的二阶导
if eta <=0: # eta是alphaj的二阶导数.根据二阶导数性质,只有当二阶导数>0时,原函数才能取到最小值. 所以小于等于0时,直接返回0
return 0
alphaJnew = alphaJold + self.y[j] * (Ei - Ej) / eta # 根据推导过程中的公式,计算新的alphaj
alphaJnew = self.clip_alpha(alphaJnew, L, H) # 裁剪alphaj
if abs(alphaJnew - alphaJold) < 0.00001: # 如果变化量太小,也视为没有改变,返回0
return 0
alphaInew = alphaIold + self.y[i] * self.y[j] * (alphaJold - alphaJnew) # 根据推导过程中的公式,计算新的alphai
bi = float(-Ei + self.y[i] * self.kernel_matrix[i, i] * (alphaIold - alphaInew) + self.y[j] * self.kernel_matrix[i, j] * (alphaJold - alphaJnew) + self.b)
bj = float(-Ej + self.y[i] * self.kernel_matrix[i, j] * (alphaIold - alphaInew) + self.y[j] * self.kernel_matrix[j, j] * (alphaJold - alphaJnew) + self.b)
if 0 < alphaInew < self.C: # 如果alphaInew是支持向量,那么根据公式,此时bi=b
self.b = bi
elif 0 < alphaJnew < self.C: # 同理
self.b = bj
else: # 如果都不是支持向量,取均值
self.b = (bi + bj) / 2
self.alpha[i] = alphaInew # 更新alphai
self.alpha[j] = alphaJnew
self.update_E(i) # 更新Ecache
self.update_E(j)
return 1
return 0
def predict(self, X):
'''
先找出alpha>0的索引,得到相应的训练样本(根据公式,只有alpha>0的样本才对预测结果产生影响),再使用这些训练样本与待测样本进行核函数计算,得到核函数矩阵,最后利用h(xi)的公式算出结果
注意,利用alpha的值计算出权重w,再用h(xi)=wx+b的公式计算预测结果的方式,只针对线性核函数有效.而不能用于其他核函数.
:param X: 待测样本p*n,p为样本数,n为特征数
:param kernel_option: 所选择的核函数(高斯核函数或线性核函数)
'''
X = np.mat(X)
alpha_nonzero = np.nonzero(self.alpha)[0] # 获取非零alpha的索引
uesful_alpha = self.alpha[alpha_nonzero, :] # 截取非零alpha,以下同理
useful_X = self.X[alpha_nonzero, :]
useful_y = self.y[alpha_nonzero, :]
p, q = X.shape[0], useful_X.shape[0] # p,q分别为待测样本数,和选取的训练样本数
kernel_mat = np.mat(np.zeros((p, q))) # 定义待测样本与训练样本的核函数矩阵
for i in range(p): # 更新核函数矩阵的值
kernel_mat[i, :] = self.cal_kernel_value(useful_X, X[i, :], self.kernel_opt).T
pred = np.dot(kernel_mat, np.multiply(uesful_alpha, useful_y)) + self.b # 根据公式计算预测结果
return [1 if x >= 0 else -1 for x in pred]
def accuracy(self, X, y):
'''
计算预测准确率
:param X: 待测样本p*n,p为样本数,n为特征数
:param y: 待测样本的标签值p*1
:return: 预测准确率
'''
predictions = self.predict(X) # 计算预测值
correct = [1 if a == b else 0 for a, b in zip(predictions, y)] # 预测值与原值相等则为1,否则0
return correct.count(1) / len(correct)
def test():
row_data = loadmat('data/ex6data1.mat')
data = pd.DataFrame(row_data['X'], columns=['X1', 'X2'])
data['y'] = row_data['y']
X = np.array(data[['X1', 'X2']])
y = np.array(data['y'])
y = np.array([1 if x == 1 else -1 for x in y]).reshape((y.shape[0], 1))
svm1 = SVM(kernel_option=('', 0)) # 线性核函数
svm2 = SVM(kernel_option=('rbf', 1)) # 高斯核函数
svm1.training(X, y)
svm2.training(X, y)
print(svm1.accuracy(X, y))
print(svm2.accuracy(X, y))
test()