TensorFlow : Tutorials : Sequences : お絵描き分類のためのリカレント・ニューラルネットワーク (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 07/16/2018
作成日時 : 05/30/2018
* TensorFlow 1.9 でドキュメント構成が変わりましたので調整しました。
* 本ページは、TensorFlow 本家サイトの Tutorials – Sequences – Recurrent Neural Networks for Drawing Classification を
翻訳した上で適宜、補足説明したものです:
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
Quick, Draw! はプレーヤーが幾つかの物体を描くことに挑戦してコンピュータがそのお絵描きを認識できるかを見るゲームです。
Quick, Draw! の認識は、x と y の点のストロークのシークエンスとして与えられる、ユーザ入力を取ってそしてユーザが描こうとするオブジェクトカテゴリーを認識する分類器により遂行されます。
このチュートリアルではこの問題に対する RNN ベースの認識器をどのように構築するかを示します。モデルはお絵描きを分類するために畳み込み層、LSTM 層、そして softmax 出力の組み合わせを使用します :
上の図はこのチュートリアルで構築するモデルの構造を示しています。入力はお絵描きで、x, y, そして n の点のストロークのシークエンスとしてエンコードされます。ここで n は点が新しいストロークの最初の点であるかどうかを示します。
そして 1-次元畳み込みのシリーズが適用されます。それから LSTM 層が適用されて既知のお絵描きのクラスの中から分類の決定を行なうために総ての LSTM ステップの出力の総和が softmax 層に供給されます。
このチュートリアルは 公に利用可能な 実際の Quick, Draw! ゲームからのデータを使用します。このデータセットは 345 カテゴリーの 50M のお絵描きから成ります。
チュートリアル・コードを実行する
このチュートリアルのためのコードを試すには :
- チュートリアル・コード をダウンロードします。
- TFRecord フォーマットのデータを ここ からダウンロードしてそれを unzip します (訳注 : データは約 1 GB 程度あります)。
- このチュートリアルで記述されている RNN ベースのモデルを訓練するために次のコマンドでチュートリアル・コードを実行します。ダウンロードから unzip されたデータを指すようにパスは調整してください。
python train_model.py \ --training_data=rnn_tutorial_data/training.tfrecord-?????-of-????? \ --eval_data=rnn_tutorial_data/eval.tfrecord-?????-of-????? \ --classes_file=rnn_tutorial_data/training.tfrecord.classes
チュートリアル詳細
データをダウンロードする
このチュートリアルで使用する、TFExamples を含む TFRecord ファイルとして利用可能なデータを作成しました。そのデータはここからダウンロードできます :
http://download.tensorflow.org/data/quickdraw_tutorial_dataset_v1.tar.gz
代わりに ndjson 形式の元データを Google cloud からダウンロードしてそしてそれを次のセクションで説明されるように TFExamples を含む TFRecord ファイルに貴方自身で変換することができます。
オプション: 完全な Quick Draw データをダウンロードする
完全な Quick, Draw! データセット はカテゴリーに分かれた ndjson ファイルとして Google Cloud Storage 上で利用可能です。Cloud Console でファイルのリストをブラウズ することができます。
データをダウンロードするため、データセット全体をダウンロードするために gsutil を使用することを推奨します (訳注: 原文まま)。元の .ndjson ファイルは ~22GB のダウンロードを必要とすることに注意してください。
それからgsutil インストールが動作してデータバケットにアクセスできることを確認するために次のコマンドを使用してください :
gsutil ls -r "gs://quickdraw_dataset/full/simplified/*"
これは次のようなファイルの長いリストを出力するでしょう :
gs://quickdraw_dataset/full/simplified/The Eiffel Tower.ndjson gs://quickdraw_dataset/full/simplified/The Great Wall of China.ndjson gs://quickdraw_dataset/full/simplified/The Mona Lisa.ndjson gs://quickdraw_dataset/full/simplified/aircraft carrier.ndjson ...
それからフォルダを作成してデータセットをそこにダウンロードします。
mkdir rnn_tutorial_data cd rnn_tutorial_data gsutil -m cp "gs://quickdraw_dataset/full/simplified/*" .
このダウンロードはしばらくかかりそして 23GB より少し大きいデータをダウンロードします。
オプション: データを変換する
ndjson ファイルを tf.train.Example protos を含む TFRecord ファイルに変換するためには次のコマンドを実行します。
python create_dataset.py --ndjson_path rnn_tutorial_data \ --output_path rnn_tutorial_data
これはデータを訓練のためのクラス毎 10000 項目と評価データのための 1000 項目を持つ TFRecord ファイルの 10 シャードにストアします。
この変換プロセスは以下でより詳細に説明されます。
元の QuickDraw データは ndjson ファイルとしてフォーマットされ、そこでは各行は次のような JSON オブジェクトを含みます :
{"word":"cat", "countrycode":"VE", "timestamp":"2017-03-02 23:25:10.07453 UTC", "recognized":true, "key_id":"5201136883597312", "drawing":[ [ [130,113,99,109,76,64,55,48,48,51,59,86,133,154,170,203,214,217,215,208,186,176,162,157,132], [72,40,27,79,82,88,100,120,134,152,165,184,189,186,179,152,131,114,100,89,76,0,31,65,70] ],[ [76,28,7], [136,128,128] ],[ [76,23,0], [160,164,175] ],[ [87,52,37], [175,191,204] ],[ [174,220,246,251], [134,132,136,139] ],[ [175,255], [147,168] ],[ [171,208,215], [164,198,210] ],[ [130,110,108,111,130,139,139,119], [129,134,137,144,148,144,136,130] ],[ [107,106], [96,113] ] ] }
分類器を構築する目的のためには “word” と “drawing” フィールドをケアするだけです。ndjson ファイルを解析する間、drawing フィールドからのストロークを連続するポイントの差異を含むサイズ [number of points, 3] の tensor に変換する関数を使用してそれらを一行ずつ解析します。この関数はまたクラス名を文字列として返します。
def parse_line(ndjson_line): """Parse an ndjson line and return ink (as np array) and classname.""" sample = json.loads(ndjson_line) class_name = sample["word"] inkarray = sample["drawing"] stroke_lengths = [len(stroke[0]) for stroke in inkarray] total_points = sum(stroke_lengths) np_ink = np.zeros((total_points, 3), dtype=np.float32) current_t = 0 for stroke in inkarray: for i in [0, 1]: np_ink[current_t:(current_t + len(stroke[0])), i] = stroke[i] current_t += len(stroke[0]) np_ink[current_t - 1, 2] = 1 # stroke_end # Preprocessing. # 1. Size normalization. lower = np.min(np_ink[:, 0:2], axis=0) upper = np.max(np_ink[:, 0:2], axis=0) scale = upper - lower scale[scale == 0] = 1 np_ink[:, 0:2] = (np_ink[:, 0:2] - lower) / scale # 2. Compute deltas. np_ink = np_ink[1:, 0:2] - np_ink[0:-1, 0:2] return np_ink, class_name
書くためにデータがシャッフルされることを望むのでランダム順序でカテゴリー・ファイルの各々から読みそしてランダム・シャードに書きます。
訓練データのために各クラスの最初の 10000 項目を読みそして評価データのために各クラスの次の 1000 項目を読みます。
それからこのデータは shape [num_training_samples, max_length, 3] の tensor に再フォーマットされます。それからスクリーン座標における元のお絵描きのバウンディング・ボックスを決定してそしてお絵描きが単位 (= unit) 高さを持つようにサイズを正規化します。
最後に、連続ポイントの差異を計算してこれらをキー ink のもとに VarLenFeature として tensorflow.Example にストアします。更に class_index を単一のエントリ FixedLengthFeature としてそして ink の shape を長さ 2 の FixedLengthFeature としてストアします。
モデルを定義する
モデルを定義するために新しい Estimator を作成します。
モデルを構築するために :
- 入力を元の shape に reshape して戻します – そこではミニバッチはコンテンツの最大長にパディングされます。ink データに加えて各サンプルのための長さとターゲット・クラスもまた持ちます。これは関数 _get_input_tensors 内で起きます。
- 入力を _add_conv_layers の畳み込み層のシリーズへ渡し通します。
- 畳み込みの出力を _add_rnn_layers の bidirectional LSTM 層のシリーズへ渡します。その最後に、各 time ステップのための出力は入力の簡潔で固定長の埋め込みを持つように総計されます。
- _add_fc_layers の softmax 層を使用してこの埋め込みを分類します。
コードではこれは次のように見えます :
inks, lengths, targets = _get_input_tensors(features, targets) convolved = _add_conv_layers(inks) final_state = _add_rnn_layers(convolved, lengths) logits =_add_fc_layers(final_state)
_get_input_tensors
入力特徴を得るために最初に特徴辞書から shape を得てそして入力シークエンスの長さを含むサイズ [batch_size] の 1D tensor を作成します。ink は特徴辞書の SparseTensor としてストアされてこれを dense tensor に変換してから[batch_size, ?, 3] になるように reshape します。そして最後に、ターゲットが渡された場合それらがサイズ [batch_size] の 1D tensor としてストアされることを確実にします。
コードではこれはこのように見えます :
shapes = features["shape"] lengths = tf.squeeze( tf.slice(shapes, begin=[0, 0], size=[params["batch_size"], 1])) inks = tf.reshape( tf.sparse_tensor_to_dense(features["ink"]), [params["batch_size"], -1, 3]) if targets is not None: targets = tf.squeeze(targets)
_add_conv_layers
畳み込み層とフィルタの長さの望む数は params 辞書のパラメータ num_conv と conv_len を通して configure されます。
入力はシークエンスで各ポイントは次元 3 を持ちます。私達は 1D 畳み込みを使用します、そこでは 3 入力特徴をチャネルとして扱います。これは、入力が [batch_size, length, 3] tensor で出力が [batch_size, length, number_of_filters] tensor であることを意味します。
convolved = inks for i in range(len(params.num_conv)): convolved_input = convolved if params.batch_norm: convolved_input = tf.layers.batch_normalization( convolved_input, training=(mode == tf.estimator.ModeKeys.TRAIN)) # Add dropout layer if enabled and not first convolution layer. if i > 0 and params.dropout: convolved_input = tf.layers.dropout( convolved_input, rate=params.dropout, training=(mode == tf.estimator.ModeKeys.TRAIN)) convolved = tf.layers.conv1d( convolved_input, filters=params.num_conv[i], kernel_size=params.conv_len[i], activation=None, strides=1, padding="same", name="conv1d_%d" % i) return convolved, lengths
_add_rnn_layers
畳み込みからの出力を bidirectional LSTM 層へ渡します、そのために contrib からのヘルパー関数を使用します。
outputs, _, _ = contrib_rnn.stack_bidirectional_dynamic_rnn( cells_fw=[cell(params.num_nodes) for _ in range(params.num_layers)], cells_bw=[cell(params.num_nodes) for _ in range(params.num_layers)], inputs=convolved, sequence_length=lengths, dtype=tf.float32, scope="rnn_classification")
より詳細とどのように CUDA アクセラレートされた実装を使用するかについてはコードを見てください。
簡潔で固定長の埋め込みを作成するために、LSTM の出力を総計します。最初にシークエンスがデータを持たないバッチの領域をゼロ設定します。
mask = tf.tile( tf.expand_dims(tf.sequence_mask(lengths, tf.shape(outputs)[1]), 2), [1, 1, tf.shape(outputs)[2]]) zero_outside = tf.where(mask, outputs, tf.zeros_like(outputs)) outputs = tf.reduce_sum(zero_outside, axis=1)
_add_fc_layers
入力の埋め込みは完全結合層に渡されます、それからそれを softmax 層として使用します。
tf.layers.dense(final_state, params.num_classes)
損失、予測、そして optimizer
最後に、ModelFn を作成するために loss (損失), train op (訓練 op)、そして predictions (予測) を追加する必要があります :
cross_entropy = tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits( labels=targets, logits=logits)) # Add the optimizer. train_op = tf.contrib.layers.optimize_loss( loss=cross_entropy, global_step=tf.train.get_global_step(), learning_rate=params.learning_rate, optimizer="Adam", # some gradient clipping stabilizes training in the beginning. clip_gradients=params.gradient_clipping_norm, summaries=["learning_rate", "loss", "gradients", "gradient_norm"]) predictions = tf.argmax(logits, axis=1) return model_fn_lib.ModelFnOps( mode=mode, predictions={"logits": logits, "predictions": predictions}, loss=cross_entropy, train_op=train_op, eval_metric_ops={"accuracy": tf.metrics.accuracy(targets, predictions)})
モデルを訓練して評価する
モデルを訓練して評価するために Estimator API の機能に依拠して Experiment API で簡単に訓練と評価を実行することができます :
estimator = tf.estimator.Estimator( model_fn=model_fn, model_dir=output_dir, config=config, params=model_params) # Train the model. tf.contrib.learn.Experiment( estimator=estimator, train_input_fn=get_input_fn( mode=tf.contrib.learn.ModeKeys.TRAIN, tfrecord_pattern=FLAGS.training_data, batch_size=FLAGS.batch_size), train_steps=FLAGS.steps, eval_input_fn=get_input_fn( mode=tf.contrib.learn.ModeKeys.EVAL, tfrecord_pattern=FLAGS.eval_data, batch_size=FLAGS.batch_size), min_eval_frequency=1000)
このチュートリアルは貴方をリカレント・ニューラルネットワークと estimator の API に精通させるための比較的小さいデータセット上の単なるクイック・サンプルであることに注意してください。そのようなモデルは巨大なデータセット上で試せばよりパワフルにさえなれるでしょう。
モデルを 1M ステップ訓練するとき top-1 候補上でおよそ 70 % の精度を得ることが期待できます。この精度はゲーム・ダイナミクスゆえに quickdraw ゲームの構築には十分であることに注意してくださいユーザはそれが ready になるまで彼らのお絵描きを調整できるでしょう。また、ゲームは top-1 候補だけを使用せずにターゲット・カテゴリーが固定されたスレッショルドよりも良いスコアを伴って現れる場合にはお絵描きを正しいものとして受け入れます。
以上