7rpn’s blog: うわああああな日常

好きなことをつらつらと。AIとかで面白いことをしたい。

Tensorflowのサンプルを理解してみる。(初心者向け)

Googleが出したTensorflow,盛り上がり具合がやばいですね。
githubのスター数とかを見ていると,スタンダードであるChainerとかCaffeとかを(盛り上がり具合だけは)軽く越えてしまった感じ。

というわけで,MNISTの(畳み込みしているほうの)サンプルをざっと読んでみました。 備忘録がてらメモしようかな,と思ってブログにしてみます。
自分は初心者なので,まぁそのレベルで色々注をつけて行こうと思います。

Tensorflowの良かったところ

最初に,Tensorflowの良かったところを。
大規模処理で並列化とかが楽らしいんですけど,正直よく分からないです笑

コードを見て思ったのは,必要な部分だけが厳密に書かれているなーと思いました。
低い次元の記述する必要のないものはしっかり隠して,必要な記述はちゃんと書かれているってのが自分みたいな初学者には良いと思います。Chainerとかだと大切な部分も結構略されてたりするので。

Deeplearningを研究でやっている友人とコードをおさらいした(というかほぼ教えてもらった)ので,復習のためにメモを残そうかな,と思ったわけです。

というわけでコード

インストール(Mac)

pip install https://storage.googleapis.com/tensorflow/mac/tensorflow-0.5.0-py2-none-any.whl

サンプル等のセットをダウンロード。

git clone https://github.com/tensorflow/tensorflow

今回見てみたのはtensorflow/tensorflow/models/image/mnistの中のconvolutional.pyです。 チュートリアルで使われているMNISTは畳み込みしてないただのニューラルネットですが,こっちはしてるっぽいので,より実用的かなーと思ったので。
途中で感想挟みつつ。

とりあえずモジュール部分。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import gzip
import os
import sys

import tensorflow.python.platform
import numpy
from six.moves import urllib
from six.moves import xrange # pylint: disable=redefined-builtin
import tensorflow as tf

Googleでもnumpy使ってるんだなぁとか思ったり。

Main部分から見ていきます。

def main(argv=None):  # pylint: disable=unused-argument
  #自分自身のテスト
  #フェイクデータを読み込んでうまく行くか試す
  if FLAGS.self_test:
    print('Running self-test.')
    train_data, train_labels = fake_data(256)
    validation_data, validation_labels = fake_data(16)
    test_data, test_labels = fake_data(256)
    num_epochs = 1

ここまでで全体のテスト。

  else:
    # テストじゃなければデータを読み込む
    train_data_filename = maybe_download('train-images-idx3-ubyte.gz')
    train_labels_filename = maybe_download('train-labels-idx1-ubyte.gz')
    test_data_filename = maybe_download('t10k-images-idx3-ubyte.gz')
    test_labels_filename = maybe_download('t10k-labels-idx1-ubyte.gz')

    # 画像をnumpy配列に展開
    train_data = extract_data(train_data_filename, 60000)
    train_labels = extract_labels(train_labels_filename, 60000)
    test_data = extract_data(test_data_filename, 10000)
    test_labels = extract_labels(test_labels_filename, 10000)

    # 検証用のデータとトレーニング用のデータに分割
    validation_data = train_data[:VALIDATION_SIZE, :, :, :]
    validation_labels = train_labels[:VALIDATION_SIZE]
    train_data = train_data[VALIDATION_SIZE:, :, :, :]
    train_labels = train_labels[VALIDATION_SIZE:]
    num_epochs = NUM_EPOCHS
  train_size = train_labels.shape[0] # 0次元目のサイズ取得

ここでデータのダウンロード。maybe_downloadでは学習用のMNISTのファイルがなかったらダウンロードする仕組みの関数。 ダウンロードしたデータをextract_data等の関数に渡してnumpyのArrayに変換しています。

extract_data関数の中身はこんな感じ。

def extract_data(filename, num_images):

  print('Extracting', filename)
  with gzip.open(filename) as bytestream:
    bytestream.read(16)
    buf = bytestream.read(IMAGE_SIZE * IMAGE_SIZE * num_images)
    data = numpy.frombuffer(buf, dtype=numpy.uint8).astype(numpy.float32)
    data = (data - (PIXEL_DEPTH / 2.0)) / PIXEL_DEPTH
    data = data.reshape(num_images, IMAGE_SIZE, IMAGE_SIZE, 1)
    return data

ここで0-255の色の濃さを-0.5から0.5までに変更しているっぽいです。 returnしているdataは画像の種類,x軸,y軸を格納したnumpy配列になってます。 つまりdata[0-60000個くらい][x軸0-31][y軸0-31]みたいな感じ。
MNISTのデータなので白黒です。色の次元はありません。

まぁこの辺は機械学習関係ないのでさらっと行きましょう。状況によって書き方変わるでしょうしね。

main関数に戻ります。

  # バッチを入れるための入れ物を最初に作っておくみたい
  # 学習ごとにバッチの中身は変わるので
  train_data_node = tf.placeholder(
      tf.float32,
      shape=(BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, NUM_CHANNELS))
  train_labels_node = tf.placeholder(tf.float32,
                                     shape=(BATCH_SIZE, NUM_LABELS))
  # 検証用だからconstとして固定しておく,これも入れ物
  validation_data_node = tf.constant(validation_data)
  test_data_node = tf.constant(test_data)

  # モデルの重み部分の初期化,いわゆるwの部分
  # {tf.initialize_all_variables().run()}を実行した際に初期化される
  conv1_weights = tf.Variable(# 畳み込み層の重み,Variableなので途中で学習に応じて変わる
      tf.truncated_normal([5, 5, NUM_CHANNELS, 32],  # 5x5 filter, depth 32.
                          stddev=0.1, # 標準偏差
                          seed=SEED))
  conv1_biases = tf.Variable(tf.zeros([32])) # バイアス0埋め
  conv2_weights = tf.Variable(
      tf.truncated_normal([5, 5, 32, 64],
                          stddev=0.1,
                          seed=SEED))
  conv2_biases = tf.Variable(tf.constant(0.1, shape=[64])) # バイアス0.1埋め
  fc1_weights = tf.Variable(  # fully connected, depth 512. # 全結合層の重み
      tf.truncated_normal(
          [IMAGE_SIZE // 4 * IMAGE_SIZE // 4 * 64, 512],
          stddev=0.1,
          seed=SEED))
  fc1_biases = tf.Variable(tf.constant(0.1, shape=[512]))
  fc2_weights = tf.Variable(
      tf.truncated_normal([512, NUM_LABELS],
                          stddev=0.1,
                          seed=SEED))
  fc2_biases = tf.Variable(tf.constant(0.1, shape=[NUM_LABELS]))

モデルの定義をする前に初期化だけ先に定義して,それを(後から)モデルに突っ込むって感じです。 学習によって変化していく部分はVariableと定義して,変化しない部分はconstantと定義しているので見やすいですね。

バッチの入れ物だけを最初に作っているのが珍しいかなって思いました。Chainerとかでは学習中に分割してるみたいなコードの書き方をしていたので。 モデルの重みの初期化までしっかりやってるんだなーって感じ。 標準偏差0.1と決めてあるのも略しても良いのに書いてあるのがありがたい。 必要なところをしっかり厳密にって感じ。エンジニア魂を感じますね。

どうでも良いけど第二全結合層ってFC2って略すんですね。 あのウェブサイトもここから名前を取ったんですかね。 本当にどうでも良かった。

そしてモデルの定義。一番重要なネットワーク部分。

  def model(data, train=False):
    # データとモデルの重みを二次元の畳み込み層に突っ込む
    conv = tf.nn.conv2d(data,
                        conv1_weights,
                        strides=[1, 1, 1, 1], # ストライド,畳み込む際に画像をいくつ飛ばしで処理して行くか
                        padding='SAME') # ふちを付けて同じサイズの画像が出力されるようにしている
    # 活性化関数としてrelu通すよ
    relu = tf.nn.relu(tf.nn.bias_add(conv, conv1_biases))
    #最大プーリング,画像を分割してその中で最も値が大きいものだけを残してあとは削る方法で画像を縮小
    pool = tf.nn.max_pool(relu,
                          ksize=[1, 2, 2, 1],
                          strides=[1, 2, 2, 1],
                          padding='SAME')
    conv = tf.nn.conv2d(pool,
                        conv2_weights,
                        strides=[1, 1, 1, 1],
                        padding='SAME')
    relu = tf.nn.relu(tf.nn.bias_add(conv, conv2_biases))
    pool = tf.nn.max_pool(relu,
                          ksize=[1, 2, 2, 1],
                          strides=[1, 2, 2, 1],
                          padding='SAME')
    # 二次元画像を一次元に変更して全結合層へ渡す
    pool_shape = pool.get_shape().as_list()
    reshape = tf.reshape(
        pool,
        [pool_shape[0], pool_shape[1] * pool_shape[2] * pool_shape[3]])
    hidden = tf.nn.relu(tf.matmul(reshape, fc1_weights) + fc1_biases)
  # トレーニング時はドロップアウトする,50%
  # ドロップアウトは使用するネットを毎バッチごとランダムで(今回では50%だけ)選んで学習させるとより最適化が進むってやつ
    if train:
      hidden = tf.nn.dropout(hidden, 0.5, seed=SEED)
    return tf.matmul(hidden, fc2_weights) + fc2_biases

画像から特徴を抽出しつつデータ量を減らし,それを最終的に縦に並べて普通の(linearな)ニューラルネットに突っ込んでる部分です。 ごくごく一般的な畳み込みネットだと思います。

ストライドが1ずつとか略しても問題ないこともしっかり書いてあるところがGoogleっぽい。 "tf.matmul(reshape, fc1_weights) + fc1_biases"の部分とか,実際の式に近い感じで見やすいですね。

そして,重み減衰やモメンタムなど,細かい部分の初期設定を以下で行います。

# Training computation: logits + cross-entropy loss.
# logitsにモデルからの出力が返ってくる
  logits = model(train_data_node, True)
  # 結果を誤差関数に突っ込む
  loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
      logits, train_labels_node))

  # 正規化,いわゆる重み減衰,これやっとくと効率よくなるよ
  regularizers = (tf.nn.l2_loss(fc1_weights) + tf.nn.l2_loss(fc1_biases) +
                  tf.nn.l2_loss(fc2_weights) + tf.nn.l2_loss(fc2_biases))
  # 正規化の強さパラメータを重みの二乗和にかけて,それを誤差関数に足す
  loss += 5e-4 * regularizers

  # バッチも毎回変わるからVariable扱い
  batch = tf.Variable(0)
  # Decay once per epoch, using an exponential schedule starting at 0.01.
  # 学習率の初期化,ある程度学習が進んだらより細かくモデルを形作っていくから学習量を減衰させるよ
  learning_rate = tf.train.exponential_decay(
      0.01,                # 学習率の初期設定
      batch * BATCH_SIZE,  # いまdatasetのいくつめか
      train_size,          # 学習がどの程度進んだら学習量を減衰させるか
      0.95,                #  減衰させる量
      staircase=True)
  # モメンタムの係数0.9で最適化する
  # モメンタムは,重みを修正する量に前回の修正量を加えるとより最適化されやすくなるってのらしい
  optimizer = tf.train.MomentumOptimizer(learning_rate,
                                         0.9).minimize(loss,
                                                       global_step=batch)

  # 学習時のデータをバッチごとに予測
  train_prediction = tf.nn.softmax(logits)
  # 検証時とテストデータをモデルに突っ込んで予測
  validation_prediction = tf.nn.softmax(model(validation_data_node))
  test_prediction = tf.nn.softmax(model(test_data_node))

モデルから返って来た結果から,答えのラベルデータと比較して誤差を出しています。その後,重み減衰のために正規化を誤差に足しています。
その誤差をoptimizerで最小化することによってモデルを改善して学習しています。

ここまで来たらあとは実行するだけ。
Tensorflowは最初に初期設定を厳密に決めて,あとは実行するだけって仕組みなので実行部分のプログラムは非常に分かりやすいです。 以下が機械学習の実行部分。

# 計算するセッションを作るんだって,その後s.runで走らせるみたい
with tf.Session() as s:
  # すべてのvariable部分を初期化
  tf.initialize_all_variables().run()
  print('Initialized!')
  # Loop through training steps.
  # 学習データをループ回して学習します
  for step in xrange(num_epochs * train_size // BATCH_SIZE):
    # Compute the offset of the current minibatch in the data.
    # Note that we could use better randomization across epochs.
    # ephochsごとにより良いランダマイズの手法取れるよって書いてある?
    # numpyとかでパーミュテーションに渡した方が良くなるってこと?
    offset = (step * BATCH_SIZE) % (train_size - BATCH_SIZE)
    batch_data = train_data[offset:(offset + BATCH_SIZE), :, :, :]
    batch_labels = train_labels[offset:(offset + BATCH_SIZE)]
    # This dictionary maps the batch data (as a numpy array) to the
    # node in the graph is should be fed to.
    # バッチデータとラベルデータを辞書形式として入れ物に入れる
    feed_dict = {train_data_node: batch_data,
                 train_labels_node: batch_labels}
    # 今までの設定を突っ込んで,機械学習を走らせる
    _, l, lr, predictions = s.run(
        [optimizer, loss, learning_rate, train_prediction],
        feed_dict=feed_dict)
    if step % 100 == 0:  # 学習状況の表示
      print('Epoch %.2f' % (float(step) * BATCH_SIZE / train_size))
      print('Minibatch loss: %.3f, learning rate: %.6f' % (l, lr))
      print('Minibatch error: %.1f%%' % error_rate(predictions, batch_labels))
      print('Validation error: %.1f%%' %
            error_rate(validation_prediction.eval(), validation_labels))
      sys.stdout.flush()
  # 結果の表示
  test_error = error_rate(test_prediction.eval(), test_labels)
  print('Test error: %.1f%%' % test_error)
  if FLAGS.self_test:
    print('test_error', test_error)
    assert test_error == 0.0, 'expected 0.0 test_error, got %.2f' % (
        test_error,)

最初に必要な定義を済ませているので,実際の学習部分の記述量がめちゃくちゃ少なく感じますね。

初学者なので間違っているところがあったら教えてもらえたらありがたいです。

で,結局Tensorflowってどうなの?

CaffeとかChainerとかを長らくやって来た友人的には,まだ早いっぽいです。情報が出そろってないので,様子見と言っていました。
現状公式サイトですらComing soonが結構ある状況なので,判断を下すには早すぎますよね。

自分は割とその友人を信用しているので,まぁ様子見したほうが良いのかなって感じです。
RubyのTensorflowが出たら飛びつくと思いますが,そうじゃなければChainerで良いかなって思います。
Tensorflowも自分の勉強に使いつつ,Chainerとどっちが良いのか見比べて行くつもりです。現状Chainerメイン。 メインって言えるほど使いこなせてないですけど笑

RubyのTensorflow出てほしいなあーー まぁ多分出なさそうですけどね...


関連:
名大で一番リア充が入る学部はどこだ? Project OxfordのEmotion APIを試す

Googleが出した囲碁ソフト「AlphaGo」の論文を翻訳して解説してみる。