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

P

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 la resolución de problemas de secuencia con Keras, expliqué cómo resolver muchos o muchos problemas de secuencia en los que tanto las entradas como las salidas se dividen en varios pasos de tiempo. La arquitectura seq2seq es un tipo de modelado de secuencia de muchos a muchos, y se utiliza comúnmente para una variedad de tareas como resumen de texto, desarrollo de chatbot, modelado conversacional 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 tiene un buen conocimiento de las redes neuronales recurrentes , en particular LSTM . El código de este artículo está escrito en Python con la biblioteca de Keras. Por lo tanto, se supone que tiene un buen conocimiento del lenguaje Python, así como de la biblioteca de Keras. Entonces, sin más preámbulos, comencemos.

Bibliotecas y opciones de configuración

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

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:

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 un modelo de este tipo, necesitamos un conjunto de datos que contenga oraciones en inglés y sus traducciones al francés. Afortunadamente, este conjunto de datos está disponible gratuitamente en este enlace . Descarga el archivo fra-eng.zipy extráelo. Luego verá el fra.txtarchivo. En cada línea, el archivo de texto contiene una oración en inglés y su traducción al francés, separados por una pestaña. Las primeras 20 líneas del fra.txtarchivo se ven así:

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

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 símbolo de inicio de oración. El resultado es la oración de destino real con un símbolo de fin 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 guión que hace eso:

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 fra.txtarchivo 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 forbucle, el fra.txtarchivo se lee línea por línea. 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 input_sentences[]lista. La subcadena a la derecha de la pestaña es la oración francesa traducida correspondiente. La <eos>ficha, que marca el final de la oración, se antepone a la oración traducida y la oración resultante se agrega a la output_sentences[]lista. De manera similar, el <sos>token, que significa “comienzo de oración”, se concatenó al comienzo de la oración traducida y el resultado se agregará a la output_sentences_inputs[]lista. El ciclo termina si el número de oraciones agregadas a las listas es mayor que elNUM_SENTENCES variable, es decir, 20.000.

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

num samples input: 20000
num samples output: 20000
num samples output input: 20000

Ahora vamos a imprimir al azar una frase del input_sentences[], output_sentences[]y output_sentences_inputs[]listas:

print(input_sentences[172])
print(output_sentences[172])
print(output_sentences_inputs[172])

Aquí está el resultado:

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

Puede ver la oración original, es decir I'm ill; su traducción correspondiente en la salida, es decir Je suis malade. <eos>. Fíjense, aquí tenemos <eos>token 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 Tokenizerclase de la keras.preprocessing.textbiblioteca. La tokenizerclase realiza dos tareas:

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

Esto es extremadamente importante ya que los algoritmos de aprendizaje profundo y de Machine Learning funcionan con números. La siguiente secuencia de comandos se usa para tokenizar las oraciones de entrada:

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 word_indexatributo de la Tokenizerclase 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 el número de palabras únicas en el diccionario y la longitud de la oración más larga en la entrada:

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 convertir en token de la misma manera que se muestra a continuación:

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á el resultado:

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

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 son normalmente más cortas y contienen un número 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 mediante el relleno.

En el 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 utilizará para rellenar las oraciones de entrada y salida, respectivamente. La oración más larga de la entrada contiene 6 palabras. Para las oraciones que contienen menos de 6 palabras, se agregarán ceros en los índices vacíos. La siguiente secuencia de comandos aplica relleno a las oraciones de entrada.

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 de arriba imprime la forma de las oraciones de entrada acolchadas. También se imprime la secuencia de enteros rellenados para la oración en el índice 172. Aquí está el resultado:

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

Dado que 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 números enteros para la oración en el índice 172 de la oración de entrada, puede ver que hay tres ceros, seguidos por los valores 6 y 539. Puede recordar que la oración original en el índice 172 es I'm ill. El tokenizador dividió la oración en dos palabras I'my las illconvirtió en números enteros, y luego aplicó el relleno previo agregando tres ceros al comienzo de la secuencia entera correspondiente para la oración en el índice 172 de la lista de entrada.

Para verificar que los valores enteros para i'my illson 6 y 539 respectivamente, puede pasar las palabras al word2index_inputsdiccionario, como se muestra a continuación:

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

Salida:

6
539

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

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])

Salida:

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 números enteros correspondientes del word2idx_outputsdiccionario, debería ver 2, 3, 6 y 188 impresos en la consola, como se muestra aquí:

print(word2idx_outputs["<sos>"])
print(word2idx_outputs["je"])
print(word2idx_outputs["suis"])
print(word2idx_outputs["malade."])

Salida:

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, se rellenaron ceros al principio. 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 los ceros se rellenaron al principio. Por otro lado, en el caso del decodificador, el procesamiento comienza desde el principio de una frase y, por lo tanto, se realiza un relleno posterior en las entradas y salidas del decodificador.

Incrustaciones de palabras

He escrito un artículo detallado sobre incrustaciones de palabras, que es posible que desee comprobar para comprender 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 utilizamos modelos de aprendizaje profundo y los modelos de aprendizaje profundo funcionan con números, debemos convertir nuestras palabras en sus correspondientes representaciones vectoriales numéricas. Pero ya convertimos nuestras palabras en números enteros. Entonces, ¿cuál es la diferencia entre la representación de números 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 solo 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 utilizar incrustaciones de palabras personalizadas o puede utilizar incrustaciones de palabras previamente entrenadas.

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

Primero creemos incrustaciones de palabras para las entradas. Para hacerlo, necesitamos cargar los vectores de palabras 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:

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.

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 illimprimamos las incrustaciones de palabras para la palabra usando el diccionario de incrustaciones de palabras GloVe.

print(embeddings_dictionary["ill"])

Salida:

[ 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 illes 539. Ahora revisemos el índice 539 de la matriz de incrustación de palabras.

print(embedding_matrix[539])

Salida:

[ 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 illen el diccionario de GloVe, lo que confirma que las filas de la matriz de incrustación representan las 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 para nuestro modelo LSTM.

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

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

Creando el modelo

Ahora es 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 tanto, la forma final de la salida será:

(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:

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:

decoder_targets_one_hot.shape

Salida:

(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 one-hot, ya que usaremos la función de activación softmax en la capa densa. Para crear dicha 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 para <sos> je suis maladees [ 2 3 6 188 0 0 0 0 0 0 0 ]. En la decoder_targets_one_hotmatriz de salida, en la segunda columna de la primera fila, se insertará 1. De manera similar, en el tercer índice de la segunda fila, se insertará otro 1, y así sucesivamente.

Mira el siguiente guión:

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 la celda del LSTM.

El siguiente script define el codificador:

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 la celda del codificador y la oración de entrada, que en realidad será la oración de salida con un <sos>token agregado al principio.

El siguiente script crea el decodificador LSTM:

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í:

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

El siguiente paso es compilar el modelo:

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:

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

Salida:

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

Aquí la lstm_2capa es el decodificador LSTM. El input_2contiene las sentencias de salida con <sos>símbolo anexado al comienzo. El input_2también se pasa a través de una capa de encaje y se usa como entrada para el LSTM decodificador, 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 fit()método:

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 2.000 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 está sobreajustado. Para reducir el sobreajuste, puede agregar abandonos o más registros. Solo estamos capacitando 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 i'm ill. La oración se traduce de la siguiente manera:

// 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(h2,c3)

step 3:

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

Puede ver que la entrada al decodificador y la salida del decodificador son conocidas y 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 <sos>y <eos>tokens. 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>porque todas las oraciones de salida comienzan con <sos>.

Un ejemplo de lo que sucede durante la predicción es el siguiente. Traduciremos nuevamente la oración i'm ill:

// 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(h2,c3)

step 3:

enc(h2,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 se 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 la celda del codificador, y el <sos>, se utilizan como entrada al decodificador. El decodificador predice una palabra y1que puede ser cierta o no. Sin embargo, según nuestro modelo, la probabilidad de una 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 usa como entrada para el decodificador, que predice y2. El proceso continúa hasta que <eos>se encuentra el token. Luego, todas las salidas previstas del decodificador se concatenan para formar la oración de salida final. Modifiquemos nuestro modelo para implementar esta lógica.

El modelo de codificador sigue siendo el mismo:

encoder_model = Model(encoder_inputs_placeholder, encoder_states)

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

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, necesitamos modificar la capa de incrustación del decodificador de la siguiente manera:

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

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

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:

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

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

decoder_model = Model(
    [decoder_inputs_single] + decoder_states_inputs,
    [decoder_outputs] + decoder_states
)

Tracemos ahora nuestro decodificador LSTM modificado que hace predicciones:

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

Salida:

En la imagen de arriba lstm_2está 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_3y input_4). Puede ver que la forma de la oración de entrada es ahora, (none,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 fue (None,6)dado que la entrada contenía una oración completa con una longitud máxima de 6.

Haciendo predicciones

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

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 francés. Para hacerlo, necesitamos volver a convertir los números enteros en palabras. Crearemos nuevos diccionarios tanto para entradas como para salidas donde las claves serán los enteros y los valores correspondientes serán las palabras.

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 frase en inglés de secuencia rellenada de entrada (en forma de número entero) y devolverá la frase en francés traducida. Mira el translate_sentence()método:

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 al encoder_model, que predice el estado oculto y el estado de la celda, que se almacenan en la states_valuevariable.

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

Después de eso, eosse inicializa la variable, que almacena el valor entero del <eos>token. En la siguiente línea, output_sentencese define la lista, que contendrá la traducción prevista.

A continuación, ejecutamos un forbucle. El número de ciclos de ejecución del forbucle es igual a la longitud de la oración más larga de la salida. Dentro del bucle, en la primera iteración, decoder_modelpredice 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 idxvariable. Si el valor del índice predicho es igual al <eos>token, el ciclo termina. De lo contrario, si el índice predicho es mayor que cero, la palabra correspondiente se recupera del idx2worddiccionario y se almacena en la wordvariable, que luego se agrega a la output_sentencelista. losstates_valueLa variable se actualiza con el nuevo estado oculto y de celda del decodificador y el índice de la palabra predicha se almacena en la target_seqvariable. En el siguiente ciclo de bucle, los estados ocultos y de celda actualizados, junto con el índice de la palabra predicha previamente, se utilizan para hacer nuevas predicciones. El bucle continúa hasta que se alcanza la longitud máxima de secuencia de salida o <eos>se encuentra el token.

Finalmente, las palabras en la output_sentencelista se concatenan usando un espacio y la cadena resultante se devuelve a la función que llama.

Probando el modelo

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

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

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á el resultado:

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

Brillante, ¿no? Nuestro modelo ha traducido con éxito la frase You're not firedal francés. También puede verificarlo en Google Translate. Probemos con otro.

Nota: Dado que las oraciones se seleccionan al azar, lo más probable es que tenga 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:

-
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.

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 sentencias de entrada mientras que el decodificador decodifica las entradas y genera las salidas correspondientes. La técnica explicada en este artículo se puede utilizar para crear cualquier modelo de traducción automática, siempre que el conjunto de datos tenga un formato similar al utilizado en este artículo. También puede utilizar la arquitectura seq2seq para desarrollar chatbots.

La arquitectura seq2seq es bastante exitosa cuando se trata de mapear las relaciones de entrada con la salida. Sin embargo, existe una limitación para una arquitectura seq2seq. La arquitectura vanilla seq2seq explicada 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 utilizar un modelo seq2seq basado en codificador-decodificador simple si desea crear un chatbot bastante avanzado.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad