以下內容參考CS231n。
上一篇關於分類器的文章,使用的是KNN分類器,KNN分類有兩個主要的缺點:
- 空間上,需要存儲所有的訓練數據用於比較。
- 時間上,每次分類操作,需要和所有訓練數據比較。
本文開始線性分類器的學習。
和KNN相比,線性分類器才算得上真正具有實用價值的分類器,也是后面神經網絡和卷積神經網絡的基礎。
線性分類器中包括幾個非常重要的部分:
- 權重矩陣W,偏差向量b
- 評分函數
- 損失函數
- 正則化
- 最優化
權重矩陣W (Weights)
- 可以理解為所有分類對應的模版向量w組成的矩陣,模版就是分類期望成為的樣子。
- 訓練數據可以理解為是N維空間中的一個向量v,v和W中每個模版做點積,點積越大,表示兩個向量的夾角越小,也就越接近。
- 點積最大的模版w,就是v所對應的分類。
- W不是一直不變的。它會隨着對損失函數最優化的過程,不斷的調整。
偏差向量b (bias vector)
b是不是可以理解為,如果不設置b,那所有的分類線都要通過原點,那其實就起不到分類的作用了?
參考下圖?三條線都通過原點,是無法對數據做分類的。
W和b分別對直線做旋轉和平移。
評分函數(score function)
之所以是線性分類器,就是因為評分函數使用線性方程計算分數。后面的神經網絡會對線性函數做非線性處理。
下圖直觀的展示了分類器的線性。
損失函數 (loss function)
如何判斷當前的W和b是否合適,是否能夠輸出准確的分類?
通過損失函數,就可以計算預測的分類和實際分類之間的差異。通過不斷減小損失函數的值,也就是減少差異,就可以得到對應的W和b。
Python實現
數據預處理
# 每行均值
mean_image = np.mean(X_train, axis=0) # second: subtract the mean image from train and test data # 零均值化,中心化,使數據分布在原點周圍,可以加快訓練的收斂速度 X_train -= mean_image X_val -= mean_image X_test -= mean_image X_dev -= mean_image
處理b的技巧
# third: append the bias dimension of ones (i.e. bias trick) so that our SVM # only has to worry about optimizing a single weight matrix W. # 技巧,將bias加到矩陣中,作為最后一列,直接參與矩陣運算。不用單獨計算。 # hstack,水平地把數組的列組合起來 # 每行后面 加一個元素1 X_train = np.hstack([X_train, np.ones((X_train.shape[0], 1))]) X_val = np.hstack([X_val, np.ones((X_val.shape[0], 1))]) X_test = np.hstack([X_test, np.ones((X_test.shape[0], 1))]) X_dev = np.hstack([X_dev, np.ones((X_dev.shape[0], 1))])
計算損失函數
使用多類支持向量機作為損失函數。 SVM loss function,
,
全體訓練集上的平均損失值:
為了體現向量計算的高效,這里給出了非向量計算作為對比。
非向量版:
1 # 多類支持向量機損失函數 2 # SVM的損失函數想要SVM在正確分類上的得分始終比不正確分類上的得分高出一個邊界值Delta 3 # max(0, s1 - s2 + delta) 4 # s1,錯誤分類, s2,正確分類 5 def svm_loss_naive(W, X, y, reg): 6 7 # N, 500 8 # D, 3073 9 # C, 10 10 # W (3073, 10), 用(1,3073)左乘W,得到(1, 10) 11 # X(500, 3073), y(500,) 12 13 # 對W求導。 后面求使損失函數L減小的最快的w,也就是梯度下降最快的方向。 14 dW = np.zeros(W.shape) # initialize the gradient as zero 15 16 # compute the loss and the gradient 17 num_classes = W.shape[1] # 10 18 num_train = X.shape[0] # 500 19 loss = 0.0 20 for i in range(num_train): 21 # 分數 = 點積 22 # 向量(1,D)和矩陣(D,C)的點積 = (1,C) (c,) 23 # scores中的一行,表示所有類的打分 24 scores = X[i].dot(W) 25 26 # y[i],對的分類 27 correct_class_score = scores[y[i]] 28 for j in range(num_classes): 29 # 相等的分類不做處理 30 if j == y[i]: 31 continue 32 # 用其他分類的得分 - 最終分類的得分 + 1 33 margin = scores[j] - correct_class_score + 1 # note delta = 1 34 35 if margin > 0: 36 loss += margin 37 38 # (錯的-對的),所以第一個是-X,第二個是X 39 # 500張圖片的dW和,后面再除個500 40 dW[ :, y[i] ] += -X[ i, : ] 41 dW[ :, j ] += X[ i, : ] 42 43 loss /= num_train 44 dW /= num_train 45 46 # Add regularization to the loss. 47 # 正則化,https://blog.csdn.net/gsww404/article/details/80414675 48 # L2正則化 49 loss += reg * np.sum(W * W) 50 51 # 這里為什么不是2W 52 dW += reg * W 53 54 return loss, dW
向量版:
def svm_loss_vectorized(W, X, y, reg): loss = 0.0 dW = np.zeros(W.shape) # initialize the gradient as zero num_classes = W.shape[1] num_train = X.shape[0] scores = X.dot(W) # (500, 10) correct_scores = scores[ np.arange(num_train), y] #(500, ), 取( 0~500, y[0~500]) correct_scores = np.tile(correct_scores.reshape(num_train, 1) , (1, num_classes)) # (500, 10), 復制出10列,一行里的值是相同的 margin = scores - correct_scores + 1 margin[np.arange(num_train), y] = 0 # 正確的分類,不考慮 margin[margin < 0] = 0 # 小於0的置為0,不考慮 loss = np.sum(margin) / num_train loss += reg * np.sum(W * W) margin[ margin > 0 ] = 1 # 大於0的是造成損失的權重 row_sum = np.sum( margin, axis = 1 ) # 對每行取和, dW[ :, y[i] ] += -X[ i, : ], 參考非向量的寫法,每次遍歷里,正確的分類都有被減一次 margin[np.arange( num_train ), y] = -row_sum # 正確分類是負的,-X # 點積 dW += np.dot(X.T, margin) / num_train + reg * W return loss, dW
隨機初始化W,計算loss和gradient
# randn,高斯分布(正態分布)隨機數 # 初始權重采用小一點的值 W = np.random.randn(3073, 10) * 0.0001
# grad,analytic gradient loss, grad = svm_loss_naive(W, X_dev, y_dev, 0.000005)
檢查梯度值的正確性。用數值梯度值(numerical gradient)和分析梯度值(analytic gradient)比較。
檢查梯度方法:
def grad_check_sparse(f, x, analytic_grad, num_checks=10, h=1e-5): for i in range(num_checks): # 隨機取一個位置 ix = tuple([randrange(m) for m in x.shape]) oldval = x[ix] x[ix] = oldval + h # increment by h fxph = f(x) # evaluate f(x + h) x[ix] = oldval - h # increment by h # decrement ? fxmh = f(x) # evaluate f(x - h) x[ix] = oldval # reset # 對稱差分 grad_numerical = (fxph - fxmh) / (2 * h) grad_analytic = analytic_grad[ix] # 相對誤差 ?? rel_error = abs(grad_numerical - grad_analytic) / (abs(grad_numerical) + abs(grad_analytic))
比較分析梯度和數值梯度:
# 檢查計算的梯度值是否正確 # Compute the loss and its gradient at W. loss, grad = svm_loss_naive(W, X_dev, y_dev, 0.0) # Numerically compute the gradient along several randomly chosen dimensions, and # compare them with your analytically computed gradient. The numbers should match # almost exactly along all dimensions. from cs231n.gradient_check import grad_check_sparse f = lambda w: svm_loss_naive(w, X_dev, y_dev, 0.0)[0] # 取loss grad_numerical = grad_check_sparse(f, W, grad) # do the gradient check once again with regularization turned on # you didn't forget the regularization gradient did you? loss, grad = svm_loss_naive(W, X_dev, y_dev, 5e1) f = lambda w: svm_loss_naive(w, X_dev, y_dev, 5e1)[0] grad_numerical = grad_check_sparse(f, W, grad)
創建一個線性分類器類
最優化方法使用 隨機梯度下降法(SGD,Stochastic Gradient Descent)。
class LinearClassifier(object): def __init__(self): self.W = None # 訓練,也是最優化,調參的過程。 def train(self, X, y, learning_rate=1e-3, reg=1e-5, num_iters=100, batch_size=200, verbose=False): """ Train this linear classifier using stochastic gradient descent. Inputs: - X: A numpy array of shape (N, D) containing training data; there are N training samples each of dimension D. - y: A numpy array of shape (N,) containing training labels; y[i] = c means that X[i] has label 0 <= c < C for C classes. C是所有類別吧? - learning_rate: (float) learning rate for optimization. - reg: (float) regularization strength. - num_iters: (integer) number of steps to take when optimizing - batch_size: (integer) number of training examples to use at each step. - verbose: (boolean) If true, print progress during optimization. Outputs: A list containing the value of the loss function at each training iteration. """ num_train, dim = X.shape num_classes = np.max(y) + 1 # assume y takes values 0...K-1 where K is number of classes if self.W is None: self.W = 0.001 * np.random.randn(dim, num_classes) loss_history = [] for it in range(num_iters): X_batch = None y_batch = None # 默認是replacement = true, 隨機出來的數,還要再放回到樣本池中 randomRows = np.random.choice(num_train, batch_size) X_batch = X[randomRows] y_batch = y[randomRows] # evaluate loss and gradient loss, grad = self.loss(X_batch, y_batch, reg) loss_history.append(loss) # perform parameter update self.W += -learning_rate * grad if verbose and it % 100 == 0: print('iteration %d / %d: loss %f' % (it, num_iters, loss)) return loss_history # 預測 def predict(self, X): y_pred = np.zeros(X.shape[0]) y_pred = np.argmax( np.dot( X, self.W ), axis=1 ) return y_pred # 計算損失值 def loss(self, X_batch, y_batch, reg): pass
繼承基類LinearClassifier,定義SVM分類器
class LinearSVM(LinearClassifier): """ A subclass that uses the Multiclass SVM loss function """ def loss(self, X_batch, y_batch, reg): return svm_loss_vectorized(self.W, X_batch, y_batch, reg)
每次隨機取出200個數據,訓練1500次。 下圖顯示,損失值隨着訓練的迭代,追減降低。
對於CIFAR-10的訓練集和驗證集的准確度如下。

下圖是最終訓練出來的W所對應的圖片。從圖中可以看出最終的W,通過5萬張圖的訓練, 提取出了具有指定分類的特征。
python相關:
- np.mean(a, axis=None),
- 計算平均值。 axis=0,對整列計算均值。
- np.hstack(tup),
- 在水平方向上合並
- np.vstack(tup),
- 在豎直方向上合並
- np.stack(arrays, axis=0),
- 增加了一個維度,而且合並的兩個的形狀必須一樣。
- np.tile(A, reps),
- A是被重復的對象,reps是在不同維度上重復的次數
- np.random.randn,
- [0,1)范圍內的隨機數,滿足高斯分布
- tuple([randrange(m) for m in x.shape]),
- 隨機選擇x中的任意位置
- f = lambda w: svm_loss_naive(w, X_dev, y_dev, 0.0)[0],
- lambda函數,w是參數,:后面是函數體。
- np.random.choice(a, size=None, replace=True, p=None),
- 從a中隨機選擇size個數。replace=True,表示隨機取出的數,還要放回去。
- np.max(a, axis=None, out=None, keepdims=np._NoValue),
- 取最大值,也可以取每行或者每列中的最大值。
- np.argmax,
- 和np.max類似,區別是返回的是索引。
Reference:
1、知乎CS231n中文版,https://zhuanlan.zhihu.com/p/20918580?refer=intelligentunit