SMO序列最小最優化算法

首先回顧一下SVM模型的數學表達,即svm的對偶問題:

mina12i=1Nj=1NaiajyiyjK(xi,xj)i=1Nais.t.i=1Naiyi=00aiC,i=1,2,,N

選擇一個 a 的正分量 0<aj<C , 計算(或者通過所有解求平均值):

b=yji=1NaiyiK(xixj)

決策函數爲

f(x)=sign(i=1NaiyiK(xi,xj)+b)

svm的學習,就是通過訓練數據計算出ab,然後通過決策函數判定xj的分類。其中a是一個向量,長度與訓練數據的樣本數相同,如果訓練數據很大,那麼這個向量會很長,不過絕大部分的分量值都是0,只有支持向量的對應的分量值大於0 。

SMO是一種啓發式算法,其基本思想是:如果所有變量的解都滿足了此最優化問題的KKT條件,那麼這個最優化問題的解就得到了。否則,選擇兩個變量,固定其它變量,針對這兩個變量構建一個二次規劃問題,然後關於這個二次規劃的問題的解就更接近原始的二次歸還問題的解,因爲這個解使得需要優化的問題的函數值更小。

翻譯一下:對於svm我們要求解a,如果 a 的所有分量滿足svm對偶問題的KKT條件,那麼這個問題的解就求出來了,我們svm模型學習也就完成了。如果沒有滿足KKT,那麼我們就在 a 中找兩個分量 ai 和 aj,其中 ai 是違反KKT條件最嚴重的分量,通過計算,使得 ai 和 aj 滿足KKT條件,直到a 的所有分量都滿足KKT條件。而且這個計算過程是收斂的,因爲每次計算出來的新的兩個分量,使得對偶問題中要優化的目標函數(就是min對應的那個函數)值更小。至於爲什麼是收斂的,是因爲,每次求解的那兩個分量,是要優化問題在這兩個分量上的極小值,所以每一次優化,都會使目標函數比上一次的優化結果的值變小。

我們來看看KKT條件。

KKT

上面的問題,是通過svm的原始問題,構造拉格朗日函數,並通過對偶換算得出的對偶問題。與對偶問題等價的是對偶問題的KKT條件,參考《統計學習方法》的附錄C的定理C.3。換句話說,就是隻要找到對應的a滿足了下列KKT條件,那麼原始問題和對偶問題就解決了。

SVM的對偶問題對應的KKT條件爲:

ai=0yig(xi)10<ai<Cyig(xi)=1ai=Cyig(xi)1

其中:

g(x)=i=1NaiyiK(xi,xj)+b

因爲計算機在計算的時候是有精度範圍的,所以我們引入一個計算精度值ε

ai=0yig(xi)1ε0<ai<C1εyig(xi)1+εai=Cyig(xi)1+ε{ai<C1εyig(xi)0<aiyig(xi)1+ε}

同時由於yi=±1,所以yiyi=1,上面的公式可以換算爲

ai<Cεyi(g(xi)yi)0<aiyi(g(xi)yi)+ε

定義:

Ei=g(xi)yi

其中,g(x)其實就是決策函數,所以Ei可以認爲是對輸入的xi的預測值與真實輸出yi之差。

上面的公式就可以換算爲,即KKT條件可以表示爲:

ai<CεyiEi0<aiyiEi+ε

那麼相應的違規KKT條件的分量應該滿足下列不等式:

Against KKT:
ai<Cε>yiEi0<aiyiEi>+ε

其實上面的推導過程不必關心,只需要應用違犯KKT條件的公式就可以了。

SMO算法描述

輸入:訓練數據集 T={(x_1,y_1),(x_2,y_2),,(x_N,y_N)}

其中xiχRnyi{1,+1}i=1,2,,N,精度ε

輸出:近似解a^

算法描述:

(1) 取初始值a(0)=0,令K=0

(2) 選取優化變量 a(k)1 , a(k)2 , 針對優化問題,求得最優解 a(k+1)1 , a(k+1)2 更新 a(k) 爲 a(k+1) 。

(3) 在精度條件範圍內是否滿足停機條件,即是否有變量違反KKT條件,如果違反了,則令k=k+1,跳轉(2),否則(4)。

(4) 求得近似解a^=a(k+1)

上面算法的(1)、(3)、(4)步都不難理解,其中第(3)步中,是否違反KKT條件,對於a(k)的每個分量按照上一節的違反KKT條件的公式進行驗算即可。難於理解的是第(2)步,下面就重點解釋優化變量選取和如何更新選取變量。

變量選取

變量選取分爲兩步,第一步是選取違反KKT條件最嚴重的ai,第二步是根據已經選取的第一個變量,選擇優化程度最大的第二個變量。

違反KKT條件最嚴重的變量可以按照這樣的規則選取,首先看0<ai<C的那些分量中,是否有違反KKT條件的,如果有,則選取yig(xi)最小的那個做爲a1。如果沒有則遍歷所有的樣本點,在違反KKT條件的分量中選取yig(xi)最小的做爲a1

當選擇了a1後,如果a1對應的E1爲正,選擇Ei最小的那個分量最爲a2,如果E1爲負,選擇Ei最大的那個分量最爲a2,這是因爲anew2依賴於|E1E2|(後面的公式會講到)。 如果選擇的a2,不能滿足下降的最小步長,那麼就遍歷所有的支持向量點做爲a2進行試用,如果仍然都不能滿足下降的最小步長,那麼就遍歷所有的樣本點做爲a2試用。如果還算是不能滿足下降的最小步長,那麼就重新選擇a1

計算選取變量的新值

首先計算出來的新值必須滿足約束條件i=1Naiyi=0 ,那麼求出來的anew2需要滿足下列條件(具體推導見《統計學習方法》的7.4.1):

Lanew2HL=max(0,aold2aold1),H=min(C,C+aold2aold1),y1y2L=max(0,aold2+aold1C),H=min(C,aold2+aold1),y1=y2

未經過裁剪的a2的解爲:

anew,unc2=aold2+y2(E1E2)ηη=K11+K222K12

裁剪後的解爲

anew2=H,anew,unc2>Hanew,unc2,Lanew,unc2HL,anew,unc2<L

第一個變量的解爲

anew1=aold1+y1y2(aold2anew2)

還需要更新b:

bnew1=E1y1K11(anew1aold1)y2K21(anew2aold2)+boldbnew2=E2y1K12(anew1aold1)y2K22(anew2aold2)+bold

在更新b時,如果有0<anew1<C, 則bnew=bnew1,如果有0<anew2<C, 則 bnew=bnew2, 否則bnew=bnew1+bnew22

由於緩存了Ei,所以需要計算新的Ei:

Enewi=j=1NyjajK(xi,xj)+bnewyi

SMO的一個實現例子

我實現了一個簡單的基於SMO的線性svm,是一個python腳本。實現的過程中,變量的選取並未嚴格按照算法講的方法選取,選擇了一個簡單的選取方法。 一次迭代中,遍歷所有的ai,如果ai違反了KKT條件,那麼就將它做爲第一個變量,然後再遍歷所有的ai,依次做爲第二個變量,如果第二個變量有足夠的下降,那麼就更新兩個變量。如果沒有,就不更新。

實現的python腳本如下:

使用python實現的基於SMO的SVM (smo.py)download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# SMO的一個簡單實現
# implement SMO

import sys
import math
import matplotlib.pyplot as plt

samples = []
labels = []
class svm_params:
    def __init__(self):
        self.a = []
        self.b = 0

params = svm_params()
e_dict = []

#train_data = "svm.train_mix_ok"
train_data = "svm.train"

def loaddata():
    fn = open(train_data,"r")
    for line in fn:
        line = line[:-1]
        vlist = line.split("\t")
        samples.append((int(vlist[0]), int(vlist[1])))
        labels.append(int(vlist[2]))
        params.a.append(0.0)
    fn.close()

# linear
def kernel(j, i):
    ret = 0.0
    for idx in range(len(samples[j])):
        ret += samples[j][idx] * samples[i][idx]
    return ret

def predict_real_diff(i):
    diff = 0.0
    for j in range(len(samples)):
        diff += params.a[j] * labels[j] * kernel(j,i)
    diff = diff + params.b - labels[i]
    return diff

def init_e_dict():
    for i in range(len(params.a)):
        e_dict.append(predict_real_diff(i))

def update_e_dict():
    for i in range(len(params.a)):
        e_dict[i] = predict_real_diff(i)

def train(tolerance, times, C):
    time = 0
    init_e_dict()
    updated = True
    while time < times and updated:
        updated = False
        time += 1
        for i in range(len(params.a)):
            ai = params.a[i]
            Ei = e_dict[i]
            # 違反KKT
            # agaist the KKT
            if (labels[i] * Ei < -tolerance and ai < C) or (labels[i] * Ei > tolerance and ai > 0):
                for j in range(len(params.a)):
                    if j == i: continue
                    eta = kernel(i, i) + kernel(j, j) - 2 * kernel(i, j)
                    if eta <= 0:
                        continue
                    new_aj = params.a[j] + labels[j] * (e_dict[i] - e_dict[j]) / eta
                    L = 0.0
                    H = 0.0
                    if labels[i] == labels[j]:
                        L = max(0, params.a[j] + params.a[i] - C)
                        H = min(C, params.a[j] + params.a[i])
                    else:
                        L = max(0, params.a[j] - params.a[i])
                        H = min(C, C + params.a[j] - params.a[i])
                    if new_aj > H:
                        new_aj = H
                    if new_aj < L:
                        new_aj = L
                    # 《統計學習方法》公式7.109(下同)
                    # formula 7.109
                    new_ai = params.a[i] + labels[i] * labels[j] * (params.a[j] - new_aj)

                    # 第二個變量下降是否達到最小步長
                    # decline enough for new_aj
                    if abs(params.a[j] - new_aj) < 0.001:
                        print "j = %d, is not moving enough" % j
                        continue

                    # formula 7.115
                    new_b1 = params.b - e_dict[i] - labels[i]*kernel(i,i)*(new_ai-params.a[i]) - labels[j]*kernel(j,i)*(new_aj-params.a[j])
                    # formula 7.116
                    new_b2 = params.b - e_dict[j] - labels[i]*kernel(i,j)*(new_ai-params.a[i]) - labels[j]*kernel(j,j)*(new_aj-params.a[j])
                    if new_ai > 0 and new_ai < C: new_b = new_b1
                    elif new_aj > 0 and new_aj < C: new_b = new_b2
                    else: new_b = (new_b1 + new_b2) / 2.0

                    params.a[i] = new_ai
                    params.a[j] = new_aj
                    params.b = new_b
                    update_e_dict()
                    updated = True
                    print "iterate: %d, changepair: i: %d, j:%d" %(time, i, j)

def draw(tolerance, C):
    plt.xlabel(u"x1")
    plt.xlim(0, 100)
    plt.ylabel(u"x2")
    plt.ylim(0, 100)
    plt.title("SVM - %s, tolerance %f, C %f" % (train_data, tolerance, C))
    ftrain = open(train_data, "r")
    for line in ftrain:
        line = line[:-1]
        sam = line.split("\t")
        if int(sam[2]) > 0:
            plt.plot(sam[0],sam[1], 'or')
        else:
            plt.plot(sam[0],sam[1], 'og')

    w1 = 0.0
    w2 = 0.0
    for i in range(len(labels)):
        w1 += params.a[i] * labels[i] * samples[i][0]
        w2 += params.a[i] * labels[i] * samples[i][1]
    w = - w1 / w2

    b = - params.b / w2
    r = 1 / w2

    lp_x1 = [10, 90]
    lp_x2 = []
    lp_x2up = []
    lp_x2down = []
    for x1 in lp_x1:
        lp_x2.append(w * x1 + b)
        lp_x2up.append(w * x1 + b + r)
        lp_x2down.append(w * x1 + b - r)
    plt.plot(lp_x1, lp_x2, 'b')
    plt.plot(lp_x1, lp_x2up, 'b--')
    plt.plot(lp_x1, lp_x2down, 'b--')
    plt.show()

if __name__ == "__main__":
    loaddata()
    print samples
    print labels
    # 懲罰係數
    # penalty for mis classify
    C = 10
    # 計算精度
    # computational accuracy 
    tolerance = 0.0001
    train(tolerance, 100, C)
    print "a = ", params.a
    print "b = ", params.b
    support =  []
    for i in range(len(params.a)):
        if params.a[i] > 0 and params.a[i] < C:
            support.append(samples[i])
    print "support vector = ", support
    draw(tolerance, C)

腳本使用的訓練數據可以下載SMO實現的代碼的svm.train文件,或者使用blog_linear.py,通過改變變量separable可以生成能夠完全劃分開的樣本和不能劃分開的樣本。

這個smo.py腳本是一個線性的svm,替換掉腳本中kernel函數,就可以成爲一個非線性的svm。 下面這兩張圖片是用訓練數據訓練的結果。這一張是樣本能完全分離開的:

這一張是樣本不能完全分離開的:

以上就是如何實現SMO的全部內容。之前的一個同事實現了一個簡單的識別手寫數字ocr,下一章,我們也來用svm實現一個簡單的識別數字的ocr吧。

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