Avoir une question? Connectez-vous avec la communauté sur le forum TensorFlow Visiter le forum

tf.data : Créer des pipelines d'entrée TensorFlow

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

L'API tf.data vous permet de créer des pipelines d'entrée complexes à partir de pièces simples et réutilisables. Par exemple, le pipeline d'un modèle d'image peut agréger des données à partir de fichiers dans un système de fichiers distribué, appliquer des perturbations aléatoires à chaque image et fusionner des images sélectionnées au hasard dans un lot pour l'apprentissage. Le pipeline d'un modèle de texte peut impliquer l'extraction de symboles à partir de données de texte brutes, leur conversion en identifiants intégrés avec une table de recherche et le regroupement de séquences de différentes longueurs. L'API tf.data permet de gérer de grandes quantités de données, de lire à partir de différents formats de données et d'effectuer des transformations complexes.

L'API tf.data introduit une abstractiontf.data.Dataset qui représente une séquence d'éléments, dans laquelle chaque élément se compose d'un ou plusieurs composants. Par exemple, dans un pipeline d'images, un élément peut être un exemple d'apprentissage unique, avec une paire de composants tenseurs représentant l'image et son étiquette.

Il existe deux manières distinctes de créer un jeu de données :

  • Une source de données construit un Dataset partir de données stockées en mémoire ou dans un ou plusieurs fichiers.

  • Une transformation de données construit un ensemble de données à partir d'un ou plusieurs objetstf.data.Dataset .

import tensorflow as tf
import pathlib
import os
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

np.set_printoptions(precision=4)

Mécanique de base

Pour créer un pipeline d'entrée, vous devez commencer par une source de données . Par exemple, pour construire un Dataset partir de données en mémoire, vous pouvez utiliser tf.data.Dataset.from_tensors() ou tf.data.Dataset.from_tensor_slices() . Alternativement, si vos données d'entrée sont stockées dans un fichier au format TFRecord recommandé, vous pouvez utiliser tf.data.TFRecordDataset() .

Une fois que vous avez un objet Dataset , vous pouvez le transformer en un nouveau Dataset en enchaînant les appels de méthode sur l'objettf.data.Dataset . Par exemple, vous pouvez appliquer des transformations par élément telles que Dataset.map() , et des transformations multi-éléments telles que Dataset.batch() . Consultez la documentation detf.data.Dataset pour une liste complète des transformations.

L'objet Dataset est un itérable Python. Cela permet de consommer ses éléments à l'aide d'une boucle for :

dataset = tf.data.Dataset.from_tensor_slices([8, 3, 0, 8, 2, 1])
dataset
<TensorSliceDataset shapes: (), types: tf.int32>
for elem in dataset:
  print(elem.numpy())
8
3
0
8
2
1

Ou en créant explicitement un itérateur Python en utilisant iter et en consommant ses éléments en utilisant next :

it = iter(dataset)

print(next(it).numpy())
8

Alternativement, les éléments de l'ensemble de données peuvent être consommés à l'aide de la transformation de reduce , qui réduit tous les éléments pour produire un seul résultat. L'exemple suivant illustre comment utiliser la transformation de reduce pour calculer la somme d'un ensemble de données d'entiers.

print(dataset.reduce(0, lambda state, value: state + value).numpy())
22

Structure du jeu de données

Un ensemble de données produit une séquence d' éléments , où chaque élément est la même structure (emboîtée) de composants . Les composants individuels de la structure peuvent être de n'importe quel type représentable par tf.TypeSpec , y compris tf.Tensor , tf.sparse.SparseTensor ,tf.RaggedTensor , tf.TensorArray outf.data.Dataset .

Les constructions Python qui peuvent être utilisées pour exprimer la structure (emboîtée) des éléments incluent tuple , dict , NamedTuple et OrderedDict . En particulier, list n'est pas une construction valide pour exprimer la structure des éléments de l'ensemble de données. C'est parce que les premiers utilisateurs de tf.data pensaient que les entrées de list (par exemple passées à tf.data.Dataset.from_tensors ) étaient automatiquement compressées en tant que tenseurs et que les sorties de list (par exemple, les valeurs de retour des fonctions définies par l'utilisateur) étaient forcées dans un tuple . En conséquence, si vous souhaitez qu'une entrée de list soit traitée comme une structure, vous devez la convertir en tuple et si vous souhaitez qu'une sortie de list soit un seul composant, vous devez la tf.stack explicitement à l'aide de tf.stack .

La propriété Dataset.element_spec vous permet d'inspecter le type de chaque composant d'élément. La propriété renvoie une structure imbriquée d'objets tf.TypeSpec , correspondant à la structure de l'élément, qui peut être un seul composant, un tuple de composants ou un tuple imbriqué de composants. Par example:

dataset1 = tf.data.Dataset.from_tensor_slices(tf.random.uniform([4, 10]))

dataset1.element_spec
TensorSpec(shape=(10,), dtype=tf.float32, name=None)
dataset2 = tf.data.Dataset.from_tensor_slices(
   (tf.random.uniform([4]),
    tf.random.uniform([4, 100], maxval=100, dtype=tf.int32)))

dataset2.element_spec
(TensorSpec(shape=(), dtype=tf.float32, name=None),
 TensorSpec(shape=(100,), dtype=tf.int32, name=None))
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))

dataset3.element_spec
(TensorSpec(shape=(10,), dtype=tf.float32, name=None),
 (TensorSpec(shape=(), dtype=tf.float32, name=None),
  TensorSpec(shape=(100,), dtype=tf.int32, name=None)))
# Dataset containing a sparse tensor.
dataset4 = tf.data.Dataset.from_tensors(tf.SparseTensor(indices=[[0, 0], [1, 2]], values=[1, 2], dense_shape=[3, 4]))

dataset4.element_spec
SparseTensorSpec(TensorShape([3, 4]), tf.int32)
# Use value_type to see the type of value represented by the element spec
dataset4.element_spec.value_type
tensorflow.python.framework.sparse_tensor.SparseTensor

Les Dataset de Dataset transformations prennent en charge les jeux de données de toute structure. Lors de l'utilisation des Dataset.map() et Dataset.filter() , qui appliquent une fonction à chaque élément, la structure de l'élément détermine les arguments de la fonction :

dataset1 = tf.data.Dataset.from_tensor_slices(
    tf.random.uniform([4, 10], minval=1, maxval=10, dtype=tf.int32))

dataset1
<TensorSliceDataset shapes: (10,), types: tf.int32>
for z in dataset1:
  print(z.numpy())
[2 7 7 7 5 9 3 3 5 2]
[9 9 7 3 6 2 3 8 7 1]
[2 9 7 1 8 7 8 3 8 2]
[7 5 2 6 9 4 4 1 6 4]
dataset2 = tf.data.Dataset.from_tensor_slices(
   (tf.random.uniform([4]),
    tf.random.uniform([4, 100], maxval=100, dtype=tf.int32)))

dataset2
<TensorSliceDataset shapes: ((), (100,)), types: (tf.float32, tf.int32)>
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))

dataset3
<ZipDataset shapes: ((10,), ((), (100,))), types: (tf.int32, (tf.float32, tf.int32))>
for a, (b,c) in dataset3:
  print('shapes: {a.shape}, {b.shape}, {c.shape}'.format(a=a, b=b, c=c))
shapes: (10,), (), (100,)
shapes: (10,), (), (100,)
shapes: (10,), (), (100,)
shapes: (10,), (), (100,)

Lecture des données d'entrée

Consommer des tableaux NumPy

Voir Chargement des tableaux NumPy pour plus d'exemples.

Si toutes vos données d'entrée tiennent en mémoire, le moyen le plus simple de créer un ensemble de Dataset partir de celles-ci est de les convertir en objets tf.Tensor et d'utiliser Dataset.from_tensor_slices() .

train, test = tf.keras.datasets.fashion_mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/train-labels-idx1-ubyte.gz
32768/29515 [=================================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/train-images-idx3-ubyte.gz
26427392/26421880 [==============================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/t10k-labels-idx1-ubyte.gz
8192/5148 [===============================================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/t10k-images-idx3-ubyte.gz
4423680/4422102 [==============================] - 0s 0us/step
images, labels = train
images = images/255

dataset = tf.data.Dataset.from_tensor_slices((images, labels))
dataset
<TensorSliceDataset shapes: ((28, 28), ()), types: (tf.float64, tf.uint8)>

Consommer des générateurs Python

Une autre source de données courante qui peut facilement être ingérée en tant quetf.data.Dataset est le générateur python.

def count(stop):
  i = 0
  while i<stop:
    yield i
    i += 1
for n in count(5):
  print(n)
0
1
2
3
4

Le constructeur Dataset.from_generator convertit le générateur python en untf.data.Dataset entièrement fonctionnel.

Le constructeur prend un callable en entrée, pas un itérateur. Cela lui permet de redémarrer le générateur lorsqu'il atteint la fin. Il prend un argument args facultatif, qui est passé en tant qu'arguments de l'appelable.

L'argument output_types est requis car tf.data construit un tf.Graph interne et les arêtes du graphe nécessitent un tf.dtype .

ds_counter = tf.data.Dataset.from_generator(count, args=[25], output_types=tf.int32, output_shapes = (), )
for count_batch in ds_counter.repeat().batch(10).take(10):
  print(count_batch.numpy())
[0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24  0  1  2  3  4]
[ 5  6  7  8  9 10 11 12 13 14]
[15 16 17 18 19 20 21 22 23 24]
[0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24  0  1  2  3  4]
[ 5  6  7  8  9 10 11 12 13 14]
[15 16 17 18 19 20 21 22 23 24]

L'argument output_shapes n'est pas obligatoire mais est fortement recommandé car de nombreuses opérations tensorflow ne prennent pas en charge les tenseurs de rang inconnu. Si la longueur d'un axe particulier est inconnue ou variable, définissez-la sur None dans output_shapes .

Il est également important de noter que les output_shapes et output_types suivent les mêmes règles d'imbrication que les autres méthodes d'ensemble de données.

Voici un exemple de générateur qui illustre les deux aspects, il renvoie des tuples de tableaux, où le deuxième tableau est un vecteur de longueur inconnue.

def gen_series():
  i = 0
  while True:
    size = np.random.randint(0, 10)
    yield i, np.random.normal(size=(size,))
    i += 1
for i, series in gen_series():
  print(i, ":", str(series))
  if i > 5:
    break
0 : [0.2357]
1 : [-0.4635  0.0882 -0.7401 -1.2436 -0.1392  1.8694 -2.2567  1.5493 -1.0368]
2 : []
3 : []
4 : [1.1482 1.0136]
5 : [ 0.7923 -2.2942  0.4162  1.5056  1.6008  0.1861]
6 : [ 0.7311  0.9217  1.3697 -1.0795  1.0586 -1.0768]

La première sortie est un int32 la seconde est un float32 .

Le premier élément est un scalaire, shape () , et le second est un vecteur de longueur inconnue, shape (None,)

ds_series = tf.data.Dataset.from_generator(
    gen_series, 
    output_types=(tf.int32, tf.float32), 
    output_shapes=((), (None,)))

ds_series
<FlatMapDataset shapes: ((), (None,)), types: (tf.int32, tf.float32)>

Maintenant, il peut être utilisé comme untf.data.Dataset normal. Notez que lors du traitement par lots d'un ensemble de données avec une forme variable, vous devez utiliser Dataset.padded_batch .

ds_series_batch = ds_series.shuffle(20).padded_batch(10)

ids, sequence_batch = next(iter(ds_series_batch))
print(ids.numpy())
print()
print(sequence_batch.numpy())
[17 11 18  8 23  2  6 13 26 28]

[[ 1.0784e+00  6.2397e-01  3.3750e-01 -2.1123e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [ 1.3756e-01 -1.5717e+00 -8.0335e-01  0.0000e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-9.1093e-01  6.3951e-01  4.9384e-04  2.0273e+00 -3.6473e-01 -3.6264e-02
  -7.3862e-01 -5.3504e-01]
 [ 1.6893e-01  0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-1.1584e+00 -7.9125e-01  0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-1.7383e-01 -2.5935e-01  4.8755e-01  1.5578e+00 -4.7534e-01  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-4.0072e-01 -7.4969e-01 -1.1954e+00  9.0354e-02  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-8.6638e-02 -1.4680e+00  1.3155e+00  1.1772e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-1.1352e-01 -1.1264e+00  0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]
 [-1.2858e+00  7.7001e-02 -1.7588e-01  0.0000e+00  0.0000e+00  0.0000e+00
   0.0000e+00  0.0000e+00]]

Pour un exemple plus réaliste, essayez d' emballage preprocessing.image.ImageDataGenerator commetf.data.Dataset .

Téléchargez d'abord les données :

flowers = tf.keras.utils.get_file(
    'flower_photos',
    'https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
    untar=True)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz
228818944/228813984 [==============================] - 1s 0us/step

Créez l' image.ImageDataGenerator

img_gen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255, rotation_range=20)
images, labels = next(img_gen.flow_from_directory(flowers))
Found 3670 images belonging to 5 classes.
print(images.dtype, images.shape)
print(labels.dtype, labels.shape)
float32 (32, 256, 256, 3)
float32 (32, 5)
ds = tf.data.Dataset.from_generator(
    lambda: img_gen.flow_from_directory(flowers), 
    output_types=(tf.float32, tf.float32), 
    output_shapes=([32,256,256,3], [32,5])
)

ds.element_spec
(TensorSpec(shape=(32, 256, 256, 3), dtype=tf.float32, name=None),
 TensorSpec(shape=(32, 5), dtype=tf.float32, name=None))
for images, label in ds.take(1):
  print('images.shape: ', images.shape)
  print('labels.shape: ', labels.shape)
Found 3670 images belonging to 5 classes.
images.shape:  (32, 256, 256, 3)
labels.shape:  (32, 5)

Consommation des données TFRecord

Voir Chargement de TFRecords pour un exemple de bout en bout.

L'API tf.data prend en charge une variété de formats de fichiers afin que vous puissiez traiter des ensembles de données volumineux qui ne tiennent pas en mémoire. Par exemple, le format de fichier TFRecord est un format binaire simple orienté enregistrement que de nombreuses applications TensorFlow utilisent pour les données d'entraînement. La classe tf.data.TFRecordDataset vous permet de diffuser le contenu d'un ou plusieurs fichiers TFRecord dans le cadre d'un pipeline d'entrée.

Voici un exemple utilisant le fichier test de la Signalisation des Rues Françaises (FSNS).

# Creates a dataset that reads all of the examples from two files.
fsns_test_file = tf.keras.utils.get_file("fsns.tfrec", "https://storage.googleapis.com/download.tensorflow.org/data/fsns-20160927/testdata/fsns-00000-of-00001")
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/fsns-20160927/testdata/fsns-00000-of-00001
7905280/7904079 [==============================] - 0s 0us/step

L'argument filenames de l'initialiseur TFRecordDataset peut être une chaîne, une liste de chaînes ou un tf.Tensor de chaînes. Par conséquent, si vous disposez de deux ensembles de fichiers à des fins d'entraînement et de validation, vous pouvez créer une méthode de fabrique qui produit l'ensemble de données, en prenant les noms de fichiers comme argument d'entrée :

dataset = tf.data.TFRecordDataset(filenames = [fsns_test_file])
dataset
<TFRecordDatasetV2 shapes: (), types: tf.string>

De nombreux projets TensorFlow utilisent des enregistrements tf.train.Example sérialisés dans leurs fichiers TFRecord. Ceux-ci doivent être décodés avant de pouvoir être inspectés :

raw_example = next(iter(dataset))
parsed = tf.train.Example.FromString(raw_example.numpy())

parsed.features.feature['image/text']
bytes_list {
  value: "Rue Perreyon"
}

Consommation de données textuelles

Voir Chargement de texte pour un exemple de bout en bout.

De nombreux ensembles de données sont distribués sous forme d'un ou plusieurs fichiers texte. Le tf.data.TextLineDataset fournit un moyen simple d'extraire des lignes d'un ou plusieurs fichiers texte. Étant donné un ou plusieurs noms de fichiers, un TextLineDataset produira un élément de valeur chaîne par ligne de ces fichiers.

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

file_paths = [
    tf.keras.utils.get_file(file_name, directory_url + file_name)
    for file_name in file_names
]
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
dataset = tf.data.TextLineDataset(file_paths)

Voici les premières lignes du premier fichier :

for line in dataset.take(5):
  print(line.numpy())
b"\xef\xbb\xbfAchilles sing, O Goddess! Peleus' son;"
b'His wrath pernicious, who ten thousand woes'
b"Caused to Achaia's host, sent many a soul"
b'Illustrious into Ades premature,'
b'And Heroes gave (so stood the will of Jove)'

Pour alterner les lignes entre les fichiers, utilisez Dataset.interleave . Cela facilite le brassage des fichiers ensemble. Voici les première, deuxième et troisième lignes de chaque traduction :

files_ds = tf.data.Dataset.from_tensor_slices(file_paths)
lines_ds = files_ds.interleave(tf.data.TextLineDataset, cycle_length=3)

for i, line in enumerate(lines_ds.take(9)):
  if i % 3 == 0:
    print()
  print(line.numpy())
b"\xef\xbb\xbfAchilles sing, O Goddess! Peleus' son;"
b"\xef\xbb\xbfOf Peleus' son, Achilles, sing, O Muse,"
b'\xef\xbb\xbfSing, O goddess, the anger of Achilles son of Peleus, that brought'

b'His wrath pernicious, who ten thousand woes'
b'The vengeance, deep and deadly; whence to Greece'
b'countless ills upon the Achaeans. Many a brave soul did it send'

b"Caused to Achaia's host, sent many a soul"
b'Unnumbered ills arose; which many a soul'
b'hurrying down to Hades, and many a hero did it yield a prey to dogs and'

Par défaut, un TextLineDataset chaque ligne de chaque fichier, ce qui peut ne pas être souhaitable, par exemple, si le fichier commence par une ligne d'en-tête ou contient des commentaires. Ces lignes peuvent être supprimées à l'aide des Dataset.skip() ou Dataset.filter() . Ici, vous sautez la première ligne, puis filtrez pour ne trouver que les survivants.

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
titanic_lines = tf.data.TextLineDataset(titanic_file)
Downloading data from https://storage.googleapis.com/tf-datasets/titanic/train.csv
32768/30874 [===============================] - 0s 0us/step
for line in titanic_lines.take(10):
  print(line.numpy())
b'survived,sex,age,n_siblings_spouses,parch,fare,class,deck,embark_town,alone'
b'0,male,22.0,1,0,7.25,Third,unknown,Southampton,n'
b'1,female,38.0,1,0,71.2833,First,C,Cherbourg,n'
b'1,female,26.0,0,0,7.925,Third,unknown,Southampton,y'
b'1,female,35.0,1,0,53.1,First,C,Southampton,n'
b'0,male,28.0,0,0,8.4583,Third,unknown,Queenstown,y'
b'0,male,2.0,3,1,21.075,Third,unknown,Southampton,n'
b'1,female,27.0,0,2,11.1333,Third,unknown,Southampton,n'
b'1,female,14.0,1,0,30.0708,Second,unknown,Cherbourg,n'
b'1,female,4.0,1,1,16.7,Third,G,Southampton,n'
def survived(line):
  return tf.not_equal(tf.strings.substr(line, 0, 1), "0")

survivors = titanic_lines.skip(1).filter(survived)
for line in survivors.take(10):
  print(line.numpy())
b'1,female,38.0,1,0,71.2833,First,C,Cherbourg,n'
b'1,female,26.0,0,0,7.925,Third,unknown,Southampton,y'
b'1,female,35.0,1,0,53.1,First,C,Southampton,n'
b'1,female,27.0,0,2,11.1333,Third,unknown,Southampton,n'
b'1,female,14.0,1,0,30.0708,Second,unknown,Cherbourg,n'
b'1,female,4.0,1,1,16.7,Third,G,Southampton,n'
b'1,male,28.0,0,0,13.0,Second,unknown,Southampton,y'
b'1,female,28.0,0,0,7.225,Third,unknown,Cherbourg,y'
b'1,male,28.0,0,0,35.5,First,A,Southampton,y'
b'1,female,38.0,1,5,31.3875,Third,unknown,Southampton,n'

Consommation de données CSV

Voir Chargement de fichiers CSV et Chargement de cadres de données Pandas pour plus d'exemples.

Le format de fichier CSV est un format populaire pour stocker des données tabulaires en texte brut.

Par example:

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
df = pd.read_csv(titanic_file)
df.head()

Si vos données Dataset.from_tensor_slices en mémoire, la même méthode Dataset.from_tensor_slices fonctionne sur les dictionnaires, ce qui permet d'importer facilement ces données :

titanic_slices = tf.data.Dataset.from_tensor_slices(dict(df))

for feature_batch in titanic_slices.take(1):
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
'survived'          : 0
  'sex'               : b'male'
  'age'               : 22.0
  'n_siblings_spouses': 1
  'parch'             : 0
  'fare'              : 7.25
  'class'             : b'Third'
  'deck'              : b'unknown'
  'embark_town'       : b'Southampton'
  'alone'             : b'n'

Une approche plus évolutive consiste à charger à partir du disque si nécessaire.

Le module tf.data fournit des méthodes pour extraire les enregistrements d'un ou plusieurs fichiers CSV conformes à la RFC 4180 .

La fonction experimental.make_csv_dataset est l'interface de haut niveau pour lire des ensembles de fichiers csv. Il prend en charge l'inférence de type de colonne et de nombreuses autres fonctionnalités, telles que le traitement par lots et la lecture aléatoire, pour simplifier l'utilisation.

titanic_batches = tf.data.experimental.make_csv_dataset(
    titanic_file, batch_size=4,
    label_name="survived")
for feature_batch, label_batch in titanic_batches.take(1):
  print("'survived': {}".format(label_batch))
  print("features:")
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
'survived': [1 1 1 1]
features:
  'sex'               : [b'female' b'female' b'male' b'female']
  'age'               : [40. 30. 17. 19.]
  'n_siblings_spouses': [0 0 0 0]
  'parch'             : [0 0 2 0]
  'fare'              : [ 13.      12.475  110.8833  26.    ]
  'class'             : [b'Second' b'Third' b'First' b'Second']
  'deck'              : [b'unknown' b'unknown' b'C' b'unknown']
  'embark_town'       : [b'Southampton' b'Southampton' b'Cherbourg' b'Southampton']
  'alone'             : [b'y' b'y' b'n' b'y']

Vous pouvez utiliser l'argument select_columns si vous n'avez besoin que d'un sous-ensemble de colonnes.

titanic_batches = tf.data.experimental.make_csv_dataset(
    titanic_file, batch_size=4,
    label_name="survived", select_columns=['class', 'fare', 'survived'])
for feature_batch, label_batch in titanic_batches.take(1):
  print("'survived': {}".format(label_batch))
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
'survived': [0 0 0 0]
  'fare'              : [ 15.5   55.9    8.05 108.9 ]
  'class'             : [b'Third' b'First' b'Third' b'First']

Il existe également une classe experimental.CsvDataset niveau inférieur qui fournit un contrôle plus fin. Il ne prend pas en charge l'inférence de type de colonne. Au lieu de cela, vous devez spécifier le type de chaque colonne.

titanic_types  = [tf.int32, tf.string, tf.float32, tf.int32, tf.int32, tf.float32, tf.string, tf.string, tf.string, tf.string] 
dataset = tf.data.experimental.CsvDataset(titanic_file, titanic_types , header=True)

for line in dataset.take(10):
  print([item.numpy() for item in line])
[0, b'male', 22.0, 1, 0, 7.25, b'Third', b'unknown', b'Southampton', b'n']
[1, b'female', 38.0, 1, 0, 71.2833, b'First', b'C', b'Cherbourg', b'n']
[1, b'female', 26.0, 0, 0, 7.925, b'Third', b'unknown', b'Southampton', b'y']
[1, b'female', 35.0, 1, 0, 53.1, b'First', b'C', b'Southampton', b'n']
[0, b'male', 28.0, 0, 0, 8.4583, b'Third', b'unknown', b'Queenstown', b'y']
[0, b'male', 2.0, 3, 1, 21.075, b'Third', b'unknown', b'Southampton', b'n']
[1, b'female', 27.0, 0, 2, 11.1333, b'Third', b'unknown', b'Southampton', b'n']
[1, b'female', 14.0, 1, 0, 30.0708, b'Second', b'unknown', b'Cherbourg', b'n']
[1, b'female', 4.0, 1, 1, 16.7, b'Third', b'G', b'Southampton', b'n']
[0, b'male', 20.0, 0, 0, 8.05, b'Third', b'unknown', b'Southampton', b'y']

Si certaines colonnes sont vides, cette interface de bas niveau vous permet de fournir des valeurs par défaut au lieu des types de colonnes.

%%writefile missing.csv
1,2,3,4
,2,3,4
1,,3,4
1,2,,4
1,2,3,
,,,
Writing missing.csv
# Creates a dataset that reads all of the records from two CSV files, each with
# four float columns which may have missing values.

record_defaults = [999,999,999,999]
dataset = tf.data.experimental.CsvDataset("missing.csv", record_defaults)
dataset = dataset.map(lambda *items: tf.stack(items))
dataset
<MapDataset shapes: (4,), types: tf.int32>
for line in dataset:
  print(line.numpy())
[1 2 3 4]
[999   2   3   4]
[  1 999   3   4]
[  1   2 999   4]
[  1   2   3 999]
[999 999 999 999]

Par défaut, un CsvDataset chaque colonne de chaque ligne du fichier, ce qui peut ne pas être souhaitable, par exemple si le fichier commence par une ligne d'en-tête qui doit être ignorée, ou si certaines colonnes ne sont pas requises dans l'entrée. Ces lignes et champs peuvent être supprimés avec les arguments header et select_cols respectivement.

# Creates a dataset that reads all of the records from two CSV files with
# headers, extracting float data from columns 2 and 4.
record_defaults = [999, 999] # Only provide defaults for the selected columns
dataset = tf.data.experimental.CsvDataset("missing.csv", record_defaults, select_cols=[1, 3])
dataset = dataset.map(lambda *items: tf.stack(items))
dataset
<MapDataset shapes: (2,), types: tf.int32>
for line in dataset:
  print(line.numpy())
[2 4]
[2 4]
[999   4]
[2 4]
[  2 999]
[999 999]

Consommer des ensembles de fichiers

Il existe de nombreux ensembles de données distribués sous forme d'ensemble de fichiers, où chaque fichier est un exemple.

flowers_root = tf.keras.utils.get_file(
    'flower_photos',
    'https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
    untar=True)
flowers_root = pathlib.Path(flowers_root)

Le répertoire racine contient un répertoire pour chaque classe :

for item in flowers_root.glob("*"):
  print(item.name)
sunflowers
daisy
LICENSE.txt
roses
tulips
dandelion

Les fichiers de chaque répertoire de classe sont des exemples :

list_ds = tf.data.Dataset.list_files(str(flowers_root/'*/*'))

for f in list_ds.take(5):
  print(f.numpy())
b'/home/kbuilder/.keras/datasets/flower_photos/roses/3871586333_5a708d5cf4_n.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/dandelion/19551343954_83bb52f310_m.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/tulips/13289268363_b9337d751e.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/roses/3253243865_435c1f2c2b_m.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/daisy/22244161124_53e457bb66_n.jpg'

Lisez les données à l'aide de la fonctiontf.io.read_file et extrayez l'étiquette du chemin, en renvoyant des paires (image, label) :

def process_path(file_path):
  label = tf.strings.split(file_path, os.sep)[-2]
  return tf.io.read_file(file_path), label

labeled_ds = list_ds.map(process_path)
for image_raw, label_text in labeled_ds.take(1):
  print(repr(image_raw.numpy()[:100]))
  print()
  print(label_text.numpy())
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xfe\x00\x0cAppleMark\n\xff\xe2\x05XICC_PROFILE\x00\x01\x01\x00\x00\x05Happl\x02 \x00\x00scnrRGB XYZ \x07\xd3\x00\x07\x00\x01\x00\x00\x00\x00\x00\x00acspAPPL\x00\x00\x00\x00'

b'roses'

Regroupement des éléments du jeu de données

Dosage simple

La forme la plus simple de traitement par lots empile n éléments consécutifs d'un ensemble de données en un seul élément. C'est exactement ce que fait la transformation Dataset.batch() , avec les mêmes contraintes que l'opérateur tf.stack() , appliquées à chaque composante des éléments : c'est-à-dire que pour chaque composante i , tous les éléments doivent avoir un tenseur exactement de la même forme.

inc_dataset = tf.data.Dataset.range(100)
dec_dataset = tf.data.Dataset.range(0, -100, -1)
dataset = tf.data.Dataset.zip((inc_dataset, dec_dataset))
batched_dataset = dataset.batch(4)

for batch in batched_dataset.take(4):
  print([arr.numpy() for arr in batch])
[array([0, 1, 2, 3]), array([ 0, -1, -2, -3])]
[array([4, 5, 6, 7]), array([-4, -5, -6, -7])]
[array([ 8,  9, 10, 11]), array([ -8,  -9, -10, -11])]
[array([12, 13, 14, 15]), array([-12, -13, -14, -15])]

Alors que tf.data essaie de propager les informations de forme, les paramètres par défaut de Dataset.batch entraînent une taille de lot inconnue car le dernier lot peut ne pas être plein. Notez les None dans la forme :

batched_dataset
<BatchDataset shapes: ((None,), (None,)), types: (tf.int64, tf.int64)>

Utilisez l'argument drop_remainder pour ignorer ce dernier lot et obtenir une propagation de forme complète :

batched_dataset = dataset.batch(7, drop_remainder=True)
batched_dataset
<BatchDataset shapes: ((7,), (7,)), types: (tf.int64, tf.int64)>

Tenseurs de dosage avec rembourrage

La recette ci-dessus fonctionne pour les tenseurs qui ont tous la même taille. Cependant, de nombreux modèles (par exemple les modèles de séquence) fonctionnent avec des données d'entrée qui peuvent avoir des tailles variables (par exemple des séquences de différentes longueurs). Pour gérer ce cas, la transformation Dataset.padded_batch vous permet de Dataset.padded_batch des tenseurs de forme différente en spécifiant une ou plusieurs dimensions dans lesquelles ils peuvent être remplis.

dataset = tf.data.Dataset.range(100)
dataset = dataset.map(lambda x: tf.fill([tf.cast(x, tf.int32)], x))
dataset = dataset.padded_batch(4, padded_shapes=(None,))

for batch in dataset.take(2):
  print(batch.numpy())
  print()
[[0 0 0]
 [1 0 0]
 [2 2 0]
 [3 3 3]]

[[4 4 4 4 0 0 0]
 [5 5 5 5 5 0 0]
 [6 6 6 6 6 6 0]
 [7 7 7 7 7 7 7]]

La transformation Dataset.padded_batch vous permet de définir un remplissage différent pour chaque dimension de chaque composant, et il peut être de longueur variable (signifié par None dans l'exemple ci-dessus) ou de longueur constante. Il est également possible de remplacer la valeur de remplissage, qui par défaut est 0.

Workflows de formation

Traitement de plusieurs époques

L'API tf.data offre deux manières principales de traiter plusieurs époques des mêmes données.

Le moyen le plus simple d'itérer sur un ensemble de données à plusieurs époques consiste à utiliser la transformation Dataset.repeat() . Tout d'abord, créez un ensemble de données titanesques :

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
titanic_lines = tf.data.TextLineDataset(titanic_file)
def plot_batch_sizes(ds):
  batch_sizes = [batch.shape[0] for batch in ds]
  plt.bar(range(len(batch_sizes)), batch_sizes)
  plt.xlabel('Batch number')
  plt.ylabel('Batch size')

L'application de la transformation Dataset.repeat() sans argument répétera l'entrée indéfiniment.

La transformation Dataset.repeat concatène ses arguments sans signaler la fin d'une époque et le début de l'époque suivante. Pour cette raison, un Dataset.batch appliqué après Dataset.repeat produira des lots qui chevauchent les limites de l'époque :

titanic_batches = titanic_lines.repeat(3).batch(128)
plot_batch_sizes(titanic_batches)

png

Si vous avez besoin d'une séparation claire des époques, placez Dataset.batch avant la répétition :

titanic_batches = titanic_lines.batch(128).repeat(3)

plot_batch_sizes(titanic_batches)

png

Si vous souhaitez effectuer un calcul personnalisé (par exemple pour collecter des statistiques) à la fin de chaque époque, il est plus simple de redémarrer l'itération de l'ensemble de données à chaque époque :

epochs = 3
dataset = titanic_lines.batch(128)

for epoch in range(epochs):
  for batch in dataset:
    print(batch.shape)
  print("End of epoch: ", epoch)
(128,)
(128,)
(128,)
(128,)
(116,)
End of epoch:  0
(128,)
(128,)
(128,)
(128,)
(116,)
End of epoch:  1
(128,)
(128,)
(128,)
(128,)
(116,)
End of epoch:  2

Mélanger aléatoirement les données d'entrée

La transformation Dataset.shuffle() maintient un tampon de taille fixe et choisit l'élément suivant uniformément au hasard dans ce tampon.

Ajoutez un index à l'ensemble de données pour voir l'effet :

lines = tf.data.TextLineDataset(titanic_file)
counter = tf.data.experimental.Counter()

dataset = tf.data.Dataset.zip((counter, lines))
dataset = dataset.shuffle(buffer_size=100)
dataset = dataset.batch(20)
dataset
<BatchDataset shapes: ((None,), (None,)), types: (tf.int64, tf.string)>

Étant donné que buffer_size est de 100 et que la taille du lot est de 20, le premier lot ne contient aucun élément avec un index supérieur à 120.

n,line_batch = next(iter(dataset))
print(n.numpy())
[ 39   5  30  77  45 104 102  54  31  89  93  29  38  85  60   9  78  76
   1  41]

Comme avec Dataset.batch l'ordre relatif à Dataset.repeat important.

Dataset.shuffle ne Dataset.shuffle pas la fin d'une époque tant que le tampon de Dataset.shuffle n'est pas vide. Ainsi, un shuffle placé avant une répétition affichera chaque élément d'une époque avant de passer à la suivante :

dataset = tf.data.Dataset.zip((counter, lines))
shuffled = dataset.shuffle(buffer_size=100).batch(10).repeat(2)

print("Here are the item ID's near the epoch boundary:\n")
for n, line_batch in shuffled.skip(60).take(5):
  print(n.numpy())
Here are the item ID's near the epoch boundary:

[618 522 447 528 576 514 626 610 502 404]
[601 577 586 560 490 469 604 275 551 561]
[567 550 423 486 544 457 578 448]
[85 78 32 73 45 99 37 94 13 54]
[101  55   4 109  41  25  80 106   9 113]
shuffle_repeat = [n.numpy().mean() for n, line_batch in shuffled]
plt.plot(shuffle_repeat, label="shuffle().repeat()")
plt.ylabel("Mean item ID")
plt.legend()
<matplotlib.legend.Legend at 0x7fe00c013690>

png

Mais une répétition avant un shuffle mélange les limites de l'époque :

dataset = tf.data.Dataset.zip((counter, lines))
shuffled = dataset.repeat(2).shuffle(buffer_size=100).batch(10)

print("Here are the item ID's near the epoch boundary:\n")
for n, line_batch in shuffled.skip(55).take(15):
  print(n.numpy())
Here are the item ID's near the epoch boundary:

[611 401 577 625   2 557 585   6 618 502]
[ 31 607 588  24   0 627 455 613 547 605]
[537 544 240  15  43 555 621  34 590   8]
[ 27 475 528 546 560  48  53 489  54  37]
[599 561  30 570  21 499 586  10   5  12]
[367  60 568 525   1 619 589  23 548  35]
[ 17 470 616  42 569  83  70 405  46 463]
[ 50  72 612 623  28 522 581  86  77  76]
[474 598 609  25  65 491 543  97 536  93]
[ 16 101  58  90  19  38 111 615 119  49]
[ 39 110  75  95 122  94   4  67  64  51]
[ 22 606 610  99 601 526 116 571  80 109]
[ 29  32 125 138 608  33 139 106 147 127]
[130 114 117  18  59  79  66 123 124 155]
[103 579 100 107 165 115  92  84   3  52]
repeat_shuffle = [n.numpy().mean() for n, line_batch in shuffled]

plt.plot(shuffle_repeat, label="shuffle().repeat()")
plt.plot(repeat_shuffle, label="repeat().shuffle()")
plt.ylabel("Mean item ID")
plt.legend()
<matplotlib.legend.Legend at 0x7fe004095b50>

png

Prétraitement des données

La Dataset.map(f) produit un nouvel ensemble de données en appliquant une fonction donnée f à chaque élément de l'ensemble de données d'entrée. Il est basé sur la fonction map() qui est couramment appliquée aux listes (et autres structures) dans les langages de programmation fonctionnels. La fonction f prend les objets tf.Tensor qui représentent un seul élément dans l'entrée et renvoie les objets tf.Tensor qui représenteront un seul élément dans le nouvel ensemble de données. Sa mise en œuvre utilise des opérations TensorFlow standard pour transformer un élément en un autre.

Cette section couvre des exemples courants d'utilisation de Dataset.map() .

Décodage des données d'image et redimensionnement

Lors de l'apprentissage d'un réseau de neurones sur des données d'images du monde réel, il est souvent nécessaire de convertir des images de différentes tailles en une taille commune, afin qu'elles puissent être regroupées en une taille fixe.

Reconstruisez l'ensemble de données des noms de fichiers Flower :

list_ds = tf.data.Dataset.list_files(str(flowers_root/'*/*'))

Écrivez une fonction qui manipule les éléments de l'ensemble de données.

# Reads an image from a file, decodes it into a dense tensor, and resizes it
# to a fixed shape.
def parse_image(filename):
  parts = tf.strings.split(filename, os.sep)
  label = parts[-2]

  image = tf.io.read_file(filename)
  image = tf.image.decode_jpeg(image)
  image = tf.image.convert_image_dtype(image, tf.float32)
  image = tf.image.resize(image, [128, 128])
  return image, label

Testez que cela fonctionne.

file_path = next(iter(list_ds))
image, label = parse_image(file_path)

def show(image, label):
  plt.figure()
  plt.imshow(image)
  plt.title(label.numpy().decode('utf-8'))
  plt.axis('off')

show(image, label)

png

Mappez-le sur l'ensemble de données.

images_ds = list_ds.map(parse_image)

for image, label in images_ds.take(2):
  show(image, label)

png

png

Appliquer une logique Python arbitraire

Pour des raisons de performances, utilisez les opérations TensorFlow pour prétraiter vos données dans la mesure du possible. Cependant, il est parfois utile d'appeler des bibliothèques Python externes lors de l'analyse de vos données d'entrée. Vous pouvez utiliser l'opération tf.py_function() dans une transformation Dataset.map() .

Par exemple, si vous souhaitez appliquer une rotation aléatoire, le tf.image module a seulement tf.image.rot90 , ce qui est très utile pour l' augmentation de l' image.

Pour illustrer tf.py_function , essayez plutôt d'utiliser la fonction scipy.ndimage.rotate :

import scipy.ndimage as ndimage

def random_rotate_image(image):
  image = ndimage.rotate(image, np.random.uniform(-30, 30), reshape=False)
  return image
image, label = next(iter(images_ds))
image = random_rotate_image(image)
show(image, label)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

png

Pour utiliser cette fonction avec Dataset.map les mêmes mises en garde s'appliquent qu'avec Dataset.from_generator , vous devez décrire les formes et les types de retour lorsque vous appliquez la fonction :

def tf_random_rotate_image(image, label):
  im_shape = image.shape
  [image,] = tf.py_function(random_rotate_image, [image], [tf.float32])
  image.set_shape(im_shape)
  return image, label
rot_ds = images_ds.map(tf_random_rotate_image)

for image, label in rot_ds.take(2):
  show(image, label)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).

png

png

Analyse des messages de tampon de protocole tf.Example

De nombreux pipelines d'entrée extraient les messages de tampon de protocole tf.train.Example partir d'un format TFRecord. Chaque enregistrement tf.train.Example contient une ou plusieurs « caractéristiques » et le pipeline d'entrée convertit généralement ces caractéristiques en tenseurs.

fsns_test_file = tf.keras.utils.get_file("fsns.tfrec", "https://storage.googleapis.com/download.tensorflow.org/data/fsns-20160927/testdata/fsns-00000-of-00001")
dataset = tf.data.TFRecordDataset(filenames = [fsns_test_file])
dataset
<TFRecordDatasetV2 shapes: (), types: tf.string>

Vous pouvez travailler avec des protos tf.train.Example dehors d'untf.data.Dataset pour comprendre les données :

raw_example = next(iter(dataset))
parsed = tf.train.Example.FromString(raw_example.numpy())

feature = parsed.features.feature
raw_img = feature['image/encoded'].bytes_list.value[0]
img = tf.image.decode_png(raw_img)
plt.imshow(img)
plt.axis('off')
_ = plt.title(feature["image/text"].bytes_list.value[0])

png

raw_example = next(iter(dataset))
def tf_parse(eg):
  example = tf.io.parse_example(
      eg[tf.newaxis], {
          'image/encoded': tf.io.FixedLenFeature(shape=(), dtype=tf.string),
          'image/text': tf.io.FixedLenFeature(shape=(), dtype=tf.string)
      })
  return example['image/encoded'][0], example['image/text'][0]
img, txt = tf_parse(raw_example)
print(txt.numpy())
print(repr(img.numpy()[:20]), "...")
b'Rue Perreyon'
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02X' ...
decoded = dataset.map(tf_parse)
decoded
<MapDataset shapes: ((), ()), types: (tf.string, tf.string)>
image_batch, text_batch = next(iter(decoded.batch(10)))
image_batch.shape
TensorShape([10])

Fenêtrage de séries temporelles

Pour un exemple de série chronologique de bout en bout, consultez : Prévisions de séries chronologiques .

Les données de séries chronologiques sont souvent organisées avec l'axe temporel intact.

Utilisez un simple Dataset.range pour démontrer :

range_ds = tf.data.Dataset.range(100000)

En règle générale, les modèles basés sur ce type de données voudront une tranche de temps contiguë.

L'approche la plus simple serait de regrouper les données :

Utilisation du batch

batches = range_ds.batch(10, drop_remainder=True)

for batch in batches.take(5):
  print(batch.numpy())
[0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]
[40 41 42 43 44 45 46 47 48 49]

Ou pour faire des prédictions denses un pas dans le futur, vous pouvez décaler les caractéristiques et les étiquettes d'un pas les unes par rapport aux autres :

def dense_1_step(batch):
  # Shift features and labels one step relative to each other.
  return batch[:-1], batch[1:]

predict_dense_1_step = batches.map(dense_1_step)

for features, label in predict_dense_1_step.take(3):
  print(features.numpy(), " => ", label.numpy())
[0 1 2 3 4 5 6 7 8]  =>  [1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18]  =>  [11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28]  =>  [21 22 23 24 25 26 27 28 29]

Pour prédire une fenêtre entière au lieu d'un décalage fixe, vous pouvez diviser les lots en deux parties :

batches = range_ds.batch(15, drop_remainder=True)

def label_next_5_steps(batch):
  return (batch[:-5],   # Inputs: All except the last 5 steps
          batch[-5:])   # Labels: The last 5 steps

predict_5_steps = batches.map(label_next_5_steps)

for features, label in predict_5_steps.take(3):
  print(features.numpy(), " => ", label.numpy())
[0 1 2 3 4 5 6 7 8 9]  =>  [10 11 12 13 14]
[15 16 17 18 19 20 21 22 23 24]  =>  [25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]  =>  [40 41 42 43 44]

Pour permettre un certain chevauchement entre les caractéristiques d'un lot et les étiquettes d'un autre, utilisez Dataset.zip :

feature_length = 10
label_length = 3

features = range_ds.batch(feature_length, drop_remainder=True)
labels = range_ds.batch(feature_length).skip(1).map(lambda labels: labels[:label_length])

predicted_steps = tf.data.Dataset.zip((features, labels))

for features, label in predicted_steps.take(5):
  print(features.numpy(), " => ", label.numpy())
[0 1 2 3 4 5 6 7 8 9]  =>  [10 11 12]
[10 11 12 13 14 15 16 17 18 19]  =>  [20 21 22]
[20 21 22 23 24 25 26 27 28 29]  =>  [30 31 32]
[30 31 32 33 34 35 36 37 38 39]  =>  [40 41 42]
[40 41 42 43 44 45 46 47 48 49]  =>  [50 51 52]

Utilisation de la window

Lors de l'utilisation de Dataset.batch , il existe des situations où vous pouvez avoir besoin d'un contrôle plus fin. La méthode Dataset.window vous donne un contrôle total, mais nécessite une certaine prudence : elle renvoie un Dataset de Datasets . Voir Structure du jeu de données pour plus de détails.

window_size = 5

windows = range_ds.window(window_size, shift=1)
for sub_ds in windows.take(5):
  print(sub_ds)
<_VariantDataset shapes: (), types: tf.int64>
<_VariantDataset shapes: (), types: tf.int64>
<_VariantDataset shapes: (), types: tf.int64>
<_VariantDataset shapes: (), types: tf.int64>
<_VariantDataset shapes: (), types: tf.int64>

La méthode Dataset.flat_map peut prendre un ensemble de données d'ensembles de données et l'aplatir en un seul ensemble de données :

for x in windows.flat_map(lambda x: x).take(30):
   print(x.numpy(), end=' ')
0 1 2 3 4 1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9

Dans presque tous les cas, vous voudrez d'abord .batch l'ensemble de données :

def sub_to_batch(sub):
  return sub.batch(window_size, drop_remainder=True)

for example in windows.flat_map(sub_to_batch).take(5):
  print(example.numpy())
[0 1 2 3 4]
[1 2 3 4 5]
[2 3 4 5 6]
[3 4 5 6 7]
[4 5 6 7 8]

Maintenant, vous pouvez voir que l'argument shift contrôle de combien chaque fenêtre se déplace.

En rassemblant tout cela, vous pourriez écrire cette fonction :

def make_window_dataset(ds, window_size=5, shift=1, stride=1):
  windows = ds.window(window_size, shift=shift, stride=stride)

  def sub_to_batch(sub):
    return sub.batch(window_size, drop_remainder=True)

  windows = windows.flat_map(sub_to_batch)
  return windows
ds = make_window_dataset(range_ds, window_size=10, shift = 5, stride=3)

for example in ds.take(10):
  print(example.numpy())
[ 0  3  6  9 12 15 18 21 24 27]
[ 5  8 11 14 17 20 23 26 29 32]
[10 13 16 19 22 25 28 31 34 37]
[15 18 21 24 27 30 33 36 39 42]
[20 23 26 29 32 35 38 41 44 47]
[25 28 31 34 37 40 43 46 49 52]
[30 33 36 39 42 45 48 51 54 57]
[35 38 41 44 47 50 53 56 59 62]
[40 43 46 49 52 55 58 61 64 67]
[45 48 51 54 57 60 63 66 69 72]

Ensuite, il est facile d'extraire les étiquettes, comme précédemment :

dense_labels_ds = ds.map(dense_1_step)

for inputs,labels in dense_labels_ds.take(3):
  print(inputs.numpy(), "=>", labels.numpy())
[ 0  3  6  9 12 15 18 21 24] => [ 3  6  9 12 15 18 21 24 27]
[ 5  8 11 14 17 20 23 26 29] => [ 8 11 14 17 20 23 26 29 32]
[10 13 16 19 22 25 28 31 34] => [13 16 19 22 25 28 31 34 37]

Rééchantillonnage

Lorsque vous travaillez avec un ensemble de données très déséquilibré en classe, vous souhaiterez peut-être rééchantillonner l'ensemble de données. tf.data fournit deux méthodes pour ce faire. L'ensemble de données sur la fraude par carte de crédit est un bon exemple de ce type de problème.

zip_path = tf.keras.utils.get_file(
    origin='https://storage.googleapis.com/download.tensorflow.org/data/creditcard.zip',
    fname='creditcard.zip',
    extract=True)

csv_path = zip_path.replace('.zip', '.csv')
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/creditcard.zip
69156864/69155632 [==============================] - 2s 0us/step
creditcard_ds = tf.data.experimental.make_csv_dataset(
    csv_path, batch_size=1024, label_name="Class",
    # Set the column types: 30 floats and an int.
    column_defaults=[float()]*30+[int()])

Maintenant, vérifiez la distribution des classes, elle est très asymétrique :

def count(counts, batch):
  features, labels = batch
  class_1 = labels == 1
  class_1 = tf.cast(class_1, tf.int32)

  class_0 = labels == 0
  class_0 = tf.cast(class_0, tf.int32)

  counts['class_0'] += tf.reduce_sum(class_0)
  counts['class_1'] += tf.reduce_sum(class_1)

  return counts
counts = creditcard_ds.take(10).reduce(
    initial_state={'class_0': 0, 'class_1': 0},
    reduce_func = count)

counts = np.array([counts['class_0'].numpy(),
                   counts['class_1'].numpy()]).astype(np.float32)

fractions = counts/counts.sum()
print(fractions)
[0.9961 0.0039]

Une approche courante de l'entraînement avec un ensemble de données déséquilibré consiste à l'équilibrer. tf.data inclut quelques méthodes qui permettent ce workflow :

Échantillonnage des jeux de données

Une approche pour rééchantillonner un ensemble de données consiste à utiliser sample_from_datasets . Ceci est plus applicable lorsque vous avez undata.Dataset séparé pour chaque classe.

Ici, utilisez simplement le filtre pour les générer à partir des données de fraude par carte de crédit :

negative_ds = (
  creditcard_ds
    .unbatch()
    .filter(lambda features, label: label==0)
    .repeat())
positive_ds = (
  creditcard_ds
    .unbatch()
    .filter(lambda features, label: label==1)
    .repeat())
for features, label in positive_ds.batch(10).take(1):
  print(label.numpy())
[1 1 1 1 1 1 1 1 1 1]

Pour utiliser tf.data.experimental.sample_from_datasets transmettez les ensembles de données et le poids pour chacun :

balanced_ds = tf.data.experimental.sample_from_datasets(
    [negative_ds, positive_ds], [0.5, 0.5]).batch(10)

Maintenant, l'ensemble de données produit des exemples de chaque classe avec une probabilité de 50/50 :

for features, labels in balanced_ds.take(10):
  print(labels.numpy())
[1 1 1 0 1 0 1 1 0 1]
[1 1 1 0 0 1 0 0 0 0]
[0 1 1 1 1 1 1 1 0 0]
[0 0 1 0 0 0 1 0 0 0]
[0 1 0 0 1 0 1 1 0 1]
[1 0 0 0 0 1 1 1 0 1]
[0 0 0 1 1 0 0 1 0 1]
[1 0 0 1 1 0 0 1 1 0]
[0 0 1 0 0 0 0 1 0 0]
[1 1 1 1 0 0 1 1 1 0]

Rejet du rééchantillonnage

Un problème avec l'approche experimental.sample_from_datasets ci-dessus est qu'elle a besoin d'untf.data.Dataset séparé par classe. L'utilisation de Dataset.filter fonctionne, mais entraîne le chargement double de toutes les données.

La fonction data.experimental.rejection_resample peut être appliquée à un ensemble de données pour le rééquilibrer, tout en ne le chargeant qu'une seule fois. Les éléments seront supprimés de l'ensemble de données pour atteindre l'équilibre.

data.experimental.rejection_resample prend un argument class_func . Ce class_func est appliqué à chaque élément de jeu de données et est utilisé pour déterminer à quelle classe appartient un exemple à des fins d'équilibrage.

Les éléments de creditcard_ds sont déjà des paires (features, label) . Ainsi, class_func juste besoin de renvoyer ces étiquettes :

def class_func(features, label):
  return label

Le rééchantillonneur a également besoin d'une distribution cible, et éventuellement d'une estimation de distribution initiale :

resampler = tf.data.experimental.rejection_resample(
    class_func, target_dist=[0.5, 0.5], initial_dist=fractions)

Le rééchantillonneur traite des exemples individuels, vous devez donc unbatch l'ensemble de données avant d'appliquer le rééchantillonneur :

resample_ds = creditcard_ds.unbatch().apply(resampler).batch(10)
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/data/experimental/ops/resampling.py:156: Print (from tensorflow.python.ops.logging_ops) is deprecated and will be removed after 2018-08-20.
Instructions for updating:
Use tf.print instead of tf.Print. Note that tf.print returns a no-output operator that directly prints the output. Outside of defuns or eager mode, this operator will not be executed unless it is directly specified in session.run or used as a control dependency for other operators. This is only a concern in graph mode. Below is an example of how to ensure tf.print executes in graph mode:

Le rééchantillonneur renvoie crée (class, example) paires à partir de la sortie de class_func . Dans ce cas, l' example était déjà une paire (feature, label) , utilisez donc map pour supprimer la copie supplémentaire des étiquettes :

balanced_ds = resample_ds.map(lambda extra_label, features_and_label: features_and_label)

Maintenant, l'ensemble de données produit des exemples de chaque classe avec une probabilité de 50/50 :

for features, labels in balanced_ds.take(10):
  print(labels.numpy())
[1 0 0 0 1 0 1 1 0 0]
[1 1 0 0 0 1 0 0 0 0]
[1 1 1 1 0 0 0 0 0 1]
[1 0 0 1 0 1 1 1 1 1]
[1 0 1 0 1 1 0 0 1 1]
[1 1 0 1 0 1 0 0 0 0]
[0 0 0 1 1 1 0 1 1 1]
[1 0 0 0 1 0 0 0 0 1]
[1 1 0 1 0 0 1 0 1 0]
[0 1 1 1 0 1 0 1 1 1]

Point de contrôle de l'itérateur

Tensorflow prend en charge la prise de points de contrôle afin que, lorsque votre processus d'entraînement redémarre, il puisse restaurer le dernier point de contrôle pour récupérer la majeure partie de sa progression. En plus de pointer les variables du modèle, vous pouvez également pointer la progression de l'itérateur de jeu de données. Cela peut être utile si vous disposez d'un ensemble de données volumineux et que vous ne souhaitez pas recommencer l'ensemble de données depuis le début à chaque redémarrage. Notez cependant que les points de contrôle de l'itérateur peuvent être volumineux, car les transformations telles que la prefetch shuffle et la prefetch nécessitent des éléments de mise en prefetch tampon dans l'itérateur.

Pour inclure votre itérateur dans un point de contrôle, transmettez l'itérateur au constructeur tf.train.Checkpoint .

range_ds = tf.data.Dataset.range(20)

iterator = iter(range_ds)
ckpt = tf.train.Checkpoint(step=tf.Variable(0), iterator=iterator)
manager = tf.train.CheckpointManager(ckpt, '/tmp/my_ckpt', max_to_keep=3)

print([next(iterator).numpy() for _ in range(5)])

save_path = manager.save()

print([next(iterator).numpy() for _ in range(5)])

ckpt.restore(manager.latest_checkpoint)

print([next(iterator).numpy() for _ in range(5)])
[0, 1, 2, 3, 4]
[5, 6, 7, 8, 9]
[5, 6, 7, 8, 9]

Utiliser tf.data avec tf.keras

L'API tf.keras simplifie de nombreux aspects de la création et de l'exécution de modèles d'apprentissage automatique. Ses .fit() et .evaluate() et .predict() prennent en charge les ensembles de données comme entrées. Voici un ensemble de données rapide et une configuration de modèle :

train, test = tf.keras.datasets.fashion_mnist.load_data()

images, labels = train
images = images/255.0
labels = labels.astype(np.int32)
fmnist_train_ds = tf.data.Dataset.from_tensor_slices((images, labels))
fmnist_train_ds = fmnist_train_ds.shuffle(5000).batch(32)

model = tf.keras.Sequential([
  tf.keras.layers.Flatten(),
  tf.keras.layers.Dense(10)
])

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

Passer un ensemble de données de paires ( Model.fit (feature, label) est tout ce qui est nécessaire pour Model.fit et Model.evaluate :

model.fit(fmnist_train_ds, epochs=2)
Epoch 1/2
1875/1875 [==============================] - 3s 1ms/step - loss: 0.5999 - accuracy: 0.7962
Epoch 2/2
1875/1875 [==============================] - 3s 1ms/step - loss: 0.4618 - accuracy: 0.8428
<tensorflow.python.keras.callbacks.History at 0x7fdffe98bd10>

Si vous passez un ensemble de données infini, par exemple en appelant Dataset.repeat() , il vous suffit de passer également l'argument steps_per_epoch :

model.fit(fmnist_train_ds.repeat(), epochs=2, steps_per_epoch=20)
Epoch 1/2
20/20 [==============================] - 0s 1ms/step - loss: 0.3764 - accuracy: 0.8656
Epoch 2/2
20/20 [==============================] - 0s 1ms/step - loss: 0.4980 - accuracy: 0.8406
<tensorflow.python.keras.callbacks.History at 0x7fe00c129050>

Pour l'évaluation, vous pouvez passer le nombre d'étapes d'évaluation :

loss, accuracy = model.evaluate(fmnist_train_ds)
print("Loss :", loss)
print("Accuracy :", accuracy)
1875/1875 [==============================] - 2s 951us/step - loss: 0.4384 - accuracy: 0.8501
Loss : 0.4384005665779114
Accuracy : 0.8500999808311462

Pour les ensembles de données longs, définissez le nombre d'étapes à évaluer :

loss, accuracy = model.evaluate(fmnist_train_ds.repeat(), steps=10)
print("Loss :", loss)
print("Accuracy :", accuracy)
10/10 [==============================] - 0s 1ms/step - loss: 0.4299 - accuracy: 0.8625
Loss : 0.4299231171607971
Accuracy : 0.862500011920929

Les étiquettes ne sont pas requises lors de l'appel de Model.predict .

predict_ds = tf.data.Dataset.from_tensor_slices(images).batch(32)
result = model.predict(predict_ds, steps = 10)
print(result.shape)
(320, 10)

Mais les étiquettes sont ignorées si vous transmettez un ensemble de données les contenant :

result = model.predict(fmnist_train_ds, steps = 10)
print(result.shape)
(320, 10)