《動手學深度學習》Day2:Softmax與分類模型



一、softmax的基本概念

1.分類問題

一個簡單的圖像分類問題,輸入圖像的高和寬均爲2像素,色彩爲灰度。
圖像中的4像素分別記爲x1,x2,x3,x4x_1,x_2,x_3,x_4
假設真實標籤爲狗、貓或者雞,這些標籤對應的離散值爲y1,y2,y3y_1,y_2,y_3
我們通常使用離散的數值來表示類別,例如y1=1,y2=2,y3=3y_1=1,y_2=2,y_3=3

2.權重矢量
在這裏插入圖片描述
3.神經網絡圖
下圖用神經網絡圖描繪了上面的計算。softmax迴歸同線性迴歸一樣,也是一個單層神經網絡。由於每個輸出o1,o2,o3o_1,o_2,o_3的計算都要依賴於所有的輸入x1,x2,x3,x4x_1,x_2,x_3,x_4,softmax迴歸的輸出層也是一個全連接層。
在這裏插入圖片描述
既然分類問題需要得到離散的預測輸出,一個簡單的辦法是將輸出值oio_i當作預測類別是ii的置信度,並將值最大的輸出所對應的類作爲預測輸出,即輸出arg maxi oiarg\ \underset{i}{max}\ o_i 。例如,如果o1,o2,o3o_1,o_2,o_3分別爲0.1,10,0.1,由於o2o_2最大,那麼預測類別爲2,其代表貓。

4.輸出問題
直接使用輸出層的輸出有兩個問題:

  1. 一方面,由於輸出層的輸出值的範圍不確定,我們難以直觀上判斷這些值的意義。例如,剛纔舉的例子中的輸出值10表示“很置信”圖像類別爲貓,因爲該輸出值是其他兩類的輸出值的100倍。但如果o1=o3=103o_1=o_3=10^3,那麼輸出值10卻又表示圖像類別爲貓的概率很低。
  2. 另一方面,由於真實標籤是離散值,這些離散值與不確定範圍的輸出值之間的誤差難以衡量。

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

在這裏插入圖片描述
其中
在這裏插入圖片描述
容易看出y1^+y2^+y3^=1\hat{y_1}+\hat{y_2}+\hat{y_3}=10y1^,y2^,y3^0\leq \hat{y_1}, \hat{y_2}, \hat{y_3},因此y1^,y2^,y3^\hat{y_1}, \hat{y_2}, \hat{y_3}是一個合法的概率分佈。這時候,如果 y2^=0.8\hat{y_2}=0.8,不管y1^\hat{y_1}y3^\hat{y_3}的值是多少,我們都知道圖像類別爲貓的概率是80%。此外,我們注意到
arg maxi oi=arg maxi yi^arg\ \underset{i}{max}\ o_i=arg\ \underset{i}{max}\ \hat{y_i}
因此softmax運算不改變預測類別輸出。

5.計算效率

  • 單樣本矢量計算表達式
    爲了提高計算效率,我們可以將單樣本分類通過矢量計算來表達。在上面的圖像分類問題中,假設softmax迴歸的權重和偏差參數分別爲
    在這裏插入圖片描述
    設高和寬分別爲2個像素的圖像樣本的特徵爲
    在這裏插入圖片描述
    輸出層的輸出爲
    在這裏插入圖片描述
    預測爲狗、貓或雞的概率分佈爲
    在這裏插入圖片描述
    softmax迴歸對樣本分類的矢量計算表達式爲
    在這裏插入圖片描述

6.小批量矢量計算表達式
爲了進一步提升計算效率,我們通常對小批量數據做矢量計算。廣義上講,給定一個小批量樣本,其批量大小爲n,輸入個數(特徵數)爲d,輸出個數(類別數)爲q。設批量特徵爲 XRn×dX\in R^{n\times d}。假設softmax迴歸的權重和偏差參數分別爲WRd×qW\in R^{d\times q}bR1×qb\in R^{1\times q}。softmax迴歸的矢量計算表達式爲
在這裏插入圖片描述
其中的加法運算使用了廣播機制, O,Y^Rn×qO,\hat{Y}\in R^{n\times q}且這兩個矩陣的第i行分別爲樣本i的輸出o(i)o^{(i)}和概率分佈y^(i)\hat{y}^{(i)}

二、交叉熵損失函數

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

  • 平方損失估計

在這裏插入圖片描述
然而,想要預測分類結果正確,我們其實並不需要預測概率完全等於標籤概率。例如,在圖像分類的例子裏,如果y(i)=3y^{(i)}=3,那麼我們只需要y^3(i)\hat{y}_3^{(i)}比其他兩個預測值y^1(i)\hat{y}_1^{(i)}y^2(i)\hat{y}_2^{(i)}大就行了。即使y^3(i)\hat{y}_3^{(i)}值爲0.6,不管其他兩個預測值爲多少,類別預測均正確。而平方損失則過於嚴格,例如y^1(i)=y^2(i)=0.2\hat{y}_1^{(i)}=\hat{y}_2^{(i)}=0.2y^1(i)=0\hat{y}_1^{(i)}=0,y^2(i)=0.4\hat{y}_2^{(i)}=0.4的損失要小很多,雖然兩者都有同樣正確的分類預測結果。

改善上述問題的一個方法是使用更適合衡量兩個概率分佈差異的測量函數。其中,交叉熵(cross entropy)是一個常用的衡量方法:
在這裏插入圖片描述
其中帶下標的y^j(i)\hat{y}_j^{(i)}是向量y(i)\textbf{y}^{(i)}中非0即1的元素,需要注意將它與樣本ii類別的離散數值,即不帶下標的區分。在上式中,我們知道向量y(i)\textbf{y}^{(i)}中只有第y(i)y^{(i)}個元素y(i)y(i)y^{(i)}y^{(i)}爲1,其餘全爲0,於是H(y(i),y^(i))=logy^y(i)(i)\textbf{H}(y^{(i)},\hat{y}^{(i)})=-log\hat{y}_{y^{(i)}}^{(i)}。也就是說,交叉熵只關心對正確類別的預測概率,因爲只要其值足夠大,就可以確保分類結果正確。當然,遇到一個樣本有多個標籤時,例如圖像裏含有不止一個物體時,我們並不能做這一步簡化。但即便對於這種情況,交叉熵同樣只關心對圖像中出現的物體類別的預測概率。

假設訓練數據集的樣本數爲nn,交叉熵損失函數定義爲
在這裏插入圖片描述
其中Θ\Theta代表模型參數。同樣地,如果每個樣本只有一個標籤,那麼交叉熵損失可以簡寫成
在這裏插入圖片描述
從另一個角度來看,我們知道最小化l(Θ)l(\Theta)等價於最大化
在這裏插入圖片描述
即最小化交叉熵損失函數等價於最大化訓練數據集所有標籤類別的聯合預測概率。

三、模型訓練和預測

在訓練好softmax迴歸模型後,給定任一樣本特徵,就可以預測每個輸出類別的概率。通常,我們把預測概率最大的類別作爲輸出類別。如果它與真實類別(標籤)一致,說明這次預測是正確的。在3.6節的實驗中,我們將使用準確率(accuracy)來評價模型的表現。它等於正確預測數量與總預測數量之比。

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

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

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

  1. torchvision.datasets: 一些加載數據的函數及常用的數據集接口;
  2. torchvision.models: 包含常用的模型結構(含預訓練模型),例如
  3. AlexNet、VGG、ResNet等;
  4. torchvision.transforms: 常用的圖片變換,例如裁剪、旋轉等;
  5. 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

print(torch.__version__)
print(torchvision.__version__)

1.3.0
0.4.1a0+d94043a

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))

<class ‘torchvision.datasets.mnist.FashionMNIST’>
60000 10000

# 我們可以通過下標來訪問任意一個樣本
feature, label = mnist_train[0]
print(feature.shape, label)  # Channel x Height x Width

torch.Size([1, 28, 28]) 9

如果不做變換輸入的數據是圖像,我們可以看一下圖片的類型參數:

mnist_PIL = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True)
PIL_feature, label = mnist_PIL[0]
print(PIL_feature)

<PIL.Image.Image image mode=L size=28x28 at 0x7F54A41612E8>

# 本函數已保存在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)
start = time.time()
for X, y in train_iter:
    continue
print('%.2f sec' % (time.time() - start))

4.95 sec

五、softmax的Pytorch簡實現

# 加載各種包或者模塊
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

print(torch.__version__)

5.1初始化參數和獲取數據

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

5.2 定義網絡模型

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) 也可以
        )

5.3 初始化模型參數

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

Parameter containing:
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True)

5.4 定義損失函數

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

5.5 定義優化函數

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

5.6 訓練

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

epoch 1, loss 0.0031, train acc 0.751, test acc 0.795
epoch 2, loss 0.0022, train acc 0.813, test acc 0.809
epoch 3, loss 0.0021, train acc 0.825, test acc 0.806
epoch 4, loss 0.0020, train acc 0.833, test acc 0.813
epoch 5, loss 0.0019, train acc 0.837, test acc 0.822

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章