LoginSignup
1
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-15

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

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

さきに結論

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

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

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

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

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

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

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

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

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

損失関数の設定

損失関数は前回の記事と同じく $ (x^2+y^2-1)^2 + \frac{1}{8}(x + 1)^2 $ です。グラフにするとこんな感じです。

image.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

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

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

青: シンプルな勾配降下法
橙: モーメンタム
緑: Adagrad
赤: RMSprop
紫: Adadelta
茶: Adam
桃: 自作アルゴリズム

各Optimizerでの学習率は $ k_x=1, k_y=1, k_l=1 $ のときにもっとも速く収束する値を適当に探しました。前回の記事と同じです。

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

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

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

image.png
image.png
image.png
image.png
image.png

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

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

image.png
image.png
image.png
image.png
image.png

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

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

RMSpropは、学習推移が変わりません。損失関数のスケールには依存しません。

Adadeltaは、学習推移が変わっています。なぜ変わるのかは理解できていないです。そもそも収束させる方法がわかりません。

Adamは、学習推移が少し変わりますが、傾向や学習速度はあまり変わりません。損失関数のスケールへの依存は少ないです。

自作アルゴリズムもAdamと同じく、損失関数のスケールへの依存は少ないです。

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

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

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

image.png
image.png
image.png
image.png
image.png

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

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

image.png
image.png
image.png
image.png
image.png

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

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

RMSpropもAdagradと同じく、入力値のスケールに依存しますので、学習率の調整が必要です。

Adadeltaは、そもそも収束させる方法がわかりません。

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

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

Pythonソースコード

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

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を自作する

まとめ

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

リンク

関連する私の記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1