Copyright 2018 The TensorFlow Authors.
Licensed under the Apache License, Version 2.0 (the "License");
tf.data: criando pipelines de entrada no TensorFlow
A API tf.data
permite a criação de 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 num 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 de embeddings numa tabela de pesquisa e agrupar sequências de comprimentos diferentes em um lote. A API tf.data
torna possível lidar com grandes quantidades de dados, ler diferentes formatos de dados e realizar transformações complexas.
A API tf.data
introduz 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, num 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 dataset:
Uma fonte de dados constrói um
Dataset
a partir de dados armazenados na memória ou em um ou mais arquivos.Uma transformação de dados constrói um dataset a partir de um ou mais objetos
tf.data.Dataset
.
Mecanismo básico
Para criar um pipeline de entrada, você deve começar com uma fonte de dados. Por exemplo, para construir um Dataset
a partir de dados na memória, você pode usar tf.data.Dataset.from_tensors()
ou tf.data.Dataset.from_tensor_slices()
. Alternativamente, se seus dados de entrada estiverem armazenados em arquivo no formato TFRecord recomendado, você poderá usar tf.data.TFRecordDataset()
.
Assim que você tiver um objeto Dataset
, você pode transformá-lo num 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 multi-elemento, como Dataset.batch
. Consulte a documentação de tf.data.Dataset
para uma lista completa de transformações.
O objeto Dataset
é um iterável (iterable) em Python. Isso possibilita o consumo dos seus elementos usando um loop for:
Ou criando explicitamente um iterador Python com iter
e consumindo seus elementos usando next
:
Alternativamente, os elementos do dataset podem ser consumidos usando a transformação reduce
, que reduz todos os elementos para produzir um único resultado. O exemplo a seguir ilustra como usar a transformação reduce
para calcular a soma de um conjunto de dados de números inteiros.
Estrutura do dataset
Um dataset produz uma sequência de elementos , onde cada elemento é a mesma estrutura (aninhada) de componentes. 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
.
Os construtos do Python que podem ser usados para expressar a estrutura (aninhada) dos elementos incluem tuple
, dict
, NamedTuple
e OrderedDict
. No entanto, list
não é considerado um construto válido para expressar a estrutura dos elementos do dataset. Isto ocorre devido aos primeiros usuários de tf.data
se preocuparem com a possibilidade das entradas list
(por exemplo, quando passadas para tf.data.Dataset.from_tensors
) serem automaticamente empacotadas como tensores e as saídas list
(por exemplo, valores de retorno de funções definidas pelo usuário) serem coagidas para uma tuple
. Como consequência disso, se você quiser que uma entrada list
seja tratada como uma estrutura, você precisa convertê-la numa tuple
e se quiser que uma saída de list
seja tratado como um único componente, então você precisará empacotá-la explicitamente usando tf.stack
.
A propriedade Dataset.element_spec
permite inspecionar o tipo de cada componente de um 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:
As transformações Dataset
suportam datasets de qualquer estrutura. Ao usar as transformações Dataset.map
e Dataset.filter
, que aplicam uma função a cada elemento, a estrutura do elemento determina os argumentos da função:
Lendo dados de entrada
Consumindo arrays NumPy
Consulte o tutorial Carregando arrays NumPy para mais exemplos.
Se todos os seus dados de entrada couberem na memória, a maneira mais simples de criar um Dataset
a partir deles é convertê-los em objetos tf.Tensor
e usar Dataset.from_tensor_slices
.
Observação: o snippet de código acima incorporará os arrays features
e labels
no seu grafo do TensorFlow como operações tf.constant()
. Isto funciona bem para um dataset pequeno, mas desperdiça memória --- porque o conteúdo do array será copiado múltiplas vezes --- e poderá atingir o limite de 2 GB para o buffer de protocolo tf.GraphDef
.
Consumindo geradores Python
Outra fonte de dados comum que pode ser facilmente ingerida como tf.data.Dataset
é o gerador python.
Cuidado: Embora esta seja uma abordagem conveniente, ela é limitada quanto à portabilidade e escalabilidade. Ela deve ser executado no mesmo processo python que criou o gerador, e ainda está sujeita ao Python GIL .
O construtor Dataset.from_generator
converte o gerador python em um tf.data.Dataset
totalmente funcional.
O construtor recebe um callable como entrada, não um iterator. Isto permite reiniciar o gerador quando ele chega ao fim. Ele recebe um argumento args
opcional, que é passado como os argumentos do callable.
O argumento output_types
é necessário porque tf.data
constrói um tf.Graph
internamente e as bordas do grafo requerem um tf.dtype
.
O argumento output_shapes
não é obrigatório, mas é altamente recomendado, pois muitas operações do TensorFlow não oferecem suporte a tensores com posto desconhecido. 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 um dataset.
Eis um exemplo de gerador que demonstra ambos os aspectos: ele retorna tuplas de arrays, onde o segundo array é um vetor com comprimento desconhecido.
A primeira saída é um int32
e a segunda é um float32
.
O primeiro item é um escalar, formato ()
, e o segundo é um vetor de comprimento desconhecido, formato (None,)
Agora ele pode ser usado como um tf.data.Dataset
comum. Observe que ao construir um lote com um dataset de formato variável, você precisa usar Dataset.padded_batch
.
Para um exemplo mais realista, experimente empacotar preprocessing.image.ImageDataGenerator
como um tf.data.Dataset
.
Primeiro baixe os dados:
Crie o image.ImageDataGenerator
Consumindo dados TFRecord
Consulte o tutorial Carregando TFRecords para um exemplo completo.
A API tf.data
oferece suporte a uma variedade de formatos de arquivo para que você possa processar grandes datasets que não cabem na memória. Por exemplo, o formato de arquivo TFRecord é um formato binário simples orientado a registros que muitos aplicativos TensorFlow usam para dados de treinamento. A classe tf.data.TFRecordDataset
permite transmitir o conteúdo de um ou mais arquivos TFRecord como parte de um pipeline de entrada.
Eis um exemplo usando o arquivo de teste da French Street Name Signs (FSNS).
O argumento filenames
(nomes de arquivos) passado 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, poderá criar um método de fábrica que produza o dataset, tomando filenames como argumento de entrada:
Muitos projetos do TensorFlow usam registros tf.train.Example
serializados em seus arquivos TFRecord. Eles precisam ser decodificados antes de serem inspecionados:
Consumindo dados de texto
Consulte o tutorial Carregando texto para um exemplo completo.
Muitos datasets 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 arquivos, um TextLineDataset
produzirá um elemento com valor de string por linha desses arquivos.
Aqui estão as primeiras linhas do primeiro arquivo:
Para alternar linhas entre arquivos use Dataset.interleave
. Isso facilita o processo de embaralhar os arquivos. Aqui estão a primeira, segunda e terceira linhas de cada tradução:
Por padrão, um TextLineDataset
produz cada linha 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 transformações Dataset.skip()
ou Dataset.filter
. Aqui, você pula a primeira linha e depois filtra para encontrar apenas linhas com sobreviventes.
Consumindo dados CSV
Consulte os tutoriais Carregando arquivos CSV e Carregando DataFrames Pandas para mais exemplos.
O formato de arquivo CSV é um formato popular para armazenar dados tabulares em texto simples.
Por exemplo:
Se seus dados couberem na memória o mesmo método Dataset.from_tensor_slices
funciona em dicionários, permitindo que esses dados sejam facilmente importados:
Uma abordagem mais escalá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 estejam em conformidade com a RFC 4180.
A função tf.data.experimental.make_csv_dataset
é a interface de alto nível para leitura de conjuntos de arquivos CSV. Ela suporta inferência de tipo de coluna e muitos outros recursos, como lote e embaralhamento, para simplificar o uso.
Você pode usar o argumento select_columns
se precisar apenas de um subconjunto das colunas.
Há também uma classe experimental.CsvDataset
de nível inferior que oferece um controle mais refinado. Mas ela não suporta inferência de tipo de coluna. Em vez disso, você precisa especificar o tipo de cada coluna.
Se algumas colunas estiverem vazias, esta interface de baixo nível permite fornecer valores padrão em vez de tipos de colunas.
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 obrigatórias na entrada. Essas linhas e campos podem ser removidos com os argumentos header
e select_cols
respectivamente.
Consumindo conjuntos de arquivos
Existem muitos datasets distribuídos como um conjunto de arquivos, onde cada arquivo é um exemplo.
Observação: essas imagens são licenciadas de acordo com a CC-BY. Veja LICENSE.txt para mais detalhes.
O diretório raiz contém um diretório para cada classe:
Os arquivos em cada diretório de classe são exemplos:
Leia os dados usando a função tf.io.read_file
e extraia o rótulo do caminho, retornando pares (image, label)
:
Lotes de elementos de um dataset
Lotes simples
A forma mais simples de criar um lote é empilhando n
elementos consecutivos de um dataset num único elemento. A transformação Dataset.batch()
faz exatamente isso, com as mesmas restrições do operador tf.stack()
, aplicadas a cada componente dos elementos: ou seja, para cada componente i, todos os elementos devem ter um tensor do mesmo formato.
Embora tf.data
tente propagar informações de formato, as configurações padrão de Dataset.batch
resultam num tamanho de lote desconhecido porque o último lote pode não estar cheio. Observe os None
no formato:
Use o argumento drop_remainder
para ignorar o último lote e obter a propagação completa do formato:
Lotes de tensores com preenchimento
A receita acima funciona para tensores que têm todos o mesmo tamanho. No entanto, muitos modelos (incluindo 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 agrupar em lote tensores de diferentes formatos especificando uma ou mais dimensões nas quais eles podem ser preenchidos.
A transformação Dataset.padded_batch
permite definir preenchimentos diferentes para cada dimensão de cada componente e pode ter comprimento variável (representado por None
no exemplo acima) ou comprimento constante. Também é possível substituir o valor do preenchimento, cujo padrão é 0.
Workflows de treinamento
Processando múltiplas épocas
A API tf.data
oferece duas formas principais de processar múltiplas épocas dos mesmos dados.
A maneira mais simples de iterar sobre um dataset em múltiplas épocas é usar a transformação Dataset.repeat()
. Primeiro, crie um conjunto de dados do Titanic:
Aplicando 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 época seguinte. Por causa disso, um Dataset.batch
aplicado após Dataset.repeat
produzirá lotes que ultrapassam os limites da época:
Se você precisar de uma separação clara de épocas, coloque Dataset.batch
antes do repeat:
Se você quiser realizar uma computação personalizada (por exemplo, para coletar estatísticas) ao final de cada época, é mais simples reiniciar a iteração do dataset em cada época:
Embaralhando dados de entrada aleatoriamente
A transformação Dataset.shuffle()
mantém um buffer de tamanho fixo e escolhe o próximo elemento de maneira uniforme e aleatória a partir desse buffer.
Observação: embora um buffer_size grande seja embaralhado de maneira mais completa, ele pode consumir muita memória e demorar um tempo considerável para ser preenchido. Considere usar Dataset.interleave
entre arquivos se isso se tornar um problema.
Adicione um índice ao dataset para ver o efeito:
Já que o buffer_size
é 100 e o tamanho do lote é 20, o primeiro lote não contém elementos com índice superior a 120.
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 aleatório esteja vazio. Portanto, um shuffle colocado antes de um repeat mostrará todos os elementos de uma época antes de passar para a seguinte:
Mas um repeat antes de um shuffle mistura os limites da época:
Pré-processamento de dados
A transformação Dataset.map(f)
produz um novo dataset aplicando uma determinada função f
a cada elemento do dataset de entrada. É baseado na função map()
que é frequentemente 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 dataset. 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 e redimensionando dados de imagem
Ao treinar uma rede neural com dados de imagens do mundo real, muitas vezes é necessário converter imagens de tamanhos diferentes em um tamanho comum, para que possam ser agrupadas em lotes de tamanho fixo.
Reconstrua o dataset de nomes de arquivos de flores:
Escreva uma função que manipule os elementos do dataset.
Teste para ver se funciona.
Faça o mapeamento com o dataset.
Aplicando lógica Python arbitrária
Por questões de desempenho, use operações do TensorFlow para pré-processar seus dados sempre que possível. No entanto, às vezes é útil chamar bibliotecas Python externas ao processar seus dados de entrada. Você pode usar a operação tf.py_function
em uma transformação Dataset.map
.
Por exemplo, se você quiser aplicar uma rotação aleatória, o módulo tf.image
possui apenas tf.image.rot90
, o que não é muito útil para aumento de imagem.
Observação: O tensorflow_addons
tem uma rotate
compatível com TensorFlow em tensorflow_addons.image.rotate
.
Para demonstrar a tf.py_function
, tente usar a função scipy.ndimage.rotate
:
Para usar esta função com Dataset.map
as mesmas ressalvas se aplicam a Dataset.from_generator
, você precisa descrever os formatos e tipos de retorno ao aplicar a função:
Processando mensagens de buffer de protocolo tf.Example
Muitos pipelines de entrada extraem mensagens de buffer de protocolo tf.train.Example
de um formato TFRecord. Cada registro tf.train.Example
contém uma ou mais "características", e o pipeline de entrada normalmente converte essas características em tensores.
Você pode trabalhar com protos tf.train.Example
fora de um tf.data.Dataset
para entender os dados:
Para um exemplo de série temporal de ponta a ponta, consulte: Previsão em séries temporais .
Os dados de séries temporais geralmente são organizados com o eixo temporal intacto.
Use um Dataset.range
simples para demonstrar:
Normalmente, os modelos baseados neste tipo de dados desejarão um intervalo de tempo contíguo.
A abordagem mais simples seria agrupar os dados em lote:
Usando batch
Ou, para fazer previsões densas um passo no futuro, você pode mudar as características e os rótulos um passo em relação um ao outro:
Para prever uma janela inteira em vez de um deslocamento fixo, você pode dividir os lotes em duas partes:
Para permitir alguma sobreposição entre os recursos de um lote e os rótulos de outro, use Dataset.zip
:
Usando window
Embora o uso Dataset.batch
funcione, há situações em que você poderá precisar de um controle mais preciso. O método Dataset.window
garante controle total, mas requer alguns cuidados: ele retorna um Dataset
de Datasets
. Vá para a seção Estrutura do dataset para mais detalhes.
O método Dataset.flat_map
pode pegar um dataset de datasets e achatá-lo num único dataset:
Em praticamente todas as situações, você vai querer primeiro fazer o Dataset.batch
do dataset:
Agora, você pode ver que o argumento shift
controla o quanto cada janela se move.
Juntando tudo isso, você pode escrever esta função:
Depois disso fica fácil extrair rótulos, como antes:
Reamostragem
Ao trabalhar com um dataset que é pouco balanceado quanto a classes, você poderá querer fazer uma reamostragem do dataset. tf.data
fornece dois métodos para fazer isso. O dataset credit card fraud é um ótimo exemplo desse tipo de problema.
Observação: Vá para Classificação de dados não balanceados para um tutorial completo.
Agora, verifique a distribuição das classes. Ela tem um grande desvio:
Uma abordagem comum para treinar com um dataset desbalanceado é balanceá-lo. tf.data
inclui alguns métodos que permitem realizar este workflow:
Reamostragem de datasets
Uma abordagem para fazer a reamostragem de um conjunto de dados é usar sample_from_datasets
. Isto é mais útil quando você tem um tf.data.Dataset
separado para cada classe.
Aqui, basta usar o filtro para gerá-los a partir dos dados do dataset credit card fraud:
Para usar tf.data.Dataset.sample_from_datasets
, passe os datasets e o peso de cada um:
Agora o dataset produz exemplos de cada classe com probabilidade de 50/50:
Reamostragem de rejeição
Um problema com a abordagem Dataset.sample_from_datasets
acima é que ela precisa de um tf.data.Dataset
separado por classe. Você poderia usar Dataset.filter
para criar esses dois datasets, mas isso resulta no carregamento de todos os dados duas vezes.
O método tf.data.Dataset.rejection_resample
pode ser aplicado a um dataset para rebalanceá-lo, carregando-o apenas uma vez. Os elementos serão eliminados ou repetidos para alcançar o balanceamento.
O método rejection_resample
recebe um argumento class_func
. Este class_func
é aplicado a cada elemento do dataset e é usado para determinar a qual classe um exemplo pertence para fins de balanceamento.
O objetivo aqui é balancear a distribuição dos rótulos, e os elementos de creditcard_ds
já são pares (features, label)
. Então class_func
só precisa retornar esses rótulos:
O método de reamostragem lida com exemplos individuais, portanto, neste caso, você deve fazer unbatch
do lote do dataset antes de aplicar esse método.
O método precisa de uma distribuição-alvo e, opcionalmente, de uma estimativa de distribuição inicial como entradas.
O método rejection_resample
retorna pares (class, example)
onde a class
é a saída de class_func
. Neste caso, o example
já era um par (feature, label)
, então use map
para descartar a cópia extra dos rótulos:
Agora o dataset produz exemplos de cada classe com probabilidade de 50/50:
Checkpoints do iterador
O Tensorflow oferece suporte à tomada de checkpoints para que, quando o processo de treinamento for reiniciado, ele possa restaurar o checkpoint mais recente para recuperar a maior parte de seu progresso. Além de fazer checkpointing das variáveis do modelo, você também pode fazer checkpointing do progresso do iterador do dataset. Isso pode ser útil se você tiver um dataset grande e não quiser iniciá-lo do zero a cada reinicialização. Observe, entretanto, que os checkpoints do iterador podem ser grandes, pois transformações como Dataset.shuffle
e Dataset.prefetch
requerem elementos de buffer dentro do iterador.
Para incluir seu iterador num checkpoint, passe o iterador para o construtor tf.train.Checkpoint
.
Observação: Não é possível aplicar um checkpoint em um iterador que dependa de um estado externo, como tf.py_function
. Tentar fazer isso gerará uma exceção reclamando do estado externo.
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 Model.fit
, Model.evaluate
e Model.predict
suportam datasets como entradas. Aqui está a configuração rápida de um dataset e modelo:
Passar um dataset de pares (feature, label)
é tudo o que você precisa para Model.fit
e Model.evaluate
:
Se você passar um dataset infinito, por exemplo, ao chamar Dataset.repeat
, você só precisa passar também o argumento steps_per_epoch
:
Para a avaliação você pode passar o número de passos de avaliação:
Para datasets longos, defina o número de passos a serem avaliados:
Os rótulos não são necessários ao chamar Model.predict
.
Mas os rótulos serão ignorados se você passar um dataset que os contenha: