tf.data を使ったテキストの読み込み

View on TensorFlow.org Run in Google Colab View source on GitHub Download notebook

このチュートリアルでは、tf.data.TextLineDataset を使ってテキストファイルからサンプルを読み込む方法を例示します。TextLineDataset は、テキストファイルからデータセットを作成するために設計されています。この中では、元のテキストファイルの一行一行がサンプルです。これは、(たとえば、詩やエラーログのような)基本的に行ベースのテキストデータを扱うのに便利でしょう。

このチュートリアルでは、おなじ作品であるホーマーのイリアッドの異なる 3 つの英語翻訳版を使い、テキスト 1 行から翻訳者を特定するモデルを訓練します。

設定

!pip install -q tf-nightly
import tensorflow as tf

import tensorflow_datasets as tfds
import os

3 つの翻訳のテキストは次のとおりです。

このチュートリアルで使われているテキストファイルは、ヘッダ、フッタ、行番号、章のタイトルの削除など、いくつかの典型的な前処理を行ったものです。前処理後のファイルをダウンロードしましょう。

DIRECTORY_URL = 'https://storage.googleapis.com/download.tensorflow.org/data/illiad/'
FILE_NAMES = ['cowper.txt', 'derby.txt', 'butler.txt']

for name in FILE_NAMES:
  text_dir = tf.keras.utils.get_file(name, origin=DIRECTORY_URL+name)

parent_dir = os.path.dirname(text_dir)

parent_dir
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/illiad/cowper.txt
819200/815980 [==============================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/illiad/derby.txt
811008/809730 [==============================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/illiad/butler.txt
811008/807992 [==============================] - 0s 0us/step

'/home/kbuilder/.keras/datasets'

テキストをデータセットに読み込む

ファイルをイテレートし、それぞれを別々のデータセットに読み込みます。

サンプルはそれぞれにラベル付けが必要なので、ラベル付け関数を適用するために tf.data.Dataset.map を使います。このメソッドは、データセット中のすべてのサンプルをイテレートし、(example, label)というペアを返します。

def labeler(example, index):
  return example, tf.cast(index, tf.int64)  

labeled_data_sets = []

for i, file_name in enumerate(FILE_NAMES):
  lines_dataset = tf.data.TextLineDataset(os.path.join(parent_dir, file_name))
  labeled_dataset = lines_dataset.map(lambda ex: labeler(ex, i))
  labeled_data_sets.append(labeled_dataset)

ラベル付けの終わったデータセットを結合して一つのデータセットにし、シャッフルします。

BUFFER_SIZE = 50000
BATCH_SIZE = 64
TAKE_SIZE = 5000
all_labeled_data = labeled_data_sets[0]
for labeled_dataset in labeled_data_sets[1:]:
  all_labeled_data = all_labeled_data.concatenate(labeled_dataset)

all_labeled_data = all_labeled_data.shuffle(
    BUFFER_SIZE, reshuffle_each_iteration=False)

tf.data.Dataset.takeprint を使って、(example, label) のペアがどのようなものかを見ることができます。numpy プロパティがそれぞれのテンソルの値を示します。

for ex in all_labeled_data.take(5):
  print(ex)
(<tf.Tensor: shape=(), dtype=string, numpy=b'side of the iron axle-tree. The felloes of the wheels were of gold,'>, <tf.Tensor: shape=(), dtype=int64, numpy=2>)
(<tf.Tensor: shape=(), dtype=string, numpy=b"For Chryses' sake, his priest, whom Atreus' son">, <tf.Tensor: shape=(), dtype=int64, numpy=1>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'Held in firm grasp, to drive the ashen spear.'>, <tf.Tensor: shape=(), dtype=int64, numpy=1>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'which Vulcan the smith had given Jove to strike terror into the hearts'>, <tf.Tensor: shape=(), dtype=int64, numpy=2>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'For Tellus and for Sol; we on our part'>, <tf.Tensor: shape=(), dtype=int64, numpy=1>)

テキスト行を数字にエンコードする

機械学習モデルが扱うのは単語ではなくて数字であるため、文字列は数字のリストに変換する必要があります。このため、一意の単語を一意の数字にマッピングします。

ボキャブラリーの構築

まず最初に、テキストをトークン化し、個々の一意な単語の集まりとして、ボキャブラリーを構築します。これを行うには、TensorFlow やPython を使ういくつかの方法があります。ここでは次のようにします。

  1. 各サンプルの numpy 値をイテレートします。
  2. tfds.features.text.Tokenizer を使って、それをトークンに分割します。
  3. 重複を排除するため、トークンを Python の集合に集約します。
  4. あとで使用するため、ボキャブラリーのサイズを取得します。
tokenizer = tfds.features.text.Tokenizer()

vocabulary_set = set()
for text_tensor, _ in all_labeled_data:
  some_tokens = tokenizer.tokenize(text_tensor.numpy())
  vocabulary_set.update(some_tokens)

vocab_size = len(vocabulary_set)
vocab_size
17178

サンプルをエンコードする

vocabulary_settfds.features.text.TokenTextEncoder に渡してエンコーダーを作成します。エンコーダーの encode メソッドは、テキスト文字列を引数にとり、整数のリストを返します。

encoder = tfds.features.text.TokenTextEncoder(vocabulary_set)

1行だけにこれを適用し、出力がどの様になるか確かめることができます。

example_text = next(iter(all_labeled_data))[0].numpy()
print(example_text)
b'side of the iron axle-tree. The felloes of the wheels were of gold,'

encoded_example = encoder.encode(example_text)
print(encoded_example)
[9670, 1055, 6187, 732, 14544, 5463, 3080, 4785, 1055, 6187, 2840, 8248, 1055, 646]

次に、このエンコーダーを tf.py_function でラッピングして、データセットの map メソッドに渡し、データセットに適用します。

def encode(text_tensor, label):
  encoded_text = encoder.encode(text_tensor.numpy())
  return encoded_text, label

データセットの個々の要素にこの関数を適用するために Dataset.map を使いたくなるかもしれません。Dataset.map はグラフモードで動作します。

  • グラフ Tensor は値を持ちません。
  • グラフモードでは TensorFlow の演算や関数のみが利用できます。

したがって、この関数を直接 .map で用いることはできません。それを tf.py_function でラップする必要があります。tf.py_function は通常の Tensor (値を持ち、.numpy() メソッドでそれにアクセスできるもの) をラップされた Python の関数に渡します。

def encode_map_fn(text, label):
  # py_func は返り値の Tensor に shape を設定しません
  encoded_text, label = tf.py_function(encode, 
                                       inp=[text, label], 
                                       Tout=(tf.int64, tf.int64))

  # `tf.data.Datasets` はすべての要素に shape が設定されているときにうまく動きます
  #  なので、shape を手動で設定しましょう
  encoded_text.set_shape([None])
  label.set_shape([])

  return encoded_text, label


all_encoded_data = all_labeled_data.map(encode_map_fn)

データセットを、テスト用と訓練用のバッチに分割する

tf.data.Dataset.taketf.data.Dataset.skipを使って、小さなテスト用データセットと、より大きな訓練用セットを作成します。

モデルに渡す前に、データセットをバッチ化する必要があります。通常、バッチの中のサンプルはおなじサイズと形状である必要があります。しかし、これらのデータセットの中のサンプルはすべておなじサイズではありません。テキストの各行の単語数は異なっています。このため、(batchの代わりに)tf.data.Dataset.padded_batch メソッドを使ってサンプルをおなじサイズにパディングします。

train_data = all_encoded_data.skip(TAKE_SIZE).shuffle(BUFFER_SIZE)
train_data = train_data.padded_batch(BATCH_SIZE, padded_shapes=([None],[]))

test_data = all_encoded_data.take(TAKE_SIZE)
test_data = test_data.padded_batch(BATCH_SIZE, padded_shapes=([None],[]))
train_data = all_encoded_data.skip(TAKE_SIZE).shuffle(BUFFER_SIZE)
train_data = train_data.padded_batch(BATCH_SIZE)

test_data = all_encoded_data.take(TAKE_SIZE)
test_data = test_data.padded_batch(BATCH_SIZE)

もう、test_datatrain_data は、(example, label)というペアのコレクションではなく、バッチのコレクションです。それぞれのバッチは、(たくさんのサンプル, たくさんのラベル)という配列のペアです。

見てみましょう。

sample_text, sample_labels = next(iter(test_data))

sample_text[0], sample_labels[0]
(<tf.Tensor: shape=(15,), dtype=int64, numpy=
 array([ 9670,  1055,  6187,   732, 14544,  5463,  3080,  4785,  1055,
         6187,  2840,  8248,  1055,   646,     0])>,
 <tf.Tensor: shape=(), dtype=int64, numpy=2>)

(ゼロをパディングに使用した)新しいトークン番号を1つ導入したので、ボキャブラリーサイズは1つ増えています。

vocab_size += 1

モデルを構築する

model = tf.keras.Sequential()

最初の層は、整数表現を密なベクトル埋め込みに変換します。詳細は単語埋め込みのチュートリアルを参照ください。

model.add(tf.keras.layers.Embedding(vocab_size, 64))

次の層はLong Short-Term Memory 層です。この層により、モデルは単語をほかの単語の文脈の中で解釈します。LSTM の Bidirectional ラッパーにより、データポイントを、その前とその後のデータポイントとの関連で学習することができます。

model.add(tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)))

最後に、一つ以上の全結合層があり、最後の層は出力層です。出力層はラベルすべての確率を生成します。もっと複雑なも確率の高いラベルが、モデルが予測するサンプルのラベルです。

# 1 つ以上の Dense 層
# `for` 行の中のリストを編集して、層のサイズの実験をしてください
for units in [64, 64]:
  model.add(tf.keras.layers.Dense(units, activation='relu'))

# 出力層 最初の引数はラベルの数
model.add(tf.keras.layers.Dense(3))

最後にモデルをコンパイルします。ソフトマックスによるカテゴリー分類モデルでは、損失関数として sparse_categorical_crossentropy を使用します。ほかのオプティマイザを使うこともできますが、adam がよく使われます。

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

モデルを訓練する

このモデルをこのデータに適用すると(約83%の)まともな結果が得られます。

model.fit(train_data, epochs=3, validation_data=test_data)
Epoch 1/3
697/697 [==============================] - 25s 35ms/step - loss: 0.6382 - accuracy: 0.6667 - val_loss: 0.3869 - val_accuracy: 0.8264
Epoch 2/3
697/697 [==============================] - 20s 29ms/step - loss: 0.2932 - accuracy: 0.8731 - val_loss: 0.3733 - val_accuracy: 0.8318
Epoch 3/3
697/697 [==============================] - 20s 29ms/step - loss: 0.2126 - accuracy: 0.9091 - val_loss: 0.4046 - val_accuracy: 0.8270

<tensorflow.python.keras.callbacks.History at 0x7fd980592b38>
eval_loss, eval_acc = model.evaluate(test_data)

print('\nEval loss: {:.3f}, Eval accuracy: {:.3f}'.format(eval_loss, eval_acc))
79/79 [==============================] - 3s 32ms/step - loss: 0.4046 - accuracy: 0.8270

Eval loss: 0.405, Eval accuracy: 0.827