斯坦福CS231n assignment1:SVM圖像分類原理及實現

分類
本文Github代碼

斯坦福CS231n課程講解了實現圖像分類的方法,從傳統的KNN,SVM,到CNN,LSTM模型,講解的非常專業精準。同時該課程提供了相應的習題來檢驗和鞏固講授的知識,如果能按部就班的完成,對神經網絡將會有深刻的體會和理解。本文將結合代碼實現講解其中的SVM方法實現圖像分類的原理和方法,以及需要注意的知識細節。

SVM模型原理

SVM通過平面將空間中的實例劃分到不同的類別,從而實現分類。這裏的空間包括二維空間,三維空間,一直到高維空間,具體的維數等於實例的特徵數量,如果我們待分類的圖片是32*32*3(長寬分別是32個像素,RGB3個顏色通道)維的,那麼圖片所處的空間就是3072維的空間。在這個高維空間,我們通過由權重向量W和偏置項b確定的一個(實際上是一組)超平面來將圖片進行分類。爲了可視化,我們將多維空間壓縮到二維空間,那麼就是下面的圖像:
通過超平面進行分類

這裏每一個平面都將整個高維空間劃分成兩部分,平面的一側是某一類圖片,另一側是這個類別之外的其他圖片。比如紅色的平面一側是汽車這個類別,另一側是非汽車類別。每一個類別都對應一個平面,這些平面互相之間不存在關聯,利用SVM模型進行分類的目的就是確定這樣一組平面,使得同一類儘可能劃分在該類對應的平面的一側,其他類儘可能在另一側,而且兩種類別離平面的距離越大越好(平面儘可能把兩類分的更開),這是SVM模型的思路。

所有這些類別對應的平面通過下面的矩陣唯一確定:
線性映射矩陣

其中改變W可以使平面旋轉,而改變b使平面平移。如果b爲0,此時W*0=0,那麼平面會經過原點。

SVM的一種直觀解釋

SVM模型用於圖像分類可以看做給每一種圖像的類別生成一個圖像模板,然後拿待分類的圖像和這個圖像模板做內積,計算他們的相似度,相似度最高的類別就是分類類別。根據這個思想,生成的權重向量可視化如下:

圖像模板

可以看出,這些圖像模板比較能夠代表某種類別的共性,比如car類別是一輛紅色的車的形象,而horse類型是左右兩匹馬的形象,這些是集合了所有訓練樣本得出的模板。從這個角度,SVM可以看做KNN模型的一種簡化,KNN模型對一張圖片分類時需要和所有訓練樣本做比較,而SVM只需要和抽象出來的每個類別下的一個圖像模板做比較即可,顯然更高效。

損失函數

SVM模型有多種不同的實現,區別主要體現在損失函數的定義上,可以根據實現分爲:

  1. 經典SVM
  2. Structured SVM

其中經典SVM模型核心思路是找一個超平面將不同類別分開,同時使得離超平面最近的點的距離最大,這樣能保證即使是最難區分的點,也有較大的確信度將它們分開,可以通過數學方法證明這樣的超平面是唯一存在的(參考《統計學習方法》)。

而Structured SVM可以看做是對經SVM模型的泛化,則是通過事先構造損失函數來求解一個統計上的最優解,該方式避免了經典SVM簡單粗暴的懲罰方式(比如分錯懲罰1,分對懲罰0),對分類錯誤程度不同的樣本進行程度不同的懲罰,對實際訓練數據中噪音數據具有更大的容錯性,分類效果也更好。

Structured SVM模型中我們使用摺頁損失函數(hinge loss function)來計算損失值。摺頁損失函數可以有不同的表示,我們使用如下的表示:

摺頁損失

某個樣本經過f映射後會得到N個分值,對應爲N個類別的得分。這N個類別中只有一個類別是這個樣本的實際類別,用yi表示。類別yi的分值用Syi表示,其他不正確的類別得分用Sj表示,Δ是一個距離閾值。從這個摺頁損失函數的定義可以看出,這個樣本在N個類別上的得分,有些會產生損失,有些不會:

  1. 得分比正確類別yi高的類別肯定會產生損失,對於這些類別Sj>Syi,所以Sj-Syi+Δ>0,這說明正確分類的分值應該是最高的。
  2. 得分比正確類別yi低的那些類別也有可能會產生損失,如果Sj<Syi,但是他們的距離太近,小於一個Δ值,也就是Syi-Sj<Δ,此時Sj-Syi+Δ>0,也會貢獻損失。這說明我們不僅希望錯誤類別得分比正確類別低,而且要至少低一個Δ值

損失函數加入正則化項

損失函數數值的大小跟權重參數W成正比,對於同一個樣本,成倍的擴大權重向量W會導致損失值成倍增加,這是損失值的變化沒有意義的,爲了約束這種變化,我們在損失函數中添加一個正則化項,L2正則化項定義如下:

正則化損失項

這樣,當權重擴大的時候會導致損失擴大,然後通過反向傳遞作用回權重矩陣W,從而對過大的權重加以修正,避免了權重向量的單向增大。

在數據損失(data loss)的基礎上,添加了L2正則化損失(regularization loss)項的損失函數如下:
加入L2正則項的損失函數

舉個例子,假設輸入向量x=[1, 1, 1, 1], 權重向量W1=[1, 0, 0, 0],權重向量 W2=[0.25, 0.25, 0.25, 0.25],W1*x = 1,W2*x=1,可以看到兩個內積是一樣的。但是W1的L2懲罰是1,而W2的L2懲罰是0.25,所以會更傾向於選擇W2。直觀上解釋就是L2懲罰希望各個權重分量是比較均衡的,而不是某些分量權重一支獨大,因爲這樣更能利用起樣本中各個特徵,而避免過度依賴某幾個特徵。

展開損失函數:
損失函數

這裏Δ和λ是兩個超參數,它們的值是如何確定的?
實際上Δ控制的是損失函數中數據損失部分,λ控制正則化損失部分,這兩個變量對損失的貢獻是正相關的,實際上它們兩個共同控制着損失函數中數據損失和正則化損失的權衡,所以同時調整兩個值是沒意義的。實際使用中,我們一般設置Δ=1爲固定值,而通過交叉驗證的方式調整λ值。也就是選擇不同的λ,通過訓練數據進行模型訓練,在驗證集上驗證,然後調整λ,看看結果是不是更優,最終選擇一個最優的λ,確定最優的模型。

梯度下降和梯度檢驗

定義好損失函數後,我們就可以通過梯度下降的方法利用訓練集來更新權重參數,也就是模型訓練的過程。

SVM模型中有兩個參數,權重矩陣W和偏移量b,我們可以將這兩個變量合併起來訓練,將b作爲一列添加在W後面,同時在輸入向量X裏添加一行,值爲1,這樣就可以值訓練一個合併後的矩陣:

W和b合併技巧

梯度下降的核心就是計算損失函數對各個權重分量的梯度,然後根據梯度更新各個權重。

對SVM損失函數進行梯度計算,根據損失函數定義,一個樣本梯度的計算分爲正確分類權重對應行和不正確分類權重對應的行兩種情況,數據損失項的梯度爲下面兩個公式:
錯誤分類項的梯度

正確分類項的梯度

正則化損失項的梯度很簡單,2Wij,可以在正則化損失前面添加一個參數0.5,這樣可以將正則化損失項的梯度變爲Wij,在代碼中會講到這一點。

這樣我們就得到了梯度的解析解,在利用梯度進行權重更新之前,我們還需要驗證一下這個梯度解析解是不是正確,方法就是利用梯度的定義求解權重向量分量的梯度的數值解,比較數值解和解析解,如果兩者的差距非常小,說明解析解是正確的,否則說明有誤,需要查找錯誤。

梯度數值解的計算很簡單,根據下面梯度的公式,在給定的樣本和權重矩陣下,計算輸出,然後讓權重矩陣的變量發生微小的變化,再計算輸出,兩次輸出的差值除以變化量就是權重矩陣的梯度:

梯度的公式

檢驗沒問題後,就可以進行訓練了。

圖像預處理

通常我們會將所有圖片,包括訓練數據和待分類數據,減去圖片每個位置像素的均值,使得數據中心化,這樣可以提高模型的效果。同時,也可以對中心化後的數據歸一化處理,使其分佈在[-1, 1]區間,進一步優化模型效果。

小批量數據梯度下降(Mini-batch gradient descent)

相比於每次拿一個樣例數據進行梯度更新,每次使用一個小批量數據進行梯度更新能夠更好的避免單個樣本數據的擾動,可以顯著提高模型訓練的效率,損失的變化更加平滑,使得模型更快的收斂。具體操作方法是一次計算一個批量的數據的結果,如256個樣本,計算每個結果下權重矩陣每個變量的梯度。對於每個權重分量,累加256個梯度值,求均值作爲該分量的梯度,更新該分量。

代碼實現

完整的代碼在github,包括訓練數據,測試數據的獲取,圖像預處理等,這裏只給出計算損失和梯度的關鍵代碼:

def svm_loss_vectorized(W, X, Y, reg):
    """
    :param X: 200 X 3073
    :param Y: 200
    :param W: 3073 X 10
    :return: reg: 正則化損失係數(無法通過拍腦袋設定,需要多試幾個值,交叉驗證,然後找個最優的)
    """
    delta = 1.0
    num_train = X.shape[0]

    patch_X = X  # 200 X 3073
    patch_Y = Y  # 200

    patch_result = patch_X.dot(W)  # 200 X 3073 3073 X 10 -> 200 X 10

    sample_label_value = patch_result[[xrange(patch_result.shape[0])], patch_Y]  # 1 X 200 切片操作,將得分array中標記位置的得分取出來作爲新的array
    loss_array = np.maximum(0, patch_result - sample_label_value.T + delta)  # 200 X 10 計算誤差
    loss_array[[xrange(patch_result.shape[0])], patch_Y] = 0  # 200 X 10 將label值所在的位置誤差置零

    loss = np.sum(loss_array)

    loss /= num_train  # get mean

    # regularization: 這裏給損失函數中正則損失項添加了一個0.5參數,是爲了後面在計算損失函數中正則化損失項的梯度時和梯度參數2進行抵消
    loss += 0.5 * reg * np.sum(W * W)

    # 將loss_array大於0的項(有誤差的項)置爲1,沒誤差的項爲0
    loss_array[loss_array > 0] = 1  # 200 X 10

    # 沒誤差的項中有一項是標記項,計算標記項的權重分量對誤差也有共享,也需要更新對應的權重分量
    # loss_array中這個參數就是當前樣本結果錯誤分類的數量
    loss_array[[xrange(patch_result.shape[0])], patch_Y] = -np.sum(loss_array, 1)

    # patch_X:200X3073  loss_array:200 X 10   -> 10*3072
    dW = np.dot(np.transpose(patch_X), loss_array)  # 3073 X 10
    dW /= num_train  # average out weights
    dW += reg * W  # regularize the weights

    return loss, dW

該模型的準確率在38%左右,所以SVM模型分類效果還是比較差的,通過更復雜的神經網絡,或者CNN等模型,可以實現95%以上的準確率。不過SVM是神經網絡的基礎,理解了SVM的原理,再學習神經網絡就比較容易觸類旁通。

本文Github代碼

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