TensorFlowのOptimizerごとのスケール許容範囲の比較

入力値や損失関数のスケールを変えると、学習推移はどのように変わるかを調べました。

スケールを変えても学習推移が変わらないOptimizerと、大きく変わってしまい学習率のパラメータを調整しないといけないOptimizerがあります。

# さきに結論

○: 学習率のパラメータはスケールに依存しない、またはわずか
✗: スケールが変わると学習速度が大きく変わり、学習率を調整することが必須

| Optimizer | 損失関数のスケール | 入力値のスケール |
| ---- | ---- | ---- |
| シンプルな勾配降下法 | ✗ | ✗ |
| モーメンタム | ✗ | ✗ |
| Adagrad | △ | ✗ |
| RMSprop | ○ | ✗ |
| Adadelta | ? | ? |
| Adam | ○ | △ |
| 自作アルゴリズム | ○ | ○ |

Adagradの△は、数式を見てもスケールに依存しなさそうですが、一度でも勾配が極端に大きいところに行ってしまうとその後の学習が遅くなってしまう性質があるため、スケールにやや依存します。

Adadeltaは挙動が理解できません。

Adamの△は、数式を見るとスケールに依存しそうですが、挙動を見るとうまいいくようです。

○や✗は数式からスケール依存性がわかる箇所です。

自作アルゴリズムは、スケールに依存しないように作ったアルゴリズムです。

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

Optimizerの数式は以前の記事にまとめました。

- [勾配降下法のアルゴリズム一覧のメモ](https://suzuki-navi.hatenablog.com/entry/2020/05/20/134615)

以下、実際に動かしてみて、この性質を確認します。


# 損失関数の設定

損失関数は[前回の記事](https://qiita.com/suzuki-navi/items/5e47a7cfc41aaa4c3ec7)と同じく $ (x^2+y^2-1)^2 + \frac{1}{8}(x + 1)^2 $ です。グラフにするとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/e7b7bfb5-ace9-7aed-938d-2f4fe834550b.png)

スケールを表すパラメータ $ k_x, k_y, k_l $ を導入して損失関数を $ k_l \left( ((k_x x)^2+(k_y y)^2-1)^2 + \frac{1}{8}(k_x x + 1)^2 \right) $ とします。

$ k_x=1, k_y=1, k_l=1 $ のときの学習推移は次のグラフです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/86e72d32-956b-8ddd-4002-a7ea41a0084d.png)

この $ k_{\bullet} $ を1から10000や0.0001などにしてみてグラフがどうなるかを調べます。

この記事のグラフでは色を統一しています。

<font color="#1f77b4">**青: シンプルな勾配降下法**</font>
<font color="#ff7f0e">**橙: モーメンタム**</font>
<font color="#2ca02c">**緑: Adagrad**</font>
<font color="#d62728">**赤: RMSprop**</font>
<font color="#9467bd">**紫: Adadelta**</font>
<font color="#8c564b">**茶: Adam**</font>
<font color="#e377c2">**桃: 自作アルゴリズム**</font>

各Optimizerでの学習率は $ k_x=1, k_y=1, k_l=1 $ のときにもっとも速く収束する値を適当に探しました。[前回の記事](https://qiita.com/suzuki-navi/items/5e47a7cfc41aaa4c3ec7)と同じです。


# 損失関数のスケールによる違い

$ k_l $ を大きい方に変えたときの様子。

上から $ k_l=1, 10, 100, 1000, 10000 $ です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/86e72d32-956b-8ddd-4002-a7ea41a0084d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/9a51c226-42a8-2056-45da-2900ddf949f4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/6ba0a53a-43f6-61d9-c5d1-ab25d7444d62.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/d0df97da-28cc-e915-bf4e-dfa397645f0f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/7358bbd3-22a8-933a-f1ed-a2e214d93234.png)

$ k_l $ を小さい方に変えたときの様子。

上から $ k_l=1, 0.1, 0.01, 0.001, 0.0001 $ です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/86e72d32-956b-8ddd-4002-a7ea41a0084d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/58fbcb65-4500-8ce3-81a0-7e58480576a5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/8892b827-f209-9f91-9096-173e1b852aa6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/6ca6e58c-7786-9309-34aa-db3f08e2fb83.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/36c8ebb1-ce78-e7fb-5dfc-55398f542538.png)

<font color="#1f77b4">**シンプルな勾配降下法**</font>と<font color="#ff7f0e">**モーメンタム**</font>は、$ k_l $ を大きくすると発散してしまって、グラフから消えています。$ k_l $ を小さくすると学習の速度が極端に遅くなってしまいます。損失関数のスケールが変わると学習率の調整が必須です。

<font color="#2ca02c">**Adagrad**</font>は、シンプルな勾配降下法やモーメンタムほどではありませんが、学習推移が変わっています。一度でも勾配が極端に大きいところに行ってしまうとその後の学習が遅くなってしまうのが原因だと想像しています。

<font color="#d62728">**RMSprop**</font>は、学習推移が変わりません。損失関数のスケールには依存しません。

<font color="#9467bd">**Adadelta**</font>は、学習推移が変わっています。なぜ変わるのかは理解できていないです。そもそも収束させる方法がわかりません。

<font color="#8c564b">**Adam**</font>は、学習推移が少し変わりますが、傾向や学習速度はあまり変わりません。損失関数のスケールへの依存は少ないです。

<font color="#e377c2">**自作アルゴリズム**</font>もAdamと同じく、損失関数のスケールへの依存は少ないです。


# 入力値のスケールによる違い

$ k_x, k_y $ を大きい方に変えたときの様子。

上から $ k_x=k_y=1, 10, 100, 1000, 10000 $ です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/86e72d32-956b-8ddd-4002-a7ea41a0084d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/a508ab90-11a2-d699-cbe7-0f9c49b2fd3c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/992c8c6b-5a57-51f2-433b-60a29120e145.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/6f9fa9fb-9a1f-e0be-7d2e-1ecb061f9bf6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/a0fb5c24-2941-7e2e-4617-00ed9ff0bf2e.png)

$ k_x, k_y $ を小さい方に変えたときの様子。

上から $ k_x=k_y=1, 0.1, 0.01, 0.001, 0.0001 $ です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/86e72d32-956b-8ddd-4002-a7ea41a0084d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/2f6e8cba-e999-17e6-85c6-24a37f04c9b2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/4f737b91-6ecf-de28-9d37-ae336f5d8ad6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/641b3232-097c-abca-53df-3be596f09195.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/0f4c9e3e-f0c4-7a94-300d-5ce26becf476.png)

<font color="#1f77b4">**シンプルな勾配降下法**</font>と<font color="#ff7f0e">**モーメンタム**</font>は、$ k_x, k_y $ を大きくすると発散してしまって、グラフから消えています。$ k_x, k_y $ を小さくすると学習の速度が極端に遅くなってしまいます。入力値のスケールが変わると学習率の調整が必須です。

<font color="#2ca02c">**Adagrad**</font>は、シンプルな勾配降下法やモーメンタムほどではありませんが、学習推移が変わっています。入力値のスケールに依存しますので、学習率の調整が必要です。

<font color="#d62728">**RMSprop**</font>もAdagradと同じく、入力値のスケールに依存しますので、学習率の調整が必要です。

<font color="#9467bd">**Adadelta**</font>は、そもそも収束させる方法がわかりません。

<font color="#8c564b">**Adam**</font>は、0.1〜1000ぐらいの範囲では、傾向や学習速度はあまり変わりません。入力値のスケールへの依存は少ないです。大きくスケールが変わると対応できないようです。

<font color="#e377c2">**自作アルゴリズム**</font>は、Adamよりもう少し広く 0.0001〜1000ぐらいの範囲では、傾向や学習速度はあまり変わりません。入力値のスケールへの依存は少ないです。


# Pythonソースコード

今回の調査に使ったソースコードです。Google Colaboratoryで実行しました。

```python
opts = [
  (tf.optimizers.SGD(learning_rate=0.1), "sgd"),
  (tf.optimizers.SGD(learning_rate=0.1, momentum=0.5), "momentum"),
  (tf.optimizers.Adagrad(learning_rate=2.0), "adagrad"),
  (tf.optimizers.RMSprop(learning_rate=0.005), "rmsprop"),
  (tf.optimizers.Adadelta(learning_rate=100), "adadelta"),
  (tf.optimizers.Adam(learning_rate=0.2), "adam"),
  (CustomOptimizer(learning_rate=0.1), "custom"),
]

def calculate(k1x, k1y, k2, maxLoopCount, plotXY):
  k1x2 = k1x * k1x
  k1y2 = k1y * k1y

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

  thres = k2 * 0.0001

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

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

  maxLoopCountAnimation = maxLoopCount

  x_ini = 0.1 / k1x
  y_ini = 2.0 / k1y

  for i in range(len(opts)):
    x.append(tf.Variable(x_ini))
    y.append(tf.Variable(y_ini))
    xHistory.append([])
    yHistory.append([])
    lossHistory.append([])
    convergenceCounter1.append(maxLoopCount)
    convergenceCounter2.append(0)
    start = time.time()
    for loopCount in range(maxLoopCount):
      l = float(loss(i))
      # グラフにするために記録
      xHistory[i].append(float(x[i]))
      yHistory[i].append(float(y[i]))
      lossHistory[i].append(l)
      if (math.isfinite(l) and l < thres and convergenceCounter1[i] >= maxLoopCount):
        convergenceCounter1[i] = loopCount
      if (not math.isfinite(l) or l >= thres):
        convergenceCounter2[i] = loopCount + 1

      # 最適化
      opts[i][0].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']

  print((k1x, k1y, k2))
  # グラフ化1つ目
  plt.rcParams['figure.figsize'] = (6.4, 4.8)
  #plt.ylim(k2 * -1.1, k2 * 3.5)
  plt.ylim(-10.0, +2.0)
  for i in range(len(opts)):
    plt.plot(range(maxLoopCount), np.log10(lossHistory[i]) - np.log10(k2), color=colors[i % len(colors)])
  plt.show()

  ths = np.linspace(-math.pi, math.pi, 100)
  thsx = np.cos(ths) / k1x
  thsy = np.sin(ths) / k1y

  if plotXY:
    # グラフ化2つ目以降
    plt.rcParams['figure.figsize'] = (16.0, 6.0)
    for i in range(len(opts)):
      print(opts[i][1])
      print("time: " + str(calculationTime[i]))
      print("counter1: " + str(convergenceCounter1[i]))
      print("counter2: " + str(convergenceCounter2[i]))
      print("loss: " + str(lossHistory[i][-1]))
      fig, (ax1, ax2) = plt.subplots(ncols=2)
      ax1.set_xlim(-2 / k1x, +2 / k1x)
      ax1.set_ylim(-1.5 / k1y, +1.5 / k1y)
      ax1.plot(thsx, thsy, color="#aaaaaa")
      ax1.add_patch(patches.Ellipse(xy=(-1.0, 0.0), width=0.2 / k1x, height=0.2 / k1y, fc="#cccccc"))
      ax1.plot(xHistory[i], yHistory[i], color=colors[i % len(colors)])
      ax2.set_ylim(-10.0, +2.0)
      ax2.plot(range(maxLoopCount), np.log10(lossHistory[i]) - np.log10(k2), color=colors[i % len(colors)])
      plt.show()

calculate(1.0, 1.0, 1.0, 1000, False)
calculate(0.1, 0.1, 1.0, 1000, False)
calculate(0.01, 0.01, 1.0, 1000, False)
calculate(0.001, 0.001, 1.0, 1000, False)
calculate(0.0001, 0.0001, 1.0, 1000, False)
```

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


# まとめ

ディープラーニングでは値のスケールがだいたい決まっているケースが多いので、幅広いスケールへの対応は重要ではありませんが、値範囲が予想しづらい最適化問題では、スケールの許容範囲が広いほうがよいです。私の自作アルゴリズムはもともとスケールに対する耐性を意識したものですが、Adamも強いことがわかりました。


# リンク

関連する私の記事

- [勾配降下法のアルゴリズム一覧のメモ](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)
- [TensorFlowのOptimizerの違いによる学習推移の比較](https://qiita.com/suzuki-navi/items/aace3fef0b355e23d0f0)
- [TensorFlowのOptimizerの違いによる学習推移をアニメーションにした](https://qiita.com/suzuki-navi/items/5e47a7cfc41aaa4c3ec7)