LoginSignup
26
22

More than 3 years have passed since last update.

【PyTorchチュートリアル②】Autograd: 自動微分

Last updated at Posted at 2020-02-28

はじめに

前回に引き続き、PyTorch 公式チュートリアル の第2弾です。
今回は Autograd: Automatic Differentiation を進めてみたいと思います。

目次

1.Autograd(自動微分)
2.Tensor
3.勾配
4.You can do many crazy things with autograd!
5.最後に
履歴

1.Autograd(自動微分)

PyTorch には Autograd(自動微分)の機能が実装されています。
Tensor に勾配情報を保持しておき、定義した計算グラフ(式)に対して backward() メソッドで勾配が計算されます。
以下で具体例をあげながら Autograd について見ていきましょう。

2.Tensor

PyTorch の Tensor は、requires_grad 属性を True にすることで勾配が記録されるようになります。
backward() で勾配を計算すると、Tensor の grad 属性に勾配が保持されます。

以下の記述で Tensor を定義します。
requires_grad=True を指定し、勾配が記録されるようにします。

import torch
x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

計算グラフ(式) y を作成します。

y = x + 2
print(y)
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)

print すると、出力に grad_fn があります。
これは、勾配を計算するための計算グラフが構築されていることを示しています。

print(y.grad_fn)
<AddBackward0 object at 0x7f8cc977e5c0>

y を利用してさらに計算グラフ(式) z、 out を作成します。

z = y * y * 3
out = z.mean()

print(z, out)
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)

[参考情報]
tensor.requires_grad_() で requires_grad 属性を変更することができます。

a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
False
True
<SumBackward0 object at 0x7fcb2ba0a3c8>

3.勾配

out.backward() で勾配を計算します。

out.backward()

out の x による偏微分、d(out)/dx を出力します。

print(x.grad)
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])

out は out = z.mean() 、 z は z = y * y * 3 ですので、以下の式になります。

out = \frac{1}{4}\sum_{i=1}^{4}z_i
 、 
z_i = 3(x_i + 2)^2
 、 
z_i\bigr\rvert_{x_i=1} = 27 

従って out を x で偏微分すると

\begin{align}
\frac{\partial out}{\partial x_i} &= \frac{3}{2}(x_i+2)\\
&= \frac{9}{2} = 4.5
\end{align}

になり、自動微分されていることが分かります。

4.You can do many crazy things with autograd!

以下のコードの意味するところがよく分からないのですが、見ていきます。

x = torch.randn(3, requires_grad=True)

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

print(y)
tensor([ -492.4446, -1700.8485,  -339.7951], grad_fn=<MulBackward0>)

x は標準化(平均0、標準偏差1)されたランダム値です。
y.data.norm() はウィキペディアにも説明がありますが、ベクトル空間における距離のことで、以下のユークリッドノルムのことです。

ユークリッドノルム = \sqrt{|x_1|^2+\cdots+|x_n|^2} \\

2次元の場合、2点間の距離と同じ式になりますので、まさに距離のことです。
実際、x の値と norm() を出力してみると、上記の計算式の結果がノルムとして返却されます。

\begin{eqnarray}
ユークリッドノルム &=& \sqrt{|-0.9618|^2+|-3.3220|^2+|-0.6637|^2}\\
&=& 3.5215
\end{eqnarray}
print(x)
print(x.data.norm())
tensor([-0.9618, -3.3220, -0.6637], requires_grad=True)
tensor(3.5215)

ですので、上記のコードは、x のノルムが 1,000 になるまで x を2倍し続けるという式を表します。

y の勾配を y.backward() で計算したいのですが、y はスカラーではないため、そのまま計算することができません。
実際、y.backward() を実行するとエラーが発生します。

y.backward()
RuntimeError: grad can be implicitly created only for scalar outputs

適当なベクトルを設定することで、勾配が計算されます。

gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients)

print(x.grad)
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])

このチュートリアルの意味するところを考えてみます。
このチュートリアルにも記載されていますが、勾配はヤコビ行列で表すことができます。

\begin{split}J=\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)\end{split}

また、autograd はヤコビ行列と与えられたベクトルの積を計算するためのエンジンであるとの記載もあります。

\begin{split}J^{T}\cdot v=\left(\begin{array}{ccc}
 \frac{\partial y_{1}}{\partial x_{1}} & \cdots & \frac{\partial y_{m}}{\partial x_{1}}\\
 \vdots & \ddots & \vdots\\
 \frac{\partial y_{1}}{\partial x_{n}} & \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_{n}}
 \end{array}\right)\end{split}

この情報をもとに、今回のケースに当てはめてみます。
ここからは、自分の想像も入っているので正しくないかもしれません。

x = torch.randn(3, requires_grad=True)

x はランダムの3変数ですので x の添え字 n は 3 になります。

x_1 ,  x_2 , x_3

y を考えます。
y の定義は以下です。

y = x * 2
while y.data.norm() < 1000:
    y = y * 2

まず、最初の y = x * 2 を見てみます。
変数の数に注目すると、y は x * 2 で、単純に2倍するだけですので、変数の数は変わりません。
ですので、y で変換後も 値は 3つのままです。
従って m も 3 になり、ヤコビ行列のサイズは 3 × 3 です。

{\begin{split}J=\left(\begin{array}{ccc}
 \frac{\partial y_{1}}{\partial x_{1}} & \frac{\partial y_{1}}{\partial x_{2}} & \frac{\partial y_{1}}{\partial x_{3}}\\
 \frac{\partial y_{2}}{\partial x_{1}} & \frac{\partial y_{2}}{\partial x_{2}} & \frac{\partial y_{2}}{\partial x_{3}}\\
 \frac{\partial y_{3}}{\partial x_{1}} & \frac{\partial y_{3}}{\partial x_{2}} & \frac{\partial y_{3}}{\partial x_{3}}\\
 \end{array}\right)\end{split}
}

上の print(x)の値 [-0.9618, -3.3220, -0.6637] が [x1, x2, x3 ] に当てはまります。
また、y = x * 2 にこの数値を当てはめると [-1.9236, -6.644, -1.3274] になり、これが [y1, y2, y3] になります。
数値を当てはめるまでもありませんでしたが、x と y の変換式を書くと以下になります。

y_1 = 2x_1\\
y_2 = 2x_2\\
y_3 = 2x_3\\

この式を、それぞれ x1, x2, x3 で偏微分すると以下のようになります。

\frac{\partial y_{1}}{\partial x_{1}} = 2 , 
\frac{\partial y_{1}}{\partial x_{2}} = 0 , 
\frac{\partial y_{1}}{\partial x_{3}} = 0\\
\frac{\partial y_{2}}{\partial x_{1}} = 0 , 
\frac{\partial y_{2}}{\partial x_{2}} = 2 , 
\frac{\partial y_{2}}{\partial x_{3}} = 0\\
\frac{\partial y_{3}}{\partial x_{1}} = 0 , 
\frac{\partial y_{3}}{\partial x_{2}} = 0 , 
\frac{\partial y_{3}}{\partial x_{3}} = 2\\

従ってヤコビ行列は以下になります。
while 前の1回目の変換(y = x * 2)を表すため、J1 としました。

{\begin{split}J_1=\left(\begin{array}{ccc}
 2 & 0 & 0\\
 0 & 2 & 0\\
 0 & 0 & 2\\
 \end{array}\right)\end{split}
}

2回目の y を考えます。
while の後の y = y * 2 が2回目の y です。
式が1回目と同じですので、2回目の y のヤコビ行列も先ほどと同じで以下になります。
2回目の y のヤコビ行列を表すため、J2 とします。

{\begin{split}J_2=\left(\begin{array}{ccc}
 2 & 0 & 0\\
 0 & 2 & 0\\
 0 & 0 & 2\\
 \end{array}\right)\end{split}
}

これを繰り返します。
初期値である x.data.norm() が「3.5215」で、y.data.norm() < 1000 でループさせるため、ループは8回実行され、y は9回定義されます。
全体としては、以下のようになります。

x1の値 x2の値 x3の値
- x1 x2 x3
1回目のyで変換 2 * x1 2 * x2 2 * x3
2回目のyで変換 4 * x1 4 * x2 4 * x3
3回目のyで変換 8 * x1 8 * x2 8 * x3
4回目のyで変換 16 * x1 16 * x2 16 * x3
5回目のyで変換 32 * x1 32 * x2 32 * x3
6回目のyで変換 64 * x1 64 * x2 64 * x3
7回目のyで変換 128 * x1 128 * x2 128 * x3
8回目のyで変換 256 * x1 256 * x2 256 * x3
9回目のyで変換 512 * x1 512 * x2 512 * x3

最終的に y はこの9個の変換の合成関数になります。
こちらの数学サイト にも記載がありますが、合成関数のヤコビ行列は行列の式で表せるので
y 全体のヤコビ行列 J は

\begin{eqnarray}
J &=& J_9 \times J_8 \times J_7 \times J_6 \times J_5 \times J_4 \times J_3 \times J_2 \times J_1\\\\
&=&
\left(
    \begin{array}{ccc}
      2 & 0 & 0 \\
      0 & 2 & 0 \\
      0 & 0 & 2
    \end{array}
\right) 
\left(
    \begin{array}{ccc}
      2 & 0 & 0 \\
      0 & 2 & 0 \\
      0 & 0 & 2
    \end{array}
\right) 
\cdots
\left(
    \begin{array}{ccc}
      2 & 0 & 0 \\
      0 & 2 & 0 \\
      0 & 0 & 2
    \end{array}
\right) 
\left(
    \begin{array}{ccc}
      2 & 0 & 0 \\
      0 & 2 & 0 \\
      0 & 0 & 2
    \end{array}
\right) \\\\
&=&
\left(
    \begin{array}{ccc}
      512 & 0 & 0 \\
      0 & 512 & 0 \\
      0 & 0 & 512
    \end{array}
\right) 

\end{eqnarray}

になります。

これに当てはめて以下の計算を考えてみましょう。

gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients)

print(x.grad)

gradients はヤコビ行列に掛けるベクトルです。
上記で計算したヤコビ行列に当てはめると以下のようになります。

{\begin{split}x.grad=\left(\begin{array}{ccc}
 512 & 0 & 0\\
 0 & 512 & 0\\
 0 & 0 & 512\\
 \end{array}\right)\left(\begin{array}{c}
 0.1\\
 1.0\\
 0.0001
 \end{array}\right)=\left(\begin{array}{c}
 51.2\\
 512\\
 0.0512
 \end{array}\right)\end{split}
}

まとめると、Autograd(自動微分)は以下のようなイメージでしょうか。

  • 関数(式)を定義する度にヤコビ行列を保持しておく。
  • backward メソッドで保持したヤコビ行列から「微分」を計算する。

ここで話は変わって、以下のように、torch.no_grad() ブロックで記述することにより、関数の変化を追跡しなくなります。
(x ** 2)は追跡されません。

print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
    print((x ** 2).requires_grad)
True
True
False

また、detach() は、tensorの変数はコピーされますが、勾配は引き継がれません。

print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())
True
False
tensor(True)

5.最後に

以上が、PyTorch の2つ目のチュートリアル「Autograd: Automatic Differentiation」の内容です。
1つ目のチュートリアルとは次元が違う内容でした。
後半は、想像の部分もありますので、間違っているかもしれません。
ご指摘いただければ幸いです。

次回は3つ目のチュートリアル「NEURAL NETWORKS」を進めてみたいと思います。

履歴

2020/02/28 初版公開
2020/04/22 次回のリンク追加

26
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
22