動手學深度學習v2-09-04-softmax回歸的從零開始實現¶


1 softmax回歸的從零開始實現

#使用Fashion-MNIST數據集,並設置數據迭代器的批量大小為256

from IPython import display

from mxnet import autograd,gluon,np,npx
from d2l import mxnet as d2l

npx.set_np()


batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
  • 出現的問題:cannot import name 'np' from 'mxnet' (unknown location) 報錯:表示沒有這個包
  • 原因:激活環境是能夠運行代碼的前提
  • 解決辦法:在d2l-zh目錄運行conda activate gluon命令,然后再打開jupyter notebook,則可以正常導入mxnet模塊。

1.1 初始化模型參數

  • 原始數據集中的每個樣本都是 28×28 的圖像。在本節中,我們將展平每個圖像,把它們看作長度為784的向量。
  • 在softmax回歸中,我們的輸出與類別一樣多。因為我們的數據集有10個類別,所以網絡輸出維度為10。因此,權重將構成一個 784×10 的矩陣,偏置將構成一個 1×10 的行向量。
  • 與線性回歸一樣,我們將使用正態分布初始化我們的權重W,偏置初始化為0。
num_inputs = 784
num_outputs = 10

W = np.random.normal(0, 0.01, (num_inputs, num_outputs))
b = np.zeros(num_outputs)
W.attach_grad()
b.attach_grad()

1.2 定義softmax操作

在實現softmax回歸模型之前,讓我們簡要地回顧一下sum運算符如何沿着張量中的特定維度工作。給定一個矩陣X,我們可以對所有元素求和(默認情況下),也可以只求同一個軸上的元素,即同一列(軸0)或同一行(軸1)。

X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdims=True), X.sum(1, keepdims=True)

我們現在已經准備好實現softmax操作了。softmax由三個步驟組成: (1)對每個項求冪(使用exp); (2)對每一行求和(小批量中每個樣本是一行),得到每個樣本的歸一化常數; (3)將每一行除以其歸一化常數,確保結果的和為1。 在查看代碼之前,讓我們回顧一下這個表達式:

def softmax(X):
    X_exp = np.exp(X)
    partition = X_exp.sum(1, keepdims=True)
    return X_exp / partition  # 這里應用了廣播機制
# 對於任何隨機輸入,我們將每個元素變成一個非負數。此外,依據概率原理,每行總和為1。
X = np.random.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)

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

1.3 定義模型

  • 下面的代碼定義了輸入如何通過網絡映射到輸出。
  • 注意,在將數據傳遞到我們的模型之前,我們使用reshape函數將每張原始圖像展平為向量。
def net(X):
    return softmax(np.dot(X.reshape((-1, W.shape[0])), W) + b)

1.4 定義損失函數

我們可以通過一個運算符選擇所有元素。 下面,我們創建一個數據y_hat,其中包含2個樣本在3個類別的預測概率,它們對應的標簽y。 有了y,我們知道在第一個樣本中,第一類是正確的預測,而在第二個樣本中,第三類是正確的預測。 然后使用y作為y_hat中概率的索引,我們選擇第一個樣本中第一個類的概率和第二個樣本中第三個類的概率。

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

  • 交叉熵損失函數
  • 交叉熵采用真實標簽的預測概率的負對數似然。
def cross_entropy(y_hat, y):
    return - np.log(y_hat[range(len(y_hat)), y])

cross_entropy(y_hat, y)

1.5 分類准確率

為了計算准確率,我們執行以下操作。首先,如果y_hat是矩陣,那么假定第二個維度存儲每個類的預測分數。我們使用argmax獲得每行中最大元素的索引來獲得預測類別。然后我們將預測類別與真實y元素進行比較。由於等式運算符“==”對數據類型很敏感,因此我們將y_hat的數據類型轉換為與y的數據類型一致。結果是一個包含0(錯)和1(對)的張量。進行求和會得到正確預測的數量。

def accuracy(y_hat, y):  #@save
    """計算預測正確的數量。"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.astype(y.dtype) == y
    return float(cmp.astype(y.dtype).sum())

我們將繼續使用之前定義的變量y_hat和y分別作為預測的概率分布和標簽。我們可以看到,第一個樣本的預測類別是2(該行的最大元素為0.6,索引為2),這與實際標簽0不一致。第二個樣本的預測類別是2(該行的最大元素為0.5,索引為2),這與實際標簽2一致。因此,這兩個樣本的分類准確率率為0.5。

accuracy(y_hat, y) / len(y)

對於任意數據迭代器data_iter可訪問的數據集,我們可以評估在任意模型net的准確率。

def evaluate_accuracy(net, data_iter):  #@save
    """計算在指定數據集上模型的精度。"""
    metric = Accumulator(2)  # 正確預測數、預測總數
    for X, y in data_iter:
        metric.add(accuracy(net(X), y), d2l.size(y))
    return metric[0] / metric[1]

Accumulator是一個實用程序類,用於對多個變量進行累加。 在上面的evaluate_accuracy函數中,我們在Accumulator實例中創建了2個變量,用於分別存儲正確預測的數量和預測的總數量。當我們遍歷數據集時,兩者都將隨着時間的推移而累加。

class Accumulator:  #@save
    """在`n`個變量上累加。"""
    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。

evaluate_accuracy(net, test_iter)

1.6 訓練

softmax回歸的訓練過程代碼應該看起來非常熟悉。在這里,我們重構訓練過程的實現以使其可重復使用。首先,我們定義一個函數來訓練一個迭代周期。請注意,updater是更新模型參數的常用函數,它接受批量大小作為參數。它可以是封裝的d2l.sgd函數,也可以是框架的內置優化函數。

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """訓練模型一個迭代周期(定義見第3章)。"""
    # 訓練損失總和、訓練准確度總和、樣本數
    metric = Accumulator(3)
    if isinstance(updater, gluon.Trainer):
        updater = updater.step
    for X, y in train_iter:
        # 計算梯度並更新參數
        with autograd.record():
            y_hat = net(X)
            l = loss(y_hat, y)
        l.backward()
        updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.size)
    # 返回訓練損失和訓練准確率
    return metric[0] / metric[2], metric[1] / metric[2]

定義一個在動畫中繪制數據的實用程序類。它能夠簡化本書其余部分的代碼。

class Animator:  #@save
    """在動畫中繪制數據。"""
    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=(3.5, 2.5)):
        # 增量地繪制多條線
        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, ]
        # 使用lambda函數捕獲參數
        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()
        display.display(self.fig)
        display.clear_output(wait=True)

實現一個訓練函數,它會在train_iter訪問到的訓練數據集上訓練一個模型net。該訓練函數將會運行多個迭代周期(由num_epochs指定)。在每個迭代周期結束時,利用test_iter訪問到的測試數據集對模型進行評估。我們將利用Animator類來可視化訓練進度。

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """訓練模型(定義見第3章)。"""
    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_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc

作為一個從零開始的實現,我們使用小批量隨機梯度下降來優化模型的損失函數,設置學習率為0.1。

lr = 0.1

def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)

我們訓練模型20個迭代周期。迭代周期(num_epochs)和學習率(lr)都是可調節的超參數。通過更改它們的值,我們可以提高模型的分類准確率。

num_epochs = 20
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

1.7 預測

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

def predict_ch3(net, test_iter, n=10):  #@save
    """預測標簽(定義見第3章)。"""
    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)

2 小結

  • 借助softmax回歸,我們可以訓練多分類的模型。
  • softmax回歸的訓練循環與線性回歸中的訓練循環非常相似:讀取數據、定義模型和損失函數,然后使用優化算法訓練模型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM