TensorFlowにおけるクリッピングと正規化

# 概要

TensorFlowで扱うtensorが保持している各値のクリッピングについての記事です。
具体的には`tf.clip_by...`系メソッドなどの説明と実装例になります。

# そもそも何でこんな事するの？

用途は色々あるかと思いますが、TensorFlowの主たる使用目的である機械学習関連の計算(特に勾配計算)では、扱う変数のスケール感に違いが出てうまく計算できないということが多々あります。
そんな時に用いられるのがクリッピングや正規化になります。

# TensorFlowの用語
TensorFlowでは従来の変数や定数、更に各種演算を表す処理をOpノードと呼んでいます([参考記事](https://qiita.com/yanosen_jp/items/70e6d6afc36e1c0a3ef3))
セッション内で実行（run)する事でOpノードは演算結果の値の集合(テンソル)を保持し次のOpノードに伝えていく仕組みになっています。
本記事ではこれに合わせて何らかの処理が終わった後の数値集合のことを**ノード**と表記します。

# clip_by系

何か決め打ちした基準に対して条件を満たす場合にノードの値を抑えるイメージ

## tf.clip_by_value

```python

tf.clip_by_value(
    t,
    clip_value_min,
    clip_value_max,
    name=None
)
```

ノードが保持する各値に対して最大値`clip_value_max`より大きい値を`clip_value_max`に、最小値`clip_value_min`より小さい値を`clip_value_min`に直します。

```python:example1.py
p1 = tf.placeholder(tf.int32, 6, name='p1')
p2 = tf.placeholder(tf.float32, 6, name='p2')

clip_value1 = tf.clip_by_value(p1, clip_value_max=2, clip_value_min=-2, name='clip_value1')
clip_value2 = tf.clip_by_value(p2, clip_value_max=2., clip_value_min=-2., name='clip_value2')

num1 = np.linspace(-4, 6, 6)

with tf.Session() as sess:
    print(p1.eval(feed_dict={p1: num1}, session=sess))
    print(p2.eval(feed_dict={p2: num1}, session=sess))

    print(clip_value1.eval(feed_dict={p1: num1}, session=sess))
    print(clip_value2.eval(feed_dict={p2: num1}, session=sess))
```

```console:console
[-4 -2  0  2  4  6]
[-4. -2.  0.  2.  4.  6.]

[-2 -2  0  2  2  2]
[-2. -2.  0.  2.  2.  2.]
```

**ノードと`clip_value`の型が一致していないとエラーを吐かれます。**

```python:example1.py
    print(clip_error1.eval(feed_dict={p1: num1}, session=sess))
```

```console:console
TypeError: Expected int32 passed to parameter 'y' of op 'Minimum', got 2.0 of type 'float' instead.
```

## tf.clip_by_norm

```python
tf.clip_by_norm(
    t,
    clip_norm,
    axes=None,
    name=None
)
```

ノードのL2ノルムが`clip_norm`よりも大きい場合このノルムに直すように各値を変更します。`clip_norm`よりも小さい場合は変更されません。


```python:example2.py
p3 = tf.placeholder(tf.float32, [2, 3], name='p3')

clip_norm1 = tf.clip_by_norm(p3, clip_norm=4, name='clip_norm1')
clip_norm2 = tf.clip_by_norm(p3, clip_norm=5, name='clip_norm2')

num2 = np.linspace(-2, 3, 6).reshape((2, 3))

with tf.Session() as sess:
    print(p3.eval(feed_dict={p3: num2}, session=sess))
    print(clip_norm1.eval(feed_dict={p3: num2}, session=sess))
    print(clip_norm2.eval(feed_dict={p3: num2}, session=sess))
```

```console:console
[[-2. -1.  0.]
 [ 1.  2.  3.]]   # 全体のL2ノルムは4.358 ...

[[-1.8353258 -0.9176629  0.       ]
 [ 0.9176629  1.8353258  2.7529888]]

[[-2. -1.  0.]
 [ 1.  2.  3.]]
```

`tf.clip_by_norm`ではaxesを指定できます。
axesで指定した軸ごとのL2ノルムで値を正規化します。

```python:example3.py
clip_norm3 = tf.clip_by_norm(p3, clip_norm=3, axes=1, name='clip_norm3')

with tf.Session() as sess:
    print(p3.eval(feed_dict={p3: num2}, session=sess))
    print(clip_norm3.eval(feed_dict={p3: num2}, session=sess))
```

```console:console
[[-2. -1.  0.]    # 0列目のL2ノルムは2.236 ...
 [ 1.  2.  3.]]   # 1列目のL2ノルムは3.741 ...

[[-2.        -1.         0.       ]
 [ 0.8017837  1.6035674  2.4053512]]
```

なお、`tf.clip_by_norm`は噛ませるノードが小数点を扱えないとTypeErrorとなります。
float◯◯やcomplex◯◯型を使ってください。

## tf.clip_by_global_norm

```python

tf.clip_by_global_norm(
    t_list,
    clip_norm,
    use_norm=None,
    name=None
)
```

tf.clip_by_normと違ってノードではなく**ノードのリスト**を渡します。
ノード自体を渡すとTypeErrorになります。

リストに格納されているノード全体でのL2ノルムを`global_norm`とし、この値が`clip_norm`よりも大きい場合L2ノルムが`clip_norm`になるようにリスト内の全ての値を変更します。`clip_norm`よりも小さい場合は変更されません。

また、返値はクリッピングした後の**ノードが格納されたリスト**`list_clipped`と計算された`global_norm`の2つです。

```python:example4.py
c1 = tf.constant([[0, 1, 2], [3, 4, 5]], dtype=tf.float32, name='c1')
c2 = tf.constant([[-2, -4], [2, 4]], dtype=tf.float32, name='c2')
C = [c1, c2]

clip_global_norm, global_norm = tf.clip_by_global_norm(C, clip_norm=9, name='clip_global_norm')

with tf.Session() as sess:
    for c in C:
        print(c.eval(session=sess))
    print(global_norm.eval(session=sess))
    for cgn in clip_global_norm1:
        print(cgn.eval(session=sess))
```

```console:console
[[0. 1. 2.]
 [3. 4. 5.]]
[[-2. -4.]
 [ 2.  4.]]

9.746795
[[0.        0.9233805 1.846761 ]
 [2.7701416 3.693522  4.6169024]]
[[-1.846761 -3.693522]
 [ 1.846761  3.693522]]
```

---

`tf.clip_by_norm`や`tf.clip_by_global_norm`メソッド自体は単純ではありますが、たとえばRNNにおける勾配爆発の対策「勾配クリッピング」に対応するために用いることができます。

以下が参考になります。
> - [Pascanu et al., (2012), On the difficulty of training Recurrent Neural Networks (pdf)](https://arxiv.org/pdf/1211.5063.pdf)
> - [わかるLSTM ～ 最近の動向と共に#rnnと勾配のクリッピングgradient-cliping](https://qiita.com/t_Signull/items/21b82be280b46f467d1b#rnn%E3%81%A8%E5%8B%BE%E9%85%8D%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0gradient-cliping)

モデルを構築した後、いざ学習させようと思ったら誤差の伝播計算などでinfにぶっ飛ぶ時などはこの方法を取ると解決するかもしれません。

# おまけ

クリッピングは実際にノードの保有値を変更していましたが、ノルムの計算のみする事もできます。

## tf.norm

```python
tf.norm(
    tensor,
    ord='euclidean',
    axis=None,
    keepdims=None,
    name=None
)
```

パラメタ`ord`でLpノルムのpの値を決定します。
L∞ノルムの場合はnp.infを指定します。

```example4.py
p4 = tf.placeholder(tf.float32, [3, 4], name='p4')

normalize1 = tf.norm(p4, name='normalize1')
normalize2 = tf.norm(p4, ord=1.5, axis=0, name='normalize2')
normalize3 = tf.norm(p4, ord=np.inf, axis=1, name='normalize3')

num3 = np.linspace(-10, 8, 12).reshape((3, 4))

with tf.Session() as sess:
    print(p4.eval(feed_dict={p4: num3}, session=sess))
    print(normalize1.eval(feed_dict={p4: num3}, session=sess))
    print(normalize2.eval(feed_dict={p4: num3}, session=sess))
    print(normalize3.eval(feed_dict={p4: num3}, session=sess))
```

```console:console
[[-10.          -8.363636    -6.7272725   -5.090909  ]
 [ -3.4545455   -1.8181819   -0.18181819   1.4545455 ]
 [  3.090909     4.7272725    6.3636365    8.        ]]

19.87232

[12.364525  11.0871725 10.408293  10.876119 ]

[10.         3.4545455  8.       ]
```

# 参考
[TensorFlow > API > TensorFlow Core r2.0 > Python > tf.cilp_by_value](https://www.tensorflow.org/api_docs/python/tf/clip_by_value?version=stable)
[TensorFlow > API > TensorFlow Core r2.0 > Python > tf.clip_by_norm](https://www.tensorflow.org/api_docs/python/tf/clip_by_norm?version=stable)
[TensorFlow > API > TensorFlow Core r2.0 > Python > tf.clip_by_global_norm](https://www.tensorflow.org/api_docs/python/tf/clip_by_global_norm)
[TensorFlow > API > TensorFlow Core r2.0 > Python > tf.norm](https://www.tensorflow.org/api_docs/python/tf/norm?version=stable)

---
明日は、@yoshishinさんによる「Rubyを使って予約システムをハックする」です。
引き続き、[GMOアドマーケティング Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gmo-am)をお楽しみください！