TensorFlow で「y=x*(x-1)*(x-2)」の極小値を求める

TensorFlow を利用し関数の局所的に凹になってる箇所ー Local Minima ーすなわち数学的にいう極小値を求めてみようと思います。今回、対象とする関数は 「y = x*(x-1)*(x-2)」です。グラフにすると以下のようになると思います。汚くてすいません。

f:id:keimina:20180822204257j:plain

ちなみにこの関数は x = 1 ± 1/√3 (約1.577, 約0.422)のとき極小値と極大値をとります。極小値と極大値はy=f(x)のグラフの傾きが0となる箇所を求めればよいです。すなわち、関数を微分したものをf'(x)とすればf’(x)=0となるxを求めればよいことになります。試しに Sympy で確かめてみます。

from sympy import Symbol, solve
x = Symbol('x')
y = x*(x-1)*(x-2)
print(solve(y.diff(x)))
# output:
# [-sqrt(3)/3 + 1, sqrt(3)/3 + 1]

Sympy により、すでに答え(x =1.577)は出てしまったのですが、これを TensorFlow を用いて数値計算的に求めることが今回のゴールです。(上記 Sympy の例では 記号的なアルゴリズムで求めていると思われるため今回の TensorFlow で求めるやり方と内部の動きが異なります。)

ここからTensorFlow を用いて最急降下法(Gradient Descent)によって関数の極小値を見つけます。TensorFlow では、最急降下法により関数の極小値を求めるための「TensorFlow.train.GradientDescentOptimizer」クラスとその「minimize」オペレーションが用意されています。今回は、それと同様のオペレーションのある「TensorFlow.train.MomentumOptimizer」クラスを使用し、得られたxとyの値をグラフにプロットし、画像として保存し得られたxとyを見てみます。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

# 関数 y = x*(x-1)*(x-2) を定義
def f(x):
    return x * (x - 1.0) * (x - 2.0)

def save_graph(solved_x, filename):
    # グラフ描画して保存
    # x軸とy軸を描画
    plt.axis('off')
    plt.axhline(0, color='black', linewidth=1)
    plt.axvline(0, color='black', linewidth=1)
    # (x, y) = (0, 0) に点を描画
    plt.plot(0, 0, marker='o', color='black')
    plt.text(0, 0, "(0, 0)")
    # (x, y) = (1, 0) に点を描画
    plt.plot(1, 0, marker='o', color='black')
    plt.text(1, 0, "(1, 0)")
    # (x, y) = (2, 0) に点を描画
    plt.plot(2, 0, marker='o', color='black')
    plt.text(2, 0, "(2, 0)")
    # x軸にラベルを追加
    plt.text(2.3 + 0.2, 0, "x", verticalalignment='center')
    # y軸にラベルを追加
    plt.text(0, f(2.3)+0.2, "y", horizontalalignment='center')
    # 関数 y = f(x) を x = [-0.3, 2.3] の範囲で描画
    a = np.linspace(-0.3, 2.3, 100)
    b = f(a)
    plt.plot(a, b, label='y = x*(x-1)*(x-2)')
    plt.legend(loc=1)
    # 極小値の描画
    plt.plot(solved_x, f(solved_x), marker='o', color='orange')
    # f(x)=0 の点をわかりやすくするための線を描画
    plt.axvline(solved_x, linestyle='--', color='orange')
    plt.axhline(f(solved_x), linestyle='--', color='orange')
    plt.text(solved_x, f(solved_x)-0.1, "(x, y)=(%f, %f)"%(solved_x, f(solved_x)), horizontalalignment='center')
    plt.savefig(filename)
    plt.close()


################################################################
# メイン
################################################################
x_kyokusyou = 1.0 - 1.0/np.sqrt(3) # 極大値(local maxima)をとるxの値
#x_start = x_kyokusyou - 0.01 # パターン1
#x_start = x_kyokusyou + 0.01 # パターン2
x_start = 2.2 # パターン3
x = tf.Variable(x_start)

y = x * (x - 1.0) * (x - 2.0)
#grad_opt = tf.train.GradientDescentOptimizer(0.03)
grad_opt = tf.train.MomentumOptimizer(0.01, 0.95)
grad_opt_minimize = grad_opt.minimize(y)
init = tf.global_variables_initializer()

with tf.Session() as sess:
    sess.run(init)
    for i in range(200):
        sess.run(grad_opt_minimize)
        solved_x = sess.run(x)
        if solved_x < -0.3: # -infに落ちるので途中でやめる
            break
        # グラフ描画して保存
        save_graph(solved_x, "/Users/kei/Desktop/pic/%03d.png"%i)

初期値を変えて3パターン(パターン1、パターン2、パターン3)で試し、出力されたグラフを動画にしてみました。

・パターン1(初期値 x=1-1/√3 - 0.01)
f:id:keimina:20180822205755g:plain

・パターン2(初期値 x=1-1/√3 + 0.01)
f:id:keimina:20180822205848g:plain

・パターン3(初期値 x=2.2)
f:id:keimina:20180822210005g:plain

パターン1では求めた点がx軸のマイナス方向へ向かって凹があるまで移動していきます。この関数は x < 1 - 1/√3 では凹となる箇所がないため点が無限に移動し続けます。パターン2、パターン3は振り子のように減衰しながら極小値に収束していくように移動します。

最後に、パターン3で求めたxを見て見ます。

print(solved_x)
# output:
# 1.5798204

多少の誤差はありますが、現在の繰り返し回数を200回から増やせば誤差はなくなっていくものと思われます。

以上より、最急降下法によって関数の極小値を数値計算的に求められることがわかりました。
今回の例ではパラメータが一つなため計算量が少なくCPUでもそんなに時間もかからず求まりますが、、ニューラルネットワークの場合は計算量が層の深さの累乗のオーダーで大きくなっていきます。ただ、計算自体は1度、計算式を組み立ててしまえば、その計算式にひたすら数値を代入して演算を繰り返してニューラルネットワークを更新するだけです。そのためGPUのように安く大量の数値計算の並列処理ができるデバイスとこの方法を掛け合わせることによりディープラーニングが実用的なものになり、それが現在の第3次人口知能ブームのきっかけなのではないかと思っています。

以上です。おやすみなさい。