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++が動いているので、他のフレームワークの内部コードの理解にも繋がる重要な知見が書かれていると思います。