見出し画像

モバイルアプリエンジニアのためのTensorFlow 2.x 入門 (5) - 画像データセットの読み込み

モバイルアプリエンジニアの方がTensorFlowに入門するための連載記事です。

今回は画像データセットの読み込み方について説明します。

前回まではモデルの書き方の説明など、基本的な使い方を説明しましたが、モバイルアプリエンジニアのという観点からすると、画像の読み込みはよくありそうなユースケースなので、このテーマを選びました。

画像の読み込みは、tf.data.Dataset & TFRecordを用いた方法と、ImageDataGeneratorを用いた方法の2種類があるので、それぞれについて説明します。

この入門は、Google Colaboratoryを使います。使い方は第1回にありますので、そちらをご覧下さい。

過去の記事はこちらからご覧下さい。

tf.dataを用いた画像の読み込み

ここでは、TensorFlowのチュートリアルにある基本的な画像読み込みであるtf.dataとTFRecordを用いた方法を説明します。

画像を読み込んで、モデルに入力するためのデータセットにするためには次の要件を考慮する必要があります。

・Tensor型になっていること
・モデルに合わせてリサイズしていること
・モデルに合わせて正規化、標準化されていること
・ラベルと画像のペアになっていること
・よくシャッフルされていること
・バッチ化されていること
・限りなく繰り返し読み込めること
・データがなるべく早く読み込めること(CPUの処理がGCUの処理のボトルネックにならないこと)

リサイズや正規化、ラベルとのペアの作成は手動でやりますが、tf.data.Datasetを使うと、バッチ化や繰り返し、早い読み込みが実現できます。

また、画像データをTFRecordというTensorFlowの読み込みに最適化したファイル形式で保存しておくと、さらに読み込みが早くなります。

チュートリアルの方は、関係のない処理も入っていて冗長なので、かなりスリムにしたバージョンのコードを載せます。(といってもそれなりに長いです)

import tensorflow as tf
import pathlib
import random
import matplotlib.pyplot as plt

BATCH_SIZE = 32

# AUTOTUNEはGPUの処理とCPUの処理の配分を動的に設定してくれるパラメータ
AUTOTUNE = tf.data.experimental.AUTOTUNE

# ファイルをURLの位置からダウンロードする。
# untar=Trueとなっているので、解凍までしてくれる。
# 戻り値は解凍後のディレクトリ名
data_root_orig = tf.keras.utils.get_file(origin='https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
                                       fname='flower_photos', untar=True)

# pathLibのインスタンスを作成。
# pathLibはファイルやディレクトリをオブジェクトとして操作できる
data_root = pathlib.Path(data_root_orig)

# サブディレクトリにあるファイルをすべて抽出する
all_image_paths = list(data_root.glob('*/*'))
# 抽出したファイルのパスをすべて文字列にする
all_image_paths = [str(path) for path in all_image_paths]

def load_and_preprocess_image(path):
 # ファイルを1つ読み込む。ファイルを表現した生データのテンソルが得られる
 image = tf.io.read_file(path)
 # 生データのテンソルを画像のテンソルに変換する。
 # これによりshape=(240,240,3)、dtype=uint8になる
 image = tf.image.decode_jpeg(image, channels=3)
 # モデルに合わせてリサイズする
 image = tf.image.resize(image, [192, 192])
 # モデルに合わせて正規化する(値を0〜1の範囲に収める処理)
 image /= 255.0
 return image

# ディレクトリ名からラベル名を得る
label_names = sorted(item.name for item in data_root.glob('*/') if item.is_dir())
# label_names =  ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']
# ラベルに番号をつけ辞書型に登録する
label_to_index = dict((name, index) for index,name in enumerate(label_names))
# label_to_index = {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
# すべての画像データのパスからディレクトリ名を元にラベル番号を得てリストに入れる
all_image_labels = [label_to_index[pathlib.Path(path).parent.name]
                   for path in all_image_paths]
# all_image_labels = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ・・・・

# イメージの数を数える
image_count = len(all_image_paths)

# from_tensor_slicesはリストをイテレートできるTensor型のコレクションに変換してくれる
paths_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)
# 画像のパス名をmapして、読み込みとリサイズなどの前処理をする
image_ds = paths_ds.map(load_and_preprocess_image)

# シリアライズする。文字列のテンソルになる
ds = image_ds.map(tf.io.serialize_tensor)

# TFRecordの形式で保存する
tfrec = tf.data.experimental.TFRecordWriter('images.tfrec')
tfrec.write(ds)

# TFRecordからデータを読み込む
ds = tf.data.TFRecordDataset('images.tfrec')

# パース用の関数
def parse(x):
result = tf.io.parse_tensor(x, out_type=tf.float32)
result = tf.reshape(result, [192, 192, 3])
return result

# データセットをパースする
# TFRecordにはテンソルの型や形状(shape)が保存されていないので、それらを復元する
ds = ds.map(parse, num_parallel_calls=AUTOTUNE)

# all_image_labelsをスライスしてデータセットにする
label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(all_image_labels, tf.int64))

# データセットをラベルとペアにして読み込みを実行する
ds = tf.data.Dataset.zip((ds, label_ds))
# キャシュ化する
ds = ds.cache(filename='./cache.tf-data')
# データをシャッフルして繰り返すようにする
ds = ds.apply(
tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
# モデル訓練中にバックグラウンドで読み込めるようにする
ds=ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)

# イテレータにする
it = iter(ds)
# 1つデータを取り出す
d = next(it)

# Numpy配列の画像を表示する
plt.imshow(d[0][0]) # 最初の添字0は0=画像、1=ラベル、2番目の添字はバッチ内での位置

画像がこんな感じで表示されます。

画像2

ポイントは、tf.data.Dataset.from_tensor_slicesのところです。これによりリストをTensor型のコレクションにしたTensorSliceDataset型の変数を得ることができます。

TensorSliceDataset型にしておくとキャッシュ化、バッチ化やシャッフル、繰り返し、事前読み込みなどをメソッドを呼ぶだけで実現できるようになります。

また、TFRecordDatasetクラスを用いてTFRecordの形式でファイルを保存しています。保存したデータは読み込んでパースして使います。

なぜわざわざ書いてから読み込むかというと、メモリに収まらない場合や、データセットを1回だけでなく2回、3回と使いたい場合に早いからです。逆に言うとそれらに該当しないのならこのような手間をかける必要はありません。

これだと説明を省略しすぎという方は、もう少し詳しい説明の入った記事もありますのでこちらをご覧下さい。

ImageDataGeneratorを用いた画像の読み込み

ImageDataGeneratorを使うとデータセットの読み込みを簡単に書くことができます。

所定のディレクトリ構造で画像データを保存しておけば、、それらを自動的に読み込んでトレーニングデータ、検証データの振り分けやラベルの設定をしてくれます。

また、バッチ化、ランダム化、繰り返しをしてくれたり、元の画像をランダムに回転したりシフトしたりして水増し(データオーギュメンテーション)もしてくれます。

利用するケースがマッチするならこちらを使うほうがラクですね。

ということで、公式のチュートリアルをベースにImageDataGeneratorの使い方を説明します。

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D
from tensorflow.keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import os
import numpy as np

# 定数の設定
batch_size = 128
epochs = 15
IMG_HEIGHT = 150
IMG_WIDTH = 150

# 犬と猫の画像のURL
_URL = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'

# ファイルを取得して解凍
path_to_zip = tf.keras.utils.get_file('cats_and_dogs.zip', origin=_URL, extract=True)

# ディレクトリ名を得る
#
# ディレクトリは次のような構造になっていれば、
# ImageDataGeneratorが自動的にトレーニング用、検証用を判断してくれる
#
# cats_and_dogs_filtered
# |__ train
#     |______ cats: [cat.0.jpg, cat.1.jpg, cat.2.jpg ....]
#     |______ dogs: [dog.0.jpg, dog.1.jpg, dog.2.jpg ...]
# |__ validation
#     |______ cats: [cat.2000.jpg, cat.2001.jpg, cat.2002.jpg ....]
#     |______ dogs: [dog.2000.jpg, dog.2001.jpg, dog.2002.jpg ...]
PATH = os.path.join(os.path.dirname(path_to_zip), 'cats_and_dogs_filtered')

train_dir = os.path.join(PATH, 'train')
validation_dir = os.path.join(PATH, 'validation')

# 学習用、検証用のそれぞれのディレクトリ名を得る
train_cats_dir = os.path.join(train_dir, 'cats')  # 学習用の猫画像のディレクトリ
train_dogs_dir = os.path.join(train_dir, 'dogs')  # 学習用の犬画像のディレクトリ
validation_cats_dir = os.path.join(validation_dir, 'cats')  # 検証用の猫画像のディレクトリ
validation_dogs_dir = os.path.join(validation_dir, 'dogs')  # 検証用の犬画像のディレクトリ

num_cats_tr = len(os.listdir(train_cats_dir))
num_dogs_tr = len(os.listdir(train_dogs_dir))

num_cats_val = len(os.listdir(validation_cats_dir))
num_dogs_val = len(os.listdir(validation_dogs_dir))

total_train = num_cats_tr + num_dogs_tr
total_val = num_cats_val + num_dogs_val

# 画像を変形して水増しするジェネレータを作る(データオーギュメンテーション)
image_gen_train = ImageDataGenerator(
                   rescale=1./255,
                   rotation_range=45, # 画像をランダムに回転する回転範囲
                   width_shift_range=.15, # ランダムに水平シフトする範囲
                   height_shift_range=.15, # ランダムに垂直シフトする範囲
                   horizontal_flip=True, # 水平方向に入力をランダムに反転します.
                   zoom_range=0.5 # ランダムにズームする範囲
                   )

# ジェネレータに画像データを与える                  
train_data_gen = image_gen_train.flow_from_directory(batch_size=batch_size,
                                                    directory=train_dir,
                                                    shuffle=True,
                                                    target_size=(IMG_HEIGHT, IMG_WIDTH),
                                                    class_mode='binary')

# イテレータにする
it = iter(train_data_gen)
# 1つデータを取り出す
d = next(it)

# Numpy配列の画像を表示する
plt.imshow(d[0][0]) # 最初の添字0は0=画像、1=ラベル、2番目の添字はバッチ内での位置

実行すると次のように画像が表示されます。
少し歪んでいるのが分かりますね。

画像1

補足

2種類の読み込み方法を説明しましたが、読み込みの速さの違いはどのぐらいあるのか気になるところだと思います。

調べたところ、次のような記事が見つかりました。

ptf.data.Datasetを使ったほうが、ImageDataGeneratorを使うよりも5倍早いと結論づけているようです。


最後に、若干宣伝ぽくて恐縮ですが、私はフリーランスエンジニアをしております。このような機械学習をiPhoneデバイス上で動作させるといったお仕事もできますので、お気軽にご相談下さい。

連絡先名:TokyoYoshida
連絡先: yoshidaforpublic@gmail.com


この記事が気に入ったらサポートをしてみませんか?