Python para PNL: traducción automática neuronal con Seq2Seq en Keras

La arquitectura seq2seq es un tipo de modelado de secuencia de muchos a muchos. En este artículo, crearemos un modelo de traducción automática en Python con Keras.

Este es el artículo número 22 de mi serie de artículos sobre Python para PNL. En uno de mis artículos anteriores sobre resolviendo problemas de secuencias con Keras, expliqué cómo resolver muchos problemas de secuencia donde tanto las entradas como las salidas se dividen en múltiples pasos de tiempo. La arquitectura seq2seq es un tipo de modelado de secuencia de muchos a muchos, y se usa comúnmente para una variedad de tareas como resumen de texto, desarrollo de chatbot, conversacional modelado y traducción automática neuronal, etc.

En este artículo, veremos cómo crear un modelo de traducción de idiomas que también es una aplicación muy famosa de traducción automática neuronal. Usaremos la arquitectura seq2seq para crear nuestro modelo de traducción de idiomas usando la biblioteca Keras de Python.

Se supone que tienes un buen conocimiento de redes neuronales recurrentes, particularmente LSTM. El código de este artículo está escrito en Python con la biblioteca Keras. Por lo tanto, se supone que tiene un buen conocimiento del lenguaje Python, así como de la biblioteca Keras. Entonces, sin más preámbulos, comencemos.

Bibliotecas y ajustes de configuración

Como primer paso, importaremos las bibliotecas requeridas y configuraremos valores para diferentes parámetros que usaremos en el código. Importemos primero las bibliotecas requeridas:

1
2
3
4
5
6
7
8
9
import os, sys

from keras.models import Model
from keras.layers import Input, LSTM, GRU, Dense, Embedding
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
import numpy as np
import matplotlib.pyplot as plt

Ejecute el siguiente script para establecer valores para diferentes parámetros:

1
2
3
4
5
6
7
BATCH_SIZE = 64
EPOCHS = 20
LSTM_NODES =256
NUM_SENTENCES = 20000
MAX_SENTENCE_LENGTH = 50
MAX_NUM_WORDS = 20000
EMBEDDING_SIZE = 100

El conjunto de datos

El modelo de traducción de idiomas que vamos a desarrollar en este artículo traducirá oraciones en inglés a sus contrapartes en francés. Para desarrollar dicho modelo, necesitamos un conjunto de datos que contenga oraciones en inglés y sus traducciones al francés. Afortunadamente, dicho conjunto de datos está disponible gratuitamente en este enlace. Descargue el archivo fra-eng.zip y extráigalo. Luego verá el archivo fra.txt. En cada línea, el archivo de texto contiene una oración en inglés y su traducción al francés, separadas por una tabulación. Las primeras 20 líneas del archivo fra.txt se ven así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Go. Va !
Hi. Salut !
Hi. Salut.
Run!    Cours!
Run!    Courez!
Who?    Qui ?
Wow!    Ça alors!
Fire!   Au feu !
Help!   À l'aide!
Jump.   Saute.
Stop!   Ça suffit!
Stop!   Stop!
Stop!   Arrête-toi !
Wait!   Attends !
Wait!   Attendez !
Go on.  Poursuis.
Go on.  Continuez.
Go on.  Poursuivez.
Hello!  Bonjour !
Hello!  Salut !

El modelo contiene más de 170 000 registros, pero solo usaremos los primeros 20 000 registros para entrenar nuestro modelo. Puede utilizar más registros si lo desea.

Preprocesamiento de datos {#preprocesamiento de datos}

Los modelos de traducción automática neuronal a menudo se basan en la arquitectura seq2seq. La arquitectura seq2seq es una arquitectura de codificador-decodificador que consta de dos redes LSTM: el codificador LSTM y el decodificador LSTM. La entrada al codificador LSTM es la oración en el idioma original; la entrada al decodificador LSTM es la oración en el idioma traducido con un token de inicio de oración. El resultado es la oración de destino real con un token de final de oración.

En nuestro conjunto de datos, no necesitamos procesar la entrada; sin embargo, necesitamos generar dos copias de la oración traducida: una con el token de inicio de oración y la otra con el token de final de oración. Aquí está el script que hace eso:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
input_sentences = []
output_sentences = []
output_sentences_inputs = []

count = 0
for line in open(r'/content/drive/My Drive/datasets/fra.txt', encoding="utf-8"):
    count += 1

    if count > NUM_SENTENCES:
        break

    if '\t' not in line:
        continue

    input_sentence, output = line.rstrip().split('\t')

    output_sentence = output + ' <eos>'
    output_sentence_input = '<sos> ' + output

    input_sentences.append(input_sentence)
    output_sentences.append(output_sentence)
    output_sentences_inputs.append(output_sentence_input)

print("num samples input:", len(input_sentences))
print("num samples output:", len(output_sentences))
print("num samples output input:", len(output_sentences_inputs))

Nota: Es probable que deba cambiar la ruta del archivo fra.txt en su computadora para que esto funcione.

En el script anterior creamos tres listas input_sentences[], output_sentences[] y output_sentences_inputs[]. A continuación, en el bucle for se lee el archivo fra.txt linea por linea. Cada línea se divide en dos subcadenas en la posición donde aparece la pestaña. La subcadena izquierda (la oración en inglés) se inserta en la lista input_sentences[]. La subcadena a la derecha de la pestaña es la oración francesa traducida correspondiente. El token <eos>, que marca el final de la oración, se antepone a la oración traducida, y la oración resultante se agrega a la lista output_sentences[]. De manera similar, el token <sos>, que significa "inicio de oración", se concatena al comienzo de la oración traducida y el resultado se agrega a la lista output_sentences_inputs[]. El ciclo termina si el número de oraciones agregadas a las listas es mayor que la variable NUM_SENTENCES, es decir, 20,000.

Finalmente, el número de muestras en las tres listas se muestra en la salida:

1
2
3
num samples input: 20000
num samples output: 20000
num samples output input: 20000

Ahora imprimamos aleatoriamente una oración de las listas input_sentences[], output_sentences[] y output_sentences_inputs[]:

1
2
3
print(input_sentences[172])
print(output_sentences[172])
print(output_sentences_inputs[172])

Aquí está la salida:

1
2
3
I'm ill.
Je suis malade. <eos>
<sos> Je suis malade.

Puedes ver la oración original, es decir, ‘Estoy enfermo’; su traducción correspondiente en la salida, es decir, Je suis malade. <eos>. Note, aquí tenemos el token <eos> al final de la oración. De manera similar, para la entrada al decodificador, tenemos <sos> Je suis malade.

Tokenización y relleno

El siguiente paso es tokenizar las oraciones originales y traducidas y aplicar relleno a las oraciones que son más largas o más cortas que una cierta longitud, que en el caso de las entradas será la longitud de la oración de entrada más larga. Y para la salida, esta será la longitud de la oración más larga en la salida.

Para la tokenización, se puede utilizar la clase Tokenizer de la biblioteca keras.preprocessing.text. La clase tokenizer realiza dos tareas:

  • Divide una oración en la correspondiente lista de palabras
  • Luego convierte las palabras a números enteros

Esto es extremadamente importante ya que los algoritmos de aprendizaje profundo y aprendizaje automático funcionan con números. El siguiente script se utiliza para tokenizar las oraciones de entrada:

1
2
3
4
5
6
7
8
9
input_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS)
input_tokenizer.fit_on_texts(input_sentences)
input_integer_seq = input_tokenizer.texts_to_sequences(input_sentences)

word2idx_inputs = input_tokenizer.word_index
print('Total unique words in the input: %s' % len(word2idx_inputs))

max_input_len = max(len(sen) for sen in input_integer_seq)
print("Length of longest sentence in input: %g" % max_input_len)

Además de la tokenización y la conversión de enteros, el atributo word_index de la clase Tokenizer devuelve un diccionario de palabra a índice donde las palabras son las claves y los enteros correspondientes son los valores. El script anterior también imprime la cantidad de palabras únicas en el diccionario y la longitud de la oración más larga en la entrada:

1
2
Total unique words in the input: 3523
Length of longest sentence in input: 6

De manera similar, las oraciones de salida también se pueden tokenizar de la misma manera que se muestra a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
output_tokenizer = Tokenizer(num_words=MAX_NUM_WORDS, filters='')
output_tokenizer.fit_on_texts(output_sentences + output_sentences_inputs)
output_integer_seq = output_tokenizer.texts_to_sequences(output_sentences)
output_input_integer_seq = output_tokenizer.texts_to_sequences(output_sentences_inputs)

word2idx_outputs = output_tokenizer.word_index
print('Total unique words in the output: %s' % len(word2idx_outputs))

num_words_output = len(word2idx_outputs) + 1
max_out_len = max(len(sen) for sen in output_integer_seq)
print("Length of longest sentence in the output: %g" % max_out_len)

Aquí está la salida:

1
2
Total unique words in the output: 9561
Length of longest sentence in the output: 13

A partir de la comparación del número de palabras únicas en la entrada y la salida, se puede concluir que las oraciones en inglés normalmente son más cortas y contienen una cantidad menor de palabras en promedio, en comparación con las oraciones traducidas al francés.

A continuación, necesitamos rellenar la entrada. La razón detrás de rellenar la entrada y la salida es que las oraciones de texto pueden tener una longitud variable, sin embargo, LSTM (el algoritmo con el que vamos a entrenar nuestro modelo) espera instancias de entrada con la misma longitud. Por lo tanto, necesitamos convertir nuestras oraciones en vectores de longitud fija. Una forma de hacerlo es a través del relleno.

En relleno, se define una cierta longitud para una oración. En nuestro caso, la longitud de la oración más larga en las entradas y salidas se usará para rellenar las oraciones de entrada y salida, respectivamente. La oración más larga en la entrada contiene 6 palabras. Para las oraciones que contengan menos de 6 palabras, se agregarán ceros en los índices vacíos. El siguiente script aplica relleno a las oraciones de entrada.

1
2
3
encoder_input_sequences = pad_sequences(input_integer_seq, maxlen=max_input_len)
print("encoder_input_sequences.shape:", encoder_input_sequences.shape)
print("encoder_input_sequences[172]:", encoder_input_sequences[172])

El script anterior imprime la forma de las oraciones de entrada rellenadas. También se imprime la secuencia de enteros rellenada para la oración en el índice 172. Aquí está la salida:

1
2
encoder_input_sequences.shape: (20000, 6)
encoder_input_sequences[172]: [  0   0   0   0   6 539]

Como hay 20 000 oraciones en la entrada y cada oración de entrada tiene una longitud de 6, la forma de la entrada ahora es (20000, 6). Si observa la secuencia de enteros para la oración en el índice 172 de la oración de entrada, puede ver que hay tres ceros, seguidos de los valores 6 y 539. Puede recordar que la oración original en el índice 172 es ‘Soy enfermo`. El tokenizador dividió la oración en dos palabras “Estoy” y “Enfermo”, las convirtió en números enteros y luego aplicó relleno previo agregando tres ceros al comienzo de la secuencia entera correspondiente para la oración en el índice 172 de la entrada. lista.

Para verificar que los valores enteros de soy y mal son 6 y 539 respectivamente, puede pasar las palabras al diccionario word2index_inputs, como se muestra a continuación:

1
2
print(word2idx_inputs["i'm"])
print(word2idx_inputs["ill"])

Producción:

1
2
6
539

De la misma manera, las salidas del decodificador y las entradas del decodificador se rellenan de la siguiente manera:

1
2
3
decoder_input_sequences = pad_sequences(output_input_integer_seq, maxlen=max_out_len, padding='post')
print("decoder_input_sequences.shape:", decoder_input_sequences.shape)
print("decoder_input_sequences[172]:", decoder_input_sequences[172])

Producción:

1
2
decoder_input_sequences.shape: (20000, 13)
decoder_input_sequences[172]: [  2   3   6 188   0   0   0   0   0   0   0   0   0]

La oración en el índice 172 de la entrada del decodificador es <sos> je suis malade.. Si imprime los enteros correspondientes del diccionario word2idx_outputs, debería ver 2, 3, 6 y 188 impresos en la consola, como se muestra aquí:

1
2
3
4
print(word2idx_outputs["<sos>"])
print(word2idx_outputs["je"])
print(word2idx_outputs["suis"])
print(word2idx_outputs["malade."])

Producción:

1
2
3
4
2
3
6
188

Además, es importante mencionar que en el caso del decodificador, se aplica el relleno posterior, lo que significa que se agregan ceros al final de la oración. En el codificador, los ceros se rellenaron al comienzo. La razón detrás de este enfoque es que la salida del codificador se basa en las palabras que aparecen al final de la oración, por lo tanto, las palabras originales se mantuvieron al final de la oración y se agregaron ceros al principio. Por otro lado, en el caso del decodificador, el procesamiento comienza desde el principio de una oración, y por lo tanto se realiza un post-relleno en las entradas y salidas del decodificador.

Incrustaciones de palabras {#incrustaciones de palabras}

He escrito un detallado artículo sobre incrustaciones de palabras, que tal vez quieras revisar para entender las incrustaciones de palabras en Keras. Esta sección solo proporciona la implementación de incrustaciones de palabras para la traducción automática neuronal. Sin embargo, el concepto básico sigue siendo el mismo.

Dado que estamos utilizando modelos de aprendizaje profundo, y los modelos de aprendizaje profundo funcionan con números, necesitamos convertir nuestras palabras en sus correspondientes representaciones de vectores numéricos. Pero ya convertimos nuestras palabras en números enteros. Entonces, ¿cuál es la diferencia entre la representación de enteros y las incrustaciones de palabras?

Hay dos diferencias principales entre la representación de un solo entero y las incrustaciones de palabras. Con la representación de números enteros, una palabra se representa solo con un número entero. Con la representación vectorial, una palabra se representa mediante un vector de 50, 100, 200 o las dimensiones que desee. Por lo tanto, las incrustaciones de palabras capturan mucha más información sobre las palabras. En segundo lugar, la representación de un solo entero no captura las relaciones entre diferentes palabras. Por el contrario, las incrustaciones de palabras conservan las relaciones entre las palabras. Puede usar incrustaciones de palabras personalizadas o puede usar incrustaciones de palabras preentrenadas.

En este artículo, para las oraciones en inglés, es decir, las entradas, usaremos las incrustaciones de palabras Guante. Para las oraciones en francés traducidas en la salida, utilizaremos incrustaciones de palabras personalizadas.

Primero creemos incrustaciones de palabras para las entradas. Para hacerlo, necesitamos cargar los vectores de palabras de GloVe en la memoria. Luego crearemos un diccionario donde las palabras son las claves y los vectores correspondientes son los valores, como se muestra a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from numpy import array
from numpy import asarray
from numpy import zeros

embeddings_dictionary = dict()

glove_file = open(r'/content/drive/My Drive/datasets/glove.6B.100d.txt', encoding="utf8")

for line in glove_file:
    records = line.split()
    word = records[0]
    vector_dimensions = asarray(records[1:], dtype='float32')
    embeddings_dictionary[word] = vector_dimensions
glove_file.close()

Recuerde que tenemos 3523 palabras únicas en la entrada. Crearemos una matriz donde el número de fila representará el valor entero de la palabra y las columnas corresponderán a las dimensiones de la palabra. Esta matriz contendrá las incrustaciones de palabras para las palabras en nuestras oraciones de entrada.

1
2
3
4
5
6
num_words = min(MAX_NUM_WORDS, len(word2idx_inputs) + 1)
embedding_matrix = zeros((num_words, EMBEDDING_SIZE))
for word, index in word2idx_inputs.items():
    embedding_vector = embeddings_dictionary.get(word)
    if embedding_vector is not None:
        embedding_matrix[index] = embedding_vector

Primero imprimamos las incrustaciones de palabras para la palabra “enfermo” usando el diccionario de incrustaciones de palabras de GloVe.

1
print(embeddings_dictionary["ill"])

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[ 0.12648    0.1366     0.22192   -0.025204  -0.7197     0.66147
  0.48509    0.057223   0.13829   -0.26375   -0.23647    0.74349
  0.46737   -0.462      0.20031   -0.26302    0.093948  -0.61756
 -0.28213    0.1353     0.28213    0.21813    0.16418    0.22547
 -0.98945    0.29624   -0.62476   -0.29535    0.21534    0.92274
  0.38388    0.55744   -0.14628   -0.15674   -0.51941    0.25629
 -0.0079678  0.12998   -0.029192   0.20868   -0.55127    0.075353
  0.44746   -0.71046    0.75562    0.010378   0.095229   0.16673
  0.22073   -0.46562   -0.10199   -0.80386    0.45162    0.45183
  0.19869   -1.6571     0.7584    -0.40298    0.82426   -0.386
  0.0039546  0.61318    0.02701   -0.3308    -0.095652  -0.082164
  0.7858     0.13394   -0.32715   -0.31371   -0.20247   -0.73001
 -0.49343    0.56445    0.61038    0.36777   -0.070182   0.44859
 -0.61774   -0.18849    0.65592    0.44797   -0.10469    0.62512
 -1.9474    -0.60622    0.073874   0.50013   -1.1278    -0.42066
 -0.37322   -0.50538    0.59171    0.46534   -0.42482    0.83265
  0.081548  -0.44147   -0.084311  -1.2304   ]

En la sección anterior, vimos que la representación entera de la palabra enfermo es 539. Ahora revisemos el índice 539 de la matriz de incrustación de palabras.

1
print(embedding_matrix[539])

Producción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[ 0.12648    0.1366     0.22192   -0.025204  -0.7197     0.66147
  0.48509    0.057223   0.13829   -0.26375   -0.23647    0.74349
  0.46737   -0.462      0.20031   -0.26302    0.093948  -0.61756
 -0.28213    0.1353     0.28213    0.21813    0.16418    0.22547
 -0.98945    0.29624   -0.62476   -0.29535    0.21534    0.92274
  0.38388    0.55744   -0.14628   -0.15674   -0.51941    0.25629
 -0.0079678  0.12998   -0.029192   0.20868   -0.55127    0.075353
  0.44746   -0.71046    0.75562    0.010378   0.095229   0.16673
  0.22073   -0.46562   -0.10199   -0.80386    0.45162    0.45183
  0.19869   -1.6571     0.7584    -0.40298    0.82426   -0.386
  0.0039546  0.61318    0.02701   -0.3308    -0.095652  -0.082164
  0.7858     0.13394   -0.32715   -0.31371   -0.20247   -0.73001
 -0.49343    0.56445    0.61038    0.36777   -0.070182   0.44859
 -0.61774   -0.18849    0.65592    0.44797   -0.10469    0.62512
 -1.9474    -0.60622    0.073874   0.50013   -1.1278    -0.42066
 -0.37322   -0.50538    0.59171    0.46534   -0.42482    0.83265
  0.081548  -0.44147   -0.084311  -1.2304   ]

Puede ver que los valores para la fila 539 en la matriz de incrustación son similares a la representación vectorial de la palabra “enfermo” en el diccionario de GloVe, lo que confirma que las filas en la matriz de incrustación representan incrustaciones de palabras correspondientes del diccionario de incrustación de palabras de GloVe. Esta matriz de incrustación de palabras se utilizará para crear la capa de incrustación de nuestro modelo LSTM.

El siguiente script crea la capa de incrustación para la entrada:

1
embedding_layer = Embedding(num_words, EMBEDDING_SIZE, weights=[embedding_matrix], input_length=max_input_len)

Creación del modelo

Ha llegado el momento de desarrollar nuestro modelo. Lo primero que debemos hacer es definir nuestras salidas, ya que sabemos que la salida será una secuencia de palabras. Recuerde que el número total de palabras únicas en la salida es 9562. Por lo tanto, cada palabra en la salida puede ser cualquiera de las 9562 palabras. La longitud de una oración de salida es 13. Y para cada oración de entrada, necesitamos una oración de salida correspondiente. Por lo tanto, la forma final de la salida será:

1
(number of inputs, length of the output sentence, the number of words in the output)

El siguiente script crea la matriz de salida vacía:

1
2
3
4
5
6
7
decoder_targets_one_hot = np.zeros((
        len(input_sentences),
        max_out_len,
        num_words_output
    ),
    dtype='float32'
)

El siguiente script imprime la forma del decodificador:

1
decoder_targets_one_hot.shape

Producción:

1
(20000, 13, 9562)

Para hacer predicciones, la capa final del modelo será una capa densa, por lo tanto, necesitamos las salidas en forma de vectores codificados en caliente, ya que usaremos la función de activación softmax en la capa densa. Para crear tal salida codificada en caliente, el siguiente paso es asignar 1 al número de columna que corresponde a la representación entera de la palabra. Por ejemplo, la representación entera de <sos> je suis malade es [ 2 3 6 188 0 0 0 0 0 0 0 ]. En la matriz de salida decoder_targets_one_hot, en la segunda columna de la primera fila, se insertará 1. Del mismo modo, en el tercer índice de la segunda fila, se insertará otro 1, y así sucesivamente.

Mira el siguiente guión:

1
2
3
for i, d in enumerate(decoder_output_sequences):
    for t, word in enumerate(d):
        decoder_targets_one_hot[i, t, word] = 1

A continuación, necesitamos crear el codificador y los decodificadores. La entrada al codificador será la oración en inglés y la salida será el estado oculto y el estado de celda del LSTM.

El siguiente script define el codificador:

1
2
3
4
5
6
encoder_inputs_placeholder = Input(shape=(max_input_len,))
x = embedding_layer(encoder_inputs_placeholder)
encoder = LSTM(LSTM_NODES, return_state=True)

encoder_outputs, h, c = encoder(x)
encoder_states = [h, c]

El siguiente paso es definir el decodificador. El decodificador tendrá dos entradas: el estado oculto y el estado de celda del codificador y la oración de entrada, que en realidad será la oración de salida con un token <sos> adjunto al principio.

El siguiente script crea el decodificador LSTM:

1
2
3
4
5
6
7
decoder_inputs_placeholder = Input(shape=(max_out_len,))

decoder_embedding = Embedding(num_words_output, LSTM_NODES)
decoder_inputs_x = decoder_embedding(decoder_inputs_placeholder)

decoder_lstm = LSTM(LSTM_NODES, return_sequences=True, return_state=True)
decoder_outputs, _, _ = decoder_lstm(decoder_inputs_x, initial_state=encoder_states)

Finalmente, la salida del decodificador LSTM pasa a través de una capa densa para predecir las salidas del decodificador, como se muestra aquí:

1
2
decoder_dense = Dense(num_words_output, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

El siguiente paso es compilar el modelo:

1
2
3
4
5
6
7
model = Model([encoder_inputs_placeholder,
  decoder_inputs_placeholder], decoder_outputs)
model.compile(
    optimizer='rmsprop',
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

Tracemos nuestro modelo para ver cómo se ve:

1
2
from keras.utils import plot_model
plot_model(model, to_file='model_plot4a.png', show_shapes=True, show_layer_names=True)

Producción:

Desde la salida, puede ver que tenemos dos tipos de entrada. input_1 es el marcador de posición de entrada para el codificador, que está incrustado y pasa a través de la capa lstm_1, que básicamente es el codificador LSTM. Hay tres salidas de la capa lstm_1: la salida, la capa oculta y el estado de la celda. Sin embargo, solo el estado de celda y el estado oculto se pasan al decodificador.

Aquí la capa lstm_2 es el decodificador LSTM. El input_2 contiene las oraciones de salida con el token <sos> agregado al comienzo. La entrada_2 también se pasa a través de una capa de incrustación y se usa como entrada para el decodificador LSTM, lstm_2. Finalmente, la salida del decodificador LSTM pasa a través de la capa densa para hacer predicciones.

El siguiente paso es entrenar el modelo usando el método fit():

1
2
3
4
5
6
7
r = model.fit(
    [encoder_input_sequences, decoder_input_sequences],
    decoder_targets_one_hot,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.1,
)

El modelo se entrena en 18 000 registros y se prueba en los 2000 registros restantes. El modelo está entrenado para 20 épocas, puede modificar el número de épocas para ver si puede obtener mejores resultados. Después de 20 épocas, obtuve una precisión de entrenamiento del 90,99 % y una precisión de validación del 79,11 %, lo que muestra que el modelo se está sobreajustando. Para reducir el sobreajuste, puede agregar abandonos o más registros. Solo estamos entrenando en 20,0000 registros, por lo que puede agregar más registros para reducir el sobreajuste.

Modificación del modelo para predicciones

Durante el entrenamiento, conocemos las entradas reales al decodificador para todas las palabras de salida en la secuencia. Un ejemplo de lo que sucede durante el entrenamiento es el siguiente. Supongamos que tenemos una oración estoy enfermo. La frase se traduce de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Inputs on the left of Encoder/Decoder, outputs on the right.

Step 1:
I'm ill -> Encoder -> enc(h1,c1)

enc(h1,c1) + <sos> -> Decoder -> je + dec(h1,c1)

step 2:

enc(h1,c1) + je -> Decoder -> suis + dec(h2,c2)

step 3:

enc(h2,c2) + suis -> Decoder -> malade. + dec(h3,c3)

step 3:

enc(h3,c3) + malade. -> Decoder -> <eos> + dec(h4,c4)

Puede ver que se conoce la entrada al decodificador y la salida del decodificador y que el modelo se entrena sobre la base de estas entradas y salidas.

Sin embargo, durante las predicciones, la siguiente palabra se predecirá sobre la base de la palabra anterior, que a su vez también se predice en el paso de tiempo anterior. Ahora comprenderá el propósito de los tokens <sos> y <eos>. Al hacer predicciones reales, la secuencia de salida completa no está disponible; de ​​hecho, eso es lo que tenemos que predecir. Durante la predicción, la única palabra disponible para nosotros es <sos> ya que todas las oraciones de salida comienzan con <sos>.

Un ejemplo de lo que sucede durante la predicción es el siguiente. Traduciremos de nuevo la frase estoy enfermo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Inputs on the left of Encoder/Decoder, outputs on the right.

Step 1:

I'm ill -> Encoder -> enc(h1,c1)

enc(h1,c1) + <sos> -> Decoder -> y1(je) + dec(h1,c1)

step 2:

enc(h1,c1) + y1 -> Decoder -> y2(suis) + dec(h2,c2)

step 3:

enc(h2,c2) + y2 -> Decoder -> y3(malade.) + dec(h3,c3)

step 3:

enc(h3,c3) + y3 -> Decoder -> y4(<eos>) + dec(h4,c4)

Puede ver que la funcionalidad del codificador sigue siendo la misma. La oración en el idioma original pasa a través del codificador y el estado oculto, y el estado de la celda es la salida del codificador.

En el paso 1, el estado oculto y el estado de celda del codificador, y <sos>, se utilizan como entrada para el decodificador. El decodificador predice una palabra y1 que puede ser cierta o no. Sin embargo, según nuestro modelo, la probabilidad de predicción correcta es 0,7911. En el paso 2, el estado oculto del decodificador y el estado de la celda del paso 1, junto con y1, se utilizan como entrada para el decodificador, que predice y2. El proceso continúa hasta que se encuentra el token <eos>. Todas las salidas predichas del decodificador se concatenan para formar la oración de salida final. Modifiquemos nuestro modelo para implementar esta lógica.

El modelo del codificador sigue siendo el mismo:

1
encoder_model = Model(encoder_inputs_placeholder, encoder_states)

Dado que ahora en cada paso necesitamos el decodificador oculto y los estados de celda, modificaremos nuestro modelo para aceptar los estados oculto y de celda como se muestra a continuación:

1
2
3
decoder_state_input_h = Input(shape=(LSTM_NODES,))
decoder_state_input_c = Input(shape=(LSTM_NODES,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

Ahora, en cada paso de tiempo, solo habrá una palabra en la entrada del decodificador, debemos modificar la capa de incrustación del decodificador de la siguiente manera:

1
2
decoder_inputs_single = Input(shape=(1,))
decoder_inputs_single_x = decoder_embedding(decoder_inputs_single)

A continuación, debemos crear el marcador de posición para las salidas del decodificador:

1
decoder_outputs, h, c = decoder_lstm(decoder_inputs_single_x, initial_state=decoder_states_inputs)

Para hacer predicciones, la salida del decodificador se pasa a través de la capa densa:

1
2
decoder_states = [h, c]
decoder_outputs = decoder_dense(decoder_outputs)

El paso final es definir el modelo de decodificador actualizado, como se muestra aquí:

1
2
3
4
decoder_model = Model(
    [decoder_inputs_single] + decoder_states_inputs,
    [decoder_outputs] + decoder_states
)

Tracemos ahora nuestro decodificador LSTM modificado que hace predicciones:

1
2
from keras.utils import plot_model
plot_model(decoder_model, to_file='model_plot_dec.png', show_shapes=True, show_layer_names=True)

Producción:

En la imagen de arriba lstm_2 está el decodificador LSTM modificado. Puede ver que acepta la oración con una palabra como se muestra en input_5, y los estados ocultos y de celda de la salida anterior (input_3 y input_4). Puede ver que la forma de la oración de entrada ahora es (ninguno, 1) ya que solo habrá una palabra en la entrada del decodificador. Por el contrario, durante el entrenamiento, la forma de la oración de entrada era (Ninguno, 6) ya que la entrada contenía una oración completa con una longitud máxima de 6.

Hacer predicciones

En este paso, verá cómo hacer predicciones usando oraciones en inglés como entrada.

En los pasos de tokenización, convertimos palabras en números enteros. Las salidas del decodificador también serán números enteros. Sin embargo, queremos que nuestra salida sea una secuencia de palabras en el idioma francés. Para hacerlo, necesitamos convertir los números enteros nuevamente en palabras. Crearemos nuevos diccionarios tanto para entradas como para salidas donde las claves serán los números enteros y los valores correspondientes serán las palabras.

1
2
idx2word_input = {v:k for k, v in word2idx_inputs.items()}
idx2word_target = {v:k for k, v in word2idx_outputs.items()}

A continuación, crearemos un método, es decir, translate_sentence(). El método aceptará una oración en inglés de secuencia con relleno de entrada (en forma de número entero) y devolverá la oración en francés traducida. Mira el método translate_sentence():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def translate_sentence(input_seq):
    states_value = encoder_model.predict(input_seq)
    target_seq = np.zeros((1, 1))
    target_seq[0, 0] = word2idx_outputs['<sos>']
    eos = word2idx_outputs['<eos>']
    output_sentence = []

    for _ in range(max_out_len):
        output_tokens, h, c = decoder_model.predict([target_seq] + states_value)
        idx = np.argmax(output_tokens[0, 0, :])

        if eos == idx:
            break

        word = ''

        if idx > 0:
            word = idx2word_target[idx]
            output_sentence.append(word)

        target_seq[0, 0] = idx
        states_value = [h, c]

    return ' '.join(output_sentence)

En el script anterior, pasamos la secuencia de entrada a encoder_model, que predice el estado oculto y el estado de la celda, que se almacenan en la variable states_value.

A continuación, definimos una variable target_seq, que es una matriz 1 x 1 de todos ceros. La variable target_seq contiene la primera palabra del modelo del decodificador, que es <sos>.

Después de eso, se inicializa la variable eos, que almacena el valor entero para el token <eos>. En la siguiente línea, se define la lista output_sentence, que contendrá la traducción predicha.

A continuación, ejecutamos un bucle for. El número de ciclos de ejecución para el bucle for es igual a la longitud de la oración más larga en la salida. Dentro del bucle, en la primera iteración, decoder_model predice la salida y los estados ocultos y de celda, utilizando el estado oculto y de celda del codificador y el token de entrada, es decir, <sos>. El índice de la palabra predicha se almacena en la variable idx. Si el valor del índice predicho es igual al token <eos>, el ciclo termina. De lo contrario, si el índice predicho es mayor que cero, la palabra correspondiente se recupera del diccionario idx2word y se almacena en la variable word, que luego se agrega a la lista output_sentence. La variable states_value se actualiza con el nuevo estado oculto y de celda del decodificador y el índice de la palabra predicha se almacena en la variable target_seq. En el siguiente ciclo de bucle, los estados ocultos y de celda actualizados, junto con el índice de la palabra predicha anteriormente, se utilizan para hacer nuevas predicciones. El bucle continúa hasta que se alcanza la longitud máxima de la secuencia de salida o se encuentra el token <eos>.

Finalmente, las palabras en la lista output_sentence se concatenan usando un espacio y la cadena resultante se devuelve a la función de llamada.

Probando el modelo

Para probar el código, elegiremos aleatoriamente una oración de la lista input_sentences, recuperaremos la secuencia rellenada correspondiente para la oración y la pasaremos al método translate_sentence(). El método devolverá la oración traducida como se muestra a continuación.

Aquí está el script para probar la funcionalidad del modelo:

1
2
3
4
5
6
i = np.random.choice(len(input_sentences))
input_seq = encoder_input_sequences[i:i+1]
translation = translate_sentence(input_seq)
print('-')
print('Input:', input_sentences[i])
print('Response:', translation)

Aquí está la salida:

1
2
3
-
Input: You're not fired.
Response: vous n'êtes pas viré.

Brillante, ¿no? Nuestro modelo ha traducido con éxito la frase No estás despedido al francés. También puede verificar eso en Google Translate. Probemos con otro.

Nota: Dado que las oraciones se seleccionan al azar, lo más probable es que tengas una oración en inglés diferente traducida al francés.

Ejecute el script anterior una vez más para ver alguna otra oración en inglés traducida al idioma francés. Obtuve los siguientes resultados:

1
2
3
-
Input: I'm not a lawyer.
Response: je ne suis pas avocat.

El modelo ha traducido con éxito otra frase del inglés al francés.

Yendo más lejos: proyecto de extremo a extremo portátil

¿Tu naturaleza inquisitiva te hace querer ir más allá? Recomendamos consultar nuestro Proyecto guiado: ["Subtítulos de imágenes con CNN y Transformers con Keras"](https://wikihtp.com/courses/image-captioning-with-cnns-and -transformadores-con-keras/#cta){target="_blank"}.

En este proyecto guiado, aprenderá a crear un modelo de subtítulos de imágenes, que acepta una imagen como entrada y produce un subtítulo de texto como salida.

Aprenderás a:

  • Preprocesar texto
  • Vectorizar la entrada de texto fácilmente
  • Trabaje con la API tf.data y cree conjuntos de datos de alto rendimiento
  • Cree Transformers desde cero con TensorFlow/Keras y KerasNLP: la adición horizontal oficial a Keras para crear modelos NLP de última generación
  • Cree arquitecturas híbridas donde la salida de una red se codifica para otra

¿Cómo enmarcamos los subtítulos de las imágenes? La mayoría lo considera un ejemplo de aprendizaje profundo generativo, porque estamos enseñando a una red a generar descripciones. Sin embargo, me gusta verlo como una instancia de traducción automática neuronal: estamos traduciendo las características visuales de una imagen en palabras. A través de la traducción, estamos generando una nueva representación de esa imagen, en lugar de simplemente generar un nuevo significado. Verlo como traducción, y solo por generación de extensión, enfoca la tarea bajo una luz diferente y la hace un poco más intuitiva.

Enmarcar el problema como uno de traducción hace que sea más fácil averiguar qué arquitectura querremos usar. Los transformadores solo de codificador son excelentes para comprender el texto (análisis de opinión, clasificación, etc.) porque los codificadores codifican representaciones significativas. Los modelos de solo decodificador son excelentes para la generación (como GPT-3), ya que los decodificadores pueden inferir representaciones significativas en otra secuencia con el mismo significado. La traducción generalmente se realiza mediante una arquitectura de codificador-decodificador, donde los codificadores codifican una representación significativa de una oración (o imagen, en nuestro caso) y los decodificadores aprenden a convertir esta secuencia en otra representación significativa que es más interpretable para nosotros (como una oración).

Conclusión y perspectiva

La traducción automática neuronal es una aplicación bastante avanzada del procesamiento del lenguaje natural e implica una arquitectura muy compleja.

Este artículo explica cómo realizar la traducción automática neuronal a través de la arquitectura seq2seq, que a su vez se basa en el modelo codificador-decodificador. El codificador es un LSTM que codifica oraciones de entrada mientras que el decodificador decodifica las entradas y genera las salidas correspondientes. La técnica explicada en este artículo se puede usar para crear cualquier modelo de traducción automática, siempre que el conjunto de datos esté en un formato similar al que se usa en este artículo. También puede usar la arquitectura seq2seq para desarrollar chatbots.

La arquitectura seq2seq es bastante exitosa cuando se trata de mapear relaciones de entrada a salida. Sin embargo, existe una limitación para una arquitectura seq2seq. La arquitectura vanilla seq2seq que se explica en este artículo no es capaz de capturar el contexto. Simplemente aprende a asignar entradas independientes a salidas independientes. Las conversaciones en tiempo real se basan en el contexto y los diálogos entre dos o más usuarios se basan en lo que se dijo en el pasado. Por lo tanto, no se debe usar un modelo simple seq2seq basado en codificador-decodificador si desea crear un chatbot bastante avanzado.