就像我們從零開始實現線性回歸一樣, 我們認為softmax回歸也是重要的基礎,因此應該知道實現softmax回歸的細節。 本節我們將使用剛剛在2-3節中引入的Fashion-MNIST數據集, 并設置數據迭代器的批量大小為256
。
import torch
from IPython import display
from d2l import torch as d2lbatch_size = 256 # 每次隨機讀取256張圖片
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) # 返回訓練集的iter迭代器和測試集的iter迭代器
初始化模型參數
和之前線性回歸的例子一樣,這里的每個樣本都將用固定長度的向量表示。 原始數據集中的每個樣本都是 28 × 28 28 \times 28 28×28的圖像。 本節將展平每個圖像,把它們看作長度為 784 784 784的向量(對于softmax回歸而言,我的輸入需要是一個向量)。 在后面的章節中,我們將討論能夠利用圖像空間結構的特征, 但現在我們暫時只把每個像素位置看作一個特征。(拉長以后,會損失掉很多空間信息)
回想一下,在softmax回歸中,我們的輸出與類別一樣多。 因為我們的數據集有 10 10 10個類別,所以網絡輸出維度為 10 10 10。 因此,權重將構成一個 784 × 10 784 \times 10 784×10的矩陣, 偏置將構成一個 1 × 10 1 \times 10 1×10的行向量(聯系softmax回歸那里的圖和公式例子來理解)。 與線性回歸一樣,我們將使用正態分布初始化我們的權重 W W W,偏置初始化為 0 0 0。
num_inputs = 784 # softmax的輸入是長為784的行向量
num_outputs = 10 # 模型輸出的維度為10W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
# 將權重初始化成一個高斯隨機分布的值,均值為0,方差為0.01,行數為輸入的個數,列數為輸出的個數,requires_grad=True表示需要計算梯度
b = torch.zeros(num_outputs, requires_grad=True)
# 對每一個輸出,都需要一個偏移,所以偏移是一個長為10的向量,同樣,我們需要計算梯度
定義softmax操作
在實現softmax回歸模型之前,我們簡要回顧一下sum運算符如何沿著張量中的特定維度工作。 如前所述, 給定一個矩陣 X X X,我們可以對所有元素求和(默認情況下)。 也可以只求同一個軸上的元素,即同一列(軸 0 0 0)或同一行(軸 1 1 1)。 如果 X X X是一個形狀為 ( 2 , 3 ) (2, 3) (2,3)的張量,我們對列進行求和, 則結果將是一個具有形狀 ( 3 , ) (3,) (3,)的向量。 當調用sum運算符時,我們可以指定保持在原始張量的軸數,而不折疊求和的維度。 這將產生一個具有形狀 ( 1 , 3 ) (1, 3) (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)
# 按照維度0來求和,那就是把我的形狀shape中的第0號元素從2變成了1
# 按照維度1來求和,那就是把我的形狀shape中第一個元素變成1,那么它就變成了一個2*1的列向量
# keepdim=True 表示還是一個2維的矩陣
# X.sum(0, keepdim=True) 按行求和
# X.sum(1, keepdim=True) 按列求和
回想一下,實現softmax由三個步驟組成:
-
對每個項求冪(使用exp);
-
對每一行求和(小批量中每個樣本是一行),得到每個樣本的規范化常數;
-
將每一行除以其規范化常數,確保結果的和為1。
在查看代碼之前,我們回顧一下這個表達式:
分母或規范化常數,有時也稱為配分函數(其對數稱為對數-配分函數)。 該名稱來自統計物理學中一個模擬粒子群分布的方程。
def softmax(X):X_exp = torch.exp(X) # 對每一個元素作指數計算partition = X_exp.sum(1, keepdim=True) # 我們按照維度為1來求和,就是把每一行進行求和,keepdim=True保持二維矩陣的shapereturn X_exp / partition # 這里應用了廣播機制
正如上述代碼,對于任何隨機輸入,我們將每個元素變成一個非負數。 此外,依據概率原理,每行總和為1。
X = torch.normal(0, 1, (2, 5)) # 初始化了一個均值為0,方差為1的,2行5列的矩陣X
X_prob = softmax(X) # 把它放進softmax之后,它的形狀沒有發生變化
X_prob, X_prob.sum(1) # 按照行來做加法的話,會得到一個長為2的行向量,每一行的值為1,表示上面的概率每一行的和為1
注意,雖然這在數學上看起來是正確的,但我們在代碼實現中有點草率。 矩陣中的非常大或非常小的元素可能造成數值上溢或下溢,但我們沒有采取措施來防止這點。
定義模型
定義softmax操作后,我們可以實現softmax回歸模型。 下面的代碼定義了輸入如何通過網絡映射到輸出。 注意,將數據傳遞到模型之前,我們使用reshape函數將每張原始圖像展平為向量。
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)# 對于輸入X,我們需要的是一個批量大小 x 輸入維數的矩陣,所以我們把它reshape成一個2d的矩陣,-1表示讓它自己算一下,這個維度表示批量大小# W.shape[0]是784# X被reshape成一個256*784的矩陣# 然后我們再對X和W進行矩陣乘法# 通過廣播機制,加上我們的偏移# 最后放進softmax里面# 拿到一個所有的元素值大于0,而且行和為1的輸出
定義損失函數
接下來,我們實現交叉熵損失函數。 這可能是深度學習中最常見的損失函數,因為目前分類問題的數量遠遠超過回歸問題的數量。
在講代碼之前,我們補一個細節,怎么樣在我的預測值里面根據我的標號把我們對應的預測值拿出來?
y = torch.tensor([0, 2]) # 創建一個長度為2的向量,這里表示兩個真實的標號,這里標號的含義是該樣本被分為第幾類!就是實際該樣本屬于下標為幾的類
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]]) # 對2個樣本作3類預測
y_hat[[0, 1], y]
解釋:
- 對y_hat中的第0個樣本,拿出下標為0的預測值
- 對y_hat中的第1個樣本,拿出下標為2的預測值
現在我們只需一行代碼就可以實現交叉熵損失函數。
def cross_entropy(y_hat, y):return - torch.log(y_hat[range(len(y_hat)), y])# range(len(y_hat)) 生成一個0-y_hat-1的行向量,也就是說對y_hat中的每一行(每一個樣本)而言# 拿出來對應真實標號的預測值# 取個log# 求負數cross_entropy(y_hat, y)
2.3026是樣本0的損失,0.6931是樣本1的損失,損失都是大于0的。
分類精度
因為我們做的是分類問題,所以我們要判斷說預測的類別和真實的類別是不是正確的。
我們這里實現一個小函數,說給定我們的預測值y_hat和我們的真實值y,我們來計算我們分類正確的類別數。
def accuracy(y_hat, y):"""計算預測正確的數量"""if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: # 如果y_hat是一個二維矩陣的話,列數也大于1y_hat = y_hat.argmax(axis=1) # 按照每一行求argmax,每一行中元素值最大的那個下標,存到y_hat里面,這就是我預測的分類類別cmp = y_hat.type(y.dtype) == y # 可能我的y_hat和我的y數據類型不一樣,把y_hat轉成y的數據類型,作比較,變成一個bool的tensorreturn float(cmp.type(y.dtype).sum()) # 把結果轉成和y一樣的數據類型,求和,再轉成浮點數accuracy(y_hat, y) / len(y) # 找出來預測正確的樣本數 除以 y的長度,就是預測正確的概率
# 為什么除以y的長度,y的長度就是真實需要預測樣本的總數
同樣,對于任意數據迭代器data_iter可訪問的數據集, 我們可以評估在任意模型net的精度。
def evaluate_accuracy(net, data_iter): """計算在指定數據集上模型的精度"""if isinstance(net, torch.nn.Module): # 如果是用torch nn實現的模型net.eval() # 將模型設置為評估模式,意思是說不要計算梯度了,我們只做一個前向傳遞metric = Accumulator(2) # 正確預測數、預測總數with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())# 先把X放到net里面算出評測值# 然后計算所有的預測正確的樣本數# y.numel() 樣本總數# 放進一個Accumulator累加器里return metric[0] / metric[1] # metric[0]分類正確的樣本數 metric[1]總樣本數 做除法,就能算出模型的精度了
這里定義一個實用程序類Accumulator
,用于對多個變量進行累加。 在上面的evaluate_accuracy
函數中, 我們在Accumulator
實例中創建了2個變量, 分別用于存儲正確預測的數量和預測的總數量。 當我們遍歷數據集時,兩者都將隨著時間的推移而累加。
class Accumulator: """在n個變量上累加"""def __init__(self, n):self.data = [0.0] * ndef 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]
訓練
在我們看過線性回歸實現, softmax回歸的訓練過程代碼應該看起來非常眼熟。 在這里,我們重構訓練過程的實現以使其可重復使用。 首先,我們定義一個函數來訓練一個迭代周期。 請注意,updater
是更新模型參數的常用函數,它接受批量大小作為參數。 它可以是d2l.sgd
函數,也可以是框架的內置優化函數。
def train_epoch_ch3(net, train_iter, loss, updater): #@save"""訓練模型一個迭代周期(定義見第3章)"""# 將模型設置為訓練模式if isinstance(net, torch.nn.Module): # 如果我是用nn 模塊net.train() # 告訴我的模型開始訓練模式,即告訴pytorch我要計算梯度# 訓練損失總和、訓練準確度總和、樣本數metric = Accumulator(3)for X, y in train_iter:# 計算梯度并更新參數y_hat = net(X)l = loss(y_hat, y)if isinstance(updater, torch.optim.Optimizer):# 使用PyTorch內置的優化器和損失函數updater.zero_grad() # 先把梯度設成0l.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=(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, fmtsdef add(self, x, y):# 向圖表中添加多個數據點if not hasattr(y, "__len__"):y = [y]n = len(y)if not hasattr(x, "__len__"):x = [x] * nif 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']) # 上面這個是用來可視化的,可以忽略# 掃n遍數據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_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert test_acc <= 1 and test_acc > 0.7, test_acc
作為一個從零開始的實現,我們使用小批量隨機梯度下降來優化模型的損失函數,設置學習率為0.1
。
lr = 0.1def updater(batch_size):return d2l.sgd([W, b], lr, batch_size)
現在,我們訓練模型10
個迭代周期。 請注意,迭代周期(num_epochs)和學習率(lr)都是可調節的超參數。 通過更改它們的值,我們可以提高模型的分類精度。
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:breaktrues = 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)
小結
-
借助softmax回歸,我們可以訓練多分類的模型。
-
訓練softmax回歸循環模型與訓練線性回歸模型非常相似:先讀取數據,再定義模型和損失函數,然后使用優化算法訓練模型。大多數常見的深度學習模型都有類似的訓練過程。