[研究レポート] 学習率の調整

[研究レポート] 学習率の調整

学習率関数 (α関数)

α関数の定義

 勾配降下法において 学習率 $\alpha$ を小さく設定すると精度は上がりますが、斜面をゆっくりと降りることになるので、最小値を求めるまでに時間がかかってしまいます。とはいえ、学習率を大きくすると、最小値のある点を通り越してしまうことがあるので、いつまでたっても窪みの底に達しないということが起こってしまいます。このあたりのトレードオフに頭を悩ませながら、
「どうすりゃいいんだよ~!」
とイライラしている人もいるかもしれません。そこで、学習率 (Learning Rate) を定数ではなく、ステップ数 $t$ の関数として定義して、処理の効率化を図ることが考えてみます。この関数を 学習率関数 (LR function)、あるいは $\alpha$ 関数 (alpha function) とよぶことにしましょう。

 最も単純な方法としては、ステップ数 $t$ について単調減少するように定義することです。たとえば次のような関数です。
 
\[\alpha(t)=a+b\exp(-ct)\]
 この関数は $t=0$ のときに初期値 $a+b$ をとり、$t$ の増加とともに減少してゆき、最終的にはほぼ $a$ の値に落ち着きます。Matplotlib でグラフをプロットしてみましょう。

# https://python.atelierkobato.com/alpha/
# リストR5-A-1  Plotting the graph of LR function

import numpy as np
import matplotlib.pyplot as plt

# α関数を定義
def alpha(t, a, b, c):
    return a + b * np.exp(-c * t)

# データを用意
t = np.arange(0, 101, 1)
ft = alpha(t, 0.01, 0.01, 0.05)

# データをプロット
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title("alpha function", fontsize = 16)
ax.set_xlabel("t", fontsize = 16)
ax.set_ylabel("α(t)", fontsize = 16)
ax.set_xlim([0, 100])
ax.set_ylim([0.01, 0.02])
ax.scatter(t, ft, s = 1.2, color = "darkblue")

# α(100)の値を表示
print("α(t=100) = {0:.3f}".format(ft[100]))
α(t=100) = 0.010

 α関数で学習率を調整

 上のコードでは $a=b=0.01,\ c=0.05$ に設定してあります。
 

α関数の実装

 $\alpha$ 関数を使って 2 次関数の最小値を求めてみます。

# https://python.atelierkobato.com/alpha/
# リストR5-B-1

import numpy as np
import matplotlib.pyplot as plt

# α関数を定義
def alpha(t, a, b, c):
    return a + b * np.exp(-c * t)

# 関数を定義
def func(x):
    return x**2

# 関数の勾配
def dfunc(x):
    return 2 * x

# データを作成
x = np.arange(-4, 4, 0.1)
y = func(x)

# ★★★★★★★★★★

# 勾配降下法による最小値の探索

x = 2
eps = 0.001 
t_max = 1000

for t in range(1, t_max):
    a = alpha(t, 0.01, 0.1, 0.002)
    x -= a * dfunc(x)

    if abs(dfunc(x)) < eps:
        break

print("t = {0}".format(t))
print("xmin = {0:.3f}".format(x))
t = 35
xmin = 0.000

 a = 0.01, b = 0.1, c = 0.002 に設定してあります。学習率を一定値 0.01 に設定した場合は 411 ステップかかるので、1/10 以下のステップで最小値を発見できたことになります。もちろん処理速度も各段に速くなります。
  

α関数とパワーレーダーの併用

 α関数と拡散式パワーレーダーを併用して、2 変数関数
 
\[z=\cos x\sin(\sqrt{2}y)+\frac{1}{24}\sin(\sqrt{3}xy)\]
の最小値を求めるコードを載せておきます。

# リストR5-C-1

import numpy as np
import matplotlib.pyplot as plt

def mesh_2d(xr, yr, n):
    x = np.linspace(xr[0], xr[1], n)
    y = np.linspace(yr[0], yr[1], n)
    X, Y = np.meshgrid(x, y)
    return X, Y

def r_mesh(xr, yr, n):
    x = np.random.uniform(xr[0], xr[1], (n, n))
    y = np.random.uniform(yr[0], yr[1], (n, n))
    X, Y = np.array([x, y])
    return X, Y

# α関数を定義
def alpha(t, a, b, c):
    return a + b * np.exp(-c * t)

# テスト関数を定義
def func(x, y):
    sin = np.sin
    cos = np.cos
    sqrt = np.sqrt
    z = cos(x) * sin(sqrt(2)*y) + sin(sqrt(3)*x*y/24)
    return z

# テスト関数の勾配
def grad(x, y):
    sin = np.sin
    cos = np.cos
    sqrt = np.sqrt
    gx = -sin(x) * sin(sqrt(2)*y) + sqrt(3) * y * cos(sqrt(3)*x*y)/24
    gy = sqrt(2) * cos(x) * cos(sqrt(2)*y) + sqrt(3) * x * cos(sqrt(3)*x*y)/24
    return np.array([gx, gy])
    
# 等高線描画データ
xr = [0, 16]
yr = [0, 8]
n = 257
X, Y = mesh_2d(xr, yr, n)
Z = func(X, Y)

# Figureと3DAxesの設定
fig = plt.figure(figsize = (8, 6))
ax = fig.add_subplot(111)
ax.set_xlabel("x", size = 16)
ax.set_ylabel("y", size = 16)
ax.set_xlim([xr[0], xr[1]])
ax.set_ylim([yr[0], yr[1]])

# 等高線をプロット
ct = ax.contour(X, Y, Z, colors = "black")
ax.clabel(ct, fmt = "%0.1f", fontsize = 10, zorder = 1)

# 乱数の初期化
np.random.seed(0)

# 初期地点を設定
pt = [2, 2]

# 初期地点の高度
zt = func(pt[0], pt[1])

# ループの最大数と最小数
t_max = 8000
t_min = 500

# レーダーの探査頻度
fq = 8

# 収束判定条件
eps = 0.01

for t in range(t_max):
    a = alpha(t, 0.01, 0.5, 0.002)
    dp_0 = a * grad(pt[0], pt[1])[0]
    dp_1 = a * grad(pt[0], pt[1])[1]
    pt[0] -= dp_0
    pt[1] -= dp_1

    if t % fq == 0:
        Xs, Ys = r_mesh(xr, yr, 32)
        Zs = func(Xs, Ys)
        Zs_min = np.min(Zs)

        if zt > Zs_min:
            Xs_min = Xs[Zs == Zs_min][0]
            Ys_min = Ys[Zs == Zs_min][0]
            pt = [Xs_min, Ys_min]
            ax.plot(pt[0], pt[1], color = "red",
                    marker = "o", markersize = 1,
                    zorder = 1, linestyle = "none")

    g = grad(pt[0], pt[1])
    d = np.sqrt(g[0]**2 + g[1]**2)
    
    if t > t_min and d < eps:
        break

zt = func(pt[0], pt[1])
pt = np.round(pt, 3)
zt = np.round(zt, 3)

print("t = {}".format(t))
print("(x, y, z) = ({0:.3f}, {1:.3f}, {2:.3f})".format(pt[0], pt[1], zt))
t = 797
(x, y, z) = (9.742, 5.818, -1.697)

Python テスト関数の最小値探索

 処理時間は約 3 秒でした。ちなみに、α関数を使わずに alpha = 0.01 に設定して動かすと、1 万回ループしても終わりません。図の赤い軌跡を見てもわかるように、パワーレーダーが最小値となる窪みを見逃すことはまずありません。仮にループの初期段階でα関数が大きな値をとっているときに最小値を通り越すことがあっても、レーダーは最小値の近辺へ引き戻すようにはたらきます(これが確率的勾配法などの他の手法と大きく異なる点です)。ですから、レーダー勾配法においては最小値のある窪みの底に達するまでの時間をどれだけ短縮できるのかという点のほうが重要なのです。次回は拡散式パワーレーダーを上回る性能をもつ拡散式局所探索レーダーを紹介します。