7.3 小批量隨機梯度下降
在每一次迭代中护昧,梯度下降使用整個訓練數(shù)據(jù)集來計算梯度贺归,因此它有時也被稱為批量梯度下降(batch gradient descent)杰扫。而隨機梯度下降在每次迭代中只隨機采樣一個樣本來計算梯度。正如我們在前幾章中所看到的拦宣,我們還可以在每輪迭代中隨機均勻采樣多個樣本來組成一個小批量菊霜,然后使用這個小批量來計算梯度屯曹。下面就來描述小批量隨機梯度下降抄谐。
7.3.1 讀取數(shù)據(jù)
本章里我們將使用一個來自NASA的測試不同飛機機翼噪音的數(shù)據(jù)集來比較各個優(yōu)化算法 [1]。我們使用該數(shù)據(jù)集的前1,500個樣本和5個特征筹燕,并使用標準化對數(shù)據(jù)進行預處理轧飞。
%matplotlib inline
import numpy as np
import time
import torch
from torch import nn, optim
import sys
sys.path.append("..")
import d2lzh_pytorch as d2l
def get_data_ch7(): # 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
data = np.genfromtxt('../../data/airfoil_self_noise.dat', delimiter='\t')
data = (data - data.mean(axis=0)) / data.std(axis=0)
return torch.tensor(data[:1500, :-1], dtype=torch.float32), \
torch.tensor(data[:1500, -1], dtype=torch.float32) # 前1500個樣本(每個樣本5個特征)
features, labels = get_data_ch7()
features.shape # torch.Size([1500, 5])
7.3.2 從零開始實現(xiàn)
3.2節(jié)(線性回歸的從零開始實現(xiàn))中已經(jīng)實現(xiàn)過小批量隨機梯度下降算法。我們在這里將它的輸入?yún)?shù)變得更加通用撒踪,主要是為了方便本章后面介紹的其他優(yōu)化算法也可以使用同樣的輸入过咬。具體來說,我們添加了一個狀態(tài)輸入states
并將超參數(shù)放在字典hyperparams
里糠涛。此外援奢,我們將在訓練函數(shù)里對各個小批量樣本的損失求平均兼犯,因此優(yōu)化算法里的梯度不需要除以批量大小忍捡。
def sgd(params, states, hyperparams):
for p in params:
p.data -= hyperparams['lr'] * p.grad.data
下面實現(xiàn)一個通用的訓練函數(shù),以方便本章后面介紹的其他優(yōu)化算法使用切黔。它初始化一個線性回歸模型砸脊,然后可以使用小批量隨機梯度下降以及后續(xù)小節(jié)介紹的其他算法來訓練模型。
# 本函數(shù)已保存在d2lzh_pytorch包中方便以后使用
def train_ch7(optimizer_fn, states, hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
net, loss = d2l.linreg, d2l.squared_loss
w = torch.nn.Parameter(torch.tensor(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=torch.float32),
requires_grad=True)
b = torch.nn.Parameter(torch.zeros(1, dtype=torch.float32), requires_grad=True)
def eval_loss():
return loss(net(features, w, b), labels).mean().item()
ls = [eval_loss()]
data_iter = torch.utils.data.DataLoader(
torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)
for _ in range(num_epochs):
start = time.time()
for batch_i, (X, y) in enumerate(data_iter):
l = loss(net(X, w, b), y).mean() # 使用平均損失
# 梯度清零
if w.grad is not None:
w.grad.data.zero_()
b.grad.data.zero_()
l.backward()
optimizer_fn([w, b], states, hyperparams) # 迭代模型參數(shù)
if (batch_i + 1) * batch_size % 100 == 0:
ls.append(eval_loss()) # 每100個樣本記錄下當前訓練誤差
# 打印結果和作圖
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
d2l.set_figsize()
d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
d2l.plt.xlabel('epoch')
d2l.plt.ylabel('loss')
當批量大小為樣本總數(shù)1,500時纬霞,優(yōu)化使用的是梯度下降凌埂。梯度下降的1個迭代周期對模型參數(shù)只迭代1次∈撸可以看到6次迭代后目標函數(shù)值(訓練損失)的下降趨向了平穩(wěn)瞳抓。
def train_sgd(lr, batch_size, num_epochs=2):
train_ch7(sgd, None, {'lr': lr}, features, labels, batch_size, num_epochs)
train_sgd(1, 1500, 6)
輸出:
loss: 0.243605, 0.014335 sec per epoch
當批量大小為1時,優(yōu)化使用的是隨機梯度下降伏恐。為了簡化實現(xiàn)孩哑,有關(小批量)隨機梯度下降的實驗中,我們未對學習率進行自我衰減翠桦,而是直接采用較小的常數(shù)學習率横蜒。隨機梯度下降中,每處理一個樣本會更新一次自變量(模型參數(shù)),一個迭代周期里會對自變量進行1,500次更新丛晌〗龃叮可以看到,目標函數(shù)值的下降在1個迭代周期后就變得較為平緩澎蛛。
train_sgd(0.005, 1)
輸出:
loss: 0.243433, 0.270011 sec per epoch
雖然隨機梯度下降和梯度下降在一個迭代周期里都處理了1,500個樣本抚垄,但實驗中隨機梯度下降的一個迭代周期耗時更多。這是因為隨機梯度下降在一個迭代周期里做了更多次的自變量迭代谋逻,而且單樣本的梯度計算難以有效利用矢量計算督勺。
當批量大小為10時,優(yōu)化使用的是小批量隨機梯度下降斤贰。它在每個迭代周期的耗時介于梯度下降和隨機梯度下降的耗時之間智哀。
train_sgd(0.05, 10)
輸出:
loss: 0.242805, 0.078792 sec per epoch
7.3.3 簡潔實現(xiàn)
在PyTorch里可以通過創(chuàng)建optimizer
實例來調用優(yōu)化算法。這能讓實現(xiàn)更簡潔荧恍。下面實現(xiàn)一個通用的訓練函數(shù)瓷叫,它通過優(yōu)化算法的函數(shù)optimizer_fn
和超參數(shù)optimizer_hyperparams
來創(chuàng)建optimizer
實例。
# 本函數(shù)與原書不同的是這里第一個參數(shù)優(yōu)化器函數(shù)而不是優(yōu)化器的名字
# 例如: optimizer_fn=torch.optim.SGD, optimizer_hyperparams={"lr": 0.05}
def train_pytorch_ch7(optimizer_fn, optimizer_hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
net = nn.Sequential(
nn.Linear(features.shape[-1], 1)
)
loss = nn.MSELoss()
optimizer = optimizer_fn(net.parameters(), **optimizer_hyperparams)
def eval_loss():
return loss(net(features).view(-1), labels).item() / 2
ls = [eval_loss()]
data_iter = torch.utils.data.DataLoader(
torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)
for _ in range(num_epochs):
start = time.time()
for batch_i, (X, y) in enumerate(data_iter):
# 除以2是為了和train_ch7保持一致, 因為squared_loss中除了2
l = loss(net(X).view(-1), y) / 2
optimizer.zero_grad()
l.backward()
optimizer.step()
if (batch_i + 1) * batch_size % 100 == 0:
ls.append(eval_loss())
# 打印結果和作圖
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
d2l.set_figsize()
d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
d2l.plt.xlabel('epoch')
d2l.plt.ylabel('loss')
使用PyTorch重復上一個實驗送巡。
train_pytorch_ch7(optim.SGD, {"lr": 0.05}, features, labels, 10)
輸出:
loss: 0.245491, 0.044150 sec per epoch
小結
- 小批量隨機梯度每次隨機均勻采樣一個小批量的訓練樣本來計算梯度摹菠。
- 在實際中,(小批量)隨機梯度下降的學習率可以在迭代過程中自我衰減骗爆。
- 通常次氨,小批量隨機梯度在每個迭代周期的耗時介于梯度下降和隨機梯度下降的耗時之間。
參考文獻
[1] 飛機機翼噪音數(shù)據(jù)集摘投。https://archive.ics.uci.edu/ml/datasets/Airfoil+Self-Noise
注:除代碼外本節(jié)與原書此節(jié)基本相同煮寡,原書傳送門