#0011

脱初心者!MNIST beginnerに隠れ層を加えたニューラルネット解説

2018-05-02 16:49 2018-05-29 12:03 "ゆざ"

ゆざ(@yuzajo_plan)です。

MNIST実行環境の準備から手書き文字識別までを徹底解説」では、TensorFlowのチュートリアルであるMNIST beginnerを使用して、実際の手書き文字識別を行うプログラムを解説しました。

今回は、そのモデルを少し応用して、隠れ層と活性化関数を加えたニューラルネットワークで手書き文字識別を行っていきます。

知識ゼロで機械学習・AIを理解するために必要なニューラルネットワークの基礎知識」で解説したモデルをそのままの形で実装していくので、これで基本的なニューラルネットワークをマスターすることができると思います。

スポンサーリンク

今回の解説から学べること

MNIST実行環境の準備から手書き文字識別までを徹底解説」で解説したMNIST beginnerは、下の図のような隠れ層と活性化関数がなく、入力層から処理をされ、直接出力層につながるモデルでした。

MNIST beginnerのモデル

このモデルでも手書き文字の簡単な識別を行うには十分でしたが、もっと複雑なデータや画像を予測するためにはやはり隠れ層と活性化関数は必要不可欠です。

そのため、今回は、MNIST beginnerに隠れ層と活性化関数を加えた3層構造のニューラルネットワークのモデルを使用することで、どのような違いが生まれるのか、またはどのように3層構造をプログラムで実現するのかを学べると思います。

また、AnacondaでのTensorFlow環境構築と基礎的な使い方を先に読んでおくと理解が早いかもしれません。

それでは、さっそく見ていきましょう。

隠れ層と活性化関数を追加したモデル

今回、使用するニューラルネットワークは、「知識ゼロで機械学習・AIを理解するために必要なニューラルネットワークの基礎知識」で解説した入力層・隠れ層・出力層の3層からなるモデルです。

入力層には、28*28pixelの手書き文字データをピクセルごとにバラバラに分けた計784コのデータが入ります。

そのデータは、それぞれ隠れ層のノード(ニューロン)に入っていき、そこに入ってきた値に重みWをかけ、それらを全て足し合わせ、バイアスbを足した値を隠れ層の値hとして計算します。

そして、hを活性化関数であるシグモイド関数に代入することで隠れ層での処理が終わります。

隠れ層から出力層の処理には、入力層から隠れ層の処理と同様に行われ、最後に出力する値を0-9の確率として出すためにソフトマックス関数に代入して、予測値を算出します。

この予測値と正解ラベルとの誤差を小さくするために、誤差逆伝播法を用いて重みW,バイアスbを更新していきます。

モデルの学習結果

それでは、実際に隠れ層を含む3層のモデルを用いた学習結果を見てもらいましょう。

縦軸は、トレーニング時の予測精度を表し、横軸は試行回数を表しています。

そして、次の図がトレーニング時の誤差を表しています。

前回の2層構造のモデルを使用した結果と比較しても一見あまり違いがみられません。それは、前回のモデルでも十分な精度を実現することができていたからです。

しかし、前回との違いが2点あります。

  • 処理時間
  • 30,000回トレーニングをした後の予測精度

この2点です。まずは、処理時間です。

前回は、30,000回のトレーニングを終えるまでに約44秒かかったのですが、今回は、すべて終えるまで8分23秒かかりました。(使用したスペックは、MacBook Air (13-inch, Early 2014))

2層と3層で約11倍の処理時間の違いが生じました。これは、ひとえに計算量の違いによるものです。

2層の場合は、予測値を算出する際は行列計算を1回のみ行い、W,bを更新する際も1層分のみの更新(微分計算のこと:「知識ゼロで機械学習・AIを理解するために必要なニューラルネットワークの基礎知識」参照)を行うだけでした。

ですが、3層では予測値を算出する際は行列計算を2回とその間に活性化関数での処理行い、さらに最も計算の負荷が大きいのはW,bの更新で、2層分での微分計算を行う必要があり、ただ2層だから処理も倍ではなく、層が増える毎に指数演算的に増えていくのです。

これが、処理時間の違いが生じる理由であり、10層・100層と階層があるディープラーニングがスペックの高いGPUを用いて処理を行わせる理由です。近年のAIブームも、スペックの高いPCが誕生したことが大きな要因の1つです。

これでは、層を増やすメリットが今の所ありませんよね。

そこで、2つめの違いである30,000回トレーニングをした後の予測精度です。

これが、実際のテスト用データセットを使用して検証した予測精度です。

前回の2層の構造での結果がコチラです。

これで、なぜ処理時間が長くなってしまうのに隠れ層を増やすメリットがあるのかお分かりいただけたのではないでしょうか。

トレーニング時・テスト時ともに隠れ層ありの3像モデルのほうが高い予測成功率を出すことができたのです。

  処理時間 トレーニング時の予測成功確率 テスト時の予測成功確率
2層モデル/隠れ層なし 0:44 94.0% 92.2%
3層モデル/隠れ層あり 8:23 97.0% 96.6%
隠れ層と活性化関数が必要な理由

隠れ層と活性化関数が必要な理由は、処理時間は長くなりますがより高い予測精度を叩き出すことができるからです。

下の図を見てください。入力層と出力層のみのモデルで表せる領域には限界があり、本当にほしいモデル、今の場合は、テストデータに対しても100回中100回予測を成功させるモデルです。

これを実現するには、より階層を深くしたり、ノード数を変更する必要があります。そのことにより、よりニューラルネットワークで表現できる領域が広くなり、モデルの精度を高めることができます。

活性化関数が必要な理由も同様で、活性化関数がないと、y=Wx+bの線形表現のみしかできず、表現できるモデルの範囲が狭くなってしまうからです。

今回のケースにおいては、隠れ層のない2層のモデルでも高い精度で予測を行えたため、そこまでの恩恵は受けませんでした。

しかし、これがより複雑なデータ予測、例えば、自動運転や自動翻訳システムの開発には2層では全く太刀打ちできません。

もちろん、隠れ層の3層であれば、自動翻訳ができるモデルが作れるわけではないし、層を増やせば全ての複雑な予測ができるというわけではないですが、より難しくて複雑性の高い予測においてはこの隠れ層がとても大きな役割を果たすのです。

過学習には注意!

ココでニューラルネットワークを扱う上で、注意して置かなければいけないことがあります。

それは、とにかく試行回数を多く学習させれば、どんどん予測の精度が上がっていくというわけでもないということです。

例えば、今回、バッチを試行回数30,000回で学習を行いましたが、その100倍の300万回の試行を行えばもっと精度の高いモデルができるというわけではありません。

確かにトレーニング用のデータでは、精度が高くなっていくのですが、逆にトレーニング用のデータに特化し過ぎたモデルになってしまい、トレーニングに使用していないテスト用のデータに対しては正しい予測ができなくなってしまうのです。

これを過学習といいます。わかりやすい例としては、正面を向いたある人の顔の写真のみを使ってめちゃくちゃ学習させたモデルがあるとして、そのモデルに少しでも横を向いた写真をインプットすると別人と判断されてしまうのです。

実際に使用したコード

では、次に実際に隠れ層ありの3層ニューラルネットワークを実装したプログラムを解説していきます。

以下のプログラムが、全体のコードです。

# ===========import===========
import tensorflow as tf
import os
from tensorflow.examples.tutorials.mnist import input_data
# GPUの無効化
os.environ['CUDA_VISIBLE_DEVICES'] = ""

def main():
	# ===========init===========
	image_size = 28*28
	output_num = 10
	learning_rate = 0.001
	loop_num = 30000
	batch_size = 100
	hidden_size = 625

	mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

	# ===========model===========
	with tf.device("/cpu:0"):
		x = tf.placeholder(tf.float32, [None, image_size])
		y_ = tf.placeholder(tf.float32, [None, output_num])

		# ⬇ココだけ違う!(隠れ層を追加)
		# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
		W1 = tf.Variable(tf.random_normal([image_size, hidden_size], mean=0.0, stddev=0.05))
		b1 = tf.Variable(tf.zeros([hidden_size]))
		h = tf.sigmoid(tf.matmul(x, W1) + b1)

		W2 = tf.Variable(tf.random_normal([hidden_size, output_num], mean=0.0, stddev=0.05))
		b2 = tf.Variable(tf.zeros([output_num]))
		y = tf.nn.softmax(tf.matmul(h, W2) + b2)
		# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

		cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
		train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)
		correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
		accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

		init = tf.global_variables_initializer()
		sess = tf.InteractiveSession()
		sess.run(init)

		# ===========training===========
		for i in range(loop_num):
			batch_xs, batch_ys = mnist.train.next_batch(batch_size)
			sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

			if i % 100 == 0:
				print("step", i, "train_accuracy:", sess.run(accuracy, feed_dict={x: batch_xs, y_: batch_ys}))

		# ===========test===========
		print("test_accuracy:", sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

if __name__ == "__main__":
	main()

前回の記事を読んでいただいた人には、もうお分かりではないでしょうか。

前回、使用したMNIST beginnerのコードとの違いは破線で囲まれたたった6行だけです。

TensorFlowを使う場合は、層を増やすことはすごく簡単にできます。

まずは、変更する前の前回のコードを見ていきましょう。

# 前回のMNIST beginnerでのコード
W = tf.Variable(tf.zeros([image_size, output_num]))
b = tf.Variable(tf.zeros([output_num]))
y = tf.nn.softmax(tf.matmul(x, W) + b)

image_size=28*28, output_num=10と定義しています。

よって、ノード間の繋がりの強さである重みWは、28*28個の入力層ノードと10個の出力層ノードの間にあるので、合計で重みWは(28*28)*10個必要です。そのため、Wは、(28*28)*10のゼロの入った行列として初期値を定義されています。

バイアスbは、出力層の数だけ必要なので、10個です。そして、予測値yは、入力した値xとWを掛け、全てを足したものにbを足して計算されます。

次に、隠れ層を追加したコードを見ていきましょう。

# ⬇ココだけ違う!(隠れ層を追加)
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
W1 = tf.Variable(tf.random_normal([image_size, hidden_size], mean=0.0, stddev=0.05))
b1 = tf.Variable(tf.zeros([hidden_size]))
h = tf.sigmoid(tf.matmul(x, W1) + b1)

W2 = tf.Variable(tf.random_normal([hidden_size, output_num], mean=0.0, stddev=0.05))
b2 = tf.Variable(tf.zeros([output_num]))
y = tf.nn.softmax(tf.matmul(h, W2) + b2)
# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

hidden_size=625としているので、隠れ層のノード数は625個です。これは任意に変更してOKです。

よって、重みW1は、28*28個の入力層ノードと625個の隠れ層ノードの間にあるので、合計で重みW1は(28*28)*625個必要です。そのため、W1は、(28*28)*625の行列として初期値を定義されています。

配列の中には、重みにばらつきを生じされて線形性をなくすために0から1の数字がランダムに入っています。

バイアスb1は、隠れ層の数だけ必要なので、625個です。そして、hは、入力した値xとW1を掛けて、全てを足したものにb1を足してた値をシグモイド関数に代入して計算します。

そして、このhを次は先程のxのように扱います。重みW2は、625個の隠れ層ノードと10個の出力層ノードの間にあるので、625*10の行列として定義されます。同様にb2は、10個です。

よって、yは、hとW2を掛けて、全てを足したものにb1を足しすことで計算できます。

これで、入力層と出力層しかなかった前回のモデルを入力層と隠れ層(+活性化関数)と出力層がある3層構造のニューラルネットワークとなりました。

未経験者が苦戦しそうなところ(私がぶち当たった壁)

ここまで、いろいろ解説してきた私ですが、これらをしっかり自分の言葉で説明できるようになったのは記事を書き始めてからでした。

そんな中でプログラミング未経験の私がぶち当たった壁についてまとめておきたいと思います。

きっと、これから機械学習をやろうと思っている人やプログラミングをやろうしている人の助けになるのではないかと期待しています。

TensorFlowに慣れるのが大変

これは、はじめてプログラミングを勉強している人がTensorFlowを使って機械学習をやろうとして挫折するポイントです。

TensorFlowについては詳しくは別の記事で解説するつもりですが、ここで少しだけ触れると、例えば、Pythonで1+1を計算して「答えは2です」と表示させるには、

a = 1
b = 1
sum = a + b
print("答えは%sです。"%sum) => 答えは2です。

これだけで済みます。しかし、TensorFlowの場合は、

import tensorflow as tf
a = tf.constant(1)
b = tf.constant(1)
sum = tf.add(a, b)

init = tf.global_variables_initializer()
sess = tf.InteractiveSession()
sess.run(init)

print("答えは%sです。"%sess.run(sum)) => 答えは2です。

こんなに書かなければいけません。ただの1+1で一苦労です。

この壁を乗り越えるには、TensorFlowがどんな処理をやっているのかということを理解するのと実際の動作を一つ一つ表示させて可視化することが大切です。

最適化関数の動きがブラックボックス

ニューラルネットワークの要になる誤差逆伝播法による最適化はTensorFlowの最適化関数でびっくりするくらい簡単に実装することができます。

上手くいっている時はそれでも構わないのですが、自分が望むように学習してくれなかった時には手詰まりになってしまいます。

しかも、最適化関数(今回は「GradientDescentOptimizer」)でどんな処理が行われているかはすごく分かりづらくブラックボックスになっています。

そのため、この最適化関数はどんな動きをしているのかを原理に理解して、結果で可視化していってください。

それでも、最適化関数を変えたら上手くいったけど理屈では理解できないといったときもあります。

この「ハイパーパラメータ」は、現役のデータアナリストの人も経験による判断をすることが多いようです。

なので、深く考えすぎで泥沼にはまらないようにも心がけてください!(私はここですごく泥沼にハマりました)

やっぱり原理をしっかりと理解するまでは大変

やはり、わかったつもりでも100%原理を理解するのはとても大変です。

しかし、その地盤を固めていないとせっかく積み上げていっても崩れ落ちとします。

コレに関しては、「知識ゼロで機械学習・AIを理解するために必要なニューラルネットワークの基礎知識」にほとんど全てのエッセンスを詰め込んであると思います。

もし、少しでもココで躓いてモヤモヤした部分があるようでしたら、いつでも私(@yuzajo_plan)に連絡してください。どんなに初歩的な質問にも答えます!

ココ乗り越えれば、かなりのチカラがつくと思うので、とにかく妥協せずに頑張ってみてください。

環境設定が地味にうざい

これは、Anacondaにせよ、Pythonにせよ、TensorFlowにせよ、どこに何を入れてどの環境で何を動かすのかが難しいです。

プログラミングができる人からすれば、造作もない事かもしれないですが、プログラミング初心者にはすごく理解しにくいところです。

一番いいのは、近くのプログラミングができる人に教えてもらうことですが、なかなかそう居ないと思います。

なので、とにかく「Python インストール」等と調べて出てきたサイトの解説やコードの意味をしっかりと噛み砕いて少しずつでも理解していくしかありません。

困った人は自分で調べるか、人に聞くかの2択しかないので、出来るだけ早くどちらかの行動を実行しましょう。

サンプルコードをコピペするだけでは動かない

これは、プログラミングあるあるかもしれません。世の中には沢山のコードがネット上に転がっています。しかし、この大半はただコピペしても正常に動きません。

だから、コピペして動いてもなんで上手く動いているかの意味がわからないと次に使えるツールにはなりません

プログラミングとは、本当に勉強しなければいけないことが沢山でしかも常に新しい技術が生まれています。

だから、「自分で学び続けることができるチカラ」はとても大きな武器になるはずです。大変なことも多いですが、その先に楽しさもすごく多いです。一緒に頑張りましょう!

今回のまとめ

今回は、MNIST beginnerを応用した隠れ層と活性化関数を含むニューラルネットワークを用いたモデルについて解説しました。

まとめると、

  • 隠れ層と活性化関数を使うと、処理時間が長くなるがより精度の高い結果を得ることができる
  • コードに書く時は、重みW1,W2の配列の大きさに注意する

正直、今までの記事「プログラミング未経験の私がPythonの機械学習で手書き文字の識別を行うまで」(前半後半)を理解していれば、すんなり理解することができる内容だったと思います。

ここまで、できれば機械学習の基本を習得したと言っても過言ではありません

実際にこの技術を使って色んなことを試してみるとより今までの知識が自分の血となり肉となると思うので、頑張ってみて下さい。

この記事を書いた人

学生Webエンジニア PLANインターン生 PHP Laravel Python HTML CSS JS

【名前】 "ゆざ"

【関連】 株式会社PLAN / MIYABI Lab / Tmeet(twitterユーザーマッチングサービス) /

【MIYABI Lab運営】23歳/同期がト◯タやMicr◯softに就職する中、ベンチャーに未経験でWebエンジニアになるのを選んだ脳科学専攻の理系院生◆人見知り日本縦断◆機械学習/Web歴5ヶ月

Twitterやってます

最新の技術ブログはこちら