自動微分と勾配テープ(TensorFlow)入門

# はじめに

自動微分と勾配テープでつまずいているので、調査しまとめることにしました。

主に以下のサイトを参考にしました。

https://www.tensorflow.org/tutorials/customization/autodiff?hl=ja

https://www.tensorflow.org/api_docs/python/tf/GradientTape

https://deepage.net/tensorflow/2018/10/04/tensorflow-basic-operations.html

公式より、「自動微分と勾配テープ」とは

>TensorFlow には、自動微分、すなわち、入力変数に対する計算結果の勾配を計算するためのtf.GradientTape API があります。TensorFlow は、tf.GradientTape のコンテキスト内で行われる演算すべてを「テープ」に「記録」します。その後 TensorFlow は、そのテープと、そこに記録された演算ひとつひとつに関連する勾配を使い、トップダウン型自動微分（リバースモード）を使用して、「記録」された計算の勾配を計算します。

レポジトリはこちらです。

https://github.com/sugiyama404/traning_tensowflow_tape/blob/main/traning_tensowflow_tape.ipynb

# import

```python
import tensorflow as tf
```

# 基礎
## １回微分

```math
y = x^2
```
を微分して見ます。

```math
\frac{dy}{dx} = 2x \\

\frac{dy}{dx} |_{x=8} = 16
```

![x2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/58664ec4-5a50-bfe5-90bf-345bbdeecfba.png)

操作は、このコンテキストマネージャー内で実行され、その入力の少なくとも1つが「監視」されている場合に記録されます。

```python
x = tf.constant(8.0) # 8.0, shape=()
with tf.GradientTape() as t:
    t.watch(x)
    y = x * x
    # 元の入力テンソル x に対する z の微分
    dy_dx = t.gradient(y, x) # 16.0, shape=()
```

## ２回微分

```math
y = x^2
```
を微分して見ます。

```math
\frac{dy}{dx} = 2x \\
\frac{dy^2}{d^2x} = 2 \\
```

```python
x = tf.constant(3.0) # 3.0, shape=()
print(x)
with tf.GradientTape() as t:
    t.watch(x)
    with tf.GradientTape() as tt:
        tt.watch(x)
        y = x * x
        dy_dx = tt.gradient(y, x) # 6.0, shape=()
        dy2_dx2 = t.gradient(dy_dx, x) # 2.0, shape=()
```

## 定数で微分できない場合

公式サイトによれば、微分できない場合は, '1' のテンソルを返すそうです。

```python
x = tf.constant(8.0) # 8.0, shape=()
with tf.GradientTape() as t:
    t.watch(x)
    y = x
    dy_dx = t.gradient(y, x) # 1.0, shape=()
```

## 制御フローの記録

>勾配テープは演算を実行の都度記録するため、（たとえば if や while を使った）Python の制御フローも自然に扱われます。

```python
def f(x, y):
  output = 1.0
  for i in range(y):
    if i > 1 and i < 5:
      output = tf.multiply(output, x)
  return output

def grad(x, y):
  with tf.GradientTape() as t:
    t.watch(x)
    out = f(x, y)
  return t.gradient(out, x) 

x = tf.convert_to_tensor(2.0)
grad(x, 6).numpy() # 12.0
```

# 勾配テープ内で使用するメソッド
テンソルのまま処理できます。
## よく使うメソッド
reduce_sum = 配列内のすべての要素を足し算する関数
multiply = テンソルの要素の掛け算する関数

```math
z = y^2 \\
\frac{dz}{dy} = 2x \\
\frac{dz}{dy} |_{x=4} = 8
```

より

```math
\frac{dz}{dx} 
= 8 \cdot \begin{bmatrix}
1 & 1 \\
1 & 1 
\end{bmatrix}
=\begin{bmatrix}
8 & 8 \\
8 & 8 
\end{bmatrix}
```

```python
x = tf.ones((2, 2)) # x = [[1. 1.] [1. 1.]]
with tf.GradientTape() as t:
    t.watch(x)
    y = tf.reduce_sum(x) # 4.0, shape=()
    z = tf.multiply(y, y) # 16.0, shape=()
dz_dx = t.gradient(z, x) # [[8. 8.] [8. 8.]], shape=(2, 2)
```

## ループ

add = 関数の足し算
less = 未満の条件式

```python
i = tf.constant(0)
c = lambda i: tf.less(i, 10)
# b = lambda i: (tf.add(i, 1), )
def b(i):
    print(i)
    return (tf.add(i, 1), )
r = tf.while_loop(c, b, [i])
```

```console
tf.Tensor(0, shape=(), dtype=int32)
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor(3, shape=(), dtype=int32)
tf.Tensor(4, shape=(), dtype=int32)
tf.Tensor(5, shape=(), dtype=int32)
tf.Tensor(6, shape=(), dtype=int32)
tf.Tensor(7, shape=(), dtype=int32)
tf.Tensor(8, shape=(), dtype=int32)
tf.Tensor(9, shape=(), dtype=int32)

Out[27]: (<tf.Tensor: shape=(), dtype=int32, numpy=10>,)
```

## 指定したインデックスを出力

gather_nd = テンソルと、そのテンソル内の位置を表すインデックスを提供します。指定したインデックスに対応するテンソルの要素を返します。第一引数は、探すテンソル、第二引数は、そのインデックスを指定します。

### 一次元配列の場合

```python
x = tf.constant([0.1, 0.1, 1.5, 4.5, 3.6, 1.2])
y = tf.constant([3.1, 2.3, 1.4, 2.3, 4.4, 3.1])

uniqueIndices = tf.constant([0, 1, 2, 3, 4, 5])

x = tf.gather_nd(x, uniqueIndices[:,None])
y = tf.gather_nd(y, uniqueIndices[:,None])

tf.multiply(x, y) # shape=(6,), array([ 0.31    ,  0.23    ,  2.1     , 10.349999, 15.84    ,  3.72    ])
```

array([ 0.31    ,  0.23    ,  2.1     , 10.349999, 15.84    ,  3.72    ])
の間が空いているのは、その分のメモリを開けているからだと思われます。

### 二次元配列の場合

```python
x = [[1,2,3],[4,5,6]]
y = tf.gather_nd(x, [[1,1],[1,2]]) # shape=(2,) numpy=array([5, 6], dtype=int32)
```

## 条件分岐

less = 大小比較のメソッド
cond = いわゆるif。条件分岐します。

```python
x = tf.constant(2)
y = tf.constant(5)
def f1(): return tf.multiply(x, 17)
def f2(): return tf.add(y, 23)
r = tf.cond(tf.less(x, y), f1, f2)
```
rはf1（）の値が設定されます。f2の操作（例：tf.add）は実行されません。
(x= 2, r = 34)

## 関数

公式より

>tf.functionを用いてある関数にアノテーションを付けたとしても、一般の関数と変わらずに呼び出せます。一方、実行時にはその関数はグラフへとコンパイルされます。これにより、より高速な実行や、 GPU や TPU での実行、SavedModel へのエクスポートといった利点が得られます。

```python
@tf.function
def mini_func(x):
    return x * x

x = tf.constant(5.0)
with tf.GradientTape() as t:
    t.watch(x)
    result = mini_func(x)
    dy_dx = t.gradient(result, x)
print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32)
```

### constantとVariableの違いについて

TensorFlowにおけるconstantとVariableの違いは、constantを宣言した場合、その値は将来的に変更できない（また、初期化は操作ではなく値で行う必要がある）ことです。
また、デフォルトでは、GradientTapeは、コンテキスト内でアクセスされるすべてのtrainable変数を自動的に監視します。(t.watch(v)しなくてよい。)

#### constantの場合
```python
@tf.function
def mini_func(x):
    return x * x

x = tf.constant(5.0)
with tf.GradientTape() as t:
    t.watch(x)
    result = mini_func(x)
    dy_dx = t.gradient(result, x)
print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32)
```

#### Variableの場合
```python
@tf.function
def mini_func(x):
  return x * x

x = tf.Variable(5.0)
with tf.GradientTape() as t:
  result = mini_func(x)
dy_dx = t.gradient(result, x)
print(dy_dx) # tf.Tensor(10.0, shape=(), dtype=float32)
```

## クリッピング

clip_value_min ≤ value ≤ clip_value_max の場合は value
または - value < clip_value_min の場合は clip_value_min
または - clip_value_max < value の場合は clip_value_max

```python
t = tf.constant([[-10., -1., 0.], [0., 2., 10.]])
t2 = tf.clip_by_value(t, clip_value_min=-1, clip_value_max=1)
t2.numpy()
```

```console
array([[-1., -1.,  0.],
       [ 0.,  1.,  1.]], dtype=float32)
```

-1 ≤ value ≤ 1 の場合は value
または - value < -1 の場合は -1
または - 1 < value の場合は 1
を満たす形になっていることがわかります。

## tensorの操作

### 全て0のtensorを作る

```python
tf.zeros([2, 3])
```

```console
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[0., 0., 0.],
        [0., 0., 0.]], dtype=float32)>
```

### 全て1のtensorを作る

```python
tf.ones((2, 3))
```

```console
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
 array([[1., 1., 1.],
        [1., 1., 1.]], dtype=float32)>
```

### 全て同じ値のtensorを作る

```python
tf.fill([2, 3], 9)
```

```console
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
 array([[9, 9, 9],
        [9, 9, 9]], dtype=int32)>
```

## キャスト

tf.float32に揃えていないことで、以下のエラー文が出ます。なので、揃っていない時は、揃えましょう。

>InvalidArgumentError: cannot compute Mul as input #1(zero-based) was expected to be a float tensor but is a double tensor [Op:Mul]

```python
labels = tf.constant([0, 3, 1])
print(labels)
tf.cast(labels, tf.float32)
```

```console
tf.Tensor([0 3 1], shape=(3,), dtype=int32)
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0., 3., 1.], dtype=float32)>
```

## numpyに変換

tensorに慣れてないうちは、かなり使います。

```python
labels = tf.constant([0, 3, 1])
print(labels)
labels.numpy()
```

```console
tf.Tensor([0 3 1], shape=(3,), dtype=int32)
array([0, 3, 1], dtype=int32)
```