ニューラルネットで任意の関数が近似できる?ならやってみよう: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関数はゆっくり変化する関数なので、こちらのがふんわりしている。