This notebook shows how grammar-guided genetic programming (G3P) can be used to solve the MountainCar-v0 problem from OpenAI Gym. This is achieved by searching for a small program that defines an agent, who uses an algebraic expression of the observed variables to decide which action to take in each moment.
import time
import warnings
import alogos as al
import gym
import unified_map as um
warnings.filterwarnings('ignore')
MountainCar-v0: The aim is to drive a car up the right hill, but its engine is not strong enough, so it needs to build up momentum first. The agent observes the current position and velocity of the car. It can act by pushing the car to the left (value 0), applying no push (value 1), or pushing it to the right (value 2).
env = gym.make('MountainCar-v0')
It allows an agent to act in an environment and collect rewards until the environment signals it is done.
def simulate_single_run(env, agent, render=False):
observation = env.reset()
episode_reward = 0.0
while True:
action = agent.decide(observation)
observation, reward, done, info = env.step(action)
episode_reward += reward
if render:
time.sleep(0.025)
env.render()
if done:
break
env.close()
return episode_reward
def simulate_multiple_runs(env, agent, n):
total_reward = sum(simulate_single_run(env, agent) for _ in range(n))
mean_reward = total_reward / n
return mean_reward
num_sim = 200
class Agent:
def decide(self, observation):
position, velocity = observation
lb = min(-0.09 * (position + 0.25) ** 2 + 0.03,
0.3 * (position + 0.9) ** 4 - 0.008)
ub = -0.07 * (position + 0.38) ** 2 + 0.07
if lb < velocity < ub:
action = 2 # push right
else:
action = 0 # push left
return action
agent = Agent()
simulate_multiple_runs(env, agent, num_sim)
class Agent:
def decide(self, observation):
position, velocity = observation
output = (7.83**velocity)
action = 0 if output < 1.0 else 2
return action
agent = Agent()
simulate_multiple_runs(env, agent, num_sim)
class Agent:
def decide(self, observation):
position, velocity = observation
output = (4.59/(3.35*velocity))
action = 0 if output < 1.0 else 2
return action
agent = Agent()
simulate_multiple_runs(env, agent, num_sim)
class Agent:
def decide(self, observation):
position, velocity = observation
output = (((((2.18/velocity)-velocity)-((((velocity/7.27)/position)+(velocity*(position-position)))*(((2.64+(8.48*velocity))+(5.86*position))*9.40)))+((5.59*position)+(((0.19*(((velocity-(velocity*4.62))+1.42)+(((0.09-position)**6.40)*5.21)))**((4.09/(7.32/6.71))/5.33))**((position*(position*(1.69+(3.20-3.13))))**8.44))))/(velocity/velocity))
action = 0 if output < 1.0 else 2
return action
agent = Agent()
simulate_multiple_runs(env, agent, num_sim)
This grammar defines the search space: a Python program that creates an Agent who uses an algebraic expression of the observed variables to decide how to act in each situation.
ebnf_text = """
PROGRAM = L0 NL L1 NL L2 NL L3 NL L4 NL L5
L0 = "class Agent:"
L1 = " def decide(self, observation):"
L2 = " position, velocity = observation"
L3 = " output = " EXPR
L4 = " action = 0 if output < 1.0 else 2"
L5 = " return action"
NL = "\n"
EXPR = VAR | CONST | "(" EXPR OP EXPR ")"
VAR = "position" | "velocity"
CONST = DIGIT "." DIGIT DIGIT
OP = "+" | "-" | "*" | "/" | "**"
DIGIT = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
"""
grammar = al.Grammar(ebnf_text=ebnf_text)
The objective function gets a candidate solution (=a string of the grammar's language) and returns a fitness value for it. This is done by 1) executing the string as a Python program, so that it creates an agent object, and then 2) using the agent in multiple simulations to see how good it can handle different situations: the higher the total reward, the better is the candidate.
def string_to_agent(string):
local_vars = dict()
exec(string, None, local_vars)
Agent = local_vars['Agent']
return Agent()
def objective_function(string):
agent = string_to_agent(string)
avg_reward = simulate_multiple_runs(env, agent, 10)
return avg_reward
Check if grammar and objective function work as intended.
random_string = grammar.generate_string()
print(random_string)
objective_function(random_string)
ea = al.EvolutionaryAlgorithm(
grammar, objective_function, 'max', max_or_min_fitness=-100,
population_size=200, offspring_size=200, evaluator=um.univariate.parallel.futures, verbose=True)
best_ind = ea.run()
string = best_ind.phenotype
print(string)
agent = string_to_agent(string)
simulate_multiple_runs(env, agent, 100)
simulate_single_run(env, agent, render=True)