1. 程式人生 > >PyTorch學習筆記(12)——PyTorch中的Autograd機制介紹

PyTorch學習筆記(12)——PyTorch中的Autograd機制介紹

在《PyTorch學習筆記(11)——論nn.Conv2d中的反向傳播實現過程[1]中,談到了Autograd在nn.Conv2d的權值更新中起到的用處。今天將以官方的說明為基礎,補充說明一下關於計算圖、Autograd機制、Symbol2Symbol等內容。

0. 提出問題

不知道大家在使用PyTorch的時候是否有過“為什麼在每次迭代(iteration)的時候,optimizer都要清空?”這個問題,通過下面的Autograd機制的介紹,這個疑問會被解答。

下面,讓我們帶著問題,對PyTorch中使用的Autograd機制進行分析。

1. Autograd mechanics[1]

這個Note[2]將會告訴大家,autograd是怎樣執行的,並且它是如何記錄運算元ops的。如果你對autograd熟悉了之後,它會幫助你寫出更高效、簡潔的程式,並且有助於你的debug。

1.1 Excluding subgraphs from backward(從後向中排除子圖)

注意,在PyTorch中,每個Tensor都有requires_grad的屬性,它有助於對網路結構進行微調,並且,設定requires_grad=False的子圖將不用參與梯度計算,從而提高效率。

Every Tensor has a flag: :attr:requires_grad

that allows for fine grained exclusion of subgraphs from gradient computation and can increase efficiency.

>>> x = torch.randn(5, 5)  # requires_grad=False by default
>>> y = torch.randn(5, 5)  # requires_grad=False by default
>>> z = torch.randn((5, 5), requires_grad=
True) >>> a = x + y >>> a.requires_grad False >>> b = a + z >>> b.requires_grad True

需要注意的是,如果一個op對應單一的輸入(requires_grad=True),那麼該op的輸出的requires_grad也為True。反過來,也就是說當且僅當所有的輸入都不需要梯度的時候(requires_grad=False),那麼輸出也不需要梯度了。

當subgraph中所有的Tensor都不需要梯度的時候,那麼該subgraph也就永遠不會執行backward computation了。

當你想要固定模型中的某一部分的時候,或者說,當你提前知曉某些引數不需要進行梯度修正的時候,都非常有用。

For example if you want to finetune a pretrained CNN, it’s enough to switch the:attr:requires_grad flags in the frozen base, and no intermediate buffers will be saved, until the computation gets to the last layer, where the affine transform will use weights that require gradient, and the output of the network will also require them.

model = torchvision.models.resnet18(pretrained=True)
# 讓resnet18的所有的引數都不參與BP過程
for param in model.parameters():
    param.requires_grad = False
# Replace the last fully-connected layer
# Parameters of newly constructed modules have requires_grad=True by default
model.fc = nn.Linear(512, 100)

# 只對分類器進行優化
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)

1.2 How autograd encodes the history

Autograd是自動微分系統的反向過程。在概念上,autograd記錄了資料需要執行的所有運算元,並將其根據順序放置在一個有向無環圖(DAG)上。

可以理解為葉子節點是輸入的Tensor,根節點是輸出的Tensor。根據tracing圖的根到葉子,可以根據鏈式法則來求解梯度。

Autograd is reverse automatic differentiation system. Conceptually, autograd records a graph recording all of the operations that created the data as you execute operations, giving you a directed acyclic graph(DAG) whose leaves are the input tensors and roots are the output tensors. By tracing this graph from roots to leaves, you can automatically compute the gradients using the chain rule.

從原理來講,autograd將計算圖表示成一種由Function物件組成的圖。
在前向計算的時候,autograd同時執行運算元op對應的計算,並且建立一個表示計算梯度的函式的圖。
當前向計算完成時,我們將在反向回傳過程中評估這個圖,並計算梯度。

Internally, autograd represents this graph as a graph of :class:Function objects (really expressions), which can be:meth:~torch.autograd.Function.apply ed to compute the result of evaluating the graph. When computing the forwards pass, autograd simultaneously performs the requested computations and builds up a graph representing the function that computes the gradient (the .grad_fn attribute of each :class:torch.Tensor is an entry point into this graph). When the forwards pass is completed, we evaluate this graph in the backwards pass to compute the gradients.

很重要的一點在於,圖結構在每次迭代的時候都重新建立(每個batchsize),這也正是在PyTorch中,允許使用者使用任意的Python控制流語句的原因(for之類的)。可以在每次迭代中改變計算圖的整體形狀和大小。

An important thing to note is that the graph is recreated from scratch at every iteration, and this is exactly what allows for using arbitrary Python control flow statements, that can change the overall shape and size of the graph at every iteration. You don’t have to encode all possible paths before you launch the training - what you run is what you differentiate.

1.3 In-place operations with autograd(In-place的運算元)

在autograd支援in-place ops是一件困難的事情,關於in-place ops實際上就是在原資料的基礎上進行操作,避免建立新物件的機制,比如a.unsqueeze_(0)這種就是in-place ops。我們不建議在大多數情況下使用它,因為Autograd中高效的快取釋放和重用非常高效,in-place ops相比正常ops,在極少數時候才會顯著的降低記憶體使用率。
除非你在沉重的記憶體壓力下執行,否則你可能永遠不需要使用它們。

Supporting in-place operations in autograd is a hard matter, and we discourage their use in most cases. Autograd’s aggressive buffer freeing and reuse makes it very efficient and there are very few occasions when in-place operations actually lower memory usage by any significant amount. Unless you’re operating under heavy memory pressure, you might never need to use them.

There are two main reasons that limit the applicability of in-place operations:
In-place operations can potentially overwrite values required to compute gradients.
Every in-place operation actually requires the implementation to rewrite the computational graph. Out-of-place versions simply allocate new objects and keep references to the old graph, while in-place operations, require changing the creator of all inputs to the :class:Function representing this operation. This can be tricky, especially if there are many Tensors that reference the same storage (e.g. created by indexing or transposing), and in-place functions will actually raise an error if the storage of modified inputs is referenced by any other :class:Tensor.

2. 計算圖和Symbol2Symbol[2]

通過第1部分的內容,我們知道了PyTorch的這種動態圖跟Tensorflow的這種靜態圖計算方式的區別:PyTorch的動態圖在每次迭代中都需要重建。對於大型的網路結構來說,這裡面還是有一定的開銷的。

這裡,動態圖和靜態圖都是把計算對映為圖的一種方式,目前所有的常用深度學習框架都用到了計算圖的概念。那麼,什麼是計算圖呢?Symbol2Symbol又是什麼?下面來進行展開說明。

2.1 計算圖

為了更精確地描述反向傳播演算法,使用更精確的 計算圖(computational graph)語言是很有幫助的。將計算抽象為圖形的方法有很多,這裡,我們使用圖中的每一個節點來表示一個變數(可以是標量、向量、矩陣、張量等各種形式)。
為了形式化我們的圖形,我們還需引入 運算元(operator)這一概念(PyTorch、Tensorflow等框架提供了成百上千種不同的運算元,比如卷積、Pooling、GRU、LSTM、RNN等等等等)。運算元(operator是指一個或多個變數的簡單函式(也可能不那麼“簡單”)。我們的圖形語言伴隨著一組被允許的操作。我們可以通過將多個操作複合在一起來描述更為複雜的函式。
這裡,假設每個運算元只返回單個輸出變數。目的是:避免引入對概念理解不重要的許多額外細節。

注意,如果一個變數y是由變數x經過一個**運算元(operator)**得到的,那麼在計算圖中,會建立一條從x到y的有向邊。

而每個節點的名稱,在一般的框架中,是用op的名稱表示(加上一些引數,比如conv的話,會加上kernel size, stride等引數)
下圖[2]就是計算圖的例項:

在這裡插入圖片描述

2.2 Symbol2Symbol

計算圖說完了,下面說說以PyTorch為代表的動態圖和以Tensorflow、Theano為代表的靜態圖的差別。這裡面就不得不提到這個Symbol2Symbol了。

代數表示式和計算圖都對符號(symbol)不具有特定值的變數進行操作。這些代數或者基於圖的表示式被稱為符號表示(symbolic representation)
這個部分很好理解,因為我們對網路的輸入每個batch、每個樣本的的內容都不一樣,為了避免重複的計算(比如重複的求解梯度表示式),所以需要制定出每個運算元(operator)的確定的計算邏輯。

當我們實際使用或者訓練神經網路時,我們必須給這些符號賦值。我們用一個特定的數值(numeric value) 來替代網路的符號輸入x,例如 [ 1.2 , 3 , 765 , 1.8 ] T [1.2,3,765,−1.8]^T

一些反向傳播的方法採用計算圖和一組用於圖的輸入的數值,然後返回在這些輸入值處的梯度的數值。這種方法稱為Symbol2Value——符號到數值的微分方法。也就是PyTorch以及其前身Torch和Caffe所採用的方法。

另一種方法是採用計算圖和為計算圖新增額外的節點(用於計算運算元梯度),這些額外的節點提供了我們所需的導數的符號描述,稱為Symbol2Symbol。這就是Theano和Tensorflow使用的方法。

Tensorflow和Theano類似,額外節點提供了所需導數的符號描述。這種方法的主要優點是導數可以使用與原始表示式相同的語言來描述。因為導數只是另外一張計算圖(新增到主計算圖中),我們可以再次執行反向傳播,對導數 再進行求導就能得到更高階的導數。

補充,這也就是為什麼Tensorflow的模型在部署的時候,可以進行去掉訓練節點這種方法的原因。

這也是Tensorflow這種靜態圖的優越之處————一勞永逸。DL這本書使用Symbol2Symbol來描述反向傳播演算法——為導數構建出一個計算圖並加到主計算圖中。

基於Symbol2Symbol——符號到符號的方法的描述包含了Symbol2Value符號到數值的方法。符號到數值的方法可以理解為執行了與符號到符號的方法中構建圖的過程中完全相同的計算。關鍵的區別:符號到數值的方法不會顯示出計算圖。

下圖是符號到符號的方法,即構建出導數的計算圖,這裡容易看出: d z d w = d x d w d z d x \frac {dz} {dw} = \frac {dx} {dw} * \frac {dz} {dx}
d z d x = d y d x d z d y \frac {dz} {dx} = \frac {dy} {dx} * \frac {dz} {dy}
在這裡插入圖片描述

3. 問題回顧

可能看到這裡,大家覺得我只是羅列一些概念和知識點,並沒有真正的回答開篇提到的問題“為什麼在每次迭代(iteration)的時候,optimizer都要清空?

這裡結合前面提到的知識點進行總結可知:

  • PyTorch是動態圖
  • 動態圖在每次iteration後都需要重建圖本身
  • PyTorch的optimizer預設情況會自動對梯度進行accumulate[4],所以對下一次iteration(一個新的batch),需要對optimizer進行清空操作。使用方式如下:
for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs
        inputs, labels = data

        # 情況引數梯度
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')

[1] 《PyTorch學習筆記(11)——論nn.Conv2d中的反向傳播實現過程
[2] Autograd In PyTorch
[3] 《Deep Learning——6.5 反向傳播與其它的微分演算法
[4] PyTorch 1.0.0.dev20181002 TRAINING A CLASSIFIER