【机器学习】SVM(基于SMO算法)—— python3 实现方案

在上个版本的基础上修订,改正了一些错误,优化了一些代码结构,修改了一些注释。

不厌其烦的详细注释,对入门来说还是挺友好的哈,欢迎指正!

测试数据是吴恩达机器学习课程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()

猜你喜欢

转载自blog.csdn.net/zhenghaitian/article/details/83713968