Blog
Proyecto: Desarrollar un modelo de traducción automática neuronal
- Publicado por: Rafael Fernandez
- Categoría: Uncategorized
La traducción automática es una tarea desafiante que tradicionalmente involucra grandes modelos estadísticos desarrollados utilizando conocimientos lingüísticos altamente sofisticados. La traducción neuronal automática es el uso de redes neuronales profundas para el problema de la traducción automática. En este tutorial, descubrirás cómo desarrollar un sistema de traducción automática neural para traducir frases alemanas al inglés. Después de completar este tutorial, tú sabrás:
- Cómo limpiar y preparar los datos para entrenar un sistema de traducción neural automática.
- Cómo desarrollar un modelo de codificador-decodificador para la traducción automática.
- Cómo usar un modelo entrenado para inferir nuevas frases de entrada y evaluar la habilidad del modelo.
Vamos a empezar.
Resumen del Tutorial
Este tutorial se divide en las siguientes partes:
- Conjunto de datos de traducción del alemán al inglés
- Preparación de los datos de texto
- Modelo de traducción neuronal de entrenamientos
- Evaluar el modelo de traducción neuronal
Conjunto de datos de traducción del alemán al inglés
En este tutorial, utilizaremos un conjunto de datos de términos del alemán al inglés que se utilizan como base para los tokens de aprendizaje de idiomas. El conjunto de datos está disponible en el sitio web ManyThings.org, con ejemplos extraídos del Proyecto Tatoeba. El conjunto de datos está compuesto por frases en alemán y sus contrapartes en inglés y está destinado a ser utilizado con el software Anki flashcard.
Descargar el conjunto de datos de los pares inglés-alemán.
http://www.manythings.org/anki/deu-eng.zip
Descarga el conjunto de datos a su directorio de trabajo actual y descomprímalo, por ejemplo:
unzip deu-eng.zip
Tendrás un archivo llamado deu.txt que contiene 152.820 pares de fases de inglés a alemán, un par por línea con una pestaña que separa el idioma. Por ejemplo, las primeras 5 líneas del archivo tienen el siguiente aspecto:
Hi. Hallo! Hi. GruB Gott! Run! Lauf! Wow! Potzdonner! Wow! Donnerwetter!
Enmarcaremos el problema de la predicción como si se tratara de una secuencia de palabras en alemán como entrada, traducción o predicción de la secuencia de palabras en inglés. El modelo que desarrollaremos será adecuado para algunas frases en alemán para principiantes.
Preparación de los datos de texto
El siguiente paso es preparar los datos de texto para el modelado. Echa un vistazo a los datos en bruto y observa lo que ves que podríamos necesitar manejar en una operación de limpieza de datos. Por ejemplo, he aquí algunas observaciones que observo al revisar los datos brutos:
- Hay puntuación.
- El texto contiene mayúsculas y minúsculas.
- Hay caracteres especiales en el alemán.
- Hay frases duplicadas en inglés con diferentes traducciones al alemán.
El archivo está ordenado por la longitud de la oración con oraciones muy largas hacia el final del archivo. Un buen procedimiento de limpieza de texto puede manejar algunas o todas estas observaciones. La preparación de los datos se divide en dos subsecciones:
- Texto limpio
- Dividir texto
Texto Limpio
En primer lugar, debemos cargar los datos de forma que se conserven los caracteres alemanes Unicode. La siguiente función llamada load_doc() cargará el archivo como una nota de texto.
# carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename, mode='rt', encoding='utf-8') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text
Cada línea contiene un único par de frases, primero en inglés y luego en alemán, separadas por un tabulador. Tenemos que dividir el texto cargado por línea y luego por frase. La función to_pairs() abajo dividirá el texto cargado.
# dividir un documento cargado en oraciones def to_pairs(doc): lines = doc.strip().split('\n') pairs = [line.split('\t') for line in lines] return pairs
Ahora estamos listos para limpiar cada frase. Las operaciones de limpieza específicas que realizaremos son las siguientes:
- Eliminaremos todos los caracteres no imprimibles.
- Eliminaremos todos los caracteres de puntuación.
- Normalizaremos todos los caracteres Unicode a ASCII (por ejemplo, caracteres latinos).
- Normalizaremos el caso a minúsculas.
- Eliminaremos los tokens restantes que no estén en orden alfabético.
Realizaremos estas operaciones en cada frase para cada par del conjunto de datos cargado. La función clean_pairs() que se encuentra debajo implementa estas operaciones.
# limpiar una lista de líneas def clean_pairs(lines): cleaned = list() # preparar a regex para el filtrado de caracteres re_punc = re.compile('[%s]'% re.escape(string.punctuation)) re_print = re.compile('[^%s]'% re.escape(string.printable)) for pair in lines: clean_pair = list() for line in pair: # normalizar los caracteres unicode line = normalize('NFD', line).encode('ascii','ignore') line = line.decode('UTF-8') # tokenize en espacio en blanco line = line.split() # convertir a minúsculas line = [word.lower() for word in line] # eliminar la puntuación de cada token line = [re_punc.sub('', w) for w in line] # eliminar caracteres no imprimibles de cada token line = [re_print.sub('', w) for w in line] # eliminar tokens con números en ellos line = [word for word in line if word.isalpha()] # store como cadena clean_pair.append(''.join(line)) cleaned.append(clean_pair) return array(cleaned)
Finalmente, ahora que los datos han sido limpiados, podemos guardar la lista de pares de frases en un archivo listo para su uso. La función save_clean_data() utiliza la API de Pickle para guardar la lista de texto limpio en un archivo. Juntando todo esto, el ejemplo completo se muestra a continuación.
import string import re from pickle import dump from unicodedata import normalize from numpy import array # carga doc en la memoria def load_doc(filename): # abrir el archivo como de sólo lectura file = open(filename, mode='rt', encoding='utf-8') # leer todo el texto text = file.read() # cerrar el archivo file.close() return text # dividir un documento cargado en oraciones def to_pairs(doc): lines = doc.strip().split('\n') pairs = [line.split('\t') for line in lines] return pairs # limpiar una lista de líneas def clean_pairs(lines): cleaned = list() # preparar a regex para el filtrado de caracteres re_punc = re.compile('[%s]'% re.escape(string.punctuation)) re_print = re.compile('[^%s]'% re.escape(string.printable)) for pair in lines: clean_pair = list() for line in pair: # normalizar los caracteres unicode line = normalize('NFD', line).encode('ascii','ignore') line = line.decode('UTF-8') # tokenize en espacio en blanco line = line.split() # convertir a minúsculas line = [word.lower() for word in line] # eliminar la puntuación de cada token line = [re_punc.sub('', w) for w in line] # eliminar caracteres no imprimibles de cada token line = [re_print.sub('', w) for w in line] # eliminar tokens con números en ellos line = [word for word in line if word.isalpha()] # store como cadena clean_pair.append(''.join(line)) cleaned.append(clean_pair) return array(cleaned) # guardar una lista de oraciones limpias para archivar def save_clean_data(sentences, filename): dump(sentences, open(filename,'wb')) print('Saved: %s'% filename) # cargar conjunto de datos filename ='deu.txt'doc = load_doc(filename) # dividido en parejas de inglés-alemán pairs = to_pairs(doc) # oraciones limpias clean_pairs = clean_pairs(pairs) # guardar pares limpios en un archivo save_clean_data(clean_pairs,'english-german.pkl') # control al azar for i in range(100): print('[%s] => [%s]'% (clean_pairs[i,0], clean_pairs[i,1]))
Al ejecutar el ejemplo se crea un nuevo archivo en el directorio de trabajo actual con el texto limpio llamado en inglés-alemán.pkl. Algunos ejemplos del texto limpio se imprimen para que los evaluemos al final de la ejecución para confirmar que las operaciones limpias se realizaron como se esperaba.
Dividir texto
Los datos limpios contienen un poco más de 150.000 pares de frases y algunos de los pares hacia el final del archivo son muy largos. Este es un buen número de ejemplos para desarrollar un pequeño modelo de traducción. La complejidad del modelo aumenta con el número de ejemplos, igual que con la longitud de las frases y el tamaño del vocabulario. Aunque tenemos un buen conjunto de datos para la traducción de modelos, simplificaremos el problema ligeramente para reducir drásticamente el tamaño del modelo requerido y, a su vez, el tiempo de formación necesario para adaptarlo al modelo.
Puedes explorar el desarrollo de un modelo en el conjunto de datos más completo como una extensión; me encantaría saber cómo lo haces. Simplificaremos el problema reduciendo el conjunto de datos a los primeros 10.000 ejemplos del archivo; estas serán las frases más cortas del conjunto de datos. Más adelante, pondremos en juego los primeros 9.000 de ellos como ejemplos para la formación y los 1.000 ejemplos restantes para probar el modelo de ajuste.
A continuación se muestra el ejemplo completo de cómo cargar los datos limpios, dividirlos y guardar las partes divididas de los datos en nuevos archivos.
from pickle import load from pickle import dump from numpy.random import shuffle # cargar un conjunto de datos limpio def load_clean_sentences(filename): return load(open(filename,'rb')) # guardar una lista de oraciones limpias para archivar def save_clean_data(sentences, filename): dump(sentences, open(filename,'wb')) print('Saved: %s'% filename) # conjunto de datos de carga raw_dataset = load_clean_sentences('english-german.pkl') # reducir el tamaño del conjunto de datos n_sentences = 10000 dataset = raw_dataset[:n_sentences, :] # orden aleatorio shuffle(dataset) # dividido en entrenamiento/prueba train, test = dataset[:9000], dataset[9000:] # guardar save_clean_data(dataset,'english-german-both.pkl') save_clean_data(train,'english-german-train.pkl') save_clean_data(test,'english-german-test.pkl')
Al ejecutar el ejemplo se crean tres nuevos archivos: el archivo english-german-both.pkl, que contiene todos los ejemplos de entrenamiento y de prueba que podemos usar para definir los parámetros del problema, como la longitud máxima de las frases y el vocabulario, y los archivos english-german-train.pkl y english-german-test.pkl del entrenamiento y del conjunto de datos de prueba. Ahora estamos listos para empezar a desarrollar nuestro modelo de traducción.
Modelo de traducción neural de entrenamiento
En esta sección desarrollaremos el modelo de traducción. Esto implica cargar y preparar los datos de texto limpio listos para modelar, definir, y entrenar el modelo sobre los datos preparados. Comencemos por cargar los conjuntos de datos para poder preparar los datos. La siguiente función denominada load_clean_sentences() puede utilizarse para cargar el entrenamiento, la prueba y ambos conjuntos de datos a su vez
# cargar un conjunto de datos limpio def load_clean_sentences(filename): return load(open(filename,'rb')) # cargar conjuntos de datos dataset = load_clean_sentences('english-german-both.pkl') train = load_clean_sentences('english-german-train.pkl') test = load_clean_sentences('english-german-test.pkl')
Usaremos ambos o la combinación de los conjuntos de datos del entrenamiento y de la prueba para definir la longitud y el vocabulario máximos del problema. Esto es por simplicidad. Alternativamente, podríamos definir estas propiedades a partir del conjunto de datos de entrenamiento y truncar ejemplos en el conjunto de pruebas que sean demasiado largos o que tengan palabras que estén fuera del vocabulario. Podemos usar la clase Keras Tokenize para mapear palabras a números enteros, según sea necesario para el modelado. Usaremos un tokenizador separado para las secuencias en inglés y en alemán. La siguiente función create_tokenizer() entrenará un tokenizer en una lista de frases.
# caben en un tokenizador def create_tokenizer(lines): tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer
De manera similar, la función denominada max_length() encontrará la longitud de la secuencia más larga en una lista de frases.
# duración máxima de la frase def max_length(lines): return max(len(line.split()) for line in lines)
Podemos llamar a estas funciones con el conjunto de datos combinado para preparar tokenizadores, tamaños de vocabulario y longitudes máximas para las frases en inglés y alemán.
# prepara el tokenizer en inglés eng_tokenizer = create_tokenizer(dataset[:, 0]) eng_vocab_size = len(eng_tokenizer.word_index) + 1 eng_length = max_length(dataset[:, 0]) print('English Vocabulary Size: %d'% eng_vocab_size) print('English Max Length: %d'% (eng_length)) # Prepara el tokenizador alemán ger_tokenizer = create_tokenizer(dataset[:, 1]) ger_vocab_size = len(ger_tokenizer.word_index) + 1 ger_length = max_length(dataset[:, 1]) print('German Vocabulary Size: %d'% ger_vocab_size) print('German Max Length: %d'% (ger_length))
Ahora estamos listos para preparar el conjunto de datos de formación. Cada secuencia de entrada y salida debe codificarse en números enteros y rellenarse hasta la longitud máxima de frase. Esto se debe a que usaremos una incrustación de palabras para las secuencias de entrada y una codificación en caliente de las secuencias de salida. La función a continuación denominada encode_sequences() realizará estas operaciones y devolverá el resultado.
# codificar y secuencias de pads def encode_sequences(tokenizer, length, lines): # secuencias enteras de codificación X = tokenizer.texts_to_sequences(lines) # secuencias de pads con valores 0 X = pad_sequences(X, maxlen=length, padding='post') return X
La secuencia de salida debe estar codificada en caliente. Esto se debe a que el modelo predecirá la probabilidad de que cada palabra del vocabulario se convierta en salida. La función encode_output() a continuación codificará en caliente las secuencias de salida en inglés.
# una secuencia de destino codificada en caliente def encode_output(sequences, vocab_size): ylist = list() for sequence in sequences: encoded = to_categorical(sequence, num_classes=vocab_size) ylist.append(encoded) y = array(ylist) y = y.reshape(sequences.shape[0], sequences.shape[1], vocab_size) return y
Podemos hacer uso de estas dos funciones y preparar tanto el entrenamiento como el conjunto de datos de prueba para el entrenamiento del modelo.
# preparar los datos de la formación trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1]) trainY = encode_sequences(eng_tokenizer, eng_length, train[:, 0]) trainY = encode_output(trainY, eng_vocab_size) # preparar los datos de validación testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1]) testY = encode_sequences(eng_tokenizer, eng_length, test[:, 0]) testY = encode_output(testY, eng_vocab_size)
Ahora estamos listos para definir el modelo. Usaremos un modelo de codificador-decodificador LSTM para este problema. En esta arquitectura, la secuencia de entrada es codificada por un modelo de front-end llamado el codificador y luego decodificada palabra por palabra por un modelo de back-end llamado el decodificador. La función define_model() a continuación define el modelo y toma un número de argumentos usados para configurar el modelo, tales como el tamaño de los vocabularios de entrada y salida, la longitud máxima de las frases de entrada y salida, y el número de unidades de memoria usadas para configurar el modelo.
El modelo está entrenado usando el eficiente enfoque de Adam para el descenso estocástico por gradiente y minimiza la función de pérdida categórica porque hemos enmarcado el problema de predicción como una clasificación multi-clase. La configuración del modelo no fue optimizada para este problema, lo que significa que hay muchas oportunidades para que puedas afinarlo y elevar la habilidad de las traducciones. Me encantaría ver qué se te ocurre.
# definir el modelo NMT def define_model(src_vocab, tar_vocab, src_timesteps, tar_timesteps, n_units): model = Sequential() model.add(Embedding(src_vocab, n_units, input_length=src_timesteps, mask_zero=True)) model.add(LSTM(n_units)) model.add(RepeatVector(tar_timesteps)) model.add(LSTM(n_units, return_sequences=True)) model.add(TimeDistributed(Dense(tar_vocab, activation='softmax'))) # compilar modelo model.compile(optimizer='adam', loss='categorical_crossentropy') # resumir el modelo definido model.summary() plot_model(model, to_file='model.png', show_shapes=True) return model
Finalmente, podemos entrenar al modelo. Entrenamos el modelo para 30 épocas y un tamaño de lote de 64 ejemplos. Utilizamos el checkpointing para asegurarnos de que cada vez que la habilidad del modelo en el conjunto de pruebas mejora, el modelo se guarda en un archivo.
# modelo de ajuste checkpoint = ModelCheckpoint('model.h5', monitor='val_loss', verbose=1,save_best_only=True, mode='min') model.fit(trainX, trainY, epochs=30, batch_size=64, validation_data=(testX, testY),callbacks=[checkpoint], verbose=2)
Podemos unir todo esto y ajustar el modelo de traducción neuronal. El ejemplo de trabajo completo se muestra a continuación.
from pickle import load 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.utils.vis_utils import plot_model from keras.models import Sequential from keras.layers import LSTM from keras.layers import Dense from keras.layers import Embedding from keras.layers import RepeatVector from keras.layers import TimeDistributed from keras.callbacks import ModelCheckpoint # cargar un conjunto de datos limpio def load_clean_sentences(filename): return load(open(filename,'rb')) # instalar un tokenizador def create_tokenizer(lines): tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer # duración máxima de la frase def max_length(lines): return max(len(line.split()) for line in lines) # codificar y secuencias de pads def encode_sequences(tokenizer, length, lines): # secuencias de codificación de números enteros X = tokenizer.texts_to_sequences(lines) # secuencias de pads con valores 0 X = pad_sequences(X, maxlen=length, padding='post') return X # una secuencia de destino codificada en caliente def encode_output(sequences, vocab_size): ylist = list() for sequence in sequences: encoded = to_categorical(sequence, num_classes=vocab_size) ylist.append(encoded) y = array(ylist) y = y.reshape(sequences.shape[0], sequences.shape[1], vocab_size) return y # definir el modelo NMT def define_model(src_vocab, tar_vocab, src_timesteps, tar_timesteps, n_units): model = Sequential() model.add(Embedding(src_vocab, n_units, input_length=src_timesteps, mask_zero=True)) model.add(LSTM(n_units)) model.add(RepeatVector(tar_timesteps)) model.add(LSTM(n_units, return_sequences=True)) model.add(TimeDistributed(Dense(tar_vocab, activation='softmax'))) # modelo de compilación model.compile(optimizer='adam', loss='categorical_crossentropy') # resumir el modelo definido model.summary() plot_model(model, to_file='model.png', show_shapes=True) return model # conjuntos de datos de carga dataset = load_clean_sentences('english-german-both.pkl') train = load_clean_sentences('english-german-train.pkl') test = load_clean_sentences('english-german-test.pkl') # preparar tokenizer en inglés eng_tokenizer = create_tokenizer(dataset[:, 0]) eng_vocab_size = len(eng_tokenizer.word_index) + 1 eng_length = max_length(dataset[:, 0]) print('English Vocabulary Size: %d'% eng_vocab_size)print('English Max Length: %d'% (eng_length)) # preparar tokenizer alemán ger_tokenizer = create_tokenizer(dataset[:, 1]) ger_vocab_size = len(ger_tokenizer.word_index) + 1 ger_length = max_length(dataset[:, 1]) print('German Vocabulary Size: %d'% ger_vocab_size) print('German Max Length: %d'% (ger_length)) # preparar los datos de formación trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1]) trainY = encode_sequences(eng_tokenizer, eng_length, train[:, 0]) trainY = encode_output(trainY, eng_vocab_size) # preparar los datos de validación testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1]) testY = encode_sequences(eng_tokenizer, eng_length, test[:, 0]) testY = encode_output(testY, eng_vocab_size) # definir el modelo model = define_model(ger_vocab_size, eng_vocab_size, ger_length, eng_length, 256) # modelo adecuado checkpoint = ModelCheckpoint('model.h5', monitor='val_loss', verbose=1,save_best_only=True, mode='min') model.fit(trainX, trainY, epochs=30, batch_size=64, validation_data=(testX, testY), callbacks=[checkpoint], verbose=2)
Al ejecutar el ejemplo, primero se imprime un resumen de los parámetros del conjunto de datos, como el tamaño del vocabulario y la longitud máxima de las frases.
English Vocabulary Size: 2404 English Max Length: 5 German Vocabulary Size: 3856 German Max Length: 10
A continuación se imprime un resumen del modelo definido, lo que nos permite confirmar la configuración del modelo.
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 10, 256) 987136 _________________________________________________________________ lstm_1 (LSTM) (None, 256) 525312 _________________________________________________________________ repeat_vector_1 (RepeatVecto (None, 5, 256) 0 _________________________________________________________________ lstm_2 (LSTM) (None, 5, 256) 525312 _________________________________________________________________ time_distributed_1 (TimeDist (None, 5, 2404) 617828 ================================================================= Total params: 2,655,588 Trainable params: 2,655,588 Non-trainable params: 0 _________________________________________________________________
También se crea un gráfico del modelo que proporciona otra perspectiva de la configuración del modelo.

A continuación, el modelo es entrenado. Cada época dura unos 30 segundos en el hardware de la CPU moderna; no se necesita una GPU. Durante la ejecución, el modelo se guardará en el archivo model.h5, listo para inferirlo en el siguiente paso.
... Epoch 26/30 Epoch 00025: val_loss improved from 2.20048 to 2.19976, saving model to model.h5 17s - loss: 0.7114 - val_loss: 2.1998 Epoch 27/30 Epoch 00026: val_loss improved from 2.19976 to 2.18255, saving model to model.h5 17s - loss: 0.6532 - val_loss: 2.1826 Epoch 28/30 Epoch 00027: val_loss did not improve 17s - loss: 0.5970 - val_loss: 2.1970 Epoch 29/30 Epoch 00028: val_loss improved from 2.18255 to 2.17872, saving model to model.h5 17s - loss: 0.5474 - val_loss: 2.1787 Epoch 30/30 Epoch 00029: val_loss did not improve 17s - loss: 0.5023 - val_loss: 2.1823
Evaluar el modelo de traducción neuronal
Evaluaremos el modelo en el entrenamiento y el conjunto de datos de prueba. El modelo debe funcionar muy bien en el conjunto de datos del entrenamiento y lo ideal es que se haya generalizado para que funcione bien en el conjunto de datos de prueba. Idealmente, utilizaríamos un conjunto de datos de validación separado para ayudar con la selección del modelo durante la capacitación en lugar del conjunto de pruebas. Puede intentarlo como una extensión. Los conjuntos de datos limpios deben cargarse y prepararse como antes.
... # conjuntos de datos de carga dataset = load_clean_sentences('english-german-both.pkl') train = load_clean_sentences('english-german-train.pkl') test = load_clean_sentences('english-german-test.pkl') # preparar tokenizer en inglés eng_tokenizer = create_tokenizer(dataset[:, 0]) eng_vocab_size = len(eng_tokenizer.word_index) + 1 eng_length = max_length(dataset[:, 0]) # preparar tokenizer alemán ger_tokenizer = create_tokenizer(dataset[:, 1]) ger_vocab_size = len(ger_tokenizer.word_index) + 1 ger_length = max_length(dataset[:, 1]) # preparar los datos trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1]) testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1])
A continuación, se debe cargar el mejor modelo guardado durante el entrenamiento.
# cargar modelo model = load_model('model.h5')
La evaluación implica dos pasos: primero generar una secuencia de salida traducida, y luego repetir este proceso para muchos ejemplos de entrada y resumir la habilidad del modelo en múltiples casos. Comenzando con la inferencia, el modelo puede predecir la secuencia de salida completa de una sola vez.
translation = model.predict(source, verbose=0)v
Esta será una secuencia de números enteros que podemos enumerar y buscar en el tokenizer para volver a las palabras. La siguiente función, llamada word_for_id(), realizará este mapeo inverso.
# asignar un número entero a una palabra def word_for_id(integer, tokenizer): for word, index in tokenizer.word_index.items(): if index == integer: return word return None
Podemos realizar este mapeo para cada entero en la traducción y devolver el resultado como una cadena de palabras. La función predict_sequence() realiza esta operación para una sola frase fuente codificada.
# Generar la secuencia de la fuente del objetivo def predict_sequence(model, tokenizer, source): prediction = model.predict(source, verbose=0)[0] integers = [argmax(vector) for vector in prediction] target = list() for i in integers: word = word_for_id(i, tokenizer) if word is None: break target.append(word) return''.join(target)
A continuación, podemos repetir esto para cada frase fuente en un conjunto de datos y comparar el resultado predicho con la frase de destino esperada en inglés. Podemos imprimir algunas de estas comparaciones para tener una idea de cómo funciona el modelo en la práctica. También calcularemos las puntuaciones BLEU para tener una idea cuantitativa de lo bien que ha funcionado el modelo. La función evaluate_model() de abajo implementa esto, llamando a la función predict_sequence() de arriba para cada frase en un conjunto de datos proporcionado.
# evaluar la habilidad del modelo def evaluate_model(model, tokenizer, sources, raw_dataset): actual, predicted = list(), list() for i, source in enumerate(sources): # traducir el texto fuente codificado source = source.reshape((1, source.shape[0])) translation = predict_sequence(model, eng_tokenizer, source) raw_target, raw_src = raw_dataset[i] if i < 10: print('src=[%s], target=[%s], predicted=[%s]'% (raw_src, raw_target, translation)) actual.append(raw_target.split()) predicted.append(translation.split()) # Calcular la puntuación BLEU print('BLEU-1: %f'% corpus_bleu(actual, predicted, weights=(1.0, 0, 0, 0))) print('BLEU-2: %f'% corpus_bleu(actual, predicted, weights=(0.5, 0.5, 0, 0))) print('BLEU-3: %f'% corpus_bleu(actual, predicted, weights=(0.3, 0.3, 0.3, 0))) print('BLEU-4: %f'% corpus_bleu(actual, predicted, weights=(0.25, 0.25, 0.25,0.25)))
Podemos unir todo esto y evaluar el modelo cargado tanto en los conjuntos de datos de entrenamiento como en los de prueba. La lista completa de códigos se proporciona a continuación.
from pickle import load from numpy import argmax from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.models import load_model from nltk.translate.bleu_score import corpus_bleu # cargar un conjunto de datos limpio def load_clean_sentences(filename): return load(open(filename,'rb')) # instalar un tokenizador def create_tokenizer(lines): tokenizer = Tokenizer() tokenizer.fit_on_texts(lines) return tokenizer # duración máxima de la frase def max_length(lines): return max(len(line.split()) for line in lines) # codificar y secuencias de pads def encode_sequences(tokenizer, length, lines): # integer encode sequences X = tokenizer.texts_to_sequences(lines) # secuencias de pads con valores 0 X = pad_sequences(X, maxlen=length, padding='post') return X # asignar un número entero a una palabra def word_for_id(integer, tokenizer): for word, index in tokenizer.word_index.items(): if index == integer: return word return None # generar la secuencia de origen del objetivo def predict_sequence(model, tokenizer, source): prediction = model.predict(source, verbose=0)[0] integers = [argmax(vector) for vector in prediction] target = list() for i in integers: word = word_for_id(i, tokenizer) if word is None: break target.append(word) return''.join(target) # evaluar la habilidad del modelo def evaluate_model(model, tokenizer, sources, raw_dataset): actual, predicted = list(), list() for i, source in enumerate(sources): # traducir el texto fuente codificado source = source.reshape((1, source.shape[0])) translation = predict_sequence(model, eng_tokenizer, source) raw_target, raw_src = raw_dataset[i] if i < 10: print('src=[%s], target=[%s], predicted=[%s]'% (raw_src, raw_target, translation)) actual.append(raw_target.split()) predicted.append(translation.split()) # Calcular la puntuación BLEU print('BLEU-1: %f'% corpus_bleu(actual, predicted, weights=(1.0, 0, 0, 0))) print('BLEU-2: %f'% corpus_bleu(actual, predicted, weights=(0.5, 0.5, 0, 0))) print('BLEU-3: %f'% corpus_bleu(actual, predicted, weights=(0.3, 0.3, 0.3, 0))) print('BLEU-4: %f'% corpus_bleu(actual, predicted, weights=(0.25, 0.25, 0.25,0.25))) # conjuntos de datos de carga dataset = load_clean_sentences('english-german-both.pkl') train = load_clean_sentences('english-german-train.pkl') test = load_clean_sentences('english-german-test.pkl') # preparar tokenizer en inglés eng_tokenizer = create_tokenizer(dataset[:, 0]) eng_vocab_size = len(eng_tokenizer.word_index) + 1 eng_length = max_length(dataset[:, 0]) # preparar tokenizer alemán ger_tokenizer = create_tokenizer(dataset[:, 1]) ger_vocab_size = len(ger_tokenizer.word_index) + 1 ger_length = max_length(dataset[:, 1]) # preparar los datos trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1]) testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1]) # modelo de carga model = load_model('model.h5') # probar en algunas secuencias de prueba print('train') evaluate_model(model, trainX, train) # probar en algunas secuencias de prueba print('test') evaluate_model(model, testX, test) Listing 30.27: Complete example of
Al ejecutar el ejemplo, primero se imprimen los ejemplos del texto original, las traducciones esperadas y previstas, así como las puntuaciones del conjunto de datos de la formación, seguidas del conjunto de datos de la prueba. Sus resultados específicos serán diferentes dada la mezcla aleatoria del conjunto de datos y la naturaleza estocástica de las redes neuronales. Observando primero los resultados del conjunto de datos de prueba, podemos ver que las traducciones son legibles y en su mayoría correctas. Por ejemplo: ‘ich liebe dich ‘ se tradujo correctamente a ‘i love you’.
También podemos ver que las traducciones no fueron perfectas, con ‘ich konnte nicht gehen’ traducido a i cant go en lugar de lo esperado ‘i couldnt walk’. También podemos ver la puntuación del BLEU-4 de 0,51, que proporciona un límite superior sobre lo que podemos esperar de este modelo.
src=[ich liebe dich], target=[i love you], predicted=[i love you] src=[ich sagte du sollst den mund halten], target=[i said shut up], predicted=[i said stop up] src=[wie geht es eurem vater], target=[hows your dad], predicted=[hows your dad] src=[das gefallt mir], target=[i like that], predicted=[i like that] src=[ich gehe immer zu fu], target=[i always walk], predicted=[i will to] src=[ich konnte nicht gehen], target=[i couldnt walk], predicted=[i cant go] src=[er ist sehr jung], target=[he is very young], predicted=[he is very young] src=[versucht es doch einfach], target=[just try it], predicted=[just try it] src=[sie sind jung], target=[youre young], predicted=[youre young] src=[er ging surfen], target=[he went surfing], predicted=[he went surfing] BLEU-1: 0.085682 BLEU-2: 0.284191 BLEU-3: 0.459090 BLEU-4: 0.517571
Mirando los resultados en el equipo de prueba, fíjate en las traducciones legibles, lo cual no es una tarea fácil. Por ejemplo, vemos ‘ich mag dich nicht’ correctamente traducido a ‘i dont like you’. También vemos algunas traducciones deficientes y un buen caso que el modelo podría soportar de más afinación, como ‘ich bin etwas beschwipst ‘ traducido como ‘i a bit bit’ en lugar del esperado ‘im a bit tipsy’. Se logró una puntuación de BLEU-4 de 0,076238, lo que proporcionó una habilidad de baseline para mejorarla con mejoras adicionales al modelo.
src=[tom erblasste], target=[tom turned pale], predicted=[tom went pale] src=[bring mich nach hause], target=[take me home], predicted=[let us at] src=[ich bin etwas beschwipst], target=[im a bit tipsy], predicted=[i a bit bit] src=[das ist eine frucht], target=[its a fruit], predicted=[thats a a] src=[ich bin pazifist], target=[im a pacifist], predicted=[im am] src=[unser plan ist aufgegangen], target=[our plan worked], predicted=[who is a man] src=[hallo tom], target=[hi tom], predicted=[hello tom] src=[sei nicht nervos], target=[dont be nervous], predicted=[dont be crazy] src=[ich mag dich nicht], target=[i dont like you], predicted=[i dont like you] src=[tom stellte eine falle], target=[tom set a trap], predicted=[tom has a cough] BLEU-1: 0.082088 BLEU-2: 0.006182 BLEU-3: 0.046129 BLEU-4: 0.076238
Extensiones
En esta sección se enumeran algunas ideas para ampliar el tutorial que tal vez desees explorar.
- Limpieza de datos: Se podrían realizar diferentes operaciones de limpieza de datos, tales como no eliminar la puntuación o el caso de normalización, o tal vez eliminar frases duplicadas en inglés.
- Vocabulario: El vocabulario podría ser refinado, tal vez eliminando palabras usadas menos de 5 o 10 veces en el conjunto de datos y reemplazadas por unk.
- Más datos: El conjunto de datos utilizado para ajustar el modelo podría ampliarse a 50.000, o 100.000, frases o más.
- Orden de entrada: El orden de las frases de entrada puede ser invertido, lo que se ha reportado que eleva la habilidad, o se puede usar una capa de entrada bidireccional.
- Capas: Los modelos de codificador y/o decodificador pueden ser ampliados con capas adicionales y entrenados para más épocas, proporcionando una mayor capacidad de representación para el modelo.
- Unidades: El número de unidades de memoria en el codificador y decodificador podría incrementarse, proporcionando una mayor capacidad de representación para el modelo.
- Regularización: El modelo podría utilizar la regularización, como la regularización del peso o la activación, o el uso de la deserción en las capas de LSTM.
- Vectores de Palabra Pre-Formados: En el modelo se pueden utilizar vectores de palabras previamente entrenados.
- Medida alternativa: Explora medidas de rendimiento alternativas junto a BLEU como ROGUE. Compare las puntuaciones de las mismas traducciones para desarrollar una intuición de cómo difieren las medidas en la práctica.
- Modelo Recursivo: Se podría utilizar una formulación recursiva del modelo en la que la siguiente palabra de la secuencia de salida pudiera estar condicionada a la secuencia de entrada y la secuencia de salida generada hasta el momento.
Si exploras alguna de estas extensiones, me encantaría saberlo.
➡ Aprende mucho mas de Procesamiento de Lenguaje Natural con nuestro curso: