In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

TFRecords と tf.Example の使用法

Note: これらのドキュメントは私たちTensorFlowコミュニティが翻訳したものです。コミュニティによる 翻訳はベストエフォートであるため、この翻訳が正確であることや英語の公式ドキュメントの 最新の状態を反映したものであることを保証することはできません。 この翻訳の品質を向上させるためのご意見をお持ちの方は、GitHubリポジトリtensorflow/docsにプルリクエストをお送りください。 コミュニティによる翻訳やレビューに参加していただける方は、 docs-ja@tensorflow.org メーリングリストにご連絡ください。

データの読み込みを効率的にするには、データをシリアライズし、連続的に読み込めるファイルのセット(各ファイルは100-200MB)に保存することが有効です。データをネットワーク経由で流そうとする場合には、特にそうです。また、データの前処理をキャッシュする際にも役立ちます。

TFRecord形式は、バイナリレコードの系列を保存するための単純な形式です。

プロトコルバッファ は、構造化データを効率的にシリアライズする、プラットフォームや言語に依存しないライブラリです。

プロトコルメッセージは.protoという拡張子のファイルで定義されます。メッセージ型を識別する最も簡単な方法です。

tf.Exampleメッセージ(あるいはプロトコルバッファ)は、{"string": value}形式のマッピングを表現する柔軟なメッセージ型です。これは、TensorFlow用に設計され、TFXのような上位レベルのAPIで共通に使用されています。

このノートブックでは、tf.Exampleメッセージの作成、パースと使用法をデモし、その後、tf.Exampleメッセージをパースして、.tfrecordに書き出し、その後読み取る方法を示します。

注:こうした構造は有用ですが必ずそうしなければならなというものではありません。tf.data を使っていて、それでもなおデータの読み込みが訓練のボトルネックである場合でなければ、既存のコードをTFRecordsを使用するために変更する必要はありません。データセットの性能改善のヒントは、 Data Input Pipeline Performanceを参照ください。

設定


In [ ]:
import tensorflow.compat.v1 as tf


import numpy as np
import IPython.display as display

tf.Example

tf.Example用のデータ型

基本的にはtf.Example{"string": tf.train.Feature}というマッピングです。

tf.train.Featureメッセージ型は次の3つの型のうち1つをとることができます(.proto fileを参照)。一般的なデータ型の多くは、これらの型のいずれかに強制的に変換することができます。

  1. tf.train.BytesList (次の型のデータを扱うことが可能)
    • string
    • byte
  2. tf.train.FloatList (次の型のデータを扱うことが可能)
    • float (float32)
    • double (float64)
  3. tf.train.Int64List (次の型のデータを扱うことが可能)
    • bool
    • enum
    • int32
    • uint32
    • int64
    • uint64

通常のTensorFlowの型をtf.Example互換の tf.train.Featureに変換するには、次のショートカット関数を使うことができます。

どの関数も、1個のスカラー値を入力とし、上記の3つのlist型のうちの一つを含むtf.train.Featureを返します。


In [ ]:
# 下記の関数を使うと値を tf.Exampleと互換性の有る型に変換できる

def _bytes_feature(value):
    """string / byte 型から byte_listを返す"""
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def _float_feature(value):
    """float / double 型から float_listを返す"""
    return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))

def _int64_feature(value):
    """bool / enum / int / uint 型から Int64_listを返す"""
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

注:単純化のため、このサンプルではスカラー値の入力のみを扱っています。スカラー値ではない特徴を扱う最も簡単な方法は、tf.serialize_tensorを使ってテンソルをバイナリ文字列に変換する方法です。TensorFlowでは文字列はスカラー値として扱います。バイナリ文字列をテンソルに戻すには、tf.parse_tensorを使用します。

上記の関数の使用例を下記に示します。入力が様々な型であるのに対して、出力が標準化されていることに注目してください。入力が、強制変換できない型であった場合、例外が発生します。(例:_int64_feature(1.0)はエラーとなります。1.0が浮動小数点数であるためで、代わりに_float_feature関数を使用すべきです)


In [ ]:
print(_bytes_feature(b'test_string'))
print(_bytes_feature(u'test_bytes'.encode('utf-8')))

print(_float_feature(np.exp(1)))

print(_int64_feature(True))
print(_int64_feature(1))

メッセージはすべて.SerializeToString を使ってバイナリ文字列にシリアライズすることができます。


In [ ]:
feature = _float_feature(np.exp(1))

feature.SerializeToString()

tf.Example メッセージの作成

既存のデータからtf.Exampleを作成したいとします。実際には、データセットの出処はどこでも良いのですが、1件の観測記録からtf.Exampleメッセージを作る手順は同じです。

  1. 観測記録それぞれにおいて、各値は上記の関数を使って3種類の互換性のある型のうち1つだけを含むtf.train.Featureに変換する必要があります。

  2. 次に、特徴の名前を表す文字列と、#1で作ったエンコード済みの特徴量を対応させたマップ(ディクショナリ)を作成します。

  3. #2で作成したマップをFeaturesメッセージに変換します。

このノートブックでは、NumPyを使ってデータセットを作成します。

このデータセットには4つの特徴量があります。

  • False または Trueを表す論理値。出現確率は等しいものとします。
  • [0,5)の範囲から一様にサンプリングした整数値。
  • 整数特徴量をインデックスとした文字列テーブルを使って生成した文字列特徴量
  • 標準正規分布からサンプリングした浮動小数点数。

サンプルは上記の分布から独立して同じ様に分布した10,000件の観測記録からなるものとします。


In [ ]:
# データセットに含まれる観測結果の件数
n_observations = int(1e4)

# ブール特徴量 FalseまたはTrueとしてエンコードされている
feature0 = np.random.choice([False, True], n_observations)

# 整数特徴量  0以上 5未満の乱数
feature1 = np.random.randint(0, 5, n_observations)

# バイト文字列特徴量
strings = np.array([b'cat', b'dog', b'chicken', b'horse', b'goat'])
feature2 = strings[feature1]

# 浮動小数点数特徴量 標準正規分布から発生
feature3 = np.random.randn(n_observations)

これらの特徴量は、_bytes_feature, _float_feature, _int64_featureのいずれかを使ってtf.Example互換の型に強制変換されます。その後、エンコード済みの特徴量からtf.Exampleメッセージを作成できます。


In [ ]:
def serialize_example(feature0, feature1, feature2, feature3):
    """
    Creates a tf.Example message ready to be written to a file.
    ファイル出力可能なtf.Exampleメッセージを作成する
    """

    # 特徴量名とtf.Example互換データ型との対応ディクショナリを作成

    feature = {
        'feature0': _int64_feature(feature0),
        'feature1': _int64_feature(feature1),
        'feature2': _bytes_feature(feature2),
        'feature3': _float_feature(feature3),
    }

    # tf.train.Exampleを用いて特徴メッセージを作成

    example_proto = tf.train.Example(features=tf.train.Features(feature=feature))
    return example_proto.SerializeToString()

例えば、データセットに[False, 4, bytes('goat'), 0.9876]という1つの観測記録があるとします。create_message()を使うとこの観測記録からtf.Exampleメッセージを作成し印字できます。上記のように、観測記録一つ一つがFeaturesメッセージとして書かれています。tf.Example メッセージは、このFeatures メッセージを包むラッパーに過ぎないことに注意してください。


In [ ]:
# データセットからの観測記録の例

example_observation = []

serialized_example = serialize_example(False, 4, b'goat', 0.9876)
serialized_example

メッセージをデコードするには、tf.train.Example.FromStringメソッドを使用します。


In [ ]:
example_proto = tf.train.Example.FromString(serialized_example)
example_proto

TFRecordフォーマットの詳細

TFRecordファイルにはレコードのシーケンスが含まれます。このファイルはシーケンシャル読み取りのみが可能です。

それぞれのレコードには、データを格納するためのバイト文字列とデータ長、そして整合性チェックのためのCRC32C(Castagnoli多項式を使った32ビットのCRC)ハッシュ値が含まれます。

各レコードのフォーマットは下記の通りです。

uint64 長さ
uint32 長さのマスク済みcrc32ハッシュ値
byte   data[長さ]
uint32 データのマスク済みcrc32ハッシュ値

複数のレコードが結合されてファイルを構成します。CRCについてはここに說明があります。CRCのマスクは下記のとおりです。

masked_crc = ((crc >> 15) | (crc << 17)) + 0xa282ead8ul

注:TFRecordファイルを作るのに、tf.Exampleを使わなければならないということはありません。tf.Exampleは、ディクショナリをバイト文字列にシリアライズする方法の1つです。エンコードされた画像データや、(tf.io.serialize_tensorを使ってシリアライズされ、tf.io.parse_tensorで読み込まれる)シリアライズされたテンソルもあります。その他のオプションについては、tf.ioモジュールを参照してください。

tf.dataを使用したTFRecordファイル

tf.dataモジュールには、TensorFlowでデータを読み書きするツールが含まれます。

TFRecordファイルの書き出し

データをデータセットにする最も簡単な方法はfrom_tensor_slicesメソッドです。

配列に適用すると、このメソッドはスカラー値のデータセットを返します。


In [ ]:
tf.data.Dataset.from_tensor_slices(feature1)

配列のタプルに適用すると、タプルのデータセットが返されます。


In [ ]:
features_dataset = tf.data.Dataset.from_tensor_slices((feature0, feature1, feature2, feature3))
features_dataset

In [ ]:
#  データセットから1つのサンプルだけを取り出すには`take(1)` を使います。
for f0,f1,f2,f3 in features_dataset.take(1):
    print(f0)
    print(f1)
    print(f2)
    print(f3)

Datasetのそれぞれの要素に関数を適用するには、tf.data.Dataset.mapメソッドを使用します。

マップされる関数はTensorFlowのグラフモードで動作する必要があります。関数はtf.Tensorsを処理し、返す必要があります。create_exampleのような非テンソル関数は、互換性のためtf.py_funcでラップすることができます。

tf.py_funcを使用する際には、シェイプと型は取得できないため、指定する必要があります。


In [ ]:
def tf_serialize_example(f0,f1,f2,f3):
    tf_string = tf.py_func(
        serialize_example, 
        (f0,f1,f2,f3),  # pass these args to the above function.
        tf.string)      # the return type is `tf.string`.
    return tf.reshape(tf_string, ()) # The result is a scalar

この関数をデータセットのそれぞれの要素に適用します。


In [ ]:
serialized_features_dataset = features_dataset.map(tf_serialize_example)
serialized_features_dataset

TFRecordファイルに書き出します。


In [ ]:
filename = 'test.tfrecord'
writer = tf.data.experimental.TFRecordWriter(filename)
writer.write(serialized_features_dataset)

TFRecordファイルの読み込み

tf.data.TFRecordDatasetクラスを使ってTFRecordファイルを読み込むこともできます。

tf.dataを使ってTFRecordファイルを取り扱う際の詳細については、こちらを参照ください。

TFRecordDatasetを使うことは、入力データを標準化し、パフォーマンスを最適化するのに有用です。


In [ ]:
filenames = [filename]
raw_dataset = tf.data.TFRecordDataset(filenames)
raw_dataset

この時点で、データセットにはシリアライズされたtf.train.Exampleメッセージが含まれています。データセットをイテレートすると、スカラーの文字列テンソルが返ってきます。

.takeメソッドを使って最初の10レコードだけを表示します。

注:tf.data.Datasetをイテレートできるのは、Eager Executionが有効になっている場合のみです。


In [ ]:
for raw_record in raw_dataset.take(10):
    print(repr(raw_record))

これらのテンソルは下記の関数でパースできます。

注:ここでは、feature_descriptionが必要です。データセットはグラフ実行を使用するため、この記述を使ってシェイプと型を構築するのです。


In [ ]:
# 特徴の記述
feature_description = {
    'feature0': tf.FixedLenFeature([], tf.int64, default_value=0),
    'feature1': tf.FixedLenFeature([], tf.int64, default_value=0),
    'feature2': tf.FixedLenFeature([], tf.string, default_value=''),
    'feature3': tf.FixedLenFeature([], tf.float32, default_value=0.0),
}

def _parse_function(example_proto):
  # 上記の記述を使って入力のtf.Exampleを処理
  return tf.parse_single_example(example_proto, feature_description)

あるいは、tf.parse exampleを使ってバッチ全体を一度にパースします。

tf.data.Dataset.mapメソッドを使って、データセットの各アイテムにこの関数を適用します。


In [ ]:
parsed_dataset = raw_dataset.map(_parse_function)
parsed_dataset

Eager Execution を使ってデータセット中の観測記録を表示します。このデータセットには10,000件の観測記録がありますが、最初の10個だけ表示します。
データは特徴量のディクショナリの形で表示されます。それぞれの項目はtf.Tensorであり、このテンソルのnumpy 要素は特徴量を表します。


In [ ]:
for parsed_record in parsed_dataset.take(10):
  print(repr(raw_record))

ここでは、tf.parse_exampletf.Exampleのフィールドを通常のテンソルに展開しています。

tf.python_ioを使ったTFRecordファイル

tf.python_ioモジュールには、TFRecordファイルの読み書きのための純粋なPython関数も含まれています。

TFRecordファイルの書き出し

次にこの10,000件の観測記録をtest.tfrecordsファイルに出力します。観測記録はそれぞれtf.Exampleメッセージに変換され、ファイルに出力されます。その後、test.tfrecordsファイルが作成されたことを確認することができます。


In [ ]:
# `tf.Example`観測記録をファイルに出力
with tf.python_io.TFRecordWriter(filename) as writer:
  for i in range(n_observations):
    example = serialize_example(feature0[i], feature1[i], feature2[i], feature3[i])
    writer.write(example)

In [ ]:
!ls

TFRecordファイルの読み込み

モデルに入力にするため、このデータを読み込みたいとしましょう。

次の例では、データをそのまま、tf.Exampleメッセージとしてインポートします。これは、ファイルが期待されるデータを含んでいるかを確認するのに役に立ちます。これは、また、入力データがTFRecordとして保存されているが、この例のようにNumPyデータ(またはそれ以外のデータ型)として入力したい場合に有用です。このコーディング例では値そのものを読み取れるからです。

入力ファイルの中のTFRecordをイテレートして、tf.Exampleメッセージを取り出し、その中の値を読み取って保存できます。


In [ ]:
record_iterator = tf.python_io.tf_record_iterator(path=filename)

for string_record in record_iterator:
  example = tf.train.Example()
  example.ParseFromString(string_record)
  
  print(example)
  
  # Exit after 1 iteration as this is purely demonstrative.
  # 純粋にデモであるため、イテレーションの1回目で終了
  break

(上記で作成したtf.Example型の)exampleオブジェクトの特徴量は(他のプロトコルバッファメッセージと同様に)ゲッターを使ってアクセス可能です。example.featuresrepeated featureメッセージを返し、featureメッセージをを取得すると(Pythonのディクショナリとして保存された)特徴量の名前と特徴量の値のマッピングが得られます。


In [ ]:
print(dict(example.features.feature))

このディクショナリから、指定した値をディクショナリとして得ることができます。


In [ ]:
print(example.features.feature['feature3'])

次に、ゲッターを使って値にアクセスできます。


In [ ]:
print(example.features.feature['feature3'].float_list.value)

ウォークスルー: 画像データの読み書き

以下は、TFRecordを使って画像データを読み書きする方法の例です。この例の目的は、データ(この場合は画像)を入力し、そのデータをTFRecordファイルに書き込んで、再びそのファイルを読み込み、画像を表示するという手順を最初から最後まで示すことです。

これは、例えば、同じ入力データセットを使って複数のモデルを構築するといった場合に役立ちます。画像データをそのまま保存する代わりに、TFRecord形式に前処理しておき、その後の処理やモデル構築に使用することができます。

まずは、雪の中の猫の画像と、ニューヨーク市にあるウイリアムズバーグ橋の 写真をダウンロードしましょう。

画像の取得


In [ ]:
cat_in_snow  = tf.keras.utils.get_file('320px-Felis_catus-cat_on_snow.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/Felis_catus-cat_on_snow.jpg/320px-Felis_catus-cat_on_snow.jpg')
williamsburg_bridge = tf.keras.utils.get_file('194px-New_East_River_Bridge_from_Brooklyn_det.4a09796u.jpg','https://upload.wikimedia.org/wikipedia/commons/thumb/f/fe/New_East_River_Bridge_from_Brooklyn_det.4a09796u.jpg/194px-New_East_River_Bridge_from_Brooklyn_det.4a09796u.jpg')

In [ ]:
display.display(display.Image(filename=cat_in_snow))
display.display(display.HTML('Image cc-by: <a "href=https://commons.wikimedia.org/wiki/File:Felis_catus-cat_on_snow.jpg">Von.grzanka</a>'))

In [ ]:
display.display(display.Image(filename=williamsburg_bridge))
display.display(display.HTML('<a "href=https://commons.wikimedia.org/wiki/File:New_East_River_Bridge_from_Brooklyn_det.4a09796u.jpg">source</a>'))

TFRecordファイルの書き出し

上記で行ったように、この特徴量をtf.Exampleと互換のデータ型にエンコードできます。この場合には、生画像のバイト文字列を特徴として保存するだけではなく、縦、横のサイズにチャネル数、更に画像を保存する際に猫の画像と橋の画像を区別するためのlabel特徴量を付け加えます。猫の画像には0を、橋の画像には1を使うことにしましょう。


In [ ]:
image_labels = {
    cat_in_snow : 0,
    williamsburg_bridge : 1,
}

In [ ]:
# 猫の画像を使った例
image_string = open(cat_in_snow, 'rb').read()

label = image_labels[cat_in_snow]

# 関連する特徴量のディクショナリを作成
def image_example(image_string, label):
  image_shape = tf.image.decode_jpeg(image_string).shape

  feature = {
      'height': _int64_feature(image_shape[0]),
      'width': _int64_feature(image_shape[1]),
      'depth': _int64_feature(image_shape[2]),
      'label': _int64_feature(label),
      'image_raw': _bytes_feature(image_string),
  }

  return tf.train.Example(features=tf.train.Features(feature=feature))

for line in str(image_example(image_string, label)).split('\n')[:15]:
  print(line)
print('...')

ご覧のように、すべての特徴量がtf.Exampleメッセージに保存されました。上記のコードを関数化し、このサンプルメッセージをimages.tfrecordsファイルに書き込みます。


In [ ]:
# 生の画像をimages.tfrecordsファイルに書き出す
# まず、2つの画像をtf.Exampleメッセージに変換し、
# 次に.tfrecordsファイルに書き出す
with tf.python_io.TFRecordWriter('images.tfrecords') as writer:
  for filename, label in image_labels.items():
    image_string = open(filename, 'rb').read()
    tf_example = image_example(image_string, label)
    writer.write(tf_example.SerializeToString())

In [ ]:
!ls

TFRecordファイルの読み込み

これで、images.tfrecordsファイルができました。このファイルの中のレコードをイテレートし、書き込んだものを読み出します。このユースケースでは、画像を復元するだけなので、必要なのは生画像の文字列だけです。上記のゲッター、すなわち、example.features.feature['image_raw'].bytes_list.value[0]を使って抽出することができます。猫と橋のどちらであるかを決めるため、ラベルも使用します。


In [ ]:
raw_image_dataset = tf.data.TFRecordDataset('images.tfrecords')

# 特徴量を記述するディクショナリを作成
image_feature_description = {
    'height': tf.FixedLenFeature([], tf.int64),
    'width': tf.FixedLenFeature([], tf.int64),
    'depth': tf.FixedLenFeature([], tf.int64),
    'label': tf.FixedLenFeature([], tf.int64),
    'image_raw': tf.FixedLenFeature([], tf.string),
}

def _parse_image_function(example_proto):
  # 入力のtf.Exampleのプロトコルバッファを上記のディクショナリを使って解釈
  return tf.parse_single_example(example_proto, image_feature_description)

parsed_image_dataset = raw_image_dataset.map(_parse_image_function)
parsed_image_dataset

TFRecordファイルから画像を復元しましょう。


In [ ]:
for image_features in parsed_image_dataset:
  image_raw = image_features['image_raw'].numpy()
  display.display(display.Image(data=image_raw))