入力値や損失関数のスケールを変えると、学習推移はどのように変わるかを調べました。
スケールを変えても学習推移が変わらないOptimizerと、大きく変わってしまい学習率のパラメータを調整しないといけないOptimizerがあります。
さきに結論
○: 学習率のパラメータはスケールに依存しない、またはわずか
✗: スケールが変わると学習速度が大きく変わり、学習率を調整することが必須
Optimizer | 損失関数のスケール | 入力値のスケール |
---|---|---|
シンプルな勾配降下法 | ✗ | ✗ |
モーメンタム | ✗ | ✗ |
Adagrad | △ | ✗ |
RMSprop | ○ | ✗ |
Adadelta | ? | ? |
Adam | ○ | △ |
自作アルゴリズム | ○ | ○ |
Adagradの△は、数式を見てもスケールに依存しなさそうですが、一度でも勾配が極端に大きいところに行ってしまうとその後の学習が遅くなってしまう性質があるため、スケールにやや依存します。
Adadeltaは挙動が理解できません。
Adamの△は、数式を見るとスケールに依存しそうですが、挙動を見るとうまいいくようです。
○や✗は数式からスケール依存性がわかる箇所です。
自作アルゴリズムは、スケールに依存しないように作ったアルゴリズムです。
Optimizerの数式は以前の記事にまとめました。
以下、実際に動かしてみて、この性質を確認します。
損失関数の設定
損失関数は前回の記事と同じく $ (x^2+y^2-1)^2 + \frac{1}{8}(x + 1)^2 $ です。グラフにするとこんな感じです。
スケールを表すパラメータ $ 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 $ のときの学習推移は次のグラフです。
この $ 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 $ です。
$ k_l $ を小さい方に変えたときの様子。
上から $ k_l=1, 0.1, 0.01, 0.001, 0.0001 $ です。
シンプルな勾配降下法とモーメンタムは、$ k_l $ を大きくすると発散してしまって、グラフから消えています。$ k_l $ を小さくすると学習の速度が極端に遅くなってしまいます。損失関数のスケールが変わると学習率の調整が必須です。
Adagradは、シンプルな勾配降下法やモーメンタムほどではありませんが、学習推移が変わっています。一度でも勾配が極端に大きいところに行ってしまうとその後の学習が遅くなってしまうのが原因だと想像しています。
RMSpropは、学習推移が変わりません。損失関数のスケールには依存しません。
Adadeltaは、学習推移が変わっています。なぜ変わるのかは理解できていないです。そもそも収束させる方法がわかりません。
Adamは、学習推移が少し変わりますが、傾向や学習速度はあまり変わりません。損失関数のスケールへの依存は少ないです。
自作アルゴリズムもAdamと同じく、損失関数のスケールへの依存は少ないです。
入力値のスケールによる違い
$ k_x, k_y $ を大きい方に変えたときの様子。
上から $ k_x=k_y=1, 10, 100, 1000, 10000 $ です。
$ k_x, k_y $ を小さい方に変えたときの様子。
上から $ k_x=k_y=1, 0.1, 0.01, 0.001, 0.0001 $ です。
シンプルな勾配降下法とモーメンタムは、$ 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も強いことがわかりました。
リンク
関連する私の記事