アラカン"BOKU"のITな日常

文系システムエンジニアの”BOKU”が勉強したこと、経験したこと、日々思うことを書いてます。

シンプルなニューラルネットワークでテキストデータを学習する:Tensorflow入門の入門3/文系向

今回は、1層のみのシンプルなニューラルネットワークを、TenosroFlowで書いて動かします。

 

使うデータは、シンプルなCSV形式のテキストデータです。

 

今回は、主目的がネットワークを組むことなので、データはファイルから読み込むのではなく、ソースに直接データを書く簡易的なやり方でやります。

 

2017/5/30追加

CSVデータをファイルから読み込むように変更する記事はこちらです。

arakan-pgm-ai.hatenablog.com

さらに、CSVデータをファイルから読み込む部分を関数化して、学習用とテスト用で汎用的に使えるようにした記事はこちらに書いてます。

arakan-pgm-ai.hatenablog.com

 

参考にするのは、本家のチュートリアルMNIST For ML Beginners  |  TensorFlow」ですが、そのまま動かすだけだと面白くないので、データは手作りのテキストデータに変更してやってみます。

 

説明には、TensorFlowの専門用語をつかってますので、よくわからない場合は、前に説明した記事があるので、そちらを読んでからすすめてもらえればと思います。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

 

データの準備 

必要なのは以下の4つです。

 

  • 学習またはテスト用データ本体(data)
  • 正解ラベル(label)
  • ウエイト(weight)
  • バイアス(bias)

 

その使い方をイメージとして書いてみます。

  1. 計算結果 = データ本体 * ウエイト + バイアス
  2. 正解である確率(y) = SOFTMAX関数(計算結果)
  3. 損失率(cross_entropy) = 正解ラベル(label)と正解である確率ラベルから計算。
  4. ウエイト = 損失率が最小になるようにウエイトを調整 

 

ざっくり書くなら、1から4迄を繰り返すのが学習で、1と2だけで結果を出して、正解と比較しているのがテストって感じです。

 

データの準備する部分をコードで書いてみます。

data = tf.placeholder(dtype=tf.float32,shape=[None,5])

label = tf.placeholder(dtype=tf.float32,shape=[None,2])

weight = tf.Variable(tf.zeros([5,2],dtype=tf.float32))

bias = tf.Variable(tf.zeros([2],dtype=tf.float32))

 

手でデータを作るので、データが5列、正解ラベルは2種類 の非常に小さいデータにしました。

 

こんなんでちゃんと学習できるか不安ですが、まあ、やってみます。

 

data([None,5])、weight([5,2])、bias([2]) は行列計算できるように行と列の数をあわしています。(参考:行列の相等,和.差,実数倍行列の積

 

 

計算して正解である確率(Y)を求める

前に整理した1と2の両方をあわせた部分になりますが、コードで書くとたった1行です。

y = tf.nn.softmax(tf.matmul(data,weight) + bias)

 

tf.matmul()は、行列の掛け算(積を求める)関数なので、この部分は行列に対して「data*weight+bias」をしているだけです。

 

それを「softmax関数」で、正解である確率(y)を求めています。

 

出力例は、例えばこんな感じです。

f:id:arakan_no_boku:20170506223943j:plain

 

上記は、今回のように選択肢が2つの場合ですが、1番目である確率が約85.5%、2番目である確率が約14.5%・・と読み替えてみれば、感覚的にはあってるみたいです。

 

選択肢が何個あったとしても、合計が必ず1.0(つまり、100%)になるようにソフトマックス関数が調整してくれるところがミソで、だから確率と同じように使えるわけです。

 

なぜ、そうできるのか?という、ソフトマックス関数のアルゴリズムにはふれません。興味のある方は、こちらをどうぞ。

 

損失率(cross_entropy)を計算する

前提として、正解ラベルが、選択肢の中で正解であるインデックスのみ「1」、残りは「0」がセットされた形(one-hot表現と言います)で与えられている必要があります。

 

例えば、今回のように選択肢が2つで、最初のインデックスが正解なら、[1,0]となっているということですね。

 

損失率を求める方法にも何通りかあるのですが、ソフトマックス関数と組み合わせるのは、だいたい「交差エントロピー誤差」という方法です。

 

これも、コードで書くと1行です。

cross_entropy = tf.reduce_mean(-tf.reduce_sum(label * tf.log(y), reduction_indices=[1]))

 

上記の、tf.resuce_mean()は平均値を計算する関数 、tf.resuce_sum()は総和を計算する関数、tf.log()は、eを底にする自然対数を計算する関数です。

 

以下が交差エントロピーを計算する式ですが、yが計算でもとめた正解確率で、y'が正解ラベルだと読み替えてみれば、上記は、ほぼ計算式をそのまま実装しているだけなのがわかると思います。

f:id:arakan_no_boku:20170507001717j:plain 

 

ここまで見て、なんで、この式でうまくいくのか?と不思議に思う人も、文系ならいると思います。

 

自分は思いました。で、調べてみました。

 

ご存知の通り、ニューラルネットワークは何階層も連なる深いネットワークになるので、学習するためには、最終出力結果と正解ラベルの誤差を、その各階層にできるだけ正確に遡って伝える必要があります。(逆伝播といいます)

 

その逆伝播は、各階層で微分をとりながら伝わるので、最終のところで普通に誤差を計算しただけでは、逆伝播の過程で形が変わってしまうので意味がないわけです。

 

そこで、逆伝播していった最終段階で、正確に最終出力結果と正解ラベルの誤差を反映する式に収束するように計算式が設計されている必要があります。

 

その答えが、ソフトマックス関数と誤差エントロピー誤差の組み合わせだったみたいなのです。

 

この式の展開と証明は、相当に複雑です。

 

なので、形として、こういう風に使えば正確に誤差が伝わるのだと理解しとくのが、文系人間にとっては無難かな・・と思います。

 

損失率が最小になるようウエイトを調整(=学習)

 

損失率を求めたら、それが最小になるようにウエイトのパラメータ値を調整していきます。

 

このウエイト値の調整のことを、学習と呼ぶわけですね。

 

ここも、コードで書くと1行です。

train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)

 

 解説すると、さっき求めた損失率(cross_entropy)が最小になる方向(minimize)を、GradientDescentOptimizerは「勾配降下法」というアルゴリズムを使って求めて、0.1(これを学習率といいます)だけ、ウエイトを減らしたり、増やしたりして調整するということをやりますよということです。 

 

ポイントは、ウエイトを増やすにせよ、減らすにせよ、1回では学習率で指定した分だけしか動かないということです。

 

だから、一発で最適値になることはなくて、何回も繰り返し学習をすることで、だんだん正解に近づいていくということなんですね。

 

学習の実行

ここまでは定義しているだけなので、学習を実行してみます。

 

学習を実行する部分のコードです。

with tf.Session() as s:
    s.run(tf.global_variables_initializer())
    for i in range(5):
          s.run(train_step,feed_dict={data:[[1.2,2.3,5.4,2.4,1.6],[9.2,8.3,7.4,6.5,5.6]],label:[[1.,0.],[0.,1.]]}) 

 

学習用のデータは2件(超少ないですが)ダイレクトに入力してます。

 

「[1.2,2.3,5.4,2.4,1.6]」のデータは、「[1.,0.]」なので、インデックス1が正解、「[9.2,8.3,7.4,6.5,5.6]」は逆に「[0.,1.]」なので、インデックス2が正解というものです。

 

その同じデータをfor分で5回繰り返して学習を実行させてます。

 

学習後のウエイトの値はこんな感じです。

最初は全部「0」で初期化したので、たしかにウエイトが更新されてますね。

f:id:arakan_no_boku:20170507111241j:plain

 

ところで、上記のs.runの行を見て、不思議に思いませんでしたか?

 

s.run(train_step,feed_dict={ ・・・} って、引数にtrain_stepしか指定していませんし、そもそも、feed_dictで値を与えているdataとlabelというプレースホルダーは、train_stepの定義式の中のどこにも書いてありませんから。

 

実際、前に自分が混乱したのもここでした。

 

だって、普通のプログラミング言語の感覚だとありえないですから。

 

tensorflowでは、指定されたオペレーション(この例では train_step)を実行すると、そのオペレーション以下で関連が定義されているすべてを実行する仕組みになっているんですね。 

 

今回の場合だと、

s.run(train_step,feed_dict={data:[[1.2,2.3,5.4,2.4,1.6],[9.2,8.3,7.4,6.5,5.6]],label:[[1.,0.],[0.,1.]]})

 の1行で、以下のすべてが与えられたdataとlabelの値を使って実行されているわけです。

bias = tf.Variable(tf.zeros([2],dtype=tf.float32))
weight = tf.Variable(tf.zeros([5,2],dtype=tf.float32))
data = tf.placeholder(dtype=tf.float32,shape=[None,5])
label = tf.placeholder(dtype=tf.float32,shape=[None,2])
y = tf.nn.softmax(tf.matmul(data,weight) + bias)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(label * tf.log(y), reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)

 

これがわかって初めて、Tensorflowが、Graph(グラフ)構造の各ノードのことを、「Operation(オペレーション)」と呼んでいる理由が腑に落ちました。

 

すごい、設計です。さすが、Googleです。

 

学習結果を確認する

せっかく、学習したのでテストをしてみます。

 

定義する部分のコードです。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(label,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

 

 実行する部分のコードです。

acc = s.run(accuracy, feed_dict={data:[[3.2,4.5,6.4,4.4,3.6],[6.2,5.3,4.4,3.5,2.6]],label:[[1.,0.],[0.,1.]]})
print("結果:{:.2f}%".format(acc * 100))

 

オペレーション"accuracy"を実行するだけで、feed_dictで与えたテスト用のdataとlabel、それと学習済のweightとbiasを使って、関連づいてぶらさがっている以下をまとめて実行してくれるわけですね。

y = tf.nn.softmax(tf.matmul(data,weight) + bias)
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(label,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))

 

tf.argmax()は、引数で与えられた中で最も大きい数字のインデックスを返すので、例えば[0.85,0.15]と[1.,0.]なら、両方とも最初のインデックス番号が返されるので、tf.equal()の結果はTrueになるわけです。

 

ただ判定した結果「correct_prediction」には、TrueかFalseの論理値がはいっているので、平均を計算するために、tf.castで論理値を"float"にキャストして計算しているわけです。

 

結果は、1.0以下の数字で返ってくるので、100倍してパーセントっぽく整形して表示します。

print("結果:{:.2f}%".format(acc * 100)) 

 

実行してみた結果です。

f:id:arakan_no_boku:20170507175435j:plain

 

おお!学習できてる。たった2件の学習データなのに。

 

ちなみに学習データとテストデータの違いをちょっと補足しますね。

 

たぶん、データを見ただけだとピンと来ないと思うので、グラフにしてみます。

 

こちらが学習用データです。

f:id:arakan_no_boku:20170507180706j:plain

 

そしてテスト用データです。

f:id:arakan_no_boku:20170507180759j:plain

 

グラフの形は、ほぼ同じにしてますが、構成する数字は上下にずらしているので全く違います。

 

なので、通常のプログラムのアプローチで一致判断して分類しようとすると、かなり面倒くさいロジックを組まないといけないと思うんですけどね。

 

まあ、最低限のデータ・最低限のネットワークしかやってない結果としては上出来なんじゃないかなと、思います。

 

次は、データを増やしてファイルから読み込めるようにしてみて、どうかってとこですね。

 

最後に今回のソース全体を掲載しておきます。

import tensorflow as tf

bias = tf.Variable(tf.zeros([2],dtype=tf.float32))
weight = tf.Variable(tf.zeros([5,2],dtype=tf.float32))
data = tf.placeholder(dtype=tf.float32,shape=[None,5])
label = tf.placeholder(dtype=tf.float32,shape=[None,2])
y = tf.nn.softmax(tf.matmul(data,weight) + bias)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(label * tf.log(y), reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(label,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
with tf.Session() as s:
    s.run(tf.global_variables_initializer())
    for i in range(5):
        s.run(train_step,feed_dict={data:[[1.2,2.3,5.4,2.4,1.6],[9.2,8.3,7.4,6.5,5.6]],label:[[1.,0.],[0.,1.]]})
    acc = s.run(accuracy, feed_dict={data:[[3.2,4.5,6.4,4.4,3.6],[6.2,5.3,4.4,3.5,2.6]],label:[[1.,0.],[0.,1.]]})
    print("結果:{:.2f}%".format(acc * 100))

2017/12/09追記

いちおう、tensorflow v1.4で動作確認しました。

 

2018/02/12追記

tensorflow v1.5で動作確認しました。 

 


Tensorflow入門の入門カテゴリの記事一覧はこちらです。

arakan-pgm-ai.hatenablog.com

 

f:id:arakan_no_boku:20170404211107j:plain