Python
機械学習
ニューラルネットワーク
TensorFlow

ニューラルネットで任意の関数が近似できる?ならやってみよう:TensorFlowで。

機械学習でよく言われる、「隠れ層一層のニューラルネットワークは任意の関数を近似できる」ということを実際に確かめてみよう。隠れ層を一層として、ユニット数を増やしてみる。

TensorFlowを用いる。
基本的なことは

TensorFlowの高レベルAPIの使用方法2:Dataset APIを使ってみる
https://qiita.com/cometscome_phys/items/a95a91f9822353303dd8

を参照のこと。

バージョン

TensorFlow: 1.9.0
Python: 3.6.4

再現すべき関数

ここでは、ある関数
$$
y = a_0 x+ a_1 x^2 + b_0 + 3cos(\alpha x)
$$
という関数を考える。これまでは、最後のcosはノイズのようなものとして考えており、$a_0$と$a_1$と$b_0$によって得られる二次関数を得ることが目的としていた。今回は、この関数自体をフィッティングすることを目指す。$\alpha$を大きくすると振動が増え、より一致が難しくなる。
$\alpha = 3$としてみよう。
データを300点作っておく。

test.py
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

n = 300
x0 = np.linspace(-2.0, 2.0, n)
a0 = 3.0
a1 = 2.0
b0 = 1.0
kk = 3
y0 = np.zeros(n)
y0[:] = a0*x0+a1*x0**2 + b0 + 3*np.cos(kk*x0)

plt.plot(x0,y0 )
plt.show()
plt.savefig("graph.png")

この時のグラフは以下の通りである。

Unknown-6.png

これまではデータのフィッティングには多項式による展開をしていたが、今回は、$x$座標そのものを用いる。
つまり、インプットデータは$x$座標、教師データは$y$座標である。

インプットデータの生成

ここはこれまでの記事とほとんど同じである。
$x$そのものをインプットデータとするので、

test.py
def make_phi(x0,n,k):    
    phi = np.array([x0])
#    phi = np.array([x0**j for j in range(k)])
    return phi.T

という形でインプットデータを生成することにする。ここで$n$はデータの数である。前回のコードを使いまわしているので$k$があるが、この記事では使わない。
データの範囲は-2から2とすると

test.py
x0 = np.linspace(-2.0, 2.0, n)
k = 1
phi = make_phi(x0,n,k)

となる。
その時の教師データはy0である。

また、学習には使わない「テストデータ」を100点用意し、

test.py
n_test = 100
x0_test = np.linspace(-2.0, 2.0, n_test)
y0_test = np.zeros(n_test)
y0_test[:] = a0*x0_test+a1*x0_test**2 + b0 + 3*np.cos(kk*x0_test)
phi_test = make_phi(x0_test,n_test,k)

としておく。

Dataset APIの使用

ここは前回の記事と同じなので、コードだけ再掲する。
今、バッチサイズを20としている。

test.py
batch_size = 20
dataset = tf.data.Dataset.from_tensor_slices({"phi":phi, "y0":y0}).shuffle(10).batch(batch_size)

test_dataset = tf.data.Dataset.from_tensor_slices({"phi":phi_test, "y0":y0_test}).batch(n_test)
iterator = tf.data.Iterator.from_structure(dataset.output_types,
                                           dataset.output_shapes)
train_init_op = iterator.make_initializer(dataset)
test_init_op = iterator.make_initializer(test_dataset)
next_step= iterator.get_next()

となる。

グラフの構築

グラフの構築も前回の記事と同様。

test.py
def build_graph_layers_class(d_input,d_middle,d_type):
    x = next_step["phi"]
    yout= next_step["y0"]
    hidden1 = tf.layers.Dense(units=d_middle,activation=tf.nn.relu)
    x1 = hidden1(x)
    outlayer = tf.layers.Dense(units=1,activation=None)
    y = outlayer(x1)

    diff = tf.subtract(y,yout)
    loss = tf.nn.l2_loss(diff)
    minimize = tf.train.AdamOptimizer().minimize(loss)
    return x,y,yout,diff,loss,minimize,hidden1,outlayer

隠れ層1層としてそのユニットを10であるとして、グラフを作る。

test.py
d_type = tf.float32
d_input = 1
d_middle = 10
x,y,yout,diff,loss,minimize,hidden1,outlayer= build_graph_layers_class(d_input,d_middle,d_type)

で設定する。d_middleが隠れ層のユニットの数である。

学習

学習についても前回と同じであり、

test.py
np = 5000
with tf.Session() as sess:
    tf.global_variables_initializer().run()
    for i in range(np):
        sess.run(train_init_op)
        while True:
            try:
                sess.run(minimize)
                #print(sess.run(next_step))
                losstrain = sess.run(loss)/batch_size
                #print(losstrain)


            except tf.errors.OutOfRangeError:
                break

        sess.run(test_init_op)
        losstest = sess.run(loss)/n_test
        if i % 100 is 0:
            print("i",i,"loss",losstest)
    W = sess.run(hidden1.weights)
    print(W)
    a = sess.run(outlayer.weights)
    print(a)
    sess.run(test_init_op)
    yt = sess.run(y)

plt.plot(x0_test,y0_test )
plt.plot(x0_test,yt )
plt.show()
plt.savefig("graph2.png")

となる。

結果

それでは、ユニット数を変化させたときにどうなるか見てみよう。学習はどれも5000回で止めている。

test.py
d_type = tf.float32
d_input = 1
d_middle = 10
x,y,yout,diff,loss,minimize,hidden1,outlayer= build_graph_layers_class(d_input,d_middle,d_type)

のd_middleを変えて再実行すればよい。

まず、$\alpha = 3$とする。

ユニット数4

Unknown-1.png

ユニット数10

Unknown.png

ユニット数20

Unknown-2.png

ユニット数50

Unknown-3.png

ユニット数100

Unknown-4.png

ユニット数200

Unknown-5.png

ユニット数500

Unknown-6.png

だんだん良くなっている気はするが、思ったよりも上がっていないような気もする。

ユニット数500。データ数3000

Dataset APIに入れたデータは300個だったが、これを10倍すると何か変わるだろうか?

Unknown-7.png

とても綺麗になった。

ユニット数50。データ数3000

データ数は多いままユニット数を1/10にしてみた。

Unknown-8.png

あれ、ユニット数500とあまり変わらなく、良い感じ。

ユニット数10。データ数3000

final.png

さすがにこれは少なかったらしい。

結論

データ数重要。

(18/08/03)追記

activation関数を変化させたときにどうなるのか、というコメントがあったので、試しに変えてみた。
先ほどまで使っていたactivation関数はReLUと呼ばれるもので、
$$
f(x) = max(0,x)
$$
というものであった。これは、xが0より小さいときには0を、xが0より大きいときはxそのものを返す関数である。
activation関数には他にもあり、例えばシグモイド(sigmoid)関数は
$$
f(x) = \frac{1}{1+\exp(-x)}
$$
である。$x$が0より小さいときはほとんどゼロで、$x=0$の時には1/2、$x$が大きくなっていくと1になる関数である。
TensorFlowでは、activation=tf.nn.reluの代わりにactivation=tf.sigmoidを使えば、シグモイド関数に変更できる。

activation関数の影響がわかりやすいように、ユニット数を2とする。

test.py
d_type = tf.float32
d_input = 1
d_middle = 2
x,y,yout,diff,loss,minimize,hidden1,outlayer= build_graph_layers_class(d_input,d_middle,d_type)

データ点は3000点としておく。

ReLUの場合

これは先ほどと同じactivation関数である。
結果は、

tes1.png

である。今、隠れ層が1層なので、ユニット数が2個ということは二つの原点の異なるReLU関数の重ね合わせということである。
実際、カクカクっとしていて、そうなっていることがわかる。

シグモイドの場合

シグモイドの場合には、

グラフを

test.py
def build_graph_layers_class(d_input,d_middle,d_type):
    x = next_step["phi"]
    yout= next_step["y0"]
    hidden1 = tf.layers.Dense(units=d_middle,activation=tf.sigmoid)
    x1 = hidden1(x)
    outlayer = tf.layers.Dense(units=1,activation=None)
    y = outlayer(x1)

    diff = tf.subtract(y,yout)
    loss = tf.nn.l2_loss(diff)
    minimize = tf.train.AdamOptimizer().minimize(loss)
    return x,y,yout,diff,loss,minimize,hidden1,outlayer

と一箇所変更して、

test.py
d_type = tf.float32
d_input = 1
d_middle = 2
x,y,yout,diff,loss,minimize,hidden1,outlayer= build_graph_layers_class(d_input,d_middle,d_type)

を再実行する。その結果、

Unknown-1.png

となる。
sigmoid関数はゆっくり変化する関数なので、こちらのがふんわりしている。