家樂的深度學習筆記「4」 – softmax回歸

  • 2020 年 3 月 31 日
  • 筆記

softmax回歸

線性回歸模型適用於輸出為連續值的情景,而softmax回歸的輸出單元由一個變成了多個,且引入了softmax運算輸出類別的概率分布,使輸出更適合離散值的預測與訓練,模型輸出可以是一個像影像類別這樣的離散值,其是一個單層神經網路,輸出個數等於分類問題中的類別個數。

分類問題

考慮一個簡單的影像分類問題,其輸入影像的寬和高均為2像素,且色彩為灰度。這樣每個像素值都可以用一個標量表示。將影像中的4像素分別記為,假設訓練數據集中影像的真實標籤為?、?或?,這些標籤分別對應離散值
通常使用離散的數值來表示類別,例如。如此,一張影像的標籤為1、2和3這3個數值中的一個。雖然仍可以使用回歸模型來進行建模,並將預測值就近定點化到1、2和3這3個離散值之一,但這種連續值到離散值的轉換通常會影響到分類品質。因此一般使用更加適合離散值輸出的模型來解決分類問題。

softmax回歸模型

softmax與線性回歸一樣將輸入特徵與權重做線性疊加。其輸出值個數等於標籤里的類別數。由於每個輸出的計算都要依賴於所有的輸入,所以其輸出層也是一個全連接層。

softmax運算

假如將輸出值當作預測類別是的置信度,並將值最大的輸出所對應的類作為預測輸出,即輸出,會存在兩個問題:其一,由於輸出層的輸出值的範圍不確定,很難直觀上判斷這些值的意義;另一方面,由於真實標籤為離散值,這些離散值與不確定範圍的輸出值之間的誤差難以衡量。
於是我們擁有了softmax運算,將輸出值變換成值為正且和為1的概率分布:

其中:

這樣輸出值就是一個合法的概率分布,其值還可以直接代表該類別的概率,並且不改變預測類別輸出。

矢量表達式

單樣本分類的矢量計算表達式

可以將原影像樣本視為一整個行向量,即:

則權重項和偏差參數分別為:

輸出層略(為一行向量),則softmax回歸對樣本分類的矢量計算表達式為:

小批量樣本分類的矢量計算表達式

為了進一步提升效率,可以從把多個單樣本組成小批量數據。廣義上講,給定一個小批量樣本,其批量大小為,輸入個數(特徵數)為,輸出個數(類別數)為。設批量特徵為。假設softmax回歸的權重和偏差參數分別為。softmax回歸的矢量計算表達式為:

其中加法運算使用了廣播機制,且這兩個矩陣的第行分別為樣本的輸出和概率分布

交叉熵損失函數

softmax運算將輸出變換成一個合法的類別預測分布,而真實標籤也可以用類別分布表達:對於樣本,我們構造向量為前文提到的輸出個數(類別數)),使其第個(樣本i類別的離散數值)個元素為1,其餘為0。這樣訓練目標就可以設為使預測概率分布儘可能接近真實的標籤概率分布
家樂:即構造的向量也是一個合法的概率分布,只不過只存在一個尖峰,下面的對比差異就容易理解了。
可以像線性回歸那樣使用平方損失函數(L2損失),然而,想要預測分類結果正確,其實並不需要預測概率完全等於標籤概率。如在影像分類問題中,只需要某類別預測值大於其他所有預測值即可,而平方損失就會過於嚴格。
於是可以使用更適合衡量兩個概率分布差異的測量函數。其中,交叉熵(cross entropy)是一個常用的衡量方法:

其中帶下標的是向量中非0即1的元素,需要注意將它與樣本類別的離散數值,即不帶下標的區分。
上式中我們知道向量中只有第個元素為1,其餘全為0,於是。也就是說,交叉熵只關心對正確類別的預測概率,因為只要其值足夠大,就可以確保分類結果正確。然而,當一個樣本具有多個標籤時,例如影像中不止一個物體時,並不能做這一步簡化。但即便對於這種情況,交叉熵同樣只關心對影像中出現的物體類別的預測概率。

假設訓練數據集的樣本數為交叉熵損失函數定義為:

其中代表模型參數。同樣地,如果每個樣本只有一個標籤,那麼交叉熵損失可以簡寫成。另一個角度看,最小化等價於最大化,即最小化交叉熵損失函數等價於最大化訓練數據集所有標籤類別的聯合預測概率。
家樂:這裡除以是因為需要衡量整個模型的品質,如果是小批量過程中,是除以,自己概念繞混了,已更新在前一篇的損失函數章節上。

練習一:最小化交叉熵損失函數與最大似然估計(MLE):最大似然估計的目的是利用已知的樣本結果,反推最有可能(最大概率)導致這樣結果的參數值,兩者同樣可以用來調整模型參數值,不過MLE假設的前提是訓練樣本的分布能夠代表樣本的真實分布,每個樣本集中的樣本都是所謂獨立同分布的隨機變數 (iid條件),且有充分的訓練樣本,其將概率密度的估計轉化為參數估計問題。

模型預測及評價

在訓練好softmax回歸模型後,給定一樣本特徵,就可以預測每個輸出類別的概率,通常使用準確率(accuracy)來評價模型的表現,其為正確預測數量與總預測數量之比。

影像分類數據集(Fashion-MNIST)

Fashion-MNIST,顧名思義,時尚的MNIST,那MNIST是什麼呢?MNIST數據集來自美國國家標準與技術研究所,由來自 250 個不同人手寫的數字構成,全都是壓縮過的手寫數字圖片,由於已經被玩爛了(大部分模型都可以做到95%以上的準確率),所以新的入門數據集被推了出來,即Fashion-MNIST。

Fashion-MNIST一共包括十個類別,分別為’t-shirt’, ‘trouser’, ‘pullover’, ‘dress’, ‘coat’, ‘sandal’, ‘shirt’, ‘sneaker’, ‘bag’, ‘ankle boot’,即T恤、褲子、套衫、連衣裙、外套、涼鞋、襯衫、運動鞋、包和短靴,由於存儲空間原因,標籤僅記錄了數值標籤0-9。

獲取數據集

必要的包

其中d2lzh是「Dive into deep learning」書作者沐神寫的,可以直接用pip安裝,以後不表。

from mxnet.gluon import data as gdata  import d2lzh as d2l  import time  import sys  

訓練與測試數據集

現在的框架基本上都帶了常見數據集的一鍵下載,方便管理與組織。

mnist_train = gdata.vision.FashionMNIST(train=True)  mnist_test = gdata.vision.FashionMNIST(train=False)  

練習三、四:gluon.data.vision裡面還包括了常見的MNIST、CIFAT10、CIFAR100和自定義ImageRecordDataSet、ImageFolderDataSet(遍歷文件夾的圖片)等;transforms里還提供了各種隨機化實例,用於擴充訓練集。

訓練數據集樣本數

len(mnist_train), len(mnist_test)  

可以查看訓練集與測試集所含樣本數,每個類別分別為6000和1000種。

(60000, 10000)  

查看樣本

feature, label = mnist_train[0]  print(feature.shape, feature.dtype)  print(label, type(label), label.dtype)  

特徵為寬高均為28像素的影像,每個像素的值為0到255之間8位無符號整數(uint8),使用三維NDArray存儲。

(28, 28, 1) <class 'numpy.uint8'>  2 <class 'numpy.int32'> int32  

獲取標籤

def get_fashion_mnist_labels(labels):      text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',                     'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']      return [text_labels[int(i)] for i in labels]  

畫出影像

def show_fashion_mnist(images, labels):      d2l.use_svg_display()      _, figs = d2l.plt.subplots(1, len(images), figsize=(12, 12))      for f, img, lbl in zip(figs, images, labels):          f.imshow(img.reshape((28, 28)).asnumpy())          f.set_title(lbl)          f.axes.get_xaxis().set_visible(False)          f.axes.get_yaxis().set_visible(False)    X, y = mnist_train[:9]  show_fashion_mnist(X, get_fashion_mnist_labels(y))  

output_6_0.svg

讀取小批量

雖然可以使用模仿上節線性回歸,使用yield來做生成器,但為了程式碼簡潔,直接創建了DataLoader實例。
書後練習一:減小batch_size(如到1)會影響讀取性能嗎?答:會嚴重影響性能,4執行緒時,讀取速度達到了驚人的152.24秒!

batch_size = 256  transformer = gdata.vision.transforms.ToTensor()  if sys.platform.startswith('win'):      num_workers = 0  else:      num_workers = 80    train_iter = gdata.DataLoader(mnist_train.transform_first(transformer),                               batch_size=batch_size, shuffle=True,                               num_workers=num_workers)  test_iter = gdata.DataLoader(mnist_test.transform_first(transformer),                              batch_size=batch_size, shuffle=False,                              num_workers=num_workers)  

上面的num_worker是激活多執行緒來實現加速數據讀取(暫不支援win),因為數據讀取經常是訓練的性能瓶頸,特別是當模型較簡單或計算硬體性能較高時。另,使用ToTensor實例將影像數據從uint8格式轉換為32位浮點數格式,併除以255使得所有像素的數值均在0到1之間,這一步叫作歸一化;其還將影像通道從最後一維移到最前一維來方便之後介紹的卷積神經網路計算。通過transform_first函數,使ToTensor變換應用在每個數據樣本(影像和標籤)的第一個元素,即影像之上。

start = time.time()  for X, y in train_iter:      continue  '%.2f sec' % (time.time() - start)  

練習二:測試一下批量讀取的時間,這裡因為使用了40核的伺服器,執行緒調到了80個,可以看到速度的明顯提升,對比4執行緒時為1.72秒,40執行緒時為0.86秒。

'0.74 sec'  

全部程式碼

影像分類數據集(Fashion-MNIST).html

softmax回歸的從零實現

絕大多數深度學習模型的訓練都有著類似的步驟:獲取並讀取數據、定義模型和損失函數並使用優化演算法訓練模型。

必要的包

前文已有基礎,不多做介紹。

from mxnet import autograd, gpu, nd  import d2lzh as d2l  

改一下使用的gpu。

ctx = gpu(3)  

讀取數據集

這裡批大小設置為512,其實這個數據集本身就挺小的,感覺完全可以直接全扔顯示記憶體。

batch_size = 512  train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)  

初始化模型參數

把每個樣本拉長為一行長向量(28*28即784),作為參與運算即可。

num_inputs = 28 * 28  num_outputs = 10    W = nd.random.normal(scale=0.01, shape=(num_inputs, num_outputs), ctx=ctx)  b = nd.zeros(num_outputs, ctx=ctx)    W.attach_grad()  b.attach_grad()  

實現softmax運算

前面介紹過數學公式定義,這裡利用NDArray實現上述操作。

X = nd.array([[1, 2, 3], [3, 4, 5]], ctx=ctx)  print(X.sum(axis=0, keepdims=True), X.sum(axis=1, keepdims=True))  print(X.sum(axis=0), X.sum(axis=1, keepdims=False))  

可以看到,如果不加保留維度選項,輸出會變回一維向量。

[[4. 6. 8.]]  <NDArray 1x3 @gpu(3)>  [[ 6.]   [12.]]  <NDArray 2x1 @gpu(3)>    [4. 6. 8.]  <NDArray 3 @gpu(3)>  [ 6. 12.]  <NDArray 2 @gpu(3)>  

將預測值轉化為一個合法的概率分布。

def softmax(X):      X_exp = X.exp()      partition = X_exp.sum(axis=1, keepdims=True)      return X_exp / partition  

測試一下這個函數:

softmax(X)  
[[0.09003057 0.24472848 0.66524094]   [0.09003057 0.24472846 0.66524094]]  <NDArray 2x3 @gpu(3)>  

另外,這個只利用數學定義實現的softmax存在問題:因為指數函數非線性增大,如計算exp(50),結果值就會變得非常大,難以維護數值穩定性。

a = nd.array([50], ctx=ctx)  a.exp()  
[5.1847055e+21]  <NDArray 1 @gpu(3)>  

對一個隨機的輸入,驗證一下softmax運算會將其變為非負數,且每行和為1。

X = nd.random.normal(shape=(2, 5), ctx=ctx)  X_prob = softmax(X)  print(X)  print(X_prob, X_prob.sum(axis=1))  
[[-1.1795309   1.7976178   1.52335    -1.3275213  -0.21527036]   [ 0.35299432  1.0368916  -1.1053166   0.08840044 -1.0292935 ]]  <NDArray 2x5 @gpu(3)>    [[0.02561495 0.5028665  0.38224316 0.02209134 0.0671841 ]   [0.23625386 0.46815717 0.05495946 0.18132897 0.05930058]]  <NDArray 2x5 @gpu(3)>  [1. 1.]  <NDArray 2 @gpu(3)>  

定義模型

在這一步把圖形數據拉長為長向量。

def net(X):      return softmax(nd.dot(X.reshape((-1, num_inputs)).as_in_context(ctx), W) + b)  

定義損失函數

首先介紹一下nd.pick()方法,會按照索引從一個數組中挑出元素。

y_hat = nd.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])  y = nd.array([0, 2], dtype='int32')  nd.pick(y_hat, y)  
[0.1 0.5]  <NDArray 2 @cpu(0)>  

按照數學定義的交叉熵損失函數即為預測值與真實值之差的負對數,這樣損失函數對模型的懲罰非線性增大,但對數函數定義域為,這裡輸入參數的值域為(不取閉的原因是一般情況下不會出現,除非剛開始初始化時網路權重全0),所以輸出值域就會變得很不穩定,如假設我們對於正確類別的預測值太低,會導致極大的loss。

下面(簡潔實現)會介紹一種同時包括softmax與交叉熵的函數,其具有更好的數值穩定性。

def cross_entropy(y_hat, y):      return -nd.pick(y_hat, y).log()  

這裡演示了有可能遇到的極限情況時,兩者的差值。

a = nd.array([0.000001, 0.999999], ctx=ctx)  print(a[1] / a[0])  
[999999.]  <NDArray 1 @gpu(3)>  

計算分類準確率

給定一個預測概率分布,將預測概率最大的類別作為輸出類別。分類準確率即正確預測數量與總預測數量之比。由於標籤為整數,需要將其轉化為32位浮點數再進行比較。

def accuracy(y_hat, y):      return (y_hat.argmax(axis=1) == y.astype('float32')).mean().asscalar()  

測試一下上面定義的變數的準確性,可以看出來,每行代表一個樣本,每列代表一個樣本的預測概率分布。

accuracy(y_hat, y)  
0.5  

同理,可以對整個模型計算分類準確率。

def evaluate_accuracy(data_iter, net):      acc_sum, n = 0.0, 0      for X, y in data_iter:          y = y.astype('float32').as_in_context(ctx)          acc_sum += (net(X).argmax(axis=1) == y).sum().asscalar()          n += y.size      return acc_sum / n  

由於模型權重為隨機初始化,所以現在對理論準確率為10%。

evaluate_accuracy(test_iter, net)  
0.0608  

訓練模型

這裡超參數的設置可以自己隨便調調觀察效果。

num_epochs, lr = 5, 0.2    def train(net, train_iter, test_iter, loss, num_epochs,            batch_size, params=None, lr=None, trainer=None):      for epoch in range(num_epochs):          train_l_sum, train_acc_sum, n = 0.0, 0.0, 0          for X, y in train_iter:              with autograd.record():                  y_hat = net(X)                  l = loss(y_hat, y.as_in_context(ctx)).sum()              l.backward()              if trainer is None:                  d2l.sgd(params, lr, batch_size)              else:                  traniner.step(batch_size)              y = y.astype('float32')              train_l_sum += l.asscalar()              train_acc_sum += (y_hat.argmax(axis=1) == y.as_in_context(ctx)).sum().asscalar()              n += y.size          test_acc = evaluate_accuracy(test_iter, net)          print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'                % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))    train(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)  
epoch 1, loss 0.9251, train acc 0.703, test acc 0.778  epoch 2, loss 0.6221, train acc 0.789, test acc 0.805  epoch 3, loss 0.5600, train acc 0.810, test acc 0.825  epoch 4, loss 0.5325, train acc 0.819, test acc 0.826  epoch 5, loss 0.5085, train acc 0.827, test acc 0.821  

預測效果

這裡演示一下如何對影像進行分類,可以看出五次迭代即達到了80%的準確度。

count = 0  for X, y in test_iter:      if count == 233:          break      count += 1    true_labels = d2l.get_fashion_mnist_labels(y.asnumpy())  pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1).asnumpy())  titles = [true + 'n' + pred for true, pred in zip(true_labels, pred_labels)]    d2l.show_fashion_mnist(X[:10], titles[:10])  

output_18_0.svg

全部程式碼

softmax回歸從零實現.html

softmax簡潔實現

同樣適用gluon自帶的庫。

必要的包

from mxnet.gluon import loss as gloss, nn  from mxnet import autograd, gluon, init, gpu  import d2lzh as d2l  

切換下gpu。

ctx = gpu(3)  

讀取數據集

同上。

batch_size = 512  train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)  

定義和初始化模型

同上篇文章。

net = nn.Sequential()  net.add(nn.Dense(10))  net.initialize(init.Normal(sigma=0.01), ctx=ctx)  

softmax和交叉熵損失函數

使用gluon提供的混合包,可以獲得更好的數值穩定性,參見上面的損失函數部分遇到的問題。

loss = gloss.SoftmaxCrossEntropyLoss()  

訓練模型

trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.2})  

這裡因為需要使用gpu訓練,重新定義了準確度函數。

def evaluate_accuracy(data_iter, net):      acc_sum, n = 0.0, 0      for X, y in data_iter:          X = X.as_in_context(ctx)          y = y.astype('float32').as_in_context(ctx)          acc_sum += (net(X).argmax(axis=1) == y).sum().asscalar()          n += y.size      return acc_sum / n  

訓練模型,一回生二回熟。

num_epochs = 5    def train(net, train_iter, test_iter, loss, num_epochs, batch_size, trainer):      for epoch in range(num_epochs):          train_l_sum, train_acc_sum, n = 0.0, 0.0, 0          for X, y in train_iter:              with autograd.record():                  y_hat = net(X.as_in_context(ctx))                  l = loss(y_hat, y.as_in_context(ctx)).sum()              l.backward()              trainer.step(batch_size)              y = y.astype('float32')              train_l_sum += l.asscalar()              train_acc_sum += (y_hat.argmax(axis=1) == y.as_in_context(ctx)).sum().asscalar()              n += y.size          test_acc = evaluate_accuracy(test_iter, net)          print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'                % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))    train(net, train_iter, test_iter, loss, num_epochs, batch_size, trainer)  

可以看出來準確度還是不錯的:84%,省下的測試略,見上例。

epoch 1, loss 0.9137, train acc 0.703, test acc 0.788  epoch 2, loss 0.6413, train acc 0.784, test acc 0.820  epoch 3, loss 0.5698, train acc 0.805, test acc 0.816  epoch 4, loss 0.5274, train acc 0.822, test acc 0.833  epoch 5, loss 0.5265, train acc 0.819, test acc 0.840  

全部程式碼

softmax回歸簡潔實現.html

另:Batch_size的選擇

批量的選擇會影響模型最終訓練的準確度,採用全量訓練更有助於確定梯度要優化的方向,但因為迭代次數更少實際上容易陷入極小點而出不來;另一方面採用過小的批量,如1(在線學習),又會導致模型難以收斂;所以,批量作為超參數之一,也需要合適選擇來確保模型訓練迭代速度更快、準確度更好,同時適當的批量引入的雜訊會使模型具有更大的不確定性來逃離極小點,最終到達最小點的可能性會增加。

參考資料

極大似然估計詳解
哈?你還認為似然函數跟交叉熵是一個意思呀?
機器學習演算法原理、實現與實踐——機器學習的三要素
深度學習中的batch的大小對學習效果有何影響?
Asynchronous Parallel Stochastic Gradient for Nonconvex Optimization∗