Esta página foi traduzida pela API Cloud Translation.
Switch to English

tf.data: construir pipelines de entrada do TensorFlow

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

A API tf.data permite que você crie pipelines de entrada complexos a partir de peças simples e reutilizáveis. Por exemplo, o pipeline para um modelo de imagem pode agregar dados de arquivos em um sistema de arquivos distribuído, aplicar perturbações aleatórias a cada imagem e mesclar imagens selecionadas aleatoriamente em um lote para treinamento. O pipeline para um modelo de texto pode envolver a extração de símbolos de dados de texto brutos, convertendo-os em identificadores incorporados com uma tabela de pesquisa e agrupando sequências de comprimentos diferentes. A API tf.data torna possível manipular grandes quantidades de dados, ler de diferentes formatos de dados e realizar transformações complexas.

A API tf.data apresenta uma abstração tf.data.Dataset que representa uma sequência de elementos, em que cada elemento consiste em um ou mais componentes. Por exemplo, em um pipeline de imagem, um elemento pode ser um único exemplo de treinamento, com um par de componentes tensores representando a imagem e seu rótulo.

Existem duas maneiras distintas de criar um conjunto de dados:

  • Uma fonte de dados constrói um Dataset de dados a partir de dados armazenados na memória ou em um ou mais arquivos.

  • Uma transformação de dados constrói um conjunto de dados a partir de um ou mais objetos 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)

Mecânica básica

Para criar um pipeline de entrada, você deve começar com uma fonte de dados. Por exemplo, para construir um Dataset de dados a partir de dados na memória, você pode usar tf.data.Dataset.from_tensors() ou tf.data.Dataset.from_tensor_slices() . Como alternativa, se seus dados de entrada estiverem armazenados em um arquivo no formato TFRecord recomendado, você pode usar tf.data.TFRecordDataset() .

Depois de ter um objeto Dataset , você pode transformá- lo em um novo Dataset encadeando chamadas de método no objeto tf.data.Dataset . Por exemplo, você pode aplicar transformações por elemento, como Dataset.map() , e transformações de vários elementos, como Dataset.batch() . Consulte a documentação de tf.data.Dataset para uma lista completa de transformações.

O objeto Dataset é iterável em Python. Isso torna possível consumir seus elementos usando um loop 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 criando explicitamente um iterador Python usando iter e consumindo seus elementos usando next :

it = iter(dataset)

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

Como alternativa, os elementos do conjunto de dados podem ser consumidos usando a transformação de reduce , que reduz todos os elementos para produzir um único resultado. O exemplo a seguir ilustra como usar a transformação de reduce para calcular a soma de um conjunto de dados de inteiros.

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

Estrutura do conjunto de dados

Um conjunto de dados contém elementos em que cada um tem a mesma estrutura (aninhada) e os componentes individuais da estrutura podem ser de qualquer tipo representável por tf.TypeSpec , incluindo tf.Tensor , tf.sparse.SparseTensor , tf.RaggedTensor , tf.TensorArray , ou tf.data.Dataset .

A propriedade Dataset.element_spec permite que você inspecione o tipo de cada componente do elemento. A propriedade retorna uma estrutura aninhada de objetos tf.TypeSpec , correspondendo à estrutura do elemento, que pode ser um único componente, uma tupla de componentes ou uma tupla aninhada de componentes. Por exemplo:

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

Os Dataset transformações suportar conjuntos de dados de qualquer estrutura. Ao usar as Dataset.map() e Dataset.filter() , que aplicam uma função a cada elemento, a estrutura do elemento determina os argumentos da função:

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())
[6 4 2 6 4 7 8 5 2 3]
[3 6 2 3 8 6 1 2 3 4]
[8 4 4 2 7 3 1 7 7 8]
[9 9 3 7 9 1 9 1 1 7]

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

Lendo dados de entrada

Consumindo matrizes NumPy

Veja Carregando matrizes NumPy para mais exemplos.

Se todos os seus dados de entrada couberem na memória, a maneira mais simples de criar um Dataset partir deles é convertê-los em objetos tf.Tensor e usar 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)>

Consumindo geradores Python

Outra fonte de dados comum que pode ser facilmente ingerida como um tf.data.Dataset é o gerador 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

O construtor Dataset.from_generator converte o gerador Python em um tf.data.Dataset totalmente funcional.

O construtor recebe um chamável como entrada, não um iterador. Isso permite que ele reinicie o gerador quando chegar ao fim. args argumento args opcional, que é passado como os argumentos do chamável.

O argumento output_types é necessário porque tf.data cria um tf.Graph internamente e as bordas do gráfico exigem um 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]

O argumento output_shapes não é necessário, mas é altamente recomendado, pois muitas operações de tensorflow não suportam tensores com classificação desconhecida. Se o comprimento de um eixo específico for desconhecido ou variável, defina-o como None em output_shapes .

Também é importante observar que output_shapes e output_types seguem as mesmas regras de aninhamento de outros métodos de conjunto de dados.

Aqui está um gerador de exemplo que demonstra ambos os aspectos, ele retorna tuplas de matrizes, onde a segunda matriz é um vetor com comprimento desconhecido.

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.119  -0.1705  0.6298  1.6472]
1 : [0.4102 0.858 ]
2 : [ 1.1845 -0.4004 -0.5682  0.7648 -2.0806  1.2242 -2.0966  0.8446]
3 : [-2.2548  1.4521  0.8092]
4 : [ 0.8081  0.7604 -0.6913  0.5043  1.0684]
5 : [-0.1358  0.3046  0.0346]
6 : []

A primeira saída é um int32 a segunda é um float32 .

O primeiro item é um escalar, forma () , e o segundo é um vetor de comprimento e forma desconhecidos (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)>

Agora ele pode ser usado como um tf.data.Dataset normal. Observe que, ao agrupar um conjunto de dados com uma forma variável, você precisa usar 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())
[16 19 15  2  0 23 12  6 27 20]

[[ 1.7982  0.6075  0.8935  1.5496 -0.9427  0.9026 -1.0912  0.      0.    ]
 [ 0.      0.      0.      0.      0.      0.      0.      0.      0.    ]
 [-0.3835 -0.7199  0.6697 -0.5535 -1.2751  1.0727 -1.6975 -1.8727  0.6132]
 [-0.7542  2.4467  1.6744  0.6795  0.3545  0.4416  0.      0.      0.    ]
 [-1.1485  0.2186 -0.5212 -0.6324 -1.1361 -2.0172  0.      0.      0.    ]
 [ 0.3903  0.0763  0.1203 -1.5849  0.      0.      0.      0.      0.    ]
 [-0.361   0.      0.      0.      0.      0.      0.      0.      0.    ]
 [-0.3172 -0.3045 -0.4404  1.0413 -2.2203 -0.9137  0.      0.      0.    ]
 [ 0.1888  1.432   1.4547  0.5166 -0.2852  2.3501 -0.3476 -0.1323 -0.0585]
 [ 0.0677  0.5017  1.5934  0.5202  0.      0.      0.      0.      0.    ]]

Para um exemplo mais realista, tente envolver preprocessing.image.ImageDataGenerator como um tf.data.Dataset .

Primeiro baixe os dados:

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 [==============================] - 3s 0us/step

Crie o 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(
    img_gen.flow_from_directory, args=[flowers], 
    output_types=(tf.float32, tf.float32), 
    output_shapes=([32,256,256,3], [32,5])
)

ds
<FlatMapDataset shapes: ((32, 256, 256, 3), (32, 5)), types: (tf.float32, tf.float32)>

Consumir dados TFRecord

Consulte Carregando TFRecords para ver um exemplo de ponta a ponta.

A API tf.data suporta uma variedade de formatos de arquivo para que você possa processar grandes conjuntos de dados que não cabem na memória. Por exemplo, o formato de arquivo TFRecord é um formato binário orientado a registro simples que muitos aplicativos TensorFlow usam para dados de treinamento. A classe tf.data.TFRecordDataset permite que você transmita o conteúdo de um ou mais arquivos TFRecord como parte de um pipeline de entrada.

Aqui está um exemplo usando o arquivo de teste da 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

O argumento de filenames para o inicializador TFRecordDataset pode ser uma string, uma lista de strings ou um tf.Tensor de strings. Portanto, se você tiver dois conjuntos de arquivos para fins de treinamento e validação, pode criar um método de fábrica que produz o conjunto de dados, tomando nomes de arquivos como um argumento de entrada:

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

Muitos projetos do TensorFlow usam registros tf.train.Example serializados em seus arquivos TFRecord. Eles precisam ser decodificados antes de serem inspecionados:

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

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

Consumindo dados de texto

Veja Carregando texto para um exemplo de ponta a ponta.

Muitos conjuntos de dados são distribuídos como um ou mais arquivos de texto. O tf.data.TextLineDataset fornece uma maneira fácil de extrair linhas de um ou mais arquivos de texto. Dado um ou mais nomes de arquivo, um TextLineDataset produzirá um elemento com valor de string por linha desses arquivos.

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)

Aqui estão as primeiras linhas do primeiro arquivo:

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

Para alternar linhas entre os arquivos, use Dataset.interleave . Isso torna mais fácil embaralhar os arquivos. Aqui estão a primeira, segunda e terceira linhas de cada tradução:

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'

Por padrão, um TextLineDataset produz todas as linhas de cada arquivo, o que pode não ser desejável, por exemplo, se o arquivo começar com uma linha de cabeçalho ou contiver comentários. Essas linhas podem ser removidas usando as Dataset.skip() ou Dataset.filter() . Aqui, você pula a primeira linha e, em seguida, filtra para encontrar apenas sobreviventes.

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'

Consumir dados CSV

Consulte Carregando arquivos CSV e Carregando DataFrames do Pandas para obter mais exemplos.

O formato de arquivo CSV é um formato popular para armazenar dados tabulares em texto simples.

Por exemplo:

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()

Se seus dados Dataset.from_tensor_slices na memória, o mesmo método Dataset.from_tensor_slices funciona em dicionários, permitindo que esses dados sejam facilmente importados:

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'

Uma abordagem mais escalonável é carregar do disco conforme necessário.

O módulo tf.data fornece métodos para extrair registros de um ou mais arquivos CSV que estão em conformidade com a RFC 4180 .

A função experimental.make_csv_dataset é a interface de alto nível para a leitura de conjuntos de arquivos csv. Ele suporta inferência de tipo de coluna e muitos outros recursos, como envio em lote e embaralhamento, para simplificar o uso.

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 1]
features:
  'sex'               : [b'male' b'male' b'female' b'male']
  'age'               : [30. 28. 28. 36.]
  'n_siblings_spouses': [0 0 0 0]
  'parch'             : [0 0 2 1]
  'fare'              : [ 13.       7.8958   7.75   512.3292]
  'class'             : [b'Second' b'Third' b'Third' b'First']
  'deck'              : [b'unknown' b'unknown' b'unknown' b'B']
  'embark_town'       : [b'Southampton' b'Southampton' b'Queenstown' b'Cherbourg']
  'alone'             : [b'y' b'y' b'n' b'n']

Você pode usar o argumento select_columns se precisar apenas de um subconjunto de colunas.

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 1 0 0]
  'fare'              : [ 7.925 32.5   13.     8.05 ]
  'class'             : [b'Third' b'Second' b'Second' b'Third']

Há também uma classe.CsvDataset experimental.CsvDataset nível inferior que fornece um controle mais refinado. Ele não suporta inferência de tipo de coluna. Em vez disso, você deve especificar o tipo de cada coluna.

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

Se algumas colunas estiverem vazias, esta interface de baixo nível permite que você forneça valores padrão em vez de tipos de coluna.

%%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]

Por padrão, um CsvDataset produz cada coluna de cada linha do arquivo, o que pode não ser desejável, por exemplo, se o arquivo começar com uma linha de cabeçalho que deve ser ignorada ou se algumas colunas não forem necessárias na entrada. Essas linhas e campos podem ser removidos com os argumentos header e select_cols respectivamente.

# 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]

Consumir conjuntos de arquivos

Existem muitos conjuntos de dados distribuídos como um conjunto de arquivos, onde cada arquivo é um exemplo.

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)

O diretório raiz contém um diretório para cada classe:

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

Os arquivos em cada diretório de classe são exemplos:

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/4746648726_e37a2de16d_n.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/daisy/2862944799_45bc8e7302.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/tulips/13513644515_a51470b899.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/daisy/506018088_4f7a15a7c5_n.jpg'
b'/home/kbuilder/.keras/datasets/flower_photos/dandelion/15381511376_fd743b7330_n.jpg'

Leia os dados usando a função tf.io.read_file e extraia o rótulo do caminho, retornando pares (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'sunflowers'

Elementos de conjunto de dados em lote

Lote simples

A forma mais simples de agrupamento empilha n elementos consecutivos de um conjunto de dados em um único elemento. A transformação Dataset.batch() faz exatamente isso, com as mesmas restrições que o operador tf.stack() , aplicado a cada componente dos elementos: ou seja, para cada componente i , todos os elementos devem ter um tensor exatamente da mesma forma.

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

Enquanto tf.data tenta propagar as informações de forma, as configurações padrão de Dataset.batch resultam em um tamanho de lote desconhecido porque o último lote pode não estar cheio. Observe os None na forma:

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

Use o argumento drop_remainder para ignorar o último lote e obter a propagação de forma completa:

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

Tensores de dosagem com preenchimento

A receita acima funciona para tensores que têm todos o mesmo tamanho. No entanto, muitos modelos (por exemplo, modelos de sequência) trabalham com dados de entrada que podem ter tamanhos variados (por exemplo, sequências de comprimentos diferentes). Para lidar com esse caso, a transformação Dataset.padded_batch permite que você Dataset.padded_batch tensores de formas diferentes especificando uma ou mais dimensões nas quais eles podem ser preenchidos.

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


A transformação Dataset.padded_batch permite definir diferentes preenchimentos para cada dimensão de cada componente, e pode ser de comprimento variável (indicado por None no exemplo acima) ou de comprimento constante. Também é possível substituir o valor de preenchimento, cujo padrão é 0.

Fluxos de trabalho de treinamento

Processando várias épocas

A API tf.data oferece duas maneiras principais de processar várias épocas dos mesmos dados.

A maneira mais simples de iterar em um conjunto de dados em várias épocas é usar a transformação Dataset.repeat() . Primeiro, crie um conjunto de dados titânicos:

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

Aplicar a transformação Dataset.repeat() sem argumentos repetirá a entrada indefinidamente.

A transformação Dataset.repeat concatena seus argumentos sem sinalizar o fim de uma época e o início da próxima época. Por causa disso, um Dataset.batch aplicado após Dataset.repeat produzirá lotes que ultrapassam os limites da época:

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

png

Se você precisar de uma separação de época clara, coloque Dataset.batch antes da repetição:

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

plot_batch_sizes(titanic_batches)

png

Se você gostaria de realizar um cálculo personalizado (por exemplo, para coletar estatísticas) no final de cada época, é mais simples reiniciar a iteração do conjunto de dados em cada época:

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

Misturar dados de entrada aleatoriamente

A transformação Dataset.shuffle() mantém um buffer de tamanho fixo e escolhe o próximo elemento de maneira aleatória nesse buffer.

Adicione um índice ao conjunto de dados para ver o efeito:

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

Como o buffer_size é 100 e o tamanho do lote é 20, o primeiro lote não contém elementos com índice acima de 120.

n,line_batch = next(iter(dataset))
print(n.numpy())
[  6  41  35  53  36  50  52  80   1  26  22  29  38  55  84  18  47  68
 100 105]

Tal como acontece com Dataset.batch a ordem relativa a Dataset.repeat importante.

Dataset.shuffle não sinaliza o fim de uma época até que o buffer de reprodução aleatória esteja vazio. Portanto, um shuffle colocado antes de uma repetição mostrará todos os elementos de uma época antes de passar para a próxima:

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:

[594 456 585 615 536 627 514 297 617 620]
[495 530 356 622 395 602 505 593 611 492]
[573 597 575 528 561 616 609 494]
[51 85 41  4 37  9 91 74 89  8]
[ 13 107 104  65  52  83  71  16 101  92]

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

png

Mas uma repetição antes de um embaralhamento mistura os limites da época:

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:

[570 263 533  22  17  14 485 348 517 606]
[501 488 620 611 624 599 534 398 542 521]
[446 596 359 587   6 475 436 610   2 605]
[ 35 583  49  16 547  46  39 576  47   8]
[ 30  26  63  55 496  51   9 575  66 571]
[ 41  57  33 589  43 550 540 404 591 595]
[ 12 512  48 581 600  25  67  19  85  56]
[ 83  65 563 597  52  61  90 585  91  78]
[ 11  37 445 621 618 482  92 103  82  32]
[ 42  79  62  88 543  80  95 598 539 113]
[ 38  86 107 124  50 114  76 535   3  27]
[111  64  71 130  29 129 470 120  94 101]
[425 122 131  18  40  84  87  73 519  21]
[272  58 143 145 331  77  60 612 127  13]
[495  96  99 523 138 627 115 167 135 152]

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

png

Dados de pré-processamento

A transformação Dataset.map(f) produz um novo conjunto de dados aplicando uma determinada função f a cada elemento do conjunto de dados de entrada. É baseado na função map() que é comumente aplicada a listas (e outras estruturas) em linguagens de programação funcionais. A função f pega os objetos tf.Tensor que representam um único elemento na entrada e retorna os objetos tf.Tensor que representarão um único elemento no novo conjunto de dados. Sua implementação usa operações padrão do TensorFlow para transformar um elemento em outro.

Esta seção cobre exemplos comuns de como usar Dataset.map() .

Decodificando dados de imagem e redimensionando-os

Ao treinar uma rede neural em dados de imagem do mundo real, geralmente é necessário converter imagens de tamanhos diferentes em um tamanho comum, para que possam ser agrupadas em um tamanho fixo.

Reconstrua o conjunto de dados de nomes de arquivos de flores:

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

Escreva uma função que manipule os elementos do conjunto de dados.

# 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

Teste se funciona.

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

Mapeie-o no conjunto de dados.

images_ds = list_ds.map(parse_image)

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

png

png

Aplicação de lógica Python arbitrária

Por motivos de desempenho, use as operações do TensorFlow para pré-processar seus dados sempre que possível. No entanto, às vezes é útil chamar bibliotecas Python externas ao analisar seus dados de entrada. Você pode usar a operação tf.py_function() em uma transformação Dataset.map() .

Por exemplo, se você deseja aplicar uma rotação aleatória, a tf.image módulo só tem tf.image.rot90 , o que não é muito útil para o aumento da imagem.

Para demonstrar tf.py_function , tente usar a função 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)

png

Para usar esta função com Dataset.map as mesmas ressalvas se aplicam a Dataset.from_generator , você precisa descrever as formas e tipos de retorno ao aplicar a função:

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

Analisando tf.Example mensagens de buffer de protocolo

Muitos pipelines de entrada extraem mensagens de buffer de protocolo tf.train.Example de um formato TFRecord. Cada registro tf.train.Example contém um ou mais "recursos", e o pipeline de entrada normalmente converte esses recursos em tensores.

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>

Você pode trabalhar com tf.train.Example protos fora de um tf.data.Dataset para entender os dados:

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

Janelas de séries temporais

Para um exemplo de série temporal de ponta a ponta, consulte: Previsão de série temporal .

Os dados da série temporal geralmente são organizados com o eixo do tempo intacto.

Use um Dataset.range simples para demonstrar:

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

Normalmente, os modelos baseados neste tipo de dados requerem uma fração de tempo contígua.

A abordagem mais simples seria agrupar os dados em lote:

Usando 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, para fazer previsões densas um passo no futuro, você pode mudar os recursos e rótulos em uma etapa em relação uns aos outros:

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]

Para prever uma janela inteira em vez de um deslocamento fixo, você pode dividir os lotes em duas partes:

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]

Para permitir alguma sobreposição entre os recursos de um lote e os rótulos de outro, use 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]

Usando a window

Embora o uso de Dataset.batch funcione, há situações em que você pode precisar de um controle mais preciso. O método Dataset.window dá a você controle total, mas requer alguns cuidados: ele retorna um Dataset de Datasets . Consulte a estrutura do conjunto de dados para obter detalhes.

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>

O método Dataset.flat_map pode pegar um conjunto de dados e achatá-lo em um único conjunto de dados:

 for x in windows.flat_map(lambda x: x).take(30):
   print(x.numpy(), end=' ')
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7fda544008c8> 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 0x7fda544008c8> 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 

Em quase todos os casos, você desejará .batch o conjunto de dados primeiro:

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]

Agora, você pode ver que o argumento shift controla o quanto cada janela se move.

Juntando isso, você pode escrever esta função:

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]

Então, é fácil extrair rótulos, como antes:

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]

Reamostragem

Ao trabalhar com um conjunto de dados que é muito desequilibrado por classes, você pode querer reamostrar o conjunto de dados. tf.data fornece dois métodos para fazer isso. O conjunto de dados de fraude de cartão de crédito é um bom exemplo desse tipo de problema.

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

Agora, verifique a distribuição das classes, ela é altamente distorcida:

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.9964 0.0036]

Uma abordagem comum para treinar com um conjunto de dados desequilibrado é equilibrá-lo. tf.data inclui alguns métodos que permitem este fluxo de trabalho:

Amostragem de conjuntos de dados

Uma abordagem para reamostrar um conjunto de dados é usar sample_from_datasets . Isso é mais aplicável quando você tem um data.Dataset separado para cada classe.

Aqui, basta usar o filtro para gerá-los a partir dos dados de fraude de cartão de crédito:

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 0x7fda54400400> 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 0x7fda54400400> 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 0x7fda54440400> 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 0x7fda54440400> 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]

Para usar tf.data.experimental.sample_from_datasets passe os conjuntos de dados e o peso de cada um:

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

Agora o conjunto de dados produz exemplos de cada classe com probabilidade de 50/50:

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

Reamostragem de rejeição

Um problema com a abordagem experimental.sample_from_datasets acima é que ela precisa de um tf.data.Dataset separado por classe. Usar Dataset.filter funciona, mas faz com que todos os dados sejam carregados duas vezes.

A função data.experimental.rejection_resample pode ser aplicada a um conjunto de dados para reequilibrá-lo, enquanto o carrega apenas uma vez. Os elementos serão eliminados do conjunto de dados para atingir o equilíbrio.

data.experimental.rejection_resample leva um argumento class_func . Este class_func é aplicado a cada elemento do conjunto de dados e é usado para determinar a qual classe um exemplo pertence para fins de balanceamento.

Os elementos de creditcard_ds já são pares (features, label) . Portanto, o class_func só precisa retornar esses rótulos:

def class_func(features, label):
  return label

O reamostrador também precisa de uma distribuição de destino e, opcionalmente, de uma estimativa de distribuição inicial:

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

O reamostrador lida com exemplos individuais, portanto, você deve unbatch o conjunto de dados antes de aplicar o reamostrador:

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:


O reamostrador retorna cria (class, example) pares da saída do class_func . Nesse caso, o example já era um par (feature, label) , então use map para descartar a cópia extra dos rótulos:

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

Agora o conjunto de dados produz exemplos de cada classe com probabilidade de 50/50:

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

Ponto de verificação do iterador

O Tensorflow permite fazer checkpoints para que, quando o processo de treinamento for reiniciado, ele possa restaurar o último checkpoint para recuperar a maior parte do seu progresso. Além de marcar as variáveis ​​do modelo, você também pode marcar o andamento do iterador do conjunto de dados. Isso pode ser útil se você tiver um grande conjunto de dados e não quiser iniciar o conjunto de dados desde o início a cada reinicialização. Observe, entretanto, que os pontos de verificação do iterador podem ser grandes, uma vez que transformações como shuffle e prefetch - prefetch exigem elementos de buffer dentro do iterador.

Para incluir seu iterador em um ponto de verificação, passe o iterador para o construtor 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]

Usando tf.data com tf.keras

A API tf.keras simplifica muitos aspectos da criação e execução de modelos de aprendizado de máquina. Suas APIs .fit() e .evaluate() e .predict() suportam conjuntos de dados como entradas. Aqui está um conjunto de dados rápido e configuração do modelo:

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

Passar um conjunto de dados de pares (feature, label) é tudo o que é necessário para Model.fit e 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.6033 - accuracy: 0.7961
Epoch 2/2
1875/1875 [==============================] - 3s 2ms/step - loss: 0.4633 - accuracy: 0.8411

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

Se você passar um conjunto de dados infinito, por exemplo, chamando Dataset.repeat() , você só precisa passar também o argumento 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.4781 - accuracy: 0.8313
Epoch 2/2
20/20 [==============================] - 0s 2ms/step - loss: 0.4285 - accuracy: 0.8516

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

Para avaliação, você pode passar no número de etapas de avaliação:

loss, accuracy = model.evaluate(fmnist_train_ds)
print("Loss :", loss)
print("Accuracy :", accuracy)
1875/1875 [==============================] - 3s 1ms/step - loss: 0.4449 - accuracy: 0.8468
Loss : 0.4448782205581665
Accuracy : 0.846750020980835

Para conjuntos de dados longos, defina o número de etapas para avaliar:

loss, accuracy = model.evaluate(fmnist_train_ds.repeat(), steps=10)
print("Loss :", loss)
print("Accuracy :", accuracy)
10/10 [==============================] - 0s 2ms/step - loss: 0.4509 - accuracy: 0.8438
Loss : 0.45086926221847534
Accuracy : 0.84375

Os rótulos não são obrigatórios ao chamar 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)

Mas os rótulos são ignorados se você passar um conjunto de dados que os contenha:

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