La generación de datos utilizando una red neuronal recurrente se está convirtiendo en un método cada vez más popular y se está utilizando en muchas áreas de la informática. Desde el comienzo del nacimiento del concepto seq2seq en 2014, solo han pasado cinco años, pero el mundo ha visto muchas aplicaciones, comenzando con los modelos clásicos de traducción y reconocimiento de voz, y terminando con la generación de descripciones de objetos en fotografías.
Por otro lado, con el tiempo, la biblioteca Tensorflow, lanzada por Google específicamente para el desarrollo de redes neuronales, ganó popularidad. Naturalmente, los desarrolladores de Google no podían ignorar un paradigma tan popular como seq2seq, por lo que la biblioteca Tensorflow proporciona clases para el desarrollo dentro de este paradigma. Este artículo describe este sistema de clases.
Redes recurrentes
En la actualidad, las redes recurrentes son uno de los formalismos más conocidos y prácticos para construir redes neuronales profundas. Las redes recursivas están diseñadas para procesar datos en serie, por lo tanto, a diferencia de una célula normal (neurona), que recibe datos como entrada y genera el resultado de los cálculos, una célula recursiva contiene dos entradas y dos salidas.
Una de las entradas representa los datos del elemento actual de la secuencia, y la segunda entrada se llama estado y se transmite como resultado de los cálculos de celda en el elemento anterior de la secuencia.

La figura muestra la celda A, para la cual se ingresan los datos de un elemento de secuencia así como la condición no indicada aquí . En la salida, la celda A da el estado y el resultado del cálculo .
En la práctica, la secuencia de datos generalmente se divide en subsecuencias de cierta longitud fija y se pasa al cálculo por subconjuntos completos (lotes). En otras palabras, las subsecuencias son ejemplos de aprendizaje. Las entradas, salidas y estados de celda de una red recursiva son secuencias de números reales. Para el cálculo de entrada es necesario usar un estado que no fue el resultado de un cálculo en una secuencia de datos dada. Tales estados se llaman estados iniciales. Si la secuencia es lo suficientemente larga, entonces tiene sentido mantener el contexto de los cálculos en cada subsecuencia. En este caso, es posible transmitir el último estado calculado en la secuencia anterior como el estado inicial. Si la secuencia no es tan larga o la subsecuencia es el primer segmento, puede inicializar el estado inicial con ceros.
Por el momento, para entrenar redes neuronales en casi todas partes se utiliza el algoritmo de propagación hacia atrás de errores . El resultado del cálculo en el conjunto de ejemplos transmitidos (en nuestro caso, el conjunto de subsecuencias) se compara con el resultado esperado (datos marcados). La diferencia entre los valores reales y esperados se denomina error y este error se propaga a los pesos de la red en la dirección opuesta. Por lo tanto, la red se adapta a los datos etiquetados y, como regla, el resultado de esta adaptación funciona bien para los datos que la red no reunió en los ejemplos de entrenamiento inicial (hipótesis de generalización).
En el caso de una red recursiva, tenemos varias opciones sobre qué salidas considerar el error. Aquí describiremos dos principales:
- Puede considerar el error comparando la salida de la última celda de la subsecuencia con la salida esperada. Esto funciona bien para la tarea de clasificación. Por ejemplo, necesitamos determinar el color emocional de un tweet. Para hacer esto, seleccionamos tweets y los marcamos en tres categorías: negativo, positivo y neutral. La salida de la celda será de tres números: pesos de categoría. El tweet también se marcará con tres números: las probabilidades de que el tweet pertenezca a la categoría correspondiente. Después de calcular el error en un subconjunto de datos, puede propagarlo a través de la salida o el estado que desee.
- Puede leer el error inmediatamente en las salidas del cálculo de celda para cada elemento de la subsecuencia. Esto es muy adecuado para la tarea de predecir el siguiente elemento de una secuencia de los anteriores. Este enfoque se puede utilizar, por ejemplo, en el problema de determinar anomalías en series temporales de datos o en la tarea de predecir el siguiente carácter en el texto, para luego generarlo. La propagación de errores también es posible a través de estados o salidas.
A diferencia de una red neuronal normal totalmente conectada, una red recursiva es profunda en el sentido de que el error se propaga no solo desde las salidas de la red a sus pesos, sino también hacia la izquierda, a través de las conexiones entre estados. La profundidad de la red está así determinada por la longitud de la subsecuencia. Para propagar el error a través del estado de la red recursiva, hay un algoritmo especial. Su característica es que los gradientes de los pesos se multiplican entre sí, cuando el error se propaga de derecha a izquierda. Si el error inicial es mayor que la unidad, como resultado, el error puede llegar a ser muy grande. Por el contrario, si el error inicial es menor que la unidad, entonces, en algún lugar al comienzo de la secuencia, el error puede desvanecerse. Esta situación en la teoría de las redes neuronales se llama el carrusel del error estándar. Para evitar tales situaciones durante el entrenamiento, se inventaron células especiales que no tienen tales inconvenientes. La primera de estas células fue LSTM , ahora hay una amplia gama de alternativas, de las cuales la GRU más popular.
Una buena introducción a las redes de recurrencia se puede encontrar en este artículo . Otra fuente conocida es un artículo del blog de Andrey Karpaty.
La biblioteca Tensorflow tiene muchas clases y funciones para implementar redes recursivas. Aquí hay un ejemplo de cómo crear una red recursiva dinámica basada en una celda del tipo GRU:
cell = tf.contrib.rnn.GRUCell(dimension) outputs, state = tf.nn.dynamic_rnn(cell, input, sequence_length=input_length, dtype=tf.float32)
En este ejemplo, se crea una celda GRU, que luego se usa para crear una red recursiva dinámica. El tensor de datos de entrada y las longitudes reales de las subsecuencias se transmiten a la red. Los datos de entrada siempre se especifican mediante un vector de números reales. Para un solo valor, por ejemplo, un código de símbolo o una palabra, el llamado incrustación: asigna este código a alguna secuencia de números. La función de crear una red recursiva dinámica devuelve un par de valores: una lista de salidas de red para todos los valores de la secuencia y el último estado calculado. Como entrada, la función toma una celda, datos de entrada y un tensor de longitud de subsecuencia.
Una red recursiva dinámica difiere de una estática en que no crea una red de celdas de red para la subsecuencia por adelantado (en la etapa de determinación del gráfico de cálculo), sino que lanza las celdas en las entradas dinámicamente durante el cálculo del gráfico en los datos de entrada. Por lo tanto, esta función necesita conocer las longitudes de subsecuencias de los datos de entrada para detenerse en el momento adecuado.
Generando modelos basados en redes de recurrencia
Generando redes de recurrencia
Anteriormente, consideramos dos métodos para calcular los errores de las redes recursivas: en la última salida o en todas las salidas para una secuencia dada. Aquí consideramos el problema de generar secuencias. La capacitación de la red de generadores se basa en el segundo método de lo anterior.
Con más detalle, estamos tratando de entrenar una red recursiva para predecir el siguiente elemento de una secuencia. Como se mencionó anteriormente, la salida de una celda en una red recursiva es simplemente una secuencia de números. Este vector no es muy conveniente para el aprendizaje, por lo tanto, introducen otro nivel, que recibe este vector en la entrada, y en la salida da el peso de las predicciones. Este nivel se llama nivel de proyección y le permite comparar la salida de la celda en un elemento dado de la secuencia con la salida esperada en los datos etiquetados.
Para ilustrar, considere la tarea de generar texto que se representa como una secuencia de caracteres. La longitud del vector de salida del nivel de proyección es igual al tamaño del alfabeto del texto fuente. El tamaño del alfabeto generalmente no supera los 150 caracteres, si cuenta los caracteres de los idiomas ruso e inglés, así como los signos de puntuación. La salida del nivel de proyección es un vector con la longitud del alfabeto, donde cada símbolo corresponde a una determinada posición en este vector: el índice de este símbolo. Los datos etiquetados también son un vector que consiste en ceros, donde uno se encuentra en la posición del personaje que sigue la secuencia.
Para el entrenamiento, utilizamos dos secuencias de datos:
- Una secuencia de caracteres en el texto fuente, al principio de la cual se agrega un carácter especial que no forma parte del texto fuente. Por lo general, se conoce como ir .
- La secuencia de caracteres del texto fuente tal cual, sin adiciones.
Ejemplo para el texto "mamá lavó el marco":
['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', ''] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '']
Para el entrenamiento, generalmente se forman minibatches, que consisten en un pequeño número de ejemplos. En nuestro caso, se trata de cadenas que pueden tener diferentes longitudes. El código descrito a continuación utiliza el siguiente método para resolver el problema de diferentes longitudes. De las muchas líneas en este minipaquete, se calcula la longitud máxima. Todas las demás líneas se rellenan con un carácter especial (relleno) para que todos los ejemplos en el minipacket tengan la misma longitud. En el ejemplo de código a continuación, la cadena del pad se usa como dicho carácter. Además, para una mejor generación, al final del ejemplo, agregue el símbolo para el final de la oración - eos . Por lo tanto, en realidad, los datos del ejemplo se verán un poco diferentes:
['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>'] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>', '<pad>']
La primera secuencia se alimenta a la entrada de la red, y la segunda secuencia se usa como datos etiquetados. El entrenamiento de predicción se basa en desplazar la secuencia original un personaje a la izquierda.
Entrenamiento y desove
Entrenamiento
El algoritmo de aprendizaje es bastante simple. Para cada elemento de la secuencia de entrada, calculamos el vector de salida de su nivel de proyección y lo comparamos con el marcado. La única pregunta es cómo calcular el error. Puede usar el error cuadrático medio, pero para calcular el error en esta situación, es mejor usar entropía cruzada . La biblioteca Tensorflow proporciona varias funciones para su cálculo, aunque no hay nada que detenga la implementación de la fórmula de cálculo directamente en el código.
Para mayor claridad, presentamos alguna notación. Por symbol_id indicaremos el identificador del símbolo (su número de serie en el alfabeto). El término símbolo aquí es bastante arbitrario y simplemente significa un elemento del alfabeto. El alfabeto puede no contener símbolos, sino palabras o incluso algunos conjuntos de atributos más complejos. El término symbol_embedding se usará para denotar el vector de números que corresponde a un elemento dado del alfabeto. Por lo general, estos conjuntos de números se almacenan en una tabla de tamaños que coincide con el tamaño del alfabeto.
Tensorflow proporciona una función que le permite acceder a la tabla de incrustación y reemplazar los índices de caracteres con sus vectores de incrustación. Primero, definimos una variable para almacenar la tabla:
embedding_table = tf.Variable(tf.random_uniform([alphabet_size, embedding_size]))
Después de eso, puede convertir los tensores de entrada en tensores de inserción:
input_embeddings = tf.nn.embedding_lookup(embedding_table, input_ids)
El resultado de la llamada a la función es un tensor de la misma dimensión que se transfirió a la entrada, pero como resultado, todos los índices de caracteres se reemplazan con las secuencias de incrustación correspondientes.
Engendrar
Para calcular, una celda de una red recursiva necesita un estado y el carácter actual. El resultado del cálculo es una salida y un nuevo estado. Si aplicamos el nivel de proyección a la salida, podemos obtener un vector de pesos donde el peso en la posición correspondiente puede considerarse (muy condicionalmente) como la probabilidad de que este símbolo aparezca en la siguiente posición de la secuencia.
Se pueden usar varias estrategias para seleccionar el siguiente símbolo en función del vector de peso generado por el nivel de proyección:
- Estrategia de búsqueda codiciosa. Cada vez que seleccionamos el símbolo con el mayor peso, es decir muy probablemente en esta situación, pero no necesariamente la más apropiada en el contexto de toda la secuencia.
- Estrategia para elegir la mejor secuencia (búsqueda de haz). No seleccionamos un símbolo a la vez, pero recordamos varias variantes de los símbolos más probables. Después de calcular todas estas opciones para todos los elementos de la secuencia generada, seleccionamos la secuencia de caracteres más probable, teniendo en cuenta el contexto de toda la secuencia. Por lo general, esto se implementa por medio de una matriz cuyo ancho es igual a la longitud de la secuencia y la altura al número de anchos de generación de haz. Una vez completada la generación de las variantes de secuencia, se utiliza una de las variantes del algoritmo de Viterbi para seleccionar la secuencia más probable.
Sistema de tipo seq2seq de la biblioteca Tensorflow
Dado lo anterior, está claro que la implementación de modelos generativos basados en redes de recurrencia es una tarea bastante difícil de codificar. Por lo tanto, naturalmente, se propusieron sistemas de clase para facilitar la solución de este problema. Uno de estos sistemas se llama seq2seq, luego describimos la funcionalidad de sus tipos principales.
Pero, antes que nada, algunas palabras sobre el nombre de la biblioteca. El nombre seq2seq es la abreviatura de secuencia a secuencia (de secuencia a secuencia). Se propuso la idea original de generar una secuencia para implementar un sistema de traducción. La secuencia de entrada de palabras se alimentó a la entrada de una red recursiva, llamada codificador en este sistema. El resultado de esta red recursiva fue el estado del cálculo de la celda en el último carácter de la secuencia. Este estado se presentó como el estado inicial de la segunda red recursiva, el decodificador, que fue entrenado para generar la siguiente palabra. Las palabras fueron utilizadas como símbolos en ambas redes. Los errores en el decorador se propagaron al codificador a través del estado transmitido. El vector de estado mismo en esta terminología se llamaba vector de pensamiento. La presentación intermedia se utilizó en los modelos de traducción tradicionales y, como regla, era un gráfico que representaba la estructura del texto de entrada para la traducción. El sistema de traducción generó texto de salida basado en esta estructura intermedia.
En realidad, la implementación de seq2seq en Tensorflow pertenece a la parte del decodificador, sin afectar el codificador. Por lo tanto, sería correcto llamar a la biblioteca 2seq, pero la fuerza de la tradición y la inercia de pensar aquí obviamente prevalecieron sobre el sentido común.
Los dos metatipos principales en la biblioteca seq2seq son:
- Clase auxiliar .
- Decodificador de clase.
Los desarrolladores de la biblioteca identificaron estos tipos basándose en las siguientes consideraciones. Consideremos el proceso de aprendizaje y el proceso de generación, que describimos anteriormente, desde un ángulo ligeramente diferente.
Para el entrenamiento necesitas:
- Para cada carácter, pase el cálculo del estado actual y la incrustación del carácter actual.
- Recuerde el estado de salida y la proyección calculada para la salida.
- Obtenga el siguiente personaje en la secuencia y vaya al paso 1.
Después de eso, puede comenzar a contar los errores comparando los resultados de los cálculos con los siguientes caracteres de la secuencia.
Para generarlo es necesario:
- Para cada carácter, pase el cálculo del estado actual y la incrustación del carácter actual.
- Recuerde el estado de salida y la proyección calculada para la salida.
- Calcule el siguiente carácter como el máximo de los índices de nivel de proyección y vaya al paso 1.
Como se puede ver en la descripción, los algoritmos son muy similares. Por lo tanto, los desarrolladores de la biblioteca decidieron encapsular el procedimiento para obtener el siguiente carácter en la clase Helper. Para el entrenamiento, esto es solo leer el siguiente personaje de la secuencia, y para generarlo, seleccionar el personaje con el peso máximo (por supuesto, para la búsqueda codiciosa).
En consecuencia, la clase base Helper implementa el método next_inputs para obtener el siguiente carácter del estado y actual, así como el método de muestra para obtener índices de caracteres del nivel de proyección. La clase TrainingHelper se proporciona para la capacitación, y la clase GreedyEmbeddingHelper está disponible para la generación codiciosa. Desafortunadamente, el modelo de búsqueda de haz no encaja en este tipo de sistema, por lo tanto, se implementa una clase especial BeamSearchDecoder en la biblioteca para esto. sin usar Helper.
La clase Decoder proporciona una interfaz para implementar un decodificador. De hecho, la clase proporciona dos métodos:
- inicializar para inicializar al comienzo del trabajo.
- paso para implementar un paso o generación de aprendizaje. El contenido de este paso está determinado por el Ayudante correspondiente.
La biblioteca implementa la clase BasicDecoder , que se puede usar tanto para entrenar como para reproducirse con los asistentes TrainingHelper y GreedyEmbeddingHelper. Estas tres clases suelen ser suficientes para implementar modelos de generación basados en redes de recurrencia.
Finalmente, las funciones dynamic_decode se usan para organizar el paso a través de una entrada o secuencia generada.
A continuación, consideraremos un ejemplo ilustrativo, que muestra métodos para construir modelos de generación para varios tipos de biblioteca seq2seq.
Ejemplo ilustrativo
En primer lugar, debe decirse que todos los ejemplos se implementan en Python 2.7. Se puede encontrar una lista de bibliotecas adicionales en el archivo require.txt.
Como ejemplo ilustrativo, considere parte de los datos para el Desafío de normalización de texto: concurso de idioma ruso realizado por Kaggle por Google en 2017. El propósito de esta competencia fue convertir el texto ruso en una forma adecuada para leer. El texto del concurso se desglosó en expresiones mecanografiadas. Los datos de entrenamiento se especificaron en un archivo CSV de la siguiente forma:
"sentence_id","token_id","class","before","after" 0,0,"PLAIN","","" 0,1,"PLAIN","","" 0,2,"PLAIN","","" 0,3,"DATE","1862 "," " 0,4,"PUNCT",".","." 1,0,"PLAIN","","" 1,1,"PLAIN","","" 1,2,"PLAIN","","" 1,3,"PLAIN","","" 1,4,"PLAIN","","" 1,5,"PLAIN","","" 1,6,"PLAIN","","" 1,7,"PLAIN","","" 1,8,"PLAIN","","" 1,9,"PUNCT",".","." ...
En el ejemplo anterior, una expresión de tipo DATE es interesante, en ella, "1862" se traduce en "mil ochocientos sesenta y dos años". Para ilustrar, consideramos los datos de tipo DATE solo como pares de la forma (expresión antes, expresión después). Inicio del archivo de datos:
before,after 1862 , 1811 , 12 2013, 15 2013, 1905 , 17 2014, 7 2010 , 1 , 1843 , 30 2007 , 1846 , 1996 , 9 , ...
Construiremos el modelo generador utilizando la biblioteca seq2seq, en la que el codificador se implementará a nivel de símbolo (es decir, los elementos del alfabeto son símbolos), y el decodificador usará las palabras como alfabeto. El código de muestra, como los datos, está disponible en el repositorio en Github .
Los datos de entrenamiento se dividen en tres subconjuntos: train.csv, test.csv y dev.csv, para entrenamiento, prueba y verificación de reentrenamiento, respectivamente. Los datos están en el directorio de datos. Se implementan tres modelos en el repositorio: seq2seq_greedy.py, seq2seq_attention.py y seq2seq_beamsearch.py. Aquí miramos el código para el modelo de búsqueda codicioso básico.
Todos los modelos usan la clase Estimator para implementar. El uso de esta clase le permite simplificar la codificación sin distraerse con partes que no sean modelos. Por ejemplo, no es necesario implementar un ciclo de transferencia de datos para el entrenamiento, crear sesiones para trabajar con Tensorflow, pensar en transferir datos a Tensorboard, etc. Estimator requiere solo dos funciones para su implementación: para la transferencia de datos y para construir un modelo. Los ejemplos también usan la clase Dataset para pasar datos para su procesamiento. Esta implementación moderna es mucho más rápida que los diccionarios tradicionales para transferir datos del formulario feed_dict.
Considere un código de generación de datos para capacitación y generación.
def parse_fn(line_before, line_after):
La función input_fn se usa para crear una colección de datos que el Estimador luego pasa al entrenamiento y la generación. El tipo de datos se establece primero. Este es un par de la forma ((secuencia del codificador, longitud), (secuencia del decodificador, secuencia del decodificador con un prefijo, longitud)). La cadena "" se usa como prefijos, cada secuencia de codificador termina con una palabra especial "". Además, debido al hecho de que las secuencias (tanto de entrada como de salida) tienen una longitud desigual, se utiliza el símbolo de relleno con el valor "".
El código de preparación de datos lee el archivo de datos, divide la cadena del codificador en caracteres y la cadena del decodificador en palabras, utilizando la biblioteca nltk para esto. Una fila procesada de esta manera es un ejemplo de datos de entrenamiento. La colección generada se divide en mini paquetes y la cantidad de datos se clona de acuerdo con el número de eras de entrenamiento (cada época es un pase de datos).
Trabajar con diccionarios
Los diccionarios se almacenan como una lista en archivos, una línea para una palabra o carácter. Para construir diccionarios, use el script build_vocabs.py. Los diccionarios generados se encuentran en el directorio de datos como archivos de la forma vocab. *. Txt.
Código para leer diccionarios:
Aquí, probablemente, la función index_table_from_file, que lee las entradas del diccionario de un archivo, es interesante, y su parámetro num_oov_buckets es el número de canastas de vocabulario. Por defecto, este número es igual a uno, es decir todas las palabras que no están en el diccionario tienen el mismo índice igual al tamaño del diccionario + 1. Tenemos tres palabras desconocidas: "", "" y "", para las cuales queremos tener índices diferentes. Por lo tanto, establezca este parámetro en el número tres. Desafortunadamente, debe volver a leer el archivo de entrada para obtener el número de palabras en el diccionario como una constante de tiempo para configurar el gráfico del modelo.
Todavía necesitamos crear una tabla para implementar la incrustación - _source_embedding, así como para traducir cadenas de palabras a cadenas de identificación:
Implementación del codificador
Para el codificador, utilizaremos una red recursiva bidireccional con varios niveles. , , .
GRU, MultiRNNCell, , rnn.Cell. ,
sequence_length — , , .
, , , . , 128, 256. , , 128. .
. Porque , , bidirectional_dynamic_rnn, , . , . , .. . , , . , , .
, . .
TrainingHelper + BasicDecoder.
.
GreedyEmbeddingHelper "", "". . , , dynamic_decode . , , . , , .
, seq2seq.
, , sequence_mask.
Adam , .
optimizer = tf.train.AdamOptimizer(learning_rate=params.get('lr', .001)) grads, vs = zip(*optimizer.compute_gradients(loss)) grads, gnorm = tf.clip_by_global_norm(grads, params.get('clip', .5)) train_op = optimizer.apply_gradients(zip(grads, vs), global_step=tf.train.get_or_create_global_step())
. 0.9 . , , , . , .
24 1944 1 2003 1992 . 11 1927 1969 1 2016 1047 1863 17 22 2014
. — , — , — .
, — . . , ( ), . . , .
Conclusión
seq2seq. , , . , .
. Tensorflow , , . , , . , . , , padding , embedding ? , , . — . , , . , , , . , . , , , , .