"機械学習","信号解析","ディープラーニング"の勉強

HELLO CYBERNETICS

深層学習、機械学習、強化学習、信号処理、制御工学などをテーマに扱っていきます

【学習を実行】ニューラルネットとTensorFlow入門のためのオリジナルチュートリアル2

 

 

f:id:s0sem0y:20170707094814j:plain

はじめに

この記事は以下の記事の続編です。

s0sem0y.hatenablog.com

 

順伝播の写像を定義し、実際にデータをTensorFlowの世界に流すところまでをやってきました。次はニューラルネットワークの学習を行うための方法紹介します。

 

これまでどおり、結局のところは学習を行うための写像を定義していくだけなのですが、この部分は非常に難しいです。でも安心してください。ニューラルネットワークは大抵同じような方法を取って学習が行われるので、既にTensorFlowから提供されているものを使えば良いのです。

 

ただ、TensorFlowの動作に慣れる上でも、写像(あるいは作用素、あるいは作用素で構成される計算グラフと言ってもいい)を定義していっている気持ちは忘れないでください。あくまでTensorFlowのPythonAPIで提供されている写像を流用して定義をしたということになります。

 

では本題に入っていきます。

 

ニューラルネットの学習

これまで作ってきた写像を実行して出てくるのは、ランダムな行列によって変換されて出てくる意味のないデータです。ニューラルネットワークの本分は、このランダムな状態の\bf Wから開始し、徐々に目的の値を出力するように行列\bf Wを獲得することです(これを学習と言う)。

 

ニューラルネットワークの学習は突き詰めれば最適化問題として定式化できます。

 

 

最適化問題

最適化問題というのは目的関数J(w)を最大化(あるいは最小化)するようなwを見つけることです。

 

J(w) = w^2-2w

 

という目的関数を最小化したいとしましょう。この場合平方完成によって

 

J(w) = (w-1)^2 - 1 

 

となって、w=1J=-1が最小値です((w-1)^2 \geq 0 より)。あるいは

 

\displaystyle \frac{ dJ(w)}{dw}=2w-2=0

 

を解いて、w = 1が解としても良いです。しかしこれが可能なのはdJ/dw=0の解が1つしか無いことが分かっているからです。これがいっぱいあると全く検討がつかなくなりますし、そもそもこんな綺麗に方程式が解ける保証もありません。

 

従ってニューラルネットでは勾配法が用いられるわけです。

 

s0sem0y.hatenablog.com

 

損失関数 

 

ニューラルネットの学習は、最終的には目的関数の最小化で定式化します。その目的関数のことを機械学習では損失関数と呼びます。

 

損失関数は通常、ニューラルネット(合成写像)f({\bf w,x})の出力と、本来行って欲しい出力\bf tとの相違を表す形で定義されます。すなわち、損失関数とは以下のような形をしているわけです。

 

L({\bf t},f({\bf w,x}))

 

ここに\bf t,xは両方外から与えるものであり、プレースホルダーになります。ニューラルネットが調整できるのは\bf wだけです。つまり、\bf wを少しずつ調整して、Lを最小化するようなf({\bf w,x})を獲得したいということになります。

 

ですからTensorFlowでニューラルネットを学習させたければ、Lという損失値を返してくる写像を作ってやらねばなりません。これも非常に簡単です。例として分類で使われる交差エントロピーを用いましょう。a,bの交差エントロピーは以下で定義されます。

 

l(a,b) = -alog(b)

 

データは複数同時に扱うため、それぞれのデータにおける損失の平均を使うことにします。

 

\displaystyle L({\bf t},f({\bf w,x})) = \frac{1}{N}\sum_{n=1}^N -t_n\log(f({\bf w,x_n}))

 

コードとしては以下の形になります。

 

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

 

ベクトルの各成分に対してt\log(f)を計算し、「tf.reduce_sum」で各成分の交差エントロピーの和を取ります(-符号はどこにあってもいい)。その後、データが複数あるので平均を取るという処理を行っています。

 

コイツを準備しておけば、以下のように損失関数まで実装できます。(ただし、\bf tがプレースホルダーであることを忘れないように)

 

今回は4次元のデータを入れて、3つのクラスに分類するニューラルネットワークを書きます。つまり、4次元データを3次元データに変換する合成写像を作り、その出力から\rm lossを算出する写像を追加するということです。

 

def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)

def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

Q = 4
P = 4
R = 3

sess = tf.InteractiveSession()

X
= tf.placeholder(dtype = tf.float32, shape = [None, Q])
t = tf.placeholder(dtype = tf.float32, shape = [None, R])

W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = tf.nn.sigmoid(f1)

W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2

f = tf.nn.softmax(f2)

loss = loss(t, f)

 

これで、lossを獲得する写像までを定義してやることができました。 

 

ここで追加したコードは青色ですが、ニューラルネットの出力をソフトマックス関数にしていることに注意してください。通常、分類問題ではこのように出力をソフトマックスで0~1の値に制限します。これによって、出力があるクラスに属する確率を表していると見なせます。

 

これは0~1に制限したからそうみなせるというのも1つのポイントですが、実は交差エントロピーを損失関数に設定することで、最尤推定を行っていると見なすことができるため、確率論的にも妥当な設計になっているのです。

 

 

学習を行う誤差逆伝播法

 

勾配法

 

学習とは、プレースホルダー\bf t,xを持つ損失関数L({\bf t},f({\bf w,x}))を、\bf wに関して最小化することです。最小化するために

 

\displaystyle \frac{ \partial L({\bf w})}{\partial {\bf w}}=0

 

を求めたいのですが、実はこれが上手く求まりません。なので、ここは妥協策として

 

\displaystyle {\bf w^{t}} = {\bf w^{t-1}} - ε\frac{ \partial L({\bf w^{t-1}})}{\partial {\bf w^{t-1}}}

 

と少しずつ更新していくことにします。ここにεは学習係数と呼ばれるスカラー値になります。これは通常0.001などの小さな値が取られます。tは何回目の更新かを表しています。

 

\displaystyle \frac{ \partial L({\bf w})}{\partial {\bf w}}

 

Lを増加させる方向を示しているため、マイナス方向に進むことでLを(今よりは)減少させる\bf wが獲得できるという戦略です。これを繰り返しおこなっていくのが勾配法になります。

 

誤差逆伝搬法

ここまでTensorFlowで定義してきたのは、\bf t,xを与えてL({\bf t},f({\bf w,x}))を計算するところまでです。従って、次にTensorFlowに与えてやらなければ写像は

 

\displaystyle {\rm opitimizer}:L \rightarrow \frac{ \partial L({\bf w})}{\partial {\bf w}}

 

ということになります。実はこれは大変な数式になってしまい、手に負えないほどではありませんが、それでもかなり大変です。もちろんこの写像を手で書いてやれば、学習は行えますが、あらゆるニューラルネットワークでいつも使われるものであるため、既にTensorFlowで実装されています。

 

この微分を上手く求める写像を誤差逆伝搬法と良い、ニューラルネットの肝になる技術です。

 

s0sem0y.hatenablog.com

 

TensorFlowでは誤差逆伝搬をしてくれる計算が既に実装されており

 

opitimizer = tf.train.GradientDescentOptimizer(learning_rate = 0.001)

 

と書くことができます。「learning_rate」が学習率に相当します。コイツを求めたら、\bf wを以下のように変換する写像が必要です。

 

\displaystyle {\rm optimizer}:  {\bf w} \rightarrow {\bf w} - ε\frac{ \partial L({\bf w})}{\partial {\bf w}}

 

これは引き算をするだけで簡単ですが、TensorFlowでは、この更新式も実装されており

 

update = optimizer.minimize(loss)

 

で与えることができます。(説明の便宜上、こう説明していますが、optimizerインスタンスの中に更新の式も入っており、メソッドで呼び出すという形になっています)

通常はupdateではなくtrain_stepという名前をつけるので、それに合わせましょう。その場合の全体のコードは以下のようになります。

 

def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)

def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

Q = 4
P = 4
R = 3

sess = tf.InteractiveSession()

X
= tf.placeholder(dtype = tf.float32, shape = [None, Q])
t = tf.placeholder(dtype = tf.float32, shape = [None, R])

W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = tf.nn.sigmoid(f1)

W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2

f = tf.nn.softmax(f2)

loss = loss(t, f)

optimizer = tf.train.GradientDescentOptimizer(learning_rate = 0.001)
train_step = optimizer.minimize(loss)

 

ここまでで、私達はもう、学習(すなわち\bf wを更新)する写像を完全に定義することができました。TensorFlowは完全に個々に設計した写像の繋がりを把握し、合成写像として全体を認識しているため、学習を行う際に呼び出さなければいけない写像は、train_stepだけです。

 

学習を実行

プレースホルダーになっている\bf X,tに注意して、train_stepを好きなだけ実行してあげましょう。そのコードはとても簡単に書くことができます。

 

init = tf.global_variables_initializer()
sess.run(init) ## TensorFlowの世界を初期化(必ず必要)

num_epoch = 100
for epoch in range(num_epoch):
sess.run(train_step, feed_dict = {X: 学習データ, t: 教師データ})

 

sess.runのひとつ目の引数に写像の名前を与えます。二個目の引数「feed_dict」がプレースホルダーに与える具体的なデータです。こいつらは既に順伝播の際にやりましたね。今回でlossを計算し、そして更新するtrain_stepまでの合成写像を書きましたので、これで学習が可能です。

 

def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)

def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

Q = 4
P = 4
R = 3

sess = tf.InteractiveSession()

X
= tf.placeholder(dtype = tf.float32, shape = [None, Q])
t = tf.placeholder(dtype = tf.float32, shape = [None, R])

W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = tf.nn.sigmoid(f1)

W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2

f = tf.nn.softmax(f2)

loss = loss(t, f)

optimizer = tf.train.GradientDescentOptimizer(learning_rate = 0.001)
train_step = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess.run(init) ## TensorFlowの世界を初期化(必ず必要)

num_epoch = 100
for epoch in range(num_epoch):
sess.run(train_step, feed_dict = {X: 学習データ, t: 教師データ})

 

 

実際に学習を動作させる

学習させる際の基本

結局は何らかの写像を色々書いて、TensorFlowの世界に渡して実行させるというのが全体の流れです。私達は今、「学習データと教師データを与えたら、パラメータを更新する写像」を定義し、それを実行する流れを得ました。

 

これを実際のデータを使ってやりたいと思います。

 

import numpy as np
import tensorflow as tf


def weight(shape = []):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)

def bias(dtype = tf.float32, shape = []):
initial = tf.zeros(shape, dtype = dtype)
return tf.Variable(initial)

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

Q = 4
P = 4
R = 3

sess = tf.InteractiveSession()

X = tf.placeholder(dtype = tf.float32, shape = [None, Q])
t = tf.placeholder(dtype = tf.float32, shape = [None, R])

W1 = weight(shape = [Q, P])
b1 = bias(shape = [P])
f1 = tf.matmul(X, W1) + b1
sigm = tf.nn.sigmoid(f1)

W2 = weight(shape = [P, R])
b2 = bias(shape = [R])
f2 = tf.matmul(sigm, W2) + b2
f = tf.nn.softmax(f2)

loss = loss(t, f)

optimizer = tf.train.GradientDescentOptimizer(learning_rate = 0.001)
train_step = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess.run(init) ## TensorFlowの世界を初期化(必ず必要)


from sklearn import datasets
iris = datasets.load_iris()
train_x = iris.data
train_t = iris.target
train_t = np.eye(3)[train_t]


num_epoch = 10000
for epoch in range(num_epoch):
sess.run(train_step, feed_dict = {X: train_x, t: train_t})
if epoch % 100 == 0:
train_loss = sess.run(loss, feed_dict = {X: train_x, t: train_t})
print('epoch : {} loss:{}'.format(epoch, train_loss))

 

追加したのは2箇所。1つ目がデータを持ってくるところです。

 

one-hot表現

 

iris.dataには4個の特徴を格納した4次元ベクトルが150個入っています。

iris.data.shape=(150,4)

 

iris.targetには0,1,2のいずれかの値が格納された1次元配列で

iris.target.shape=(150,)

 

4次元のデータを受け取った3クラス分類問題のデータセットとなっています。

 

学習データの方はこれで良いのですが、教師データの方をone-hot表現に変えてやらねばなりません。one-hot表現というのは、クラスiに属するならi番目の要素が1となり、それ以外が0となるベクトル表現です。例えば

 

(0,1,0,2,1)

 

に対して以下の表現

 
 \left( \begin{array}{c} 1 0 0 \\0 1 0 \\1 0 0 \\0 0 1  \\0 1 0 \end{array} \right)

 

がone-hot表現であり、こちらに変更してやらなければいけません。そのコードが以下の部分であり、np.eye(3)の引数「3」がここではクラス数に相当します。

train_t = np.eye(3)[train_t]

別にどのような方法でもone-hotにできれば良いのですが、手元のデータが必ずしもone-hotになっていないケースが多々あると思うので、この書き方を覚えておくと楽でしょう。

 

 

lossを見張る

以下のコードではepoch(学習)が100回行われる毎にlossを表示するようにしています。

    if epoch % 100 == 0:
train_loss = sess.run(loss, feed_dict = {X: train_x, t: train_t})
print('epoch : {} loss:{} '.format(epoch, train_loss))

lossを算出する写像というのは既に定義してあるわけですから、sess.runでそれを呼び起こすだけです。あとはそれを表示するコードをPythonで書いてあげれば良いわけです。

 

他にも表示したいものがあれば、同じようにsess.runで写像を呼び出してあげればいいです。例えば、ニューラルネットの出力が確率になっているわけですから、それを見たければ、

 

sess.run(f, feed_dict = {X: train_x, t: train_t})

 

で取り出して表示してあげればいいということです。

 

最低限lossは見張るようにしたほうが良いでしょう。なにせニューラルネットワークはこれを最小化するために学習を行っているのですから。これが中々下がらないようであれば、何かがおかしいと思ったほうが良いです(ニューラルネットの構造がおかしいか、コードがおかしいか)。

 

 

 

最後に

今回のコードはコピペでもすぐに動かすことができますが、やはり自分で一度書いてみることをお勧めします。

 

極力、ニューラルネットワークのこと自体も、そしてTensorFlowのコードを書く際の考えも伝えていくつもりですが、やはり最終的には手で書いて慣れるより他ありません。もしももっと合う考え方を持っているのであれば、その自分なりの心構えが一番です。

 

Accuracyも見張りたい

ニューラルネットはlossを下げるために学習を行いますが、人間としてはAccuracyを向上させることが目的になってくるでしょう。そうであればそれを学習中に見張りたいのが心情です。

lossを計算する方法を私達は以下で予め定義しておきました。

 

def loss(t, f):
cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(f)))
return cross_entropy

 

Accuracy自体は学習中はニューラルネットにとっては興味のないもので、一切考慮されていません(人間がlossを下げればAccuracyが上がると考えて学習させているに過ぎない)。

 

TensorFlowにAccuracyの計算も行って欲しければ

 

def accuracy(t, f):
correct_prediction = tf.equal(tf.argmax(t, 1), tf.argmax(f, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
return accuracy

 

という関数をPythonで書いておき、これをTensorFlowの世界に渡してしまえばいいでしょう。引数は教師データとニューラルネットの予測です。関数内の一行目は、教師データとニューラルネットの出力が一致していればTrueを、していなければFalseを返します。

 

二行目で合っていたTrueを1にFalseを0に変換し、平均を取ります。なぜ平均かと言うと、データは一度に複数個入れるわけですから、1の数を集計し、データ数で割ればAccuracyが出るという仕組みです。これを表示する方法は「loss」をTensorFlowの世界で写像として定義してあげて、学習中に表示する仕組みと同じなので、練習で実装してみてください。

 

また、学習中に学習データ以外を流して、汎化性能を見張ることも通常は必要です(これも今回の知識だけでできます。学習データとは異なるデータを流してlossあるいはaccuracyを表示すればいいだけです。学習はあくまでtrain_stepという写像で定義しているので、これを使わない限り学習は行われません)。

 

TensorFlowの世界に定義した写像を消す(重要)

やることが終わったら以下のコマンドを実行して、TensorFlowの世界に渡した写像を消してあげてください。そうでなければTensorFlowに定義された写像がメモリを専有し続けます。いつでも計算が実行できる準備をしているのです。

 

sess.close()

 

通常はエディターの最後にこれを入れてあげればいいというだけですが、例えば、Pythonのインタラクティブモードで色々やっていて、使っていないネットワークのためにドンドンメモリが足りなくなるということが無いように。

 

これはPythonで与えられているwith構文を使うことで、with内の処理が終わったら自動でsess.closeをしてくれるようになります。

 

with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init) ## TensorFlowの世界を初期化(必ず必要)


from sklearn import datasets
iris = datasets.load_iris()
train_x = iris.data
train_t = iris.target
train_t = np.eye(3)[train_t]


num_epoch = 10000
for epoch in range(num_epoch):
sess.run(train_step, feed_dict = {X: train_x, t: train_t})
if epoch % 100 == 0:
train_loss = sess.run(loss, feed_dict = {X: train_x, t: train_t})
train_acc = sess.run(acc, feed_dict = {X: train_x, t: train_t})
print('epoch{} \n loss:{} \n acc:{}'.format(epoch, train_loss, train_acc))

 

tensor flowの書き方

インデントはスペース2つ

TensorFlowではインデントをスペース2つに設定することを推奨しています。with構文にしてもfor文にしても、どんどんインデントが重なるようになって横に広がるようになってくるため、省スペース化のためにそうしているようです

 

まあこれは好みなのでどちらでも良いでしょう。私はインデントはスペース4つの方が見やすいと思うタイプですが、実際横への広がりがきついなと思うケースは多いです(もちろん改行してもいいが)。

 

クラス化について

Pythonを使うんだからクラス化したいと思うかもしれませんが、よほど素晴らしいニューラルネットワークができたのでない限り必要ないと思います。クラス化は再利用性が高いのですが、再利用しない状況、例えば実験段階で色々いじることは難しくなると思います。

 

Web上に公開されている(例えばgithub)TensorFlowのコードはクラス化されているものが多いですが、それはそれくらい良いネットワークだからです。この構造は素晴らしいものであって、使い回していけるくらいだからです。

 

むしろよく扱うデータや前処理の方法をクラス化して上げるほうが大事だと思います。例えばone-hotの表現を操作してくれたり、前処理として正規化や白色化をしたい場合はデータをクラス化してメソッドとしてやってくれるようにしたほうが便利でしょう。

 

何よりまずはTensorFlowのAPIを使いこなせるようになるのが先です。

 

TensorFlowAPIは非常に充実している

実はAccuracyの計算だってネットワークの構成だってもっと楽に書ける方法はあります。例えばKerasもTensorFlowのモジュールとして吸収されました。他にもたくさんのモジュールが存在します。

 

s0sem0y.hatenablog.com

 

 

しかし、生TensorFlowはそれらを使いこなす上でも、やっておいて損は無いと思います。何よりもニューラルネットのことをかなり意識して書くことができます(逆にそれが難しいのかもしれませんが)。また、生TensorFlowでもAccuracyやlossの計算などは提供されています(今回は自分で書きましたが)。

 

 

 次は学習において最もスタンダードに用いられているミニバッチ学習の実装の仕方について触れていきます。

s0sem0y.hatenablog.com

 

ニューラルネットに関する 関連記事

 

s0sem0y.hatenablog.com

s0sem0y.hatenablog.com

s0sem0y.hatenablog.com

 

前回記事

s0sem0y.hatenablog.com