天天看點

Pytorch的backward()相關了解

最近一直在用pytorch做GAN相關的實驗,pytorch 架構靈活易用,很适合學術界開展研究工作。

這兩天遇到了一些模型參數尋優的問題,才發現自己對pytorch的自動求導和尋優功能沒有深刻了解,導緻無法靈活的進行實驗。于是查閱資料,同時自己做了一點小實驗,做了一些總結,雖然好像都是一些顯而易見的結論,但是如果不能清晰的了解,對于實驗複雜的網絡模型程式會造成困擾。以下僅針對pytorch 0.2 版本,如有錯誤,希望得到指正。(17年寫的博文,小作修改)

  • 相關标志位/函數
    • 1
      • requires_grad
      • volatile
      • detach()/detach_()
    • 2
      • retain_graph
      • retain_variables
      • create_graph

    前三個标志位中,最關鍵的就是 requires_grad,另外兩個都可以轉化為 requires_grad 來了解。

    後三個标志位,與計算圖的保持與建立有關系。其中 retain_variables 與 retain_graph等價,retain_variables 會在pytorch 新版本中被取消掉。

  • requires_grad 的含義及标志位說明
    • 如果對于某Variable 變量 x ,其

      x.requires_grad == True

      , 則表示 它可以參與求導,也可以從它向後求導。

      預設情況下,一個新的Variables 的 requires_grad 和 volatile 都等于 False 。

    • requires_grad == True

      具有傳遞性,如果:

      x.requires_grad == True

      y.requires_grad == False

      z=f(x,y)

      則,

      z.requires_grad == True

    • 凡是參與運算的變量(包括 輸入量,中間輸出量,輸出量,網絡權重參數等),都可以設定 requires_grad 。
    • volatile==True

      就等價于

      requires_grad==False

      volatile==True

      同樣具有傳遞性。一般隻用在inference過程中。若是某個過程,從 x 開始 都隻需做預測,不需反傳梯度的話,那麼隻需設定

      x.volatile=True

      ,那麼 x 以後的運算過程的輸出均為

      volatile==True

      ,即

      requires_grad==False

      雖然inference 過程不必backward(),是以requires_grad 的值為False 或 True,對結果是沒有影響的,但是對程式的運算效率有直接影響;是以使用

      volatile=True

      ,就不必把運算過程中所有參數都手動設一遍

      requires_grad=False

      了,友善快捷。
    • detach()

      ,如果 x 為中間輸出,

      x' = x.detach

      表示建立一個與 x 相同,但

      requires_grad==False

      的variable, (實際上是把x’ 以前的計算圖 grad_fn 都消除了),x’ 也就成了葉節點。原先反向傳播時,回傳到x時還會繼續,而現在回到x’處後,就結束了,不繼續回傳求到了。另外值得注意, x (variable類型) 和 x’ (variable類型)都指向同一個Tensor ,即 x.data

      detach_()

      表示不建立新變量,而是直接修改 x 本身。
    • retain_graph

      ,每次 backward() 時,預設會把整個計算圖free掉。一般情況下是每次疊代,隻需一次 forward() 和一次 backward() ,前向運算forward() 和反向傳播backward()是成對存在的,一般一次backward()也是夠用的。但是不排除,由于自定義loss等的複雜性,需要一次forward(),多個不同loss的backward()來累積同一個網絡的grad,來更新參數。于是,若在目前backward()後,不執行forward() 而可以執行另一個backward(),需要在目前backward()時,指定保留計算圖,即backward(retain_graph)。
    • create_graph ,這個标志位暫時還未深刻了解,等之後再更新。
  • 反向求導 和 權重更新
    • 求導和優化(權重更新)是兩個獨立的過程,隻不過優化時一定需要對應的已求取的梯度值。是以求得梯度值很關鍵,而且,經常會累積多種loss對某網絡參數造成的梯度,一并更新網絡。
    • 反向傳播過程中,肯定需要整個過程都鍊式求導。雖然中間參數參與求導,但是卻可以不用于更新該處的網絡參數。參數更新可以隻更新想要更新的網絡的參數。
    • 如果obj是函數運算結果,且是标量,則 obj.backward() (注意,backward()函數中沒有填入任何tensor值, 就相當于

      backward(torch.tensor([1]))

      )。
    • 對于繼承自 nn.Module 的某一網絡 net 或網絡層,定義好後,發現 預設情況下,net.paramters 的 requires_grad 就是 True 的(雖然隻是實驗證明的,還未從源碼處找到證據),這跟普通的Variable張量不同。是以,當

      x.requires_grad == False

      ,

      y = net(x)

      後, 有

      y.requires_grad == True

      ;但值得注意,雖然nn.xxloss和激活層函數,是繼承nn.Module的,但是這兩種并沒有網絡參數,就更談不上 paramters.requires_grad 的值了。是以類似這兩種函數的輸出,其requires_grad隻跟輸入有關,不一定是 True .
  • 計算圖相關
    • 計算圖就是模型 前向forward() 和後向求梯度backward() 的流程參照。
    • 能擷取回傳梯度(grad)的隻有計算圖的葉節點。注意是擷取,而不是求取。中間節點的梯度在計算求取并回傳之後就會被釋放掉,沒辦法擷取。想要擷取中間節點梯度,可以使用 register_hook (鈎子)函數工具。當然, register_hook 不僅僅隻有這個作用。
    • 隻有标量才能直接使用 backward(),即

      loss.backward()

      , pytorch 架構中的各種nn.xxLoss(),得出的都是minibatch 中各結果 平均/求和 後的值。如果使用自定義的函數,得到的不是标量,則backward()時需要傳入 grad_variable 參數,這一點詳見部落格 https://sherlockliao.github.io/2017/07/10/backward/ 。
    • 經常會有這樣的情況:

      x1 —> |net1| —> y1 —> |net2| —> z1 , net1和net2是兩個不同的網絡。x1 依次通過 兩個網絡運算,生成 z1 。比較擔心一次性運算後,再backward(),是不是隻更新net1 而不是net1、net2都更新呢?

      類比 x2 —> |f1| —> y2 —> |f2| —> z2 , f1 、f2 是兩個普通的函數,

      z2=f2(y2)

      ,

      y2=f1(x2)

      按照以下代碼實驗

    w1 = torch.Tensor([2]) #認為w1 與 w2 是函數f1 與 f2的參數
    w1 = Variable(w1,requires_grad=True)
    w2 = torch.Tensor([2])
    w2 = Variable(w2,requires_grad=True)
    x2 = torch.rand(1)
    x2 = Variable(x2,requires_grad=True)
    y2 = x2**w1            # f1 運算
    z2 = w2*y2+1           # f2 運算
    z2.backward()
    print(x2.grad)
    print(y2.grad)
    print(w1.grad)
    print(w2.grad)
               
    發現 x2.grad,w1.grad,w2.grad 是個值 ,但是 y2.grad 卻是

    None

    , 說明x2,w1,w2的梯度保留了,y2 的梯度擷取不到。實際上,仔細想一想會發現,x2,w1,w2均為葉節點。在這棵計算樹中 ,x2 與w1 是同一深度(底層)的葉節點,y2與w2 是同一深度,w2 是單獨的葉節點,而y2 是x2 與 w1 的父節點,是以隻有y2沒有保留梯度值, 印證了之前的說法。同樣這也說明,計算圖本質就是一個類似二叉樹的結構。
    Pytorch的backward()相關了解
    那麼對于 兩個網絡,會是怎麼樣呢? 我使用pytorch 的cifar10 例程,稍作改動做了實驗。把例程中使用的一個 Alexnet 拆成了兩個net ------ net1 和 net2 。
    optimizer = torch.optim.SGD(itertools.chain(net1.parameters(), net2.parameters()),lr=0.001, momentum=0.9) # 這裡 net1 和net2 優化的先後沒有差別 !!
        #
        optimizer.zero_grad() #将參數的grad值初始化為0
        #
        # forward + backward + optimize
        outputs1 = net1(inputs)            #input 未置requires_grad為True,但不影響
        outputs2 = net2(outputs1)
        loss = criterion(outputs2, labels) #計算損失
        loss.backward()                    #反向傳播      
        #     
        print("inputs.requires_grad:")
        print(inputs.requires_grad)        # False
        print("the grad of inputs:")
        print(inputs.grad)                 # None
        
        print("outputs1.requires_grad:")
        print(outputs1.requires_grad)      # True
        print("the grad of outputs1:")        
        print(outputs1.grad)               # None     
        # 
        print("the grad of net1:")
        print(net1.conv1.bias.grad)        # no-None
        print("the grad of net2:")
        print(net2.fc3.bias.grad)          # no-None
        #
        optimizer.step() #用SGD更新參數
               

    字尾注釋就是列印的結果。可以看出,隻有網絡參數的grad是直接可擷取的。而且是兩個網絡都可以擷取grad 值,擷取grad後,當然就可以更新網絡的參數了,兩個網絡都是可以更新的。

    類比上邊例子的解釋,兩個網絡其實就是處在葉節點的位置,隻不過深度不同。同理,網絡内部的運算,每一層網絡權重參數其實也是處在葉節點上,隻不過在樹中的深度不同罷了,前向運算時按照二叉樹的結構,不斷生成父節點。

    (事實上,原先是以為 網絡 與 普通函數不同,因為它具有register_xx_hook()這個類函數工具,是以認為它可以預設儲存權重參數的grad來用于更新,後來才明白,本質上與普通函數的參數一樣,都是處在葉節點,就可以儲存參數的grad,至于register_xx_hook(),看來是另做它用,或者說用register_xx_hook()可以記錄甚至更改中間節點的grad值)

  • 一些特殊的情況:
    • 把網絡某一部分參數,固定,不讓其被訓練。可以使用requires_grad.
    for p in sub_module.parameters():
    	p.requires_grad = False
               
    可以這樣了解,因為是葉節點(而不是中間節點),是以不求grad(grad為’None’),也不會影響網絡的正常反向傳播。

以上就是一些總結,敬請指正!

繼續閱讀