En esta práctica construiremos un sistema basado en aprendizaje por refuerzo para controlar el equilibrio de un péndulo invertido sobre una plataforma móvil, como muestra la animación inferior. Usaremos el framework de experimentación en RL OpenAI Gym, concretamente el entorno Cart-pole-V0. Y todo ello lo haremos con una simple neurona.
OpenAI Gym nos proporciona todo lo necesario para empezar: observación del estado del entorno, las acciones posibles a realizar y una representación gráfica de cómo va yendo el conjunto péndulo-plataforma.
Las observaciones que tenemos son: la posición de la plataforma, la velocidad de la plataforma, el ángulo del péndulo y la velocidad de rotación. Y las acciones son mover la plataforma a la derecha o a la izquierda.
import numpy as np
import gym
env = gym.make('CartPole-v0')
[2016-11-11 18:04:47,362] Making new env: CartPole-v0
Empezaremos ejecutando algunos ejemplos de interacción con el entorno elegiendo acciones al azar. Puedes descomentar la impresión de las observaciones para ver los datos.
def run_random(env):
for _ in range(20):
observation = env.reset()
for t in range(100):
env.render()
#print(observation)
action = env.action_space.sample()
observation, reward, done, info = env.step(action)
#if done:
# print("Episode finished after {} timesteps".format(t+1))
# break
run_random(env)
Para controlar la plataforma utilizaremos una simple neurona que tendrá como entradas las observaciones y como salida la acción a aplicar: 0 para empujar en una dirección y 1 para la otra dirección.
r=4∑i=1observationi⋅wiSi r<0 entonces la salida de la neurona será 0 y si r≥0 la salida será 1.
Nuestro problema ahora es determinar el conjunto de pesos W de la neurona que consigan mantener el péndulo en equilibrio. Y aquí es donde entra en juego el método de entropía cruzada (CEM). A priori no tenemos ningún criterio para establecer unos valores iniciales a los pesos, así que podemos asignarlos al azar. Lanzamos unos, por ejemplo, 100 episodios con pesos aleatorios en cada episodio. Unos pesos funcionarán mejor que otros. Es decir, harán que el péndulo aguante más pasos en equilibiro antes de caerse. De esos 100 conjuntos de pesos escogemos los 10 mejores conjuntos. Con ellos generaremos una distribución gaussiana de probabilidad.
μ=E[X]=∑10i=1xi10A partir de esta función generaremos una nueva lista de conjuntos de pesos (x1,x2,…x100)∼N(μ,σ). La diferencia es que ahora este nuevo conjunto estará compuesto por mejores soluciones que la lista anterior. Si repetimos esta operación un número de veces llegaremos a buenas soluciones que logren el objetivo buscado.
mu = [0., 0., 0., 0.] # first means
sigma = [1., 1., 1., 1.] # first standard deviations
episodies = 100
iterations = 10
Definimos la función que ejecutará cada episodio (desde el momento inicial hasta que llegamos a los max_reward
pasos o hasta que el péndulo se nos caiga.)
def run_episode(env, weights, render=False, max_reward=200):
observation = env.reset()
totalreward = 0
for _ in xrange(max_reward):
if render:
env.render()
action = 0 if np.matmul(weights, observation) < 0 else 1 # this line is our agent (just one neuron!!)
observation, reward, done, info = env.step(action)
totalreward += reward
if done:
break
return totalreward
Comenzamos las iteraciones del método de entropía cruzada.
for iteration in xrange(iterations):
weights = []
for m, s in zip(mu, sigma):
weights.append(np.random.normal(m, s, episodies))
weights = np.transpose(weights)
rewards = []
number_of_goals = 0
for w in weights:
r = run_episode(env, w, max_reward=500)
rewards.append(r)
if r == 500:
number_of_goals += 1
# We combine in a list parameteres+rewards and sort it by rewards.
# To do that we use a lambda function
l = sorted(zip(weights, rewards), key=lambda pair: pair[1])
# We get the last ten (they will be those with the higher reward), but
# only first component (parameters) is needed.
l = list(zip(*l[-10:])[0])
mu = np.mean(l, 0)
sigma = np.std(l, 0)
print "------------"
print "Iteration:", iteration
print "Mean:", mu
print "Standard deviation:", sigma
print "# goals:", number_of_goals
------------ Iteration: 0 Mean: [-0.36985362 -0.14822899 0.72377799 0.98717426] Standard deviation: [ 0.82260298 0.66286072 0.58501013 0.39408498] # goals: 2 ------------ Iteration: 1 Mean: [-0.23977643 -0.10041071 1.28180171 1.03110944] Standard deviation: [ 0.51610659 0.53550408 0.44852136 0.42317155] # goals: 22 ------------ Iteration: 2 Mean: [-0.00332877 0.06113365 1.53675028 0.94866862] Standard deviation: [ 0.26565696 0.32307113 0.37484201 0.40319855] # goals: 31 ------------ Iteration: 3 Mean: [ 0.04251544 0.2022577 1.52623168 0.92945207] Standard deviation: [ 0.18123304 0.379544 0.19979383 0.213252 ] # goals: 62 ------------ Iteration: 4 Mean: [ 0.12392336 0.28386005 1.629934 0.9189151 ] Standard deviation: [ 0.14054255 0.4080691 0.19662461 0.22923035] # goals: 90 ------------ Iteration: 5 Mean: [ 0.04041372 0.23664824 1.47115431 0.9291692 ] Standard deviation: [ 0.11986737 0.39489579 0.12405115 0.22398743] # goals: 85 ------------ Iteration: 6 Mean: [ 0.02594185 0.26083691 1.4756004 0.9878746 ] Standard deviation: [ 0.11717854 0.41890499 0.10578672 0.17851627] # goals: 84 ------------ Iteration: 7 Mean: [ 0.03396054 0.25223639 1.50126133 0.99625333] Standard deviation: [ 0.13795761 0.40828132 0.09355774 0.15511304] # goals: 93 ------------ Iteration: 8 Mean: [ 0.02361878 0.32910758 1.51344029 1.02798995] Standard deviation: [ 0.0652423 0.36218886 0.06885026 0.16725886] # goals: 89 ------------ Iteration: 9 Mean: [ 0.04411325 0.25935775 1.54331726 1.00500786] Standard deviation: [ 0.04648671 0.26509399 0.09185551 0.09974668] # goals: 98
Ejecutamos un episodio con 1500 pasos a ver qué tal va.
run_episode(env, mu, render=True, max_reward=1500)
1500.0