Ir al contenido principal

Aprendizaje automático mediante Deep Q Ntework (DQN + TensorFlow)

"[Las neuronas son] células de formas delicadas y elegantes, las misteriosas mariposas del alma, cuyo batir de alas quién sabe si esclarecerá algún día el secreto de la vida mental." (Ramón y Cajal)

Introducción.

Este artículo es una continuación de mi entrada anterior "Las matemáticas de la mente"[2]. Vimos en ese artículo cómo era posible que un simple algoritmo de computación pudiese imitar el modo en que nuestro cerebro aprende a realizar tareas con éxito, simplemente a partir del equivalente computacional de una red neuronal.

Sin embargo, a pesar de que en dicha entrada os comentaba el caso de cómo se puede programar un algoritmo capaz de conseguir literalmente, aprender a jugar al Conecta4 (4 en raya) sin especificar (pre-programar) en ningún momento las reglas del juego; es posible que muchos notasen que aún así, todavía había que pre-procesar la entrada de la red neuronal para ofrecerle a las neuronas (nodos) de la capa de entrada (inputs) qué fichas había en cada casilla del tablero y qué casillas estaban aún libres. Este hecho podía hacer a algunos sospechar de la autonomía real de este programa para aprender de un modo no supervisado a jugar.

Por lo tanto, y con ánimo de solventar este asunto, os voy a presentar un nuevo ejemplo en esta nueva entrada para intentar reforzar así la validez de las conclusiones a las que llegamos al final del artículo "Las matemáticas de la mente"[2]. Haremos ésto mediante un nuevo experimento de aprendizaje automático mediante redes neuronales, pero esta vez; aprovechando el estado del arte teórico que nos ofrece el equipo del departamento de inteligencia artificial de Google DeepMind[4].

En concreto, he dedicado un par de semanas de mi tiempo libre en implementar un muy prometedor algoritmo de aprendizaje por refuerzo (RL según siglas en inglés). Para aquellos interesados técnicamente en el asunto, pueden leer el siguiente paper[1] del que me hecho referencia: "Asynchronous Methods for Deep Reinforcement Learning", publicado a fecha del 4 de febrero de este año 2016, en colaboración con la Universidad de Montreal.

La cuestión fundamental tratada en este paper de Google es que explica el modo en que es posible entrenar una red neuronal artificial para que realice tareas sin supervisión ni pre-programación alguna. Se trata de un proceso end-to-end, mediante el cual el algoritmo accede directamente a los píxeles de la pantalla (simulando el modo en que nosotros obtenemos la información visual desde nuestra retina), y luego convoluciona y post-procesa dicha información sensible en varias capas de nodos (neuronas) internas (simulando el modo en que nuestro cerebro procesa la información obtenida en la retina posteriormente en sucesivas partes de la corteza visual); para terminar tratando dicha información visual en nuevas capas de neuronas que darán lugar finalmente a una respuesta (una acción).

Esto implica que ya no es necesario que nosotros pre-programemos y ofrezcamos como entrada a la red neuronal el estado del entorno (que en el caso del Conecta4 era estado del tablero), sino que es la propia red neuronal la que observa visualmente el entorno (por ejemplo, el tablero), y luego procesa dicha información visual en otras zonas de la misma red de manera autónoma, hasta ofrecer finalmente como salida una determinada acción a realizar.

Y sobra decir que los chicos de Google lo han conseguido (en realidad lo que han logrado es perfeccionar una técnica ya existente en gran medida); han desarrollado un algoritmo capaz de enseñar a una red neuronal artificial a procesar toda la información sensible disponible sobre el entorno de cualquier tarea a realizar, y; conseguir de un modo totalmente autónomo (end-to-end), ¡conseguir una respuesta con habilidades bastante cercanas a las humanas!

Metodología.

En el paper[1] arriba indicado nos explican teóricamente el modo en que es posible implementar un algoritmo de aprendizaje realmente autónomo como el que estamos buscando en un intento de reforzar las conclusiones obtenidas en el anterior artículo[2]. Y lo hacen mediante una detallada explicación, y ofreciendo además un pseudocódigo de guía para el interesado en realizar algún experimento o prueba práctica de lo que proponen. En realidad se ofrecen varias alternativas y variantes de lo que en DeepMind han denominado Deep Q NetWorks, aunque yo me he decantado por utilizar la variante en principio más sencilla de implementar (aunque también es la relativamente menos eficiente). Esto es, el "Asynchronous one-step Q-learning". 

Os dejo a continuación una copia del pseudocódigo ofrecido en el paper, pero aquellos lectores no técnicos no tenéis que preocuparos, continuad adelante sin prestar más atención al mismo:




Configuración experimental.

Para poner a prueba el pseudocódigo, me decidí a aprovechar la librería para desarrollo numérico computacional que desde hace unos meses ofrece el propio Google de acceso gratuito: TensorFlow[3]. En concreto, hice uso de la interfaz Python de la librería para programar mediante esta técnica de aprendizaje por refuerzo una red neuronal artificial capaz de aprender a jugar de manera totalmente autónoma (a partir únicamente de los pixels de la pantalla del ordenador) al clásico juego del Pong (el mítico juego de Atari de la pelotita rebotando en las paredes ;)).

Revisión utilizada del mítico juego del Pong

Utilicé TensorFlow para diseñar una red neuronal artificial de 6 capas: una para recibir los inputs de la imagen del entorno (el estado del juego) en formato RGB (donde cada pixel se representa mediante un número entero que es una combinación para el tono rojo, verde y azul). Estos píxels (simulando al modo en que la retina recoge los fotones en el ojo), se pasan luego a dos capas internas (o, como se las suelen llamar: ocultas -"hidden"-) que convolucionan y pre-procesan esta información visual para facilitar su uso más tarde por parte de las otras dos capas de neuronas, que finalmente procesan la información y devuelven una acción a realizar gracias a una capa de salida (output). Y esta es la red neuronal que se entrena de manera autónoma mediante el código de RL propuesto en el paper.

Como digo, todo se programó en Python usando varios hilos (threads) como requiere el pseudocódigo (ya que este aprendizaje hace uso de un procesamiento asíncrono en paralelo del entrenamiento).

Os dejo a continuación un par de vídeos donde podréis ver gráficamente en vivo cómo se produce este proceso. En un primer vídeo podréis observar como la red neuronal al inicio no se encuentra entrenada (es decir; sus "sinapsis" no están bien balanceadas), por lo que juega a lo loco (la red neuronal es la barra azul de la izquierda) . También veréis cómo se inicia el proceso de entrenamiento en paralelo mediante ensayo y error, gracias al refuerzo que supone la recompensa de ganar una partida, o el castigo que supone perderla (algo similar a lo que nosotros experimentamos como alegría y frustración cuando nos enfrentamos al aprendizaje de una nueva tarea):


Este segundo vídeo muestra la misma red neuronal inicial una vez que sus pesos (sinapsis) han sido bien moduladas por el proceso de aprendizaje. Como se puede observar, el algoritmo ahora es capaz de obtener la imagen (pixels) de la pantalla, "objetivar" lo importante de la misma (la pelota y su posición relativa), y actuar en consecuencia para maximizar el beneficio de sus acciones (ganar partidas):


Resultados.

Los resultados son muy favorables, alcanzando el algoritmo por sí mismo tras el entrenamiento autónomo un nivel de juego muy admirable y equiparable al de una persona. Además, es importante señalar una cuestión técnica: el proceso de aprendizaje se lleva a cabo con esta novedosa técnica propuesta por Google en un simple ordenador personal, sin requerir de grandes estaciones de trabajo distribuidas, y ni siquiera de un hardware específico (como tarjetas GPU). De hecho, con un simple procesador Intel Core i5 he conseguido entrenar en menos de 24 horas lo que hasta hace poco requería de grandes estaciones de trabajo distribuidas usando la técnica conocida como "Gorila" (ver el paper[1] de referencia para más información).

Por si hay algún interesado en probar todo el tinglado, os dejo a continuación copia del código fuente Python que he desarrollado:

#!/usr/bin/env python
import threading
import tensorflow as tf
import cv2
import sys
sys.path.append("Wrapped Game Code/")
import pong_fun as game # Whichever is imported "as game" will be used
import random
import numpy as np
import time

#Shared global parameters
TMAX = 5000000
T = 0
It = 10000
Iasync = 5
THREADS = 12
WISHED_SCORE = 10

GAME = 'pong' # The name of the game being played for log files
ACTIONS = 3 # Number of valid actions
GAMMA = 0.99 # Decay rate of past observations
OBSERVE = 5. # Timesteps to observe before training
EXPLORE = 400000. # Frames over which to anneal epsilon
FINAL_EPSILONS = [0.01, 0.01, 0.05] # Final values of epsilon
INITIAL_EPSILONS = [0.4, 0.3, 0.3] # Starting values of epsilon
EPSILONS = 3

def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev = 0.01)
return tf.Variable(initial)

def bias_variable(shape):
initial = tf.constant(0.01, shape = shape)
return tf.Variable(initial)

def conv2d(x, W, stride):
return tf.nn.conv2d(x, W, strides = [1, stride, stride, 1], padding = "SAME")

def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = "SAME")

def createNetwork():
# network weights
W_conv1 = weight_variable([8, 8, 4, 32])
b_conv1 = bias_variable([32])

W_conv2 = weight_variable([4, 4, 32, 64])
b_conv2 = bias_variable([64])

W_conv3 = weight_variable([3, 3, 64, 64])
b_conv3 = bias_variable([64])

W_fc1 = weight_variable([256, 256])
b_fc1 = bias_variable([256])

W_fc2 = weight_variable([256, ACTIONS])
b_fc2 = bias_variable([ACTIONS])

# input layer
s = tf.placeholder("float", [None, 80, 80, 4])

# hidden layers
h_conv1 = tf.nn.relu(conv2d(s, W_conv1, 4) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2, 2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3, 1) + b_conv3)
h_pool3 = max_pool_2x2(h_conv3)

h_pool3_flat = tf.reshape(h_pool3, [-1, 256])

h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1)

# readout layer
readout = tf.matmul(h_fc1, W_fc2) + b_fc2

return s, readout, W_conv1, b_conv1, W_conv2, b_conv2, W_conv3, b_conv3, W_fc1, b_fc1, W_fc2, b_fc2

def copyTargetNetwork(sess):
sess.run(copy_Otarget)

def actorLearner(num, sess, lock):
# We use global shared O parameter vector
# We use global shared Otarget parameter vector
# We use global shared counter T, and TMAX constant
global TMAX, T

# Open up a game state to communicate with emulator
lock.acquire()
game_state = game.GameState()
lock.release()

# Initialize network gradients
s_j_batch = []
a_batch = []
y_batch = []

# Get the first state by doing nothing and preprocess the image to 80x80x4
lock.acquire()
x_t, r_0, terminal = game_state.frame_step([1, 0, 0])
lock.release()
x_t = cv2.cvtColor(cv2.resize(x_t, (80, 80)), cv2.COLOR_BGR2GRAY)
s_t = np.stack((x_t, x_t, x_t, x_t), axis = 2)
aux_s = s_t

time.sleep(3*num)

# Initialize target network weights
copyTargetNetwork(sess)

epsilon_index = random.randrange(EPSILONS)
INITIAL_EPSILON = INITIAL_EPSILONS[epsilon_index]
FINAL_EPSILON = FINAL_EPSILONS[epsilon_index]
epsilon = INITIAL_EPSILON

print "THREAD ", num, "STARTING...", "EXPLORATION POLICY => INITIAL_EPSILON:", INITIAL_EPSILON, ", FINAL_EPSILON:", FINAL_EPSILON

# Initialize thread step counter
t = 0
score = 0
while T < TMAX and score < WISHED_SCORE:

# Choose an action epsilon greedily
readout_t = O_readout.eval(session = sess, feed_dict = {s : [s_t]})
a_t = np.zeros([ACTIONS])
action_index = 0
if random.random() <= epsilon or t <= OBSERVE:
action_index = random.randrange(ACTIONS)
a_t[action_index] = 1
else:
action_index = np.argmax(readout_t)
a_t[action_index] = 1

# Scale down epsilon
if epsilon > FINAL_EPSILON and t > OBSERVE:
epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / EXPLORE

# Run the selected action and observe next state and reward
lock.acquire()
x_t1_col, r_t, terminal = game_state.frame_step(a_t)
lock.release()
x_t1 = cv2.cvtColor(cv2.resize(x_t1_col, (80, 80)), cv2.COLOR_BGR2GRAY)
x_t1 = np.reshape(x_t1, (80, 80, 1))
aux_s = np.delete(s_t, 0, axis = 2)
s_t1 = np.append(aux_s, x_t1, axis = 2)

# Accumulate gradients
readout_j1 = Ot_readout.eval(session = sess, feed_dict = {st : [s_t1]})
if terminal:
y_batch.append(r_t)
else:
y_batch.append(r_t + GAMMA * np.max(readout_j1))

a_batch.append(a_t)
s_j_batch.append(s_t)

# Update the old values
s_t = s_t1
T += 1
t += 1
score += r_t

# Update the Otarget network
if T % It == 0:
copyTargetNetwork(sess)

# Update the O network
if t % Iasync == 0 or terminal:
if s_j_batch:
# Perform asynchronous update of O network
train_O.run(session = sess, feed_dict = {
y : y_batch,
a : a_batch,
s : s_j_batch})

#Clear gradients
s_j_batch = []
a_batch = []
y_batch = []

# Save progress every 5000 iterations
if t % 5000 == 0:
saver.save(sess, 'save_networks_asyn/' + GAME + '-dqn', global_step = t)

# Print info
state = ""
if t <= OBSERVE:
state = "observe"
elif t > OBSERVE and t <= OBSERVE + EXPLORE:
state = "explore"
else:
state = "train"

if terminal:
print "THREAD:", num, "/ TIME", T, "/ TIMESTEP", t, "/ STATE", state, "/ EPSILON", epsilon, "/ ACTION", action_index, "/ REWARD", r_t, "/ Q_MAX %e" % np.max(readout_t), "/ SCORE", score
score = 0

# Save the last state of each thread
saver.save(sess, 'save_networks_asyn/' + GAME + '-final-' + num)


# We create the shared global networks
# O network
s, O_readout, W_conv1, b_conv1, W_conv2, b_conv2, W_conv3, b_conv3, W_fc1, b_fc1, W_fc2, b_fc2 = createNetwork()

# Training node
a = tf.placeholder("float", [None, ACTIONS])
y = tf.placeholder("float", [None])
O_readout_action = tf.reduce_sum(tf.mul(O_readout, a), reduction_indices=1)
cost_O = tf.reduce_mean(tf.square(y - O_readout_action))
train_O = tf.train.RMSPropOptimizer(0.00025, 0.95, 0.95, 0.01).minimize(cost_O)

# Otarget network
st, Ot_readout, W_conv1t, b_conv1t, W_conv2t, b_conv2t, W_conv3t, b_conv3t, W_fc1t, b_fc1t, W_fc2t, b_fc2t = createNetwork()
copy_Otarget = [W_conv1t.assign(W_conv1), b_conv1t.assign(b_conv1), W_conv2t.assign(W_conv2), b_conv2t.assign(b_conv2), W_conv3t.assign(W_conv3), b_conv3t.assign(b_conv3), W_fc1t.assign(W_fc1), b_fc1t.assign(b_fc1), W_fc2t.assign(W_fc2), b_fc2t.assign(b_fc2)]

# Initialize session and variables
sess = tf.InteractiveSession()
saver = tf.train.Saver()
sess.run(tf.initialize_all_variables())
checkpoint = tf.train.get_checkpoint_state("save_networks_asyn")
if checkpoint and checkpoint.model_checkpoint_path:
saver.restore(sess, checkpoint.model_checkpoint_path)
print "Successfully loaded:", checkpoint.model_checkpoint_path

if __name__ == "__main__":
# Start n concurrent actor threads
lock = threading.Lock()
threads = list()
for i in range(THREADS):
t = threading.Thread(target=actorLearner, args=(i,sess, lock))
threads.append(t)

# Start all threads
for x in threads:
x.start()

# Wait for all of them to finish
for x in threads:
x.join()

print "ALL DONE!!"

Por cierto, que no os engañe lo reducido del código. Detrás tenemos la enorme librería TensorFlow trabajando, y también una librería para Python llamada Pygame ;).

Conclusiones y discusión.

La principal conclusión que me gustaría destacar, es la de que la inteligencia artificial está avanzando de un modo casi exponencial estos últimos años (aunque los medios apenas se hagan eco de ello). Muy en particular, el grupo de IA dentro de Google llamado DeepMind[4] tiene gran parte de culpa. Sus papers de los últimos años son espectaculares, y están rebasando constantemente el estado del arte en casi todos los ámbitos. Y, además, señalar que todos y cada uno de los hitos conseguidos en el ámbito de la IA se están logrando sin excepción al imitarse cada vez mejor (en cuanto a eficiencia y escala) el modo en que la neurociencia nos cuenta que funciona nuestro cerebro. Cuestión que nos permite reflexionar sobre lo siguiente:

Las capacidades cognitivas humanas (y del resto del reino animal) parecen ser, como vemos, producto exclusivo del procesado eléctrico por entre trillones de sinapsis en el cerebro. Esto supone que sería precisamente este procesamiento de información eléctrica entre neuronas el que lograría otorgarnos todas y cada una de nuestras capacidades, sin que exista evidencia empírica alguna de que nada más intervenga en el proceso que origina lo que se entiende por mente: englobando aquí conducta, emociones, sensaciones, etc. Esta afirmación basa su fuerza en tres hechos probados: 1º) Que la moderna neurociencia indica experimentalmente que todo apunta a que las redes neuronales biológicas se sobran para acometer la gama completa cognitiva del hombre, 2º) que no se ha observado empíricamente nada más en el cuerpo humano capaz contribuir a tales procesos, y 3º) que además, por otra parte, al emular este comportamiento neuronal biológico de un modo computacional, se observan cada vez resultados más y más parecidos a los observados en los seres vivos.

Por lo tanto, esto nos permite concluir (gracias a todo lo que ya conocemos sobre las redes neuronales biológicas y artificiales) que:
Nuestra mente, en el fondo, no es más que el fruto de una enorme calculadora digital capaz de ejecutar en paralelo cientos de trillones de operaciones por segundo: operaciones que, por cierto, se reducen a meras sumas de potenciales eléctricos.

Referencias.

[1] http://arxiv.org/pdf/1602.01783v1.pdf "Asynchronous Methods for Deep Reinforcement Learning" (Google DeepMind) (2016)
[2] http://quevidaesta2010.blogspot.com.es/2016/02/las-matematicas-de-la-mente.html
[3] https://www.tensorflow.org/ (librería de código abierto ofrecida por Google)
[4] https://deepmind.com/publications.html (web del equipo de desarrollo Google DeepMind)

Entradas populares de este blog

Evidencia a favor de la teoría de Jeremy England (usando computación evolutiva)

"You start with a random clump of atoms, and if you shine light on it for long enough, it should not be so surprising that you get a plant." Jeremy England (2014), interview commentary with Natalie Wolchover Hace ya un mes que terminé de estudiar a fondo el interesante trabajo que el físico  Jeremy England  está realizando en el  MIT (Massachusetts Institute of Technology) . En mi blog he divulgado todo lo referente a este trabajo con mucho nivel de detalle, siendo esta entrada un compendio de todo lo que el trabajo cuenta. La idea de esta línea de investigación viene a decir, a grosso modo , que la física de nuestro mundo mantiene una relación implícita entre complejidad y energía . Esta relación indica que, cuanto más complejo es un fenómeno, más energía debe disiparse de modo que crezca la probabilidad de que tal fenómeno finalmente acontezca. Esta teoría de Jeremy parte, y se deduce, de una base termodinámica y de mecánica estadística ya establecida, por lo que sus concl

Aprendizaje autónomo por computación evolutiva (Conecta 4)

"[Las neuronas son] células de formas delicadas y elegantes, las misteriosas mariposas del alma, cuyo batir de alas quién sabe si esclarecerá algún día el secreto de la vida mental."  (Ramón y Cajal) Introducción. Dibujo de Ramón y Cajal de las células del cerebelo de un pollo,  mostrado en "Estructura de los centros nerviosos de las aves", Madrid, 1905. Dos noticias muy importantes que han tenido lugar estas últimas semanas en el campo de la neurociencia y la inteligencia artificial (de las cuales me hice eco en este mismo blog: aquí [1][2] y  aquí [3]), me hizo recordar un trabajo de computación que hice allá por el 2011 cuando inicié el doctorado en ingeniería (el cual por cierto aún no terminé, y que tengo absolutamente abandonado :( Ya me gustaría tener tiempo libre para poder retomarlo; porque además odio dejar las cosas a medias). Pues bien, el trabajo original[4] (que he mejorado) consistía en ser el desarrollo de un algoritmo capaz de aprender a jugar a