天天看點

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

目錄:softmax回歸的從零開始實作

  • 一、理論基礎
    • 1.1 前言
    • 1.2 分類問題
    • 1.3 網絡架構
    • 1.4 全連接配接層的參數開銷
    • 1.5 softmax運算
    • 1.6 小批量樣本的矢量化
    • 1.7 損失函數
      • 1.7.1 對數似然
      • 1.7.2 softmax及其導數
      • 1.7.3 交叉熵損失
    • 1.8 資訊論基礎
      • 1.8.1 熵
      • 1.8.2 資訊量
      • 1.8.3 重新審視交叉熵
    • 1.9 模型預測和評估
  • 二、softmax回歸的從零開始實作(PyTorch版本)
    • 2.1 導包
    • 2.2 初始化模型參數
    • 2.3 定義softmax操作
    • 2.4 定義模型
    • 2.5 定義損失函數
    • 2.6 分類精度
    • 2.7 訓練
    • 2.5 預測
  • 三、源代碼

一、理論基礎

1.1 前言

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.2 分類問題

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.3 網絡架構

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼
【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.4 全連接配接層的參數開銷

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.5 softmax運算

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.6 小批量樣本的矢量化

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.7 損失函數

接下來,我們需要一個損失函數來度量預測的效果。 我們将使用最大似然估計,這與線上性回歸中的方法相同。

1.7.1 對數似然

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.7.2 softmax及其導數

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.7.3 交叉熵損失

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.8 資訊論基礎

資訊論(information theory)涉及編碼、解碼、發送以及盡可能簡潔地處理資訊或資料。

1.8.1 熵

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.8.2 資訊量

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.8.3 重新審視交叉熵

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

1.9 模型預測和評估

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

二、softmax回歸的從零開始實作(PyTorch版本)

2.1 導包

import torch
from IPython import display
from d2l import torch as d2l
import torchvision
from torchvision import transforms
from torch.utils import data
import matplotlib.pyplot as plt
           

2.2 初始化模型參數

和之前線性回歸的例子一樣,這裡的每個樣本都将用固定長度的向量表示。 原始資料集中的每個樣本都是 28 × 28 28\times 28 28×28的圖像。

在本節中,我們将展平每個圖像,把它們看作長度為784的向量。

暫時隻把每個像素位置看作一個特征。

回想一下,在softmax回歸中,我們的輸出與類别一樣多。 因為我們的資料集有10個類别,是以網絡輸出次元為10。 是以,權重将構成一個 784 × 10 784\times 10 784×10的矩陣, 偏置将構成一個 1 × 10 1\times 10 1×10的行向量。 與線性回歸一樣,我們将使用正态分布初始化我們的權重W,偏置初始化為0。

num_inputs = 784
num_outputs = 10

w = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
           

2.3 定義softmax操作

在實作softmax回歸模型之前,我們簡要回顧一下sum運算符如何沿着張量中的特定次元工作。

給定一個矩陣X,我們可以對所有元素求和(預設情況下)。 也可以隻求同一個軸上的元素,即同一列(軸0)或同一行(軸1)。 如果X是一個形狀為(2, 3)的張量,我們對列進行求和, 則結果将是一個具有形狀(3,)的向量。 當調用sum運算符時,我們可以指定保持在原始張量的軸數,而不折疊求和的次元。 這将産生一個具有形狀(1, 3)的二維張量。

X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
           

輸出結果為:

(tensor([[5., 7., 9.]]),
 tensor([[ 6.],
         [15.]]))
           

0表示按列相加,1表示按行相加。

回想一下,實作softmax由三個步驟組成:

  1. 對每個項求幂(使用exp)
  2. 對每一行求和(小批量中每個樣本是一行),得到每個樣本的規範化常數
  3. 将每一行除以其規範化常數,確定結果的和為1

在檢視代碼之前,我們回顧一下這個表達式:

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

分母或規範化常數,有時也稱為配分函數(其對數稱為對數-配分函數)。 該名稱來自統計實體學中一個模拟粒子群分布的方程。

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

正如你所看到的,對于任何随機輸入,我們将每個元素變成一個非負數。 此外,依據機率原理,每行總和為1。

X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
           

輸出的結果為:

(tensor([[0.0456, 0.1734, 0.0443, 0.2028, 0.5339],
         [0.0648, 0.0213, 0.0681, 0.6248, 0.2211]]),
 tensor([1., 1.]))
           

注意,雖然這在數學上看起來是正确的,但我們在代碼實作中有點草率。 矩陣中的非常大或非常小的元素可能造成數值上溢或下溢,但我們沒有采取措施來防止這點。

2.4 定義模型

定義softmax操作後,我們可以實作softmax回歸模型。 下面的代碼定義了輸入如何通過網絡映射到輸出。 注意,将資料傳遞到模型之前,我們使用reshape函數将每張原始圖像展平為向量。

def net(X):
    return softmax(torch.matmul(X.reshape((-1, w.shape[0])), w) + b)
           

2.5 定義損失函數

接下來,我們實作 3.4節中引入的交叉熵損失函數。 這可能是深度學習中最常見的損失函數,因為目前分類問題的數量遠遠超過回歸問題的數量。

回顧一下,交叉熵采用真實标簽的預測機率的負對數似然。 這裡我們不使用Python的for循環疊代預測(這往往是低效的), 而是通過一個運算符選擇所有元素。 下面,我們建立一個資料樣本y_hat,其中包含2個樣本在3個類别的預測機率, 以及它們對應的标簽y。 有了y,我們知道在第一個樣本中,第一類是正确的預測; 而在第二個樣本中,第三類是正确的預測。 然後使用y作為y_hat中機率的索引, 我們選擇第一個樣本中第一個類的機率和第二個樣本中第三個類的機率。

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

現在我們隻需一行代碼就可以實作交叉熵損失函數。

def cross_entropy(y_hat, y):
    return - torch.log(y_hat[range(len(y_hat)), y])
cross_entropy(y_hat, y)
           

輸出結果為:

2.6 分類精度

給定預測機率分布y_hat,當我們必須輸出硬預測(hard prediction)時, 我們通常選擇預測機率最高的類。 許多應用都要求我們做出選擇。如Gmail必須将電子郵件分類為“Primary(主要郵件)”、 “Social(社交郵件)”、“Updates(更新郵件)”或“Forums(論壇郵件)”。 Gmail做分類時可能在内部估計機率,但最終它必須在類中選擇一個。

當預測與标簽分類y一緻時,即是正确的。 分類精度即正确預測數量與總預測數量之比。 雖然直接優化精度可能很困難(因為精度的計算不可導), 但精度通常是我們最關心的性能衡量标準,我們在訓練分類器時幾乎總會關注它。

為了計算精度,我們執行以下操作。 首先,如果y_hat是矩陣,那麼假定第二個次元存儲每個類的預測分數。 我們使用argmax獲得每行中最大元素的索引來獲得預測類别。 然後我們将預測類别與真實y元素進行比較。 由于等式運算符“==”對資料類型很敏感, 是以我們将y_hat的資料類型轉換為與y的資料類型一緻。 結果是一個包含0(錯)和1(對)的張量。 最後,我們求和會得到正确預測的數量。

def accuracy(y_hat, y):
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis = 1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())
           

我們将繼續使用之前定義的變量y_hat和y分别作為預測的機率分布和标簽。 可以看到,第一個樣本的預測類别是2(該行的最大元素為0.6,索引為2),這與實際标簽0不一緻。 第二個樣本的預測類别是2(該行的最大元素為0.5,索引為2),這與實際标簽2一緻。 是以,這兩個樣本的分類精度率為0.5。

同樣,對于任意資料疊代器data_iter可通路的資料集, 我們可以評估在任意模型net的精度。

def evaluate_accuracy(net, data_iter):
    if isinstance(net, torch.nn.Module):
        net.eval()
    metric = Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]
           

這裡定義一個實用程式類Accumulator,用于對多個變量進行累加。 在上面的evaluate_accuracy函數中, 我們在Accumulator執行個體中建立了2個變量, 分别用于存儲正确預測的數量和預測的總數量。 當我們周遊資料集時,兩者都将随着時間的推移而累加。

class Accumulator:
    def __init__(self, n):
        self.data = [0, 0] * n
    
    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0, 0] * len(self.data)
        
    def __getitem__(self, idx):
        return self.data[idx]
           

由于我們使用随機權重初始化net模型, 是以該模型的精度應接近于随機猜測。 例如在有10個類别情況下的精度為0.1。

2.7 訓練

在這裡,我們重構訓練過程的實作以使其可重複使用。 首先,我們定義一個函數來訓練一個疊代周期。 請注意,updater是更新模型參數的常用函數,它接受批量大小作為參數。 它可以是d2l.sgd函數,也可以是架構的内置優化函數。

def train_epoch_ch3(net, train_iter, loss, updater):
    if isinstance(net, torch.nn.Module):
        net.train()
    metric = Accumulator(3)
    for X, y in train_iter:
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    return metric[0] / metric[2], metric[1] / metric[2]
           

在展示訓練函數的實作之前,我們定義一個在動畫中繪制資料的實用程式類Animator:

class Animator:
    def __init__(self, xlabel = None, ylabel = None, legend = None, xlim = None, ylim = None, xscale = 'linear', yscale = 'linear',\
        fmts = ('-', 'm-', 'g-', 'r:'), nrows = 1, ncols = 1, figsize = (10, 6)):
        if legend is None:
            legend = []
        d2l.use_svg_display()
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize = figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        self.config_axes = lambda : d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    
    def add(self, x, y):
        if not hasattr(y, '__len__'):
            y = [y]
        n = len(y)
        if not hasattr(x, '__len__'):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        plt.draw()
        plt.pause(0.001)
        display.display(self.fig)
        display.clear_output(wait=True)
    

    def show(self):
        display.display(self.fig)
           

接下來我們實作一個訓練函數, 它會在train_iter通路到的訓練資料集上訓練一個模型net。 該訓練函數将會運作多個疊代周期(由num_epochs指定)。 在每個疊代周期結束時,利用test_iter通路到的測試資料集對模型進行評估。 我們将利用Animator類來可視化訓練進度。

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train_loss', 'train_acc', 'test_acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc, ))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_loss <= 1 and train_acc > 0.7, train_loss
    assert train_loss <= 1 and test_acc > 0.7, test_acc
           

作為一個從零開始的實作,我們使用定義的小批量随機梯度下降來優化模型的損失函數,設定學習率為0.1。現在,我們訓練模型10個疊代周期。 請注意,疊代周期(num_epochs)和學習率(lr)都是可調節的超參數。 通過更改它們的值,我們可以提高模型的分類精度。

lr = 0.1
def updater(batch_size):
    return d2l.sgd([w, b], lr, batch_size)
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
           
【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

2.5 預測

現在訓練已經完成,我們的模型已經準備好對圖像進行分類預測。 給定一系列圖像,我們将比較它們的實際标簽(文本輸出的第一行)和模型預測(文本輸出的第二行)。

def predict_ch3(net, test_iter, n = 6):
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis = 1))
    titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles = titles[0:n])


predict_ch3(net, test_iter)
plt.show()
           

預測的結果展示為:

【動手學深度學習】softmax回歸的從零開始實作(PyTorch版本)(含源代碼)一、理論基礎二、softmax回歸的從零開始實作(PyTorch版本)三、源代碼

三、源代碼

"""
softmax回歸從零開始實作
"""
import torch
from IPython import display
from d2l import torch as d2l
import torchvision
from torchvision import transforms
from torch.utils import data
import matplotlib.pyplot as plt


def get_dataloader_workers():
    return 0


def load_data_fashion_mnist(batch_size, resize=None):  #@save
    """下載下傳Fashion-MNIST資料集,然後将其加載到記憶體中"""
    trans = [transforms.ToTensor()]
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root="./15.動手學深度學習代碼手撸/data", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="./15.動手學深度學習代碼手撸/data", train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False,
                            num_workers=get_dataloader_workers()))


batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)                           
"""
初始化模型參數
"""
num_inputs = 784
num_outputs = 10

w = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)


"""
定義softmax操作
"""
def softmax(X):
    X_exp = torch.exp(X)
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition


def net(X):
    return softmax(torch.matmul(X.reshape((-1, w.shape[0])), w) + b)


y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])


def cross_entropy(y_hat, y):
    return - torch.log(y_hat[range(len(y_hat)), y])


print(y_hat[range(len(y_hat)), y])


def accuracy(y_hat, y):
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis = 1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

acc = accuracy(y_hat, y) / len(y)


class Accumulator:
    def __init__(self, n):
        self.data = [0, 0] * n
    
    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0, 0] * len(self.data)
        
    def __getitem__(self, idx):
        return self.data[idx]


def evaluate_accuracy(net, data_iter):
    if isinstance(net, torch.nn.Module):
        net.eval()
    metric = Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]


def train_epoch_ch3(net, train_iter, loss, updater):
    if isinstance(net, torch.nn.Module):
        net.train()
    metric = Accumulator(3)
    for X, y in train_iter:
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    return metric[0] / metric[2], metric[1] / metric[2]


class Animator:
    def __init__(self, xlabel = None, ylabel = None, legend = None, xlim = None, ylim = None, xscale = 'linear', yscale = 'linear',\
        fmts = ('-', 'm-', 'g-', 'r:'), nrows = 1, ncols = 1, figsize = (10, 6)):
        if legend is None:
            legend = []
        d2l.use_svg_display()
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize = figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        self.config_axes = lambda : d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    
    def add(self, x, y):
        if not hasattr(y, '__len__'):
            y = [y]
        n = len(y)
        if not hasattr(x, '__len__'):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        plt.draw()
        plt.pause(0.001)
        display.display(self.fig)
        display.clear_output(wait=True)
    

    def show(self):
        display.display(self.fig)


def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9], legend=['train_loss', 'train_acc', 'test_acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc, ))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_loss <= 1 and train_acc > 0.7, train_loss
    assert train_loss <= 1 and test_acc > 0.7, test_acc
    # animator.show()


lr = 0.1
def updater(batch_size):
    return d2l.sgd([w, b], lr, batch_size)
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

def predict_ch3(net, test_iter, n = 6):
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis = 1))
    titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles = titles[0:n])
    


predict_ch3(net, test_iter)
plt.show()
           

繼續閱讀