支持向量機SVM原理及實踐(二)

 四、序列最小優化算法 SMO

1. 核函數:

前面我們瞭解的 SVM 是線性的支持向量機,當我們遇到非線性可分的數據集時並找不到分隔超平面,可以引入核函數,將非線性問題轉化爲線性問題。核函數是通過將數據映射到高維空間,來解決在原始空間中線性不可分的問題。

計算兩個向量在隱式映射過後的空間中的內積的函數叫做核函數,但是映射到高維空間中,然後再根據內積的公式進行計算時遇到維度爆炸(原始空間是三維時,映射到高維空間維度呈指數增長)導致無法計算,這時核函數能簡化映射空間中的內積運算。

如在我們的 SVM 裏需要計算的地方數據向量總是以內積的形式出現:

\underset{a}{min}\frac{1}{2}\sum_{i,j=1}^{m}\alpha _{i}\alpha _{j}y^{i}y^{j}\left ( X^{i} \cdot X^{j}\right )-\sum_{i=1}^{m}\alpha _{i}

當核函數是高斯核函數:

K\left ( X^{i},X^{j} \right )=exp\left ( -\frac{\left \| X^{i}-X^{j} \right \|^{2}}{2\sigma ^{2}} \right )

優化問題加入核函數後變爲:

\underset{a}{min}\frac{1}{2}\sum_{i,j=1}^{m}\alpha _{i}\alpha _{j}y^{i}y^{j}K\left ( X^{i} \cdot X^{j}\right )-\sum_{i=1}^{m}\alpha _{i}

s.t. \sum_{i=1}^{m}\alpha _{i}y^{i}=0 ,0\leq \alpha _{i}\leq C,i=1,2,...,m

這樣一來可避開了直接在高維空間中進行計算。綜上在非線性情況下,SVM 首先在低維空間中完成計算,然後通過核函數將輸入空間映射到高維特徵空間,最終在高維特徵空間中構造出最優分割超平面,從而把平面上本身不好分的非線性數據分開。如圖所示:

下圖所示的例子是低維線性不可分的數據通過高斯核函數映射到了高維空間:

核函數簡要概括爲以下三點:

2. 序列最小優化算法 SMO  

在解前面原始的帶約束的優化問題時利用拉格朗日的對偶性,將問題轉化爲其對偶問題並求得最優解 a*、w* 和 b*。對如上帶約束的優化問題可以使用二次規劃的方法進行求解,最爲廣泛應用的序列最小優化算法 SMO ,能夠高效的求解上述帶約束的優化問題。它的思想是將一個大的問題劃分爲一系列小的問題,通過對子問題的求解,達到對對偶問題的求解。

在 SMO 算法中,不斷將對偶問題的二次規劃問題分解爲只有兩個變量的二次規劃子問題,並對子問題進行求解。對於 SMO 算法,每次取兩個變量進行更新,假設取兩個乘子 \alpha _{1}\alpha _{2},然後固定  和  以外的其它乘子 \alpha _{3},\alpha _{4},..,\alpha _{m},使得目標函數只是關於 和  的函數。這樣,不斷的從一堆乘子中任意抽取兩個求解,不斷的迭代求解子問題,最終達到求解原問題的目的。

此時子問題的優化目標爲:

對於新求出的\alpha _{2}^{i+1}需要滿足:L\leq \alpha _{2}^{i+1}\leq H

y^{1}\neq y^{2}L=max\left ( 0,\alpha _{2}^{i}-\alpha _{1}^{i} \right ),H=min\left ( C,C+\alpha _{2}^{i}-\alpha _{1}^{i} \right )

y^{1}= y^{2}L=max\left ( 0,\alpha _{2}^{i}+\alpha _{1}^{i} -C\right ),H=min\left ( C,\alpha _{2}^{i}+\alpha _{1}^{i} \right )

新求解出的\alpha _{2}^{i+1}值爲:

\alpha _{2}^{i+1}=\left\{\begin{matrix} H,\alpha _{2}^{i+1}> H\\ \alpha _{2}^{i+1},L\leq \alpha _{2}^{i+1}\leq H \\L,\alpha _{2}^{i+1} < L \end{matrix}\right.

當所有的變量都滿足約束條件 KKT 時,其解就爲最優化問題的解,所以我們要做的是找出不滿足KKT條件的變量,根據更新公式更新使其滿足約束條件。

KKT 條件:

滿足 KKT 的情況:

\left\{\begin{matrix} \alpha _{i}=0\Leftrightarrow y^{i}g\left ( X^{i} \right )\geq 1 ,\left ( 1 \right )\\0< \alpha _{i}< C\Leftrightarrow y^{i}g\left ( X^{i} \right )=1,\left ( 2 \right ) \\ \alpha _{i}=C\Leftrightarrow y^{i}g\left ( X^{i} \right )\leq 1,\left ( 3 \right ) \end{matrix}\right.

1.(1)的情況,表明 ai 是正常分類,在邊界內部(正確分類的點yi*f(xi)>=0);

2.(2)的情況,表明了 ai 是支持向量,在邊界上;

3.(3)的情況,表明了 ai 是在兩條邊界之間。

最優解需要滿足 KKT 條件,即(1)(2)(3)條件都滿足。

不滿足 KKT 的情況:
1.yig(xi)<=1 但是 ai<C 則違反KKT條件;

2.yig(xi)>=1 但是 ai>0 則違反KKT條件;

3.yig(xi)=1 表明是支持向量,此時無需優化

通過檢查每一個樣本點是否符合上述不滿足 KKT 條件,選擇第一個變量 a1 ,再選出第二個變量 a2,然後根據 a2 的更新公式更新。

更新完 a1 和 a2,需要重新計算閾值 b:

  1. 實際中遇到線性不可分的數據集時,是把樣例特徵映射到高維空間中去,相關特徵便被分開了,達到了分類的目的;
  2. 如果遇到線性不可分的數據集,一律映射到高維空間,那麼這個維度會很高甚至無窮;
  3. 這時核函數就起作用了,它先在低維上進行計算,再將低維映射到高維,避免了直接在高維空間中的複雜計算。
     

更新完閾值 b 後,需要重新計算誤差 E:

E_{i}=\sum_{j=1}^{m}\alpha _{j}y^{j}K\left ( X^{j} ,X^{i}\right )+b^{i+1}-y^{i}

五、支持向量機 SVM 算法實踐

# -*- coding: utf-8 -*-
"""
Created on Thu Feb 28 19:33:25 2019

@author: 2018061801
"""
#svm.py
import numpy as np

class SVM:
    def __init__(self, dataSet, labels, C, toler, kernel_option):
        self.train_x = dataSet # 訓練特徵
        self.train_y = labels  # 訓練標籤
        self.C = C # 懲罰參數
        self.toler = toler     # 迭代的終止條件之一
        self.n_samples = np.shape(dataSet)[0] # 訓練樣本的個數
        self.alphas = np.mat(np.zeros((self.n_samples, 1))) # 拉格朗日乘子
        self.b = 0
        self.error_tmp = np.mat(np.zeros((self.n_samples, 2))) # 保存E的緩存
        self.kernel_opt = kernel_option # 選用的核函數及其參數
        self.kernel_mat = calc_kernel(self.train_x, self.kernel_opt) # 核函數的輸出

def cal_kernel_value(train_x, train_x_i, kernel_option):
    '''樣本之間的核函數的值
    input:  train_x(mat):訓練樣本
            train_x_i(mat):第i個訓練樣本
            kernel_option(tuple):核函數的類型以及參數
    output: kernel_value(mat):樣本之間的核函數的值
    '''
    kernel_type = kernel_option[0] # 核函數的類型,分爲rbf和其他
    m = np.shape(train_x)[0] # 樣本的個數
    kernel_value = np.mat(np.zeros((m, 1)))
    if kernel_type == 'rbf': # rbf核函數
        sigma = kernel_option[1]
        if sigma == 0:
            sigma = 1.0
        for i in range(m):
            diff = train_x[i, :] - train_x_i
            kernel_value[i] = np.exp(diff * diff.T / (-2.0 * sigma**2))
    else: # 不使用核函數
        kernel_value = train_x * train_x_i.T
    return kernel_value

def calc_kernel(train_x, kernel_option):
    '''計算核函數矩陣
    input:  train_x(mat):訓練樣本的特徵值
            kernel_option(tuple):核函數的類型以及參數
   output: kernel_matrix(mat):樣本的核函數的值
    '''
    m = np.shape(train_x)[0] # 樣本的個數
    kernel_matrix = np.mat(np.zeros((m, m))) # 初始化樣本之間的核函數值
    for i in range(m):
        kernel_matrix[:, i] = cal_kernel_value(train_x, train_x[i, :], kernel_option)
    return kernel_matrix

def cal_error(svm, alpha_k):
    '''誤差值的計算
    input:  svm:SVM模型
            alpha_k(int):選擇出的變量
    output: error_k(float):誤差值
    '''
    output_k = float(np.multiply(svm.alphas, svm.train_y).T * svm.kernel_mat[:, alpha_k] + svm.b)
    error_k = output_k - float(svm.train_y[alpha_k])
    return error_k

def update_error_tmp(svm, alpha_k):
    '''重新計算誤差值
    input:  svm:SVM模型
            alpha_k(int):選擇出的變量
    output: 對應誤差值
    '''
    error = cal_error(svm, alpha_k)
    svm.error_tmp[alpha_k] = [1, error]


def select_second_sample_j(svm, alpha_i, error_i):
    '''選擇第二個樣本
    input:  svm:SVM模型
           alpha_i(int):選擇出的第一個變量
            error_i(float):E_i
    output: alpha_j(int):選擇出的第二個變量
            error_j(float):E_j
    '''
    # 標記爲已被優化
    svm.error_tmp[alpha_i] = [1, error_i]
    candidateAlphaList = np.nonzero(svm.error_tmp[:, 0].A)[0]
    maxStep = 0
    alpha_j = 0
    error_j = 0
    if len(candidateAlphaList) > 1:
        for alpha_k in candidateAlphaList:
            if alpha_k == alpha_i: 
                continue
            error_k = cal_error(svm, alpha_k)
            if abs(error_k - error_i) > maxStep:
                maxStep = abs(error_k - error_i)
                alpha_j = alpha_k
                error_j = error_k
    else: # 隨機選擇          
        alpha_j = alpha_i
        while alpha_j == alpha_i:
            alpha_j = int(np.random.uniform(0, svm.n_samples))
        error_j = cal_error(svm, alpha_j)
    return alpha_j, error_j



def choose_and_update(svm, alpha_i):
    '''判斷和選擇兩個alpha進行更新
    input:  svm:SVM模型
            alpha_i(int):選擇出的第一個變量
    '''
    error_i = cal_error(svm, alpha_i) # 計算第一個樣本的E_i
    # 判斷選擇出的第一個變量是否違反了KKT條件
    if (svm.train_y[alpha_i] * error_i < -svm.toler) and (svm.alphas[alpha_i] < svm.C) or\
        (svm.train_y[alpha_i] * error_i > svm.toler) and (svm.alphas[alpha_i] > 0):
     # 1、選擇第二個變量
        alpha_j, error_j = select_second_sample_j(svm, alpha_i, error_i)
        alpha_i_old = svm.alphas[alpha_i].copy()
        alpha_j_old = svm.alphas[alpha_j].copy()
        # 2、計算上下界
        if svm.train_y[alpha_i] != svm.train_y[alpha_j]:
            L = max(0, svm.alphas[alpha_j] - svm.alphas[alpha_i])
            H = min(svm.C, svm.C + svm.alphas[alpha_j] - svm.alphas[alpha_i])
        else:
            L = max(0, svm.alphas[alpha_j] + svm.alphas[alpha_i] - svm.C)
            H = min(svm.C, svm.alphas[alpha_j] + svm.alphas[alpha_i])
        if L == H:
            return 0
        # 3、計算eta
        eta = 2.0 * svm.kernel_mat[alpha_i, alpha_j] - svm.kernel_mat[alpha_i, alpha_i]- svm.kernel_mat[alpha_j, alpha_j]
        if eta >= 0:
            return 0
        # 4、更新alpha_j
        svm.alphas[alpha_j] -= svm.train_y[alpha_j] * (error_i - error_j) / eta
        # 5、確定最終的alpha_j
        if svm.alphas[alpha_j] > H:
            svm.alphas[alpha_j] = H
        if svm.alphas[alpha_j] < L:
            svm.alphas[alpha_j] = L
        # 6、判斷是否結束      
        if abs(alpha_j_old - svm.alphas[alpha_j]) < 0.00001:
            update_error_tmp(svm, alpha_j)
            return 0
        # 7、更新alpha_i
        svm.alphas[alpha_i] += svm.train_y[alpha_i] * svm.train_y[alpha_j] * (alpha_j_old - svm.alphas[alpha_j])
        # 8、更新b
        b1 = svm.b - error_i - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \
                                                    * svm.kernel_mat[alpha_i, alpha_i] \
                             - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \
                                                    * svm.kernel_mat[alpha_i, alpha_j]
        b2 = svm.b - error_j - svm.train_y[alpha_i] * (svm.alphas[alpha_i] - alpha_i_old) \
                                                    * svm.kernel_mat[alpha_i, alpha_j] \
                             - svm.train_y[alpha_j] * (svm.alphas[alpha_j] - alpha_j_old) \
                                                    * svm.kernel_mat[alpha_j, alpha_j]
        if (0 < svm.alphas[alpha_i]) and (svm.alphas[alpha_i] < svm.C):
            svm.b = b1
        elif (0 < svm.alphas[alpha_j]) and (svm.alphas[alpha_j] < svm.C):
            svm.b = b2
        else:
            svm.b = (b1 + b2) / 2.0
       # 9、更新error
        update_error_tmp(svm, alpha_j)
        update_error_tmp(svm, alpha_i)
        return 1
    else:
        return 0

def SVM_training(train_x, train_y, C, toler, max_iter, kernel_option = ('rbf', 0.431029)):
    '''SVM的訓練
    input:  train_x(mat):訓練數據的特徵
            train_y(mat):訓練數據的標籤
            C(float):懲罰係數
            toler(float):迭代的終止條件之一
            max_iter(int):最大迭代次數
            kerner_option(tuple):核函數的類型及其參數
    output: svm模型
    '''
    # 1、初始化SVM分類器
    svm = SVM(train_x, train_y, C, toler, kernel_option)
    # 2、開始訓練
    entireSet = True
    alpha_pairs_changed = 0
    iteration = 0
    while (iteration < max_iter) and ((alpha_pairs_changed > 0) or entireSet):
        print ("\t iterration: ", iteration)
        alpha_pairs_changed = 0
        if entireSet:
            # 對所有的樣本
            for x in range(svm.n_samples):
                alpha_pairs_changed += choose_and_update(svm, x)
            iteration += 1
        else:
            # 非邊界樣本
            bound_samples = []
            for i in range(svm.n_samples):
                if svm.alphas[i,0] > 0 and svm.alphas[i,0] < svm.C:
                    bound_samples.append(i)
            for x in bound_samples:
                alpha_pairs_changed += choose_and_update(svm, x)
            iteration += 1   
        # 在所有樣本和非邊界樣本之間交替
        if entireSet:
            entireSet = False
        elif alpha_pairs_changed == 0:
            entireSet = True
    return svm



def svm_predict(svm, test_sample_x):
    '''利用SVM模型對每一個樣本進行預測
    input:  svm:SVM模型
            test_sample_x(mat):樣本
    output: predict(float):對樣本的預測
   '''
    # 1、計算核函數矩陣
    kernel_value = cal_kernel_value(svm.train_x, test_sample_x, svm.kernel_opt)
    # 2、計算預測值
    predict = kernel_value.T * np.multiply(svm.train_y, svm.alphas) + svm.b
    return predict

def cal_accuracy(svm, test_x, test_y):
    '''計算預測的準確性
    input:  svm:SVM模型
            test_x(mat):測試的特徵
            test_y(mat):測試的標籤
    output: accuracy(float):預測的準確性
    '''
    n_samples = np.shape(test_x)[0] # 樣本的個數
    correct = 0.0
    for i in range(n_samples):
        # 對每一個樣本得到預測值
        predict=svm_predict(svm, test_x[i, :])
        # 判斷每一個樣本的預測值與真實值是否一致
        if np.sign(predict) == np.sign(test_y[i]):
            correct += 1
    accuracy = correct / n_samples
    return accuracy

'''def save_svm_model(svm_model, model_file):
    保存SVM模型
    input:  svm_model:SVM模型
            model_file(string):SVM模型需要保存到的文件
  
   with open(model_file, 'w') as f:
                pickle.dump(svm_model, f)'''

 

# -*- coding: utf-8 -*-
"""
Created on Thu Feb 28 20:06:30 2019

@author: 2018061801
"""
#svm_train.py
import numpy as np
import svm
def load_data_libsvm(data_file):
    '''導入訓練數據
    input:  data_file(string):訓練數據所在文件
    output: data(mat):訓練樣本的特徵
            label(mat):訓練樣本的標籤
    '''
    data = []
    label = []
    f = open(data_file)
    for line in f.readlines():
        lines = line.strip().split(' ')
        # 提取得出label
        label.append(float(lines[0]))
        # 提取出特徵,並將其放入到矩陣中
        index = 0
        tmp = []
        for i in range(1, len(lines)):
            li = lines[i].strip().split(":")
            if int(li[0]) - 1 == index:
                tmp.append(float(li[1]))
            else:
                while(int(li[0]) - 1 > index):
                    tmp.append(0)
                    index += 1
                tmp.append(float(li[1]))
            index += 1
        while len(tmp) < 13:
            tmp.append(0)
        data.append(tmp)
    f.close()
    return np.mat(data), np.mat(label).T

if __name__ == "__main__":
    # 1、導入訓練數據
    print ("------------ 1、load data --------------")
    dataSet, labels = load_data_libsvm("D:/anaconda4.3/spyder_work/svm_test.txt")
    # 2、訓練SVM模型
    print ("------------ 2、training ---------------")
    C = 0.6
    toler = 0.001
    maxIter = 500
    svm_model = svm.SVM_training(dataSet, labels, C, toler, maxIter)
    # 3、計算訓練的準確性
    print  ("------------ 3、cal accuracy --------------")
    accuracy = svm.cal_accuracy(svm_model, dataSet, labels)  
    print ("The training accuracy is: %.3f%%" % (accuracy * 100))
   
------------ 1、load data --------------
------------ 2、training ---------------
         iterration:  0
         iterration:  1
         iterration:  2
         iterration:  3
         iterration:  4
------------ 3、cal accuracy --------------
The training accuracy is: 97.037%

參考文獻:

趙志勇《python 機器學習算法》

支持向量機通俗導論(理解SVM的三層境界):https://blog.csdn.net/v_JULY_v/article/details/7624837

程序來源:https://github.com/zhaozhiyong19890102/Python-Machine-Learning-Algorithm

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章