AI

PyTorch Tutorial その2 – torch.autograd

引き続きPyTorch Tutorialを見て行きます。今回は、A GENTLE INTRODUCTION TO TORCH.AUTOGRADです。(以下翻訳です)

torch.autogradの手引き

torch.autogradは、PyTorchの自動微分engineで、neural networkの学習をpowerfulに行うものです。このsectionでは、どのようにしてautogradがneural networkの学習を支えているのか、その概要を理解しましょう。

Background

Neural network (NN)はnestした関数の集合体で、入力が与えられることによって動作します。関数は変数(重みとbias)で定義され、PyTorchではtensorに格納されます。

NNの学習は以下の二つのstepで行われます:

Forward Propagation: 順伝搬では、NNは正解出力に対して最善の推測を行います。Networkに入力を与え、出力が推測値になります。

Backward Propagation: 逆伝搬では、推測値の誤差に比例して変数値を調整します。出力から逆方向にさかのぼりながら、関数の変数値の誤差の微分(勾配)を集めて行き、最急降下法を使って変数値を最適化することによって、この調整が行われます。逆伝搬の詳細をお知りになりたい場合は、3Blue1Brownからのビデオをご覧ください。

PyTorchでの使われ方

学習の一loopを見てみましょう。ここではtorchvisionから学習済みのresnet18をloadします。random dataを用いてサイズが64×64で3 channelの1枚の画像を作り、それに対するlabelにrandom値を与えます。

import torch, torchvision
model = torchvision.models.resnet18(pretrained=True)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)

次に、modelに入力を与え各layerに伝搬して行きます。これは順方向の伝搬になります。

prediction = model(data) # forward pass

modelの推定値と対応するlabelから誤差(loss)を計算します。次のstepはこの誤差を逆伝搬することです。逆伝搬は誤差tensorで.backward()が呼ばれたときにkickされます。するとautogradが各model変数の勾配を計算し変数の.grad属性に格納します。

loss = (prediction - labels).sum()
loss.backward() # backward pass

次にoptimizerをloadします。ここではlearning rateが0.01、momentumが0.9のSGDを利用します。Modelのすべての変数がoptimizerに登録されます。

optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

最後に最急降下法を施すために.step()を呼びます。Optimizerは.gradに格納された勾配によって各変数値を調整します。

optim.step() #gradient descent

これでneural networkを学習させるための準備が揃いました。以下のsectionはautogradの動作の詳細です。- 興味がなければスキップしても構いません。

Autogradでの微分動作

どうやってautogradが勾配を集めるのか見てみましょう。ここで二つのtensor a, bをつくります。requires_grad=Trueとしますが、これはすべての演算が追跡されることをautogradに伝えます。

import torch

a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)

別のtensor Qを以下のように定義します:

Q = 3a^3 - b^2

Q = 3*a**3 - b**2


ここで、a, bはNNの変数、Qは誤差とします。NNの学習では、変数の誤差の勾配が必要です。すなわち:

\frac{\partial Q}{\partial a} = 9a^2
\frac{\partial Q}{\partial b} = -2b

Qで.backward()を呼ぶと、autogradはこれらの勾配を計算し、対応するtensorの.grad属性に格納します。

Q.backward()の中で勾配vectorは明示的に渡されなければなりません。勾配はQと同じshapeのtensorで、Qに対するQの勾配になります。すなわち:

\frac{dQ}{dQ} = 1

同様に、Q.sum().backward()のようにQをスカラー量にまとめて暗にbackwardを呼ぶことも可能です。

external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)

勾配はa.grad, b.gradに置かれました。

# check if collected gradients are correct
print(9*a**2 == a.grad)
print(-2*b == b.grad)

Out:

tensor([True, True])
tensor([True, True])

さらにご参考 – autogradを用いたvectorの微積分

数学的に、vector関数\vec{y} = f(\vec{x})があった場合、\vec{x}に対する\vec{y}の勾配は、Jacovian行列Jとなります:

J = (\frac{\partial{y}}{\partial{x_1}} \cdots \frac{\partial{y}}{\partial{x_n}}) =\left[ \begin{array}{ccc}\frac{\partial{y_1}}{\partial{x_1}} & \cdots & \frac{\partial{y_1}}{\partial{x_n}}\\\vdots & \ddots & \vdots\\\frac{\partial{y_m}}{\partial{x_1}} & \cdots & \frac{\partial{y_m}}{\partial{x_n}}\\\end{array} \right]

概して言えば、torch.autogradはvector-Jacobian積を計算するengineです。つまり、\vec{v}が与えられたとき、J^T \cdot \vec{v}を計算します。

vが以下のようなscalar関数の勾配であるとき、

l = g(\vec{y}) = (\frac{\partial{l}}{\partial{y_1}} \cdots \frac{\partial{l}}{\partial{y_m}})^T

連鎖率により、vector-Jacobian積は以下のような\vec{x}に対するlの勾配となる:

J^T \cdot \vec{v} =\left[ \begin{array}{ccc}\frac{\partial{y_1}}{\partial{x_1}} & \cdots & \frac{\partial{y_1}}{\partial{x_n}}\\\vdots & \ddots & \vdots\\\frac{\partial{y_m}}{\partial{x_1}} & \cdots & \frac{\partial{y_m}}{\partial{x_n}}\\\end{array} \right]\left( \begin{array}{c} \frac{\partial{l}}{\partial{y_1}}\\ \vdots\\ \frac{\partial{l}}{\partial{y_m}} \end{array} \right) =\left( \begin{array}{c} \frac{\partial{l}}{\partial{x_1}}\\ \vdots\\ \frac{\partial{l}}{\partial{x_m}} \end{array} \right)

このvector-Jacobian積の性質は、上の例で用いたものに他ならない; すなわち\vec{v}がexternal_gradである。

計算Graph

概念的に、autogradは、関数objectから構成されるdirected acyclic graph (DAG)の中に、data (tensor)の履歴とすべての演算(新しいtensorの結果とともに)を維持しています。DAGにおいて、入力tensorはleaf、出力tensorはrootです。このgraphをrootからleafへ追跡することにより、連鎖率から自動的に勾配を計算できます。

順伝搬では、autogradは以下の二つを同時に行います:

In a forward pass, autograd does two things simultaneously:

  • tensorの結果を計算する
  • 演算に用いられた勾配関数をDAGに維持する

逆伝搬はDAGのrootで.backward()が呼ばれたときにkickされます。するとautogradは:

  • 各.grad_fnから勾配を計算し、
  • 結果を対応する各tensorの.grad属性に蓄積し、
  • 連鎖率を用いて、leaf tensorに至るすべての経路を逆伝搬します

下の図はこの例でのDAGを可視化したものです。Graphでは、矢印は順方向の伝搬を示します。Nodeは順方向の経路での各演算の逆伝搬関数を示します。青いleaf nodeはtensor a, bを表します。

../../_images/dag_autograd.png

Note: PyTorchではDAGは動的です。重要なことですが、Graphは毎回scratchから作り直されます; つまり.backward()が呼ばれるたびに、autogradは新しいgraphを再構築します。これにより、modelで制御フロー文を用いることが許容されます; つまり必要であれば、毎loopで演算のshape, sizeを変えることができるのです。

DAGからの排除

torch.autogradは、requires_grad flagがTrueになっているすべてのtensorの演算を追跡します。勾配が必要ないtensorについては、この属性をFalseに設定することにより、勾配計算DAGから外されます。

演算の出力tensorは、requires_grad=Trueとなっている入力tensorが一つでもあれば、勾配は必要です。

x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)

a = x + y
print(f"Does `a` require gradients? : {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")

Out:

Does `a` require gradients? : False
Does `b` require gradients?: True

NNでは、勾配を計算しない変数は通常frozen parameterと呼ばれます。予め勾配が不要な変数がわかるのであれば、Modelの一部を凍らせることができるのは有用です。(autogradの計算が減らせるので計算速度的に有利になります)

DAGからの排除が重要となるもうひとつのよくあるusecaseは、学習済みnetworkの追加学習です。

追加学習では、modelのほとんどをfreezeし、新しいlabelでの推論時に概して分類layerのみ更新可能とします。小さい例を用いてこれをやってみましょう。前と同様、学習済みのresnet18をloadし、すべての変数をfreezeします。

from torch import nn, optim

model = torchvision.models.resnet18(pretrained=True)

# Freeze all the parameters in the network
for param in model.parameters():
param.requires_grad = False

10種類のlabelを持つ新しいdatasetでmodelを追加学習します。Resnetでは、分類器は最後の線形layerであるmodel.fcです。ここでは単純にmodel.fcをfreezeされていない新しい線形layerで置き換えて分類器として動作させましょう。

model.fc = nn.Linear(512, 10)

今、model.fcの変数以外のすべてのmodel変数がfreezeされている状態です。勾配が計算される変数は、model.fcの重みとbiasのみになります。

# Optimize only the classifier
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)

再度注意ですが、すべての変数がoptimizerに登録されていますが、勾配が計算される(すなわち最急降下法で更新される)のは分類器の重みとbiasのみです。

同様の排除機能は、torch.no_grad()のcontext managerでも可能です。

さらなる読み物:

全scriptの計算時間: ( 0 minutes 1.491 seconds)


さいごに

今回は結構読みごたえあったのではないでしょうか ? 知っている内容でも、新しいプラットフォームだと新鮮に感じますね。引き続きTutorialを見て行きましょう !


   
関連記事
  • PyTorchでclass_weightを適用するには
  • Torchvisionのtransforms.Composeを使いこなしてtraining accuracyを上げよう !
  • torch.squeeze()よりtorch.view()が安心だった話し
  • PyTorch LightningのckptファイルをLoadするのにはまった話のその後
  • kernel_initializerって学習の収束に大事かも
  • PyTorchで学習済みモデルの中間層出力の取得

    コメントを残す

    *

    CAPTCHA