從這里學習《DL-with-PyTorch-Chinese》 4.2用PyTorch自動求導
考慮到上一篇手動為由線性和非線性函數組成的復雜函數的導數編寫解析表達式並不是一件很有趣的事情,也不是一件很容易的事情。這里我們用通過一個名為autograd
的PyTorch模塊來解決。
利用autograd
的PyTorch模塊來替換手動求導做梯度下降
首先模型和損失函數還是不變:
def model(t_u, w, b): return w * t_u + b def loss_fn(t_p, t_c): squared_diffs = (t_p - t_c)**2 return squared_diffs.mean()
然后初始化參數張量:
params = torch.tensor([1.0, 0.0], requires_grad=True)
張量構造函數的require_grad = True
這個參數告訴PyTorch需要追蹤在params
上進行運算而產生的所有張量。換句話說,任何以params
為祖先的張量都可以訪問從params
到該張量所調用的函數鏈。如果這些函數是可微的(大多數PyTorch張量運算都是可微的),則導數的值將自動存儲在參數張量的grad
屬性中
上一篇的手動求導過程現在可以改寫成這樣:
loss = loss_fn(model(t_u, *params), t_c)
loss.backward()
params.grad
用autograd計算的模型的前向傳播圖和反向傳播圖:
關於梯度顯示清零問題:
在這種情況下,PyTorch會在沿着整個函數鏈(即計算圖)計算損失的導數,並在這些張量(即計算圖的葉節點)的grad
屬性中將這些導數值累積(accumulate)起來。
警告:PyTorch的新手(以及很多經驗豐富的人)經常忽視的事情:是積累(accumulate)而不是存儲(store)。
警告:調用
backward
會導致導數值在葉節點處累積。所以將其用於參數更新后,需要將梯度顯式清零。
重復調用backward
會導致導數在葉節點處累積。因此,如果提前調用了backward
,然后再次計算損失並再次調用backward
(如在訓練循環中一樣),那么在每個葉節點上的梯度會被累積(即求和)在前一次迭代計算出的那個葉節點上,導致梯度值不正確。
為防止這種情況發生,你需要在每次迭代時將梯度顯式清零。可以使用就地方法zero_
輕松地做到這一點。
因此現在整個循環訓練過程可以改寫為:
def training_loop(n_epochs, learning_rate, params, t_u, t_c): for epoch in range(1, n_epochs + 1): if params.grad is not None: params.grad.zero_() # 這可以在調用backward之前在循環中的任何時候完成 t_p = model(t_u, *params) loss = loss_fn(t_p, t_c) loss.backward() params = (params - learning_rate * params.grad).detach().requires_grad_() if epoch % 500 == 0: print('Epoch %d, Loss %f' % (epoch, float(loss))) return params
優化器:
現在是時候介紹PyTorch從用戶代碼(例如訓練循環)中抽象出來的優化策略了,以使你免於繁瑣地更新模型中的每個參數。torch
模塊有一個optim
子模塊,你可以在其中找到實現不同優化算法的類。
每個優化器都有兩個方法:zero_grad
和step
。zero_grad將構造時傳遞給優化器的所有參數的grad
屬性歸零;step根據特定優化器實施的優化策略更新這些參數的值。
示例使用SGD優化器:
params = torch.tensor([1.0, 0.0], requires_grad=True) learning_rate = 1e-5 optimizer = optim.SGD([params], lr=learning_rate)
現在使用優化器改寫循環訓練代碼:
def training_loop(n_epochs, optimizer, params, t_u, t_c): for epoch in range(1, n_epochs + 1): t_p = model(t_u, *params) loss = loss_fn(t_p, t_c) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 500 == 0: print('Epoch %d, Loss %f' % (epoch, float(loss))) return params params = torch.tensor([1.0, 0.0], requires_grad=True) learning_rate = 1e-2 optimizer = optim.SGD([params], lr=learning_rate) training_loop( n_epochs = 5000, optimizer = optimizer, params = params, t_u = t_un, t_c = t_c)
完整代碼:
#!/usr/bin/env python # coding: utf-8 # In[1]: import torch # In[2]: t_c = [0.5, 14.0, 15.0, 28.0, 11.0, 8.0, 3.0, -4.0, 6.0, 13.0, 21.0] t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4] t_c = torch.tensor(t_c) t_u = torch.tensor(t_u) def model(t_u, w, b): return w*t_u + b def loss_fn(t_p, t_c): squared_diffs = (t_p-t_c)**2 return squared_diffs.mean() # require_grad = True這個參數告訴PyTorch需要追蹤在params上進行運算而產生的所有張量 # 換句話說,任何以params為祖先的張量都可以訪問從params到該張量所調用的函數鏈 # 如果這些函數是可微的(大多數PyTorch張量運算都是可微的),則導數的值將自動存儲在參數張量的grad屬性中 params = torch.tensor([1.0, 0.0], requires_grad=True) # 一般來講,所有PyTorch張量都有一個初始為空的名為grad的屬性: params.grad is None # True # In[3]: # 我們需要做的就是從將require_grad設置為True開始,然后調用模型,計算損失值,然后對損失張量loss調用backward: loss = loss_fn(model(t_u, *params), t_c) loss.backward() params.grad # In[9]: ''' 警告:調用backward會導致導數值在葉節點處累積。所以將其用於參數更新后,需要將梯度顯式清零。 ''' def training_loop(n_epochs, learning_rate, params, t_u, t_c): for epoch in range(1, n_epochs+1): if params.grad is not None: params.grad.zero_() t_p = model(t_u, *params) loss = loss_fn(t_p, t_c) loss.backward() params = (params-learning_rate*params.grad).detach().requires_grad_() if epoch % 500 == 0: print('Epoch %d, Loss %f' % (epoch, float(loss))) return params ''' 請注意,更新參數時,你還執行了奇怪的.detach().requires_grad_()。要了解原因,請考慮一下你構建的計算圖。為了避免重復使用變量名,我們重構params參數更新行:p1 = (p0 * lr * p0.grad)。這里p0是用於初始化模型的隨機權重,p0.grad是通過損失函數根據p0和訓練數據計算出來的。 到目前為止,一切都很好。現在,你需要進行第二次迭代:p2 = (p1 * lr * p1.grad)。如你所見,p1的計算圖會追蹤到p0,這是有問題的,因為(a)你需要將p0保留在內存中(直到訓練完成),並且(b)在反向傳播時不知道應該如何分配誤差。 相反,應該通過調用.detatch()將新的params張量從與其更新表達式關聯的計算圖中分離出來。這樣,params就會丟失關於生成它的相關運算的記憶。然后,你可以調用.requires_grad_(),這是一個就地(in place)操作(注意下標“_”),以重新啟用張量的自動求導。現在,你可以釋放舊版本params所占用的內存,並且只需通過當前權重進行反向傳播。 '''
# In[10]: t_un = 0.1 * t_u training_loop( n_epochs = 5000, learning_rate = 1e-2, params = torch.tensor([1.0, 0.0], requires_grad=True), t_u = t_un, t_c = t_c) # In[11]: # 優化器 import torch.optim as optim dir(optim) # In[12]: def training_loop(n_epochs, optimizer, params, t_u, t_c): for epoch in range(1, n_epochs + 1): t_p = model(t_u, *params) loss = loss_fn(t_p, t_c) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 500 == 0: print('Epoch %d, Loss %f' % (epoch, float(loss))) return params params = torch.tensor([1.0, 0.0], requires_grad=True) learning_rate = 1e-2 optimizer = optim.SGD([params], lr=learning_rate) training_loop( n_epochs = 5000, optimizer = optimizer, params = params, t_u = t_un, t_c = t_c) # Adam,你現在只需要知道它自適應地設置學習率,是一種更加復雜的優化器。此外,它對參數縮放的敏感度很低,以至於你可以使用原始(非標准化)輸入t_u甚至將學習率提高到1e-1: # In[14]: params = torch.tensor([1.0, 0.0], requires_grad=True) learning_rate = 1e-1 optimizer = optim.Adam([params], lr=learning_rate) training_loop( n_epochs = 5000, optimizer = optimizer, params = params, t_u = t_u, t_c = t_c) # In[15]: # 對張量的元素進行打亂等價於重新排列其索引。randperm函數完成了這個操作: n_samples = t_u.shape[0] n_val = int(0.2 * n_samples) shuffled_indices = torch.randperm(n_samples) train_indices = shuffled_indices[:-n_val] val_indices = shuffled_indices[-n_val:] train_indices, val_indices # 划分結果是隨機的 # In[16]: # 你獲得了可用於從數據張量構建訓練集和驗證集的索引: train_t_u = t_u[train_indices] train_t_c = t_c[train_indices] val_t_u = t_u[val_indices] val_t_c = t_c[val_indices] train_t_un = 0.1 * train_t_u val_t_un = 0.1 * val_t_u # In[17]: # 訓練循環代碼和之前一樣,額外添加了評估每個epoch的驗證損失以便查看是否過度擬合: def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u, train_t_c, val_t_c): for epoch in range(1, n_epochs + 1): train_t_p = model(train_t_u, *params) train_loss = loss_fn(train_t_p, train_t_c) val_t_p = model(val_t_u, *params) val_loss = loss_fn(val_t_p, val_t_c) optimizer.zero_grad() train_loss.backward() # 注意沒有val_loss.backward因為不能在驗證集上訓練模型 optimizer.step() if epoch <= 3 or epoch % 500 == 0: print('Epoch %d, Training loss %.2f, Validation loss %.2f' % ( epoch, float(train_loss), float(val_loss))) return params params = torch.tensor([1.0, 0.0], requires_grad=True) learning_rate = 1e-2 optimizer = optim.SGD([params], lr=learning_rate) training_loop( n_epochs = 3000, optimizer = optimizer, params = params, train_t_u = train_t_un, val_t_u = val_t_un, train_t_c = train_t_c, val_t_c = val_t_c) # In[ ]: