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

#はじめに
[前回](https://qiita.com/sudominoru/items/544aec4dc867187a93fa)に引き続き、PyTorch [公式チュートリアル](https://pytorch.org/tutorials/) の第2弾です。
今回は [Autograd: Automatic Differentiation](https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html) を進めてみたいと思います。

#目次
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 を指定し、勾配が記録されるようにします。

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

```Python
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
```

計算グラフ（式） y を作成します。

```Python
y = x + 2
print(y)
```

```Python
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)
```

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

```Python
print(y.grad_fn)
```

```Python
<AddBackward0 object at 0x7f8cc977e5c0>
```

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

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

print(z, out)
```

```Python
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
```

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

```Python
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)
```

```Python
False
True
<SumBackward0 object at 0x7fcb2ba0a3c8>
```

#3.勾配

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

```Python
out.backward()
```

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

```Python
print(x.grad)
```

```Python
tensor([[4.5000, 4.5000],
        [4.5000, 4.5000]])
```

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

```math
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 で偏微分すると

```math
\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!

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

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

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

print(y)
```

```Python
tensor([ -492.4446, -1700.8485,  -339.7951], grad_fn=<MulBackward0>)
```

x は標準化（平均0、標準偏差1）されたランダム値です。
y.data.norm() は[ウィキペディア](https://ja.wikipedia.org/wiki/%E3%83%8E%E3%83%AB%E3%83%A0)にも説明がありますが、ベクトル空間における距離のことで、以下のユークリッドノルムのことです。

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

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

```math
\begin{eqnarray}
ユークリッドノルム &=& \sqrt{|-0.9618|^2+|-3.3220|^2+|-0.6637|^2}\\
&=& 3.5215
\end{eqnarray}
```

```Python
print(x)
print(x.data.norm())
```

```Python
tensor([-0.9618, -3.3220, -0.6637], requires_grad=True)
tensor(3.5215)
```

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

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

```Python
y.backward()
```

```Python
RuntimeError: grad can be implicitly created only for scalar outputs
```

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

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

print(x.grad)
```

```Python
tensor([5.1200e+01, 5.1200e+02, 5.1200e-02])
```

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


```math
\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 はヤコビ行列と与えられたベクトルの積を計算するためのエンジンであるとの記載もあります。

```math
\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}
```

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

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

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

```math
x_1 ,  x_2 , x_3
```


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

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

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

```math
{\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 の変換式を書くと以下になります。

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

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

```math
\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 としました。

```math
{\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 とします。

```math
{\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個の変換の合成関数になります。
[こちらの数学サイト](https://mathtrain.jp/rensaritsu) にも記載がありますが、合成関数のヤコビ行列は行列の式で表せるので
y 全体のヤコビ行列 J は

```math
\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}
```

になります。

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

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

print(x.grad)
```

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

```math
{\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)は追跡されません。

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

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

```Python
True
True
False
```

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

```Python
print(x.requires_grad)
y = x.detach()
print(y.requires_grad)
print(x.eq(y).all())
```

```Python
True
False
tensor(True)
```

#5.最後に

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

[次回](https://qiita.com/sudominoru/items/b6159c635315fa33933c)は3つ目のチュートリアル「NEURAL NETWORKS」を進めてみたいと思います。

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