Cette page a été traduite par l'API Cloud Translation.
Switch to English

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

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

L'API tf.data vous permet de créer des pipelines d'entrée complexes à partir d'éléments simples et réutilisables. Par exemple, le pipeline pour un modèle d'image peut regrouper 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 identificateurs d'incorporation avec une table de consultation 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 abstraction tf.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 tensoriels représentant l'image et son étiquette.

Il existe deux façons distinctes de créer un ensemble de données:

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

  • Une transformation de données construit un ensemble de données à partir d'un ou plusieurs objets tf.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() . Sinon, 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 chaînant les appels de méthode sur l'objet tf.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 de tf.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

Les éléments de l'ensemble de données peuvent également être consommés à l'aide de la transformation 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 de l'ensemble de données

Un ensemble de données contient des éléments qui ont chacun la même structure (imbriquée) et 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 , ou tf.data.Dataset .

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 exemple:

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 transformations Dataset prennent en charge les jeux de données de n'importe quelle 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())
[1 2 5 3 8 9 6 7 9 3]
[5 4 3 3 9 7 3 1 5 4]
[7 3 4 8 4 8 5 9 4 1]
[8 6 8 2 6 3 5 8 3 3]

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

Consommation de tableaux NumPy

Voir Chargement de 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 Dataset partir d'eux 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 [==============================] - 1s 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 que tf.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 un tf.data.Dataset entièrement fonctionnel.

Le constructeur prend un appelable comme 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é comme arguments de l'appelable.

L'argument output_types est requis car tf.data construit un tf.Graph interne et les bords 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 la zone 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 de jeu de données.

Voici un exemple de générateur qui démontre les deux aspects, il retourne 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 : [1.2226]
1 : [ 0.4785  1.1887 -0.2828  0.6047  1.3367 -0.4387  0.1822]
2 : [ 1.1343 -0.2676  0.0224 -0.111  -0.1384 -1.9315]
3 : [-0.8651]
4 : [0.6275]
5 : [ 0.8034  2.0773  0.6183 -0.3746]
6 : [-0.9439 -0.6686]

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 un tf.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())
[18  1  8 22  5 13  6  2 14 28]

[[ 0.0196 -1.007   0.1843  0.0289  0.0735 -0.6279 -1.0877  0.    ]
 [ 0.8238 -1.0284  0.      0.      0.      0.      0.      0.    ]
 [ 0.544  -1.1061  1.2368  0.3975  0.      0.      0.      0.    ]
 [-1.1933 -0.7535  1.0497  0.1764  1.5319  0.9202  0.4027  0.6844]
 [-1.2025 -2.7148  1.0702  1.3893  0.      0.      0.      0.    ]
 [ 1.3787  0.6817  0.1197 -0.1178  0.9764  0.8895  0.      0.    ]
 [-1.523  -0.7722 -2.13    0.2761 -1.1094  0.      0.      0.    ]
 [ 0.203   2.1858 -0.722   1.2554  1.2208  0.1813 -2.3427  0.    ]
 [ 0.7685  1.7138 -1.2376 -2.6168  0.2565  0.0753  1.5653  0.    ]
 [ 0.0908 -0.4535  0.1257 -0.5122  0.      0.      0.      0.    ]]

Pour un exemple plus réaliste, essayez d' emballage preprocessing.image.ImageDataGenerator comme tf.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 [==============================] - 9s 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 divers formats de fichiers afin que vous puissiez traiter de grands ensembles de données qui ne tiennent pas dans la 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'apprentissage. 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 de test de la French Street Name Signs (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 avez deux ensembles de fichiers à des fins de formation et de validation, vous pouvez créer une méthode d'usine 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"
}

Consommer des 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 la lecture aléatoire des fichiers. 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'

Consommer des données CSV

Voir Chargement de fichiers CSV et Chargement de Pandas DataFrames pour plus d'exemples.

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

Par exemple:

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
df = pd.read_csv(titanic_file, index_col=None)
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 des enregistrements d'un ou plusieurs fichiers CSV conformes à la RFC 4180 .

La fonction experimental.make_csv_dataset est l'interface de haut niveau pour la lecture d'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': [0 0 0 0]
features:
  'sex'               : [b'male' b'male' b'male' b'male']
  'age'               : [18. 19. 31. 34.]
  'n_siblings_spouses': [0 0 0 1]
  'parch'             : [0 0 0 0]
  'fare'              : [ 8.3    8.05   7.775 26.   ]
  'class'             : [b'Third' b'Third' b'Third' b'Second']
  'deck'              : [b'unknown' b'unknown' b'unknown' b'unknown']
  'embark_town'       : [b'Southampton' b'Southampton' b'Southampton' b'Southampton']
  'alone'             : [b'y' b'y' b'y' b'n']

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': [1 0 0 1]
  'fare'              : [ 20.25     9.     110.8833  59.4   ]
  'class'             : [b'Third' b'Third' b'First' 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 de 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 respectivement avec les arguments header et select_cols .

# 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 la forme d'un 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/sunflowers/4933229095_f7e4218b28.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/dandelion/18282528206_7fb3166041.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/daisy/3711723108_65247a3170.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/sunflowers/4019748730_ee09b39a43.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/roses/475936554_a2b38aaa8e.jpg'

Lisez les données à l'aide de la fonction tf.io.read_file et extrayez le libellé 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\xe2\x0cXICC_PROFILE\x00\x01\x01\x00\x00\x0cHLino\x02\x10\x00\x00mntrRGB XYZ \x07\xce\x00\x02\x00\t\x00\x06\x001\x00\x00acspMSFT\x00\x00\x00\x00IEC sRGB\x00\x00\x00\x00\x00\x00'

b'roses'

Éléments de jeu de données par lots

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. La transformation Dataset.batch() fait exactement cela, avec les mêmes contraintes que l'opérateur tf.stack() , appliqué à chaque composant des éléments: c'est-à-dire que pour chaque composant 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 par lots 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 vaut par défaut 0.

Workflows de formation

Traitement de plusieurs époques

L'API tf.data offre deux méthodes principales pour 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 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 d'époque:

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

png

Si vous avez besoin d'une séparation d'époque claire, 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élange aléatoire des données d'entrée

La transformation Dataset.shuffle() gère un tampon de taille fixe et choisit l'élément suivant de manière uniforme 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 de 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())
[ 29  39  94  10  81  85  74   3  91  53  87  17   1  64  54 107  20  63
 116  62]

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 mélange placé avant une répétition affichera tous les éléments 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:

[469 560 614 431 557 558 618 616 571 532]
[464 403 509 589 365 610 596 428 600 605]
[539 467 163 599 624 545 552 548]
[92 30 73 51 72 71 56 83 32 46]
[ 74 106  75  78  26  21   3  54  80  50]

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 0x7f79e43368d0>

png

Mais une répétition avant une lecture aléatoire mélange les frontières d'époque ensemble:

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:

[618 517  22  13 602 608  25 621  12 579]
[568 620 534 547 319   1  17 346  29 581]
[537 395 560  33 617 557 561 610 585 627]
[428 607  32   0 455 496 605 624   6 596]
[511 588  14 572   4  21  66 471  41  36]
[583 622  49  54 616  75  65  57  76   7]
[ 77  37 574 586  74  56  28 419  73   5]
[599  83  31  61  91 481 577  98  26  50]
[  2 625  85  23 606 102 550  20 612  15]
[ 24  63 592 615 388  39  45  51  68  97]
[ 46  34  67  55  79  94 548  47  72 436]
[127  89  30  90 126 601 360  87 104  52]
[ 59 130 106 584 118 265  48  70 597 121]
[626 105 135  10  38  53 132 136 101   9]
[512 542 109 154 108  42 114  11  58 166]

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 0x7f79e42b60b8>

png

Prétraitement des données

La Dataset.map(f) produit un nouvel ensemble de données en appliquant une fonction f donnée à 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 fonctionnelle. La fonction f prend les objets tf.Tensor qui représentent un élément unique dans l'entrée et renvoie les objets tf.Tensor qui représenteront un élément unique dans le nouvel ensemble de données. Son implémentation 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 la formation d'un réseau de neurones sur des données d'image 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 le jeu de données des noms de fichiers de fleurs:

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

Vérifiez 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

Application d'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 d'utiliser la fonction scipy.ndimage.rotate place:

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 que pour 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 d'un format TFRecord. Chaque enregistrement tf.train.Example contient une ou plusieurs «entités» et le pipeline d'entrée convertit généralement ces entités 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 tf.train.Example protos en dehors d'un tf.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êtres de séries chronologiques

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

Les données de séries chronologiques sont souvent organisées avec l'axe des temps 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 une étape dans le futur, vous pouvez déplacer les caractéristiques et les étiquettes d'une étape 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],   # Take the first 5 steps
          batch[-5:])   # take the remainder

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 fonctionnalités d'un lot et les étiquettes d'un autre, utilisez Dataset.zip :

feature_length = 10
label_length = 5

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

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

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]
[10 11 12 13 14 15 16 17 18 19]  =>  [20 21 22 23 24]
[20 21 22 23 24 25 26 27 28 29]  =>  [30 31 32 33 34]

Utilisation de la window

Dataset.batch utilisation de Dataset.batch fonctionne, il existe des situations dans lesquelles vous devrez peut-être un contrôle plus précis. La Dataset.window méthode vous donne un contrôle complet, mais nécessite des soins: elle retourne 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=' ')
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f79e44309d8> and will run it as-is.
Cause: could not parse the source code:

for x in windows.flat_map(lambda x: x).take(30):

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING: AutoGraph could not transform <function <lambda> at 0x7f79e44309d8> and will run it as-is.
Cause: could not parse the source code:

for x in windows.flat_map(lambda x: x).take(30):

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
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 souhaiterez 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 le shift de chaque fenêtre.

En mettant cela ensemble, 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 auparavant:

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 qui est très déséquilibré de classe, vous pouvez vouloir 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 [==============================] - 3s 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 fortement biaisée:

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.9957 0.0043]

Une approche courante de la formation avec un jeu de données déséquilibré consiste à l'équilibrer. tf.data inclut quelques méthodes qui activent ce flux de travail:

Échantillonnage d'ensembles 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 un data.Dataset séparé pour chaque classe.

Ici, utilisez simplement un 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())
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f79e43ce510> and will run it as-is.
Cause: could not parse the source code:

    .filter(lambda features, label: label==0)

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING: AutoGraph could not transform <function <lambda> at 0x7f79e43ce510> and will run it as-is.
Cause: could not parse the source code:

    .filter(lambda features, label: label==0)

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f79e43ceae8> and will run it as-is.
Cause: could not parse the source code:

    .filter(lambda features, label: label==1)

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING: AutoGraph could not transform <function <lambda> at 0x7f79e43ceae8> and will run it as-is.
Cause: could not parse the source code:

    .filter(lambda features, label: label==1)

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert

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 passez les ensembles de données et le poids de 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())
[0 1 1 1 1 0 0 1 0 1]
[0 0 0 0 1 1 1 0 0 1]
[1 0 1 0 1 1 0 0 1 0]
[0 0 0 0 1 0 1 1 0 0]
[0 1 1 1 0 1 1 1 1 1]
[0 1 0 1 1 0 1 1 0 1]
[0 0 1 1 1 0 1 1 1 1]
[1 1 0 0 1 1 0 1 1 0]
[0 1 0 0 1 0 0 0 0 0]
[0 1 1 1 0 1 0 1 1 0]

Rééchantillonnage de rejet

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

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 l'ensemble de données et est utilisé pour déterminer à quelle classe un exemple appartient à des fins d'équilibrage.

Les éléments de creditcard_ds sont déjà des paires (features, label) . Donc, le 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.6/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 1 1 0 0 0 0 0 0]
[1 0 1 0 1 1 0 1 0 1]
[0 1 1 0 0 1 0 0 0 0]
[1 0 1 0 0 1 0 1 0 0]
[1 1 1 1 0 0 0 0 0 1]
[1 0 1 1 1 0 1 0 1 1]
[0 0 1 1 1 1 0 0 1 1]
[0 0 1 0 0 1 1 1 0 1]
[1 0 0 0 1 0 0 0 0 0]
[0 0 0 0 1 0 0 0 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 plupart de sa progression. En plus de vérifier les variables du modèle, vous pouvez également vérifier la progression de l'itérateur de l'ensemble de données. Cela peut être utile si vous disposez d'un ensemble de données volumineux et que vous ne souhaitez pas démarrer 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 le 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]

Utilisation de 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 et une configuration de modèle rapides:

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'])

Il Model.fit de Model.fit un jeu de données de paires (d' Model.fit (feature, label) pour Model.fit et Model.evaluate :

model.fit(fmnist_train_ds, epochs=2)
Epoch 1/2
WARNING:tensorflow:Layer flatten is casting an input tensor from dtype float64 to the layer's dtype of float32, which is new behavior in TensorFlow 2.  The layer has dtype float32 because its dtype defaults to floatx.

If you intended to run this layer in float32, you can safely ignore this warning. If in doubt, this warning is likely only an issue if you are porting a TensorFlow 1.X model to TensorFlow 2.

To change all layers to have dtype float64 by default, call `tf.keras.backend.set_floatx('float64')`. To change just this layer, pass dtype='float64' to the layer constructor. If you are the author of this layer, you can disable autocasting by passing autocast=False to the base Layer constructor.

1875/1875 [==============================] - 3s 2ms/step - loss: 0.5973 - accuracy: 0.7969
Epoch 2/2
1875/1875 [==============================] - 3s 2ms/step - loss: 0.4629 - accuracy: 0.8407

<tensorflow.python.keras.callbacks.History at 0x7f79e43d8710>

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 2ms/step - loss: 0.4800 - accuracy: 0.8594
Epoch 2/2
20/20 [==============================] - 0s 2ms/step - loss: 0.4529 - accuracy: 0.8391

<tensorflow.python.keras.callbacks.History at 0x7f79e43d84a8>

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 [==============================] - 3s 2ms/step - loss: 0.4386 - accuracy: 0.8508
Loss : 0.4385797381401062
Accuracy : 0.8507500290870667

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 2ms/step - loss: 0.3737 - accuracy: 0.8656
Loss : 0.37370163202285767
Accuracy : 0.8656250238418579

Les étiquettes ne sont pas obligatoires 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 passez un ensemble de données les contenant:

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