最近学习了一下svm这一高大上的算法,为了充分理解这一算法,特地详细地查看了一下相应程序的源代码,这里将源代码简单地记录一下,方便日后更好地理解与提升。
源代码对应的网址链接
这里假设我们的mnist_train中train.csv存在的数据为
1,2,3,4,5
2,3,4,5,1
假设mnist_test中test.csv存在的数据为
2,3,4,5,1
1,2,3,4,5
首先使用数组读出文件中对应的csv文件
trainDataList, trainLabelList = loadData(r'C:\Users\xiaoguzai\Desktop\Statistical-Learning-Method_Code-master\Mnist\mnist_train\train.csv')
def loadData(fileName):
'''
加载文件
:param fileName:要加载的文件路径
:return: 数据集和标签集
'''
#存放数据及标记
dataArr = []; labelArr = []
#读取文件
fr = open(fileName)
#遍历文件中的每一行
for line in fr.readlines():
#获取当前行,并按“,”切割成字段放入列表中
#strip:去掉每行字符串首尾指定的字符(默认空格或换行符)
#split:按照指定的字符将字符串切割成每个字段,返回列表形式
curLine = line.strip().split(',')
#将每行中除标记外的数据放入数据集中(curLine[0]为标记信息)
#在放入的同时将原先字符串形式的数据转换为0-1的浮点型
dataArr.append([int(num) / 255 for num in curLine[1:]])
#将标记信息放入标记集中
#放入的同时将标记转换为整型
#数字0标记为1 其余标记为-1
#!!!注意坐标为0的数据要被拿来进行标记,所以dataArr放入的是
#从坐标1开始的数据
if int(curLine[0]) == 0:
labelArr.append(1)
else:
labelArr.append(-1)
#返回数据集和标记
return dataArr, labelArr
第一个数字除以255的结果作为标记,如果除的结果为0的时候标记为1,其余时候标记为-1。
trainDataList =
[[0.00784313725490196, 0.011764705882352941, 0.01568627450980392, 0.0196078431372549], [0.011764705882352941, 0.01568627450980392, 0.0196078431372549, 0.00392156862745098]]
label =
[-1, -1]
接下来取出testDataList中的对应数值,同理可得相应的数值
testSet =
[[0.011764705882352941, 0.01568627450980392, 0.0196078431372549, 0.00392156862745098], [0.00784313725490196, 0.011764705882352941, 0.01568627450980392, 0.0196078431372549]]
label = [-1,-1]
最后判断trainDataList与testSet的结果是否相同,初始化的时候trainDataMat = trainDataList,trainLabelMat = trainLabelList,
m,n = 对应的2,sigma对应的值为10,C对应的值为200,toler对应的值为0,(C为惩罚参数,toler为对应的松弛变量,这些都是一开始程序设定好的对应的初始化的参数)
接下来调用calcKernel计算对应的核函数
def calcKernel(self):
print('calcKernel')
'''
计算核函数
使用的是高斯核 详见“7.3.3 常用核函数” 式7.90
:return: 高斯核矩阵
'''
#初始化高斯核结果矩阵 大小 = 训练集长度m * 训练集长度m
#k[i][j] = Xi * Xj
k = [[0 for i in range(self.m)] for j in range(self.m)]
#大循环遍历Xi,Xi为式7.90中的x
for i in range(self.m):
#每100个打印一次
#不能每次都打印,会极大拖慢程序运行速度
#因为print是比较慢的
if i % 100 == 0:
print('construct the kernel:', i, self.m)
#得到式7.90中的X
X = self.trainDataMat[i, :]
#小循环遍历Xj,Xj为式7.90中的Z
# 由于 Xi * Xj 等于 Xj * Xi,一次计算得到的结果可以
# 同时放在k[i][j]和k[j][i]中,这样一个矩阵只需要计算一半即可
#所以小循环直接从i开始
for j in range(i, self.m):
#获得Z
Z = self.trainDataMat[j, :]
#先计算||X - Z||^2
result = (X - Z) * (X - Z).T
#分子除以分母后去指数,得到的即为高斯核结果
result = np.exp(-1 * result / (2 * self.sigma**2))
#将Xi*Xj的结果存放入k[i][j]和k[j][i]中
k[i][j] = result
k[j][i] = result
#返回高斯核矩阵
return k
这里采用循环的方式进行处理,
1.X=[[0.00784314 0.01176471 0.01568627 0.01960784]],Z=[[0.00784314 0.01176471 0.01568627 0.01960784]],X-Z=[[0 0 0 0]],此时使用公式
result = (X - Z) * (X - Z).T
#分子除以分母后去指数,得到的即为高斯核结果
result = np.exp(-1 * result / (2 * self.sigma**2))
得出对应的结果k[0][0]=1,k[0][0]=1,
接着Z=[[0.01176471 0.01568627 0.01960784 0.00392157]]再进行第二次的循环,
X-Z=[[-0.00392157 -0.00392157 -0.00392157 0.01568627]],此时k[0][1] = 0.9999,k[1][0] = 0.9999,
2.X=[[0.01176471 0.01568627 0.01960784 0.00392157]],此时Z=[[0.01176471 0.01568627 0.01960784 0.00392157]],X-Z=[[0 0 0 0]],此时对应的结果k[1][1] = 1,k[1][1] = 1,
所以形成的结果矩阵result=
1.0000 0.9999
0.9999 1.0000
因为有如下的语句
self.k = self.calcKernel()
所以计算得出的矩阵k为
1.0000 0.9999
0.9999 1.0000
接下来运行的语句为
self.alpha = [0] * self.trainDataMat.shape[0] # α 长度为训练集数目
计算得出的alpha = [0,0]
运行语句
self.E = [0 * self.trainLabelMat[i, 0] for i in range(self.trainLabelMat.shape[0])] #SMO运算过程中的Ei
计算得出的E = [0,0]
总结出目前得到的各项对应的指数
trainDataMat = [[0.00784313725490196, 0.011764705882352941, 0.01568627450980392, 0.0196078431372549], [0.011764705882352941, 0.01568627450980392, 0.0196078431372549, 0.00392156862745098]]
testSet = [[0.011764705882352941, 0.01568627450980392, 0.0196078431372549, 0.00392156862745098], [0.00784313725490196, 0.011764705882352941, 0.01568627450980392, 0.0196078431372549]]
self.m = 2,self.n = 2,self.sigma = 10
self.C = 100,self.toler = 0
self.k =
[1.0000 0.9999
0.9999 1.0000]
self.E = [0,0]
self.alpha = [0,0]
初始化完各项指数之后,使用svm.train()来对相应的指数进行训练
def train(self, iter = 100):
#iterStep:迭代次数,超过设置次数还未收敛则强制停止
#parameterChanged:单次迭代中有参数改变则增加1
iterStep = 0; parameterChanged = 1
#如果没有达到限制的迭代次数以及上次迭代中有参数改变则继续迭代
#parameterChanged==0时表示上次迭代没有参数改变,如果遍历了一遍都没有参数改变,说明
#达到了收敛状态,可以停止了
while (iterStep < iter) and (parameterChanged > 0):
#打印当前迭代轮数
#迭代步数加1
iterStep += 1
#新的一轮将参数改变标志位重新置0
parameterChanged = 0
#大循环遍历所有样本,用于找SMO中第一个变量
for i in range(self.m):
#查看第一个遍历是否满足KKT条件,如果不满足则作为SMO中第一个变量从而进行优化
if self.isSatisfyKKT(i) == False:
#page147页,如果下标为i的α不满足KKT条件,则进行优化
#第一个变量α的下标i已经确定,接下来按照“7.4.2 变量的选择方法”第二步
#选择变量2。由于变量2的选择中涉及到|E1 - E2|,因此先计算E1
E1 = self.calcEi(i)
#算出第一个对应的期望值Ei
#选择第2个变量
E2, j = self.getAlphaJ(E1, i)
#算出第二个对应的期望值a
#参考“7.4.1两个变量二次规划的求解方法” P126 下半部分
#获得两个变量的标签
y1 = self.trainLabelMat[i]
y2 = self.trainLabelMat[j]
#复制α值作为old值
alphaOld_1 = self.alpha[i]
alphaOld_2 = self.alpha[j]
#依据标签是否一致来生成不同的L和H
#使用page145页的公式
if y1 != y2:
L = max(0, alphaOld_2 - alphaOld_1)
H = min(self.C, self.C + alphaOld_2 - alphaOld_1)
else:
L = max(0, alphaOld_2 + alphaOld_1 - self.C)
H = min(self.C, alphaOld_2 + alphaOld_1)
#如果两者相等,说明该变量无法再优化,直接跳到下一次循环
if L == H: continue
#计算α的新值
#依据“7.4.1两个变量二次规划的求解方法”式7.106更新α2值
#先获得几个k值,用来计算事7.106中的分母η
k11 = self.k[i][i]
k22 = self.k[j][j]
k21 = self.k[j][i]
k12 = self.k[i][j]
#这里取出来的系数正是对应着的矩阵中的各项参数
#依据式page145中的7.106更新α2,该α2还未经剪切
alphaNew_2 = alphaOld_2 + y2 * (E1 - E2) / (k11 + k22 - 2 * k12)
#剪切α2
#利用梯度来优化相应的参数
#依据式page145中的7.108更新α2
if alphaNew_2 < L: alphaNew_2 = L
elif alphaNew_2 > H: alphaNew_2 = H
#更新α1,依据page145中的式7.109
alphaNew_1 = alphaOld_1 + y1 * y2 * (alphaOld_2 - alphaNew_2)
#依据“7.4.2 变量的选择方法”第三步式7.115和7.116计算b1和b2
b1New = -1 * E1 - y1 * k11 * (alphaNew_1 - alphaOld_1) \
- y2 * k21 * (alphaNew_2 - alphaOld_2) + self.b
b2New = -1 * E2 - y1 * k12 * (alphaNew_1 - alphaOld_1) \
- y2 * k22 * (alphaNew_2 - alphaOld_2) + self.b
#依据α1和α2的值范围确定新b
if (alphaNew_1 > 0) and (alphaNew_1 < self.C):
bNew = b1New
elif (alphaNew_2 > 0) and (alphaNew_2 < self.C):
bNew = b2New
else:
bNew = (b1New + b2New) / 2
#将更新后的各类值写入,进行更新
self.alpha[i] = alphaNew_1
self.alpha[j] = alphaNew_2
self.b = bNew
self.E[i] = self.calcEi(i)
self.E[j] = self.calcEi(j)
#如果α2的改变量过于小,就认为该参数未改变,不增加parameterChanged值
#反之则自增1
if math.fabs(alphaNew_2 - alphaOld_2) >= 0.00001:
parameterChanged += 1
#打印迭代轮数,i值,该迭代轮数修改α数目
print("iter: %d i:%d, pairs changed %d" % (iterStep, i, parameterChanged))
#全部计算结束后,重新遍历一遍α,查找里面的支持向量
for i in range(self.m):
#如果α>0,说明是支持向量
if self.alpha[i] > 0:
#将支持向量的索引保存起来
self.supportVecIndex.append(i)
遍历对应的迭代次数,判断每个变量i是否满足对应的KKT条件,如果不满足的情况下选择将它作为第一个选中的变量。
接下来后续调用相应的公式:
最后调用后面的公式求得对应的分母伊塔,然后求得每次迭代的情况