《動手學深度學習》系列筆記—— 1.2 Softmax迴歸與分類模型




softmax的基本概念

  • 分類問題
    • 一個簡單的圖像分類問題,輸入圖像的高和寬均爲2像素,色彩爲灰度。
    • 將圖像中的4像素分別記爲\(x_1, x_2, x_3, x_4\)
    • 假設真實標籤爲狗、貓或者雞,這些標籤對應的離散值爲\(y_1, y_2, y_3\)
    • 咱們一般使用離散的數值來表示類別,例如\(y_1=1, y_2=2, y_3=3\)
  • 權重矢量
    softmax迴歸的輸出值個數等於標籤裏的類別數。
    由於一共有4種特徵和3種輸出動物類別,因此權重包含12個標量(帶下標的\(w\))、誤差包含3個標量(帶下標的\(b\)),且對每一個輸入計算\(o_1\)\(o_2\)\(o_3\),這3個輸出:
    \[ \begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{21} + x_3 w_{31} + x_4 w_{41} + b_1 \end{aligned} \]

\[ \begin{aligned} o_2 &= x_1 w_{12} + x_2 w_{22} + x_3 w_{32} + x_4 w_{42} + b_2 \end{aligned} \]算法

\[ \begin{aligned} o_3 &= x_1 w_{13} + x_2 w_{23} + x_3 w_{33} + x_4 w_{43} + b_3 \end{aligned} \]網絡

  • 神經網絡圖
    下圖用神經網絡圖描繪了上面的計算。softmax迴歸同線性迴歸同樣,也是一個單層神經網絡。因爲每一個輸出\(o_1, o_2, o_3\)的計算都要依賴於全部的輸入\(x_1, x_2, x_3, x_4\),softmax迴歸的輸出層也是一個全鏈接層

既然分類問題須要獲得離散的預測輸出,一個簡單的辦法是將輸出值\(o_i\)看成預測類別是\(i\)的置信度,並將值最大的輸出所對應的類做爲預測輸出,即輸出 \(\underset{i}{\arg\max} o_i\)。例如,若是\(o_1,o_2,o_3\)分別爲\(0.1,10,0.1\),因爲\(o_2\)最大,那麼預測類別爲2,其表明貓。app

  • 輸出問題
    直接使用輸出層的輸出有兩個問題:
    1. 一方面,因爲輸出層的輸出值的範圍不肯定,咱們難以直觀上判斷這些值的意義。例如,剛纔舉的例子中的輸出值10表示「很置信」圖像類別爲貓,由於該輸出值是其餘兩類的輸出值的100倍。但若是\(o_1=o_3=10^3\),那麼輸出值10卻又表示圖像類別爲貓的機率很低。
    2. 另外一方面,因爲真實標籤是離散值,這些離散值與不肯定範圍的輸出值之間的偏差難以衡量。

softmax運算符(softmax operator)解決了以上兩個問題。它經過下式將輸出值變換成值爲正且和爲1的機率分佈:框架

\[ \hat{y}_1, \hat{y}_2, \hat{y}_3 = \text{softmax}(o_1, o_2, o_3) \]dom

其中svg

\[ \hat{y}1 = \frac{ \exp(o_1)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}2 = \frac{ \exp(o_2)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}3 = \frac{ \exp(o_3)}{\sum_{i=1}^3 \exp(o_i)}. \]函數

容易看出\(\hat{y}_1 + \hat{y}_2 + \hat{y}_3 = 1\)\(0 \leq \hat{y}_1, \hat{y}_2, \hat{y}_3 \leq 1\),所以\(\hat{y}_1, \hat{y}_2, \hat{y}_3\)是一個合法的機率分佈。這時候,若是\(\hat{y}_2=0.8\),無論\(\hat{y}_1\)\(\hat{y}_3\)的值是多少,咱們都知道圖像類別爲貓的機率是80%。此外,咱們注意到學習

\[ \underset{i}{\arg\max} o_i = \underset{i}{\arg\max} \hat{y}_i \]測試

所以softmax運算不改變預測類別輸出。

  • 計算效率
    • 單樣本矢量計算表達式
      爲了提升計算效率,咱們能夠將單樣本分類經過矢量計算來表達。在上面的圖像分類問題中,假設softmax迴歸的權重和誤差參數分別爲

\[ \boldsymbol{W} = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43} \end{bmatrix},\quad \boldsymbol{b} = \begin{bmatrix} b_1 & b_2 & b_3 \end{bmatrix}, \]

設高和寬分別爲2個像素的圖像樣本\(i\)的特徵爲

\[ \boldsymbol{x}^{(i)} = \begin{bmatrix}x_1^{(i)} & x_2^{(i)} & x_3^{(i)} & x_4^{(i)}\end{bmatrix}, \]

輸出層的輸出爲

\[ \boldsymbol{o}^{(i)} = \begin{bmatrix}o_1^{(i)} & o_2^{(i)} & o_3^{(i)}\end{bmatrix}, \]

預測爲狗、貓或雞的機率分佈爲

\[ \boldsymbol{\hat{y}}^{(i)} = \begin{bmatrix}\hat{y}_1^{(i)} & \hat{y}_2^{(i)} & \hat{y}_3^{(i)}\end{bmatrix}. \]

softmax迴歸對樣本\(i\)分類的矢量計算表達式爲

\[ \begin{aligned} \boldsymbol{o}^{(i)} &= \boldsymbol{x}^{(i)} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{y}}^{(i)} &= \text{softmax}(\boldsymbol{o}^{(i)}). \end{aligned} \]

  • 小批量矢量計算表達式
    爲了進一步提高計算效率,咱們一般對小批量數據作矢量計算。廣義上講,給定一個小批量樣本,其批量大小爲\(n\),輸入個數(特徵數)爲\(d\),輸出個數(類別數)爲\(q\)。設批量特徵爲\(\boldsymbol{X} \in \mathbb{R}^{n \times d}\)。假設softmax迴歸的權重和誤差參數分別爲\(\boldsymbol{W} \in \mathbb{R}^{d \times q}\)\(\boldsymbol{b} \in \mathbb{R}^{1 \times q}\)。softmax迴歸的矢量計算表達式爲

\[ \begin{aligned} \boldsymbol{O} &= \boldsymbol{X} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{Y}} &= \text{softmax}(\boldsymbol{O}), \end{aligned} \]

其中的加法運算使用了廣播機制\(\boldsymbol{O}, \boldsymbol{\hat{Y}} \in \mathbb{R}^{n \times q}\)且這兩個矩陣的第\(i\)行分別爲樣本\(i\)的輸出\(\boldsymbol{o}^{(i)}\)和機率分佈\(\boldsymbol{\hat{y}}^{(i)}\)

交叉熵損失函數

對於樣本\(i\),咱們構造向量\(\boldsymbol{y}^{(i)}\in \mathbb{R}^{q}\) ,使其第\(y^{(i)}\)(樣本\(i\)類別的離散數值)個元素爲1,其他爲0。這樣咱們的訓練目標能夠設爲使預測機率分佈\(\boldsymbol{\hat y}^{(i)}\)儘量接近真實的標籤機率分佈\(\boldsymbol{y}^{(i)}\)

  • 平方損失估計

\[ \begin{aligned}Loss = |\boldsymbol{\hat y}^{(i)}-\boldsymbol{y}^{(i)}|^2/2\end{aligned} \]

然而,想要預測分類結果正確,咱們其實並不須要預測機率徹底等於標籤機率。例如,在圖像分類的例子裏,若是\(y^{(i)}=3\),那麼咱們只須要\(\hat{y}^{(i)}_3\)比其餘兩個預測值\(\hat{y}^{(i)}_1\)\(\hat{y}^{(i)}_2\)大就好了。即便\(\hat{y}^{(i)}_3\)值爲0.6,無論其餘兩個預測值爲多少,類別預測均正確。而平方損失則過於嚴格,例如\(\hat y^{(i)}_1=\hat y^{(i)}_2=0.2\)\(\hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4\)的損失要小不少,雖然二者都有一樣正確的分類預測結果。

改善上述問題的一個方法是使用更適合衡量兩個機率分佈差別的測量函數。其中,交叉熵(cross entropy)是一個經常使用的衡量方法:

\[ H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ) = -\sum_{j=1}^q y_j^{(i)} \log \hat y_j^{(i)}, \]

其中帶下標的\(y_j^{(i)}\)是向量\(\boldsymbol y^{(i)}\)中非0即1的元素,須要注意將它與樣本\(i\)類別的離散數值,即不帶下標的\(y^{(i)}\)區分。在上式中,咱們知道向量\(\boldsymbol y^{(i)}\)中只有第\(y^{(i)}\)個元素\(y^{(i)}{y^{(i)}}\)爲1,其他全爲0,因而\(H(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}) = -\log \hat y_{y^{(i)}}^{(i)}\)。也就是說,交叉熵只關心對正確類別的預測機率,由於只要其值足夠大,就能夠確保分類結果正確。固然,遇到一個樣本有多個標籤時,例如圖像裏含有不止一個物體時,咱們並不能作這一步簡化。但即使對於這種狀況,交叉熵一樣只關心對圖像中出現的物體類別的預測機率。

假設訓練數據集的樣本數爲\(n\),交叉熵損失函數定義爲
\[ \ell(\boldsymbol{\Theta}) = \frac{1}{n} \sum_{i=1}^n H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ), \]

其中\(\boldsymbol{\Theta}\)表明模型參數。一樣地,若是每一個樣本只有一個標籤,那麼交叉熵損失能夠簡寫成
\[ \ell(\boldsymbol{\Theta}) = -(1/n) \sum_{i=1}^n \log \hat y_{y^{(i)}}^{(i)} \]
從另外一個角度來看,咱們知道最小化\(\ell(\boldsymbol{\Theta})\)等價於最大化\(\exp(-n\ell(\boldsymbol{\Theta}))=\prod_{i=1}^n \hat y_{y^{(i)}}^{(i)}\),即最小化交叉熵損失函數等價於最大化訓練數據集全部標籤類別的聯合預測機率。

模型訓練和預測

在訓練好softmax迴歸模型後,給定任同樣本特徵,就能夠預測每一個輸出類別的機率。一般,咱們把預測機率最大的類別做爲輸出類別。若是它與真實類別(標籤)一致,說明此次預測是正確的。在實驗中,將使用準確率(accuracy)來評價模型的表現。它等於正確預測數量與總預測數量之比。

獲取Fashion-MNIST訓練集和讀取數據

在介紹softmax迴歸的實現前先引入一個多類圖像分類數據集。它將在後面的章節中被屢次使用,以方便咱們觀察比較算法之間在模型精度和計算效率上的區別。圖像分類數據集中最經常使用的是手寫數字識別數據集MNIST。但大部分模型在MNIST上的分類精度都超過了95%。爲了更直觀地觀察算法之間的差別,咱們將使用一個圖像內容更加複雜的數據集Fashion-MNIST。

我這裏咱們會使用torchvision包,它是服務於PyTorch深度學習框架的,主要用來構建計算機視覺模型。torchvision主要由如下幾部分構成:

  1. torchvision.datasets: 一些加載數據的函數及經常使用的數據集接口;
  2. torchvision.models: 包含經常使用的模型結構(含預訓練模型),例如AlexNet、VGG、ResNet等;
  3. torchvision.transforms: 經常使用的圖片變換,例如裁剪、旋轉等;
  4. torchvision.utils: 其餘的一些有用的方法。
# import needed package
%matplotlib inline
from IPython import display
import matplotlib.pyplot as plt

import torch
import torchvision
import torchvision.transforms as transforms
import time

import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

get dataset

mnist_train = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=False, download=True, transform=transforms.ToTensor())
  • 說明:class torchvision.datasets.FashionMNIST(root, train=True, transform=None, target_transform=None, download=False)
    • root(string)– 數據集的根目錄,其中存放processed/training.pt和processed/test.pt文件。
    • train(bool, 可選)– 若是設置爲True,從training.pt建立數據集,不然從test.pt建立。
    • download(bool, 可選)– 若是設置爲True,從互聯網下載數據並放到root文件夾下。若是root目錄下已經存在數據,不會再次下載。
    • transform(可被調用 , 可選)– 一種函數或變換,輸入PIL圖片,返回變換以後的數據。如:transforms.RandomCrop。
    • target_transform(可被調用 , 可選)– 一種函數或變換,輸入目標,進行變換。
# show result 
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
# 咱們能夠經過下標來訪問任意一個樣本
feature, label = mnist_train[0]
print(feature.shape, label)  # Channel x Height x Width

若是不作變換輸入的數據是圖像,咱們能夠看一下圖片的類型參數:

mnist_PIL = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True)
PIL_feature, label = mnist_PIL[0]
print(PIL_feature)
# 本函數已保存在d2lzh包中方便之後使用
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 = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()
X, y = [], []
for i in range(10):
    X.append(mnist_train[i][0]) # 將第i個feature加到X中
    y.append(mnist_train[i][1]) # 將第i個label加到y中
show_fashion_mnist(X, get_fashion_mnist_labels(y))

# 讀取數據
batch_size = 256
num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)

softmax從零開始的實現

import torch
import torchvision
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

獲取訓練集數據和測試集數據

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')

模型參數初始化

num_inputs = 784    # 28*28
num_outputs = 10

W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)

對多維Tensor按維度操做

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True))  # dim爲0,按照相同的列求和,並在結果中保留列特徵
print(X.sum(dim=1, keepdim=True))  # dim爲1,按照相同的行求和,並在結果中保留行特徵
print(X.sum(dim=0, keepdim=False)) # dim爲0,按照相同的列求和,不在結果中保留列特徵
print(X.sum(dim=1, keepdim=False)) # dim爲1,按照相同的行求和,不在結果中保留行特徵

定義softmax操做

\[ \hat{y}_j = \frac{ \exp(o_j)}{\sum_{i=1}^3 \exp(o_i)} \]

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=1, keepdim=True)
    # print("X size is ", X_exp.size())
    # print("partition size is ", partition, partition.size())
    return X_exp / partition  # 這裏應用了廣播機制

softmax迴歸模型

\[ \begin{aligned} \boldsymbol{o}^{(i)} &= \boldsymbol{x}^{(i)} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{y}}^{(i)} &= \text{softmax}(\boldsymbol{o}^{(i)}). \end{aligned} \]

def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)

定義損失函數

\[ H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ) = -\sum_{j=1}^q y_j^{(i)} \log \hat y_j^{(i)}, \]

\[ \ell(\boldsymbol{\Theta}) = \frac{1}{n} \sum_{i=1}^n H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ), \]

\[ \ell(\boldsymbol{\Theta}) = -(1/n) \sum_{i=1}^n \log \hat y_{y^{(i)}}^{(i)} \]

a = torch.Tensor([[1,2],[3,4]]) 
b = orch.gather(a,1,torch.LongTensor([[0,0],[1,0]]))

torch.gather(input, dim, index, out=None)中的dim表示的就是第幾維度,在這個二維例子中,若是dim=0,那麼它表示的就是你接下來的操做是對於第一維度進行的,也就是行;若是dim=1,那麼它表示的就是你接下來的操做是對於第二維度進行的,也就是列。
上面例子中,[0,0]就是第一行對應元素的下標,也就是對應的是[1,1]; [1,0]就是第二行對應元素的下標,也就是對應的是[4,3]。

y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))

def cross_entropy(y_hat, y):
    return - torch.log(y_hat.gather(1, y.view(-1, 1)))

定義準確率

咱們模型訓練完了進行模型預測的時候,會用到咱們這裏定義的準確率。

def accuracy(y_hat, y):
    return (y_hat.argmax(dim=1) == y).float().mean().item()
# 本函數已保存在d2lzh_pytorch包中方便之後使用。該函數將被逐步改進:它的完整實現將在「圖像增廣」一節中描述
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

訓練模型

num_epochs, lr = 5, 0.1

# 本函數已保存在d2lzh_pytorch包中方便之後使用
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, optimizer=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:
            y_hat = net(X)
            l = loss(y_hat, y).sum()
            
            # 梯度清零
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()
            
            l.backward()
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                optimizer.step() 
            
            
            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        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_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

模型預測

如今咱們的模型訓練完了,能夠進行一下預測,咱們的這個模型訓練的到底準確不許確。
如今就能夠演示如何對圖像進行分類了。給定一系列圖像(第三行圖像輸出),咱們比較一下它們的真實標籤(第一行文本輸出)和模型預測結果(第二行文本輸出)。

X, y = iter(test_iter).next()

true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

d2l.show_fashion_mnist(X[0:9], titles[0:9])

softmax的簡潔實現

# 加載各類包或者模塊
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

初始化參數和獲取數據

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')

定義網絡模型

num_inputs = 784
num_outputs = 10

class LinearNet(nn.Module):
    def __init__(self, num_inputs, num_outputs):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(num_inputs, num_outputs)
    def forward(self, x): # x 的形狀: (batch, 1, 28, 28)
        y = self.linear(x.view(x.shape[0], -1))
        return y
    
# net = LinearNet(num_inputs, num_outputs)

class FlattenLayer(nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): # x 的形狀: (batch, *, *, ...)
        return x.view(x.shape[0], -1)

from collections import OrderedDict
net = nn.Sequential(
        # FlattenLayer(),
        # LinearNet(num_inputs, num_outputs) 
        OrderedDict([
           ('flatten', FlattenLayer()),
           ('linear', nn.Linear(num_inputs, num_outputs))]) # 或者寫成咱們本身定義的 LinearNet(num_inputs, num_outputs) 也能夠
        )

初始化模型參數

init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)

定義損失函數

loss = nn.CrossEntropyLoss() # 下面是他的函數原型
# class torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')

定義優化函數

optimizer = torch.optim.SGD(net.parameters(), lr=0.1) # 下面是函數原型
# class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)

訓練

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
相關文章
相關標籤/搜索