TensorFlowのOptimizerの違いによる学習推移の比較

最適化問題をTensorFlowのOptimizerを使って求め、収束の仕方のOptimizerによる違いを見ます。

この記事で解く最適化問題は2変数で $ (x^2 + y^2 - 1)^2 + x $ が最小値となる $ x $、$ y $ です。

この関数はxyの座標でいうと、原点を中心に半径1の円から少し左にずれた円周状に谷になっていて、その中でも左のほうが低くなっております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/54e56ef2-6ea8-8652-f6e9-9e37cffd5d3e.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/fe67294a-60a2-b899-8683-b4b9bfce2f2c.png)

ディープラーニングとはパラメータの数がぜんぜん違いますので、挙動もまたぜんぜん違うのだとは思いますが、Optimizerによる動き方の違いをイメージできるかもしれません。

Pythonのコードはこんな感じです。Google Colaboratoryで実行しました。

```python
import time
import numpy as np
import matplotlib.pyplot as plt
import math
import tensorflow as tf

opt1 = tf.optimizers.SGD(learning_rate=0.3) # 青
opt2 = tf.optimizers.SGD(learning_rate=0.2) # 橙
opt3 = tf.optimizers.SGD(learning_rate=0.1) # 緑
opt4 = tf.optimizers.SGD(learning_rate=0.05) # 赤 

opts = [opt1, opt2, opt3, opt4]

# 目的となる損失関数
def loss(i):
  x2 = x[i] * x[i]
  y2 = y[i] * y[i]
  r2 = (x2 + y2 - 1.0)
  return r2 * r2 + x[i]

# 最適化する変数
x = []
y = []

# グラフにするための配列
xHistory = []
yHistory = []
lossHistory = []
calculationTime = []
convergenceCounter = []

maxLoopCount1 = 100
maxLoopCount2 = 1000

for i in range(len(opts)):
  # 適当な初期値
  x.append(tf.Variable(1.5))
  y.append(tf.Variable(0.1))

  # グラフにするための配列
  xHistory.append([])
  yHistory.append([])
  lossHistory.append([])
  convergenceCounter.append(0)

  start = time.time()

  for loopCount in range(maxLoopCount2):
    l = float(loss(i))
    # グラフにするために記録
    if loopCount < maxLoopCount1:
      xHistory[i].append(float(x[i]))
      yHistory[i].append(float(y[i]))
      lossHistory[i].append(l)
    if (l >= -1.056172): # 収束の閾値
      # 収束までの回数を測定するため
      convergenceCounter[i] = loopCount + 1

    # 最適化
    opts[i].minimize(lambda: loss(i), var_list = [x[i], y[i]])

  calculationTime.append(time.time() - start)

colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']

# グラフ化1つ目: 損失関数の値の推移
plt.ylim(-1.1, +3.5)
for i in range(len(opts)):
  plt.plot(range(maxLoopCount1), lossHistory[i], color=colors[i % len(colors)])
plt.show()

# グラフ化2つ目以降: xy平面上での推移
for i in range(len(opts)):
  print("time: " + str(calculationTime[i]))
  print("convergenceCounter: " + str(convergenceCounter[i]))
  print("loss: " + str(lossHistory[i][-1]))
  plt.xlim(-2, +2)
  plt.ylim(-1.5, +1.5)
  plt.plot(xHistory[i], yHistory[i], color=colors[i % len(colors)])
  th = np.linspace(-math.pi, math.pi, 100)
  plt.plot(np.cos(th), np.sin(th), color="#888888")
  plt.show()
```


# シンプルな勾配降下法

```python
opt1 = tf.optimizers.SGD(learning_rate=0.3) # 青: ぜんぜん収束しない
opt2 = tf.optimizers.SGD(learning_rate=0.2) # 橙: おしいけど細かい振動が続く
opt3 = tf.optimizers.SGD(learning_rate=0.1) # 緑: 108回で収束
opt4 = tf.optimizers.SGD(learning_rate=0.05) # 赤: 収束しそうだけど1000回でも到達せず
# ちなみに learning_rate=0.4 では発散する
```

- 学習率を適切に設定しないと収束しない

損失関数の値の推移のグラフ（100回まで）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/6df460c7-e9e4-6cff-d34e-f1527162c6e6.png)

xy平面上での推移のグラフ（100回まで）（灰色の円は半径1の原点中心）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/2041b4a3-a2e6-6fea-3246-3f72529350eb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/ec1ce815-ac88-7043-f39e-91cb37e3b7ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/ca358243-fb11-42c6-2a10-4888f172c322.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/9b973b93-8b3c-7cb9-809a-ca60a47d50ea.png)


# モーメンタム

```python
opt1 = tf.optimizers.SGD(learning_rate=0.3, momentum=0.5) # 青: 発散
opt2 = tf.optimizers.SGD(learning_rate=0.2, momentum=0.5) # 橙: 20回で収束(下にxy図あり)
opt3 = tf.optimizers.SGD(learning_rate=0.1, momentum=0.5) # 緑: 27回で収束
opt4 = tf.optimizers.SGD(learning_rate=0.05, momentum=0.5) # 赤: 103回で収束(下にxy図あり)
```

- モーメンタムがあると窪地は通り過ぎやすい
- 窪地を往復すると、モーメンタムがない場合にくらべ、早く発散するか早く収束する
- ゆるやかな坂道が続いてるときの収束は早い

損失関数の値の推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/9bf3b665-c0b1-5db1-acd1-f01b0e952e7c.png)

xy平面上での推移のグラフ（図が多いと貼るのが疲れるので一部のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/41036964-4a86-5e1e-7f46-1857b5a6edae.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/10431159-090e-1be1-3529-ff7bfc5efbfe.png)


# Nesterov加速法

使い方が違うのか `tf.optimizers.SGD(learning_rate=0.1, nesterov=True)` と書いても普通の勾配降下法となにも変わらなかった。


# Adagrad

```python
opt1 = tf.optimizers.Adagrad(learning_rate=3.0) # 青: 203回で収束
opt2 = tf.optimizers.Adagrad(learning_rate=2.0) # 橙: 104回で収束
opt3 = tf.optimizers.Adagrad(learning_rate=1.0) # 緑: 65回で収束
opt4 = tf.optimizers.Adagrad(learning_rate=0.5) # 赤: 73回で収束
```

たまに振動する。収束の閾値付近で振動すると、収束までに時間がかかる結果になる。

学習率 learning_rate は大きめの数字のほうがいいみたい。

損失関数の値の推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/e0eee7ec-187a-cd4f-b3c4-1e76a5dd4144.png)

xy平面上での推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/cfbd2280-59f4-5c99-efc3-61d83abdad64.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/f577bd73-3822-c152-f573-c750874eda48.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/36bb6844-9d7a-2269-1616-b09cfb7954b0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/d7c60d05-c0fc-9f7f-9f99-745d3ea15252.png)


# RMSprop

```python
opt1 = tf.optimizers.RMSprop(learning_rate=0.3) # 青
opt2 = tf.optimizers.RMSprop(learning_rate=0.2) # 橙
opt3 = tf.optimizers.RMSprop(learning_rate=0.1) # 緑
opt4 = tf.optimizers.RMSprop(learning_rate=0.05) # 赤
```

学習率が大きいほど近づくのは早いけど、定期的に振動し、最適値付近でもピクピク動くので収束判定が難しい。

損失関数の値の推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/0524dfa8-7f8d-beef-9b9b-a5711e40f15e.png)

xy平面上での推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/b8e7e9b3-b1b8-fe81-b7ed-bcd7c74e458d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/f10a2273-c2ef-b522-88b6-ef692045a87f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/82bff1bb-5d49-691a-b073-08b58721a748.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/7eee1876-6d8c-42b0-a989-002195bad455.png)



# Adadelta

```python
opt1 = tf.optimizers.Adadelta(learning_rate=300) # 青
opt2 = tf.optimizers.Adadelta(learning_rate=200) # 橙 (下にxy図あり)
opt3 = tf.optimizers.Adadelta(learning_rate=100) # 緑
opt4 = tf.optimizers.Adadelta(learning_rate=50) # 赤 (下にxy図あり)
```

学習率 learning_rate のオーダーがずいぶんと違う。そして、どれも結局収束しない。

損失関数の値の推移のグラフ（他のOptimizerと違ってこれだけ300まで描画）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/4b00bf4b-f71e-174d-2df7-b85970a079ff.png)

xy平面上での推移のグラフ（図が多いと貼るのが疲れるので一部のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/2850e18c-2b65-bc0b-7a3b-afbbabaa16de.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/06328ee6-2566-65bd-dcc9-6aaea073ba3a.png)


# Adam

```python
opt1 = tf.optimizers.Adam(learning_rate=0.3) # 青: 142回で収束
opt2 = tf.optimizers.Adam(learning_rate=0.2) # 橙: 148回で収束 (下にxy図あり)
opt3 = tf.optimizers.Adam(learning_rate=0.1) # 緑: 169回で収束
opt4 = tf.optimizers.Adam(learning_rate=0.05) # 赤: 231回で収束 (下にxy図あり)
```

ボールが転がり落ちるような動きをする。

損失関数の値の推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/bff296d7-9f04-17f2-9765-720cd03fe6e9.png)

xy平面上での推移のグラフ（図が多いと貼るのが疲れるので一部のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/48303b98-78f1-32fd-b6aa-aaeb3df6b2cf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/1fa60727-1815-60d7-524c-710287223f1f.png)


# 自作アルゴリズム

以下の私の記事で紹介した自作のアルゴリズムです。

- [勾配降下法の自作アルゴリズム](https://suzuki-navi.hatenablog.com/entry/2020/05/21/215928)

```python
opt1 = CustomOptimizer(learning_rate=0.3) # 青: 7回で収束
opt2 = CustomOptimizer(learning_rate=0.2) # 橙: 9回で収束 (下にxy図あり)
opt3 = CustomOptimizer(learning_rate=0.1) # 緑: 52回で収束
opt4 = CustomOptimizer(learning_rate=0.05) # 赤: 53回で収束 (下にxy図あり)
```

2020/09/22追記：CustomOptimizerのソースコード → [TensorFlowでOptimizerを自作する](https://suzuki-navi.hatenablog.com/entry/2020/09/22/073756)

学習率 learning_rate が大きいほど早く収束しますが、これ以上大きくしてもこれ以上は早くはならなかったです。それでも、学習率を大きくしすぎても発散はせずにすぐに収束します。

損失関数の値の推移のグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/8e8f5b67-62eb-250f-18f6-0557eee7d0c6.png)

xy平面上での推移のグラフ（図が多いと貼るのが疲れるので一部のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/3ed36fc9-5309-f3a3-a085-a136ec5f812b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/f2e1234b-275d-b9da-2cd2-57fecbd8add9.png)


# リンク

TensorFlowのOptimizerのAPIレファレンス
[Module: tf.keras.optimizers  |  TensorFlow Core v2.3.0](https://www.tensorflow.org/api_docs/python/tf/keras/optimizers)

関連する私の記事

- [勾配降下法のアルゴリズム一覧のメモ](https://suzuki-navi.hatenablog.com/entry/2020/05/20/134615)
- [勾配降下法の自作アルゴリズム](https://suzuki-navi.hatenablog.com/entry/2020/05/21/215928)
- [TensorFlowの自動微分を使って勾配降下法を試してみる](https://suzuki-navi.hatenablog.com/entry/2020/08/16/235911)