概述:
由於KNN算法的侷限性,我們需要實現更強大的方法來實現圖像分類,一般情況下該方法包含兩個函數,一是評分函數(score function),它是原始圖像到每個分類的分值映射,二是損失函數(loss function),它是用來量化預測分類標籤的得分與真實標籤的一致性的。該方法可以轉化爲一個最優化問題,在最優化過程中,將通過更新評分函數的參數來最小化損失函數的值,從而使得我們找到一個更好的評分函數(參數W)。
從圖像到標籤分值的參數化映射
評分函數將圖像的像素值映射爲各個類別的得分,得分越高說明越有可能屬於該類別。
評分函數:
我們定義一個簡單的函數映射:$ f(x_i,W,b) = Wx_i + b $
需要注意的幾點:
1.該方法的一個優勢是訓練數據是用來學習到參數W和b的,一旦訓練完成,訓練數據就可以丟棄,留下學習到的參數即可。這是因爲一個測試圖像可以簡單地輸入函數,並基於計算出的分類分值來進行分類。
2.輸入數據是給定不變的,我們的目的是通過設置權重W和偏差值b,使得計算出來的分類分值情況和訓練集中圖像的數據真實類別標籤相符合
一個將圖像映射到分類分值的例子。爲了便於可視化,假設圖像只有4個像素(都是黑白像素,這裏不考慮RGB通道),有3個分類(紅色代表貓,綠色代表狗,藍色代表船,注意,這裏的紅、綠和藍3種顏色僅代表分類,和RGB通道沒有關係)。首先將圖像像素拉伸爲一個列向量,與W進行矩陣乘,然後得到各個分類的分值。需要注意的是,這個W一點也不好:貓分類的分值非常低。從上圖來看,算法倒是覺得這個圖像是一隻狗。
偏差和權重的合併技巧:
在進一步學習前,要提一下這個經常使用的技巧。它能夠將我們常用的參數W和b合二爲一。回憶一下,分類評分函數定義爲:
$f(x_i,W,b) = Wx_i + b $
分開處理這兩個參數(權重參數W和偏差參數b)有點笨拙,一般常用的方法是把兩個參數放到同一個矩陣中,同時x_i向量就要增加一個維度,這個維度的數值是常量1,這就是默認的偏差維度。這樣新的公式就簡化成下面這樣:
還是以CIFAR-10爲例,那麼x_i的大小就變成[3073x1],而不是[3072x1]了,多出了包含常量1的1個維度)。W大小就是[10x3073]了。W中多出來的這一列對應的就是偏差值b,具體見下圖:
偏差技巧的示意圖。左邊是先做矩陣乘法然後做加法,右邊是將所有輸入向量的維度增加1個含常量1的維度,並且在權重矩陣中增加一個偏差列,最後做一個矩陣乘法即可。左右是等價的。通過右邊這樣做,我們就只需要學習一個權重矩陣,而不用去學習兩個分別裝着權重和偏差的矩陣了.
損失函數
假設對於輸入的一個圖片,我們通過評分函數計算出它對於每一個分類的得分,那麼怎麼去評價他的好壞呢?這時候我們需要使用損失函數來衡量我們對該評分的滿意程度。我們可以通過調整參數W來使得評分函數的結果(最高分所在類別)與訓練集真實標籤是一致的。一般來說,評分函數輸出結果與真實結果差別越大,損失函數值越大,反之越小。
多分類支持向量機(multi-SVM)的損失函數
我們定義其損失函數爲 對於第i個輸入數據
$ L_i = \sum_{j \ne y_i} max(0,s_j - s_{y_i} + \Delta)$
我們這裏定義每一類的得分爲s,即爲對於第i個輸入數據經過評分函數後在第j個分類的得分,表示對於第i個輸入數據經過評分函數,在正確分類處的得分。 這裏是一個邊界值,具體的意思就是,Multi-SVM 我不關心正確的分類得分,我關心的是對於我在正確分類處所得的分數,是否比我在錯誤分類處所得的分數高,而且要出一定邊界值$\Delta \Delta$ 那麼我便不管它,否則我就需要計算我的損失
舉例:
用一個例子演示公式是如何計算的。假設有3個分類,並且得到了分值s=[13,-7,11]。其中第一個類別是正確類別,即同時設 上面的公式是將所有不正確分類加起來,所以我們得到兩個部分:
可以看到第一個部分結果是0,這是因爲[-7-13+10]得到的是負數,經過max(0,-)函數處理後得到0。這一對類別分數和標籤的損失值是0,這是因爲正確分類的得分13與錯誤分類的得分-7的差爲20,高於邊界值10。而SVM只關心差距至少要大於10,更大的差值還是算作損失值爲0。第二個部分計算[11-13+10]得到8。雖然正確分類的得分比不正確分類的得分要高(13>11),但是比10的邊界值還是小了,分差只有2,這就是爲什麼損失值等於8。簡而言之,SVM的損失函數想要正確分類類別的分數比不正確類別分數高,而且至少要高。如果不滿足這點,就開始計算損失值。
還必須提一下的屬於是關於0的閥值:函數,它常被稱爲折葉損失(hinge loss)。有時候會聽到人們使用平方折葉損失SVM(即L2-SVM),它使用的是,將更強烈(平方地而不是線性地)地懲罰過界的邊界值。不使用平方是更標準的版本,但是在某些數據集中,平方折葉損失會工作得更好。可以通過交叉驗證來決定到底使用哪個。
梯度推導:
這裏需要矩陣求導公式:
代碼實現:
"""
不加入正則化的多分類SVM的損失函數的實現,採用三種方法,一是兩層循環,二是一層循環,三是不用循環
"""
import numpy as np
def L_i(x,y,W,reg):
"""
unvectorized version. Compute the multiclass svm loss for a single example (x,y)
- x is a column vector representing an image (e.g. 3073 x 1 in CIFAR-10)
with an appended bias dimension in the 3073-rd position (i.e. bias trick)
- y is an integer giving index of correct class (e.g. between 0 and 9 in CIFAR-10)
- W is the weight matrix (e.g. 10 x 3073 in CIFAR-10)
"""
delta = 1.0 #delta 爲1.0 一般是安全的
# f(xi,W) = Wx,x 爲 3073 * 1,W = 10 * 3073
scores = np.dot(W,x)
correct_class_score = scores[y]
D = W.shape[0]
loss_i = 0.0
for j in xrange(D):
if j == y: # 對所有錯誤的分類進行Iterate,j == y 爲正確classification,so skip it.
continue
"""
multi SVM 只關注正確評分的損失,即要求正確分類所獲得的分數要大於錯誤分類所獲得分數,且至少要大delta,
否則就計算loss.
"""
loos_i += max(0,scores[j] - correct_class_score[y] + delta)
return loss_i
def L_i_vectorized(x,y,W):
"""
A faster half-vectorized implementation. half-vectorized
refers to the fact that for a single example the implementation contains
no for loops, but there is still one loop over the examples (outside this function)
需要一個循環對每個 test 進行call L_i_vectorized.
"""
delta = 1.0
scores = np.dot(W,x) # W = 10 * 3073 , x = 3073 * 1
margins = np.maximum(0,scores - scores[y] + delta) # 矩陣運算
margins[y] = 0
loss_i = np.sum(margins)
return loss_i
def L(X,y,W):
"""
fully-vectorized implementation :
- X holds all the training examples as columns (e.g. 3073 x 50,000 in CIFAR-10)
- y is array of integers specifying correct class (e.g. 50,000-D array)
- W are weights (e.g. 10 x 3073)
不用循環實現,這裏可以利用numpy的broadcasting機制.
"""
delta = 1.0
scores = np.dot(W,X)# 10 * 50000
num_train = X.shape[1]
num_class = W.shape[0]
scores_correct = scores[y,np.arange(num_train)] # 1 * 50000
scores_correct = np.reshape(scores_correct,(1,num_train)) # 1 * 50000
# scores is 10 * 50000, broadcasting makes scores_correct to 10 * 50000
margins = scores - scores_correct + delta
margins = np.maximum(margins,0)
margins[y,np.arange(num_train)] = 0 # 正確分類不計算
loss_i = np.sum(margins)
return loss_i
"""
我們有評分函數爲 F = WX. 怎麼求W呢?還是使用gradient descent.初始給W隨機比較小的值,
如: {
生成一個很小的SVM隨機權重矩陣
真的很小,先標準正態隨機然後乘0.0001
}
W = np.random.randn(3073, 10) * 0.0001
一邊計算損失函數,一遍計算W的梯度dW,對於損失函數loss關於W的偏導數,我們這裏還是使用了矩陣求導公式
"""
def svm_loss_naive(W,X,y,lamda):
"""
使用循環實現的SVM loss 函數。
輸入維數爲D,有C類,我們使用N個樣本作爲一批輸入。
輸入:
-W: 一個numpy array,形狀爲 (D, C) ,存儲權重。
-X: 一個numpy array, 形狀爲 (N, D),存儲一個小批數據。
-y: 一個numpy array,形狀爲 (N,), 存儲訓練標籤。y[i]=c 表示 x[i]的標籤爲c,其中 0 <= c <= C 。
-reg: float, 正則化強度。
輸出一個tuple:
- 一個存儲爲float的loss
- 權重W的梯度,和W大小相同的array
"""
# delta 設爲1.0一般比較安全
delta = 1.0
# 梯度初始化
dW = np.zeros(W.shape)
#計算損失和梯度
num_class = W.shape[1]
num_trian = X.shape[0]
loss = 0.
for i in range(num_trian):
scores = np.dot(X[i],W)
score_correct = scores[y[i]]
for j in range(num_class):
if j == y[i]:
continue
margin = scores[j] - score_correct + delta
if margin > 0:
loss += margin
dW[:,y[i]] += -X[i,:].T
dW[:,j] += X[i,:].T
dW /= num_train
loss /= num_trian # 獲得損失函數的均值
dW += lamda * W # 梯度要加入正則化部分
return loss,dW
"""
偶爾會出現梯度驗證時候,某個維度不一致,導致不一致的原因是什麼呢?這是我們要考慮的一個因素麼?梯度驗證失敗的簡單例子是?
提示:SVM 損失函數沒有被嚴格證明是可導的.
可以看上面的輸出結果,turn on reg前有一個是3.954297e-03的loss明顯變大.
參考答案
解析解和數值解的區別,數值解是用前後2個很小的隨機尺度(比如0.00001)進行計算,當Loss不可導的,兩者會出現差異。比如SyiSyi剛好比SjSj大1.
"""
def svm_vectorized(W,X,y,lamda):
"""
使用向量來實現
"""
delta = 1.0
loss = 0.0
dW = np.zeros(W.shape)
scores = np.dot(X,W) # num_train * num_class
num_class = W.shape[1]
num_train = X.shape[0]
scores_correct = scores[np.arange(num_train),y] # 1 * num_train
scores_correct = np.reshape(scores_correct,(num_train,-1)) # num_train * 1
margins = scores - scores_correct + delta
margins = np.maximum(0,margis)
margins[np.arange(num_train),y] = 0
loss += np.sum(margins) / num_train
loss += 0.5 * lamda * np.sum(W * W)
"""
使用向量計算SVM損失函數的梯度,把結果保存在dW
"""
margins[margins > 0] = 1
row_sum = np.sum(margins,axis = 1)
margins[np.arange(num_train),y] = -row_sum # 1 by N
dW += np.dot(X.T,margins) / num_train + lamda * W
return loss , dW
"""
上面的代碼都是來計算損失函數的,現在我們採用SGD(隨機梯度下降)來最小化損失函數
"""
def train_SGD(self,X,y,alpha = 1e-3,lamda = 1e-5,numIterations = 1000,batch_size = 200,verbose = False):
"""
Train this linear classifier using stochastic gradient descent.
使用隨機梯度下降來訓練這個分類器。
輸入:
-X:一個 numpy array,形狀爲 (N, D), 存儲訓練數據。 共N個訓練數據, 每個訓練數據是N維的。
-Y:一個 numpy array, 形狀爲(N,), 存儲訓練數據的標籤。y[i]=c 表示 x[i]的標籤爲c,其中 0 <= c <= C 。
-learning rate: float, 優化的學習率。
-reg:float, 正則化強度。
-num_iters: integer, 優化時訓練的步數。
-batch_size: integer, 每一步使用的訓練樣本數。
-verbose: boolean,若爲真,優化時打印過程。
輸出:
一個存儲每次訓練的損失函數值的list。
"""
num_train ,dim = X.shape
num_class = np.max(y) + 1# 假設y的值是0...K-1,其中K是類別數量
if self.W is None:
# 簡易初始化,給W初始化比較小的值
self.W = 0.001 * np.random.randn(dim,num_class)
# 使用隨機梯度下降SGD,優化W
loss_history = []
for it in range(numIterations):
x_batch = None
y_batch = None
"""
從訓練集中採樣batch_size個樣本和對應的標籤,在這一輪梯度下降中使用。
把數據存儲在 X_batch 中,把對應的標籤存儲在 y_batch 中。
"""
batch_index = np.random.choice(num_train,batch_size)
x_batch = X[batch_index,:]
y_batch = y[batch_index]
# 用隨機產生的樣本,求損失函數,以及梯度。
loss,gradient = self.svm_vectorized(X,y,W,lamda)
loss_history.append(loss)
# GradientDescent 更新W
self.W = self.W - alpha * gradient
return loss_history
"""
使用驗證集去調整超參數(正則化強度lamda,和學習率alpha)
# 使用驗證集去調整超參數(正則化強度和學習率),你要嘗試各種不同的學習率
# 和正則化強度,如果你認真做,將會在驗證集上得到一個分類準確度大約是0.4的結果。
# 設置學習率和正則化強度,多設幾個靠譜的,可能會好一點。
# 可以嘗試先用較大的步長搜索,再微調。
"""
learning_rates = [2e-7, 0.75e-7,1.5e-7, 1.25e-7, 0.75e-7]
regularization_strengths = [3e4, 3.25e4, 3.5e4, 3.75e4, 4e4,4.25e4, 4.5e4,4.75e4, 5e4]
# 結果是一個詞典,將形式爲(learning_rate, regularization_strength) 的tuples 和形式爲 (training_accuracy, validation_accuracy)的tuples 對應上。準確率就簡單地定義爲數據集中點被正確分類的比例。
results = {}
best_val = -1 # 出現的正確率最大值
best_svm = None # 達到正確率最大值的svm對象
################################################################################
# 任務: #
# 寫下你的code ,通過驗證集選擇最佳超參數。對於每一個超參數的組合,
# 在訓練集訓練一個線性svm,在訓練集和測試集上計算它的準確度,然後
# 在字典裏存儲這些值。另外,在 best_val 中存儲最好的驗證集準確度,
# 在best_svm中存儲達到這個最佳值的svm對象。
#
# 提示:當你編寫你的驗證代碼時,你應該使用較小的num_iters。這樣SVM的訓練模型
# 並不會花費太多的時間去訓練。當你確認驗證code可以正常運行之後,再用較大的
# num_iters 重跑驗證代碼。
################################################################################
for rate in learning_rates:
for regular in regularization_strengths:
svm = LinearSVM()
svm.train(X_train, y_train, learning_rate=rate, reg=regular,
num_iters=1000)
y_train_pred = svm.predict(X_train)
accuracy_train = np.mean(y_train == y_train_pred)
y_val_pred = svm.predict(X_val)
accuracy_val = np.mean(y_val == y_val_pred)
results[(rate, regular)]=(accuracy_train, accuracy_val)
if (best_val < accuracy_val):
best_val = accuracy_val
best_svm = svm
################################################################################
# 結束 #
################################################################################
for lr, reg in sorted(results):
train_accuracy, val_accuracy = results[(lr, reg)]
print ('lr %e reg %e train accuracy: %f val accuracy: %f' % (lr, reg, train_accuracy, val_accuracy))
print ('best validation accuracy achieved during cross-validation: %f' % best_val)
"""
隨堂練習 2:
描述你的SVM可視化圖像,給出一個簡單的解釋
參考答案:
將學習到的權重可視化,從圖像可以看出,權重是用於對原圖像進行特徵提取的工具,與原圖像關係很大。
很樸素的思想,在分類器權重向量上投影最大的向量得分應該最高,訓練樣本得到的權重向量,
最好的結果就是訓練樣本提取出來的共性的方向,類似於一種模板或者過濾器。
"""