Prepare text to use in TensorFlow models

Qu’entend-on par préparation du texte afin d’être utilisé par un modèle TensorFlow ? Quelles sont les contraintes ?

Tout d’abord, il faut transformer ce texte en nombres, seules données acceptées par un modèle Tf. Ensuite il faut que tous les éléments soient de même longueur. Enfin, il faut que ce codage soit pertinent.

import tensorflow as tf

import tensorflow_datasets as tfds
import os
print(tf.__version__)
2.2.0-rc4

Pour ce tutoriel, il faut au moins la version 2.1 de TensorFlow.

On travaille sur 3 traductions différentes, en anglais, de l’Iliade de Homère, celle de William Cowper, celle de Edward Smith-Stanley et celle de Samuel Butler.

Le but est de « deviner », à partir d’une ligne de traduction, à qui elle appartient.

Le code GCP est ici.

Tout d’abord, on récupère les fichiers.

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)

print(parent_dir)

Selon que votre environnement est GCP, en local avec par exemple PyCharm ou autre, le résultat de l’affichage de parent_dir sera bien entendu différent.

Sur GCP :

'/root/.keras/datasets'

Les données doivent être mises sous la forme : texte, 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)

Pour bien comprendre ce que ça donne, car ce n’est pas évident de prime abord.

Une ligne lue :

tk1 = lines_dataset.take(1)
print(list(tk1.as_numpy_iterator()))
[b'\xef\xbb\xbfSing, O goddess, the anger of Achilles son of Peleus, that brought']

Une ligne avec label.

lab_dataset = tk1.map(lambda ex: labeler(ex, 1))
print(list(lab_dataset.as_numpy_iterator()))
[(b'\xef\xbb\xbfSing, O goddess, the anger of Achilles son of Peleus, that brought', 1)]

Une ligne avec label traitée dans la boucle :

print(list(labeled_data_sets[0].take(1).as_numpy_iterator()))
[(b"\xef\xbb\xbfAchilles sing, O Goddess! Peleus' son;", 0)]
BUFFER_SIZE = 50000
BATCH_SIZE = 64
TAKE_SIZE = 5000

On regroupe toutes les lignes

all_labeled_data = labeled_data_sets[0]
for labeled_dataset in labeled_data_sets[1:]:
    all_labeled_data = all_labeled_data.concatenate(labeled_dataset)

On les mélange :

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

Et on en affiche quelques-unes :

for ex in all_labeled_data.take(5):
  print(ex)
<tf.Tensor: shape=(), dtype=string, numpy=b'He ended, and conspicuous as the height'>, <tf.Tensor: shape=(), dtype=int64, numpy=0>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'drive with his great sword at the ashen spear of Ajax. He cut it clean'>, <tf.Tensor: shape=(), dtype=int64, numpy=2>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'Come now, Olympian, swear a solemn oath'>, <tf.Tensor: shape=(), dtype=int64, numpy=1>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'first to reap the fruits of your scurvy knavery. Do you not remember'>, <tf.Tensor: shape=(), dtype=int64, numpy=2>)
(<tf.Tensor: shape=(), dtype=string, numpy=b'The limpid waters of \xc3\x86sepus, dwelt'>, <tf.Tensor: shape=(), dtype=int64, numpy=0>)

Maintenant qu’on a notre jeu de données, on peut le tokeniser.

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)

On utilise un ensemble, ce qui nous permet de n’avoir les éléments qu’en un seul exemplaire.

vocab_size = len(vocabulary_set)
print(vocab_size)
17178

Puis à partir de notre lexique, on crée un encodeur.

# Encode example
encoder = tfds.features.text.TokenTextEncoder(vocabulary_set)

example_text = next(iter(all_labeled_data))[0].numpy()
encoded_example = encoder.encode(example_text)
print(example_text)
print(encoded_example)
b'He ended, and conspicuous as the height'
[1118, 475, 13898, 10436, 10769, 10187, 1737]
def encode(text_tensor, label):
  encoded_text = encoder.encode(text_tensor.numpy())
  return encoded_text, label

On ne peut pas utilise la fonction encore tel quelle dans un map. Il faut la « convertir » pour qu’elle soit tensor friendly. Pour cela, on utilise tf.py_function.

Wraps a python function into a TensorFlow op that executes it eagerly

inp: A list of Tensor objects

Tout: A list or tuple of tensorflow data types or a single tensorflow data type if there is only one, indicating what func returns; an empty list if no value is returned (i.e., if the return value is None)

https://www.tensorflow.org/api_docs/python/tf/py_function
def encode_map_fn(text, label):
  # py_func doesn't set the shape of the returned tensors.
  encoded_text, label = tf.py_function(encode, 
                                       inp=[text, label], 
                                       Tout=(tf.int64, tf.int64))

  # `tf.data.Datasets` work best if all components have a shape set
  #  so set the shapes manually: 
  encoded_text.set_shape([None])
  label.set_shape([])

  return encoded_text, label


all_encoded_data = all_labeled_data.map(encode_map_fn)

Selon qu’on utilise la version 2.1 ou la 2.2 de Tf, la façon de faire est différente pour la suite.

En 2.1 :

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],[]))

En 2.2 (c’est plus clair !)

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)

Les TAKE_SIZE = 5000 premiers éléments sont mis dans le test_data et le reste dans le train_data. Les données sont mélangées et on effectue un padding.

Voyons un élément :

sample_text, sample_labels = next(iter(test_data))

sample_text[0], sample_labels[0]
(<tf.Tensor: shape=(16,), dtype=int64, numpy=
 array([10341,  6668, 14041,  8983,  7784,  5000,  6106,  5000, 14734,
        12683, 14115,     0,     0,     0,     0,     0])>,
 <tf.Tensor: shape=(), dtype=int64, numpy=2>)

Le padding se fait avec des 0, ce qui ajoute un nouvel élément au lexique.

vocab_size += 1