Blog
Cómo preparar un conjunto de datos de pies de foto para el modelado
- Publicado por: Rafael Fernandez
- Categoría: Uncategorized
El subtitulado automático de las fotos es un problema en el que un modelo debe generar una descripción textual legible para el ser humano a partir de una fotografía. Es un problema difícil en la inteligencia artificial que requiere tanto la comprensión de la imagen desde el campo de la visión por ordenador como la generación de lenguaje desde el campo del procesamiento del lenguaje natural. Ahora es posible desarrollar sus propios modelos de subtítulos de imágenes utilizando el Deep Learning y conjuntos de datos de fotos y sus descripciones disponibles gratuitamente. En este tutorial, tú descubrirás cómo preparar fotos y descripciones textuales listas para desarrollar un modelo de generación automática de pies de foto de Deep Learning. Después de completar este tutorial, tú sabrás:
- Acerca del conjunto de datos de Flickr8K compuesto por más de 8.000 fotos y hasta 5 pies de foto para cada foto.
- Cómo cargar y preparar datos fotográficos y de texto para modelar con un aprendizaje profundo.
- Cómo codificar específicamente datos para dos tipos diferentes de modelos de aprendizaje profundo en Keras.
Vamos a empezar.
Descripción general del tutorial
Este tutorial se divide en las siguientes partes:
- Descargar el conjunto de datos de Flickr8K
- Cómo cargar fotografías
- Funciones de pre-calcular fotos
- Cómo cargar descripciones
- Preparar texto de descripción
- Modelo de Secuencia de Descripción Completa
- Modelo palabra por palabra
- Carga progresiva
Descargar el conjunto de datos de Flickr8K
Un buen conjunto de datos para usar cuando se comienza con el subtitulado de imágenes es el conjunto de datos de Flickr8K. La razón es que es realista y relativamente pequeño para que pueda descargarlo y construir modelos en tu estación de trabajo usando una CPU. La descripción definitiva del conjunto de datos se encuentra en el documento Framing Image Description as a Ranking Task: Data, Models and Evaluation Metrics from 2013 (Enmarcar la descripción de la imagen como una tarea de clasificación: Datos, Modelos y Métricas de Evaluación a partir de 2013).
El conjunto de datos está disponible de forma gratuita. Debes completar un formulario de solicitud y los enlaces al conjunto de datos se te enviarán por correo electrónico. Me encantaría enlazar con ellos por ti, pero la dirección de correo electrónico lo solicitan expresamente: Por favor, no redistribuyas el conjunto de datos. Puedes utilizar el siguiente enlace para solicitar el conjunto de datos:
- Formulario de solicitud de conjuntos de datos. https://illinois.edu/fb/sec/1713398
En poco tiempo, recibirás un correo electrónico que contiene enlaces a dos archivos:
- Flickr8k_Dataset.zip (1 Gigabyte) Un archivo de todas las fotografías.
- Flickr8k_text.zip (2.2 Megabytes) Un archivo de todas las descripciones de texto para fotografías.
Descarga los conjuntos de datos y descomprímalos en su directorio de trabajo actual. Usted tendrá dos directorios:
- Conjunto de datos de Flicker8k: Contiene más de 8000 fotografías en formato JPEG (sí, el nombre del directorio lo escribe ‘Flicker’ no ‘Flickr’).
- Texto de Flickr8k: Contiene una serie de archivos que contienen diferentes fuentes de descripción de las fotografías.
A continuación, veamos cómo cargar las imágenes.
Cómo cargar las imágenes
En esta sección, desarrollaremos un código para cargar las fotos para usarlas con la biblioteca de Deep Learning de Keras en Python. Los nombres de los archivos de imagen son identificadores de imagen únicos. Por ejemplo, aquí hay un ejemplo de nombres de archivos de imagen:
990890291_afc72be141.jpg 99171998_7cc800ceef.jpg 99679241_adc853a5c0.jpg 997338199_7343367d7f.jpg 997722733_0cb5439472.jpg
Keras proporciona la función load_img() que puede ser usada para cargar los archivos de imagen directamente como una matriz de píxeles.
from keras.preprocessing.image import load_img image = load_img('990890291_afc72be141.jpg')
Los datos de píxeles necesitan ser convertidos a una matriz NumPy para su uso en Keras. Podemos usar la función img_to_array() Keras para convertir los datos cargados.
from keras.preprocessing.image import img_to_array image = img_to_array(image)
Es posible que queramos utilizar un modelo predefinido de extracción de características, como una red de clasificación de imágenes profunda de última generación entrenada en Image net. El modelo del Oxford Visual Geometry Group (VGG) es popular para este propósito y está disponible en Keras. Si decidimos usar este modelo pre-entrenado como un extractor de características en nuestro modelo, podemos preprocesar los datos de píxeles para el modelo usando la función preprocess_input() en Keras, por ejemplo:
from keras.applications.vgg16 import preprocess_input # cambiar la forma de los datos en una sola muestra de una imagen image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # preparar la imagen para el modelo VGG image = preprocess_input(image)
También podemos forzar la carga de la foto para que tenga las mismas dimensiones de píxeles que el modelo VGG, que son 224 x 224 píxeles. Podemos hacerlo en la llamada a load_img(), por ejemplo:
image = load_img('990890291_afc72be141.jpg', target_size=(224, 224))
Es posible que queramos extraer el identificador de imagen único del nombre de archivo de la imagen. Podemos hacerlo dividiendo la cadena del nombre del archivo por el carácter ‘.’ (punto) y recuperando el primer elemento de la matriz resultante:
image_id = filename.split('.')[0]
Podemos unir todo esto y desarrollar una función que, dado el nombre del directorio que contiene las fotos, cargue y preprocese todas las fotos para el modelo VGG y las devuelva en un diccionario tecleado en sus identificadores de imagen únicos.
from os import listdir from os import path from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input def load_photos(directory): images = dict() for name in listdir(directory): # cargar una imagen desde un archivo filename = path.join(directory, name) image = load_img(filename, target_size=(224, 224)) # convertir los píxeles de la imagen en una matriz entumecida image = img_to_array(image) # remodelar los datos para el modelo image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # preparar la imagen para el modelo VGG image = preprocess_input(image) # obtener identificación de la imagen image_id = name.split('.')[0] images[image_id] = image return images # cargar imágenes directory ='Flicker8k_Dataset'images = load_photos(directory) print('Loaded Images: %d'% len(images))
Al ejecutar este ejemplo se imprime el número de imágenes cargadas. Se tarda unos minutos en correr.
Loaded Images: 8091
Funciones para pre-calcular fotos
Es posible utilizar un modelo previamente entrenado para extraer las características de las fotos en el conjunto de datos y almacenar las características para archivar. Esta es una eficiencia que significa que la parte del lenguaje del modelo que convierte las características extraídas de la foto en descripciones textuales puede ser entrenada independientemente del modelo de extracción de características. La ventaja es que los modelos preentrenados muy grandes no necesitan ser cargados, guardados en la memoria y utilizados para procesar cada foto mientras se entrena el modelo del idioma.
Más tarde, el modelo de extracción de características y el modelo de lenguaje se pueden volver a unir para hacer predicciones sobre nuevas fotos. En esta sección, ampliaremos el comportamiento de carga de fotos desarrollado en la sección anterior para cargar todas las fotos, extraer sus características utilizando un modelo VGG previamente entrenado, y almacenar las características extraídas en un nuevo archivo que puede ser cargado y utilizado para entrenar el modelo del idioma. El primer paso es cargar el modelo VGG. Este modelo se suministra directamente en Keras y se puede cargar de la siguiente manera. Ten en cuenta que esto descargará los pesos de los modelos de 500 megabytes en tu ordenador, lo que puede tardar unos minutos.
from keras.applications.vgg16 import VGG16 # cargar el modelo in_layer = Input(shape=(224, 224, 3)) model = VGG16(include_top=False, input_tensor=in_layer, pooling='avg') model.summary()
Esto cargará el modelo VGG de 16 capas. Las dos capas de salida Dense y la capa de salida de clasificación se eliminan del modelo configurando include_top=False. La salida de la capa de pooling final se toma como las características extraídas de la imagen. A continuación, podemos pasar por encima de todas las imágenes del directorio de imágenes como en la sección anterior y llamar a la función predict() en el modelo de cada imagen preparada para obtener las características extraídas. Las características pueden ser almacenadas en un diccionario que se encuentra en el identificador de la imagen. El ejemplo completo se enumera a continuación.
from os import listdir from os import path from pickle import dump from keras.applications.vgg16 import VGG16 from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input from keras.layers import Input # extraer características de cada foto del directorio def extract_features(directory): # cargar el modelo in_layer = Input(shape=(224, 224, 3)) model = VGG16(include_top=False, input_tensor=in_layer) model.summary() # extraer características de cada foto features = dict() for name in listdir(directory): # cargar una imagen desde un archivo filename = path.join(directory, name) image = load_img(filename, target_size=(224, 224)) # convertir los píxeles de la imagen en una matriz entumecida image = img_to_array(image) # remodelar los datos para el modelo image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # preparar la imagen para el modelo VGG image = preprocess_input(image) # obtenga características feature = model.predict(image, verbose=0) # obtener identificación de la imagen image_id = name.split('.')[0] # función de tienda features[image_id] = feature print('>%s'% name) return features # extraer características de todas las imágenes directory ='Flicker8k_Dataset' features = extract_features(directory) print('Extracted Features: %d'% len(features)) # guardar archivo dump(features, open('features.pkl','wb'))
El ejemplo puede tomar algún tiempo para completarse, tal vez una hora. Después de extraer todas las características, el diccionario se almacena en el archivo features.pkl en el directorio de trabajo actual. Estas características pueden ser cargadas más tarde y utilizadas como entrada para el entrenamiento de un modelo de lenguaje. Usted podría experimentar con otros tipos de modelos pre-entrenados en Keras.
Cómo cargar descripciones
Es importante tomarse un momento para hablar de las descripciones; hay un número disponible. El archivo Flickr8k.token.txt contiene una lista de identificadores de imagen (utilizados en los nombres de archivo de imagen) y descripciones tokenizadas. Cada imagen tiene múltiples descripciones. A continuación se muestra una muestra de las descripciones del archivo que muestra 5 descripciones diferentes para una sola imagen.
1305564994_00513f9a5b.jpg#0 A man in street racer armor be examine the tire of anotherracer's motorbike . 1305564994_00513f9a5b.jpg#1 Two racer drive a white bike down a road . 1305564994_00513f9a5b.jpg#2 Two motorist be ride along on their vehicle that be oddlydesign and color . 1305564994_00513f9a5b.jpg#3 Two person be in a small race car drive by a green hill . 1305564994_00513f9a5b.jpg#4 Two person in race uniform in a street car .
El archivo ExpertAnnotations.txt indica cuáles de las descripciones de cada imagen fueron escritas por expertos que fueron escritos por trabajadores de fuente colectiva a los que se les pidió que describieran la imagen. Finalmente, el archivo CrowdFlowerAnnotations.txt proporciona la frecuencia de los trabajadores de multitudes que indican si los subtítulos se ajustan a cada imagen. Estas frecuencias pueden ser interpretadas probabilísticamente.
También hay listas de los identificadores de fotos para usar en una división de entrenamiento/prueba para que pueda comparar los resultados reportados en el documento. El primer paso es decidir qué subtítulos utilizar. El método más sencillo es utilizar la primera descripción para cada fotografía. Primero, necesitamos una función para cargar todo el archivo de anotaciones (Flickr8k.token.txt) en la memoria. Abajo hay una función para hacer esto llamada load_doc() que, dado un nombre de archivo, devolverá el documento como una cadena.
# carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text
Podemos ver en la muestra del archivo anterior que sólo necesitamos dividir cada línea por un espacio en blanco y tomar el primer elemento como identificador de la imagen y el resto como descripción de la imagen. Por ejemplo:
# línea dividida por espacio en blanco tokens = line.split() # toma el primer token como la identificación de la imagen, el resto como la descripción image_id, image_desc = tokens[0], tokens[1:]
También podemos poner los tokens de descripción de nuevo juntos en una cadena para su posterior procesamiento.
# convertir los tokens de descripción de nuevo a cadena de texto image_desc =''.join(image_desc)
Podemos poner todo esto junto en una función. Abajo se define la función load_descriptions() que tomará el archivo cargado, lo procesará línea por línea, y devolverá un diccionario de identificadores de imagen a su primera descripción.
# carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # extraer las descripciones de las imágenes def load_descriptions(doc): mapping = dict() # líneas de proceso for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() if len(line) < 2: continue # tomar el primer token como la identificación de la imagen, el resto como tokens de descripción de conversión de vuelta a la cadena image_id, image_desc = tokens[0], tokens[1:] # eliminar el nombre de archivo de la identificación de la imagen image_id = image_id.split('.')[0] # convertir tokens de descripción de nuevo a cadena image_desc =''.join(image_desc) # guardar la primera descripción de cada imagen if image_id not in mapping: mapping[image_id] = image_desc return mapping filename ='Flickr8k_text/Flickr8k.token.txt'doc = load_doc(filename) descriptions = load_descriptions(doc) print('Loaded: %d'% len(descriptions))
Al ejecutar el ejemplo se imprime el número de descripciones de imágenes cargadas.
Loaded: 8092
Hay otras maneras de cargar descripciones que pueden resultar más precisas para los datos. Utiliza el ejemplo anterior como punto de partida y hazme saber lo que se te ocurra.
Preparar Descripción Texto
Las descripciones son tokenizadas; esto significa que cada token está compuesto de palabras separadas por un espacio en blanco. También significa que la puntuación se separa como sus propios tokens, tales como puntos (‘.’) y apóstrofes para las palabras plurales (‘s). Es una buena idea limpiar el texto de descripción antes de utilizarlo en un modelo. Algunas ideas de limpieza de datos que podemos formar incluyen:
- Normalizando el caso de todos los tokens a minúsculas.
- Eliminar todos los signos de puntuación de los tokens.
- Eliminar todos los tokens que contengan uno o menos caracteres (después de eliminar la puntuación), por ejemplo, los caracteres ‘a’ y ‘s’ colgados.
Podemos implementar estas simples operaciones de limpieza en una función que limpia cada descripción en el diccionario cargado de la sección anterior. A continuación se define la función clean_descriptions() que limpiará cada descripción cargada.
# texto de descripción limpio def clean_descriptions(descriptions): # preparar a regex para el filtrado de caracteres re_punc = re.compile('[%s]'% re.escape(string.punctuation)) for key, desc in descriptions.items(): # tokenize desc = desc.split() #convertir a minúsculas desc = [word.lower() for word in desc] # quitar la puntuación de cada palabra desc = [re_punc.sub('', w) for w in desc] # Quitar la horca 's'and'a'desc = [word for word in desc if len(word)>1] # store como cadena descriptions[key] =''.join(desc)
Entonces podemos guardar el texto limpio en un archivo para su uso posterior por parte de nuestro modelo. Cada línea contendrá el identificador de la imagen seguido de la descripción limpia. A continuación se define la función save_doc() para guardar las descripciones limpias en un archivo.
# guardar las descripciones en un archivo, una por línea def save_doc(descriptions, filename): lines = list() for key, desc in mapping.items(): lines.append(key +''+ desc) data ='\n'.join(lines) file = open(filename,'w') file.write(data) file.close()
Poniendo todo esto junto con la carga de las descripciones de la sección anterior, el ejemplo completo se enumera a continuación.
import string import re # carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # extraer las descripciones de las imágenes def load_descriptions(doc): mapping = dict() # líneas de proceso for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() if len(line) < 2: continue # tome el primer token como la identificación de la imagen, el resto como image_id, image_desc = tokens[0], tokens[1:] # eliminar el nombre de archivo de la identificación de la imagen image_id = image_id.split('.')[0] # convertir tokens de descripción de nuevo a cadena image_desc =''.join(image_desc) # guardar la primera descripción de cada imagen if image_id not in mapping: mapping[image_id] = image_desc return mapping # texto de descripción limpio def clean_descriptions(descriptions): # preparar a regex para el filtrado de caracteres re_punc = re.compile('[%s]'% re.escape(string.punctuation)) for key, desc in descriptions.items(): # tokenize desc = desc.split() #convertir a minúsculas desc = [word.lower() for word in desc] # quitar la puntuación de cada palabra desc = [re_punc.sub('', w) for w in desc] # Quitar la horca 's'and'a'desc = [word for word in desc if len(word)>1] # store como cadena descriptions[key] =''.join(desc) # guardar descripciones en un archivo, una por línea def save_doc(descriptions, filename): lines = list() for key, desc in descriptions.items(): lines.append(key +''+ desc) data ='\n'.join(lines) file = open(filename,'w') file.write(data) file.close() filename ='Flickr8k_text/Flickr8k.token.txt' # descripciones de carga doc = load_doc(filename) # descripciones de análisis descriptions = load_descriptions(doc) print('Loaded: %d'% len(descriptions)) # descripciones limpias clean_descriptions(descriptions) # resumir el vocabulario all_tokens =''.join(descriptions.values()).split() vocabulary = set(all_tokens) print('Vocabulary Size: %d'% len(vocabulary)) # guardar descripciones save_doc(descriptions,'descriptions.txt')
Ejecutar el ejemplo primero carga 8.092 descripciones, las limpia, resume el vocabulario de 4.484 palabras únicas, y luego las guarda en un nuevo archivo llamado descriptions.txt.
Loaded: 8092 Vocabulary Size: 4484
Abre el nuevo archivo descriptions.txt en un editor de texto y revisa el contenido. Deberías ver descripciones algo legibles de fotos listas para modelar
... 3139118874_599b30b116 two girls pose for picture at christmastime 2065875490_a46b58c12b person is walking on sidewalk and skeleton is on the left inside of fence 2682382530_f9f8fd1e89 man in black shorts is stretching out his leg 3484019369_354e0b88c0 hockey team in red and white on the side of the ice rink 505955292_026f1489f2 boy rides horse
El vocabulario sigue siendo relativamente amplio. Para facilitar el modelado, especialmente la primera vez, yo recomendaría reducir aún más el vocabulario eliminando las palabras que sólo aparecen una o dos veces en todas las descripciones.
Modelo de Secuencia de Descripción Completa
Hay muchas maneras de modelar el problema de la generación de subtítulos. Una forma ingenua es crear un modelo que produzca la descripción textual completa de una sola vez. Este es un modelo ingenuo porque pone una pesada carga sobre el modelo tanto para interpretar el significado de la fotografía como para generar palabras, y luego ordenarlas en el orden correcto.
Esto no se diferencia del problema de traducción del idioma utilizado en una red neuronal recurrente de codificador-decodificador, en la que toda la frase traducida se emite una palabra a la vez, dada una codificación de la secuencia de entrada. Aquí usaríamos una codificación de la imagen para generar la sentencia de salida en su lugar. La imagen puede ser codificada usando un modelo pre-entrenado usado para la clasificación de imágenes, tal como el VGG entrenado en el modelo ImageNet mencionado anteriormente.
El resultado del modelo sería una distribución de probabilidad sobre cada palabra del vocabulario. La secuencia sería tan larga como la descripción de la foto más larga. Las descripciones, por lo tanto, tendrían que ser primero enteros codificados donde cada palabra en el vocabulario se asigna un número entero único y las secuencias de palabras se sustituyen por secuencias de números enteros. Las secuencias enteras tendrían que ser codificadas en caliente para representar la distribución de probabilidad idealizada sobre el vocabulario para cada palabra en la secuencia.
Podemos usar herramientas en Keras para preparar las descripciones de este tipo de modelos. El primer paso es cargar el mapeo de los identificadores de imagen para limpiar las descripciones almacenadas en descriptions.txt.
# carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar descripciones limpias en la memoria def load_clean_descriptions(filename): doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() # separar id de descripción image_id, image_desc = tokens[0], tokens[1:] # acumular mapping[image_id] =''.join(image_desc) return descriptions descriptions = load_clean_descriptions('descriptions.txt') print('Loaded %d'% (len(descriptions)))
Al ejecutar esta pieza se cargan las 8.092 descripciones de fotos en un diccionario tecleado en los identificadores de imagen. Estos identificadores se pueden utilizar para cargar cada archivo de fotos para las entradas correspondientes al modelo.
Loaded 8092
A continuación, necesitamos extraer todo el texto de descripción para poder codificarlo.
# extraer todo el texto desc_text = lista(descriptions.values())
Podemos usar la clase Keras Tokenizer para mapear consistentemente cada palabra en el vocabulario a un entero. En primer lugar, se crea el objeto y, a continuación, se ajusta al texto de descripción. El tokenizador de ajuste puede guardarse posteriormente en un archivo para decodificar de forma coherente las predicciones de vuelta a las palabras del vocabulario.
from keras.preprocessing.text import Tokenizer # prepara el tokenizer tokenizer = Tokenizer() tokenizer.fit_on_texts(desc_text) vocab_size = len(tokenizer.word_index) + 1 print('Vocabulary Size: %d'% vocab_size)
A continuación, podemos utilizar el tokenizador de ajuste para codificar las descripciones de las fotos en secuencias de números enteros.
# enteros codifican descripciones sequences = tokenizer.texts_to_sequences(desc_text)
El modelo requerirá que todas las secuencias de salida tengan la misma duración para el entrenamiento. Podemos conseguirlo rellenando todas las secuencias codificadas para que tengan la misma longitud que la secuencia codificada más larga. Podemos rellenar las secuencias con valores 0 después de la lista de palabras. Keras proporciona la función pad_sequences() para rellenar las secuencias.
from keras.preprocessing.sequence import pad_sequences # rellenar todas las secuencias con una longitud fija max_length = max(len(s) for s in sequences) print('Description Length: %d'% max_length) padded = pad_sequences(sequences, maxlen=max_length, padding='post')
Finalmente, podemos codificar en caliente las secuencias acolchadas para tener un vector disperso para cada palabra de la secuencia. Keras proporciona la función to_categorical() para realizar esta operación.
from keras.utils import to_categorical # una codificación caliente y = to_categorical(padded, num_classes=vocab_size)
Una vez codificados, podemos asegurarnos de que los datos de salida de la secuencia tengan la forma adecuada para el modelo.
y = y.reshape((len(descriptions), max_length, vocab_size)) print(y.shape)
Juntando todo esto, el ejemplo completo se enumera a continuación.
from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical # carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar descripciones limpias en la memoria def load_clean_descriptions(filename): doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() # separar id de descripción image_id, image_desc = tokens[0], tokens[1:] # acumular mapping[image_id] =''.join(image_desc) return descriptions descriptions = load_clean_descriptions('descriptions.txt') print('Loaded %d'% (len(descriptions))) # extraer todo el texto desc_text = list(descriptions.values()) # preparar tokenizer tokenizer = Tokenizer() tokenizer.fit_on_texts(desc_text) vocab_size = len(tokenizer.word_index) + 1 print('Vocabulary Size: %d'% vocab_size) # descripciones de codificación de números enteros sequences = tokenizer.texts_to_sequences(desc_text) # acolchar todas las secuencias a una longitud fija max_length = max(len(s) for s in sequences) print('Description Length: %d'% max_length) padded = pad_sequences(sequences, maxlen=max_length, padding='post') # una codificación en caliente y = to_categorical(padded, num_classes=vocab_size) y = y.reshape((len(descriptions), max_length, vocab_size)) print(y.shape)
Al ejecutar el ejemplo, primero se imprime el número de descripciones de imágenes cargadas (8.092 fotos), el tamaño del vocabulario del conjunto de datos (4.485 palabras), la longitud de la descripción más larga (28 palabras) y, finalmente, la forma de los datos para ajustar un modelo de predicción en el formulario[muestras, longitud de la secuencia, características].
Loaded 8092 Vocabulary Size: 4485 Description Length: 28 (8092, 28, 4485)
Como ya se ha mencionado, la salida de la secuencia completa puede ser un reto para el modelo. En la siguiente sección veremos un modelo más simple.
Modelo palabra por palabra
Un modelo más simple para generar un pie de foto es generar una palabra dada la imagen como entrada y la última palabra generada. Este modelo tendría que ser llamado recursivamente para generar cada palabra de la descripción con predicciones previas como entrada. Usando la palabra como entrada, dale al modelo un contexto forzado para predecir la siguiente palabra en la secuencia.
Este es el modelo utilizado en investigaciones anteriores, tales como: Show and Tell: A Neural Image Caption Generator, 2015. Se puede utilizar una capa de incrustación de palabras para representar las palabras de entrada. Al igual que el modelo de extracción de características para las fotos, este también puede ser pre-entrenado ya sea en un corpus grande o en el conjunto de datos de todas las descripciones.
El modelo tomaría una secuencia completa de palabras como entrada; la longitud de la secuencia sería la longitud máxima de las descripciones en el conjunto de datos. El modelo debe empezar con algo. Un enfoque es rodear cada descripción de la foto con etiquetas especiales para señalar el inicio y el final de la descripción, como STARTDESC y ENDDESC. Por ejemplo, la descripción:
boy rides horse
se convertiría en:
STARTDESC boy rides horse ENDDESC
Y se alimentaría al modelo con la misma entrada de imagen para obtener los siguientes pares de secuencia de palabras de entrada-salida:
Input (X), Output (y) STARTDESC, boy STARTDESC, boy, rides STARTDESC, boy, rides, horse STARTDESC, boy, rides, horse ENDDESC
La preparación de los datos comenzaría de la misma manera que se describió en la sección anterior. Cada descripción debe estar codificada en números enteros. Después de la codificación, las secuencias se dividen en múltiples pares de entrada y salida y sólo la palabra de salida (y) está codificada en caliente. Esto se debe a que el modelo sólo se requiere para predecir la distribución de probabilidad de una palabra a la vez. El código es el mismo hasta el punto en que se calcula la longitud máxima de las secuencias.
... descriptions = load_clean_descriptions('descriptions.txt') print('Loaded %d'% (len(descriptions))) # extraer todo el texto desc_text = list(descriptions.values()) # prepara el tokenizer tokenizer = Tokenizer() tokenizer.fit_on_texts(desc_text) vocab_size = len(tokenizer.word_index) + 1 print('Vocabulary Size: %d'% vocab_size) # enteros codifican descripciones sequences = tokenizer.texts_to_sequences(desc_text) #determinar la longitud máxima de la secuencia max_length = max(len(s) for s in sequences) print('Description Length: %d'% max_length)
A continuación, dividimos la secuencia codificada de cada número entero en pares de entrada y salida. Pasemos a través de una sola secuencia llamada seq en la palabra i’th de la secuencia, donde i es mayor o igual a 1. Primero, tomamos las primeras palabras i-1 como la secuencia de entrada y la palabra i ‘th como la palabra de salida.
# dividido en par de entrada y salida in_seq, out_seq = seq[:i], seq[i]
A continuación, la secuencia de entrada se rellena hasta la longitud máxima de las secuencias de entrada. Se utiliza el relleno previo (por defecto) para que aparezcan nuevas palabras al final de la secuencia, en lugar del comienzo de la entrada.
# secuencia de entrada del pad in_seq = pad_sequences([in_seq], maxlen=max_length)[0]
La palabra de salida es una codificada en caliente, como en la sección anterior.
# codificar secuencia de salida out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
Podemos juntar todo esto en un ejemplo completo para preparar los datos de descripción para el modelo palabra por palabra.
from numpy import array from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical # carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar descripciones limpias en la memoria def load_clean_descriptions(filename): doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() # separar id de descripción image_id, image_desc = tokens[0], tokens[1:] # acumular mapping[image_id] =''.join(image_desc) return descriptions descriptions = load_clean_descriptions('descriptions.txt') print('Loaded %d'% (len(descriptions))) # extraer todo el texto desc_text = list(descriptions.values()) # prepara el tokenizer tokenizer = Tokenizer() tokenizer.fit_on_texts(desc_text) vocab_size = len(tokenizer.word_index) + 1 print('Vocabulary Size: %d'% vocab_size) # enteros codifican descripciones sequences = tokenizer.texts_to_sequences(desc_text) #determinar la longitud máxima de la secuencia max_length = max(len(s) for s in sequences) print('Description Length: %d'% max_length) X, y = list(), list() for img_no, seq in enumerate(sequences): # dividir una secuencia en múltiples pares X,y for i in range(1, len(seq)): # dividido en pares de entrada y salida in_seq, out_seq = seq[:i], seq[i] # secuencia de entrada del pad in_seq = pad_sequences([in_seq], maxlen=max_length)[0] # codificar la secuencia de salida out_seq = to_categorical([out_seq], num_classes=vocab_size)[0] # acumular X.append(in_seq) y.append(out_seq) # convertir a arreglos numéricos X, y = array(X), array(y) print(X.shape) print(y.shape)
Al ejecutar el ejemplo se imprimen las mismas estadísticas, pero se imprime el tamaño de las secuencias de entrada y salida codificadas resultantes. Ten en cuenta que la entrada de imágenes debe seguir exactamente el mismo orden en el que se muestra la misma foto para cada ejemplo extraído de una sola descripción. Una forma de hacerlo sería cargar la foto y almacenarla para cada ejemplo preparado a partir de una sola descripción.
Loaded 8092 Vocabulary Size: 4485 Description Length: 28 (66456, 28) (66456, 4485)
Carga progresiva
El conjunto de datos Flicr8K de fotos y descripciones puede caber en la RAM, si tiene mucha RAM (por ejemplo, 8 Gigabytes o más), y la mayoría de los sistemas modernos sí. Esto está bien si desea ajustar un modelo de Deep Learning utilizando la CPU. Alternativamente, si quieres ajustar un modelo utilizando una GPU, entonces no podrás ajustar los datos en la memoria de una tarjeta de vídeo de GPU media. Una solución es cargar progresivamente las fotos y descripciones según las necesidades del modelo.
Keras soporta conjuntos de datos cargados progresivamente usando la función fit_generator() en el modelo. Un generador es el término utilizado para describir una función utilizada para devolver lotes de muestras para el modelo a entrenar. Esto puede ser tan simple como una función independiente, cuyo nombre se pasa a la función fit_generator() cuando se ajusta el modelo. Como recordatorio, un modelo es apto para múltiples épocas, donde una época es un paso a través de todo el conjunto de datos de formación, como todas las fotos. Una época se compone de varios lotes de ejemplos en los que los pesos de los modelos se actualizan al final de cada lote.
Un generador debe crear y producir un lote de ejemplos. Por ejemplo, la longitud media de la oración en el conjunto de datos es de 11 palabras; eso significa que cada foto dará como resultado 11 ejemplos para ajustar el modelo y dos fotos darán como resultado alrededor de 22 ejemplos en promedio. Un buen tamaño de lote predeterminado para el hardware moderno puede ser de 32 ejemplos, lo que equivale a 2-3 fotos de ejemplos.
Podemos escribir un generador personalizado para cargar algunas fotos y devolver las muestras como un solo lote. Supongamos que estamos trabajando con un modelo word by word descrito en la sección anterior que espera una secuencia de palabras y una imagen preparada como entrada y predice una sola palabra. Vamos a diseñar un generador de datos que da un diccionario cargado de identificadores de imagen para limpiar las descripciones, un tokenizer entrenado, y una longitud máxima de secuencia que cargará un valor de imagen de ejemplos para cada lote.
Un generador debe hacer un bucle para siempre y rendir cada lote de muestras. Podemos hacer un bucle para siempre con un bucle while y dentro de éste, hacer un bucle sobre cada imagen en el directorio de imágenes. Para cada nombre de archivo de imagen, podemos cargar la imagen y crear todos los pares de secuencias de entrada y salida a partir de la descripción de la imagen. A continuación se muestra la función de generador de datos.
def data_generator(mapping, tokenizer, max_length): # bucle para siempre sobre imágenes directory ='Flicker8k_Dataset' while 1: for name in listdir(directory): # cargar una imagen desde un archivo filename = directory +'/' +name image, image_id = load_image(filename) # crear secuencias de palabras desc = mapping[image_id] in_img, in_seq, out_word = create_sequences(tokenizer, max_length, desc, image) yield [[in_img, in_seq], out_word]
Tú podrías extenderlo para tomar el nombre del directorio del dataset como parámetro. El generador devuelve un array que contiene las entradas (X) y la salida (Y) del modelo. La entrada se compone de una matriz con dos elementos para las imágenes de entrada y secuencias de palabras codificadas. Las salidas son una palabra codificada en caliente. Puedes ver que llama a una función llamada load_photo() para cargar una sola foto y devolver los píxeles y el identificador de la imagen. Esta es una versión simplificada de la función de carga de fotos desarrollada al principio de este tutorial.
# cargar una sola foto como entrada para el modelo de extractor de características VGG def load_photo(filename): image = load_img(filename, target_size=(224, 224)) # convierte los píxeles de la imagen en una matriz NumPy image = img_to_array(image) # remodelar los datos para el modelo image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # preparar la imagen para el modelo VGG image = preprocess_input(image)[0] # obtener identificación de la imagen image_id = filename.split('/')[-1].split('.')[0] return image, image_id
Otra función llamada create_sequences() es usada para crear secuencias de imágenes, secuencias de entrada de palabras, y palabras de salida que luego cedemos a la parte del código que llama. Esta es una función que incluye todo lo discutido en la sección anterior, y también crea copias de los píxeles de la imagen, una para cada par entrada-salida creado a partir de la descripción de la foto.
# crear secuencias de imágenes, secuencias de entrada y palabras de salida para una imagen def create_sequences(tokenizer, max_length, descriptions, images): Ximages, XSeq, y = list(), list(),list() vocab_size = len(tokenizer.word_index) + 1 for j in range(len(descriptions)): seq = descriptions[j] image = images[j] # entero codificar seq = tokenizer.texts_to_sequences([seq])[0] # Dividir una secuencia en múltiples pares X,Y for i in range(1, len(seq)): # seleccionar in_seq, out_seq = seq[:i], seq[i] # secuencia de entrada del pad in_seq = pad_sequences([in_seq], maxlen=max_length)[0] # codificar secuencia de salida out_seq = to_categorical([out_seq], num_classes=vocab_size)[0] # tienda Ximages.append(image) XSeq.append(in_seq) y.append(out_seq) Ximages, XSeq, y = array(Ximages), array(XSeq), array(y) return Ximages, XSeq, y
Antes de preparar el modelo que utiliza el generador de datos, debemos cargar las descripciones limpias, preparar el tokenizer y calcular la longitud máxima de la secuencia. Los tres deben ser pasados al data_generator() como parámetros. Utilizamos la misma función load_clean_description() desarrollada anteriormente y una nueva función create_tokenizer() que simplifica la creación del tokenizer. Enlazando todo esto, el generador de datos completo se enumera a continuación, listo para ser utilizado para entrenar a un modelo.
from os import listdir from os import path from numpy import array from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical from keras.preprocessing.image import load_img from keras.preprocessing.image import img_to_array from keras.applications.vgg16 import preprocess_input # carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename,'r') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # cargar descripciones limpias en la memoria def load_clean_descriptions(filename): doc = load_doc(filename) descriptions = dict() for line in doc.split('\n'): # línea divisoria por espacio en blanco tokens = line.split() # separar id de descripción image_id, image_desc = tokens[0], tokens[1:] # acumular mapping[image_id] =''.join(image_desc) return descriptions # ajustar las descripciones de los subtítulos de un tokenizador def create_tokenizer(descriptions): lines = list(descriptions.values()) tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer # cargar una sola foto como entrada para el modelo de extractor de características VGG def load_photo(filename): image = load_img(filename, target_size=(224, 224)) # convertir los píxeles de la imagen en una matriz entumecida image = img_to_array(image) # remodelar los datos para el modelo image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2])) # preparar la imagen para el modelo VGG image = preprocess_input(image)[0] # obtener identificación de la imagen image_id = path.basename(filename).split('.')[0] return image, image_id # crear secuencias de imágenes, secuencias de entrada y palabras de salida para una imagen def create_sequences(tokenizer, max_length, descriptions, images): Ximages, XSeq, y = list(), list(),list() vocab_size = len(tokenizer.word_index) + 1 for j in range(len(descriptions)): seq = descriptions[j] image = images[j] # entero codificar seq = tokenizer.texts_to_sequences([seq])[0] # Dividir una secuencia en múltiples pares X,Y for i in range(1, len(seq)): # seleccionar in_seq, out_seq = seq[:i], seq[i] # secuencia de entrada del pad in_seq = pad_sequences([in_seq], maxlen=max_length)[0] # codificar secuencia de salida out_seq = to_categorical([out_seq], num_classes=vocab_size)[0] # tienda Ximages.append(image) XSeq.append(in_seq) y.append(out_seq) Ximages, XSeq, y = array(Ximages), array(XSeq), array(y) return [Ximages, XSeq, y] # generador de datos, destinado a ser usado en una llamada a model.fit_generator() def data_generator(mapping, tokenizer, max_length): # bucle para siempre sobre imágenes directory ='Flicker8k_Dataset' while 1: for name in listdir(directory): # cargar una imagen desde un archivo filename = directory +'/' +name image, image_id = load_image(filename) # crear secuencias de palabras desc = mapping[image_id] in_img, in_seq, out_word = create_sequences(tokenizer, max_length, desc, image) yield [[in_img, in_seq], out_word] # mapeo de carga de ids a las descripciones descriptions = load_clean_descriptions('descriptions.txt') # enteros codifican secuencias de palabras tokenizer = create_tokenizer(descriptions) # almohadilla de longitud fija max_length = max(len(s.split()) for s in list(descriptions.values())) print('Description Length: %d'% max_length) # probar el generador de datos generator = data_generator(descriptions, tokenizer, max_length) inputs, outputs = next(generator) print(inputs[0].shape) print(inputs[1].shape) print(outputs.shape)
Se puede probar un generador de datos llamando a la función next(). Podemos probar el generador como sigue.
# probar el generador de datos generator = data_generator(descriptions, tokenizer, max_length) inputs, outputs = next(generator) print(inputs[0].shape) print(inputs[1].shape) print(outputs.shape)
Al ejecutar el ejemplo se imprime la forma del ejemplo de entrada y salida para un solo lote (por ejemplo, 13 pares de entrada y salida):
(13, 224, 224, 3) (13, 28) (13, 4485)
El generador puede ser usado para ajustar un modelo llamando a la función fit_generator() en el modelo (en lugar de fit()) y pasando el generador. También debemos especificar el número de pasos o lotes por época. Podríamos estimar esto como (10 x tamaño del conjunto de datos de formación), quizás 70.000 si se utilizan 7.000 imágenes para la formación.
# definir modelo # ... # modelo adecuado model.fit_generator(data_generator(descriptions, tokenizer, max_length), steps_per_epoch=70000, ...)
➡ Continúa aprendiendo de Procesamiento de Lenguaje Natural en nuestro curso: