Copyright 2018 The TensorFlow Authors.
Licensed under the Apache License, Version 2.0 (the "License");
tf.data: TensorFlow 入力パイプラインの構築
tf.data
API を使用すると、単純で再利用可能なピースから複雑な入力パイプラインを構築することができます。たとえば、画像モデルのパイプラインでは、分散ファイルシステムのファイルからデータを集め、各画像にランダムな摂動を適用し、ランダムに選択された画像を訓練用のバッチとして統合することがあります。また、テキストモデルのパイプラインでは、未加工のテキストデータからシンボルを抽出し、そのシンボルをルックアップテーブルとともに埋め込み識別子に変換し、異なる長さのシーケンスをまとめてバッチ処理することもあります。tf.data
API は、大量のデータを処理し、別のデータ形式から読み取り、こういった複雑な変換の実行を可能にします。
tf.data
API は、要素のシーケンスを表す tf.data.Dataset
抽象を導入します。シーケンス内の各要素は 1 つ以上のコンポーネントで構成されています。たとえば、画像パイプラインの場合、要素は画像とそのラベルを表す一組のテンソルコンポーネントを持つ単一のトレーニングサンプルである場合があります。
データセットを作成するには、次の 2 つの方法があります。
データソースによって、メモリまたは 1 つ以上のファイルに格納されたデータから
Dataset
を作成する。データ変換によって、1 つ以上の
tf.data.Dataset
オブジェクトからデータセットを作成する。
基本的な仕組み
入力パイプラインを作成するには、データソースから着手する必要があります。たとえば、メモリ内のデータから Dataset
を作成する場合、tf.data.Dataset.from_tensors()
または tf.data.Dataset.from_tensor_slices()
を使用できます。推奨される TFRecord 形式で入力データがファイル内に格納されている場合は、tf.data.TFRecordDataset()
を使用することもできます。
Dataset
オブジェクトを作成したら、tf.data.Dataset
オブジェクトに対してチェーンメソッド呼び出しを行い、新しい Dataset
オブジェクトに変換することができます。たとえば、Dataset.map
などの要素ごとの変換を適用したり、Dataset.batch()
などの複数の要素に対する変換を適用したりすることができます。変換の全リストについては、tf.data.Dataset
のドキュメントをご覧ください。
Dataset
オブジェクトは Python イテラブルです。そのため、for ループを使って、その要素を消費することができます。
または、iter
を使って Python イテレータを明示的に作成し、next
を使って要素を消費することもできます。
また、reduce
変換を使ってデータセットの要素を消費することもできます。この場合、単一の結果を得るためにすべての要素が減らされます。次の例では、整数のデータセットの合計を計算するために reduce
変換を使用する方法を説明しています。
データセットの構造
データセットは、要素ごとに同じ(ネスト)構造のコンポーネントを持つ一連の要素を生成し、構造の各コンポーネントは tf.TypeSpec
で表現可能な、tf.Tensor
、tf.sparse.SparseTensor
、tf.RaggedTensor
、tf.TensorArray
、tf.data.Dataset
など、あらゆる型を持つことができます。
要素の(ネスト)構造を表現するために使用される Python コンストラクトには、tuple
、dict
、NamedTuple
、および OrderedDict
があります。特に list
は、データセット要素の構造を表現するコンストラクトとして有効ではありません。これは、初期の tf.data
ユーザーに、list
入力(code7}tf.data.Dataset.from_tensors に渡される入力など)がテンソルとして自動的に圧縮されることと、list
出力(ユーザー定義関数の戻り値など)が tuple
に強制されることを強く希望していたためです。その結果、list
入力を構造として扱う場合には tuple
に変換し、単一コンポーネントの list
出力が必要な場合は tf.stack
を使って明示的に圧縮する必要があります。
各要素コンポーネントの型を検査するには、Dataset.element_spec
プロパティを使用できます。このプロパティは tf.TypeSpec
オブジェクトのネストされた構造を、要素の構造を一致させて返します。これは、単一のコンポーネント、コンポーネントのタプル、またはコンポーネントのネストされたタプルである場合があります。以下に例を示します。
Dataset
変換では、あらゆる構造のデータセットがサポートされています。Dataset.map
を使用し、各要素に関数を適用する Dataset.filter
変換を適用すると、要素の構造によって関数の引数が判定されます。
入力データの読み取り
NumPy 配列を消費する
その他の例については、NumPy 配列を読み込むチュートリアルをご覧ください。
すべての入力データがメモリに収容できる場合、このデータから Dataset
を作成するには、データを tf.Tensor
オブジェクトに変換してから Dataset.from_tensor_slices
を使用するのが最も簡単な方法です。
注意: 上記のコードスニペットは、features
配列と labels
配列を tf.constant()
演算として TesorFlow グラフに埋め込みます。これは小さなデータセットではうまく機能しますが、配列の内容が何度もコピーされるため、メモリを浪費してしまい、tf.GraphDef
プロトコルバッファの 2GB 制限に達してしまう可能性があります。
Python ジェネレータの消費
tf.data.Dataset
として簡単に統合できるもう 1 つの一般的なデータソースは、Python ジェネレータです。
注意: 便利なアプローチではあるものの、これには移植性と拡張性の制限があります。ジェネレータを作成した Python プロセスで実行する必要があり、それでも、Python GIL の制約を受けます。
Dataset.from_generator
コンストラクタは Python ジェネレータを完全に機能する tf.data.Dataset
に変換します。
このコンストラクタは、イテレータではなく、呼び出し可能オブジェクトを入力として取ります。このため、ジェネレータが最後に達するとそれを再開することができます。オプションで args
引数を取ることができ、呼び出し可能オブジェクトの引数として渡されます。
tf.data
は内部で tf.Graph
を構築するため output_types
引数は必須であり、グラフのエッジには、tf.dtype
が必要です。
output_shapes
引数は必須ではありませんが、多数の TensorFlow 演算では、不明な階数のテンソルはサポートされていないため、強く推奨されます。特定の軸の長さが不明または可変である場合は、output_shapes
で None
として設定してください。
また、output_shapes
と output_types
は、ほかのデータセットのメソッドと同じネスト規則に従うことに注意することが重要です。
以下は、この両方の側面を示すジェネレーターの例です。配列のタプルを返し、2 つ目の配列は、不明な長さを持つベクトルです。
最初の出力は int32
で、2 つ目は float32
です。
最初の項目はスカラーの形状 ()
で、2 つ目は長さが不明なベクトルの形状 (None,)
です。
これで、通常の tf.data.Dataset
のように使用できるようになりました。可変形状を持つデータセットをバッチ処理する場合は、Dataset.padded_batch
を使用する必要があります。
より現実的な例として、preprocessing.image.ImageDataGenerator
を tf.data.Dataset
としてラップしてみましょう。
まず、データをダウンロードします。
image.ImageDataGenerator
を作成します。
TFRecord データの消費
エンドツーエンドの例については、TFRcords を読み込むチュートリアルをご覧ください。
tf.data
API では多様なファイル形式がサポートされているため、メモリに収まらない大規模なデータセットを処理することができます。たとえば、TFRecord ファイル形式は、単純なレコード指向のバイナリー形式で、多くの TensorFlow アプリケーションでデータの訓練に使用されています。tf.data.TFRecordDataset
クラスでは、入力パイプラインの一部として 1 つ以上の TFRecord ファイルの内容をストリーミングすることができます。
以下の例では、French Street Name Signs(FSNS)から取得したテストファイルを使用しています。
TFRecordDataset
イニシャライザの filenames
引数は、文字列、文字列のリスト、または文字列の tf.Tensor
のいずれかです。そのため、訓練と検証プロセスに使用するファイルが 2 セットある場合、入力引数として filenames を取って、データセットを生成するファクトリーメソッドを作成することができます。
多数の TensorFlow プロジェクトは、TFRecord ファイルでシリアル化された tf.train.Example
レコードを使用します。これらのレコードは、検査前にデコードされている必要があります。
テキストデータの消費
エンドツーエンドの例については、テキストを読み込むチュートリアルをご覧ください。
多くのデータセットは 1 つ以上のテキストファイルとして分散されていますが、tf.data.TextLineDataset
を使用してそれらのファイルから簡単に行を抽出することができます。1 つ以上のファイル名を指定すると、TextLineDataset
によって、ファイルの行ごとに 1 つの文字列値の要素が生成されます。
以下は、最初のファイルの数行です。
複数のファイルの行を交互に抽出するには、Dataset.interleave
を使用するとより簡単にファイルを混ぜ合わせて抽出できます。以下は、各変換の 1 行目から 3 行目です。
ファイルがヘッダー行で始まる場合やコメントが含まれている場合なども含め、TextLineDataset
は、デフォルトで各ファイルの行を抽出しますが、Dataset.skip()
または Dataset.filter()
変換を使用することで、こういった行を取り除くことができます。以下では、最初の行をスキップし、フィルタをかけて生存者のみを検索しています。
CSV データの消費
その他の例については、CSV ファイルを読み込むチュートリアルとPandas DataFrames を読み込むチュートリアルをご覧ください。
CSV ファイル形式は、表形式のデータをプレーンテキストで保管するために広く使用される形式です。
以下に例を示します。
データがメモリに収まる場合は、同じ Dataset.from_tensor_slices
メソッドを辞書に使用し、このデータをインポートしやすくすることができます。
さらに拡張性の高い手法として、必要に応じてディスクから読み込むことができます。
tf.data
モジュールには、RFC 4180 に準拠する 1 つ以上の CSV ファイルからレコードを抽出するメソッドがあります。
tf.data.experimental.make_csv_dataset
関数は、一連の CSV ファイルを読み取るための高度なインターフェースで、使用しやすいように、列の型推論や、バッチ処理とシャッフルといった多数の機能をサポートしています。
列のサブセットのみが必要である場合には、select_columns
引数を使用できます。
また、より細かい制御を行えるように、experimental.CsvDataset
という下位クラスもあります。列の型推論はサポートされていないため、各列の型を指定する必要があります。
この下位インターフェースでは、一部の列が空である場合に、列の型の代わりに使用するデフォルト値を指定することができます。
デフォルトでは、CsvDataset
はファイルのすべての行のすべての列を生成するようになっていますが、ファイルが無視すべきヘッダー行で開始している場合や、入力に不要な列がある場合など、このデフォルトの動作が望ましくないことがあります。こういった行とフィールドについては、それぞれ header
引数と select_cols
引数を使用することで取り除くことができます。
ファイルのセットの消費
多くのデータセットは一連のファイルセットとして分散されており、各ファイルは 1 つの例です。
注意: これらの画像のライセンスは CC-BY に帰属します。詳細は、LICENSE.txt を参照してください。
ルートディレクトリには、各クラスのディレクトリが含まれます。
各クラスディレクトリのファイルは例です。
tf.io.read_file
関数を使用してデータを読み取り、パスからラベルを抽出して (image, label)
のペアを返します。
データセット要素のバッチ処理
単純なバッチ処理
最も単純なバッチ処理の形態は、n
個の連続するデータセット要素を 1 つの要素にスタックする方法です。Dataset.batch()
変換によってこれを実行することができますが、 tf.stack()
演算子と同じ制約が伴い、要素の各コンポーネントに適用されます。つまり、各コンポーネント i に対し、すべての要素にまったく同じ形状のテンソルが必要です。
tf.data
は形状情報を伝搬しようとしますが、最後のバッチがいっぱいになっていない可能性があるため、Dataset.batch
のデフォルト設定は不明なバッチサイズとなります。形状の None
に注意してください。
drop_remainder
引数を使用して最後のバッチを無視し、完全に形状を伝搬させます。
パディングによるテンソルのバッチ処理
上記のレシピは、すべてのテンソルが同じサイズである場合に機能しますが、多くのモデル(シーケンスモデルなど)では、さまざまなサイズの入力データが使用されています(異なる長さのシーケンスなど)。このケースに対応するには、Dataset.padded_batch
変換を使い、パディングされている可能性のある 1 つ以上の次元を指定することで、異なる形状のテンソルをバッチ処理することができます。
Dataset.padded_batch
変換では、各コンポーネントに異なるパディングを設定できます。このパディングは可変長(上記の例では None
で指定)または固定長です。また、パディング値をオーバーライドすることも可能で、その場合は、0 となります。
トレーニングワークフロー
複数のエポックの処理
tf.data
API では、同一のデータの複数のエポックを主に 2 つの方法で処理することができます。
複数のエポック内でデータセットをイテレートする最も簡単な方法は、Dataset.repeat()
変換を使用することです。まず、titanic データのデータセットを作成します。
Dataset.repeat()
変換を引数を使用せずに適用すると、入力が無限に繰り返されます。
Dataset.repeat
変換は、1 つのエポックの最後と次のエポックの始まりを伝達することなく、その引数を連結します。このため、Dataset.repeat
の後に適用される Dataset.batch
は、エポックの境界をまたぐバッチを生成してしまいます。
エポックを明確に分離する必要がある場合は、repeat の前に Dataset.batch
を記述します。
各エポックの最後にカスタム計算(統計の収集など)を実行する場合は、エポックごとにデータセットのイテレーションを再開することが最も簡単です。
入力データのランダムシャッフル
Dataset.shuffle()
変換は、固定サイズのバッファを維持し、次の要素をそのバッファからランダムに均等して選択します。
注意: buffer_size が大きければより全体的にシャッフルされますが、メモリを多く消費し、より長い時間がかかる可能性があります。これが問題となる場合は、ファイル全体で Dataset.interleave
を使用することを検討してください。
効果を確認できるように、データセットにインデックスを追加します。
buffer_size
は 100 であり、バッチサイズは 20 であるため、最初のバッチには、120 を超えるインデックスの要素は含まれません。
Dataset.batch
と同様に、Dataset.repeat
に対する順番は重要です。
Dataset.shuffle
は、シャッフルのバッファが空になるまでエポックの最後をシグナルしません。そのため、repeat の後に記述される shuffle は、次のエポックに移動する前のエポックのすべての要素を表示します。
ただし、shuffle の前の repeat によって、エポックの境界が混合されてしまいます。
データの前処理
Dataset.map(f)
変換は、指定された関数 f
を入力データセットの各要素に適用します。これは、関数型プログラミング言語でリスト(およびその他の構造)に一般的に適用される map()
関数に基づきます。関数 f
は、入力内の単一の要素を表す tf.Tensor
オブジェクトを取り、新しいデータセット内の単一の要素を表す tf.Tensor
オブジェクトを返します。この実装には、1 つの要素を別の要素に変換する標準的な TensorFlow 演算が使用されています。
このセクションでは、Dataset.map()
の一般的な使用例を説明します。
画像データのデコードとサイズ変更
実世界の画像データでニューラルネットワークを訓練する際、異なるサイズのデータを固定サイズでバッチ処理できるように、一般的なサイズに変換しなければならないことがよくあります。
花のファイル名のデータセットを再構築します。
データセットの要素を操作する関数を記述します。
これが機能するかをテストします。
それをデータセットにマッピングします。
任意の Python ロジックの適用
パフォーマンスの理由により、データの前処理には可能な限り TensorFlow 演算を使用してください。ただし、入力データを解析する際は、外部の Python ライブラリを呼び出すと便利な場合があります。Dataset.map
変換で、tf.py_function
演算を使用することができます。
たとえば、ランダムな回転を適用する場合、tf.image
モジュールには tf.image.rot90
しかないため、画像を拡張するにはあまり便利とは言えません。
注意: tensorflow_addons
には、tensorflow_addons.image.rotate
というように、TensorFlow と互換性のある rotate
があります。
tf.py_function
の動作を実演するために、scipy.ndimage.rotate
関数を代わりに使用してみましょう。
この関数を Dataset.map
と使用する場合、Dataset.from_generator
と同じ警告が適用されるため、関数を適用する際に、戻される形状と型を記述する必要があります。
tf.Example
プロトコルバッファメッセージの解析
多くの入力パイプラインは、TFRecord 形式から tf.train.Example
プロトコルバッファメッセージを抽出します。各 tf.train.Example
レコードには、1 つ以上の「特徴量」が含まれており、入力パイプラインは通常、これらの特徴量をテンソルに変換します。
データを理解するには、tf.data.Dataset
の外部で tf.train.Example
プロトコルを処理することができます。
エンドツーエンドの時系列の例については、時系列の予測をご覧ください。
時系列データは、時間軸がそのままの状態で編成されることがよくあります。
単純な Dataset.range
を使用して実演します。
通常、このようなデータに基づくでモデルでは、連続したタイムスライスが求められます。
最も単純な手法は、データをバッチ処理することです。
batch
の使用
または、密度の高い予測を 1 ステップ先に行うには、特徴量とラベルを相互に 1 ステップずつシフトします。
固定オフセットの代わりにウィンドウ全体を予測するには、バッチを 2 つに分割することができます。
1 つのバッチの特徴量と別のバッチのラベルをオーバーラップできるようにするには、Dataset.zip
を使用します。
window
の使用
Dataset.batch
を使用することもできますが、より細かい制御が必要となる場合があります。Dataset.window
メソッドを使えば完全に制御できるようになりますが、このメソッドは、Datasets
の Dataset
を返すことに注意する必要があります。詳細は、Dataset の構造をご覧ください。
Dataset.flat_map
メソッドは、データセットのデータセットを取り、単一のデータセットにフラット化することができます。
ほぼすべての場合において、最初にデータセットを Dataset.batch
処理します。
これで、shift
引数がどれくらい各ウィンドウを移動するかがわかりました。
これを合わせて、この関数を記述することができます。
こうすると、前と同じようにラベルを簡単に抽出できるようになります。
リサンプリング
クラスが非常に不均衡なデータセットを使用する場合は、データセットをリサンプリングすることができます。tf.data
では、2 つのメソッドを使ってこれを実行することができます。こういった問題の例には、クレジットカード詐欺のデータセットを使用できます。
注意: チュートリアル全文は、不均衡なデータでの分類をご覧ください。
ここで、クラスの分布を確認してください。非常に歪んでいます。
不均衡なデータを使用して訓練する際の一般的な手法は、データの均衡をとることです。tf.data
には、このワークフローを実行するためのメソッドがいくつか含まれています。
データセットのサンプリング
データセットをリサンプリングするための 1 つに、sample_from_datasets
を使用する手法が挙げられます。これは、クラスごとに別々の tf.data.Dataset
がある場合に、さらに適用できる手法です。
ここでは、フィルタをかけて、クレジットカード詐欺のデータセットからデータセットを生成します。
tf.data.Dataset.sample_from_datasets
を使用するために、データセットと各データセットのウェイトを渡します。
これでデータセットから各クラスのサンプルが 50/50 の等しい確率で得られるようになりました。
棄却リサンプリング
上記の Dataset.sample_from_datasets
の手法には、クラスごとに個別の tf.data.Dataset
が必要となることが難点です。Dataset.filter
を使用することもできますが、すべてのデータが 2 回読み込まれることになってしまいます。
tf.data.Dataset.rejection_resample
メソッドは、適用すると、1 回の読み込みでデータセットの均衡をとることができます。要素については、均衡を得るためにドロップされるか繰り返されます。
rejection_resample
メソッドは、class_func
引数を取ります。この class_func
は、各データセット要素に適用され、均衡を取る目的で、どのクラスに例が属するかを判定するために使用されます。
ここでの目標は、ラベルの分布を均衡化することですが、creditcard_ds
はすでに (features, label)
ペアになっているため、class_func
はそれらのラベルを戻すことができます。
resampler は個別の例を処理するため、そのメソッドを適用する前にデータセットを unbatch
する必要があります。
メソッドにはターゲット分布と、オプションとして初期分布の推定が必要です。
rejection_resample
メソッドは (class, example)
ペアを返します。class
は class_func
の出力です。この場合、example
はすでに (feature, label)
ペアであるため、map
を使用して、余分なラベルのコピーを取り除きます。
これでデータセットから各クラスのサンプルが 50/50 の等しい確率で得られるようになりました。
イテレータのチェックポイント
Tensorflow では、トレーニングプロセスが再開する際に、ほとんどの進捗状況を復元するための最新のチェックポイントを復元できるように、チェックポイントの使用がサポートされています。モデルの変数にチェックポイントを設定するだけでなく、データセットのイテレータにチェックポイントを設定することもできます。このため、大規模なデータセットを使用しており、再開されるたびにデータセットの始まりから開始しないようにする場合に役立ちます。ただし、Dataset.shuffle
や Dataset.prefetch
などの変換には、イテレータ内にバッファ要素が必要となるため、イテレータのチェックポイントが大量になる可能性があります。
チェックポイントにイテレータを含めるには、イテレータを tf.train.Checkpoint
コンストラクタに渡します。
注意: tf.py_function
などの外部の状態に依存するイテレータにチェックポイントを設定することはできません。設定しようとすると、外部の状態に関する問題を示す例外が発生します。
tf.keras
と tf.data
を使用する
tf.keras
API は、機械学習モデルの作成や実行に関する多くの側面を単純化します。その Model.fit
、Model.evaluate
、および Model.predict
APIは、入力としてのデータセットをサポートします。以下は、簡単なデータセットとモデルのセットアップです。
(feature, label)
ペアのデータセットを渡すだけで、Model.fit
と Model.evaluate
を実行できます。
Dataset.repeat
を呼び出すなどして無限データセットを渡す場合も、steps_per_epoch
引数を渡すだけで完了です。
評価するには、評価のステップ数を渡すことができます。
長いデータセットについては、評価するステップ数を設定します。
Model.predict
を呼び出す際に、ラベルは必要ありません。
ただし、ラベルを含むデータセットを渡した場合、そのラベルは無視されます。