統計學習方法(1) 梯度下降法和SMO算法實現SVM

SVM

SVM是深度學習之前的一種最常用的監督學習方法, 可以用來做分類也可以做迴歸. 它的本質和感知機類似, 但是額外增加了大間隔的優化目標. 結合前面提到的核技巧, 我們可以把SVM推廣到非線性. 這樣實現的分類器將有着非常好的性質, 讓它一度成爲"最優解".

LibSVM

在線性二分類SVM中,我們不止設置一個決策平面wTx+b=0w^Tx+b = 0,還會有兩個支持平面wTx+b=1w^Tx+b = -1wTx+b=1w^Tx+b = 1. 如果我們假設數據是線性可分的, 那麼決策平面一定位於兩類樣本之間. 而現在我們想從兩類樣本之間確定一個最好的超平面,這個超平面滿足的性質是到兩類樣本的最小距離最大.很自然的, 我們可以看出這個超平面到兩類樣本的最小距離應該相等. 這時上面的支持超平面就派上用場了. 設到超平面最近的兩個樣本是x+x_+xx_-, 樣本距離d=wTx+bwd=\frac{w^T x+b}{||w||}, 也就是wTx++b=(wTx+b)=Cw^T x_++b = -(w^T x_-+b) = C. 又因爲w的縮放對超平面的性質沒有影響, 我們不妨讓C = 1, 即兩個樣本x+x_+xx_-會分別落在wTx+b=1w^Tx+b = 1wTx+b=1w^Tx+b = -1超平面上. 其他樣本都在這兩個超平面以外.
如果用上面的一系列假設, 我們的損失函數就應該是: 如果正樣本越過了wTx+b=1w^Tx+b = 1超平面, 即wTx+b<1w^Tx+b < 1那麼它受到隨距離線性增長的懲罰. 同樣如果正樣本越過了wTx+b=1w^Tx+b = -1超平面, 即wTx+b>1w^Tx+b > -1也受到懲罰. 如果我們設y是樣本標籤,正樣本爲+1,負樣本爲-1. 則我們有保證兩類樣本到決策超平面的最小距離相等的新型感知機
L(w,b)=ReLU(yi(wTxi+b)+1)w L(w,b) = \frac{ReLU(-y_i (w^T x_i+b)+1)}{||w||}
同樣的道理, 等價於
L(w,b)=ReLU(yi(wTxi+b)+1) L(w,b) = ReLU(-y_i (w^T x_i+b)+1)
到此還不算結束, 因爲我們還想要這個最小距離最大化. 我們知道d=wTx+bwd=\frac{w^T x+b}{||w||}, 而$|w^T x_++b| = |w^T x_-+b| = 1 ,, 即d=\frac{1}{||w||}$, 也就是最大化距離, 只需要最小化w即可. 最小化一個參數常用的做法是正則化, 也就是對w施加L2的懲罰(L1的懲罰性質較差, 容易讓w爲0).這樣我們就得到了最大化最小距離的損失.
minimize12w22 minimize\qquad \frac{1}{2}||w||_2^2
我們把上面兩個損失函數加起來, 並給感知機正確分類的一項乘上一個常數係數, 用來調控(正確分類) vs (最大化間隔)兩個目標的重要程度.
minimizeL(w,b)=12w22+CReLU(yi(wTxi+b)+1) minimize\quad L(w,b) = \frac{1}{2}||w||_2^2+C*ReLU(-y_i (w^T x_i+b)+1)
優化這個無約束的優化問題就能得到線性的二分類SVM, 我們用Pytorch的自動求導來實現梯度下降, 優化這個目標函數看看效果.

import torch
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
import torch.nn as nn
import matplotlib
import random

torch.manual_seed(2020)
n = 50

Xp = torch.randn(n,2)
Xp[:,0] = (Xp[:,0]+3)
Xp[:,1] = (Xp[:,1]+4)
A = torch.tensor([[-1.,0.8],[2,1]])
Xp = Xp.mm(A)

Xn = torch.randn(n,2)
Xn[:,0] = (Xn[:,0]-1)
Xn[:,1] = (Xn[:,1]-2)
A = torch.tensor([[-0.5,1],[1,0.5]])
Xn = Xn.mm(A)

X = torch.cat((Xp,Xn),axis = 0)
y = torch.cat((torch.ones(n),-1*torch.ones(n)))
y = y.reshape(-1,1)


class LibSVM:
    def __init__(self, C = 1, lr = 0.01):
        '''
        超參數包括鬆弛變量C和學習率lr
        要學習的參數是一個線性超平面的權重和偏置
        '''
        self.C = C
        self.lr = lr
        self.weights = None
        self.bias = None
    
    def train(self):
        self.weights.requires_grad = True
        self.bias.requires_grad = True
        
    def eval(self):
        self.weights.requires_grad = False
        self.bias.requires_grad = False
    
    def fit(self, X, y, max_iters = 1000):
        '''
        X是數據張量, size(n,m)
        數據維度m, 數據數目n
        y是二分類標籤, 只能是1或-1
        '''
        n,m = X.shape
        y = y.reshape(-1,1)
        self.weights = torch.randn(m,1)
        self.bias = torch.randn(1)
        self.train()
        
        for step in range(max_iters):
            out = X.mm(self.weights)+self.bias # 前向計算
            # 損失計算
            loss = 0.5*self.weights.T.mm(self.weights)+\
            self.C*torch.sum(F.relu(-y*out+1))
            # 自動求導
            loss.backward()
            # 梯度下降
            self.weights.data -= self.lr*self.weights.grad.data
            self.bias.data -= self.lr*self.bias.grad.data
            self.weights.grad.data.zero_()
            self.bias.grad.data.zero_()
            
        return loss
    
    def predict(self, x, raw = False):
        self.eval()
        out = x.mm(self.weights)+self.bias
        if raw: return out
        else: return torch.sign(out)
        
    def show_boundary(self, low1, upp1, low2, upp2):
        # meshgrid製造平面上的點
        x_axis = np.linspace(low1,upp1,100)
        y_axis = np.linspace(low2,upp2,100)
        xx, yy = np.meshgrid(x_axis, y_axis)
        data = np.concatenate([xx.reshape(-1,1),yy.reshape(-1,1)],axis = 1)
        data = torch.tensor(data).float()
        # 用模型預測
        out = self.predict(data, raw = True).reshape(100,100)
        out = out.numpy()
        Z = np.zeros((100,100))
        Z[np.where(out>1)] = 1.
        Z[np.where(out<-1)] = -1.
        # 繪製支持邊界
        plt.figure(figsize=(8,6))
        colors = ['aquamarine','seashell','palegoldenrod']
        plt.contourf(xx, yy, Z, cmap=matplotlib.colors.ListedColormap(colors))
        # 繪製決策邊界
        slope = (-self.weights[0]/self.weights[1]).data.numpy()
        b = (-self.bias/self.weights[1]).data.numpy()
        xx = np.linspace(low1, upp1)
        yy = slope*xx+b
        plt.plot(xx,yy, c = 'black')

model = LibSVM()
model.fit(X,y)
model.show_boundary(-5,12,-5,10)
plt.scatter(X[:n,0].numpy(),X[:n,1].numpy() , marker = '+', c = 'r')
plt.scatter(X[n:,0].numpy(),X[n:,1].numpy() , marker = '_', c = 'r')

在這裏插入圖片描述

Kernel SVM

有時, 我們需要的是超越超平面的決策邊界, 它應該能容忍非線性可分的兩類樣本, 並給出非線性的解. 這一點僅靠上面的方法是無法實現的, 爲此我們可以把核技巧帶到LibSVM中, 得到二分類的KerbSVM. 複習一下核方法, 我們使用核方法的核心思想是在向量內積得到的標量上進行非線性變換, 從而間接實現高維映射.
那麼怎麼把內積帶到上面的線性模型裏呢? 觀察到權重w和數據點x的維度相同, 那麼如果我們把w寫成x的線性組合, 就能得到x和x的內積. 即
w=i=1NαixiT w = \sum_{i=1}^{N}\alpha_i x_i^T
y=i=1NαixxiT y = \sum_{i=1}^{N} \alpha_i x x_i^T
在內積上套用核函數就有非線性的廣義線性模型, 常用的非線性核函數有高斯核, 多項式核等等
k(x,y)=(xyT)d k(x,y) = (xy^T)^d
k(x,y)=exp(σxy2) k(x,y) = exp(-\sigma||x-y||^2)
y=i=1Nαiϕ(x)ϕ(xi)T+b=i=1Nαik(x,xi)+b y = \sum_{i=1} ^{N} \alpha_i \phi(x) \phi(x_i)^T+b = \sum_{i=1} ^{N} \alpha_i k(x,x_i)+b
把這個新的計算式代入之前的損失函數, 得到新的, 核函數版本的bSVM的損失函數
minimizeL(w,b)=12w22+CReLU(yi(i=1Nαik(x,xi)+b)+1)=12i=1Nj=1Nαiαjk(xi,xj)+CReLU(yi(i=1Nαik(x,xi)+b)+1) minimize\quad L(w,b) = \frac{1}{2}||w||_2^2+C* ReLU(-y_i (\sum_{i=1}^{N} \alpha_i k(x,x_i)+b)+1) = \frac{1}{2}\sum_{i=1}^{N}\sum_{j=1}^{N}\alpha_i \alpha_j k(x_i,x_j)+C* ReLU(-y_i (\sum_{i=1} ^{N} \alpha_i k(x,x_i)+b)+1)
優化它, 得到的就是n維向量alpha和一個偏置b. 那麼你可能會說, 如果是這樣, 我們的數據集越大, 要計算的alpha不也會越大嗎? 而且要執行運算的話, 是不是就需要保存訓練時使用的全部xix_i, 對每個xix_i都和當前x做核函數運算再做線性組合, 才能得到結果呢? 事實也是如此, 因此, 在大規模數據上執行核SVM算法時, 我們並不會用所有的xix_i來做線性組合產生w, 我們可以只取一部分的xix_i來減少運算量.

class KerbSVM:
    def __init__(self, C = 1, sigma = None, lr = 0.01):
        self.C = C
        self.lr = lr
        self.sigma = sigma
        self.weights = None
        self.bias = None
        
    
    def train(self):
        self.weights.requires_grad = True
        self.bias.requires_grad = True
        
    def eval(self):
        self.weights.requires_grad = False
        self.bias.requires_grad = False
    
    def ker_func(self, x1, x2, sigma = None):
        '''
        高斯核函數, sigma是高斯核的帶寬
        x1, x2是矩陣, 函數計算x1和x2的核矩陣
        '''
        n1,n2 = x1.shape[0],x2.shape[0]
        if sigma is None:
            sigma = 0.5/x1.shape[1]
        K = torch.zeros((n1,n2))
        for i in range(n2):
            square = torch.sum((x1-x2[i])**2, axis = 1)
            K[:,i] = torch.exp(-square)
        return K
        
    
    def fit(self, X, y, max_iters = 1000, print_info = False):
        '''
        X是數據張量, size(n,m)
        數據維度m, 數據數目n
        y是二分類標籤, 只能是1或-1
        '''
        n,m = X.shape
        y = y.reshape(-1,1)
        self.weights = torch.zeros((n,1))
        self.bias = torch.zeros(1)
        self.train()
        
        # 計算數據集X的核矩陣
        K_mat = self.ker_func(X,X,self.sigma)
        
        for step in range(max_iters):
            out = K_mat.mm(self.weights)+self.bias # 前向計算
            # 損失計算
            rloss = 0.5*self.weights.T.mm(K_mat).mm(self.weights)
            closs = self.C*torch.sum(F.relu(-y*out+1))
            # 自動求導
            loss = rloss+closs
            loss.backward()
            # 梯度下降
            self.weights.data -= self.lr*self.weights.grad.data
            self.bias.data -= self.lr*self.bias.grad.data
            self.weights.grad.data.zero_()
            self.bias.grad.data.zero_()
            if (step+1)%20==0 and print_info:
                print("step %d, regularization loss: %.4f, classification loss: %.4f"%(step+1,rloss,closs))
        
        self.X = X
        return loss
    
    def predict(self, x, raw = False):
        self.eval()
        out = self.ker_func(x,self.X).mm(self.weights)+self.bias
        if raw: return out
        else: return torch.sign(out)
        
    def show_boundary(self, low1, upp1, low2, upp2):
        # meshgrid製造平面上的點
        x_axis = np.linspace(low1,upp1,100)
        y_axis = np.linspace(low2,upp2,100)
        xx, yy = np.meshgrid(x_axis, y_axis)
        data = np.concatenate([xx.reshape(-1,1),yy.reshape(-1,1)],axis = 1)
        data = torch.tensor(data).float()
        # 用模型預測
        out = self.predict(data, raw = True).reshape(100,100)
        out = out.numpy()
        Z = np.zeros((100,100))
        Z[np.where(out>0)] = 1.
        Z[np.where(out>1)] += 1.
        Z[np.where(out<-1)] = -1.
        # 繪製支持邊界
        plt.figure(figsize=(8,6))
        colors = ['aquamarine','seashell','paleturquoise','palegoldenrod']
        plt.contourf(xx, yy, Z, cmap=matplotlib.colors.ListedColormap(colors))

model = KerbSVM(C=1, lr = 0.01)
model.fit(X,y,max_iters = 2000)
model.show_boundary(-5,12,-5,10)
plt.scatter(X[:n,0].numpy(),X[:n,1].numpy() , marker = '+', c = 'r')
plt.scatter(X[n:,0].numpy(),X[n:,1].numpy() , marker = '_', c = 'r')

在這裏插入圖片描述

對偶SVM

上面的梯度下降優化方法是可行的, 但是考慮到SVM對超參數很敏感, 我們需要不斷地嘗試調整參數才能找到一組比較好的參數(比如上面的鬆弛變量C和高斯核帶寬sigma). 這就要求我們可能需要進行多次的交叉驗證. 但是多次交叉驗證的訓練開銷相對大, 這就成爲了SVM的應用上最大的阻礙.
但是我們對SVM的幾十年的研究並不是白費的, 研究者們總結出了一套把SVM轉爲對偶問題的方法, 並在約束的對偶問題裏進行基於二次規劃的座標下降優化. 我們還會用一些啓發式的方法幫助我們快速找到解.
我們先推導一下對偶形式的SVM, 我們改寫SVM的優化目標, 把ReLU的hinge loss那一項寫成一般的不等式約束形式, 同時優化目標函數是w的L2-norm.
minimize12w2+Ci=1Nξi minimize\quad \frac{1}{2}||w||^2+C\sum_{i=1}^N\xi_i
s.t.yi(wTxi+b)1ξi s.t.\quad y_i(w^Tx_i+b)\ge 1-\xi_i
ξi0 \xi_i \ge 0
其中xi是和hinge loss的意義相似的, 把不等式約束軟化的方法. 我們用xi放鬆對邊界的限制, 但是隨着xi增加, 我們會受到額外的L1懲罰. 解它的對偶問題有
Lagrangian:
L(w,b,ξ,α,r)=12w2+Ci=1Nξi+i=1Nαi[1ξiyi(wTxi+b)]i=1Nriξi L(w,b,\xi,\alpha,r) = \frac{1}{2}||w||^2+C\sum_{i=1}^N\xi_i+\sum_{i=1}^N\alpha_i[1-\xi_i-y_i(w^Tx_i+b)]-\sum_{i=1}^Nr_i\xi_i
pd2zeros:
Lw=wi=1Nαiyixi=0 \frac{\partial{L}}{\partial{w}} = w-\sum_{i=1}^N\alpha_i y_ix_i=0
Lb=i=1Nαiyi=0 \frac{\partial{L}}{\partial{b}} = -\sum_{i=1}^N\alpha_i y_i=0
Lξi=Cαiri=0 \frac{\partial{L}}{\partial{\xi_i}} = C-\alpha_i-r_i=0
回代原優化問題
minimizei=1Nαi12i=1Nj=1NαiαjyiyjxiTxj minimize\quad \sum_{i=1}^N\alpha_i -\frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N\alpha_i\alpha_jy_iy_jx_i^Tx_j
這是另一種推導SVM的線路, 因爲是有約束的凸優化形式, 我們可以很自然的把問題推廣爲對偶問題
maximizeα,rminw,b,ξL(w,b,ξ,α,r) maximize_{\alpha,r}\quad min_{w,b,\xi}L(w,b,\xi,\alpha,r)
αi0,ri0 \alpha_i\ge 0, r_i\ge 0
i=1Nαiyi=0 \sum_{i=1}^N\alpha_i y_i=0
Cαiri=0 C-\alpha_i-r_i=0
凸優化的對偶問題最優解和原問題相同, 我們可以直接處理對偶問題
maximizeαi=1Nαi12i=1Nj=1NαiαjyiyjxiTxj maximize_{\alpha}\quad \sum_{i=1}^N\alpha_i -\frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N\alpha_i\alpha_jy_iy_jx_i^Tx_j
0αiC 0\ge\alpha_i\le C
i=1Nαiyi=0 \sum_{i=1}^N\alpha_i y_i=0
因爲有向量內積, 我們還能很自然引入核函數實現非線性
maximizeαi=1Nαi12i=1Nj=1Nαiαjyiyjk(xi,xj) maximize_{\alpha}\quad \sum_{i=1}^N\alpha_i -\frac{1}{2}\sum_{i=1}^N\sum_{j=1}^N\alpha_i\alpha_jy_iy_jk(x_i,x_j)
到這裏, 我們推翻了前面的無約束版本的SVM, 引入了這種對偶版本的SVM. 對一般的問題, 我們也不知道一個凸優化的原問題和對偶問題哪一個比較容易求解, 但是SVM的對偶問題是極爲特殊的形式, 我們要優化的是對偶問題裏的alpha, 一旦求到了alpha的最優解, 我們就拿到了最終的SVM計算式
y=i=1Nαiyik(x,xi)+b y = \sum_{i=1}^N\alpha_i y_ik(x,x_i)+b
b可以選擇非0alpha對應的數據點計算=1或者=-1的恆等式得到. 那麼任務就是如何求解對偶問題, 這個對偶問題由一個等式約束, 一個不等式約束和一個優化目標組成. 現在, 我們就講一下怎麼用高效的方法解這個約束優化.

SMO算法

SMO使用座標下降的方法求解該問題, 每個優化step, 我們選擇所有alpha中的兩個alpha, 改變它們而固定其他. 又因爲存在等式約束i=1Nαiyi=0\sum_{i=1}^N\alpha_i y_i=0, 把它代入優化目標後要處理的變量只有一個,而且是一個關於該變量的二次函數. 剩下的二次函數和不等式約束組成了一個二次規劃問題.
maximizeαi,αjQ(αi,αj) maximize_{\alpha_i, \alpha_j} Q(\alpha_i, \alpha_j)
0αiC 0\ge\alpha_i\le C
0αjC 0\ge\alpha_j\le C
i=1Nαiyi=0 \sum_{i=1}^N\alpha_i y_i=0
我們每次優化都計算這個二次規劃的最優解, 把它更新到新的迭代點, 而且我們保證這樣的更新比梯度上升更有效, 因爲它每次更新都保證讓損失函數上升.
好, 那麼現在的問題就是求這個二次規劃的閉式解, 只要有了閉式解, 可以想象, 我們就能通過多次調用閉式解就完成優化, 而不需要執行反向傳播和梯度下降的多次迭代.
首先我們計算無約束時的最優解, 這個過程其實就是初中的二次函數極值問題
Q(α1,α2)=α1+α212K1,1y12α1212K2,2y22α22K1,2y1y2α1α2y1α1irestαiyiKi,1y2α2irestαiyiKi,2+C Q(\alpha_1, \alpha_2)=\alpha_1+\alpha_2-\frac{1}{2}K_{1,1}y_1^2\alpha_1^2-\frac{1}{2}K_{2,2}y_2^2\alpha_2^2 - K_{1,2}y_1y_2\alpha_1\alpha_2-y_1\alpha_1\sum_{i}^{rest}\alpha_iy_iK_{i,1}-y_2\alpha_2\sum_{i}^{rest}\alpha_iy_iK_{i,2}+C
我們由等式約束可以寫出
α1y1+α2y2=ζ=irestαiyi \alpha_1y_1+\alpha_2y_2 = \zeta = -\sum_i^{rest}\alpha_iy_i
α1=ζy1α2y1y2 \alpha_1= \zeta y_1-\alpha_2y_1y_2
回代到二次函數Q中, 得到一元二次函數
Q(α1,α2)=12K1,1(ζy12α12)212K2,2α22K1,2(ζα2y2)y2α2(ζα2y2)irestαiyiKi,1y2α2irestαiyiKi,2+α1+α2+C Q(\alpha_1, \alpha_2)=-\frac{1}{2}K_{1,1}(\zeta-y_1^2\alpha_1^2)^2-\frac{1}{2}K_{2,2}\alpha_2^2 - K_{1,2}(\zeta-\alpha_2y_2)y_2\alpha_2-(\zeta- \alpha_2y_2)\sum_{i}^{rest}\alpha_iy_iK_{i,1}-y_2\alpha_2\sum_{i}^{rest}\alpha_iy_iK_{i,2}+\alpha_1+\alpha_2+C
Qα2=(K1,1+K2,22K1,2)α2+K1,1ζy2K1,2ζy2+y2irestαiyiKi,1y2irestαiyiKi,2y1y2+y22=0\frac{\partial{Q}}{\partial{\alpha_2}} = -(K_{1,1}+K_{2,2}-2K_{1,2})\alpha_2+K_{1,1}\zeta y_2-K_{1,2}\zeta y_2+ y_2\sum_{i}^{rest}\alpha_iy_iK_{i,1}- y_2\sum_{i}^{rest}\alpha_iy_iK_{i,2}- y_1y_2+y_2^2 = 0
剩下的就是純粹的計算, 算到最後我們發現, 新的α2\alpha_2可以用舊的α2\alpha_2表示.
α2new=α2old+y2(E1E2)η \alpha_2^{new} = \alpha_2^{old}+\frac{y_2(E_1-E_2)}{\eta}
其中E是預測值與真實值的誤差Ei=f(xi)yiE_i = f(x_i)-y_i, η=K1,1+K2,2K1,2\eta = K_{1,1}+K_{2,2}-K_{1,2}
下一步就是考慮約束, 給出約束下的極值點. 我們考慮的約束是所謂"方形約束".
在這裏插入圖片描述
在這裏插入圖片描述
我們額外計算一下上下界, 並作clip就能計算出α2\alpha_2的值了, 然後我們用恆等式把α1\alpha_1的值也算出來, 這樣就結束了一次更新. SMO的步驟大致如此

選擇要更新的參數

每次更新我們都會面對所有n個α\alpha, 那麼要更新的是哪個呢. 我們從誤差出發, 我們上面定義的E是預測偏移標籤的值, 我們永遠選擇誤差最大的那一個α\alpha作爲α2\alpha_2更新, α1\alpha_1的話隨機選取即可.

class SVC:
    def __init__(self, C = 1, sigma = None):
        self.C = C
        self.sigma = sigma
        self.weights = None
        self.bias = None
    
    def ker_func(self, x1, x2, sigma = None):
        '''
        高斯核函數, sigma是高斯核的帶寬
        x1, x2是矩陣, 函數計算x1和x2的核矩陣
        '''
        n1,n2 = x1.shape[0],x2.shape[0]
        if sigma is None:
            sigma = 0.5/x1.shape[1]
        K = np.zeros((n1,n2))
        for i in range(n2):
            square = np.sum((x1-x2[i])**2, axis = 1)
            K[:,i] = np.exp(-square)
        return K
        
    def predict(self, x, raw = False):
        out = self.ker_func(x,self.X).dot(self.weights*self.y)+self.bias
        if raw: return out
        else: return np.sign(out)
    
    def fit(self, X, y, max_iters = 100):
        '''
        X是數據張量, size(n,m)
        數據維度m, 數據數目n
        y是二分類標籤, 只能是1或-1
        '''
        n,m = X.shape
        self.X = X
        y = y.reshape(-1,1)
        self.y = y
        self.weights = np.zeros((n,1))
        self.bias = np.zeros(1)
        
        # 計算數據集X的核矩陣
        K_mat = self.ker_func(X,X,self.sigma)
        alpha = self.weights
        b = self.bias
        C = self.C
        
        for t in range(max_iters):
            #首先爲各變量計算err
            con1 = alpha > 0
            con2 = alpha < C

            y_pred = K_mat.dot(alpha)+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)
            j = i
            while j==i:
                j = random.randint(0,n-1)

            #爲i和j計算函數值E = f(x)-y
            e1 = self.predict(X[i:i+1],raw = True)-y[i]
            e2 = self.predict(X[j:j+1],raw = True)-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:j+1] += y[i]*y[j]*(alpha[i]-alpha1_new)
            alpha[i:i+1] = alpha1_new

            #找支持向量並修正b
            b -= b
            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)
        
        # 保留支持向量和對應的權值
        '''
        indices = np.where(alpha[:,0]>0)
        self.X = self.X[indices]
        self.y = self.y[indices]
        self.weights = self.weights[indices]
        '''
        
        return alpha, b
        
    def show_boundary(self, low1, upp1, low2, upp2):
        # meshgrid製造平面上的點
        x_axis = np.linspace(low1,upp1,100)
        y_axis = np.linspace(low2,upp2,100)
        xx, yy = np.meshgrid(x_axis, y_axis)
        data = np.concatenate([xx.reshape(-1,1),yy.reshape(-1,1)],axis = 1)
        # 用模型預測
        out = self.predict(data, raw = True).reshape(100,100)
        Z = np.zeros((100,100))
        Z[np.where(out>0)] = 1.
        Z[np.where(out>1)] += 1.
        Z[np.where(out<-1)] = -1.
        # 繪製支持邊界
        plt.figure(figsize=(8,6))
        colors = ['aquamarine','seashell','paleturquoise','palegoldenrod']
        plt.contourf(xx, yy, Z, cmap=matplotlib.colors.ListedColormap(colors))


model = SVC()
model.fit(X.numpy(),y.numpy(),max_iters = 500)
model.show_boundary(-5,12,-5,10)
plt.scatter(X[:n,0].numpy(),X[:n,1].numpy() , marker = '+', c = 'r')
plt.scatter(X[n:,0].numpy(),X[n:,1].numpy() , marker = '_', c = 'r')

在這裏插入圖片描述
求解對偶問題的SMO算法比起傳統優化方法獲得的解是更穩定和高效的, 如果使用比上面更好的啓發方法, 收斂將會更快(參考sklearn).

SVM優缺點和使用注意事項

優點

  1. SVM是凸優化, 比神經網絡的解更加穩定, 理論基礎好.
  2. 在高維數據上很高效(計算的本質是矩陣運算, 數據維度越高越易並行化, 類似神經網絡)
  3. 能處理非線性數據, 比線性分類器性能更好

缺點
4. 在大規模數據上不高效, 數據越大要考慮的alpha越多.
5. 不容易應用於多分類, SVM一般通過構造多個二分類器才能實現多分類, 而神經網絡只需要單個模型.
6. 需要調參以防止核函數帶來的過擬合

使用指南
7. 標準化數據, 把數據縮放到-1,1或0,1區間會讓SVM更方便處理數據(尤其是多項式核)
8. 只保留一部分支持向量以減少內存的開銷.
9. 在較多分類問題或極大規模數據裏避免使用SVM
10. SVM對樣本不平衡的敏感度很高, 可以嘗試在SVM中添加數據的權重項來增強SVM在不平衡樣本上的表現.

實踐: 線性SVM分類乳腺癌樣本

from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn import datasets

X,y = datasets.load_breast_cancer(return_X_y=True)

y = 2*y-1
# 我們設置較高的test_size來觀察過擬合的發生
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.2, random_state = 1)

#數據標準化處理
sc_X = StandardScaler()
X_train = sc_X.fit_transform(X_train)
X_test = sc_X.transform(X_test)

model = LibSVM(C = 5)
model.fit(torch.tensor(X_train).float(),torch.tensor(y_train).float(),max_iters = 1000)

y_pred = model.predict(torch.tensor(X_test).float()).numpy()
accuracy_score(y_pred, y_test)

0.9736842105263158

實踐: 核SVM分類iris樣本

class Multi_class_SVC:
    def __init__(self, num_class, C = 1, sigma = None):
        '''
        使用ovo的構造方式, 每兩個類別之間構造一個二分類器
        '''
        self.num = num_class
        self.models = [[SVC(C = 1, sigma = None) for _ in range(i+1, num_class)]for i in range(num_class)]
        
    def fit(self, X, y, max_iters = 100):
        for i in range(self.num):
            for j in range(i+1, self.num):
                j_ = j-(i+1)
                indices_i = np.where(y==i)[0]
                indices_j = np.where(y==j)[0]
                data = np.concatenate([X[indices_i],X[indices_j]],axis = 0)
                labels = np.concatenate([
                    -np.ones(len(indices_i)),
                    np.ones(len(indices_j))],
                    axis = 0)
                self.models[i][j_].fit(data,labels,max_iters)
                
    def predict(self, x):
        '''
        採用投票制
        '''
        n = x.shape[0]
        votes = np.zeros((n,self.num))
        for i in range(self.num):
            for j in range(i+1, self.num):
                j_ = j-(i+1)
                y_pred_part = self.models[i][j_].predict(x).squeeze()
                indices = np.zeros(n).astype(np.int)
                indices[y_pred_part==-1] = i
                indices[y_pred_part==1] = j
                votes[range(n),indices] += 1
                
        return np.argmax(votes, axis = 1)

X,y = datasets.load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size = 0.2, random_state = 1)

model = Multi_class_SVC(3,C = 5)
model.fit(X_train,y_train,max_iters = 500)
y_pred = model.predict(X_test)
accuracy_score(y_pred, y_test)

1.0

備註

儘管上面的梯度下降是由torch自動求導得到的, 但是如果要自己手算導數也並不是困難的事情. 以kernel SVM的損失爲例
Lα=12(Kα+diag(K))C(yiK:j)if yi(i=1Nαik(x,xi)+b)+10\frac{\partial L}{\partial \alpha}= \frac{1}{2}(K\alpha+diag(K))-C(y_i K_{:j}) \qquad if \ -y_i (\sum_{i=1} ^{N} \alpha_i k(x,x_i)+b)+1 \ge 0
Lα=12(Kα+diag(K))if yi(i=1Nαik(x,xi)+b)+1<0\frac{\partial L}{\partial \alpha}= \frac{1}{2}(K\alpha+diag(K)) \qquad if \ -y_i (\sum_{i=1} ^{N} \alpha_i k(x,x_i)+b)+1 \lt 0
Lb=Cyiif yi(i=1Nαik(x,xi)+b)+10\frac{\partial L}{\partial b}= -C*y_i \qquad if \ -y_i (\sum_{i=1} ^{N} \alpha_i k(x,x_i)+b)+1 \ge 0
Lb=0if yi(i=1Nαik(x,xi)+b)+1<0\frac{\partial L}{\partial b}= 0 \qquad if \ -y_i (\sum_{i=1} ^{N} \alpha_i k(x,x_i)+b)+1 \lt 0

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