tf.Keras @ Tensor Flow 2.xにおけるTFRecord活用の備忘録 1/2【TFRecord作成編】

# 本記事執筆の動機
ここ数年でDeep Learning用OSSは劇的な進化を遂げました。本記事で紹介するKerasも元々はTheanoやTensor Flowのラッパーに過ぎなかった物が今やTensor Flow(以下TF)の一部(th.Keras)となっています[^keras]。学習データは専用のファイル形式(TFRecord)に全て書き込んで，それを読み込むことで簡単にデータセットを共有したり、分割書き込みにも対応しているのでファイルサイズの圧縮が出来たりととても便利になりました。また最大の恩恵はバッチサイズ毎にデータを受け渡せるパイプラインを比較的少ないコードの実装で実現出来ることです。但し、TF2.xがリリースからまだ間もないことも有り、ネット上の情報はTF1.xの情報が大半です。TF2.xはTF1.xのコードと基本的に後方互換性が無い為、モデルの実装やTFRecord活用で大変苦労しました。故に今後の備忘録も兼ねてTFRecordの作成から学習への活用までを記事まとめておこうと思います。今回はTFRecord作成編をまとめたいと思います。

:warning: 作成したTFRecordを使った学習はこの記事の続編である[tf.Keras @ Tensor Flow 2.xにおけるTFRecord活用の備忘録 2/2【学習・評価実行編】](https://qiita.com/y-vectorfield/items/9b711fa02d3c1fcc2e8c)を参照して下さい。
:warning: TF2.xからライブラリの構造などが抜本的に変更されました。今後も大幅な変更が更新の度に追加される可能性は十分に有ります。

# TFRecord作成用ソースコード
以下にTFRecord作成用のPythonソースコードを示します。コードの実行はJupyter Notebookを前提としています。画像に前処理を実施し，3000枚毎にTFRecordに書き込みます。残った半端な枚数の画像が発生する場合が有りますが，最後に条件分岐を追加することで，余すこと無く書き込みが出来ます。

```python:ソースコード例(JupyterNotebookでの実行を想定)
# データセットやNotebookの格納先ディレクトリのrootパスを記載
dataset_root = ''

import glob
import os
import re
import pathlib
import random
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf

# ファイル一覧の確認
for d in glob.glob(dataset_root+'**', recursive=True):
    if pathlib.Path(d).is_dir():
        print(d)

# ラベルデータ(Table)の格納先パス
tables_path = dataset_root + 'labels/'

# ラベルデータの読み込み
train = pd.read_csv(tables_path+'train.csv')
test = pd.read_csv(tables_path+'test.csv')

# ラベルデータの確認
print(train.head())
print(len(train))

# 水増し対象データセットの絞り込み(利用する場合)
target_argu = train[train['target']==1]
print(target_argu.head())
target_files = target_argu['image_name']
target_files = list(target_files)
len(target_files)

# 水増し後のデータセットの格納先パスを設定
img_root = dataset_root + 'images/train/'
new_dataset_path = dataset_root + 'images/train_arg/'
if not os.path.isdir(new_dataset_path):
        os.makedirs(new_dataset_path)

# 回転角の設定
angles = [i for i in range(0,361,45)]
angles = angles[:-1]

# 回転，切り出し及びリサイズ
for f in target_files:
    file_path = img_root + f + '.jpg'
    img = cv2.imread(file_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    size = (1024, 1024)
    img = cv2.resize(img, size)    
    center = tuple(np.array([img.shape[1]*0.5, img.shape[0]*0.5]))
    cutsize = (512, 512)
    scale = 1.0
    for angle in angles:
        rot_mat = cv2.getRotationMatrix2D(center, angle, scale)
        rotate_img = cv2.warpAffine(img, rot_mat, size, flags=cv2.INTER_LINEAR)
        img_name = '{}{}_{}degree.jpg'.format(new_dataset_path, f, angle)
        cut_width_start = int(cutsize[1]/2)
        cut_width_end = cut_width_start * 3
        cut_height_start = int(cutsize[0]/2)
        cut_height_end = cut_height_start * 3
        cut_img = rotate_img[cut_width_start:cut_width_end, cut_height_start:cut_height_end, :]
        cut_img = cv2.cvtColor(cut_img, cv2.COLOR_RGB2BGR)
        cv2.imwrite(img_name, cut_img)

# フリップ(左右反転)させる画像の選択(水増し後の画像）
flip_target_files = os.listdir(new_dataset_path)

# フリップ処理
for f in flip_target_files:
    base, ext = os.path.splitext(f)
    if ext == '.jpg':
        img = cv2.imread(new_dataset_path + f)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img_name = new_dataset_path + base + '_flip' + ext
        flip_img = cv2.flip(img, 1)
        flip_img = cv2.cvtColor(flip_img, cv2.COLOR_RGB2BGR)
        cv2.imwrite(img_name, flip_img)

aug_image_nums = len(flip_target_files)*2
aug_image_nums

target_org = train[train['target']==0]
print(target_org.head())

range_num = len(target_org)
print(range_num)

index_list = random.sample(range(range_num), k=aug_image_nums)
print(index_list[:5])
print(len(index_list))
target_img_list = [target_org.iloc[i]['image_name'] for i in index_list]
print(target_img_list[:5])
print(len(target_img_list))
size = (512, 512)

# 処理結果の書き込み
for f in target_img_list:
    file_path = img_root + f + '.jpg'
    save_path = new_dataset_path + f + '.jpg'
    img = cv2.imread(file_path)
    img = cv2.resize(img, size) 
    cv2.imwrite(save_path, img)
file_list = sorted(os.listdir(new_dataset_path))
label_path = dataset_root + 'labels/new_train_labels.csv'

with open(label_path, 'w') as f:
    f.write('image_name, target\n')
    for fname in file_list:
        if re.search('degree', fname) == None:
            f.write(fname+', 0\n')
        else:
            f.write(fname+', 1\n')
new_label = pd.read_csv(label_path)
new_label.head()

# TFRecordの生成
## 学習用データの作成
def _bytes_feature(value):
    """Returns a bytes_list from a string / byte."""
    if isinstance(value, type(tf.constant(0))):
        value = value.numpy() # BytesList won't unpack a string from an EagerTensor.
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def _float_feature(value):
    """Returns a float_list from a float / double."""
    return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))

def _int64_feature(value):
    """Returns an int64_list from a bool / enum / int / uint."""
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

def serialize_example(image, image_name, target):
    feature = {
        'image': _bytes_feature(image),
        'image_name': _bytes_feature(image_name),
        'target': _int64_feature(target)
    }
    example_proto = tf.train.Example(features=tf.train.Features(feature=feature))
    return example_proto.SerializeToString()

DEVIDE_SIZE = 3000                                                                                   # 3000枚毎にファイルを分ける
IMG_NUM = len(new_label)                                                                                  # 全画像枚数
loop_num = IMG_NUM // DEVIDE_SIZE + int(IMG_NUM%DEVIDE_SIZE!=0)  # ループ回数

# TFRecordファイルの保存先パス
tfrecord_root = dataset_root + 'tfrecord/train/'

if not os.path.isdir(tfrecord_root):
        os.makedirs(tfrecord_root)

for i in range(loop_num):
    tfrecord_path = tfrecord_root + 'train_argumentation_tfrecord_{}_of_{}.tfrecord'.format(i+1, loop_num)
    print('Writing: ' + tfrecord_path)
    with tf.io.TFRecordWriter(tfrecord_path) as writer:
        if(i < loop_num-1):
            for j in range(0+DEVIDE_SIZE*i, DEVIDE_SIZE+DEVIDE_SIZE*i):
                img_name = new_label.iat[j, 0]
                label = new_label.iat[j, 1]
                img_path = new_dataset_path + img_name
                img = cv2.imread(img_path)
                # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img = cv2.imencode('.jpg', img, (cv2.IMWRITE_JPEG_QUALITY, 94))[1].tostring()
                example = serialize_example(img, str.encode(img_name.split('.')[0]), label)
                writer.write(example)         
        else:
            for k in range(DEVIDE_SIZE*(loop_num-1), IMG_NUM):
                img_name = new_label.iat[k, 0]
                label = new_label.iat[k, 1]
                img_path = new_dataset_path + img_name
                img = cv2.imread(img_path)
                # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img = cv2.imencode('.jpg', img, (cv2.IMWRITE_JPEG_QUALITY, 94))[1].tostring()
                example = serialize_example(img, str.encode(img_name.split('.')[0]), label)
                writer.write(example)

## テスト用データの作成

def serialize_example_for_test(image, image_name):
    feature = {
        'image': _bytes_feature(image),
        'image_name': _bytes_feature(image_name)
    }
    example_proto = tf.train.Example(features=tf.train.Features(feature=feature))
    return example_proto.SerializeToString()

DEVIDE_SIZE = 3000  # 3000枚毎にファイルを分ける
IMG_NUM = len(test) # 全画像枚数
loop_num = IMG_NUM // DEVIDE_SIZE + int(IMG_NUM%DEVIDE_SIZE!=0)  # ループ回数
size = (512, 512)

test_path = dataset_root + 'images/test/'
tfrecord_root = dataset_root + 'tfrecord/test/'

if not os.path.isdir(tfrecord_root):
        os.makedirs(tfrecord_root)

for i in range(loop_num):
    tfrecord_path = tfrecord_root + 'test_tfrecord_{}_of_{}.tfrecord'.format(i+1, loop_num)
    print('Writing: ' + tfrecord_path)
    with tf.io.TFRecordWriter(tfrecord_path) as writer:
        if(i < loop_num-1):
            for j in range(0+DEVIDE_SIZE*i, DEVIDE_SIZE+DEVIDE_SIZE*i):
                img_name = test.iat[j, 0] + '.jpg'
                img_path = test_path + img_name
                img = cv2.imread(img_path)
                # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img = cv2.resize(img, size)
                img = cv2.imencode('.jpg', img, (cv2.IMWRITE_JPEG_QUALITY, 94))[1].tostring()
                example = serialize_example_for_test(img, str.encode(img_name.split('.')[0]))
                writer.write(example)         
        else:
            for k in range(DEVIDE_SIZE*(loop_num-1), IMG_NUM):
                img_name = test.iat[k, 0] + '.jpg'
                img_path = test_path + img_name
                img = cv2.imread(img_path)
                # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img = cv2.resize(img, size)
                img = cv2.imencode('.jpg', img, (cv2.IMWRITE_JPEG_QUALITY, 94))[1].tostring()
                example = serialize_example_for_test(img, str.encode(img_name.split('.')[0]))
                writer.write(example)
```

# 今後の改善点
1. 前処理部分も関数化して処理の実施の有無を選択可能にする。
2. TFRecord書き出し部分が条件分岐内部の処理が粗同じなので共通部分は関数化してコード量を削減する。

# Reference
* [How To Create TFRecords](https://www.kaggle.com/cdeotte/how-to-create-tfrecords)
* [EfficientNet CV](https://www.kaggle.com/ajaykumar7778/efficientnet-cv)
* [機械学習SEの取り組みログ](http://applydots.info/)[^help]
* [TensorFlow推奨フォーマット「TFRecord」の作成と読み込み方法(TF1.xの情報)](https://www.tdi.co.jp/miso/tensorflow-tfrecord-01)

## 今後の研究用(TF2.xのドキュメント)
* [tf.data.Datasetの構築](https://www.tensorflow.org/tutorials/load_data/images?hl=ja#tfdatadataset%E3%81%AE%E6%A7%8B%E7%AF%89)
* [tf.data APIによるパフォーマンスの向上](https://www.tensorflow.org/guide/data_performance?hl=ja)
* [TFRecordの書き出し](https://www.tensorflow.org/tutorials/load_data/tfrecord?hl=ja#tfrecord_%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E6%9B%B8%E3%81%8D%E5%87%BA%E3%81%97_2)
* [Module: tf.data](https://www.tensorflow.org/api_docs/python/tf/data)
* [tf.data：TensorFlow入力パイプラインを構築する](https://www.tensorflow.org/guide/data#top_of_page)
* [Advanced hair augmentation](https://www.kaggle.com/c/siim-isic-melanoma-classification/discussion/159176)
* [Advanced hair augmentation in TensorFlow](https://www.kaggle.com/c/siim-isic-melanoma-classification/discussion/163909)


[^keras]: Kerasの開発者がGoogleに入社したことがKerasのTensor Flowへの同梱に繋がった様です。[参考](https://www.atmarkit.co.jp/ait/articles/1907/09/news021.html)
[^help]: このサイトのオーナーに実装のアドバイスを貰いました。