引き続き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 = 3*a**3 - b**2
ここで、a, bはNNの変数、Qは誤差とします。NNの学習では、変数の誤差の勾配が必要です。すなわち:
Qで.backward()を呼ぶと、autogradはこれらの勾配を計算し、対応するtensorの.grad属性に格納します。
Q.backward()の中で勾配vectorは明示的に渡されなければなりません。勾配はQと同じshapeのtensorで、Qに対するQの勾配になります。すなわち:
同様に、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関数があった場合、に対するの勾配は、Jacovian行列Jとなります:
概して言えば、torch.autogradはvector-Jacobian積を計算するengineです。つまり、が与えられたとき、を計算します。
が以下のようなscalar関数の勾配であるとき、
連鎖率により、vector-Jacobian積は以下のようなに対するの勾配となる:
このvector-Jacobian積の性質は、上の例で用いたものに他ならない; すなわちが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を表します。
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を見て行きましょう !