深入理解支持向量機(SVM) 用梯度下降法和SMO算法實現SVM

在各種各樣強大的神經網絡被提出之前,SVM在機器學習領域一直是走在前列的模型;尤其是在數據規模較小時,SVM的特性能夠有效保證模型不會出現嚴重的過擬合,從而提升模型表現。即使在今天,儘管GBDT和各式各樣的深度學習模型大行其道,SVM仍然在數據挖掘領域佔有一席之地。
我個人認爲,深入學習SVM對入門機器學習幫助巨大。SVM涉及的概念,包括了基礎線性分類器,間隔最大約束最優化,核技巧以及拉格朗日鬆弛條件,甚至正則化等等,都是如果將來要進一步學習機器學習,出現的非常頻繁的知識點。
我在這裏將簡單介紹SVM原理,並給出各種方法實現它的詳細代碼。

線性模型

考慮最簡單的線性二分類器,如果我們想讓線性模型到兩種樣本的間隔最大,那麼超平面必然位於距離最近的兩種樣本點中間。設對正樣本y+有w.Tx+b = 1,負樣本y-有w.Tx+b = -1,將有這樣一個優化問題。
在這裏插入圖片描述
在這裏插入圖片描述
這將是一個很像二次型的優化問題(可以想象二維平面上的兩個點,直線到兩點的距離關於斜率會是一個二次函數)。但是如果只是讓間隔最大,優化問題將得到很無聊的解,因爲只需要將參數無限增大即可。爲此我們還需要添加約束條件,就是讓兩類樣本都被正確分類。
在這裏插入圖片描述
這裏我們把最大化問題改變了一下形式就變成最小化問題,同時問題還是一個凸優化問題。
但是問題並不總是線性完全可分的,如果約束條件不可能得到滿足,那麼問題就找不到一個可行解。因此我們可以考慮像罰函數法一樣,把約束條件當成一個鬆弛的條件加在原優化問題上。如果使用ReLU函數當作隨錯分距離增加的損失函數(hinge loss),原問題就寫成
在這裏插入圖片描述
即如果樣本點越過了支撐邊界,就會受到額外的懲罰讓loss function的值更大。
注意到這是一個無約束優化問題,我們完全可以用無約束的方法來解它,比如梯度下降、座標下降和牛頓法等等。
如果要用梯度下降來解,就先要計算梯度。
在這裏插入圖片描述
有了梯度,就可以用隨機梯度下降或者批量梯度下降來求解這個鬆弛的線性SVM問題了。
下面我用Python實現了一個批量梯度下降的SVM,並用它來分類鳶尾花數據集。

import numpy as np
import random
from copy import deepcopy
import matplotlib
from matplotlib import pyplot as plt 
from sklearn.metrics import accuracy_score
import warnings
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
import heapq

warnings.filterwarnings('ignore')

iris = load_iris()
descr = iris['DESCR']
data = iris['data']
feature_names = iris['feature_names']
target = iris['target']
target_names = iris['target_names']

首先我們導入numpy,並用sklearn裏的iris數據集來做二分類。

class LinearSVM:
    def __init__(self, dim, C, lr=0.1):
        '''
        dim指維度,給出模型要產生的超平面維度
        C是SVM的鬆弛條件,用於確定對錯分的容忍度
        '''
        self.dim = dim
        self.C = C
        self.lr = lr
        self.W = np.random.randn(1,dim)
        self.b = np.random.randn(1)
        
    def fit(self, X, y, batch_size = 5, iteration_times = 1000):
        '''
        每次取batch_size個樣本點作爲訓練樣本
        對linear svm計算loss的梯度並更新參數
        迭代一定次數後停止,lr會對迭代次數減小以控制fit過程的震盪
        y參數需要使用[1,-1]的二分類格式以契合訓練
        '''
        
        for t in range(iteration_times):
            lr = self.lr
            N = len(X)
            loss = 0.0
            #遍歷樣本,把樣本按照錯誤程度排序,只保留k個最差的
            heap = []
            for i in range(N):
                x = X[i]
                x = x.reshape(-1,1)
                margin = 1-y[i]*(self.W.dot(x)+self.b)
                if margin>0:#也就是錯分類的樣本
                    heapq.heappush(heap, (margin,i))
                if len(heap)>batch_size:#多於k則pop出來
                    heapq.heappop(heap)
                loss += max(margin,0)*self.C 
            loss += np.sum(self.W**2)/2
            #如果不滿k個,則隨機採樣採到k個
            while len(heap)<batch_size:
                i = random.randint(0,N-1)
                x = X[i].reshape(-1,1)
                margin = 1-y[i]*(self.W.dot(x)+self.b)
                heapq.heappush(heap, (margin,i))
            
            if (t+1)%200==0:
                print("Iteration %d" %(t+1))
                print("Loss",loss)
            
            
            dW = np.zeros(self.W.shape)
            db = np.zeros(self.b.shape)
            for margin,i in heap:
                x = X[i].reshape(-1,1)
                if margin<=0:
                    dW += self.W
                else:
                    dW += self.W-self.C*y[i]*x.T
                    db += (-self.C*y[i])
            
            dW,db = dW/batch_size,db/batch_size
            self.W -= lr*dW
            self.b -= lr*db
            
    def predict(self, x, raw=False):
        x = x.reshape(-1,1)
        y_pred = self.W.dot(x) + self.b
        if raw:
            return y_pred
        return np.sign(y_pred).astype(np.float32)

這裏使用的是批量梯度下降優化,並且用了一點啓發式技巧:按照錯分嚴重程度來取k個點進行訓練,如果沒有錯分則再隨機選取。對每個樣本點,首先計算它是否錯分,如果錯分則在原有的對w的梯度上再加上hinge loss的梯度。計算完k個點的梯度後,求平均並更新w和b參數。
迭代多次就可以得到一個不錯的解,下面試試在iris其中兩維上的表現。

X = data[:100][:,1:3]
y = deepcopy(target[:100])*2-1

def decision_plain():
    x_axis = np.linspace(1,6,100)
    y_axis = np.linspace(0,6,100)
    xx, yy = np.meshgrid(x_axis, y_axis)
    Z = np.zeros((100,100))
    for i in range(100):
        for j in range(100):
            Z[i][j] = svm.predict(np.array([xx[i][j],yy[i][j]]))
    plt.figure(figsize=(8,6), dpi=80)
    colors = ['aquamarine','palegoldenrod']
    plt.contourf(xx, yy, Z, cmap=matplotlib.colors.ListedColormap(colors))

svm = LinearSVM(2, 1, lr = 0.001)
svm.fit(X,y,iteration_times=3000, batch_size=10)
dicision_plain()
plt.scatter(X[:,0].reshape(-1), X[:,1].reshape(-1),edgecolors='black',
            c=y.reshape(-1))
plt.xlabel('sepal length (cm)')
plt.ylabel('petal length (cm)')
plt.title('Data distribution and decision plain')


在這裏插入圖片描述
生成的超平面大致位於樣本點中間,如果調整鬆弛因子C則有可能得到不一樣的結果。

svm = LinearSVM(2, 100, lr = 0.001)
svm.fit(X,y,iteration_times=3000, batch_size=10)
dicision_plain()
plt.scatter(X[:,0].reshape(-1), X[:,1].reshape(-1),edgecolors='black',
            c=y.reshape(-1))
plt.xlabel('sepal length (cm)')
plt.ylabel('petal length (cm)')
plt.title('Data distribution and decision plain')

在這裏插入圖片描述

核函數非線性模型

如果只能使用線性模型,則SVM並沒什麼了不起,畢竟很多任務是完全不可能用線性模型求解的。但SVM可以用一種技巧實現非線性,叫做核技巧。
原本我們的模型是 y = w.x + b,是一個在樣本空間中實現的超平面。w和數據集中的樣本點x維度相同,如果把w用所有數據集中的樣本點x_i的線性組合來表示,原問題可以寫作
y=wx+b=wxix+b y = wx+b =w\sum{x_i}x + b
如果我們把這個向量積用一個核函數取代
在這裏插入圖片描述
K(x_i,x)會得到和簡單的向量乘積不一樣的結果,實際上相當於先把數據從原空間經過某種映射函數映射到了另一個空間,然後在這個空間中進行線性二分。這樣,顯示在原空間中將是一個看起來非線性的決策平面。
這樣,我們就得到了非線性的二分類模型。同時因爲投影空間的線性決策平面仍然帶有最大間隔的特性,使用核函數的SVM依然能讓原空間的決策曲面離兩類樣本儘可能遠。
新的優化問題寫成
在這裏插入圖片描述
梯度計算基本一致,只需要在樣本上套用核函數即可。
下面一樣給出我的Python實現,這裏使用的核函數是高斯核。

def RBF(x1, x2 ,gamma = 0.5):
    x = x1-x2
    return np.exp(-gamma*np.sum(x**2))

class SVM:
    def __init__(self, C, lr=0.1):
        '''
        核SVM不需要預先設置維度,而是根據輸入訓練數據的維度設置alpha
        C是SVM的鬆弛條件,用於確定對錯分的容忍度
        '''
        self.C = C
        self.lr = lr
        self.alpha = None
        self.b = 0
        self.X = None
        self.loss = float("inf")
        
        
    def predict(self, x, raw=False):
        y_pred = 0+self.b
        N = len(self.X)
        for i in range(N):
            y_pred += RBF(x,self.X[i])*self.alpha[i]
        if raw:
            return y_pred
        return np.sign(y_pred).astype(np.float32)
        
        
    def fit(self, X, y, iteration_times = 1000, batch_size=10):
        '''
        對核SVM,使用所有樣本求loss並用批梯度下降更好用
        當然,如果樣本過大也可以使用隨機梯度下降(樣本大的時候也不會用SVM^-^)
        y參數需要使用[1,-1]的二分類格式以契合訓練
        '''
        lr = self.lr
        N = len(X)
        self.X = X
        self.alpha = np.random.rand(N)*self.C*0.01
        #計算核矩陣
        K = np.zeros((N,N))
        for i in range(N):
            for j in range(N):
                K[i][j] = RBF(X[i],X[j])  
        
        K_diag = np.diag(K)
        for t in range(iteration_times):
            a = self.alpha.reshape(1,-1)
            loss = a.dot(K).dot(a.T)
            da = np.zeros(N)
            db = 0
            self.alpha -= lr * (np.sum(self.alpha * K, axis=1) + self.alpha * K_diag) * 0.5
            indices = np.random.permutation(N)[:batch_size]
            
            for i in indices:
                margin = 1-y[i]*(np.sum(self.alpha*K[i])+self.b)
                loss += self.C*max(margin,0)
                if margin>0:
                    da-=self.C*y[i]*K[i]
                    db-=self.C*y[i]
            
            if (t+1)%500==0:
                print("Iteration %d, " %(t+1),"Loss:",loss)
            
            self.alpha -= lr*da
            self.b -= lr*db

svm = SVM(1, lr = 0.01)
svm.fit(X,y,iteration_times=2000)
dicision_plain(1,6,0,6)
plt.scatter(X[:,0].reshape(-1), X[:,1].reshape(-1),edgecolors='black',
            c=y.reshape(-1))
plt.xlabel('sepal length (cm)')
plt.ylabel('petal length (cm)')
plt.title('Data distribution and decision plain')

在這裏插入圖片描述
除此之外,我們還希望觀察一下非線性的分類情況。首先隨便製造一個線性不可分的數據集。

X = np.linspace(-1,1,50)
y1 = np.sin(X)
y2 = -X**3
X = np.concatenate((X,X))
y = np.concatenate((y1,y2))
X = np.array([X,y]).T
y = np.concatenate((np.ones(50),-np.ones(50)))
plt.scatter(X[:,0],X[:,1],c = y)

讓SVM在上面運行,觀察決策曲面。

svm = SVM(100, lr = 10**(-5))
svm.fit(X,y,iteration_times=5000)
dicision_plain(-1.5,1.5,-1.5,1.5)
plt.scatter(X[:,0].reshape(-1), X[:,1].reshape(-1),edgecolors='black',
            c=y.reshape(-1))
plt.xlabel('x_1')
plt.ylabel('x_2')
plt.title('Data distribution and SVM decision plain')

在這裏插入圖片描述

更快的算法 SMO優化

前面我們用gradient descent求解了線性和核技巧的SVM,它們在小規模數據上表現出良好的間隔特性,但是這樣的求解方法收斂速度比較慢,而且在數據規模較大時只能用隨機梯度下降,而SGD的收斂性是無法得到保證的。有沒有一種更好的方法幫助我們加速SVM的訓練呢?
幸運的是,1998年,Platt提出了SMO算法求解SVM,大大加速了SVM的訓練,這種原本比較臃腫的模型在數據時代又一次成爲了寵兒。
SMO不像我們前面的方法,針對無約束的原問題求解,而是針對原問題的對偶問題求解。SVM的原問題是凸優化問題,因此由對偶理論,對偶問題(maximize 問題)的最優解和原問題相同,所以直接解對偶問題就能得到最優解。
在這裏插入圖片描述
把KKT條件代入就得到上圖的對偶問題,這是一個約束優化問題,而且是有二次函數形式的問題,這個問題有特殊的優化算法。
如果沒有約束,就可以用牛頓法或者共軛梯度法很快求到解。但是問題是約束優化,就需要使用一些技巧。
SMO算法類似於座標下降,每次固定其他變量改變一個變量,但是這裏我們有一個等式約束,所以可以改變兩個變量來讓約束條件保持滿足。當固定變量後,求解變成單變量二次規劃問題。考慮另一個不等式約束即可通過迭代得到最優解。 而如何選擇要改變哪兩個alpha呢,這裏用了一種特殊的啓發方式
在這裏插入圖片描述
可以從KKT條件和對偶條件的結合看出,alpha的不同值對應着不同的點位置。=C的點落在間隔內,0,C之間的點落在區分超平面上,=0的點對問題沒有影響,落在間隔外。用這個方法可以判斷一個alpha違反KKT條件的程度,用於啓發式選擇參數。

def SMO(X, y, C, iteration = 100):
    N = len(X)  #data size
    alpha = np.zeros(N)
    b = 0
    #計算核矩陣
    K_mat = np.zeros((N,N))
    for i in range(N):
        for j in range(N):
            K_mat[i][j] = RBF(X[i],X[j])
    
    for t in range(iteration):
        #首先爲各變量計算err
        con1 = alpha > 0
        con2 = alpha < C

        y_pred = np.zeros(N)
        for i in range(N):
            y_pred[i] = np.sum(alpha*K_mat[i]*y)+b
        err1 = y * y_pred - 1
        err2 = err1.copy()
        err3 = err1.copy()
        err1[(con1 & (err1 <= 0)) | (~con1 & (err1 > 0))] = 0
        err2[((~con1 | ~con2) & (err2 != 0)) | ((con1 & con2) & (err2 == 0))] = 0
        err3[(con2 & (err3 >= 0)) | (~con2 & (err3 < 0))] = 0
        # 算出平方和並取出使得“損失”最大的 idx
        err = err1 ** 2 + err2 ** 2 + err3 ** 2
        i = np.argmax(err)
        #print("Before:",err)
        #找到i後,j隨機選取
        j = i
        while j==i:
            j = random.randint(0,N-1)

        #爲i和j計算函數值E = f(x)-y
        e1 = evaluate(X[i],X,y,alpha,b)-y[i]
        e2 = evaluate(X[j],X,y,alpha,b)-y[j]
        #計算內積差
        dK = K_mat[i][i]+K_mat[j][j]-2*K_mat[i][j]
        #計算上下界
        if y[i]!=y[j]:
            L = max(0,alpha[i]-alpha[j])
            H = min(C,C+alpha[i]-alpha[j])
        else:
            L = max(0,alpha[i]+alpha[j]-C)
            H = min(C,alpha[i]+alpha[j])
        #更新參數的最優值
        alpha1_new = alpha[i]-y[i]*(e1-e2)/dK
        #考慮上下界後的更新值
        alpha1_new = np.clip(alpha1_new, L, H)
        alpha[j] += y[i]*y[j]*(alpha[i]-alpha1_new)
        alpha[i] = alpha1_new
        
        #找支持向量並修正b
        b = 0
        indices = [i for i in range(N) if 0<alpha[i]<C]
        for i in indices:
            b+=(1/y[i]-np.sum(alpha*K_mat[i]*y))
        if len(indices):
            b /= len(indices)
        
    return alpha,b

函數將會返回alpha向量和bias偏置常數,代入公式就是一個SVM。

def evaluate(x_in, X, y, alpha, b, K=RBF):
    res = 0+b
    k = np.array([K(x_in,x) for x in X])
    return np.sum(alpha*k*y)+b

決策曲面的表現和上面的核函數版本應該是相同的,但是經過實測,SMO的收斂性很好,很少的迭代後就能讓大部分alpha都歸0,只保留支撐向量的alpha,達到最優解。而且收斂速度極快,在上面的例子大概是梯度下降法的幾十倍,並且在更高維度可能有更好的表現。

總結

SVM作爲很早的一批機器學習模型,到了今天仍然有相當的影響力,就已經說明了該模型的出色。即使不談性能,SVM的理念也是值得學習的。其中核函數的引入實現的非線性,以及約束優化對偶問題使用的拉格朗日乘子法,無不在顯示着數學之美。
SVM是經典的優化式模型(用最優化方法優化一個損失函數),類似的還有前饋神經網絡等。與它平行的有基於結構的模型(學習模型結構)決策樹,基於概率的模型(最大化對數似然)貝葉斯分類器、隱馬爾科夫模型,基於數據庫的模型(基於數據近似度的模型)KNN等。優化式模型是最接近今天大火的深度學習模型的一種,有很不錯的啓發性和學習價值。
上面提到的其他比較好用的傳統機器學習模型,KNN、HMM和GBDT我也會在近期進行學習,如果喜歡請繼續關注。

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