Cómo implementar una red neuronal recursiva con PyTorch
Desde Siri hasta Google Translate, las redes neuronales profundas han logrado grandes avances en la comprensión automática del lenguaje natural. La mayoría de estos modelos tratan el lenguaje como una secuencia monótona de palabras o caracteres y utilizan un modelo llamado red neuronal recurrente (RNN) para procesar esa secuencia. Pero muchos lingüistas creen que el lenguaje se entiende mejor como frases jerárquicas con una estructura de árbol. Un modelo de aprendizaje profundo llamado red neuronal recursiva tiene en cuenta esta estructura y se han realizado muchos trabajos en esta área. Si bien estos modelos son extremadamente difíciles e ineficientes de implementar, un nuevo marco de aprendizaje profundo llamado PyTorch puede hacerlos y otros modelos complejos de procesamiento de lenguaje natural mucho más fáciles.
Aunque las redes neuronales recurrentes demuestran la flexibilidad de PyTorch, también admite ampliamente otros marcos de aprendizaje profundo, en particular, puede proporcionar un soporte poderoso para los cálculos de visión por computadora. PyTorch es el resultado de desarrolladores de Facebook AI Research y varios otros laboratorios. El marco combina la biblioteca back-end eficiente y flexible acelerada por GPU de Torch7 con una interfaz intuitiva de Python. Se caracteriza por la creación rápida de prototipos, la legibilidad del código y el soporte más amplio. modelo de aprendizaje profundo.
Inicie SPINN
El artículo en el enlace (/jekbradbury/examples/tree/spinn/snli) detalla una implementación de PyTorch de una red neuronal recurrente con un rastreador de ciclos (recurrent tracker) y el nodo TreeLSTM, también conocido como SPINN: SPINN es un ejemplo de un modelo de aprendizaje profundo para el procesamiento del lenguaje natural que es difícil de construir con muchos marcos populares. La implementación del modelo aquí utiliza procesamiento por lotes, por lo que puede aprovechar la aceleración de GPU, lo que hace que se ejecute significativamente más rápido que la versión que no utiliza procesamiento por lotes.
SPINN significa Red neuronal de analizador-intérprete aumentada por pila, que fue introducida por Bowman et al en 2016 como un método para resolver tareas de razonamiento en lenguaje natural. En este artículo se utilizó el conjunto de datos SNLI de la Universidad de Stanford. .
La tarea consiste en clasificar pares de afirmaciones en tres categorías: suponiendo que la afirmación 1 es el título exacto de una imagen invisible, entonces la afirmación 2 es (a) definitivamente, (b) probablemente o (c) definitivamente no. un título exacto? (Estas clases se denominan inclusión, neutral y contradicción respectivamente). Por ejemplo, si la oración es "Dos perros corrían por un campo", la vinculación podría cambiar el par de oraciones a "Animales al aire libre" y la neutralidad podría cambiar el par de oraciones a "Algunos cachorros corren e intentan atrapar animales vivos con un palo". ", una contradicción podría convertir este par de frases en "la mascota está sentada en el sofá".
En particular, el objetivo inicial de estudiar SPINN es codificar cada oración en una representación vectorial de longitud fija antes de determinar la relación entre las oraciones (también hay otras formas, como el modelo de atención cada uno). parte de cada oración se compara entre sí utilizando un método de enfoque suave).
El conjunto de datos se genera por máquina utilizando el método del árbol de análisis sintáctico. El árbol de análisis sintáctico agrupa las palabras de cada oración en frases y cláusulas con significado independiente. Muchos lingüistas creen que los humanos combinan los significados de las palabras y entienden el lenguaje de una manera jerárquica como el árbol de arriba, por lo que valdría la pena intentar construir una red neuronal de la misma manera.
El siguiente ejemplo es una oración del conjunto de datos con un árbol de análisis representado por corchetes anidados:
( ( La iglesia ) ( ( tiene ( grietas ( en ( el techo ) ) ) ) . ) ) p >
Una forma de codificar esta oración es usar una red neuronal con un árbol de análisis para construir una capa de red neuronal Reducir. Esta capa de red neuronal puede combinar pares de palabras (representadas por incrustaciones de palabras, como GloVe) y/o. frases, y luego aplique esta capa (función) de forma recursiva, utilizando el resultado producido por el último Reducir como codificación de la oración:
X = Reducir("the", "techo")
Y = Reducir(“en”, ¿Leer y conservar el contexto de las oraciones sin dejar de combinar frases usando árboles de análisis sintáctico? ¿O qué pasa si quiero entrenar una red para que construya su propio árbol de análisis y haga que el árbol de análisis lea oraciones en función de las palabras que ve? Esta es la misma forma, aunque ligeramente diferente, de escribir el árbol de análisis:
La iglesia ) tiene grietas en el techo ) ) ) ) ) )
O use la tercera opción expresada como. sigue:
PALABRAS: La iglesia tiene grietas en el techo.
PARSES: S S R S S S S S R R R R S R R
Todo lo que hice fue quitar los soportes de apertura y usar marcas "S" "shift" y reemplaza el corchete de cierre con "R" para "reducir". Pero ahora puedes leer la información de izquierda a derecha como un conjunto de instrucciones para operar en una pila y en un buffer tipo pila, y obtener exactamente los mismos resultados que con el método recursivo anterior:
1. Poner la palabra en el buffer.
2. Saque "The" del frente del búfer y empújelo hacia la parte superior de la pila, seguido de "church".
3. Extraiga los primeros 2 valores de la pila, aplíquelos a Reducir y vuelva a colocar el resultado en la pila.
4. Saque "has" del búfer, luego empújelo hacia la pila, luego "agrieta", luego "dentro", luego "el", luego "techo".
5. Repita cuatro veces: extraiga 2 valores de pila, aplíquelos a Reducir y luego presione el resultado.
6. Extraiga "." del búfer y empújelo al nivel superior de la pila.
7. Repita dos veces: extraiga 2 valores de pila, aplíquelos a Reducir y luego presione el resultado.
8. Extraiga el valor restante de la pila y devuélvalo como una codificación de oración.
También quiero preservar el contexto de la oración para que al aplicar la capa Reducir a la segunda mitad de la oración, tenga en cuenta información sobre la parte de la oración que el sistema ya ha leído. Así que voy a reemplazar la función Reducir de dos parámetros con una función de tres parámetros que toma como entrada una cláusula izquierda, una cláusula derecha y el estado de contexto de la oración actual. Este estado es creado por la segunda capa de la red neuronal, una unidad llamada rastreador de bucle. Tracker genera un nuevo estado:
context[t+1] = Tracker(context[t], b, s1, s2)
Fácil de imaginar escribiendo código en tu lenguaje de programación favorito . Estas cosas.
Para cada oración a procesar, carga la siguiente palabra del búfer, ejecuta el rastreador, verifica si empuja la palabra a la pila o ejecuta una función Reducir, lo hace y luego repite hasta que haya terminado de procesar la totalidad; oración. Aplicado a una sola oración, el proceso forma una red neuronal profunda grande y compleja, aplicando sus dos capas entrenables una y otra vez en una pila de operaciones. Sin embargo, si está familiarizado con los marcos tradicionales de aprendizaje profundo como TensorFlow o Theano, sabrá que les resulta difícil implementar procesos tan dinámicos. Vale la pena tomarse un momento para revisar y explorar por qué PyTorch marca la diferencia.
Teoría de grafos
Figura 1: Representación de la estructura gráfica de una función
Las redes neuronales profundas son esencialmente funciones complejas con una gran cantidad de parámetros. El propósito del aprendizaje profundo es optimizar estos parámetros calculando derivadas parciales (gradientes) medidas por una función de pérdida (pérdida). Si la función se representa como una estructura gráfica computacional (Figura 1), entonces recorrer el gráfico hacia atrás permite calcular estos gradientes sin trabajo redundante. Cada marco moderno de aprendizaje profundo se basa en este concepto de retropropagación, por lo que cada marco necesita una forma de representar el gráfico computacional.
En muchos marcos populares, incluidos TensorFlow, Theano y Keras, así como la biblioteca nngraph de Torch7, el gráfico computacional es un objeto estático que se construye con anticipación. El gráfico está definido con un código que parece una expresión matemática, pero sus variables son en realidad marcadores de posición que aún no contienen ningún valor. Las variables de marcador de posición en el gráfico se compilan en una función, que luego se puede ejecutar repetidamente en lotes del conjunto de entrenamiento para producir valores de salida y gradiente.
Este método de gráfico de cálculo estático funciona bien para redes neuronales convolucionales de estructura fija. Pero en muchas otras aplicaciones, es útil que la estructura gráfica de una red neuronal varíe según los datos. En el procesamiento del lenguaje natural, los investigadores a menudo quieren desenrollar (determinar) una red neuronal recurrente mediante las palabras de entrada en cada paso de tiempo. Las operaciones de pila en el modelo SPINN descrito anteriormente dependen en gran medida del flujo de control (como declaraciones for e if) para definir la estructura gráfica computacional de una oración específica. En casos más complejos, es posible que necesite crear un modelo cuya estructura dependa de la salida de una subred del propio modelo.
Algunas (aunque no todas) de estas ideas pueden incorporarse a un sistema gráfico estático, pero casi siempre a expensas de una menor transparencia y una mayor confusión en el código. El marco debe agregar nodos especiales a su gráfico computacional que representen primitivas de programación como bucles y condicionales, y los usuarios deben aprender y usar estos nodos, no solo para y si en lenguajes de código de programación. Esto se debe a que cualquier declaración de flujo de control utilizada por el programador se ejecutará solo una vez y el programador necesita codificar una única ruta de cálculo al crear el gráfico.
Por ejemplo, ejecutar una unidad de red neuronal recurrente (rnn_unit) a través de vectores de palabras (comenzando desde el estado inicial h0) requiere el nodo de flujo de control especial tf. while_loop en TensorFlow. Se necesita un nodo especial adicional para obtener la longitud de la palabra en tiempo de ejecución, ya que es solo un marcador de posición cuando se ejecuta el código.
# TensorFlow
# (este código se ejecuta una vez, durante la inicialización del modelo)
# "palabras" no es una lista real (es una variable de marcador de posición), por lo que
# No puedo usar “len”
cond = lambda i, h: i < tf.shape(words)[0]
cell = lambda i, h: rnn_unit(palabras[i], h)
i = 0
_, h = tf. while_loop(cond, cell, (i, h0))
El método basado en gráficos de cálculo dinámico es fundamentalmente diferente de los métodos anteriores. Tiene décadas de historia de investigación académica, incluido Kayak de Harvard, la biblioteca de diferenciación automática (autograd) y los marcos centrados en la investigación Chainer y DyNet. En dicho marco (también llamado definición por ejecución), el gráfico computacional se construye y reconstruye en tiempo de ejecución, utilizando el mismo código para realizar cálculos para el paso directo y también para el paso inverso. Construya las estructuras de datos requeridas mediante retropropagación. Este enfoque produce un código más sencillo porque el flujo de control se puede escribir usando el estándar for y if. También facilita la depuración porque los puntos de interrupción en tiempo de ejecución o los seguimientos de la pila se remontarán al código real escrito, en lugar de a las funciones compiladas en el motor de ejecución. Las redes neuronales recurrentes con la misma longitud variable se pueden implementar en un marco dinámico utilizando un bucle for simple de Python.
# PyTorch (también funciona en Chainer)
# (este código se ejecuta en cada paso hacia adelante del modelo)
# “palabras” es una lista de Python con valores reales en él
h = h0
para palabra en palabras:
h = rnn_unit(word, h)
PyTorch Es el primer marco de aprendizaje profundo definido por ejecución que coincide con la funcionalidad y el rendimiento de marcos de gráficos estáticos como TensorFlow, lo que lo hace muy adecuado para todo, desde redes convolucionales estándar hasta el aprendizaje por refuerzo más loco (aprendizaje por refuerzo) y otros. ideas. Así que echemos un vistazo a la implementación de SPINN.
Código
Antes de comenzar a construir la red, necesito configurar un cargador de datos. Con el aprendizaje profundo, los modelos pueden operar con lotes de muestras de datos, acelerar el entrenamiento mediante paralelismo y tener un cambio de gradiente más suave en cada paso. Creo que esto se puede hacer aquí (más adelante explicaré cómo se puede realizar por lotes el proceso de manipulación de la pila anterior). El siguiente código Python carga datos utilizando un sistema integrado en la biblioteca de texto de PyTorch, que puede generar lotes automáticamente al concatenar muestras de datos de longitud similar. Después de ejecutar este código, train_iter, dev_iter y test_itercontain recorren los lotes SNLI fragmentados del conjunto de entrenamiento, validación y prueba.
de datos de importación de texto de antorcha, conjuntos de datos
TEXT = datasets.snli.ParsedTextField(lower=True)
TRANSITIONS = datasets.snli.ShiftReduceField() p>
p>
ETIQUETAS = data.Field(sequential=False)train, dev, test = datasets.SNLI.splits(
TEXTO, TRANSICIONES, ETIQUETAS, wv_type='guante. 42B')TEXT. build_vocab(entrenamiento, desarrollo, prueba)
train_iter, dev_iter, test_iter = data.BucketIterator.splits(
(entrenamiento, desarrollo, prueba), tamaño de lote=64 )
Puedes encontrar el resto del código que configura el ciclo de entrenamiento y las mediciones de precisión en train.py. Sigamos. Como se mencionó anteriormente, el codificador SPINN contiene una capa de Reducción parametrizada y un rastreador de bucle opcional para rastrear el contexto de la oración para actualizar el estado oculto cada vez que la red lee una palabra o aplica una Reducción. El siguiente código representa la creación de un SPINN Just significa; creando estos dos submódulos (pronto veremos su código) y colocándolos en un contenedor para su uso posterior.
importar antorchadesde antorcha importar nn
# subclase la clase Módulo del paquete de red neuronal de PyTorch
clase SPINN(nn.Module):
def __init__(self, config):
super(SPINN, self).__init__()
self.config = config self.reduce = Reducir(config.d_hidden, config. d_tracker)
si config.d_tracker no es Ninguno:
self.tracker = Tracker(config.d_hidden, config.d_tracker)
Al crear un modelo, SPINN.__init__ se llama una vez; asigna e inicializa parámetros pero no realiza ninguna operación de red neuronal ni construye ningún tipo de gráfico computacional. El código que se ejecuta en cada nuevo lote de datos se define mediante el método SPINN.forward, que es el nombre estándar de PyTorch para los métodos implementados por el usuario que se utilizan para definir el proceso de avance de un modelo. Lo que se describe anteriormente es una implementación eficiente del algoritmo de manipulación de pilas, es decir, en Python normal, ejecutándose en un lote de buffers y pilas, uno para cada ejemplo. Repito el conjunto de operaciones de "desplazamiento" y "reducción" contenidas en la matriz de transición (transición), ejecuto el rastreador (si está presente) e itero sobre cada muestra del lote para aplicar la operación de "desplazamiento" (si se solicita) , o Se agrega a la lista de muestras que requieren una operación de "reducción". Luego, la capa Reducir se ejecuta en todas las muestras de esa lista y los resultados se devuelven a sus respectivas pilas.
def forward(self, buffers, transiciones):
# La entrada viene como un tensor único de incrustaciones de palabras;
# Necesito que sea una lista de pilas, una para cada ejemplo en
# el lote, de las que podemos extraer de forma independiente. Las palabras en
# cada ejemplo ya se han invertido, para que puedan.
# se leen de izquierda a derecha apareciendo desde el final de cada
# lista; también tienen el prefijo con un valor nulo.
buffers = [list(torch.split(b.squeeze(1), 1, 0))
for b in torch.split(buffers, 1, 1)]
# también necesitamos dos valores nulos en la parte inferior de cada pila,
# para que podamos copiar los valores nulos en la entrada
# son todos necesarios para que el rastreador pueda; ejecutar incluso si el
# buffer o pila está vacío
stacks = [[buf[0], buf[0]] for buf in buffers]
if hasattr (self, 'tracker'):
self.tracker.reset_state()
para trans_batch en transiciones:
if hasattr(self, 'tracker' ') :
# Anteriormente describí que el Tracker tomaba 4
# argumentos (context_t, b, s1, s2), pero aquí
# proporciono el contenido de la pila como un único argumento
# mientras se almacena el contexto dentro del propio objeto Tracker
#.
tracker_states, _ = self.tracker(buffers, pilas)
else:
tracker_states = itertools.repeat(None)
izquierdas, derechos, seguimientos = [], [], []
batch = zip(trans_batch, buffers, stacks, tracker_states)
para transición, buf, pila, seguimiento en lote:
if transición == SHIF
T:
stack.append(buf.pop())
transición elif == REDUCIR:
rights.append(stack.pop())
lefts.append(stack.pop())
trackings.append(seguimiento)
si derechos:
reducido = iter( self.reduce(izquierdas, derechos, seguimientos))
para transición, apilar en zip(trans_batch, pilas):
if transición == REDUCIR:
stack.append(next(reduced))
return [stack.pop() for stack in stacks]
Ejecute Tracker o Reduce respectivamente al llamar a self.tracker o self.reduce Forward Método para submódulos que requiere aplicar una operación directa en una lista de muestras. En el método directo de la función principal, tiene sentido realizar operaciones independientes en diferentes muestras, es decir, tener buffers y pilas separados para cada muestra en el lote, ya que se realizan todas las operaciones matemáticas muy utilizadas que se benefician de la ejecución por lotes y que requieren aceleración de GPU. en Rastreador y Reducir. Para escribir estas funciones de manera más limpia, usaré algunos ayudantes (que se definirán más adelante) para convertir estas listas de muestras en tensores por lotes y viceversa.
Quiero que el módulo Reducir agrupe automáticamente sus argumentos para acelerar el cálculo y luego los desagrupe para que puedan insertarse y extraerse individualmente. La función de combinación real utilizada para combinar cada par de expresiones de subfrase izquierda y derecha en una frase principal es TreeLSTM, que es una variante de la unidad de red neuronal recurrente ordinaria LSTM. Esta función de combinación requiere que el estado de cada subfrase en realidad consista en dos tensores, un estado oculto hy un estado de celda de memoria c, y la función es utilizar dos capas lineales que operan en el estado oculto de la subfrase (nn.Linear) y la función de combinación no lineal tree_lstm que combina los resultados de la capa lineal con el estado de la unidad de almacenamiento de la subfrase. En SPINN, este enfoque se amplía agregando una tercera capa lineal que opera en el estado oculto del Tracker.
Figura 2: La función de combinación TreeLSTM agrega una tercera entrada (x, en este caso el estado del Tracker). En la implementación de PyTorch que se muestra a continuación, 5 conjuntos de tres transformaciones lineales (representadas por el triplete de flechas azul, negra y roja) se combinan en tres módulos nn.Linear, y la función tree_lstm realiza todos los cálculos ubicados dentro del cuadro. Figura de Chen et al.