【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点がわかりました。
- shuffle_batchを使いたいときは、 1つのTFRecordにパラメータcapacityより十分少ない数のデータしか入れてはいけない。
- バッチ作成のパフォーマンスを上げるときは、
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
の偏り検証
使用したスクリプト
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
横軸がバッチのインデックス、縦軸がレコードのインデックス、色がファイルを表します。 散布図の一点が1データを表し、何番目のバッチでどのファイルの何番目のデータが出てきたかを示しています。(全データ点の5%のみ表示します)
上記の例では、バッチ前半では各ファイルの前半のデータがよく出てきて、バッチ後半で後半のデータが出てくることがわかります。 つまり偏っています。
幾つかパラメータを変えて検証
検証では、偏りにはあまり関係ないbatch_size
を10に固定して、 他のパラメータを幾つか変えてみます。
(検証1)公式ドキュメントの例と同じパラメータ
Reading data | TensorFlowに書かれたパラメータを試します。
$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 1 data tb_log_default
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
今度はファイル間のシャッフルはいい感じになっていますが、 バッチ前半でファイル前半データが、バッチ後半でファイル後半のデータが出るようになりました。
検証3| num_threadsを利用
公式ドキュメントをさらに読むと「
他にはtf.train.shuffle_batch
でnum_threads
パラメータを1より大きくする手もある(超意訳)」
みたいなことが書いています。
とりあえず使ってみます。
$ ./visualize_mini_batch.py --min_after_dequeue 10000 --num_threads 10 data tb_log_t10
一見ではデフォルトパラメータと何も変わっていない様に見えます。
考察
shuffle_batchの挙動
Reading data | TensorFlowとプログラムの挙動を見る限り、 shuffle_batchは、以下のように動いているようです。
- ファイル単位でshuffleして、Filename Queueにenqueueする。
- Filename Queueからファイルを1つずつReaderが読み込み、Readerが指定したDecoderをつかってデータをデコードする。
- Readerはデコードしたデータ、ExampleQueueにenqueueする。
- 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のサイズcapacity
が min_after_dequeue + 3 * batch_size = 10000 + 30 = 10030
でした。
この場合、ReaderがFilenameQueueから3つ目のファイルを読み込み始めたあたりでExampleQueueが一杯になり、 shuffle_batchがほぼ2つ目のファイルのデータしか入っていないExampleQueueをshuffleしてミニバッチを作る→一部のファイルのデータしかバッチに含まれない、となっていたと思われます。
(再掲)
shuffle_batch_joinの挙動
shuffle_batch_joinでは、先に述べた「Filename Queueからファイルを1つずつReaderが読み込む」というところが、 並列化されて複数のReaderで複数のファイルを同時に読み込んでExampleQueueにデータをenqueueします。
ただし、全Readerがデータの1/5程度(全体では、5000 * 10 / 5 = 10000 ≒ capacity
) を読み込んだ時点で
ExampleQueueが一杯になってしまうため、最初の方のバッチはファイル前半のデータで占められてしまっっていたと思われます。
(再掲)
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
ドキュメントが見つからなかったので詳細は良くわかりませんが、
どうやら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
ちなみに、shuffle_batch_joinを使ったときも、Readerが複数あるので同様にファイルのロード・デコードが早くなっています。
$ tensorboard --logdir=tb_log_t10_join
num_threads
を指定した場合は同ファイルのデータが同じバッチに入りやすくなり、shuffle_batch_join
を使用した場合は同ファイルの前半・後半のデータが同バッチに入りにくくなります。
この性質に注意してどちらを使うべきかを選べばよいかと思います。
結局どうすればよかったか?
今回は、ExampleQueueのcapacityが小さすぎたのが問題なので、
一番簡単な方法はパラメータcapacity
を大きくしてやれば良いです。
# 全データが入るサイズまでcapacityを大きくする $ ./visualize_mini_batch.py --min_after_dequeue 500000 --num_threads 1 data tb_log_m500000
ただし、これは全入力データを一旦メモリに載せるという話なので、大量データを使う場合には使えません。
他の解決方法としては、1つのTFRecordファイルに入れるデータの量を減らすという方法があります。 そもそもTFRecordで複数データをまとめるとファイルの管理が楽そうだというモチベーションでTFRecordを使い始めましたが、運用上の問題が無い限り複数データを1ファイルに入れないほうが良さそうです。
やむを得ず1ファイルに複数データを入れる場合でも、1つのTFRecordにはcapacity
より十分少ない数のデータしか入れてはいけません。
まとめ
今回は、Tensorflowのshuffle_batchを使うときのパラメータと、それを変化させたときのバッチの偏りについて調査しました。
その結果、以下の2つがわかりました。
- shuffle_batchを使いたいときは、 1つのTFRecordには
capacity
より十分少ない数のデータしか入れてはいけない。 - バッチ作成のパフォーマンスを上げるときは、
num_threads
パラメータを1以上にするか、shuffle_batch_join
を使う。