TensorFlow : コード解説 : 全結合モデル for MNIST
* TensorFlow : Tutorials : TensorFlow 技法 101 (翻訳/解説) に、数式は排除/コード重視の方針で詳細な解説を加筆したものです。
目的は MNIST データセットを利用した手書き数字分類のための単純な feed-forward(順伝播型)ニューラルネットワークを学習させて評価するためにどのように TensorFlow を利用するかを示すことです。
【訳注】技法 101 のモデルは、初心者向けの単層 Softmax モデルやエキスパート向けの畳み込みモデルとはまた別のものです。
このチュートリアルでは次のコードを利用して :
コード : tensorflow/examples/tutorials/mnist/
主として次のファイルを参照します :
ファイル | 目的 |
---|---|
mnist.py | このコードは全結合な MNIST モデルを構築します。 |
fully_connected_feed.py | 構築された MNIST モデルをダウンロードしたデータセットに対して feed 辞書を使って学習させるためのメイン・コードです。 |
学習を始めるには fully_connected_feed.py を直接実行するだけです :
$ python fully_connected_feed.py
データの準備
MNIST は、手書き数字のグレースケールの 28 x 28 ピクセル画像を見て画像が 0 から 9 までのどの数字を表しているかを決定する機械学習の古典的な問題です。より詳細な情報は、Yann LeCun’s MNIST または Chris Olah’s visualizations of MNIST を参照してください。
ダウンロード
run_training() の冒頭では、input_data.read_data_sets() が正しいデータがローカル学習フォルダにダウンロードされたかを保証し、データセットのインスタンスの辞書を返すためにデータを取り出します。
def run_training(): data_sets = input_data.read_data_sets(FLAGS.train_dir, FLAGS.fake_data) ... def main(_): run_training() if __name__ == '__main__': tf.app.run()
* fake_data フラグはユニットテスト目的のフラグですので無視してかまいません。
データセット | 目的 |
---|---|
data_sets.train | 55000 イメージとラベル、主な学習用。 |
data_sets.validation | 5000 イメージとラベル、学習中の正確さの反復検証のため。 |
data_sets.test | 10000 イメージとラベル、学習した正確性の最終的なテストのため。 |
MNIST データの扱いについての詳細は、MNIST データ・ダウンロード(コード解説) を参照してください。
入力とプレースホルダー
placeholder_inputs() 関数は残りのグラフに対して batch_size を含む入力の形状を定義する、2つの tf.placeholder OP を作成して返します。ここに実際の学習サンプルが供給されます。
def placeholder_inputs(batch_size): images_placeholder = tf.placeholder(tf.float32, shape=(batch_size, mnist.IMAGE_PIXELS)) labels_placeholder = tf.placeholder(tf.int32, shape=(batch_size)) return images_placeholder, labels_placeholder def run_training(): ... with tf.Graph().as_default(): images_placeholder, labels_placeholder = placeholder_inputs(FLAGS.batch_size)
学習ループの中で、全体の画像とラベルのデータセットは :
- 各ステップのために batch_size に適合するようにスライスされ、
- 上記のプレースホルダー OP に合わせられ、
- そして feed_dict パラメータで sess.run() 関数の中へと渡されます。
グラフの構築
プレースホルダーを作成した後は、3 ステージ : inference()、loss() そして training() に沿って mnisty.py からグラフが構築されます。
- inference() – 推論処理。予測のためのネットワークを前方向に実行するために必要な推論グラフを構築する。
- loss() – 損失処理。推論グラフに損失を生成するために必要な OP を追加する。
- training() – 訓練処理。勾配を計算して適用するために必要な OP を損失グラフに追加する。
Inference(推論)
inference() 関数は、プレースホルダー作成後、run_training から呼び出されます。
def run_training(): data_sets = input_data.read_data_sets(FLAGS.train_dir, FLAGS.fake_data) with tf.Graph().as_default(): images_placeholder, labels_placeholder = placeholder_inputs( FLAGS.batch_size) logits = mnist.inference(images_placeholder, FLAGS.hidden1, FLAGS.hidden2) ...
inference() 関数はグラフを構築して出力予測を含むテンソル logits を返します。
画像プレースフォルダーを入力として取り、そのトップに ReLU 活性化関数とともに全結合層のペアを、続いて出力ロジット(対数オッズ)を明示する 10 ノードの線形層を構築します。
def inference(images, hidden1_units, hidden2_units): """Build the MNIST model up to where it may be used for inference. Args: images: Images placeholder, from inputs(). hidden1_units: Size of the first hidden layer. hidden2_units: Size of the second hidden layer. Returns: softmax_linear: Output tensor with the computed logits. """ # Hidden 1 with tf.name_scope('hidden1'): weights = tf.Variable( tf.truncated_normal([IMAGE_PIXELS, hidden1_units], stddev=1.0 / math.sqrt(float(IMAGE_PIXELS))), name='weights') biases = tf.Variable(tf.zeros([hidden1_units]), name='biases') hidden1 = tf.nn.relu(tf.matmul(images, weights) + biases) # Hidden 2 with tf.name_scope('hidden2'): weights = tf.Variable( tf.truncated_normal([hidden1_units, hidden2_units], stddev=1.0 / math.sqrt(float(hidden1_units))), name='weights') biases = tf.Variable(tf.zeros([hidden2_units]), name='biases') hidden2 = tf.nn.relu(tf.matmul(hidden1, weights) + biases) # Linear with tf.name_scope('softmax_linear'): weights = tf.Variable( tf.truncated_normal([hidden2_units, NUM_CLASSES], stddev=1.0 / math.sqrt(float(hidden2_units))), name='weights') biases = tf.Variable(tf.zeros([NUM_CLASSES]), name='biases') logits = tf.matmul(hidden2, weights) + biases return logits
各層は、scope の中で作成されたオブジェクトのプレフィックスとして一意となる作用をする、tf.name_scope の下で作成されます。
with tf.name_scope('hidden1') as scope:
定義されたスコープ内で、これらの層の個々に使用される weights(重み)と biases(バイアス)は望む形状で tf.Variable インスタンス内に生成されます :
weights = tf.Variable( tf.truncated_normal([IMAGE_PIXELS, hidden1_units], stddev=1.0 / math.sqrt(float(IMAGE_PIXELS))), name='weights') biases = tf.Variable(tf.zeros([hidden1_units]), name='biases')
例えば、これらが hidden1 スコープの下で作成された場合には weights 変数にはユニークな名前 “hidden1/weights” が与えられることになります。
各変数は initializer OP にそのコンストラクションの一部として与えられます。
この最も一般的なケースにおいて、weights は tf.truncated_normal で初期化され、2 次元テンソルの形状が与えられます。最初の次元は weights の接続元の層のユニット数を表し、2つ目は weights の接続先の層のユニット数を表します。hidden1 と命名された最初の層では、weights は画像入力を hidden1 層に接続していますから、次元は [IMAGE_PIXELS, hidden1_units] になります。
tf.truncated_normal initializer は与えられた平均値と標準偏差でランダム分布を生成します。
それから biases は全てゼロ値で始まることを保証するために tf.zeros で初期化されます。ここでの形状は単に接続先の層のユニット数です。
それからグラフの3つの主要な OP — 隠れ層の tf.matmul をラッピングした2つの tf.nn.relu OP とロジットのための1つの特別な tf.matmul OP — が、それぞれ順番に、入力プレースホルダーか前の層の出力テンソルのそれぞれに接続された別々の tf.Variable インスタンスで作成されます。
hidden1 = tf.nn.relu(tf.matmul(images, weights) + biases) hidden2 = tf.nn.relu(tf.matmul(hidden1, weights) + biases) logits = tf.matmul(hidden2, weights) + biases
最終的に、出力を含むロジット・テンソルが返されます。
Loss(損失)
loss() 関数は必要な loss(損失) OP を追加することでグラフを更に構築していきます。
with tf.Graph().as_default(): # Generate placeholders for the images and labels. images_placeholder, labels_placeholder = placeholder_inputs( FLAGS.batch_size) # Build a Graph that computes predictions from the inference model. logits = mnist.inference(images_placeholder, FLAGS.hidden1, FLAGS.hidden2) # Add to the Graph the Ops for loss calculation. loss = mnist.loss(logits, labels_placeholder)
def loss(logits, labels): """Calculates the loss from the logits and the labels. Args: logits: Logits tensor, float - [batch_size, NUM_CLASSES]. labels: Labels tensor, int32 - [batch_size]. Returns: loss: Loss tensor of type float. """ # Convert from sparse integer labels in the range [0, NUM_CLASSES) # to 1-hot dense float vectors (that is we will have batch_size vectors, # each with NUM_CLASSES values, all of which are 0.0 except there will # be a 1.0 in the entry corresponding to the label). batch_size = tf.size(labels) labels = tf.expand_dims(labels, 1) indices = tf.expand_dims(tf.range(0, batch_size), 1) concated = tf.concat(1, [indices, labels]) onehot_labels = tf.sparse_to_dense( concated, tf.pack([batch_size, NUM_CLASSES]), 1.0, 0.0) cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits, onehot_labels, name='xentropy') loss = tf.reduce_mean(cross_entropy, name='xentropy_mean') return loss
最初に、labels_placeholder からの値 labels は 1-hot 値のテンソルとしてエンコードされます。
例えばクラス識別子が ‘3’ ならば値は次のように変換されます :
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
batch_size = tf.size(labels) labels = tf.expand_dims(labels, 1) indices = tf.expand_dims(tf.range(0, batch_size, 1), 1) concated = tf.concat(1, [indices, labels]) onehot_labels = tf.sparse_to_dense( concated, tf.pack([batch_size, NUM_CLASSES]), 1.0, 0.0)
そして tf.nn.softmax_cross_entropy_with_logits OP が inference() からの出力ロジットと 1-hot ラベルを比較するために追加されます。
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits, onehot_labels, name='xentropy')
そして損失の総計としてバッチ次元(最初の次元)に渡る交差エントロピー値の平均を取るために tf.reduce_mean を 使用します。
loss = tf.reduce_mean(cross_entropy, name='xentropy_mean')
Training(訓練)
training() 関数は勾配降下法によって損失を最小化するために必要な OP を追加します。
with tf.Graph().as_default(): images_placeholder, labels_placeholder = placeholder_inputs( FLAGS.batch_size) logits = mnist.inference(images_placeholder, FLAGS.hidden1, FLAGS.hidden2) loss = mnist.loss(logits, labels_placeholder) train_op = mnist.training(loss, FLAGS.learning_rate)
def training(loss, learning_rate): """Sets up the training Ops. Creates a summarizer to track the loss over time in TensorBoard. Creates an optimizer and applies the gradients to all trainable variables. The Op returned by this function is what must be passed to the `sess.run()` call to cause the model to train. Args: loss: Loss tensor, from loss(). learning_rate: The learning rate to use for gradient descent. Returns: train_op: The Op for training. """ # Add a scalar summary for the snapshot loss. tf.scalar_summary(loss.op.name, loss) # Create the gradient descent optimizer with the given learning rate. optimizer = tf.train.GradientDescentOptimizer(learning_rate) # Create a variable to track the global step. global_step = tf.Variable(0, name='global_step', trainable=False) # Use the optimizer to apply the gradients that minimize the loss # (and also increment the global step counter) as a single training step. train_op = optimizer.minimize(loss, global_step=global_step) return train_op
最初に loss() 関数からの loss テンソルを受け取りそれを tf.scalar_summary に渡します。
この OP は後述の SummaryWriter とともに使用される時はイベントファイルに要約値を生成します。この場合には要約が書き出されるたびに損失のスナップショット値を出力します。
tf.scalar_summary(loss.op.name, loss)
次に、要求された学習率で勾配法を適用する責を負う tf.train.GradientDescentOptimizer をインスタンス化します。
optimizer = tf.train.GradientDescentOptimizer(FLAGS.learning_rate)
そしてグローバルな学習ステップのためのカウンターを含む一つの変数 global_step を作成します。
minimize() OP はシステムの訓練可能な weights(重み)の更新とグローバルステップの繰り上げの両方のために使用されます。これは慣習的に train_op として知られ、この OP は訓練の一つの完全なステップを引き起こすために、TensorFlow セッションにより実行されなければなりません。
global_step = tf.Variable(0, name='global_step', trainable=False) train_op = optimizer.minimize(loss, global_step=global_step)
訓練 OP の出力を含むテンソルが返されます。
モデルを訓練する
グラフが構築されれば、fully_connected_feed.py のユーザコードにより制御されたループの中で反復的に訓練し評価することができます。
グラフ
run_training() 関数の冒頭は、全ての構築された OP がデフォルトのグローバルな tf.Graph インスタンスと関連付けられていることを示す python コードです。
with tf.Graph().as_default():
tf.Graph はグループとして一緒に実行可能な OP のコレクションです。
TensorFlow の利用の多くの場合では、一つのデフォルト・グラフに依拠することだけで十分でしょう。複数のグラフを伴うより複雑な利用法も可能ですが、この簡単なチュートリアルの範囲を超えています。
セッション
全ての構築準備が完了し、全ての必要な OP が生成されたら、グラフを実行するために tf.Session が作成されます。
sess = tf.Session()
あるいは、スコープを使うために with ブロック内に Session を生成することもできます。
with tf.Session() as sess:
session への空パラメータは、このコードがデフォルトのローカル・セッションにアタッチされる(あるいはもし未作成ならば作成される)ことを示しています。
セッションを作成したら直ちに、initialization OP で sess.run() を呼び出すことにより全ての tf.Variable インスタンスが初期化されます。
init = tf.initialize_all_variables() sess.run(init)
sess.run() メソッドはパラメータとして渡された OP に対応するグラフの完全な部分集合を実行します。
この最初の呼び出しにおいて init OP は変数の initializer だけを含む tf.group になっています。グラフの残りはここでは実行されません; それは下記の訓練ループで発生します。
訓練ループ
セッションで変数を初期化した後には学習が始められます。
ユーザコードはステップ毎に学習を制御しますが、
有用な学習を行なうことが可能な最も単純なループは次のようなものになります :
for step in xrange(max_steps): sess.run(train_op)
けれども、先に生成されたプレースホルダーに適合させるためにステップ毎の入力データをスライスしなければならないという点で少しばかりより複雑です。
fully_connected_feed の該当箇所の実際のコードは以下のようなものです :
for step in xrange(FLAGS.max_steps): ... _, loss_value = sess.run([train_op, loss], feed_dict=feed_dict)
グラフに供給する
各ステップで、コードは feed 辞書を生成します。これはステップの学習をするサンプルのセットを含み、それらが表すプレースホルダー OP をキーとします。
fill_feed_dict() 関数では、与えられたデータセットに batch_size の次の画像とラベルのセットが問い合わせされます。そしてプレースホルダーにマッチするテンソルには次の画像とラベルを含むように設定されます。
images_feed, labels_feed = data_set.next_batch(FLAGS.batch_size)
それから、プレースホルダーをキーとし、それを表す feed テンソルを値として持つ、python 辞書オブジェクトが生成されます。
feed_dict = { images_placeholder: images_feed, labels_placeholder: labels_feed, }
これは学習のこのステップのための入力例を提供するために、sess.run() 関数の feed_dict パラメータに渡されます。
以上をまとめて、fully_connected_feed#fill_feed_dict() の実装は以下のようなものです :
def fill_feed_dict(data_set, images_pl, labels_pl): """Fills the feed_dict for training the given step. A feed_dict takes the form of: feed_dict = { <placeholder>: <tensor of values to be passed for placeholder>, .... } Args: data_set: The set of images and labels, from input_data.read_data_sets() images_pl: The images placeholder, from placeholder_inputs(). labels_pl: The labels placeholder, from placeholder_inputs(). Returns: feed_dict: The feed dictionary mapping from placeholders to values. """ # Create the feed_dict for the placeholders filled with the next # `batch size ` examples. images_feed, labels_feed = data_set.next_batch(FLAGS.batch_size, FLAGS.fake_data) feed_dict = { images_pl: images_feed, labels_pl: labels_feed, } return feed_dict
ステータスのチェック
run 呼び出しで取得するための2つの値を指定します : [train_op, loss]
for step in xrange(FLAGS.max_steps): feed_dict = fill_feed_dict(data_sets.train, images_placeholder, labels_placeholder) _, loss_value = sess.run([train_op, loss], feed_dict=feed_dict)
取得する2つの値があるので、sess.run() は2つの要素のタプルを返します。
取得する値のリストの中の各テンソルは、返されたタプルの中の numpy 配列に相当し、訓練のこのステップにおけるそのテンソルの値が設定されています。
train_op は出力値なしの OP ですから、返されたタプルの中の相当する要素は None でありそれ故に棄てられます。
しかし loss テンソルの値は学習中にモデルが拡散 (diverge) した場合には NaN になるかもしれません。よってこれらの値は捕えてロギングします。
NaN なしに訓練がきれいに走ると仮定して、訓練ループはまた、ユーザに訓練状況を知らせるために簡単なステータス・テキストを 100 ステップ毎に表示します。
if step % 100 == 0: print 'Step %d: loss = %.2f (%.3f sec)' % (step, loss_value, duration)
ステータスの視覚化
def run_training(): data_sets = input_data.read_data_sets(FLAGS.train_dir, FLAGS.fake_data) with tf.Graph().as_default(): images_placeholder, labels_placeholder = placeholder_inputs(FLAGS.batch_size) logits = mnist.inference(images_placeholder, FLAGS.hidden1, FLAGS.hidden2) loss = mnist.loss(logits, labels_placeholder) train_op = mnist.training(loss, FLAGS.learning_rate) eval_correct = mnist.evaluation(logits, labels_placeholder) summary_op = tf.merge_all_summaries() saver = tf.train.Saver() sess = tf.Session() init = tf.initialize_all_variables() sess.run(init) summary_writer = tf.train.SummaryWriter(FLAGS.train_dir, graph_def=sess.graph_def) for step in xrange(FLAGS.max_steps): ...
TensorBoard で使用されるイベントファイルを出力するために、
全ての要約(この場合は一つ)はグラフ構築フェイズで一つの OP に集められます。
summary_op = tf.merge_all_summaries()
そしてセッションが作成されると、
tf.train.SummaryWriter がグラフ自身と要約の値の両方を含むイベントファイルを書くためにインスタンス化されます。
summary_writer = tf.train.SummaryWriter(FLAGS.train_dir, graph_def=sess.graph_def)
最終的に、summary_op が実行されて writer の add_summary() 関数にその出力が渡されるたびにイベントファイルは新しい要約値で更新されます。
for step in xrange(FLAGS.max_steps): start_time = time.time() feed_dict = fill_feed_dict(data_sets.train, images_placeholder, labels_placeholder) _, loss_value = sess.run([train_op, loss], feed_dict=feed_dict) duration = time.time() - start_time if step % 100 == 0: print('Step %d: loss = %.2f (%.3f sec)' % (step, loss_value, duration)) # イベントファイルの更新。 summary_str = sess.run(summary_op, feed_dict=feed_dict) summary_writer.add_summary(summary_str, step)
イベントファイルが書かかれた時、要約値を表示するためには学習フォルダに対して TensorBoard を実行することができます。
Tensorboard をどのように構築して実行するかについての詳細は Tensorboard: 学習を視覚化する を参照してください。
チェックポイントの保存
更なる学習や評価のために後でモデルを復元するために使用される、
チェックポイント・ファイルを出力するためには tf.train.Saver を(セッション作成前に)インスタンス化しておきます。
saver = tf.train.Saver()
学習ループにおいてはチェックポイントファイルを学習ディレクトリに書くために
saver.save() メソッドが、全ての学習可能な変数の現在値とともに定期的に呼び出されます。
flags.DEFINE_string('train_dir', 'data', 'Directory to put the training data.') ... if (step + 1) % 1000 == 0 or (step + 1) == FLAGS.max_steps: saver.save(sess, FLAGS.train_dir, global_step=step)
先々どこか後のポイントで、モデル・パラメータを再ロードするために
saver.restore() メソッドを使うことにより学習を再開することも可能です。
saver.restore(sess, FLAGS.train_dir)
モデルを評価する
1000 ステップ毎に、コードは学習用とテスト用のデータセットの両方についてモデルの評価を試みます。
学習・検証そしてテスト用のデータセットのために do_eval() 関数が3度呼ばれます。
if (step + 1) % 1000 == 0 or (step + 1) == FLAGS.max_steps: saver.save(sess, FLAGS.train_dir, global_step=step) # Evaluate against the training set. print('Training Data Eval:') do_eval(sess, eval_correct, images_placeholder, labels_placeholder, data_sets.train) # Evaluate against the validation set. print('Validation Data Eval:') do_eval(sess, eval_correct, images_placeholder, labels_placeholder, data_sets.validation) # Evaluate against the test set. print('Test Data Eval:') do_eval(sess, eval_correct, images_placeholder, labels_placeholder, data_sets.test)
def do_eval(sess, eval_correct, images_placeholder, labels_placeholder, data_set): """Runs one evaluation against the full epoch of data. Args: sess: The session in which the model has been trained. eval_correct: The Tensor that returns the number of correct predictions. images_placeholder: The images placeholder. labels_placeholder: The labels placeholder. data_set: The set of images and labels to evaluate, from input_data.read_data_sets(). """ # And run one epoch of eval. true_count = 0 # Counts the number of correct predictions. steps_per_epoch = data_set.num_examples // FLAGS.batch_size num_examples = steps_per_epoch * FLAGS.batch_size for step in xrange(steps_per_epoch): feed_dict = fill_feed_dict(data_set, images_placeholder, labels_placeholder) true_count += sess.run(eval_correct, feed_dict=feed_dict) precision = true_count / num_examples print(' Num examples: %d Num correct: %d Precision @ 1: %0.04f' % (num_examples, true_count, precision))
【注意】より複雑な利用方法では通常は data_sets.test をハイパーパラメータの主要な量のチューニングの後でのみチェックする目的で隔離しておく点に注意してください。しかし簡単で小さな MNIST 問題のために全てのデータに対して評価します。
評価グラフ (Eval Graph) の構築
一般に、デフォルトグラフをオープンする前に、テスト用データセットを得るためにセットされた引数で get_data(train=False) 関数を呼び出すことによりテストデータが取得されているべきです。
test_all_images, test_all_labels = get_data(train=False)
学習ループに入る前に、loss() 関数と同様のロジット / ラベル引数とともに
mnist.py から evaluation() 関数を呼び出すことで評価 (Eval) OP を構築しておきます。
eval_correct = mnist.evaluation(logits, labels_placeholder)
def evaluation(logits, labels): """Evaluate the quality of the logits at predicting the label. Args: logits: Logits tensor, float - [batch_size, NUM_CLASSES]. labels: Labels tensor, int32 - [batch_size], with values in the range [0, NUM_CLASSES). Returns: A scalar int32 tensor with the number of examples (out of batch_size) that were predicted correctly. """ # For a classifier model, we can use the in_top_k Op. # It returns a bool tensor with shape [batch_size] that is true for # the examples where the label's is was in the top k (here k=1) # of all logits for that example. correct = tf.nn.in_top_k(logits, labels, 1) # Return the number of true entries. return tf.reduce_sum(tf.cast(correct, tf.int32))
mnist.evaluation() 関数は、真のラベルが尤もそれらしい予測 K 個の中で見つかる場合、自動的に各モデルの出力を正しいとスコアできる tf.nn.in_top_k OP を単に生成するだけです。
今回の場合は、真のラベルの場合のみ予測が正しいと考えることにして K の値を 1 に設定しています。
出力を評価する
そして与えられたデータセット上でモデルを評価するために、
feed_dict を満たして eval_correct OP に対して sess.run() を呼び出すループが作成できます。
def do_eval(sess, eval_correct, images_placeholder, labels_placeholder, data_set): true_count = 0 # Counts the number of correct predictions. steps_per_epoch = data_set.num_examples // FLAGS.batch_size num_examples = steps_per_epoch * FLAGS.batch_size for step in xrange(steps_per_epoch): feed_dict = fill_feed_dict(data_set, images_placeholder, labels_placeholder) true_count += sess.run(eval_correct, feed_dict=feed_dict)
true_count 変数は in_top_k OP が正しいと決定した予測の全てを集計するだけです。
ここから、正確性は単にサンプルの総数で除算することで計算されます。
... for step in xrange(steps_per_epoch): feed_dict = fill_feed_dict(data_set, images_placeholder, labels_placeholder) true_count += sess.run(eval_correct, feed_dict=feed_dict) precision = true_count / num_examples print(' Num examples: %d Num correct: %d Precision @ 1: %0.04f' % (num_examples, true_count, precision))
以上