r0w0

PythonやDeepLearning関連で学んだこと、調べたことの備忘録

PyTorch: Tensorはただの行列の入れ物ではなかった

Motivation

まずはPyTorchをどう適用するのか全体像をつかみたいと思い、二値分類データを使ってモデルを組んでCVするところまでPyTorchを使ってみた。ざっくりとだが、何となくPyTorchの使い方が分かった気がする。結構スクラッチな部分が多いんだなという印象。

次に、公式のチュートリアル AUTOGRAD: AUTOMATIC DIFFERENTIATION をこなしていたらTensorについて知らないことが多かったのでまとめておきたい。基本的にはチュートリアルに書いてあることを自分の言葉でまとめ直すだけの記事である。

このチュートリアルで得られたものは大きかった。 Tensorの計算は実はトレースされていて、実は計算グラフを作っていたということに今さら気づいた(思い出した)。 だからこそ、Tensor同士がチェインするような仕様になっている。

モデルクラスのForwardやinitを書いているときに、どうやってネットワーク構造を保持・定義しているのかと疑問だったけど、Forwardの計算を通じて定義されていたのだろう。 PyTorchの何がdefine by runなの?という疑問もここにきて解消するといううれしみ。

Tensor

tensorは.backward()により微分される

以下のTensorを準備する。 xのTensorでrequires_grad=Trueが指定されていることに注意。

x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
print(y)
print(z) 
print(out)
# tensor([[3., 3.],
#         [3., 3.]], grad_fn=<AddBackward0>)
# tensor([[27., 27.],
#         [27., 27.]], grad_fn=<MulBackward0>)
# tensor(27., grad_fn=<MeanBackward0>)

次にout.backward()を実行すると以下の結果が得られる。

out.backward()
print(x.grad)

# tensor([[4.5000, 4.5000],
#         [4.5000, 4.5000]])

これはxを変数としてoutを微分した結果。 \displaystyle \frac{dout}{dx} = \frac{dout}{dz} \cdot \frac{dz}{dy} \cdot \frac{dy}{dx}

具体的には

 out = \frac{1}{4} (z_1 + z_2 + z_3 + z_4)

なので、z_1に着目すると、

\displaystyle \frac{dout}{dx} = \frac{dout}{dz_1} \cdot \frac{dz_1}{dy} \cdot \frac{dy}{dx} = \frac{1}{4} \cdot 6y = \frac{18}{4} = 4.5

つまり、requires_grad=Trueを指定することでそのTensorを変数・パラメータとして扱い、かつbackpropによって微分が行われ勾配が計算される。

print(tensor)にNone/Falseのattributeは表示されない

テンソルはパラメータ扱いなのかどうか、printから確認できるのかどうか見てみた。

printでtensorを表示すると、gradがTrueの場合、該当attributeが表示される。

x = torch.ones(2, 2)
print(x)

# tensor([[1., 1.],
#         [1., 1.]])

x = torch.ones(2, 2, requires_grad=True)
print(x)

# tensor([[1., 1.],
#        [1., 1.]], requires_grad=True)

一方Falseの場合、そもそもそのAttributeの表示が省略されていることが分かる。

requires_gradはDefaultでFalse

前セクションのコードの実行結果のとおり、requires_gradは指定しなければ、DefaultでFalseである。

明示的にTrueを与えるとgrad計算の対象となる仕組み。

Weightはrequire_grad=Trueになっている?

上述の理屈に従うと、weightはrequire_grad=Trueでなければいけない。確認してみると、

for i in net.parameters():
    print(i)
    break

# Parameter containing:
# tensor([[-0.1722, -0.0790,  0.1510,  ...,  0.1590,  0.1150, -0.0679],
#         [-0.0703, -0.0227, -0.0645,  ...,  0.1713,  0.1660,  0.0976],
#         ...,
#         [ 0.1608, -0.0622,  0.0199,  ...,  0.0639, -0.0312,  0.0231]],
#        requires_grad=True)

ちゃんとrequires_grad=Trueになっている。

Operation結果によりTensorが作成されるとgrad_fnも指定される

grad_fnは以下のチュートリアルの説明にもある通り、Tensorを作成したオペレーション関数が指定される。

Each tensor has a .grad_fn attribute that references a Function that has created the Tensor (except for Tensors created by the user - their grad_fn is None).

また、Tensorを作成する式の最後のオペレーションがgrad_fnに指定されると思われる。 以下の式では、zは掛け算のオペレーションにより作成されているのでgrad_fn=<MulBackward0>が与えられる。

x = torch.ones(2, 2, requires_grad=True)
x2 = torch.ones(2, 2)
z = x * x2
print(z)

# tensor([[1., 1.],
#         [1., 1.]], grad_fn=<MulBackward0>)

一方、掛け算と足し算の組み合わせの計算では、足し算の演算が最後に行われるのでgrad_fn=<AddBackward0>が与えられている。

z = x * x2 + x
print(z)

# tensor([[2., 2.],
#         [2., 2.]], grad_fn=<AddBackward0>)

grad_fnの役割は・・・?

チュートリアル中では以下のように説明されている。grad_fnは計算グラフを構築しているよと。

Tensor and Function are interconnected and build up an acyclic graph, that encodes a complete history of computation.

またチュートリアル次のページでは以下のコードとともに、

output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

# tensor(1.4288, grad_fn=<MseLossBackward>)

以下の説明がある

Now, if you follow loss in the backward direction, using its .grad_fn attribute, you will see a graph of computations that looks like this:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss

1.4288の値を持つTensor:lossは、MSELoss関数であるgrad_fn=<MseLossBackward>の関数により作成された(計算グラフ的につながっている)ことがprint(loss)の結果より示されている。ふんわり解釈するとgrad_fnはそのTensorがどの関数(Operationともいえる?)に作成されたかを示し、その数珠繋ぎにより計算グラフが構築される。