TFRecord使用方法 -ファイル保存、読込み、ニューラルネットワークへの投入-

#はじめに
機械学習で用いるデータセットの容量が莫大になって来ており、全てメモリに乗り切らないような事態が多々起こり始めました。その結果、事前にメモリにデータセットを読み込んでおくのが困難になり始めました。また分散学習も盛んになって来ており、いかにロス無くデータを学習器に投入するかが課題になって来てると思っています。（せっかく、高いGPUを購入して学習時間が短縮できても、データ読込みにラグがあると、「何やってんのだろう・・」になってしまいます。）

##何故、記事に？
TFRecord、思いっきりはまりました・・・。

##今回の主役
主な特徴は以下の通りです。
・　大量のデータをシリアライズ化して、連続的に読み込める形式で保存する機能
・　大量のデータを逐次読込みを行い、学習器に投入する機能
・　学習器に効率良く投入するため、学習と並列して、データ変換、シャッフル、バッチ化等をする機能
まさに、縁の下の力持ちみたいな子です。

##使用ケースの想定
ほとんどのケースは以下の2通りあれば事足りるかと思い、それのみを記しました。

- 可変長のデータを含んでいない場合
- 可変長のデータを含んでいる場合<br>

上記2通りそれぞれで、下記の3項目の作業を記しました。<br>
ディープラーニングの場合、よくある作業が下記の流れです。
1. 様々な所からデータを収集、もしくは自作してデータセットとしてストレージに保存。
2. ストレージからデータセットを読込み。
3. シャッフル、バッチ化等して、ニューラルネットワークに入力

上記３項目に相当するのが以下の作業です。
1. TFRecordファイルの作成方法
2. TFRecordファイルからの読込み、パース処理
3. ニューラルネットワークへの入力のための整形（シャッフル、バッチ化）

---
###<font color="mediumblue">可変長データを含んでいない場合</font>
ローカルフォルダに保存している画像をデータセットと想定して扱います。
####1. TFRecordファイルの作成方法
TFRecord形式にします。　TFRecordはデータをバイナリ化したものですが、Key-Value形式にてデータの読み書きを可能にしたものです。　今回は、画像データとそのファイル名をTFRecord形式に変換してファイル化します。

・　今回のKey名と、Valueとして保存する内容

| Key     　　　　  | 　Value |
|-----------------|------------------|
| image_data       |  画像データをバイナリ化したもの | 
| caption           |  ファイル名の文字列をバイナリ化したもの | 

■　画像フォルダのjpgファイルのファイルパスを取得するコード

```python
import glob
import tensorflow as tf

img_file_lists = [i for i in glob.glob('画像フォルダへのパス/*.jpg')]
```
■　TFRecordファイルを作成するコード（image.tfrecord という名前のTFRecordファイルを作成）

```python
with tf.python_io.TFRecordWriter('image.tfrecord') as w:
    for img_file_path in img_file_lists:
        with tf.gfile.GFile(img_file_path, 'rb') as f:
             img_data = f.read()
             caption = img_file_path.encode('utf-8')
             # 下記クラスは一つの、KeyValueを設定
             features = tf.train.Features(feature={
                 'image_data' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_data])),
                 'caption' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[caption]))
             })
             example = tf.train.Example(features=features)
             # 新しいデータが、追加で書かれていく
             w.write(example.SerializeToString())
```

- tf.gfile.GFile(img_file_path, 'r<font color="OrangeRed">b</font>')　バイナリなので、bを必ずつけてください。
- tf.train.Example クラスはデータ１件分に相当します。
引数に保存したいデータをtf.train.Featuresクラスにて指定します。tf.train.Featuresクラスは、引数に保存したいデータをKey-Value型で指定します。その際、Valueで指定する、tf.train.Featureクラスの引数は、データ型別に以下の通りになります。

| データの型     　　　　  | 引数    |クラス　|
|-----------------|------------------|-----------------|
| byte, string       | bytes_list | tf.train.BytesList |
| float, double       | float_list | tf.train.FloatList |
| int64, uint64, int32, uint32, bool | int64_list | tf.train.Int64List |

####2. TFRecordファイルからの読込み、パース処理
■　１で作ったtfRecordファイルを読込み、パース処理するコード（ここでは、画像のバイナリデータはuint8型の３階テンソルに変換しています。）

```python
import tensorflow as tf

def parse(example):
    features = tf.parse_single_example(
        example,
        features={
            'image_data': tf.FixedLenFeature([], dtype=tf.string),
            'caption': tf.FixedLenFeature([], dtype=tf.string)
        }
    )
    img = features['image_data']
    img = tf.image.decode_jpeg(img)
    caption = features['caption']
    return img, caption
```
■　上記コードを使って、絵とキャプションの表示をしたい場合のコード
（TensorFlowのDataSetAPIを使用したケースです。）

```python
from skimage import io

# 引数がファイル名のリストになっているのは、複数ファイルを想定
dataset = tf.data.TFRecordDataset(['image.tfrecord']).map(parse)
iterator = tf.compat.v1.data.make_one_shot_iterator(dataset)

with tf.Session() as sess:
    while True:
        next_element = iterator.get_next()
        if next_element is None:
            break
        jpeg_img, caption = sess.run(next_element)
        io.imshow(jpeg_img)
        io.show()
        print(caption.decode('utf-8'))
```

####3. ニューラルネットワークへの入力のための整形（シャッフル、バッチ化）
■　整形してニューラルネットワークに入力するコード

```python
BATCH_SIZE = 32 # 任意の数
# ↓　既に下記処理は呼ばれていますが、忘れていると思うので、重複で記述しています。
dataset = tf.data.TFRecordDataset(['image.tfrecord'])
dataset = dataset.map(parse)
# シャッフル
dataset = dataset.shuffle(1000) #この場合は、1000件ずつバッファを取りながら、シャッフル
# バッチ化
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.repeat(count=None) # データセットを何回繰り返すか　Noneは無限
iterator = tf.compat.v1.data.make_one_shot_iterator(dataset)

# ニューラルネットワークをmodel()で定義したとして
prediction, loss = model(iterator.get_next())

with tf.Session() as sess:
    while True:
        pred_val, loss_val = sess.run([prediction, loss])
```

---
###<font color="mediumblue">可変長データを含んでいる場合</font>
Seq2Seqみたいに、入力データが可変長の場合に使用します。
下記コードの中で使用している変数 image_data_list はリスト構造になっており、各要素は、画像データ(byte型)と、画像の説明文章を数字化リストに変換したものが、リストで入っているという前提でコードを記入しています。
####1. TFRecordファイルの作成方法
```python
with tf.python_io.TFRecordWriter('output.tfrecord') as w:
    for image_data in image_data_list:
        # 画像データ(byte型)が入っていると前提
        image = image_data[0]
        # 画像の説明文章を、数字データリストに変換したものが入っていると前提（リスト長さは変動するという前提）
        image_description = image_data[1]

        # 固定長の部分
        context = tf.train.Features(feature={
            'image_data': tf.train.Feature(bytes_list=tf.train.BytesList(value=[image]))
        })
        # 可変長の部分
        description_feature = [tf.train.Feature(
            int64_list=tf.train.Int64List(value=[v])) for v in image_description
        ]
        feature_lists = tf.train.FeatureLists(feature_list={
            "description": tf.train.FeatureList(feature=description_feature)
        })

        # TFRecordへの書込み
        sequence_example = tf.train.SequenceExample(context=context,
                                                    feature_lists=feature_lists)
        w.write(sequence_example.SerializeToString())
```

####2. TFRecordファイルからの読込み、パース処理

```python
import tensorflow as tf

def parse(sequence_example):
    context, feature_lists = tf.parse_single_sequence_example(
        sequence_example,
        context_features={
            'image_data': tf.FixedLenFeature([], dtype=tf.string),
        },
        sequence_features={
            'description': tf.FixedLenSequenceFeature([], dtype=tf.int64),
        })

    img = context['image_data']
    img = tf.image.decode_jpeg(img)
    description = feature_lists['description']

    return (img, description)
```

####3. ニューラルネットワークへの入力のための整形（シャッフル、バッチ化）
可変長でない場合とほとんど同じですが、一部、可変長の不均衡を０（ゼロ）でpaddingする場合があるかと思います。

```python
BATCH_SIZE = 32 # 任意の数

dataset = tf.data.TFRecordDataset(['output.tfrecord'])
dataset = dataset.map(parse)

#ゼロpadding &　バッチ化
padded_shapes = (tf.TensorShape[300, 300, 3], tf.TensorShape([None]))
datset = dataset.padded_batch(BATCH_SIZE, padded_shapes=padded_shapes)

iterator = tf.compat.v1.data.make_one_shot_iterator(dataset)

# 以下は同じような使い方です。
```

##最後に
皆様は、はまらないようにしてくだされ。