天天看點

DL基礎補全計劃(二)---Softmax回歸及示例(Pytorch,交叉熵損失)

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,平方損失)》(https://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回歸,我們算是基本了解清楚了深度學習的一些基本的概念,這為我們去看和改一些比較好的、公開的模型打下了基礎。

參考文獻

  • https://github.com/d2l-ai/d2l-zh/releases (V1.0.0)
  • https://github.com/d2l-ai/d2l-zh/releases (V2.0.0 alpha1)
  • https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html

打賞、訂閱、收藏、丢香蕉、硬币,請關注公衆号(攻城獅的搬磚之路)

PS: 請尊重原創,不喜勿噴。

PS: 要轉載請注明出處,本人版權所有。

PS: 有問題請留言,看到後我會第一時間回複。

dl