1. 程式人生 > 實用技巧 >Pytorch學習記錄001-Autograd和Backward

Pytorch學習記錄001-Autograd和Backward

1.一個線性迴歸的例子

  假設你去了一些鮮為人知的地方旅遊,然後帶回了一個花哨的壁掛式模擬溫度計。這個溫度計看起來很棒,非常適合你的客廳。唯一的缺點是它不顯示單位。不用擔心,你有一個計劃。你用自己喜歡的單位建立一個讀數和相應溫度值的資料集,然後選擇一個模型,並迭代調整單位的權重,直到誤差的測量值足夠低為止,最後你就可以在新溫度計上進行準確讀數了。
  首先記錄能正常工作的舊攝氏溫度計的資料和你剛帶回來的新溫度計對應的測量值。幾周後,你得到了一些資料:
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_c是攝氏度數
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_u是未知單位度數
t_c = torch.tensor(t_c)
t_u = torch.tensor(t_u)
  為了根據未知溫度t_u獲得以攝氏度為單位的溫度值 t_c,先選擇線性模型作為一個假設, t_c = w*t_u + b
  為了用梯度下降演算法更新引數w,b,核心問題就是如何求導。
  簡單的例子,我們可以自己實現算出偏導的表示式,或者和官方示例一樣從導數的定義式出發
delta = 0.1
loss_rate_of_change_w = (loss_fn(model(t_u, w + delta, b), t_c) - loss_fn(model(t_u, w - delta, b), t_c)) / (2.0 * delta)

2.使用自動求導

  如果你有一個具有數百萬個引數的複雜模型,只要模型是可微的,損失函式相對於引數的梯度的計算就相當於編寫導數的解析表示式並對其進行一次評估(evaluation)。當然,為由線性和非線性函式組成的複雜函式的導數編寫解析表示式並不是一件很有趣的事情,也不是一件很容易的事情。
  這個問題可以通過一個名為Autograd的PyTorch模組來解決。PyTorch張量可以記住它們來自什麼運算以及其起源的父張量,並且提供相對於輸入的導數鏈。你無需手動對模型求導:不管如何巢狀,只要你給出前向傳播表示式,PyTorch都會自動提供該表示式相對於其輸入引數的梯度。
  1.首先,定義模型和損失函式
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()
  2.然後初始化引數張量
params = torch.tensor([1.0, 0.0], requires_grad=True)
  3.require_grad = True這個引數告訴PyTorch需要追蹤在params上進行運算而產生的所有張量,任何以params為祖先的張量都可以訪問從params到該張量所呼叫的函式鏈。如果這些函式是可微的,則導數的值將自動儲存在引數張量的grad屬性中。然後呼叫模型,計算損失值,然後對損失張量loss呼叫backward
loss = loss_fn(model(t_u, *params), t_c)
loss.backward()
  用一張圖來形象地表示這個過程:

  你可以將包含任意數量的張量的require_grad設定為True以及組合任何函式。在這種情況下,PyTorch會在沿著整個函式鏈(即計算圖)計算損失的導數,並在這些張量(即計算圖的葉節點)的grad屬性中將這些導數值累積**(accumulate)** 起來。
  重複呼叫backward會導致導數在葉節點處累積。因此,如果提前呼叫了backward,然後再次計算損失並再次呼叫backward(如在訓練迴圈中一樣),那麼在每個葉節點上的梯度會被累積(即求和)在前一次迭代計算出的那個葉節點上,導致梯度值不正確。
  為防止這種情況發生,你需要在每次迭代時將梯度顯式清零。
if params.grad is not None:
    params.grad.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_grad() # 這可以在呼叫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
  這裡要說明下,detach()和requires_grad_()是幹啥用的?detach()截斷了反向傳播的梯度流,官方文件描述為“ Returns a new Tensor, detached from the current graph.The result will never require gradient.”將某個node變成不需要梯度的Varibale。因此當反向傳播經過這個node時,梯度就不會從這個node往前面傳播。
  為什麼要呼叫detach呢?我們重構params引數更新行:p1 = (p0 - learning_rate * p0.grad)。這裡p0是用於初始化模型的隨機權重,p0.grad是通過損失函式根據p0和訓練資料計算出來的。到目前為止,一切都很好。現在,你需要進行第二次迭代:p2 = (p1 - learning_rater * p1.grad)。如你所見,p1的計算圖會追蹤到p0,這是有問題的。因為(a)你需要將p0保留在記憶體中(直到訓練完成),並且(b)在反向傳播時不知道應該如何分配誤差。應該通過呼叫.detatch()將新的params張量從與其更新表示式關聯的計算圖中分離出來。這樣,params就會丟失關於生成它的相關運算的記憶。然後,你可以呼叫.requires_grad_(),這是一個就地(in place)操作(注意下標“_”),以重新啟用張量的自動求導。現在,你可以釋放舊版本params所佔用的記憶體,並且只需通過當前權重進行反向傳播。

3. 優化器

  每個優化器建構函式都將引數(通常是將require_grad設定為True的PyTorch張量)作為第一個輸入。傳遞給優化器的所有引數都保留在優化器物件內,以便優化器可以更新其值並訪問其grad屬性,如圖所示。

  (A)優化器對引數的引用的概念表示,然後(B)根據輸入計算損失,(C)對backward的呼叫會將grad填充到引數內。此時,(D)優化器可以訪問grad並計算引數更新。
  每個優化器都有兩個方法:zero_grad和step。前者將構造時傳遞給優化器的所有引數的grad屬性歸零;後者根據特定優化器實施的優化策略更新這些引數的值。 現在建立引數並例項化一個梯度下降優化器:
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-5
optimizer = optim.SGD([params], lr=learning_rate)
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()
optimizer.step()
  呼叫step後params的值就會更新,無需親自更新它!呼叫step發生的事情是:優化器通過將params減去learning_rate與grad的乘積來更新的params,這與之前手動編寫的更新過程完全相同。
  還需要注意一個大陷阱:不要忘了將梯度清零。如果你在迴圈中呼叫了前面的程式碼,則在每次呼叫backward時,梯度都會在葉節點中累積且會傳播得到處都是!需要在正確的位置(在呼叫backward之前)插入額外的zero_grad。

完整程式碼如下:

import torch
import torch.optim as optim

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()


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


if __name__ == "__main__":
    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)
    params = torch.tensor([1.0, 0.0], requires_grad=True)
    # require_grad = True這個引數告訴PyTorch需要追蹤在params上進行運算而產生的所有張量
    # 建立引數並例項化一個梯度下降優化器
    params = torch.tensor([1.0, 0.0], requires_grad = True)
    learning_rate = 1e-2
    optimizer = optim.SGD([params], lr = learning_rate)   
    t_un = 0.1 * t_u # 標準化操作的粗略替代
    training_loop(
        n_epochs = 5000,
        optimizer = optimizer,
        params = params,
        t_u = t_un,
        t_c = t_c)
  和前文中手寫的梯度更新比較,使用了optimizer.zero_grad()後就不需要params.grad.zero_grad()了,這是為何呢?

4. 關閉autograd

  現在我們將上文中的資料劃分出了訓練集和測試集,分別驗證模型在這兩個資料集上的擬合程度。

  顯然,我們不需要通過測試集資料來對引數進行梯度下降更新。
  為了解決這個問題,PyTorch允許你通過使用torch.no_grad上下文管理器在不需要時關閉autograd。雖然就小規模問題而言,在速度或記憶體消耗方面沒有任何有意義的優勢。但是對於較大的問題,差別可能會很明顯。你可以通過檢查val_loss張量上require_grad屬性的值來確保此上下文管理器正常工作:
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)

        with torch.no_grad():
            val_t_p = model(val_t_u, *params)
            val_loss = loss_fn(val_t_p, val_t_c)
            assert val_loss.requires_grad == False

        optimizer.zero_grad()
        train_loss.backward()
        optimizer.step()