Introducción
Los humanos no reinician su comprensión del lenguaje cada vez que escuchamos una oración. Dado un post, captamos el contexto en función de nuestra comprensión previa de esas palabras. Una de las características definitorias que poseemos es nuestra memoria (o poder de retención).
¿Puede un algoritmo replicar esto? La primera técnica que me viene a la mente es una red neuronalLas redes neuronales son modelos computacionales inspirados en el funcionamiento del cerebro humano. Utilizan estructuras conocidas como neuronas artificiales para procesar y aprender de los datos. Estas redes son fundamentales en el campo de la inteligencia artificial, permitiendo avances significativos en tareas como el reconocimiento de imágenes, el procesamiento del lenguaje natural y la predicción de series temporales, entre otros. Su capacidad para aprender patrones complejos las hace herramientas poderosas... (NN). Pero, tristemente, las NN tradicionales no pueden hacer esto. Tome un ejemplo de querer predecir lo que viene a continuación en un video. Una red neuronal tradicional tendrá dificultades para generar resultados precisos.
Ahí es donde entra en juego el concepto de redes neuronales recurrentes (RNN). Los RNN se han vuelto extremadamente populares en el espacio del aprendizaje profundoEl aprendizaje profundo, una subdisciplina de la inteligencia artificial, se basa en redes neuronales artificiales para analizar y procesar grandes volúmenes de datos. Esta técnica permite a las máquinas aprender patrones y realizar tareas complejas, como el reconocimiento de voz y la visión por computadora. Su capacidad para mejorar continuamente a medida que se le proporcionan más datos la convierte en una herramienta clave en diversas industrias, desde la salud..., lo que hace que aprenderlos sea aún más imperativo. Algunas aplicaciones del mundo real de RNN incluyen:
- Acreditación de voz
- Máquina traductora
- Composición musical
- Acreditación de escritura a mano
- Aprendizaje gramatical
En este post, primero revisaremos rápidamente los componentes centrales de un modelo RNN típico. Posteriormente configuraremos la declaración del problema que en conclusión resolveremos implementando un modelo RNN desde cero en Python.
Siempre podemos aprovechar las bibliotecas de Python de alto nivel para codificar un RNN. Entonces, ¿por qué codificarlo desde cero? Creo firmemente que la mejor manera de aprender y verdaderamente arraigar un concepto es aprenderlo desde cero. Y eso es lo que mostraré en este tutorial.
Este post asume una comprensión básica de las redes neuronales recurrentes. En caso de que necesite un repaso rápido o esté buscando aprender los conceptos básicos de RNN, le recomiendo leer primero los posts a continuación:
Tabla de contenido
- Flashback: un resumen de los conceptos de redes neuronales recurrentes
- Predicción de secuencia usando RNN
- Construyendo un modelo RNN usando Python
Flashback: un resumen de los conceptos de redes neuronales recurrentes
Recapitulemos rápidamente los conceptos básicos detrás de las redes neuronales recurrentes.
Haremos esto usando un ejemplo de datos de secuencia, digamos las acciones de una compañía en particular. Un modelo de aprendizaje automático simple, o una red neuronal artificial, puede aprender a predecir el precio de las acciones en función de una serie de características, como el volumen de las acciones, el valor de apertura, etc. Aparte de estas, el precio además depende de cómo la acción se comportó en las fays y semanas anteriores. Para un comerciante, estos datos históricos son en realidad un factor decisivo importante para hacer predicciones.
En las redes neuronales de alimentación directa convencionales, todos los casos de prueba se consideran independientes. ¿Puede ver que eso no encaja bien al predecir los precios de las acciones? El modelo NN no consideraría los valores del precio de las acciones anteriores, ¡no es una gran idea!
Hay otro concepto en el que podemos apoyarnos cuando nos enfrentamos a datos sensibles al tiempo: las redes neuronales recurrentes (RNN).
Un RNN típico se ve así:
Esto puede parecer intimidante al principio. Pero una vez que lo desarrollamos, las cosas comienzan a verse mucho más simples:
Ahora nos resulta más fácil visualizar cómo estas redes están considerando la tendencia de los precios de las acciones. Esto nos ayuda a predecir los precios del día. Aquí, cada predicción en el tiempo t (h_t) depende de todas las predicciones anteriores y de la información obtenida de ellas. Bastante sencillo, ¿verdad?
Los RNN pueden solucionar nuestro propósito de manejo de secuencias en gran medidaLa "medida" es un concepto fundamental en diversas disciplinas, que se refiere al proceso de cuantificar características o magnitudes de objetos, fenómenos o situaciones. En matemáticas, se utiliza para determinar longitudes, áreas y volúmenes, mientras que en ciencias sociales puede referirse a la evaluación de variables cualitativas y cuantitativas. La precisión en la medición es crucial para obtener resultados confiables y válidos en cualquier investigación o aplicación práctica...., pero no del todo.
El texto es otro buen ejemplo de datos de secuencia. Ser capaz de predecir qué palabra o frase viene después de un texto determinado podría ser una ventaja muy útil. Queremos que nuestros modelos escribir sonetos de Shakespeare!
Ahora, las enfermeras registradas son excelentes cuando se trata de un contexto que es de naturaleza corta o pequeña. Pero para poder construir una historia y recordarla, nuestros modelos deben poder comprender el contexto detrás de las secuencias, como un cerebro humano.
Predicción de secuencia usando RNN
En este post, trabajaremos en un obstáculo de predicción de secuencia usando RNN. Una de las tareas más simples para esto es el pronóstico de ondas sinusoidales. La secuencia contiene una tendencia visible y es fácil de solucionar usando heurística. Así es como se ve una onda sinusoidal:
Primero diseñaremos una red neuronal recurrenteLas redes neuronales recurrentes (RNN) son un tipo de arquitectura de redes neuronales diseñadas para procesar secuencias de datos. A diferencia de las redes neuronales tradicionales, las RNN utilizan conexiones internas que permiten recordar información de entradas anteriores. Esto las hace especialmente útiles en tareas como el procesamiento de lenguaje natural, la traducción automática y el análisis de series temporales, donde el contexto y la secuencia son fundamentales para la... desde cero para solucionar este problema. Nuestro modelo RNN además debería poder generalizarse bien para que podamos aplicarlo en otros problemas de secuencia.
Formularemos nuestro problema de esta manera: dada una secuencia de 50 números que pertenecen a una onda sinusoidal, predice el número 51 de la serie. ¡Es hora de encender su computadora portátil Jupyter (o su IDE de elección)!
Codificación de RNN usando Python
Paso 0: preparación de datos
Ah, el primer paso inevitable en cualquier proyecto de ciencia de datos: preparar los datos antes de hacer cualquier otra cosa.
¿Cómo espera nuestro modelo de red que sean los datos? Aceptaría una sola secuencia de longitud 50 como entrada. Entonces, la forma de los datos de entrada será:
(number_of_records x length_of_sequence x types_of_sequences)
Aquí, types_of_sequences es 1, debido a que solo tenemos un tipo de secuencia: la onda sinusoidal.
Por otra parte, la salida tendría solo un valor para cada registro. Desde luego, este será el valor 51 en la secuencia de entrada. Entonces su forma sería:
(number_of_records x types_of_sequences) #where types_of_sequences is 1
Vamos a sumergirnos en el código. Primero, importe las bibliotecas indispensables:
%pylab inline import math
Para crear una onda sinusoidal como datos, usaremos la función sinusoidal de Python Matemáticas Biblioteca:
sin_wave = np.array([math.sin(x) for x in np.arange(200)])
Visualizando la onda sinusoidal que acabamos de generar:
plt.plot(sin_wave[:50])
Crearemos los datos ahora en el siguiente bloque de código:
X = [] Y = [] seq_len = 50 num_records = len(sin_wave) - seq_len for i in range(num_records - 50): X.append(sin_wave[i:i+seq_len]) Y.append(sin_wave[i+seq_len]) X = np.array(X) X = np.expand_dims(X, axis=2) Y = np.array(Y) Y = np.expand_dims(Y, axis=1)
Imprima la forma de los datos:
Tenga en cuenta que hicimos un bucle para (num_records – 50) debido a que queremos reservar 50 registros como nuestros datos de validación. Podemos crear estos datos de validación ahora:
X_val = [] Y_val = [] for i in range(num_records - 50, num_records): X_val.append(sin_wave[i:i+seq_len]) Y_val.append(sin_wave[i+seq_len]) X_val = np.array(X_val) X_val = np.expand_dims(X_val, axis=2) Y_val = np.array(Y_val) Y_val = np.expand_dims(Y_val, axis=1)
Paso 1: Cree la arquitectura para nuestro modelo RNN
Nuestra siguiente tarea es establecer todas las variables y funciones indispensables que usaremos en el modelo RNN. Nuestro modelo tomará la secuencia de entrada, la procesará por medio de una capa oculta de 100 unidades y producirá una salida de valor único:
learning_rate = 0.0001 nepoch = 25 T = 50 # length of sequence hidden_dim = 100 output_dim = 1 bptt_truncate = 5 min_clip_value = -10 max_clip_value = 10
Posteriormente definiremos los pesos de la red:
U = np.random.uniform(0, 1, (hidden_dim, T)) W = np.random.uniform(0, 1, (hidden_dim, hidden_dim)) V = np.random.uniform(0, 1, (output_dim, hidden_dim))
Aquí,
- U es la matriz de ponderaciones para las ponderaciones entre capas de entrada y ocultas
- V es la matriz de pesos para pesos entre capas ocultas y de salida
- W es la matriz de peso para pesos compartidos en la capa RNN (capa oculta)
En resumen, definiremos la función de activaciónLa función de activación es un componente clave en las redes neuronales, ya que determina la salida de una neurona en función de su entrada. Su propósito principal es introducir no linealidades en el modelo, permitiendo que aprenda patrones complejos en los datos. Existen diversas funciones de activación, como la sigmoide, ReLU y tanh, cada una con características particulares que afectan el rendimiento del modelo en diferentes aplicaciones...., sigmoidea, que se utilizará en la capa oculta:
def sigmoid(x): return 1 / (1 + np.exp(-x))
Paso 2: entrenar el modelo
Ahora que hemos definido nuestro modelo, en conclusión podemos continuar con el entrenamientoEl entrenamiento es un proceso sistemático diseñado para mejorar habilidades, conocimientos o capacidades físicas. Se aplica en diversas áreas, como el deporte, la educación y el desarrollo profesional. Un programa de entrenamiento efectivo incluye la planificación de objetivos, la práctica regular y la evaluación del progreso. La adaptación a las necesidades individuales y la motivación son factores clave para lograr resultados exitosos y sostenibles en cualquier disciplina.... en nuestros datos de secuencia. Podemos subdividir el procedimiento de capacitación en pasos más pequeños, a saber:
Paso 2.1: Verifique la pérdida de datos de entrenamiento
Paso 2.1.1: Pase hacia adelante
Paso 2.1.2: Calcular el error
Paso 2.2: Verifique la pérdida de datos de validación
Paso 2.2.1: Pase hacia adelante
Paso 2.2.2: Calcular el error
Paso 2.3: Comience el entrenamiento real
Paso 2.3.1: Pase hacia adelante
Paso 2.3.2: Error de retropropagación
Paso 2.3.3: Actualice los pesos
Necesitamos repetir estos pasos hasta la convergencia. Si el modelo comienza a sobreajustarse, ¡deténgase! O simplemente predefinir el número de épocas.
Paso 2.1: Verifique la pérdida de datos de entrenamiento
Haremos una pasada hacia adelante por medio de nuestro modelo RNN y calcularemos el error al cuadrado de las predicciones para todos los registros con el fin de obtener el valor de pérdida.
for epoch in range(nepoch): # check loss on train loss = 0.0 # do a forward pass to get prediction for i in range(Y.shape[0]): x, y = X[i], Y[i] # get input, output values of each record prev_s = np.zeros((hidden_dim, 1)) # here, prev-s is the value of the previous activation of hidden layer; which is initialized as all zeroes for t in range(T): new_input = np.zeros(x.shape) # we then do a forward pass for every timestep in the sequence new_input[t] = x[t] # for this, we establece a single input for that timestep mulu = np.dot(U, new_input) mulw = np.dot(W, prev_s) add = mulw + mulu s = sigmoid(add) mulv = np.dot(V, s) prev_s = s # calculate error loss_per_record = (y - mulv)**2 / 2 loss += loss_per_record loss = loss / float(y.shape[0])
Paso 2.2: Verifique la pérdida de datos de validación
Haremos lo mismo para calcular la pérdida en los datos de validación (en el mismo ciclo):
# check loss on val val_loss = 0.0 for i in range(Y_val.shape[0]): x, y = X_val[i], Y_val[i] prev_s = np.zeros((hidden_dim, 1)) for t in range(T): new_input = np.zeros(x.shape) new_input[t] = x[t] mulu = np.dot(U, new_input) mulw = np.dot(W, prev_s) add = mulw + mulu s = sigmoid(add) mulv = np.dot(V, s) prev_s = s loss_per_record = (y - mulv)**2 / 2 val_loss += loss_per_record val_loss = val_loss / float(y.shape[0]) print('Epoch: ', epoch + 1, ', Loss: ', loss, ', Val Loss: ', val_loss)
Debería obtener el siguiente resultado:
Epoch: 1 , Loss: [[101185.61756671]] , Val Loss: [[50591.0340148]] ... ...
Paso 2.3: Comience el entrenamiento real
Ahora comenzaremos con la capacitación real de la red. En este, primero haremos un pase hacia adelante para calcular los errores y un pase hacia atrás para calcular los gradientes y actualizarlos. Déjame mostrarte estos paso a paso para que puedas visualizar cómo funciona en tu mente.
Paso 2.3.1: Pase hacia adelante
En el pase adelantado:
- Primero multiplicamos la entrada con los pesos entre la entrada y las capas ocultas.
- Agregue esto con la multiplicación de pesos en la capa RNN. Esto se debe a que queremos capturar el conocimiento del paso de tiempo anterior.
- Pasarlo por medio de una función de activación sigmoidea.
- Multiplique esto con los pesos entre las capas ocultas y de salida.
- En la capa de salidaLa "capa de salida" es un concepto utilizado en el ámbito de la tecnología de la información y el diseño de sistemas. Se refiere a la última capa de un modelo de software o arquitectura que se encarga de presentar los resultados al usuario final. Esta capa es crucial para la experiencia del usuario, ya que permite la interacción directa con el sistema y la visualización de datos procesados...., tenemos una activación lineal de los valores, por lo que no pasamos explícitamente el valor por medio de una capa de activación.
- Guarde el estado en la capa actual y además el estado en el paso de tiempo anterior en un diccionario
Aquí está el código para realizar un pase hacia adelante (tenga en cuenta que es una continuación del ciclo anterior):
# train model for i in range(Y.shape[0]): x, y = X[i], Y[i] layers = [] prev_s = np.zeros((hidden_dim, 1)) dU = np.zeros(U.shape) dV = np.zeros(V.shape) dW = np.zeros(W.shape) dU_t = np.zeros(U.shape) dV_t = np.zeros(V.shape) dW_t = np.zeros(W.shape) dU_i = np.zeros(U.shape) dW_i = np.zeros(W.shape) # forward pass for t in range(T): new_input = np.zeros(x.shape) new_input[t] = x[t] mulu = np.dot(U, new_input) mulw = np.dot(W, prev_s) add = mulw + mulu s = sigmoid(add) mulv = np.dot(V, s) layers.append({'s':s, 'prev_s':prev_s}) prev_s = s
Paso 2.3.2: Error de retropropagación
Después del paso de propagación hacia adelante, calculamos los gradientes en cada capa y propagamos hacia atrás los errores. Usaremos la propagación hacia atrás truncada a través del tiempo (TBPTT), en lugar de la retropropagación de vainilla. Puede parecer complejo, pero en realidad es bastante sencillo.
La diferencia central en BPTT versus backprop es que el paso de retropropagación se realiza para todos los pasos de tiempo en la capa RNN. Entonces, si la longitud de nuestra secuencia es 50, retropropagaremos todos los pasos de tiempo anteriores al paso de tiempo actual.
Si ha adivinado correctamente, BPTT parece muy costoso computacionalmente. Entonces, en lugar de propagar hacia atrás por medio de todos los pasos de tiempo anteriores, realizamos la propagación hacia atrás hasta x pasos de tiempo para ahorrar energía computacional. Considere esto ideológicamente semejante al descenso de gradienteGradiente es un término utilizado en diversos campos, como la matemática y la informática, para describir una variación continua de valores. En matemáticas, se refiere a la tasa de cambio de una función, mientras que en diseño gráfico, se aplica a la transición de colores. Este concepto es esencial para entender fenómenos como la optimización en algoritmos y la representación visual de datos, permitiendo una mejor interpretación y análisis en... estocástico, donde incluimos un lote de puntos de datos en lugar de todos los puntos de datos.
Aquí está el código para propagar los errores hacia atrás:
# derivative of pred dmulv = (mulv - y) # backward pass for t in range(T): dV_t = np.dot(dmulv, np.transpose(layers[t]['s'])) dsv = np.dot(np.transpose(V), dmulv) ds = dsv dadd = add * (1 - add) * ds dmulw = dadd * np.ones_like(mulw) dprev_s = np.dot(np.transpose(W), dmulw) for i in range(t-1, max(-1, t-bptt_truncate-1), -1): ds = dsv + dprev_s dadd = add * (1 - add) * ds dmulw = dadd * np.ones_like(mulw) dmulu = dadd * np.ones_like(mulu) dW_i = np.dot(W, layers[t]['prev_s']) dprev_s = np.dot(np.transpose(W), dmulw) new_input = np.zeros(x.shape) new_input[t] = x[t] dU_i = np.dot(U, new_input) dx = np.dot(np.transpose(U), dmulu) dU_t += dU_i dW_t += dW_i dV += dV_t dU += dU_t dW += dW_t
Paso 2.3.3: Actualice los pesos
Por último, actualizamos los pesos con los gradientes de pesos calculados. Una cosa que debemos prestar atención es que los gradientes tienden a explotar si no los mantiene bajo control. Este es un tema fundamental en el entrenamiento de redes neuronales, llamado problema del gradiente explosivo. Por lo tanto tenemos que sujetarlos en un rango para que no exploten. Podemos hacerlo asi
if dU.max() > max_clip_value: dU[dU > max_clip_value] = max_clip_value if dV.max() > max_clip_value: dV[dV > max_clip_value] = max_clip_value if dW.max() > max_clip_value: dW[dW > max_clip_value] = max_clip_value if dU.min() < min_clip_value: dU[dU < min_clip_value] = min_clip_value if dV.min() < min_clip_value: dV[dV < min_clip_value] = min_clip_value if dW.min() < min_clip_value: dW[dW < min_clip_value] = min_clip_value # update U -= learning_rate * dU V -= learning_rate * dV W -= learning_rate * dW
Al entrenar el modelo anterior, obtenemos este resultado:
Epoch: 1 , Loss: [[101185.61756671]] , Val Loss: [[50591.0340148]] Epoch: 2 , Loss: [[61205.46869629]] , Val Loss: [[30601.34535365]] Epoch: 3 , Loss: [[31225.3198258]] , Val Loss: [[15611.65669247]] Epoch: 4 , Loss: [[11245.17049551]] , Val Loss: [[5621.96780111]] Epoch: 5 , Loss: [[1264.5157739]] , Val Loss: [[632.02563908]] Epoch: 6 , Loss: [[20.15654115]] , Val Loss: [[10.05477285]] Epoch: 7 , Loss: [[17.13622839]] , Val Loss: [[8.55190426]] Epoch: 8 , Loss: [[17.38870495]] , Val Loss: [[8.68196484]] Epoch: 9 , Loss: [[17.181681]] , Val Loss: [[8.57837827]] Epoch: 10 , Loss: [[17.31275313]] , Val Loss: [[8.64199652]] Epoch: 11 , Loss: [[17.12960034]] , Val Loss: [[8.54768294]] Epoch: 12 , Loss: [[17.09020065]] , Val Loss: [[8.52993502]] Epoch: 13 , Loss: [[17.17370113]] , Val Loss: [[8.57517454]] Epoch: 14 , Loss: [[17.04906914]] , Val Loss: [[8.50658127]] Epoch: 15 , Loss: [[16.96420184]] , Val Loss: [[8.46794248]] Epoch: 16 , Loss: [[17.017519]] , Val Loss: [[8.49241316]] Epoch: 17 , Loss: [[16.94199493]] , Val Loss: [[8.45748739]] Epoch: 18 , Loss: [[16.99796892]] , Val Loss: [[8.48242177]] Epoch: 19 , Loss: [[17.24817035]] , Val Loss: [[8.6126231]] Epoch: 20 , Loss: [[17.00844599]] , Val Loss: [[8.48682234]] Epoch: 21 , Loss: [[17.03943262]] , Val Loss: [[8.50437328]] Epoch: 22 , Loss: [[17.01417255]] , Val Loss: [[8.49409597]] Epoch: 23 , Loss: [[17.20918888]] , Val Loss: [[8.5854792]] Epoch: 24 , Loss: [[16.92068017]] , Val Loss: [[8.44794633]] Epoch: 25 , Loss: [[16.76856238]] , Val Loss: [[8.37295808]]
¡Luciendo bien! Es hora de obtener las predicciones y trazarlas para tener una idea visual de lo que hemos diseñado.
Paso 3: obtén predicciones
Haremos un pase hacia adelante por medio de los pesos entrenados para obtener nuestras predicciones:
preds = [] for i in range(Y.shape[0]): x, y = X[i], Y[i] prev_s = np.zeros((hidden_dim, 1)) # Forward pass for t in range(T): mulu = np.dot(U, x) mulw = np.dot(W, prev_s) add = mulw + mulu s = sigmoid(add) mulv = np.dot(V, s) prev_s = s preds.append(mulv) preds = np.array(preds)
Trazando estas predicciones junto con los valores reales:
plt.plot(preds[:, 0, 0], 'g') plt.plot(Y[:, 0], 'r') plt.show()
Esto estaba en los datos de entrenamiento. ¿Cómo sabemos si nuestro modelo no se ajustó demasiado? Aquí es donde entra en juego el conjunto de validación, que creamos previamente:
preds = [] for i in range(Y_val.shape[0]): x, y = X_val[i], Y_val[i] prev_s = np.zeros((hidden_dim, 1)) # For each time step... for t in range(T): mulu = np.dot(U, x) mulw = np.dot(W, prev_s) add = mulw + mulu s = sigmoid(add) mulv = np.dot(V, s) prev_s = s preds.append(mulv) preds = np.array(preds) plt.plot(preds[:, 0, 0], 'g') plt.plot(Y_val[:, 0], 'r') plt.show()
Nada mal. Las predicciones parecen impresionantes. La puntuación RMSE en los datos de validación además es respetable:
from sklearn.metrics import mean_squared_error math.sqrt(mean_squared_error(Y_val[:, 0] * max_val, preds[:, 0, 0] * max_val))
0.127191931509431
Notas finales
No puedo enfatizar lo suficiente lo útiles que son los RNN cuando se trabaja con datos de secuencia. Les imploro a todos que tomen este aprendizaje y lo apliquen en un conjunto de datos. Tome un obstáculo de PNL y vea si puede hallar una solución. Siempre puede comunicarse conmigo en la sección de comentarios a continuación si tiene alguna duda.
En este post, aprendimos cómo crear un modelo de red neuronal recurrente desde cero usando solo la biblioteca numpy. Desde luego, puede utilizar una biblioteca de alto nivel como Keras o Caffe, pero es esencial conocer el concepto que está implementando.
Comparta sus pensamientos, preguntas y comentarios sobre este post a continuación. ¡Feliz aprendizaje!