BizFrameworks and DeepLearning

ビジネスの分かる機械学習エンジニアを目指してます

TensorFlowチュートリアル和訳(Image Recognition)

TensorFlowのチュートリアルを愚直に和訳していきます。 今回は画像認識についてのチュートリアルです。
TensorFlowの中で動いているC++のコードの説明が行われています。

Image Recognition

私たちの脳は簡単に画像認識をしてしまいます。人間の脳は非常に優れているため、ライオンとジャガーの違いが分かるし、看板を読んだり、人の顔を認識するのは簡単です。
しかし、これらをコンピュータにさせるにはとても難しい問題です。

この問題に対処するために、ここ数年機械学習の分野は非常に進歩しました。
特に、deepな畳み込みニューラルネットワーク(CNN)が難しい画像認識タスクで高いパフォーマンスを達成できることを発見しました。

研究者たちは、画像認識の学術的ベンチマークであるImageNetに対するパフォーマンスを検証することで、認識技術の着実な進歩を実証しています。
QuocNetAlexNetInception(GoogleNet)BN-Inception-v2などの最先端のstate-of-the-artの結果が出ており、モデルの改善が続けられています。
Google社内外の研究者も、これらのモデルを全て記述した論文を発表していますが、その結果はまだ再現するのが難しい状況です。
現在では最新のInception-v3モデルで画像認識を実行するコードを公開しており、次のステップに踏み出しています。

Inception-v3は2012年のImageNet large Visual Recognition Challengeのデータで学習しています。
これは、シマウマやダルメシアン、皿洗い機などを含む1000クラスの分類を行う画像認識の標準タスクです。
例として、以下にAlexNetの分類結果を示します。

f:id:minison130:20170507154001p:plain

モデルの比較をするために、上位5つの推測結果に正解が含まれない頻度である"top-5 error rate"を各モデルごとに調べます。
AlexNetは2012年の検証データセットでtop5 error rateが15.3%を達成しています。
さらに、Inception(GoogleNet)は6.63%、BN-Inception-v2は4.9%、Inception-v3は3.46%を達成しています。

ちなみに、ImageNet Challengeで人間はどれぐらいの精度が出ているのでしょうか?
Andrej Karpathyがブログ記事の中で自身のパフォーマンスを測定しており、top5 error rateが5.1%となっています。
(つまり、最新の結果は人間の精度を凌駕している!)

このチュートリアルでは、Inception-v3の使い方について説明します。
ここでは、PythonC++を使って1000クラスの画像分類をする方法を学びます。
また、他の画像分類タスクに適用させるために、このモデルからより高いレベルでの特徴量を抽出する方法についても説明します。

Usage with Python API

classify_image.pyはプログラムの初回実行時に、訓練されたモデルをtensorflow.orgからダウンロードします。
このために、ハードディスクには約200Mの空き容量が必要です。

まず、GitHubからTensorFlowモデルのリポジトリをcloneし、次のコマンドを実行します。

cd models/tutorials/image/imagenet
python classify_image.py

上のコマンドは以下のパンダ画像を分類します。

f:id:minison130:20170507161400p:plain

モデルが正しく実行されると、スクリプトは以下の出力を生成します。

giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca (score = 0.88493)
indri, indris, Indri indri, Indri brevicaudatus (score = 0.00878)
lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens (score = 0.00317)
custard apple (score = 0.00149)
earthstar (score = 0.00127)

他のJPEG画像を認識したい場合は、--image_file引数を編集してください。

モデルデータを別のディレクトリにダウンロードする場合は、使用するディレクトリを--model_dirで指定する必要があります。

Usage with the C++ API

本番環境で使用するために、C++で同じInception-v3モデルを実行できます。
以下のコマンドを行うことで、モデルを定義するGraphDefを含むアーカイブをダウンロードすることができます。(TensorFlowリポジトリのルートから実行します。)

curl -L "https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz" |
  tar -C tensorflow/examples/label_image/data -x

次に、グラフをロードして実行するコードを含むC++バイナリをコンパイルする必要があります。
the instructions to download the source installation of TensorFlowに従ってTensorFlowを構築している場合は、シェルターミナルからいかのコマンドを実行してサンプルをビルドすることができます。

bazel build tensorflow/examples/label_image/...

このやり方では、バイナリ実行可能ファイルを作成して次を実行する必要があります。

bazel-bin/tensorflow/examples/label_image/label_image

これはフレームワークに同梱されているデフォルトのサンプルイメージを使用しており、次のような出力が得られます。

I tensorflow/examples/label_image/main.cc:206] military uniform (653): 0.834306
I tensorflow/examples/label_image/main.cc:206] mortarboard (668): 0.0218692
I tensorflow/examples/label_image/main.cc:206] academic gown (401): 0.0103579
I tensorflow/examples/label_image/main.cc:206] pickelhaube (716): 0.00800814
I tensorflow/examples/label_image/main.cc:206] bulletproof vest (466): 0.00535088

このケースでは、Grace Hopper提督の画像をデフォルトに使用しています。
認識してみると、ネットワークが軍服を着用していることを正しく認識していることがわかります。
スコアは0.8となります。

f:id:minison130:20170507165129p:plain

次に、–image=引数を指定して自身の手持ち画像で試してみてください。

bazel-bin/tensorflow/examples/label_image/label_image --image=my_image.png

tensorflow/example/label_image/main.ccファイルの中身を調べると、その動作を知ることができます。
このコードはTensorFlow独自のアプリケーションに統合する際に役立ちますので、主な昨日を順を追って説明します。

コマンドラインフラグでは、ファイルのロード元と入力画像のプロパティを制御します。
このモデルでは、正方形の299x299のRGB画像が得られると予想されており、input_width及びinput_heightフラグが対応します。
またピクセル値は0~255の整数値からグラフが操作する浮動小数点値にスケールする必要があり、これらはinput_meaninput_stdフラグが対応します。
ここでは、各ピクセル値からinput_meanを減算し、次にそれをinput_stdで除算します。

これらの値はマジックバリューに見えるかもしれませんが、元のモデル作成者が訓練画像に基づいて設定したものです。
自分で訓練したグラフがある場合は、訓練プロセス中に使用した値に合わせて調節すれば良いです。

ReadTensorFromImageFile()関数を見れば、どのように画像が適用されているかを見ることができます。

// Given an image file name, read in the data, try to decode it as an image,
// resize it to the requested size, and then scale the values as desired.
Status ReadTensorFromImageFile(string file_name, const int input_height,
                               const int input_width, const float input_mean,
                               const float input_std,
                               std::vector<Tensor>* out_tensors) {
  tensorflow::GraphDefBuilder b;

実行のスタートとして、ロードするモデルを指定するために使用するオブジェクトであるGraphDefBuilderを作成しています。

string input_name = "file_reader";
  string output_name = "normalized";
  tensorflow::Node* file_reader =
      tensorflow::ops::ReadFile(tensorflow::ops::Const(file_name, b.opts()),
                                b.opts().WithName(input_name));

次に、入力値をメインモデルの期待値を合わせるために、ピクセル値のロード、サイズ変更、スケーリングを行うためのノードを作成しています。
最初に作成するノードは、ロードする画像のファイル名を保持するテンソル用のConst演算子です。
この値は最初の入力として、ReadFile操作に渡されます。
全ての操作では、最後の引数としてb.opts()を渡しています。
この引数はノードがGraphDefBuilderに保持されているモデル定義に追加されることを保証するものになっています。
また、b.opts()WithName()メソッドの中で名前の指定を行なっています。
これにより、ノードに名前が割り当てられます。
この名前付けは必須ではなく、WithName()メソッドを使わない場合は自動で名前が割り当てられますが、名前付けを行なっておいた方がデバッグが楽になります。

// Now try to figure out what kind of file it is and decode it.
  const int wanted_channels = 3;
  tensorflow::Node* image_reader;
  if (tensorflow::StringPiece(file_name).ends_with(".png")) {
    image_reader = tensorflow::ops::DecodePng(
        file_reader,
        b.opts().WithAttr("channels", wanted_channels).WithName("png_reader"));
  } else {
    // Assume if it's not a PNG then it must be a JPEG.
    image_reader = tensorflow::ops::DecodeJpeg(
        file_reader,
        b.opts().WithAttr("channels", wanted_channels).WithName("jpeg_reader"));
  }
  // Now cast the image data to float so we can do normal math on it.
  tensorflow::Node* float_caster = tensorflow::ops::Cast(
      image_reader, tensorflow::DT_FLOAT, b.opts().WithName("float_caster"));
  // The convention for image ops in TensorFlow is that all images are expected
  // to be in batches, so that they're four-dimensional arrays with indices of
  // [batch, height, width, channel]. Because we only have a single image, we
  // have to add a batch dimension of 1 to the start with ExpandDims().
  tensorflow::Node* dims_expander = tensorflow::ops::ExpandDims(
      float_caster, tensorflow::ops::Const(0, b.opts()), b.opts());
  // Bilinearly resize the image to fit the required dimensions.
  tensorflow::Node* resized = tensorflow::ops::ResizeBilinear(
      dims_expander, tensorflow::ops::Const({input_height, input_width},
                                            b.opts().WithName("size")),
      b.opts());
  // Subtract the mean and divide by the scale.
  tensorflow::ops::Div(
      tensorflow::ops::Sub(
          resized, tensorflow::ops::Const({input_mean}, b.opts()), b.opts()),
      tensorflow::ops::Const({input_std}, b.opts()),
      b.opts().WithName(output_name));

上では、さらに多くのノードを追加し、画像ファイルをデコードし、整数を浮動小数点にキャストし、サイズを変更し、ピクセル値に対して減算と除算を以下のように実行しています。

  // This runs the GraphDef network definition that we've just constructed, and
  // returns the results in the output tensor.
  tensorflow::GraphDef graph;
  TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));

最後に、b変数に格納されたモデル定義を取得し、ToGraphDef()関数を使って、完全なグラフ定義を作っています。

  std::unique_ptr<tensorflow::Session> session(
      tensorflow::NewSession(tensorflow::SessionOptions()));
  TF_RETURN_IF_ERROR(session->Create(graph));
  TF_RETURN_IF_ERROR(session->Run({}, {output_name}, {}, out_tensors));
  return Status::OK();

グラフ定義ができたら、次にtf.Sessionを作っています。
これは、グラフを実行し、出力を取得するノードを出力データをどこに置くかを指定しています。

これにより、Tensorオブジェクトのベクトルが得られます。この場合、存在するオブジェクトは1つのみであることがわかります。
このコンテキストではテンソルは多次元配列として考えることができ、浮動小数点値として299ピクセルの高さと幅、3チャネルの画像を保持しています。
もし、すでに自身の画像処理フレームワークを成果物に適用している場合は、上の代わりに自身のフレームワークを使用する方が良いです。

これはC++で小さなTensorFlowグラフを動的に作成する簡単な例ですが、事前に訓練されたインセプションモデルを使う場合は、なかなか大きなファイル定義を読み込む必要があります。
LoadGraph()関数を見ることで、どのように読み込みが行われているかを知ることができます。

// Reads a model graph definition from disk, and creates a session object you
// can use to run it.
Status LoadGraph(string graph_file_name,
                 std::unique_ptr<tensorflow::Session>* session) {
  tensorflow::GraphDef graph_def;
  Status load_graph_status =
      ReadBinaryProto(tensorflow::Env::Default(), graph_file_name, &graph_def);
  if (!load_graph_status.ok()) {
    return tensorflow::errors::NotFound("Failed to load compute graph at '",
                                        graph_file_name, "'");
  } 

他の画像読み込みコードを見たことがある人は、上で使われている用語もよく知っているはずです。
上では、GraphDefオブジェクトを生成するためにGraphDefBuilderを使用するのではなく、GraphDefを直接含むprotobufファイルを読み込んでいます。

  session->reset(tensorflow::NewSession(tensorflow::SessionOptions()));
  Status session_create_status = (*session)->Create(graph_def);
  if (!session_create_status.ok()) {
    return session_create_status;
  }
  return Status::OK();
}

次に、上ではそのGraphDefからSessionオブジェクトを生成し、呼び出し元に渡して後で実行できるようにしています。

以下で使っているGetTopLabels()関数は画像の読み込みによく似ていますが、この場合はメイングラフを実行した結果を取得し、最高スコアのラベルのソート済みリストに変換しています。
これはGraphDefBuilderを作成し、いくつかのノードを追加して短いグラフを実行して出力テンソルのペアを取得します。
このケースでは、スコアが最高になるインデックス値と、ソートされたスコアを返します。

// Analyzes the output of the Inception graph to retrieve the highest scores and
// their positions in the tensor, which correspond to categories.
Status GetTopLabels(const std::vector<Tensor>& outputs, int how_many_labels,
                    Tensor* indices, Tensor* scores) {
  tensorflow::GraphDefBuilder b;
  string output_name = "top_k";
  tensorflow::ops::TopK(tensorflow::ops::Const(outputs[0], b.opts()),
                        how_many_labels, b.opts().WithName(output_name));
  // This runs the GraphDef network definition that we've just constructed, and
  // returns the results in the output tensors.
  tensorflow::GraphDef graph;
  TF_RETURN_IF_ERROR(b.ToGraphDef(&graph));
  std::unique_ptr<tensorflow::Session> session(
      tensorflow::NewSession(tensorflow::SessionOptions()));
  TF_RETURN_IF_ERROR(session->Create(graph));
  // The TopK node returns two outputs, the scores and their original indices,
  // so we have to append :0 and :1 to specify them both.
  std::vector<Tensor> out_tensors;
  TF_RETURN_IF_ERROR(session->Run({}, {output_name + ":0", output_name + ":1"},
                                  {}, &out_tensors));
  *scores = out_tensors[0];
  *indices = out_tensors[1];
  return Status::OK();

PrintTopLabels()関数はソートされた結果を受け取り、使いやすい形式でそれらを出力します。
CheckTopLabel()関数はそれと非常によく似ていますが、デバッグ目的でトップラベルが期待通りのものであることを確認しています。

最後にmain()でこれらの呼び出しを全て結びつけます。

int main(int argc, char* argv[]) {
  // We need to call this to set up global state for TensorFlow.
  tensorflow::port::InitMain(argv[0], &argc, &argv);
  Status s = tensorflow::ParseCommandLineFlags(&argc, argv);
  if (!s.ok()) {
    LOG(ERROR) << "Error parsing command line flags: " << s.ToString();
    return -1;
  }

  // First we load and initialize the model.
  std::unique_ptr<tensorflow::Session> session;
  string graph_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_graph);
  Status load_graph_status = LoadGraph(graph_path, &session);
  if (!load_graph_status.ok()) {
    LOG(ERROR) << load_graph_status;
    return -1;
  }

上ではメイングラフを読み込んでいます。

  // Get the image from disk as a float array of numbers, resized and normalized
  // to the specifications the main graph expects.
  std::vector<Tensor> resized_tensors;
  string image_path = tensorflow::io::JoinPath(FLAGS_root_dir, FLAGS_image);
  Status read_tensor_status = ReadTensorFromImageFile(
      image_path, FLAGS_input_height, FLAGS_input_width, FLAGS_input_mean,
      FLAGS_input_std, &resized_tensors);
  if (!read_tensor_status.ok()) {
    LOG(ERROR) << read_tensor_status;
    return -1;
  }
  const Tensor& resized_tensor = resized_tensors[0];

上では、画像の読み込みとリサイズをして処理を行ないます。

  // Actually run the image through the model.
  std::vector<Tensor> outputs;
  Status run_status = session->Run({ {FLAGS_input_layer, resized_tensor}},
                                   {FLAGS_output_layer}, {}, &outputs);
  if (!run_status.ok()) {
    LOG(ERROR) << "Running model failed: " << run_status;
    return -1;
  }

さらに上では画像を入力としてロードされたグラフを実行しています。

  // This is for automated testing to make sure we get the expected result with
  // the default settings. We know that label 866 (military uniform) should be
  // the top label for the Admiral Hopper image.
  if (FLAGS_self_test) {
    bool expected_matches;
    Status check_status = CheckTopLabel(outputs, 866, &expected_matches);
    if (!check_status.ok()) {
      LOG(ERROR) << "Running check failed: " << check_status;
      return -1;
    }
    if (!expected_matches) {
      LOG(ERROR) << "Self-test failed!";
      return -1;
    }
  }

また、上ではテスト目的として、期待した出力を確実に得ることができるか確認することができています。

  // Do something interesting with the results we've generated.
  Status print_status = PrintTopLabels(outputs, FLAGS_labels);

最後にTOPラベルをprintしています。

  if (!print_status.ok()) {
    LOG(ERROR) << "Running print failed: " << print_status;
    return -1;
  }

上で書いているエラー処理はTensorFlowのStatusオブジェクトを使用しています。
これを使うことでok()チェッカーでエラーが発生したかどうかを知ることができ、その後にprintして読みやすいエラーメッセージとして表示できるので便利です。

このチュートリアルのケースだと、オブジェクトの認識を実演していますが、他のモデルでも、あらゆるドメインでも非常に似たコードを使用できるはずです。
このサンプルを作り変えることで、いろんなTensorFlowの使い方が考えられるので、参考にしてみてください!

Resources for Learning More

Michael Nielsenのfree online bookは一般的なニューラルネットワークについて学ぶための優れた資料です。
特に畳み込みニューラルネットワーク(CNN)の場合、Chris Olahの素晴らしいブログ記事があります。
また、上のMichael Nielsenの書籍にも素晴らしい章があります。

畳み込みニューラルネットワーク(CNN)の実装についての詳しい部分はTensorFlow deep convolutioal networks tutorialにジャンプするか、ゆっくり始める場合はML begginers(和訳)、ML expert(和訳)から読むといいです。
より深い内容が知りたい場合は、このチュートリアルでリンク付けした論文の動向を読むと良いです。

終わりに

C++の説明がメインとなっていたので少し難しかったですが、画像認識の発展からTensorFlowの内部の実装までが細かく説明されていて良いですね。
また他のCaffeなどのフレームワークも内部でC++が動いているので、他のフレームワークの内部コードの理解にも繋がる重要な知見が書かれていると思います。

TensorFlowチュートリアル和訳(Deep MNIST for Experts)

TensorFlowのチュートリアルを愚直に和訳していきます。
今回はディープラーニングを使ってMNISTを解くチュートリアルです。ようやくCNNに入ってきますね。 MNISTや機械学習の基礎がわからない人はMNIST for ML Begginersの和訳から読むと良いです。 共通している部分もあるので、中級者の方も先に読み飛ばしておくと理解がスムーズです。

Deep MNIST for Experts

TensorFlowは、大規模な数値計算を行うための強力なライブラリです。
TensorFlowが優れているタスクは深いニューラルネットワークを実装して訓練することです。
このチュートリアルでは深い畳み込みMNIST識別器を構築することで、TensorFlowでのモデルの構築方法を学びます。

About this tutorial

このチュートリアルの最初の部分では、Tensorflowモデルの基本的な実装であるmnist_softmax.pyで何が起きているのかを説明します。
後半では精度を改善するいつくかの方法を示します。

このチュートリアルの各コードをPython環境にコピペしても、コードを読むだけでもOKです。

このチュートリアルを読むことで以下のことが達成できます。

  • MNISTの数字認識を行うために、画像内の全てのpixcelに注目してSoftmax回帰関数を作ります。
  • 数千の画像を使って数字認識モデルを訓練します。(それを行うTensorFlowの最初のセッションを構築します。)
  • テストデータを使ってモデルの精度を確認する
  • 結果を改善するために多層畳み込みニューラルネットワーク(CNN)を構築、訓練、テストする

Setup

モデルを作成する前に、まずMNISTデータセットをロードし、TensorFlowセッションを開始します。

Load MNIST Data

以下のコードをコピペすれば、たった2行でMNISTデータをダウンロードして自動的に読み込みを行うことができます。

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

上のmnistは訓練(training)、検証(validation)、テスト(test)の用データセットをNumpy配列として格納する軽量クラスです。
また、データミニバッチを反復処理するための関数も用意されています。

Start TensorFlow InteractiveSession

TensorFlowは非常に効率的なC ++バックエンドを使用して計算を行います。
このバックエンドへの接続をセッションと呼びます。
TensorFlowプログラムの一般的な使い方は、まずグラフを作成してからセッションで起動することです。

ここでは、InteractiveSessionという便利なクラスを使用します。
これによりコードの構造がより柔軟になります。
このクラスを使うことでグラフの実行と演算グラフの構築を交互に行うことができます。

これはIPythonのようなインタラクティブなコンテキストで作業するときに特に便利です。
InteractiveSessionを使用しない場合はセッションを開始してグラフを起動する前に演算グラフ全体を構築する必要があります。

import tensorflow as tf
sess = tf.InteractiveSession()

Computation Graph

Pythonで効率的な数値計算を行う際には通常NumPyのようなライブラリを使用します。 しかし、これはまだ不十分でGPUで計算をする場合やデータを転送するコストが高い場合などには多くのオーバーヘッドを生じさせてしまいます。

TensorFlowはPythonの外でも重い作業をしていますが、このオーバーヘッドを避けるためにさらにステップが必要です。 TensorFlowはPythonとは独立した単一の高価なオペレーションを実行するのではなく、Pythonの外部で実行される対話型操作のグラフを記述します。 (この様なアプローチは他の機械学習ライブラリでも同様に行われています。)

Build a Softmax Regression Model

このセクションでは、単一の線形層を持つsoftmax回帰モデルを構築します。 次のセクションではこれを多層畳み込みネットワークに拡張します。

Placeholders

演算グラフの構築を始めます。まず、入力画像と出力クラス用のノードを作成します。

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

上の定義では、xy_には特定の値が入っていません。これらはplaceholderと呼ばれるもので、TensorFlowに計算させるために用意する値です。

入力画像xは2次元テンソル浮動小数点数で構成され、ここでは[None、784]のサイズを割り当てます。
この784は[28\times 28]次元のMNIST画像のピクセル数に対応しており、Noneは任意サイズを示します。
出力クラスy_は2次元テンソルで構成され、10次元の1ホットベクトルで構成されます。
この10はMNIST画像がどの数字(0~9)の10種類を示します。

placeholderの引数であるshapeはオプションですが、明示的に指定することでTensorFlowがテンソルの要素数を間違えるバグを防ぐことができます。

Variables

ここで、モデルの重みWとバイアスbを定義します。
これらは追加の入力値としてxy_と同様に扱うことができますが、TensorFlowではそれらを処理するための優れた方法を持っています。それがVariableです。
VariableはTensorFlowの演算グラフで使われる値です。
これは演算の中で修正しながら使うことができます。
機械学習の中でモデルパラメータと呼ばれるものの大半がこのVariableに当たります。

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))

上ではtf.Variableの呼び出しで各パラメータの初期値を与えています。
この場合、Wbは要素値が全て0のテンソルとして初期化しています。
Wは784×10の行列です。(入力が784次元でと出力が10次元のため)
bは10次元のベクトルです。(出力が10次元のため)

Variableがセッション内で使用される前に、セッション内で変数を初期化する必要があります。
このステップでは、すでに指定されている初期値(この場合は全ての要素が0となる)を取り、それらを各Variableに割り当てます。
これは全てのVariableに対して同時に実行されます。

sess.run(tf.global_variables_initializer())

Predicted Class and Loss Function

ここでは回帰モデルを実装します。たった1行書くだけです!
以下のように、ベクトル化された入力画像xに重み行列Wを乗算し、バイアスbを加えます。

y = tf.matmul(x,W) + b

損失関数も簡単に指定することができます。
損失はモデルの予測値がどれほど悪いかを示すものです。
モデルの訓練では全てのデータを使って損失を最小化するように学習します。
ここで、以下で使う損失関数は目標値とモデルの予測にsoftmax関数を適用したものとの間でクロスエントロピーをとります。
for ML Biginersのチュートリアルと同様に、以下の安定した定式化を使用します。

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))

このtf.nn.softmax_cross_entropy_with_logitsは正規化していないモデルの予測値にsoftmaxを内部で適用し、全てのクラスで合計を取ります。
さらにtf.reduce_meanはこの合計に対して平均を取っています。

Train the Model

モデルと損失関数を定義したので、訓練を行うことはもう簡単です。
TensorFlowは計算グラフ全体を把握しているため、自動微分を使用して各変数に対する損失の勾配を見つけることができます。
ensorFlowにはさまざまな最適化アルゴリズムが組み込まれています。
この例では、クロスエントロピーを下降させるために勾配をステップで減少させる方式を取り、ステップ長を0.5とします。

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

この1行で実際にTensorFlowが行ったことは、新しい演算を演算グラフに追加することです。
この操作には、勾配の計算、パラメーター更新ステップの計算、更新ステップのパラメーターへの適用、が含まれています。

上のtrain_stepが実行されると、勾配降下の更新がパラメータに適用されます。
つまり、train_stepを以下のように繰り返し実行することでモデルの訓練を行うことができます。

for _ in range(1000):
  batch = mnist.train.next_batch(100)
  train_step.run(feed_dict={x: batch[0], y_: batch[1]})

上の各反復では、100個のトレーニングサンプルデータをbathとして読み込みます。
その後、feed_dictを使用してplace_holderテンソルxy_を各反復用のデータに置き換えて、train_stepの演算を実行します。
ここで、feed_dictをう使うことで演算グラフのテンソルを置き換えることができることに注意してください。
この置き換えplace_holderだけに限定されるものではありません。

Evaluate the Model

作ったモデルはどれぐらい良いものになっているのでしょうか?

ここで、最初に正しくラベルを予測するかどうかを検証してみましょう。
tf.argmaxはある軸に沿ったテンソルの最高エントリーのインデックスを与える極めて有用な関数です。 例えば、tf.argmax(y,1)は学習したモデルが各入力に対して最も可能性が高いと考えるラベルを表し、tf.argmax(y_, 1)には正しいラベルが入っています。 この予測値と正解が一致するかどうかはtf.equalを使って調べることができます。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

上の操作の結果はbooleanのリストで返されます。 どの部分が正しいかを判断するために、浮動小数点にキャストして平均をとります。 例えば[True, False, True, True][1, 0, 1, 1]になり、平均をとると0.75になります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最後に、テストデータでの精度を求めます。この結果はだいたい92%くらいになるはずです。

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

Build a Multilayer Convolutional Network

MNISTで精度92%というのは悪い値です。恥ずかしいほどに。
このセクションでは、モデルを小さな畳み込みニューラルネットワーク(CNN)に変更します。
これにより、精度が99.2%になります。 これは最先端の精度ではないですが、それに近いものです。

Weight Initialization

このモデルを作成するには、多くのweightとbiasを作成する必要があります。
パラメータを多くする場合、勾配が0になるのを防止したり対称性を破壊するために、少量のノイズでweightを初期化する必要があります。
ここではReLu)を使用しているので、微小な正の初期バイアスをかけて初期化を行うことで「ニューロンが死ぬ」ことを避ける習慣が必要です。
モデルを構築する時にこれを何度も行うのは面倒なので、以下で2つの関数を定義しましょう。

def weight_variable(shape):
  initial = tf.truncated_normal(shape, stddev=0.1)
  return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

Convolution and Pooling

TensorFlowは畳み込みとプーリングの操作で多くの柔軟性を持ちます。
境界の処理はどうしよう?stride sizeを何にしよう?という疑問があると思います。
この例では、常にバニラバージョンを選択します。
TensorFlowでの畳み込みのデフォルトではstride sizeが1で出力が入力と同じサイズになるように0で埋められます。
また、プーリングでは2x2ブロックのmax poolingを行います。
コードを綺麗に保つために、この操作を以下の関数で抽象化しましょう。

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

First Convolution Layer

これで最初のレイヤーを実装できます。最初のレイヤーは畳み込みとmax poolingで構成されます。
この畳み込みでは5x5パッチごとに32個の特徴を計算します。
この時の重みテンソル[5, 5, 1, 32]の形状を取ります。この形状の最初の2次元はパッチサイズを表しており、その次が入力チャネル数、最後は出力チャネル数を表しています。
さらに、この出力チャネルに対応したバイアスベクトルも用意します。 この時の重みとバイアスは以下のようになります。

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

このレイヤーを適用するには、最初にxを4dテンソルに再構成します。
つまり、第2次元と第3次元を画像の幅と高さにし、カラーチャネルの数を第4次元にしたもので以下のようにxx_imageとして作り直します。

x_image = tf.reshape(x, [-1,28,28,1])

その後x_imageを上の重みで畳み込み、バイアスを加え、ReLu関数を適用し、最後にmax poolingを適用します。
max_pool_2x2メソッドは画像のサイズを14x14に縮小します。

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

Second Convolution Layer

より深いネットワークを構築するために、上で作ったタイプのレイヤーを複数スタックします。
2番目のレイヤーは、5x5パッチごとに64個の特徴を計算します。

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

Densely Connected Layer

2回目のmax poolingで画像サイズは7x7に縮小されています。
さらに、画像全体を処理できるように1024個のニューロンを備えたフルコネクションレイヤーを追加します。 以下では、テンソルをプーリング層からベクトルのバッチに再構成し、重み行列を乗算し、バイアスを加え、ReLuを適用します。

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

Dropout

パラメータのオーバーフィット(過学習)を軽減するために、読み出しレイヤーの前にdropoutを適用します。
まず、dropout中にニューロンの出力が保持される確率を表すplaceholderを作成します。
これにより、トレーニング時にはdropoutを適用し、テスト時にはオフにすることができます。
TensorFlowのtf.nn.dropoutはマスキングに加えてスケーリングニューロンの出力を自動的に処理するので、dropout自体は追加のスケーリングなしで動作します。

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

Readout Layer

最後に以前のセクション(単層sofrtmax回帰)でやったとの同様のレイヤーを追加します。

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

Train and Evaluate the Model

このモデルはどれくらいうまく機能するのでしょうか?
それを訓練して評価するのは以前のセクション(単層softmax回帰)と同じコードで可能です。

違う部分は以下です。

  • 最急勾配降下最適化法をより良いADAMオプティマイザーに変更する
  • feed_dictに追加パラメータkeep_probを追加して、dropout率を制御する
  • 訓練100回ごとにログを吐き出す

以下のコードをコピペして実行すると訓練と評価が可能ですが、2万回の訓練を行なっているので、(プロセッサにもよりますが)おそらく30分 ぐらいかかると思います。

cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.global_variables_initializer())
for i in range(20000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

このコードを実行すると、最終的な精度は99.2%になっています。

チュートリアルは以上です。
このチュートリアルを行うことで、TensorFlowを使ってより洗練されたディープな学習モデルを素早く簡単に構築し、訓練し、評価する手法を学ぶことができましたね。

終わりに

MNIST Fro ML Beginnersの和訳を読んでいる方はsoftmax回帰の詳細などについての理解ができているはずなので、今回はすんなり理解できそうですね。 ConvolutionやPoolingに入るまでで躓いたり、理解が少し難しかった方は一読されると良いかもしれません。

TensorFlowチュートリアル和訳(MNIST For ML Beginners)

TensorFlowのチュートリアルを愚直に和訳していきます。
こちらは「MNISTが何か?〜機械学習の基礎をTensorFlowでどう表現するか?」を説明するセクションとなっています。

以下では、TensorFlowのインストールができていることが前提で進めていきます。

MNIST For ML Beginners

このチュートリアルは、機械学習とTensorFlowの両方を初めて使用する読者を対象としています。
すでにMNIST・softmax(多項ロジスティック)回帰が何であるかを知っている人は速めのチュートリアルの方が良いかもしれません。

プログラムの仕方を学ぶとき、最初に行うのは “Hello World"のプリントという伝統がありますね。プログラミングにHello Worldがあるように機械学習にはMNISTがあります。
MNISTは単純な画像認識のデータセットです。これは次のような手書き数字の画像で構成されています。

f:id:minison130:20170504115729p:plain

MNISTの各画像はそれぞれ手書きの数字を表しており、上記の画像のラベルは5,0,4,1を表しています。

このチュートリアルでは、画像を見てその数字が何かを予測するモデルを訓練します。
本章での目標は、最先端のパフォーマンスを実現する精巧なモデルを訓練することではありません。(後でそれを行うコードを提供します!)
TensorFlowを使ってMNISTを解き始めることが本章の目標となっており、Softmax回帰と呼ばれる非常に単純なモデルから始めます。

このチュートリアルで用いるコードは非常に短く、大事な部分は3行しかありません。
しかし、その背後にある考え方、つまりTensorFlowがどのように動作するのか、そして機械学習の中核概念を理解することは非常に重要です。
このため、コードを実行するまでを非常に慎重に説明していきます。

About this tutorial

このチュートリアルではmnist_softmax.pyコードで何が起こっているかを一行ずつ説明していきます。

このチュートリアルで以下が達成できます。 * MNISTデータとソフトマックス回帰について学ぶ * 画像内の全ての画素を見て、数字を認識するためのモデルである関数を作成する * 数千のMNIST画像を"見る"ことによって手書き数字を認識するモデルをTensorFlowで訓練する(そして、そのための最初のTensorFlowセッションを実行する) * 作ったモデルのaccuracyをテストデータを使って確認する

The MNIST Data

MNISTデータはYann LeCunのウェブサイトでホストされています。
以下のコードを使うことで、2行でMNISTデータを自動的にダウンロードして読み込むことができます。

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

MNISTデータは55000個のトレーニングデータ(mnist.train)と10000個のテストデータ(mnist.test)と5000個のバリデーションデータ(mnist.validation)の3つに分割されます。
この分割は非常に重要です。機械学習ではデータの一部を学習に使わずにとっておいてモデルが一般化されているかを確認するために用いることが不可欠です!

MNISTデータは画像と対応する数字のラベルの2つからできており、以降では画像を"x“, ラベルを”y“と呼ぶこととします。
トレーニングセット、テストセットにもそれぞれ両方の要素が含まれています。例えば、トレーニング画像はmnist.train.imagesでラベルはmnist.train.labelsに入っています。

MNISTの各画像は28×28ピクセルでできています。これは次のように28×28次元の行列として解釈することができます。

f:id:minison130:20170504123514p:plain

この行列は 28 * 28=784の要素からなるベクトルに平坦化することができます。画像間で一貫した手法で平坦化している限り、画像をどのように平坦化するかは問題ではありません。
この観点から見ると、MNIST画像は非常に豊富な構造を備えた784次元のベクトル空間内の単なる一点に過ぎません。

データを平坦化すると画像の2D構造に関する情報が破棄されますが、これは悪いことではないのでしょうか?
後のチュートリアルで説明する高度なモデルでは、この構造は悪い使われ方がされしまっています。
ただ、この章で使用するsoftmax回帰は違います。

mnist.train[55000, 784]という形のテンソル( n次元配列)で表されます。第1次元は画像自体のindex、第2次元は各画像 内のピクセルのindexを表します。
MNISTは2値画像なので、各ピクセルの値は0か1をとります。

f:id:minison130:20170504125046p:plain

MNISTの各画像には対応するラベルが付いています。画像に描かれている桁を表す0〜9の数字です。

このチュートリアルでは、ラベルを「ワンホットベクトル」にしたいと考えています。ワンホットベクトルとはある単一次元が1でそれ以外が0からなるベクトルのことを言います。 MNISTでは数字nを表す画像のラベルをn番目の次元が1になるワンホットベクトルとして表します。 例えば3は[0,0,0,1,0,0,0,0,0,0]となります。
したがって、mnist.train.labelsは[55000, 10]の配列となります。

f:id:minison130:20170504125800p:plain

ここまで説明して、やっとモデルを実際に作っていくための準備が整いました!

Softmax Regressions

これまでの説明で、MNISTのすべての画像が0〜9の手書き数字であることは分かっているはずです。 つまり、与えられた画像が何であるかのパターンは10通りしかありません。

手書きの数字は100%確実にある数字に読めるとは限りません。例えば0と6などの境目は難しいです。
そのため、機械学習では画像を見てその数字が何であるかを確率で表現します。つまり、学習で出来たモデルは9の画像を見てそれが9であることを80%確信している、というような表現になります。

Softmax回帰は自然でシンプルな古典的手法です。あるオブジェクトに対して何らか確率を割り当てたい場合、softmaxは[0:1]の値を与える手法なので目的とマッチしています。
そのため、後のセクションで説明する高度に洗練されたモデルでも最後のステップはsoftmaxを行う層で出来ています。

Softmax回帰は以下2つのステップから成ります。

  1. 入力から得たevidenceを特定のクラスに追加する
  2. その後evidenceを確率値に変換する

Softmax回帰では与えられた画像が何の数字を表すかを示す証拠(evidence)を集めるためにピクセル強度の加重和をとります。(つまり、各数字の特徴となるエッジを調べている操作に近いです。)
この加重和に使われる重みは、ある数字を表す画像毎に共通した特徴が見られる部分は正になり、共通しない特徴が見られる部分はが負になるように学習されます。

次の図は、1~9までの各ラベルについて学習した重みを表しています。
赤は負の重みを表し、青は正の重みを表しています。

f:id:minison130:20170504203301p:plainf:id:minison130:20170504203301p:plain

図を見てわかるように、重みの学習だけでは数字の証拠をうまく捉えることは出来ていません。 そこで、Softmax回帰ではバイアスと呼ばれる追加の証拠を学習します。基本的に、このバイアスの学習は入力から独立しているように見えます。
以上より、入力ベクトル xを与えた時のクラス iの証拠は次のようになります。

 {\rm evidence}_{i} = \sum_{j}W_{i, j}x_{j} + b_{i}

ここで、 W_iが上で書いていたクラス iの重みであり、 b_iがクラス iのバイアスを表します。

次に、「softmax」関数を使用して求めた証拠の集計値を予測関数 yに変換します。

 y = {\rm softmax}({\rm evidence})

ここでのsoftmaxはディープラーニングにおける「活性化関数」もしくは「リンク関数」と呼ばれる機能を果たします。つまり、線形関数の出力を必要な形、この場合は10以上の確率分布に整形します。
この操作により、求めた証拠の集計値が確率値に変換されます。これは以下の様に定義出来ます。

 {\rm softmax}(x)={\rm normalize(\exp(x))}

上式を拡張すると次の様になります。

 {\rm softmax}(x)_i = \cfrac{\exp(x_i)}{\sum_j \exp(x_j)}

ごちゃごちゃ書いてきましたが、softmax回帰は次の様な図で表すことが出来ます。簡単化のために図は3次元にしていますが、実際はもっと多いです。(MNISTは28*28=784次元)

f:id:minison130:20170504231337p:plain

上図を数式で書きあらわすと、次の様になります。

f:id:minison130:20170504231629p:plain

さらに上式をベクトル演算で簡単化すると次の様になります。

f:id:minison130:20170504231904p:plain

最後に、上式をさらに簡単化すると以下の様になります。

 y = {\rm softmax}(Wx + b)

ここまで来て、やっとTensorFlowで使える形になりました。 以下で実装をしていきましょう!

Implementing the Regression

Pythonで効率的な数値計算を行う際には通常NumPyのようなライブラリを使用します。
しかし、これはまだ不十分でGPUで計算をする場合やデータを転送するコストが高い場合などには多くのオーバーヘッドを生じさせてしまいます。

TensorFlowはPythonの外でも重い作業をしていますが、このオーバーヘッドを避けるためにさらにステップが必要です。
TensorFlowはPythonとは独立した単一の高価なオペレーションを実行するのではなく、Pythonの外部で実行される対話型操作のグラフを記述します。
(この様なアプローチは他の機械学習ライブラリでも同様に行われています。)

TensorFlowを使うための最初のステップは以下のimportを行うことです。

import tensorflow as tf

ここでは、シンボル変数を操作することによってTensorFlowの動作を説明します。まず、以下の様にxを定義してみましょう。

x = tf.placeholder(tf.float32, [None, 784])

上の様に書いた時、xには特定の値は入らず、あくまでplaceholderとなります。
これはTensorFlowに計算を実行させるために、用意する値です。
ここには784次元のベクトルに平坦化された任意の数のMNIST画像を入力することが出来ます。
TensorFlowでは、これを[None, 784]の形状の2Dテンソルとして定義しています。 (ここで、Noneは次元が任意の長さであることを意味しています。)

モデルを作るためには重みWとバイアスbが必要です。
ディープラーニングでは多くの変数を追加して扱うことになるので、TensorFlowはそれを処理する便利な手法であるVariableを持っています。
このVariableはTensorFlowのグラフでの相互動作の際に値を保持しつつ変更が可能なテンソルです。Variableは計算の際に使うことも修正することも出来ます。
一般的に機械学習において、モデルのパラメータは基本的にVariableとなります。

W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

Variableの定義の際にはtf.Variableを使って初期値を与えます。
上の場合、Wbの両方を値が全て0のテンソルとして初期化しています。
このWbは学習して更新していく値なので、最初は全て0が入っていても問題ありません。

上ではWが[784, 10]の形状をとることに注意してください。
これは784次元の画像ベクトルxWをかけて、10次元のoutputを得たいためです。 また、bWxに足し合わせるので同じく10次元の形状となります。

以上定義した値を使うことで、説明したSoftmax回帰モデルを実装することができます。なんと1行で以下の様にかけます!

y = tf.nn.doftmax(tf.matmul(x, W) + b)

上ではまず、xW内積tf.matmul(x, W)という式で計算します。
(x, W)という書き方をしますが、TensorFlowの中で計算が反転して Wxが得られることになります。
その次にbを追加して、最後にtf.nn.softmaxを適用します。

モデルの定義は以上でおしまいです。
モデルを定義するには、セットアップ数行とモデル定義の1行のみが必要なだけです。
これはTensorFlowによって簡単化されている訳ではなく、様々な数値計算に対応した非常に柔軟な方法なので、同様の操作を他の機械学習モデルから物理シミュレーションまで、様々な場面で使うことができます。
さらに一度定義したモデルはコンピュータのCPU、GPU、モバイル端末など様々なデバイスで実行することができます!

Traning

モデルを訓練するためには、モデルの良さ・悪さを定義する必要があります。  これは、コストや損失と呼ばれるものを利用することで可能です。
この損失はモデルが希望の結果からどれだけ離れているかを表すものです。
モデルの訓練ではこの誤差を最小限に抑える様にパラメータを学習させ、最も誤差マージンが少ないモデルが得られると良いモデルができたことになります。

モデルの損失を決定するための最も一般的で優れた関数の1つが「cross-entropy」です。
クロスエントロピーは情報を圧縮するための学問から生まれた関数ですが、今ではギャンブルから機械学習まで多くの分野で重要なアイデアとなっています。
それは次のように定義されます。

 H_{\grave{y}}(y)=-\sum_{i} {\grave{y}_{i}}\log(y_{i})

ここで、 yは予測確率分布、 \grave{y}は真分布(桁ラベル付きのワンホットベクトル)を指します。
粗い意味で、クロスエントロピーは、我々の予測が真実を記述するためにどれほど非効率的であるかを測定しています。
クロスエントロピーの詳細については、このチュートリアルの範囲を超えていますが、ここで理解を深めるだけの価値があります。

クロスエントロピーを実装するには、最初に新しいplaceholderを追加して正解を入力する必要があります。

y_ = tf.placeholder(tf.float32, [None, 10])

次にクロスエントロピー関数 -\sum \grave{y} \log(y)を実装します。

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

上のcroos_entropyの定義において、まず、tf.logyの各要素の対数を計算します。
次にy_の各要素にtf.log(y)の対応する要素をかけます。
その次に、tf.reduce_sumyの2番目の次元に要素を追加します。
最後にtf.reduce_meanでバッチ内の全ての例の平均を計算します。

実際のソースコードでは数値的な不安定さがあるため、この定式は使用しないことに注意してください。
代わりにtf.nn.softmax_cross_entropy_with_logitを利用しています。

モデルがしたいことが分かっているので、TensorFlowにそれを訓練させることはとても簡単です。
TensorFlowは計算のグラフ全体を把握しているため、自動的にバックプロパゲーションアルゴリズムを使用して、変数が最小化する損失にどのように影響するかを効率的に判断できます。
次に、以下で最適化アルゴリズムの選択を適用して変数を変更し、損失を減らすことができます。

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

上では、TensorFlowに学習率0.5の勾配降下アルゴリズムを使用してcross_entropyを最小にするように頼んでいます。
勾配降下は簡単な手順で、コストを削減する方向に各変数を少しだけシフトさせているだけです。
TensorFlowは他にも多くの最適化アルゴリズムを提供しています。
使い分けるのは上の1行を微調整するだけです。

裏でTensorFlowが実際に行っていることは、バックプロパゲーションと勾配降下を実装する新しい操作をグラフに追加することです。
そして実行時に勾配降下訓練のステップを行い変数を微調整して損失を減らす、という1回の操作を返します。

これでモデルをInteractiveSessionで開始することができます。

sess = tf.InteractiveSession()

セッションを開始すると、以下の操作で最初に作成した変数を初期化する必要があります。

tf.global_variables_initializer().run()

それでは、1000回の学習を行ってみましょう!

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

ループの各ステップでは、トレーニングセットから100個のランダムなデータポイントの「バッチ」を取得します。 そして各バッチごとにtrain_stepフィードを実行してplaceholderを置き換えます。

ランダムデータからなる小さなバッチを使って学習することは、この場合、確率的勾配降下法と呼びます。
理想的には、訓練のすべての段階ですべてのデータを使用したいと思っています。
その方がやりたいことをより深く理解させることができますが、高価になります。
したがって代わりに、毎回異なるサブセットを使用することで安価で同じ利益を得ています。

Evaluating Our Model

学習したモデルはどれぐらい良いものになっているのでしょうか?

ここで、最初に正しくラベルを予測するかどうかを検証してみましょう。
tf.argmaxはある軸に沿ったテンソルの最高エントリーのインデックスを与える極めて有用な関数です。
例えば、tf.argmax(y,1)は学習したモデルが各入力に対して最も可能性が高いと考えるラベルを表し、tf.argmax(y_, 1)には正しいラベルが入っています。
この予測値と正解が一致するかどうかはtf.equalを使って調べることができます。

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))

上の操作の結果はbooleanのリストで返されます。
どの部分が正しいかを判断するために、浮動小数点にキャストして平均をとります。
例えば[True, False, True, True][1, 0, 1, 1]になり、平均をとると0.75になります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

最後に、テストデータでの精度を求めます。

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

この結果はだいたい92%くらいになるはずです。

これは良い結果なのでしょうか?実は違っていて、実際はなかなか悪い値です。
非常に単純なモデルを利用しているため、まだまだ精度が出ていません。
しかし、モデルに小さな変更を加えることで精度は97%まであげることができます。
また、最高のモデルだと精度は99.7%に到達しています!
(詳細については結果リストを見てください)

重要なことは精度ではなく、モデルの組み方や機械学習の基礎などを学んだことです。 より精度をだすモデルがきになる人は次のチュートリアルに進めば、より洗練されたモデルの構築方法を知ることができます!

終わりに

疲れた。。
softmax回帰の操作などはところどころ意訳して分かりやすくしたつもりですが、文章だけで理解するのは難しいですね。 機械学習の理論的な部分の理解が難しい方はWeightとbiasをパラメータとして学習することで、モデルが出来上がることが理解できれば問題ないように思います。

TensorFlowチュートリアル和訳(GPUの利用)

TensorFlowのチュートリアルを随時和訳していきます。 第一回目はGPUの利用編です。

Supported devices

TensorFlowはCPUとGPUの利用をサポートします。これらはstringで以下のように表現します。

  • "/cpu:0" :マシンのCPU
  • "/gpu:0" :マシンのGPU(1つめ)
  • "/gpu:1" :マシンのGPU(2つ以上持っている場合は番号でサポート)

TensorFlow内の動作でCPUとGPUの両方が実装されている場合、GPUバイスにはそのデバイスに割り当てられた操作が優先されます。例えばmatmul(内積の計算)にはCPUカーネルGPUカーネルの両方があります。デバイスcpu:0gpu:0を持つシステムの場合、このmatmulの実行ではgpu:0が自動的に選択されます。

Logging Device placement

操作やテンソルが割り当てられているデバイスを調べる際にはlog_device_placementオプションをTrueに設定してセッションを作成します。

# グラフの作成
a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
# log_device_placementをTrueに設定してセッションを作成
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# セッションを開始
print(sess.run(c))

このようにすると以下のようなoutputが得られます。

Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: Tesla K40c, pci bus
id: 0000:05:00.0
b: /job:localhost/replica:0/task:0/gpu:0
a: /job:localhost/replica:0/task:0/gpu:0
MatMul: /job:localhost/replica:0/task:0/gpu:0
[[ 22.  28.]
 [ 49.  64.]]

グラフの内容やmatmulの計算がgpu:0に割り当てられていることが分かりますね。

Manual device placement

自動的に選択されたデバイスを利用せずに、特定の動作を行うデバイスを指定したい場合はtf.deviceを使用することができます。これによってコンテキスト内のすべての操作が同じデバイス割り当てを持つようになります。

# グラフの作成
with tf.device('/cpu:0'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
  c = tf.matmul(a, b)
# log_device_placementをTrueに設定してセッションを作成
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# セッションを開始
print(sess.run(c))

これによって通常はgpu:0に割り当てられるa,bcpu:0に割り当てられるように変更されます。

Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: Tesla K40c, pci bus
id: 0000:05:00.0
b: /job:localhost/replica:0/task:0/cpu:0
a: /job:localhost/replica:0/task:0/cpu:0
MatMul: /job:localhost/replica:0/task:0/gpu:0
[[ 22.  28.]
 [ 49.  64.]]

Allowing GPU memory growth

デフォルトでは、TensorFlowはGPUのメモリのほぼ全てをプロセスにマッピングします。これはメモリの断片化を軽減するために行なっており、これによって貴重なGPUメモリ資源を効率的に使用することができます。

ただ、場合によっては利用可能なメモリのサブセットを用意して、基本的にはサブセットのみを割り当て、プロセスによってGPUの使用量を増加させた方が良い場合もあります。TensorFlowにはこれを制御するための2つのConfigオプションがあります。

1つ目はallow_growthオプションです。これはランタイム割り当てに基づいてGPUメモリを割り当てることができます。このオプションを利用する場合、最初プロセスには微小のGPUメモリしか割り当てられず、必要に応じてGPUメモリ領域を拡張していきます。ただ、メモリの断片化を防ぐためにメモリの解放はしないことに注意してください。 このオプションをオンにするには、以下の方法でConfigProtoに設定します。

config = tf.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.Session(config=config, ...)

2つ目はper_process_gpu_memory_fractionオプションです。このオプションでは、TensorFlowが見えているGPUメモリ量から、割り当てるメモリの割合を決めることができます。 例えば、以下のようにするとTensorFlowに各GPUの合計メモリの40%を割り当てることができます。

config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.4
session = tf.Session(config=config, ...)

このオプションはTensorFlowで利用可能なGPUメモリ量を制限したい時に便利な手法です。

Using a single GPU on a multi-GPU system

システムに複数のGPUがある場合、最も低いIDを持つGPUがデフォルトで選択されます。別のGPUで実行したい場合は、環境設定で明示的に指定する必要があります。

# グラフの作成
with tf.device('/gpu:2'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
  c = tf.matmul(a, b)
# log_device_placementをTrueに設定してセッションを作成
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# セッションを開始
print(sess.run(c))

指定したデバイスが存在しない場合、以下のようなInvalidArgumentErrorが出ます。

InvalidArgumentError: Invalid argument: Cannot assign a device to node 'b':
Could not satisfy explicit device specification '/gpu:2'
   [[Node: b = Const[dtype=DT_FLOAT, value=Tensor<type: float shape: [3,2]
   values: 1 2 3...>, _device="/gpu:2"]()]]

指定したデバイスが存在しない場合にエラーを吐かずに自動的にデバイスを選択して操作を実行したい時は、セッションの作成時の設定オプションでallow_soft_placementTrueにします。

# グラフの作成
with tf.device('/gpu:2'):
  a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
  b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
  c = tf.matmul(a, b)
# allow_soft_placementとlog_device_placementをTrueにしてセッションを作成
sess = tf.Session(config=tf.ConfigProto(
      allow_soft_placement=True, log_device_placement=True))
# セッションを開始
print(sess.run(c))

Using multiple GPUs

複数のGPUでTensorFlowを実行したい場合、multi-tower形式を構築します。これにより、各Towerごとで別のGPUを利用することができます。例えば、

# グラフの作成
c = []
for d in ['/gpu:2', '/gpu:3']:
  with tf.device(d):
    a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3])
    b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2])
    c.append(tf.matmul(a, b))
with tf.device('/cpu:0'):
  sum = tf.add_n(c)
# log_device_placementをTrueにしてセッションを作成
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# セッションを開始
print(sess.run(sum))

のようにすると以下のようなoutputが得られます。

Device mapping:
/job:localhost/replica:0/task:0/gpu:0 -> device: 0, name: Tesla K20m, pci bus
id: 0000:02:00.0
/job:localhost/replica:0/task:0/gpu:1 -> device: 1, name: Tesla K20m, pci bus
id: 0000:03:00.0
/job:localhost/replica:0/task:0/gpu:2 -> device: 2, name: Tesla K20m, pci bus
id: 0000:83:00.0
/job:localhost/replica:0/task:0/gpu:3 -> device: 3, name: Tesla K20m, pci bus
id: 0000:84:00.0
Const_3: /job:localhost/replica:0/task:0/gpu:3
Const_2: /job:localhost/replica:0/task:0/gpu:3
MatMul_1: /job:localhost/replica:0/task:0/gpu:3
Const_1: /job:localhost/replica:0/task:0/gpu:2
Const: /job:localhost/replica:0/task:0/gpu:2
MatMul: /job:localhost/replica:0/task:0/gpu:2
AddN: /job:localhost/replica:0/task:0/cpu:0
[[  44.   56.]
 [  98.  128.]]

cifar10のチュートリアルは複数GPUを使用するのに良いトレーニングです。

終わりに

TensorFlowにおけるGPUの利用についてまとめました。初回から少し地味な内容ですが、ディープラーニングGPUは不可欠なので、最初に勉強しておいて損はないはずです。 また、GPUマシンは高価なので、複数人でマシンを共有して利用しているケースも少なくないと思います。このような場合は`per_process_gpu_memory_fractionによって個々人が利用できるGPUメモリを制限するのはとても大切ですね。