DL基礎補全計劃(二)—Softmax回歸及示例(Pytorch,交叉熵損失)
PS:要轉載請註明出處,本人版權所有。
PS: 這個只是基於《我自己》的理解,
如果和你的原則及想法相衝突,請諒解,勿噴。
前置說明
本文作為本人csdn blog的主站的備份。(BlogID=106)
環境說明
- Windows 10
- VSCode
- Python 3.8.10
- Pytorch 1.8.1
- Cuda 10.2
前言
在《DL基礎補全計劃(一)—線性回歸及示例(Pytorch,平方損失)》(//blog.csdn.net/u011728480/article/details/118463588 )一文中我們對深度學習中的非常基礎的知識進行了簡單介紹,按照常見深度學習中的基本流程設計了一個簡單的線性模型。同時,基於基本的語法,展示了數據收集,數據小批量隨機獲取,網路forward, loss設計,基於loss的bp,隨機小批量梯度下降,模型訓練,模型預測等基本的流程。 記錄這篇文章的原因也很簡單,為了將自己從學校裡面帶出來的知識和深度學習中的基礎知識關聯起來,不要出現大的斷層和空洞。
在上文我們提到,我們已經能夠設計一類模型能夠求解特定函數的數值,但是在實際應用場景中,我們還有一些問題主要還是關注他們的分類。比如我們有一堆數據,怎麼把他們分為N類。這裡就要介紹深度學習中一類常見的模型,softmax回歸模型。本文的主要目的就是基於FashionMNIST數據集(60000 * 28 * 28 訓練集,10000 * 28 * 28 測試集),從基礎的語法開始設計一個softmax分類模型,並介紹一些softmax相關的重點,在本文之後,其實我們就可以做一些深度學習的簡單分類任務了。
Softmax介紹及實例
我們可以知道,Softmax這個函數其實就是對N個類別進行打分,輸出N個類別的概率,那麼它的實際底層工作原理到底是什麼呢?
假如我們定義輸出類別為N,輸入特徵為X, 輸出類別分數為Y,參數為W,偏置為b,那麼我們可以設計一個函數為:\(Y=WX+b\),W.shape是(N, len(X)), X.shape是(len(X), 1), b.shape 是(N, len(X)),Y.shape是(N , 1),通過這樣的一個線性運算後,我們就可以將len(X)個輸入變換為N個輸出,其實這個時候的N個輸出就是我們不同類別的分數,理論上來說,我們就可以用這個當做每個類別的分數或者說概率。由於這裡的Y是實數範圍,有正有負,有大有小,存在數據不穩定性,而且我們需要把輸出的類別當做概率使用,這裡如果存在負數的話,不滿足概率的一些定義。因此我們在經過一個線性變換後,再通過softmax運算,才能夠將這些分數轉換為相應的概率。
Y.shape是(N , 1),Softmax定義為:\(Softmax(Yi)=exp(Yi)/\sum\limits_{j=0}^{N-1}Yj\) ,因此我們可以通過Softmax得到每個類別的分數。\(Y’=Softmax(Y)\),通過這樣的運算後,就把Y歸一化到0~1,而且滿足概率的一些定義和保持了和Y同樣的性質。
下面我們基於FashionMNIST數據集(此數據集有10個類別,60000個訓練集,10000個測試集,圖片為單通道28*28),設計一個簡單的分類模型。下面是python需要導入的依賴
from numpy.core.numeric import cross
import torch
from torch.utils.data import Dataset
from torchvision import datasets
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader
import numpy as np
獲取並處理FashionMNIST數據集
通過Pytorch的設計好的api直接獲取數據集,並得到解析後的數據
def LoadFashionMNISTByTorchApi():
# 60000*28*28
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)
# 10000*28*28
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
labels_map = {
0: "T-Shirt",
1: "Trouser",
2: "Pullover",
3: "Dress",
4: "Coat",
5: "Sandal",
6: "Shirt",
7: "Sneaker",
8: "Bag",
9: "Ankle Boot",
}
figure = plt.figure(figsize=(8, 8))
cols, rows = 3, 3
for i in range(1, cols * rows + 1):
sample_idx = torch.randint(len(training_data), size=(1,)).item()
img, label = training_data[sample_idx]
figure.add_subplot(rows, cols, i)
plt.title(labels_map[label])
plt.axis("off")
plt.imshow(img.squeeze(), cmap="gray")
plt.show()
return training_data, test_data

通過面的程式碼可以知道,datasets.FashionMNIST()返回的是集合,集合裡面存的是每個圖的數據以及其標籤。這裡其實Pytorch幫我們做了解析工作,實際FashionMNIST的二進位存儲格式如下,我們也可以自己寫程式碼按照此規則解析數據集,這裡就不關注這個問題了。
'''
Image:
[offset] [type] [value] [description]
0000 32 bit integer 0x00000803(2051) magic number
0004 32 bit integer 60000 number of images
0008 32 bit integer 28 number of rows
0012 32 bit integer 28 number of columns
0016 unsigned byte ?? pixel
0017 unsigned byte ?? pixel
........
xxxx unsigned byte ?? pixel
'''
'''
Label:
[offset] [type] [value] [description]
0000 32 bit integer 0x00000801(2049) magic number (MSB first)
0004 32 bit integer 60000 number of items
0008 unsigned byte ?? label
0009 unsigned byte ?? label
........
xxxx unsigned byte ?? label
The labels values are 0 to 9.
'''
還記得我們前文的隨機小批量怎麼實現的嗎?首先隨機打亂數據集中的每個數據(圖片和標籤為一個數據)的順序,然後根據batch_size參數構造一個可迭代的對象返回出來,最後訓練的時候我們通過for xx in data_iter 來訪問這一批的數據。這裡我們也不需要自己來寫這個了,直接調用Pytorch的函數來生成這樣的一個data_iter,我們應該把更多注意力放到其他地方去。程式碼如下:
training_data, test_data = LoadFashionMNISTByTorchApi()
batch_size = 200
# 返回訓練集和測試集的可迭代對象
train_dataloader = DataLoader(training_data, batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size, shuffle=True)
設計網路
我們的網路由兩個部分構成,一個是從28*28到10的一個映射函數,一個是softmax函數。我們定義一個\(Y=WX+b, Y’=Softmax(Y)\),因此我們可以看到,我們所需要學習的參數有W和b。
根據前文介紹,我們可以知道Y’/Y.shape是(10, 1), X.shape是(784, 1), W.shape是(10, 784), b.shape(10, 1)
def softmax(out):
# example:
# out = (p1, p2, p3)
# set Sum=p1+p2+p3
# softmax(out) = (p1/Sum, p2/Sum, p3/Sum)
exp_out = torch.exp(out)
partition = exp_out.sum(dim=1, keepdim=True)
return exp_out/partition
def my_net(w, b, X):
# print(X.shape)
# print(w.shape)
# print(b.shape)
liear_out = torch.matmul(X, w) + b
# print(liear_out.shape)
return softmax(liear_out)
設計Loss函數及優化函數
在前文的線性回歸中,我們使用了平方誤差來作為Loss函數,在分類這一問題裡面,我們需要引入交叉熵來作為Loss函數。交叉熵作為資訊理論中的概念,我們簡單的通過幾個前置知識來引入:
-
資訊理論研究的是一個隨機事件攜帶的資訊量,基本思想是事件發生概率越大,所攜帶的資訊量越小。因此這裡可以引入一個自資訊定義:\(I(x)=-\log_{2}(P(x))\)。通過這個定義我們可以得到同樣的趨勢,一個事件發生的概率越小,攜帶的資訊量越大。
-
熵(Entropy),自資訊是對單個隨機事件的資訊量大小描述,我們需要定義來描述整個隨機分布的資訊量大小的描述。假設隨機分布是離散的,熵的定義為:\(H(X)=-\sum\limits_{i=0}^{n-1}P(Xi)\log_{2}(P(Xi))\)
-
KL差異(Kullback-Leibler (KL) divergence),主要就是用來描述兩個分布的差異。因為在有些時候,一個概率分布很複雜,我們可以用一個簡單的概率分布來替代,但是我們需要知道這兩個分布的差異。定義原概率分布為P(X),近似概率分布為Q(X),假如X是離散隨機變數,KL差異定義為:\(D_{KL}(P(X)||Q(X))=\sum\limits_{i=0}^{n-1}P(Xi)\log_{2}(P(Xi)/Q(Xi))=\sum\limits_{i=0}^{n-1}P(Xi)[\log_{2}(P(Xi)) – \log_{2}(Q(Xi))]\)
-
交叉熵(cross-entropy),交叉熵定義為:\(H(P,Q)=-\sum\limits_{i=0}^{n-1}P(Xi)\log_{2}(Q(Xi))\),我們可以看到\(H(P,Q)=H(P)+D_{KL}(P||Q)\)。
-
在上文中,我們一步一步引出了交叉熵,這個時候,我們來看為什麼在深度學習中可以引入交叉熵作為Loss函數,對於特定的一組Feature,我們可以通過標籤得到這組feature代表什麼,相當於其概率為1,因此在原概率分布上面,\(P(Xi)=1, H(Xi)=0\),我們可以看到這個時候交叉熵和KL差異是相等的,那麼交叉熵其實就是描述我們訓練時得到的概率分布和原分布的差異。因此,在分類問題中我們得到的是當前的每個分類的概率,那麼我們分別求每個分類當前概率分布相對於原分布的KL差異,那麼我們就知道我們的訓練參數和真實參數的差異。我們求交叉熵的最小值,也就代表我們參數越接近真實值。
# 資訊理論,熵,kl熵(相對),交叉熵
def cross_entropy(y_train, y_label):
# l = -y*log(y')
# print(y_train.shape)
# print(y_label.shape)
# print(y_train)
# print(y_label)
# print(y_train[0][:].sum())
# call pick
my_loss = -torch.log(y_train[range(len(y_train)), y_label])
# nll_loss = torch.nn.NLLLoss()
# th_loss = nll_loss(torch.log(y_train), y_label)
# print(my_loss.sum()/len(y_label))
# print(th_loss)
return my_loss
設計準確率統計函數以及評估準確率函數
在前一小節,我們已經設計了損失函數,我們在訓練的過程中,除了要關注損失函數的值外,還需要關注我們模型的準確率。
模型的準確率程式碼如下:
def accuracy(y_train, y_label): #@save
"""計算預測正確的數量。"""
# y_train = n*num_class
if len(y_train.shape) > 1 and y_train.shape[1] > 1:
# argmax get the max-element-index
y_train = y_train.argmax(axis=1)
# cmp = n*1 , eg: [0 0 0 1 1 1 0 0 0]
# print(y_train.dtype)
# print(y_label.dtype)
cmp = y_train == y_label
return float(cmp.sum()/len(y_label))
從上面的程式碼可以知道,我們的網路輸出是當前feature在每個類別上的概率,因此我們求出網路輸出中,概率最大的索引,和真實label進行對比,相等就代表預測成功一個,反之。我們對最終數據求和後除以batch_size,就可以得到在batch_size個特徵中,我們的預測正確的個數佔比是多少。
我們還需要在指定的數據集上評估我們的準確率,其程式碼如下(就是分批調用獲得準確率後求平均):
def evaluate_accuracy(net, w, b, data_iter): #@save
"""計算在指定數據集上模型的精度。"""
test_acc_sum = 0.0
times = 1
for img, label in data_iter:
test_acc_sum += accuracy(net(w, b, img.reshape(-1, w.shape[0])), label)
times += 1
return test_acc_sum/times
設計預測函數
預測函數就是在特定數據上面,通過我們訓練的網路,求出類別,並與真實label進行對比,其程式碼如下:
def predict(net, w, b, test_iter, n=6): #@save
"""預測標籤(定義⻅第3章)。"""
for X, y in test_iter:
break
labels_map = {
0: "T-Shirt",
1: "Trouser",
2: "Pullover",
3: "Dress",
4: "Coat",
5: "Sandal",
6: "Shirt",
7: "Sneaker",
8: "Bag",
9: "Ankle Boot",
}
trues = [labels_map[i] for i in y.numpy()]
preds = [labels_map[i] for i in net(w, b, X.reshape(-1, w.shape[0])).argmax(axis=1).numpy()]
for i in np.arange(n):
print(f'pre-idx {i} \n true_label/pred_label: {trues[i]}/{preds[i]}')
訓練模型
訓練模型的話,其實就是將上面的程式碼縫合起來。程式碼如下:
if __name__ == '__main__':
training_data, test_data = LoadFashionMNISTByTorchApi()
batch_size = 200
train_dataloader = DataLoader(training_data, batch_size, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size, shuffle=True)
# train_features, train_labels = next(iter(train_dataloader))
# print(f"Feature batch shape: {train_features.size()}")
# print(f"Labels batch shape: {train_labels.size()}")
# img = train_features[1].squeeze()
# label = train_labels[1]
# plt.imshow(img, cmap="gray")
# plt.show()
# print(f"Label: {label}")
# 28*28
num_inputs = 784
# num of class
num_outputs = 10
# (748, 10)
w = torch.from_numpy(np.random.normal(0, 0.01, (num_inputs, num_outputs)))
w = w.to(torch.float32)
w.requires_grad = True
print('w = ', w.shape)
# (10, 1)
b = torch.from_numpy(np.zeros(num_outputs))
b = b.to(torch.float32)
b.requires_grad = True
print('b = ', b.shape)
num_epochs = 10
lr = 0.1
net = my_net
loss = cross_entropy
# if torch.cuda.is_available():
# w = w.to('cuda')
# b = b.to('cuda')
for epoch in range(num_epochs):
times = 1
train_acc_sum = 0.0
train_loss_sum = 0.0
for img, label in train_dataloader:
# if torch.cuda.is_available():
# img = img.to('cuda')
# label = label.to('cuda')
# print(img.shape, label.shape)
l = loss(net(w, b, img.reshape(-1, w.shape[0])), label)
# print(l.shape)
# print(l)
# clean grad of w,b
w.grad = None
b.grad = None
# bp
l.backward(torch.ones_like(l))
# update param
sgd([w, b], lr, batch_size)
train_acc_sum += accuracy(net(w, b, img.reshape(-1, w.shape[0])), label)
train_loss_sum += (l.sum()/batch_size)
times += 1
# break
test_acc = evaluate_accuracy(net, w, b, test_dataloader)
print('epoch = ', epoch)
print('train_loss = ', train_loss_sum.detach().numpy()/times)
print('train_acc = ', train_acc_sum/times)
print('test_acc = ', test_acc)
# break
# predict
predict(net, w, b, test_dataloader, n = 10)
從如上的程式碼可知,首先從數據集中得到小批量數據迭代器,然後隨機生成初始化參數,最後在小批量數據上推理,求loss之,bp,更新參數,記錄loss和acc,最終訓練次數完了後,去預測。
訓練截圖如下:

預測截圖如下:

從預測的截圖來看,預測成功的準確率大於1/10。說明我們的模型的有效的。此圖中看起來準確率較高,這是偶然現象,但是真實不應該這樣的,因為在測試集上,準確率只有81%左右。
後記
此外,我們這裡僅僅是按照數學定義來做計算,在電腦中,我們現在設計的一些函數可能不合理,比如softmax會產生溢出,我們會用到LogExpSum技巧,把softmax和交叉熵一起計算,通過冥函數和對數函數的一些性質,我們可以化簡後抵消一些exp的計算,保證數值的穩定性,我們只需要知道有這麼一個事情即可。但是這一切都不需要我們來弄,我們只需要調用別人設計的好的函數即可,比如pythorch中的torch.nn.CrossEntropyLoss()。如果真的有需要,可以根據LogExpSum的定義來直接編寫就行,在這裡,本文就不關注這個。
從線性回歸到softmax回歸,我們算是基本了解清楚了深度學習的一些基本的概念,這為我們去看和改一些比較好的、公開的模型打下了基礎。
參考文獻
- //github.com/d2l-ai/d2l-zh/releases (V1.0.0)
- //github.com/d2l-ai/d2l-zh/releases (V2.0.0 alpha1)
- //d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html

PS: 請尊重原創,不喜勿噴。
PS: 要轉載請註明出處,本人版權所有。
PS: 有問題請留言,看到後我會第一時間回復。