のどあめ

ググってもなかなか出てこなかった情報を垂れ流すブログ。

【Tensorflow】TFRecordファイルでshuffle_batchしたときの偏り調査

Tensorflowでは色々な形式のデータのRead/Writeに対応しています。 これらの入力形式の中で最もスタンダードなのがTFRecord形式です。(とドキュメントに書いてました)

TFRecord形式のファイルには、1つのファイルに複数のデータを格納できるので、 毎日大量に生成されるようなデータを扱う場合は、日毎にデータを作ればとてもファイルの整理がしやすそうです。

一方で、Tensorflowにはミニバッチを使った学習を簡単に行うための、tf.train.shuffle_batch関数が用意されています。 これは、複数の入力ファイル内のデータをshuffleしてミニバッチにしてくれる便利関数で、 大量データを扱うことを前提にした実装をするため、擬似的にshuffleしたバッチを作ります。

この「擬似的」というのが曲者で、 うっかり正例・負例を一つのファイルにそれぞれまとめて、 サンプルコード通りのパラメータでshuffle_batchを使ったりすると、 バッチ内が全部ラベルが同じであるミニバッチができてしまったりします。 (Reading data  |  TensorFlowをちゃんと読めば分かる話なのですが…)

今回は、TFRecordファイルでshuffle_batchを使う場合に、どのようにデータを保存すべきか、パラメータはどう選べばよいかを調査しました。

結論としては、以下の2点がわかりました。

  1. shuffle_batchを使いたいときは、 1つのTFRecordにパラメータcapacityより十分少ない数のデータしか入れてはいけない。
  2. バッチ作成のパフォーマンスを上げるときは、num_threadsパラメータを1以上にするか、shuffle_batch_joinを使う。

では、以下に詳細を述べます。

事前知識

TFRecord形式

TFRecordはTensorflowのstandardなファイル形式で、中身はProtocol Bufferっぽいです。 1ファイルに、等形式の異なる値で構成されるデータ(int, floatの組等)を1ファイルに複数書き込むことができます。

公式ドキュメント: https://www.tensorflow.org/versions/r0.12/api_docs/python/python_io.html#tfrecords-format-details

tf.train.shuffle_batch

Tensorflowでshuffleしたミニバッチを使いたいときに利用する関数です。

公式ドキュメント: https://www.tensorflow.org/versions/r0.12/api_docs/python/io_ops.html#shuffle_batch

先に述べたとおりshuffleと書いているので、よしなに入力ファイルをshuffleしてくれような気がしますが、 正確なshuffleではなく(大量データを想定して)Queueをつかった擬似的なshuffleでミニバッチを作ります。

その為、入力データの1ファイルに複数データが入っている場合は、 パラメータを適切な値にしなければ、ミニバッチに含まれるデータに偏りが生まれます。

tf.train.shuffle_batchの偏り検証

使用したスクリプト

github.com

TFRecordの作成

https://github.com/ykicisk/minibatch_TFRecord/blob/master/create_tfrecords.py

詳細は省略しますが、このスクリプトを動かすと、以下のように10個のTFRecordファイルができます。 それぞれには、5000個ずつデータが入っています。

今回はあとでミニバッチを作るときに、どのデータがどのファイルの何番目に入っていたかをわかりやすくするため、 データの中にfile_idx(ファイルが何番目), record_idx(何個目のデータ)、ランダムなnumpy.ndarray(2x4)を格納しています。

$ mkdir data
$ ./create_tfrecords.py data
wirte: data/file00.tfrecord
wirte: data/file01.tfrecord
wirte: data/file02.tfrecord
wirte: data/file03.tfrecord
wirte: data/file04.tfrecord
wirte: data/file05.tfrecord
wirte: data/file06.tfrecord
wirte: data/file07.tfrecord
wirte: data/file08.tfrecord
wirte: data/file09.tfrecord

$ tree data
data
├── file00.tfrecord
├── file01.tfrecord
├── file02.tfrecord
├── file03.tfrecord
├── file04.tfrecord
├── file05.tfrecord
├── file06.tfrecord
├── file07.tfrecord
├── file08.tfrecord
└── file09.tfrecord

0 directories, 10 files

TFRecordの偏りの可視化

https://github.com/ykicisk/minibatch_TFRecord/blob/master/visualize_mini_batch.py

上記のcreate_tfrecords.pyで作ったTFRecordファイルでミニバッチを作り、その偏りを可視化するスクリプトです。 ミニバッチを作るときのパラメータを引数で指定できます。

tf.train.shuffle_batchのパラメータ

詳細は後述しますが、とりあえずざっくりどんなものがあるかだけ先に並べておきます。

パラメータ ざっくりした説明
batch_size ミニバッチのサイズ
min_after_dequeue Queue(後述のExampleQueue)からdequeueしたときにQueueに最低でも残るデータ数
capacity Queueのサイズ
num_threads 入力Queue(後述のFilenameQueue)のenqueueするthread数

上記のスクリプトでは、パラメータcapacityについてはドキュメントに習いcapacity = min_after_dequeue + 3 * batch_sizeで固定、他のパラメータはスクリプトで指定します。

また、引数--joinで、shuffle_batchの代わりにshuffle_batch_join(後述)を使います。 この場合、引数--num_threadsで指定した数のTFRecordReaderを作成します。

動作例

# パラメータnum_threadsを指定して、shuffle_batch_joinを利用する
# dataは入力ファイル、tb_logはTensorBoardのログファイルを書き出すパス
$ ./visualize_mini_batch.py --num_threads 10 --join data tb_log

f:id:ykicisk:20161218164734p:plain

横軸がバッチのインデックス、縦軸がレコードのインデックス、色がファイルを表します。 散布図の一点が1データを表し、何番目のバッチでどのファイルの何番目のデータが出てきたかを示しています。(全データ点の5%のみ表示します)

上記の例では、バッチ前半では各ファイルの前半のデータがよく出てきて、バッチ後半で後半のデータが出てくることがわかります。 つまり偏っています。

幾つかパラメータを変えて検証

検証では、偏りにはあまり関係ないbatch_sizeを10に固定して、 他のパラメータを幾つか変えてみます。

(検証1)公式ドキュメントの例と同じパラメータ

Reading data  |  TensorFlowに書かれたパラメータを試します。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 1 data tb_log_default

f:id:ykicisk:20161218164748p:plain

1ファイル内でのshuffleは若干後半のデータが後に出てくる傾向がありますが、それなりにできている気がします。

ただし、バッチをつくるときに同じファイルのデータしか出ていないことがわかります。 上記の結果の場合はfile01, file08のデータが前半のバッチによく出現して、file04, file09のデータは後半のバッチによく出現しています。

(検証2) shuffle_batch_joinを利用

Reading data  |  TensorFlowを読むと、 「ファイル間でもっとシャッフルしたいなら、tf.train.shuffle_batch_joinを使う(超意訳)」みたいなことが書いています。

とりあえず使ってみます。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 10 --join data tb_log_t10_join

f:id:ykicisk:20161218164734p:plain

今度はファイル間のシャッフルはいい感じになっていますが、 バッチ前半でファイル前半データが、バッチ後半でファイル後半のデータが出るようになりました。

検証3| num_threadsを利用

公式ドキュメントをさらに読むと「 他にはtf.train.shuffle_batchnum_threadsパラメータを1より大きくする手もある(超意訳)」 みたいなことが書いています。

とりあえず使ってみます。

$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 10 data tb_log_t10

f:id:ykicisk:20161218165154p:plain

一見ではデフォルトパラメータと何も変わっていない様に見えます。

考察

shuffle_batchの挙動

Reading data  |  TensorFlowとプログラムの挙動を見る限り、 shuffle_batchは、以下のように動いているようです。

  1. ファイル単位でshuffleして、Filename Queueにenqueueする。
  2. Filename Queueからファイルを1つずつReaderが読み込み、Readerが指定したDecoderをつかってデータをデコードする。
  3. Readerはデコードしたデータ、ExampleQueueにenqueueする。
  4. shuffle_batchは、ExampleQueueが一定数(min_after_dequeue + batch_size?)溜まったら、ExampleQueueをshuffleして上からbatch_size個を出力する

コードではこんな感じになります。

# filenamesから、shuffle済みfilename_queueを作成
filenames = ["fileA", "fileB", "fileC"]
filename_queue = tf.train.string_input_producer(filepaths, shuffle=True)

# shuffle済みfilename_queueから、ファイルを1つずつ読み込むReaderを作成する
reader = tf.TFRecordReader()
_, selialized_data = reader.read(file_queue)

# decoder
data_def = {"key": tf.FixedLenFeature([2, 4], tf.float32)}  # データ形式を指定
example = tf.parse_single_example(selialized_data, features=data_def)

# パラメータを指定してバッチ作成
data_batch = tf.train.shuffle_batch(
    example,
    batch_size=batch_size,
    capacity=capacity,
    min_after_dequeue=min_after_dequeue,
    num_threads=num_threads,
    allow_smaller_final_batch=True
)

何故偏ってしまったか?

Reading data  |  TensorFlowのGIFを使って説明します。

shuffle_batchは入力された全データではなく、ExampleQueueをshuffleします。 shuffle_batchの中身が偏っていたのは、そもそもExampleQueueが偏っていたのが原因です。

今回は1ファイルに5000個のファイルが入っていて、 ExampleQueueのサイズcapacitymin_after_dequeue + 3 * batch_size = 10000 + 30 = 10030でした。

この場合、ReaderがFilenameQueueから3つ目のファイルを読み込み始めたあたりでExampleQueueが一杯になり、 shuffle_batchがほぼ2つ目のファイルのデータしか入っていないExampleQueueをshuffleしてミニバッチを作る→一部のファイルのデータしかバッチに含まれない、となっていたと思われます。

(再掲) f:id:ykicisk:20161218164748p:plain

shuffle_batch_joinの挙動

shuffle_batch_joinでは、先に述べた「Filename Queueからファイルを1つずつReaderが読み込む」というところが、 並列化されて複数のReaderで複数のファイルを同時に読み込んでExampleQueueにデータをenqueueします。

ただし、全Readerがデータの1/5程度(全体では、5000 * 10 / 5 = 10000 ≒ capacity) を読み込んだ時点で ExampleQueueが一杯になってしまうため、最初の方のバッチはファイル前半のデータで占められてしまっっていたと思われます。

(再掲) f:id:ykicisk:20161218164734p:plain

num_threadsを1より大きくした場合の挙動

Reading data  |  TensorFlowを良く読むと、 「num_threadsを1より大きくすると、1つのファイルをReaderが同時に読むようになって早くなるよ」的なことが書いてあります。 How toのGIF的にいえば、Reader1, Reader2というのがまさにこれに当たります。

複数スレッドでReaderが動いたとしても、1つのファイルからデータを読み込んでいるので、ExampleQueue内の偏りは解消できません。 そのため、データの偏りを可視化したときは、デフォルトパラメータと同じような結果になったと思われます。

では、num_threadsをいつ使えばよいかというと、ファイルのロードやdecodeに時間がかかる場合に、shuffle_batchが学習のボトルネックになるのを防ぐ目的で使えそうです。 この様子はReading data  |  TensorFlowにある通り、Tensorboardをみればわかります。

shuffle_batchをつかうと、TensorBoardのEVENTSにqueueというグラフが追加されます。

$ tensorboard --logdir=tb_log_default 

f:id:ykicisk:20161218175238p:plain

ドキュメントが見つからなかったので詳細は良くわかりませんが、 どうやらqueue/shuffle_batch/fraction_over_10000_of_30_fullというのが、ExampleQueueがいっぱいになっている割合みたいなものを表しているようです。

この場合は、全ステップで1より小さいので、ExampleQueueがいっぱいになっているタイミングが無い=Readerがファイルをロード・デコードする部分がボトルネックになっている、 ということがわかります。

queue/input_producer/fraction_of_32_fullというのは、FilenameQueueの方を表しているみたいです。 バッチを読み込んでいくに連れて1つずつファイルが読み込まれていることを表していると思われます。縦軸はよくわかりません。

これに対して、num_threadsを10にした場合は、これが常に1になっているので、 Readerの読み込みが十分早くボトルネックが解消されていることがわかります。

$ tensorboard --logdir=tb_log_t10 

f:id:ykicisk:20161218175251p:plain

ちなみに、shuffle_batch_joinを使ったときも、Readerが複数あるので同様にファイルのロード・デコードが早くなっています。

$ tensorboard --logdir=tb_log_t10_join

f:id:ykicisk:20161218175259p:plain

num_threadsを指定した場合は同ファイルのデータが同じバッチに入りやすくなり、shuffle_batch_joinを使用した場合は同ファイルの前半・後半のデータが同バッチに入りにくくなります。 この性質に注意してどちらを使うべきかを選べばよいかと思います。

結局どうすればよかったか?

今回は、ExampleQueueのcapacityが小さすぎたのが問題なので、 一番簡単な方法はパラメータcapacityを大きくしてやれば良いです。

# 全データが入るサイズまでcapacityを大きくする
$ ./visualize_mini_batch.py --min_after_dequeue 500000 --num_threads 1 data tb_log_m500000

f:id:ykicisk:20161218181634p:plain

ただし、これは全入力データを一旦メモリに載せるという話なので、大量データを使う場合には使えません。

他の解決方法としては、1つのTFRecordファイルに入れるデータの量を減らすという方法があります。 そもそもTFRecordで複数データをまとめるとファイルの管理が楽そうだというモチベーションでTFRecordを使い始めましたが、運用上の問題が無い限り複数データを1ファイルに入れないほうが良さそうです。

やむを得ず1ファイルに複数データを入れる場合でも、1つのTFRecordにはcapacityより十分少ない数のデータしか入れてはいけません。

まとめ

今回は、Tensorflowのshuffle_batchを使うときのパラメータと、それを変化させたときのバッチの偏りについて調査しました。

その結果、以下の2つがわかりました。

  1. shuffle_batchを使いたいときは、 1つのTFRecordにはcapacityより十分少ない数のデータしか入れてはいけない。
  2. バッチ作成のパフォーマンスを上げるときは、num_threadsパラメータを1以上にするか、shuffle_batch_joinを使う。

参考にしたページ