Copyright 2018 The TensorFlow Authors.
Licensed under the Apache License, Version 2.0 (the "License");
tf.data: Construir canalizaciones de entrada de TensorFlow
La API tf.data
le permite construir canalizaciones de entrada complejas con partes simples y reutilizables. Por ejemplo, la canalización para un modelo de imagen puede agregar datos desde archivos en un sistema de archivos distribuido, aplicar perturbaciones aleatorias a cada imagen y combinar imágenes seleccionadas de forma aleatoria en un lote para entrenamiento. La canalización para un modelo de texto puede incluir extraer símbolos de datos de texto sin procesar, convertirlos en identificadores incrustados con una tabla de búsqueda y combinar secuencias de diferentes longitudes en lotes. La API tf.data
posibilita que se puedan manipular grandes cantidades de datos, leer desde distintos formatos de datos y realizar transformaciones complejas.
La API tf.data
introduce una abstracción tf.data.Dataset
que representa una secuencia de elementos, en el que cada elemento consiste en uno o más componentes. Por ejemplo, en una canalización de imagen, un elemento puede ser un ejemplo de un solo entrenamiento, con un par de componentes de tensor que representan la imagen y su etiqueta.
Hay dos formas distintas de crear un conjunto de datos:
Un origen de datos construye un
Dataset
con datos alamacenados en la memoria o en uno o más archivos.Una transformación de datos construye un conjunto de datos con uno o más objetos
tf.data.Dataset
.
Mécanica básica
Para crear una canalización de entrada, debe comenzar con un origen de datos. Por ejemplo, para construir un Dataset
con datos almacenados en la memoria, puede usar tf.data.Dataset.from_tensors()
o tf.data.Dataset.from_tensor_slices()
. Como alternativa, si sus datos de entrada están almacenados en un archivo en el formato TFRecord recomendado, puede usar tf.data.TFRecordDataset()
.
Una vez que tenga el objeto, Dataset
, puede transformarlo en un nuevo Dataset
al encadenar las llamadas de método en el objeto tf.data.Dataset
. Por ejemplo, puede aplicar transformaciones por cada elemento como Dataset.map
y transformaciones de varios elementos como Dataset.batch
. Consulte la documentación de tf.data.Dataset
para ver una lista completa de las transformaciones.
El objeto Dataset
es un elemento de iteración de Python. Esto hace que se puedan consumir sus elementos con un bucle for:
O al crear explícitamente un elemento de iteración con iter
y consumir sus elementos con next
:
De forma alternativa, se pueden consumir los elementos del conjunto de datos con la transformación reduce
, que reduce todos los elementos para producir un resultado único. En el siguiente ejemplo, se ilustra cómo usar la transformación reduce
para calcular la suma de los conjuntos de datos de enteros.
Estructura del conjunto de datos
Un conjunto de datos produce una secuencia de elementos, donde cada elemento es la misma estructura (anidada) de componentes. Los componentes individuales de la estructura pueden ser de cualquier tipo que pueda representarse con tf.TypeSpec
, incluidos el tf.Tensor
, tf.sparse.SparseTensor
, tf.RaggedTensor
, tf.TensorArray
o tf.data.Dataset
.
Entre las construcciones de Python que pueden usarse para expresar una estructura (anidada) de elementos se incluyen tuple
, dict
, NamedTuple
y OrderedDict
. En especial, list
no es una construcción válida para expresar la estructura de los elementos del conjunto de datos. Esto es así porque los primeros usuarios de tf.data
estaban convencidos de que las entradas de list
(por ejemplo, al pasarlas a tf.data.Dataset.from_tensors
) se empaquetan automáticamente como tensores y las salidas de list
(por ejemplo, valores de retorno de las funciones definidas por el usuario) se coaccionan en una tuple
. Como consecuencia, si quiere que se trate una entrada de list
como una estructura, deberá convertirla en tuple
y si quiere que una salida de list
sea un componente individual, deberá empaquetarla explícitamente con tf.stack
.
La propiedad Dataset.element_spec
le permite inspeccionar el tipo de cada componente del elemento. La propiedad devuelve una estructura anidada de objetos tf.TypeSpec
para coincidir con la estructura del elemento, que puede ser un componente único, una tupla de componentes o una tupla anidada de componentes. Por ejemplo:
Las transformaciones de Dataset
admiten conjuntos de datos de cualquier estructura. Cuando se usan las transformaciones de Dataset.map
y de Dataset.filter
, que aplican una función a cada elemento, la estructura del elemento determina los argumentos de la función:
Leer datos de entrada
Consumir arreglos de NumPy
Consulte el tutorial de Cargar arreglos de NumPy para ver más ejemplos.
Si todos los datos de entrada entran en la memoria, la forma más simple de crear un Dataset
con ellos es convertirlos en objetos tf.Tensor
y usar Dataset.from_tensor_slices
.
Nota: El fragmento de código anterior encrustará los arreglos de features
y labels
como operaciones tf.constant()
en su gráfico de TensorFlow. Esto funciona bien para conjuntos de datos pequeños, pero gasta memoria, porque el contenido del arreglo se copiará varias veces, y puede ejecutarse dentro del límite de 2 GB para el búfer del protocolo tf.GraphDef
.
Consumir generadores de Python
Otro origen de datos común que puede ingresarse fácilmente como tf.data.Dataset
es el generador de Python.
Advertencia: Si bien puede ser un enfoque práctico, tiene portabilidad y escalabilidad limitadas. Debe ejecutarse en el mismo proceso de Python que con el que se creó el generador, y aún así está sujeto al GIL (bloqueo global del intérprete) de Python.
El constructor Dataset.from_generator
convierte el generador de Python en un tf.data.Dataset
completamente funcional.
El constructor toma un invocable como entrada, no un elemento de iteración. Esto le permite reiniciar el generador cuando finaliza. Toma argumentos args
opcionales, que se pasan como los argumentos del invocable.
Se requiere el argumento output_types
porque tf.data
construye un tf.Graph
internamente y los bordes del gráfico requieren un tf.dtype
.
El argumento de output_shapes
no es obligatorio pero se recomienda su uso, ya que muchas operaciones de TensorFlow no admiten tensores con un rango desconocido. Si la longitud de un eje en particular es desconocida o variable, establezca el valor como None
en el output_shapes
.
También es importante tener en cuenta que output_shapes
y output_types
siguen las mismas reglas de anidación que otros métodos de conjuntos de datos.
A continuación, se muestra un generador de ejemplo que muestra los dos aspectos: devuelve tuplas de arreglos, donde el segundo arreglo es un vector con una longitud desconocida.
La primera salida es un int32
y la segunda es un float32
.
El primer elemento es un escalar, forma ()
y el segundo es un vector de longitud desconocida, forma (None,)
Ahora se puede usar como un tf.data.Dataset
normal. Tenga en cuenta que al poner un conjunto de datos en un lote con una forma variable, se debe usar Dataset.padded_batch
.
Para ver un ejemplo más realista, intente encapsular preprocessing.image.ImageDataGenerator
como un tf.data.Dataset
.
Primero, descargue los datos:
Cree el image.ImageDataGenerator
Consumir datos TFRecord
Consulte el tutorial de Cargar datos TFRecord para ver un ejemplo de principio a fin.
La API tf.data
admite una variedad de formatos de archivos para poder procesar conjuntos de datos grandes que no entren en la memoria. Por ejemplo, el formato de archivo TFRecord es un formato binario simple orientado a registros que muchas de las aplicaciones de TensorFlow usan para entrenar datos. La clase tf.data.TFRecordDataset
permite transmitir el contenido de uno o más archivos TFRecord como parte de una canalización de entrada.
Aquí tiene un ejemplo que usa el archivo de prueba del nombre de las calles francesas (FSNS, por sus siglas en inglés).
El argumento filenames
del inicializador TFRecordDataset
puede ser una cadena de texto, un lista de cadenas de texto o un tf.Tensor
de cadenas de texto. Por eso, si tiene dos conjuntos de archivos para entrenamiento y validación, puede crear un método factory que genera los conjuntos de datos y toma los nombres de archivos como un argumento de entrada:
Muchos de los proyectos de TensorFlow usan registros tf.train.Example
en serie en sus archivos TFRecord. Deben decodificarse antes de inspeccionarlos:
Consumir datos de texto
Consulte el tutorial de Cargar texto para ver un ejemplo de principio a fin.
Muchos conjuntos de datos están distribuidos en uno o más archivos de texto. El tf.data.TextLineDataset
proporciona una forma fácil de extraer líneas de uno o más archivos de texto. Si se ingresan uno o más nombres de archivo, un TextLineDataset
generará un elemento con valor de cadena de texto por cada línea de los archivos.
Estas son las primeras líneas del primer archivo:
Para alternar líneas entre archivos use Dataset.interleave
. Esto hará que sea más fácil pasar de un archivo a otro de forma aleatoria. Estas son la primera, la segunda y la tercera línea de cada traducción:
De forma predeterminada, un TextLineDataset
da cada línea de cada archivo, que tal vez no sea conveniente, por ejemplo, si el archivo comienza con una línea de encabezado o tiene comentarios. Se pueden eliminar estas líneas con las transformaciones Dataset.skip()
o Dataset.filter
. Aquí, se omite la primera línea y luego se filtra para encontrar solo las sobrevivientes.
Consumir datos CSV
Consulte los tutoriales de Cargar archivos CSV y Cargar DataFrames de Panda para ver más ejemplos.
El formato de archivo CSV es un formato conocido para almacenar datos tabulares en texto sin formato.
Por ejemplo:
Si sus datos entran en la memoria, el mismo método de Dataset.from_tensor_slices
funciona con diccionarios, lo que permite que se puedan importar los datos de forma fácil:
Un enfoque más escalable es cargar datos desde el disco según sea necesario.
El módulo tf.data
proporciona métodos para extraer registros de uno o más archivos CSV que cumplan con el estándar RFC 4180.
La función tf.data.experimental.make_csv_dataset
es la interfaz de alto nivel para leer conjuntos de archivos CSV. Admite inferencias de tipo de columna y muchas otras funcionalidades, como poner datos en lotes y en orden aleatorio, para simplificar el uso.
Puede usar el argumento select_columns
si solo necesita un subconjunto de columnas.
También hay una clase experimental.CsvDataset
de nivel más bajo que proporciona un control más específico. No admite las inferencias de tipo de columna. Se debe especificar el tipo de cada columna.
Si algunas columnas están vacías, la inferencia de bajo nivel le permite proporcionar valores predeterminados en lugar de tipos de columna.
De forma predeterminada, un CsvDataset
genera cada columna de cada línea del archivo, que quizás no sea conveniente si, por ejemplo, el archivo comienza con una línea de encabezado que debería ser ignorada o si no se necesitan algunas columnas en la entrada. Se pueden eliminar estas líneas y campos con los argumentos header
y select_cols
respectivamente.
Consumir conjuntos de archivos
Hay muchos conjuntos de datos distribuidos como conjuntos de archivos, donde cada archivo es un ejemplo.
Nota: estas imágenes están bajo la licencia de CC-BY, consulte LICENSE.txt para obtener más detalles.
El directorio de raíz contiene un directorio para cada clase:
Los archivos en cada directorio de clase son ejemplos:
Lea los datos con la función tf.io.read_file
y extraiga la etiqueta desde la ruta de acceso, esto devolverá pares de (image, label)
:
Procesar elementos del conjunto de datos por lotes
Procesamiento por lotes
La forma más simple de procesamiento por lotes apila n
elementos consecutivos de un conjunto de datos en un solo elemento. La transformación Dataset.batch()
hace exactamente esto con las mismas restricciones que el operador tf.stack()
, aplicado a cada componente de los elementos: esto significa que para cada componente i, todos los elementos deben tener un tensor de exactamente la misma forma.
Mientras que tf.data
intenta propagar la información de la forma, la configuración predeterminada de Dataset.batch
da como resultado un tamaño desconocido de lote porque tal vez el último lote no está lleno. Observe los None
s en la forma:
Use el argumento drop_remainder
para ignorar el último lote y obtenga una propagación completa de la forma:
Procesar tensores con espaciado por lotes
La receta anterior funciona con tensores del mismo tamaño. Sin embargo, muchos modelos (incluidos los modelos de secuencia) funcionan con datos de entrada que pueden tener varios tamaños (por ejemplo, secuencias de diferentes longitudes). Para tratar este caso, la transformación Dataset.padded_batch
le permite procesar en lotes tensores de diferentes formas por lote al especificar una o más dimensiones en las que se pueda agregar espaciado.
La transformación Dataset.padded_batch
permite establecer diferentes espaciados para cada dimensión de cada componente y puede tener longitudes variables (está representado con None
en el ejemplo anterior) o longitudes constantes. También se puede reemplazar el valor del espaciado, cuyo valor predeterminado es 0.
Flujos de entrenamiento
Procesar varias épocas
La API tf.data
ofrece dos formas principales de procesar varias épocas de los mismos datos.
La forma más simple de hacer una iteración en un conjunto de datos en varias épocas es con la transformación Dataset.repeat()
. Primero, cree un conjunto de datos de datos del titanic:
Si se aplica la transformación Dataset.repeat()
sin argumentos, se repetirá la entrada indefinidamente.
La transformación Dataset.repeat
concatena sus argumentos sin marcar el final de una época ni el comienzo de la siguiente época. Por eso, si se aplica un Dataset.batch
después de un Dataset.repeat
generará lotes que amplían los límites de la época:
Si necesita una separación clara entre las épocas, escriba Dataset.batch
antes de la repetición:
Si quiere realizar un cálculo personalizado (por ejemplo, para recopilar estadísticas) al final de cada época, lo más simple es reiniciar la iteración del conjunto de datos en cada época:
Poner los datos de entrada en orden aleatorio
La transformación Dataset.shuffle()
mantiene un búfer de tamaño fijo y elige el siguiente elemento desde el búfer de forma uniforme y al azar.
Nota: Aunque los bufer_sizes grandes pueden ordenarse de manera aleatoriade forma más diligente, pueden usar mucha memoria y un tiempo considerable para completarse. Considere usar Dataset.interleave
en los archivos si tiene este problema.
Agregue un índice al conjunto de datos para poder ver el efecto:
Dado que el buffer_size
es 100 y el tamaño del lote es 20, el primer lote no tiene ningún elemento con un índice mayor a 120.
Al igual que con Dataset.batch
el orden en relación con Dataset.repeat
importa.
Dataset.shuffle
no señala el final de una época hasta que el búfer del orden aleatorio esté vacío. Entonces, si se agrega un orden aleatorio antes de una repetición se mostrará cada elemento de una época antes de ir a la siguiente:
Pero si hay una repetición antes de un orden aleatorio se mezclan los límites de la época:
Procesar datos
La transformación Dataset.map(f)
genera un conjunto de datos nuevo al aplicar la función ingresada f
en cada elemento del conjunto de datos de entrada. Se basa en la función map()
que comúnmente se aplica a listas (y otras estructuras) en los lenguajes de programación funcionales. La función f
toma los objetos de tf.Tensor
que representan un elemento único en la entrada y devuelve los objetos tf.Tensor
que representan un elemento único en el conjunto de datos nuevo. Al implementarse está función, se usan operaciones estándar de TensorFlow para transformar un elemento en otro.
En esta sección, se cubren ejemplos comúnes de cómo usar Dataset.map()
.
Decodificar datos de imagen y cambiar el tamaño
Cuando se entrena una red neuronal con datos de imágenes del mundo real, suele ser necesario convertir las imágenes de diferentes tamaños a un tamaño común, para que se puedan procesar por lotes en un tamaño fijo.
Reconstruya el conjunto de datos de nombres de archivos de flores:
Escriba una función que manipule los elementos del conjunto de datos.
Pruebe que funcione.
Asígnela a un conjunto de datos.
Aplicar una lógica arbitraria de Python
Por el rendimiento, use operaciones de TensorFlow para preprocesar sus datos cuando sea posible. Sin embargo, a veces sirve llamar a las bibliotecas externas de Python cuando parsea sus datos de entrada. Puede usar la operación tf.py_function
en una transformación Dataset.map
.
Por ejemplo, si quiere aplicar una rotación aleatoria, el módulo tf.image
solo tiene tf.image.rot90
, que no es muy útil para aumentar imágenes.
Nota: tensorflow_addons
tiene un rotate
compatible de TensorFlow en tensorflow_addons.image.rotate
.
Para demostrar tf.py_function
, pruebe usar la función scipy.ndimage.rotate
en su lugar:
Para usar la función con Dataset.map
aplica la misma advertencia que con Dataset.from_generator
, debe describir las formas y los tipos de retorno cuando aplique la función:
Parsear mensajes de búfer del protocolo tf.Example
Muchas canalizaciones de entrada extraen mensajes de búfer del protocolo tf.train.Example
desde un formato TFRecord. Cada registro de tf.train.Example
contiene una o más "funciones", y la canalización de entrada suele convertir estas funciones en tensores.
Puede trabajar con protocolos de tf.train.Example
fuera de un tf.data.Dataset
para entender los datos:
Para ver un ejemplo de principio a fin de una serie temporal consulte: Predecir series temporales.
Los datos de las series temporales suelen organizarse con el eje de tiempo intacto.
Use un Dataset.range
simple para demostrar:
Por lo general, los modelos que se basan en este tipo de datos necesitarán un segmento de tiempo contiguo.
El enfoque más simple sería procesar los datos por lotes:
Con batch
O para realizar predicciones densas un paso adelantado, es posible que quiera cambiar las características y etiquetas por un paso relacionado entre sí:
Para predecir una ventana completa en lugar de un desplazamiento fijo, puede dividir los lotes en dos partes:
Para permitir que las características de un lote y las etiquetas de otro se superpongan un poco, use Dataset.zip
:
Con window
Si bien usar Dataset.batch
funciona, hay situaciones en las que tal vez necesite un control más específico. El método Dataset.window
le da control total, pero necesita más atención: este método devuelve un Dataset
de los Datasets
. Para obtener más detalles, consulte la sección Estructura del conjunto de datos.
El método Dataset.flat_map
puede tomar un conjunto de datos de los conjuntos de datos y acoplarlo en un solo conjunto de datos:
En la mayoría de los casos, primero querrá Dataset.batch
el conjunto de datos:
Ahora puede ver que el argumento shift
controla cuánto se mueve cada ventana.
Si lo une todo, puede escribir esta función:
Y así, es más fácil extraer las etiquetas, como antes:
Nuevo muestreo
Si tiene un conjunto de datos de diferentes clases, deberá realizar un nuevo muestreo del conjunto de datos. tf.data
proporciona dos métodos para hacerlo. El conjunto de datos de fraude de tarjeta de crédito es un buen ejemplo de este tipo de problema.
Nota: visite Clasificación de datos desequilibrados para ver un tutorial completo.
Ahora, verifique la distribución de clases, está marcadamente asimétrica:
Un enfoque común para entrenar con conjuntos de datos desequilibrados es equilibrarlos. tf.data
incluye algunos métodos que permiten este flujo de trabajo:
Muestreo del conjuntos de datos
Un enfoque para realizar un nuevo muestreo de un conjunto de datos es usar sample_from_datasets
. Es más aplicable cuando se tiene un tf.data.Dataset
diferente para cada clase.
A continuación, solo use el filtro para generarlos con los datos de fraude de tarjeta de crédito:
Para usar tf.data.Dataset.sample_from_datasets
pase los conjuntos de datos y el peso para cada uno:
Ahora el conjunto de datos produce ejemplos de cada clase con una probabilidad de 50/50.
Rechazo del nuevo muestreo
Un problema con el enfoque Dataset.sample_from_datasets
anterior es que necesita un tf.data.Dataset
diferente por clase. Podría usar Dataset.filter
para crear esos dos conjuntos de datos, pero esto hará que se carguen los datos dos veces.
Se puede aplicar el método tf.data.Dataset.rejection_resample
en el conjunto de datos para volver a equilibrarlo, y solo se carga una vez. Se excluirán o repetirán elementos para lograr el equilibrio.
El método rejection_resample
toma un argumento class_func
. Se aplica class_func
en cada elemento del conjunto de datos y se usa para determinar a qué clase pertenece un ejemplo con el propósito de equilibrio.
El objetivo es equilibrar la distribución de las etiquetas, y los elementos de creditcard_ds
ya son pares de (features, label)
. Por eso la class_func
solo debe devolver esas etiquetas:
El método de volver a muestrear trata ejemplos individuales, por eso, en este caso debe unbatch
el conjunto de datos antes de aplicar el método.
El método necesita una distribución de destino y, de forma opcional, un cálculo estimado de la distribución inicial como entradas.
El método rejection_resample
devuelve los pares de (class, example)
donde la class
es una salida de la class_func
. En este caso, el example
ya era un par de (feature, label)
, por eso use map
para excluir la copia adicional de las etiquetas:
Ahora el conjunto de datos genera ejemplos de cada clase con una probabilidad de 50/50:
Punto de verificación del elemento de iteración
TensorFlow admite guardar puntos de verificación para que cuando el proceso de entrenamiento se reinicie, se pueda restaurar el último punto de verificación para recuperar la mayoría del progreso. Además de guardar los puntos de verificación de las variables del modelo, también puede guardar los puntos de verificación del progreso del elemento de iteración del conjunto de datos. Eso podría servirle si tiene un conjunto de datos grande y no quiere empezar desde el principio del conjunto de datos cada vez que lo reinicia. De todas maneras, tenga en cuenta que los puntos de verificación de un elemento de iteración pueden ser grandes, ya que las transformaciones como Dataset.shuffle
y Dataset.prefetch
requieren almacenar elementos en el búfer dentro del elemento de iteración.
Para incluir su elemento de iteración en el punto de verificación, pase el elemento de iteración al constructor tf.train.Checkpoint
.
Nota: no es posible guardar un punto de verificación de un elemento de iteración que depende de un estado externo, como una tf.py_function
. Si lo intenta, provocará una excepción y reclamará el estado externo.
Usar tf.data
con tf.keras
La API tf.keras
simplifica muchos aspectos de la creación y ejecución de los modelos de aprendizaje automático. Sus API Model.fit
, Model.evaluate
y Model.predict
admiten los conjuntos de datos como entradas. A continuación, encontrará una configuración rápida de conjuntos de datos y modelos:
Solo se necesita pasar un conjunto de datos de pares de (feature, label)
para Model.fit
y la Model.evaluate
:
Si pasa un conjunto de datos infinito, por ejemplo, al llamar a Dataset.repeat
, solo deberá pasar el argumento steps_per_epoch
también:
Para evaluarlo, también puede pasar la cantidad de pasos de la evaluación:
Para conjuntos de datos grandes, establezca la cantidad de pasos para evaluar:
No se requieren las etiquetas cuando se llama a Model.predict
.
Pero se ignorarán las etiquetas si pasa un conjunto de datos que tenga etiquetas: