Skip to content

Instantly share code, notes, and snippets.

@youngsoul
Created October 18, 2020 15:53
Show Gist options
  • Select an option

  • Save youngsoul/b9b96a0a1874cbf3ea2beb1627ec137c to your computer and use it in GitHub Desktop.

Select an option

Save youngsoul/b9b96a0a1874cbf3ea2beb1627ec137c to your computer and use it in GitHub Desktop.
QLearner for the Grid Reinforcement learning game
import numpy as np
import matplotlib.pyplot as plt
import pickle
import time
from matplotlib import style
from GridEnvironment import GridEnvironment
import re
import argparse
import pandas as pd
"""
10x10, 1 Lose Grid with no Q-Table
--grid-size 10 --episodes 5 --show-every 1 --exploit-only False
10x10, 1 Lose Grid with trained Q-Table
--start-q-table qtable-grid-10-1-1568086658.pickle --grid-size 10 --episodes 40 --show-every 1
10x10, 1 Lose Grid with trained Q-Table, Move Blobs
--start-q-table qtable-grid-10-1-1568086658.pickle --grid-size 10 --episodes 40 --show-every 1 --move-blobs True
20x20, 1 Lose Grid with trained Q-Table, Move Blobs
--start-q-table qtable-grid-20-1-1568087101.pickle --grid-size 20 --episodes 40 --show-every 1 --move-blobs True
10x10, 2 Lose Grid with no Q-Table
--grid-size 10 --num-lose-blobs 2 --episodes 5000 --show-every 1000 --exploit-only False
10x10, 2 Lose Grid with trained Q-Table
--grid-size 10 --num-lose-blobs 2 --start-q-table qtable-two_lose-10-2-large-__1568658668__.pickle --episodes 40 --show-every 1 --exploit-only True
10x10, 2 Lose Grid with trained Q-Table, moving blobs
--grid-size 10 --num-lose-blobs 2 --start-q-table qtable-two_lose-10-2-large-__1568658668__.pickle --episodes 40 --show-every 1 --exploit-only True --move-blobs True
"""
# np.random.seed(42)
style.use('ggplot')
# Size of the grid
SIZE = 10
TOTAL_EPISODES = 40
MOVES_PER_EPISODE = 200 # 200 for SIZE = 10
SHOW_EVERY = 1 # how often to play through the env visually
DISPLAY_FIRST_N_EPISODES = 2 # display the first N. Helps to visualize the early failures
DISPLAY_LAST_N_EPISODES = 100 # display the last N to demonstrate how much better the learning became
RENDER_SPEED_DELAY = 0.08 # 0 - no delay, 0.05 provides nice playback
MOVE_BLOBS = False
NUMBER_OF_LOSE_BLOBS = 2
SAVE_Q_TABLE = False
EXPLOIT_ONLY = False
SHOW_GRID_LINES = False
NUM_OF_WALL_BLOBS=0
EXECUTION_NAME = ""
SAVE_ACTION_Q_TABLE = False
# epsilon=0.95 Explore and Exploit
# epsilon=0.0 - then it will only exploit and never explore
epsilon = 0.95
EPS_DECAY = 0.9998 # every episode will be epsilon*EPS_DECAY
LEARNING_RATE = 0.1
DISCOUNT = 0.95
start_q_table = None # 'qtable-two_lose-10-2-large-__1568658668__.pickle' # if we have a pickled Q table, filename will be here
# start_q_table = None # if we have a pickled Q table, filename will be here
def run_q_learning_simulation():
global epsilon
# keep track of how each action's q-value changes
# array of tuples: (action,old,new)
action_q_value_changes = []
# start to iterate over the episodes
episode_rewards = []
episode_moves = []
win_count = 0
lose_count = 0
found_neither_count = 0
grid_environment = GridEnvironment(SIZE, lose_blob_count=NUMBER_OF_LOSE_BLOBS, number_of_wall_blobs=NUM_OF_WALL_BLOBS)
if start_q_table is None:
q_table = grid_environment.create_q_table()
else:
with open(start_q_table, "rb") as f:
q_table = pickle.load(f)
for episode in range(TOTAL_EPISODES+1):
current_state = grid_environment.reset()
if episode > 0 and episode % SHOW_EVERY == 0:
# print(f"on #{episode}, epsilon is {epsilon}")
# print(f"{SHOW_EVERY} ep mean: {np.mean(episode_rewards[-SHOW_EVERY:])}")
show = True
else:
show = False
if 0 <= episode <= DISPLAY_FIRST_N_EPISODES:
show = True
if (TOTAL_EPISODES - DISPLAY_LAST_N_EPISODES) <= episode <= TOTAL_EPISODES:
show = True
episode_reward = 0
for i in range(MOVES_PER_EPISODE):
if np.random.random() >= epsilon:
# Exploit
action = np.argmax(q_table[current_state])
else:
# Explore
action = np.random.randint(0, grid_environment.actionspace.number_of_actions)
if MOVE_BLOBS:
for lose_blob in grid_environment.lose_blobs:
lose_blob.move()
grid_environment.win_blob.move()
# check to see if the win blob was moved to a lose blob square
# if so keep moving the win blob
while any(x == grid_environment.win_blob for x in grid_environment.lose_blobs):
grid_environment.win_blob.move()
# take the action
next_state, reward, done = grid_environment.step(action)
max_future_q = np.max(q_table[next_state]) # max Q value for this new obs
current_q = q_table[current_state + (action,)]
if reward == grid_environment.WIN_REWARD:
new_q = grid_environment.WIN_REWARD
else:
new_q = (1 - LEARNING_RATE) * current_q + LEARNING_RATE * (reward + DISCOUNT * max_future_q)
old_q = q_table[current_state + (action,)]
q_table[current_state + (action,)] = new_q
if SAVE_ACTION_Q_TABLE:
action_q_value_changes.append((str(current_state), action, old_q, new_q))
episode_reward += reward
# ---------------------------------------------------------------------
# RENDER SIMULATION
# ---------------------------------------------------------------------
# I am drawing twice because it seems in later OpenCV versions
# it lagged by a frame when the game ended. It showed the move
# just PRIOR to winning or losing.
# I cannot figure this out but getting it to draw twice seems to
# mostly fix it.
grid_environment.render(show, slow_render=0, done=False, episode_number=episode, total_episodes=TOTAL_EPISODES,
move_number=i, show_grid=SHOW_GRID_LINES, episode_reward=episode_reward)
grid_environment.render(show, slow_render=RENDER_SPEED_DELAY, done=done, episode_number=episode, total_episodes=TOTAL_EPISODES,
move_number=i, show_grid=SHOW_GRID_LINES, episode_reward=episode_reward)
# end if we reach food or hit enemy
if reward == grid_environment.WIN_REWARD:
win_count += 1
episode_moves.append(i)
break
if reward == grid_environment.LOSE_PENALTY:
lose_count += 1
episode_moves.append(i)
break
if done: # if the environment tells us we are done then stop
break
# -------- Important Last Step ---------
# Set the next current state, to the next_state we just processed
current_state = next_state
else:
found_neither_count += 1
episode_moves.append(MOVES_PER_EPISODE)
episode_rewards.append(episode_reward)
epsilon *= EPS_DECAY
grid_environment.destroy()
# Show moving average of rewards per episode
moving_avg = np.convolve(episode_rewards, np.ones((SHOW_EVERY,)) / SHOW_EVERY, mode='valid')
plt.plot([i for i in range(len(moving_avg))], moving_avg)
plt.ylabel(f"Reward")
plt.xlabel("episode #")
if EXECUTION_NAME:
plt.savefig(f"./results/{EXECUTION_NAME}_reward.png")
plt.show()
# show moving average of moves per episode
moving_avg_moves = np.convolve(episode_moves, np.ones((SHOW_EVERY,)) / SHOW_EVERY, mode='valid')
plt.plot([i for i in range(len(moving_avg))], moving_avg_moves)
plt.ylabel(f"# Moves")
plt.xlabel("episode #")
if EXECUTION_NAME:
plt.savefig(f"./results/{EXECUTION_NAME}_moves.png")
plt.show()
if SAVE_Q_TABLE:
if start_q_table:
# then base the new name off the old name
file_name = re.sub(r"(__.*__)", f"__{int(time.time())}__", start_q_table)
else:
file_name = f"qtable-{EXECUTION_NAME}-{SIZE}-{NUMBER_OF_LOSE_BLOBS}-__{int(time.time())}__.pickle"
with open(file_name, "wb") as f:
pickle.dump(q_table, f)
print(f"Training Episodes: {TOTAL_EPISODES}")
print(f"Win Percentage: {(win_count / (TOTAL_EPISODES+1)) * 100:.2f}")
print(f"Lose Percentage: {(lose_count / (TOTAL_EPISODES+1)) * 100:.2f}")
print(f"Neither Percentage: {(found_neither_count / (TOTAL_EPISODES+1)) * 100:.2f}")
if SAVE_ACTION_Q_TABLE:
print("Saving Action Q Values")
# create a dataframe of the action q_values
df = pd.DataFrame(action_q_value_changes, columns=['Current_State', 'Action', 'Old_Q_Value', 'New_Q_Value'])
df.to_parquet('action_q.gzip', compression='gzip')
if __name__ == '__main__':
ap = argparse.ArgumentParser()
ap.add_argument("--grid-size", required=False, type=int, default=10,
help="either 10 for 10x10 or 20 for a 20x20")
ap.add_argument("--episodes", required=False, type=int, default=40,
help="Number of episodes to run. If training large values such as 50000, for demo use small values like 40")
ap.add_argument("--show-every", required=False, type=int, default=1,
help="Show the simulation every 'show-every' number of episodes")
ap.add_argument("--move-blobs", required=False, type=bool, default=False,
help="Move the blobs while running")
ap.add_argument("--num-lose-blobs", required=False, type=int, default=1,
help="Either 1 or 2. The number of Red losing blobs")
ap.add_argument("--save-q-table", required=False, type=bool, default=False,
help="True - save the Q table, False do not save Q Table")
ap.add_argument("--start-q-table", required=False, type=str, default=None,
help="Name of previously saved Q table to start with")
ap.add_argument("--exploit-only", required=False, type=bool, default=False,
help="True - only exploit the values in the Q Table. False - use a decaying epsilon to also explore the environment")
ap.add_argument("--show-grid-lines", required=False, type=bool, default=True,
help="True - show grid lines when rendering, False - do not show grid lines")
ap.add_argument("--num-wall-blobs", required=False, type=int, default=0,
help="Number of wall blobs that player has to go around")
ap.add_argument("--show-first-n", required=False, type=int, default=2,
help="Show the first n episodes")
ap.add_argument("--show-last-n", required=False, type=int, default=10,
help="Show the last n episodes")
ap.add_argument("--name", required=False, type=str, default="",
help="name to prefix the Plot images with")
ap.add_argument("--save-action-q-values", required=False, type=bool, default=False,
help="Save the Action Q Values ( action, old, new) as a Pandas parquet dataframe")
args = vars(ap.parse_args())
if 'save_action_q_values' in args:
SAVE_ACTION_Q_TABLE = args['save_action_q_values']
if 'exploit_only' in args:
EXPLOIT_ONLY = args['exploit_only']
if EXPLOIT_ONLY:
epsilon = 0.0
if 'name' in args:
if args['name']:
EXECUTION_NAME = args['name']
if 'show_first_n' in args:
DISPLAY_FIRST_N_EPISODES = args['show_first_n']
if 'show_last_n' in args:
DISPLAY_LAST_N_EPISODES = args['show_last_n']
if 'start_q_table' in args:
start_q_table = args['start_q_table']
if 'save_q_table' in args:
SAVE_Q_TABLE = args['save_q_table']
if 'num_lose_blobs' in args:
NUMBER_OF_LOSE_BLOBS = args['num_lose_blobs']
if 'move_blobs' in args:
MOVE_BLOBS = args['move_blobs']
if 'grid_size' in args:
SIZE = args['grid_size']
if 'episodes' in args:
TOTAL_EPISODES = args['episodes']
if 'show_every' in args:
SHOW_EVERY = args['show_every']
if 'show_grid_lines' in args:
SHOW_GRID_LINES = args['show_grid_lines']
if 'num_wall_blobs' in args:
NUM_OF_WALL_BLOBS = args['num_wall_blobs']
if SIZE > 10 and NUMBER_OF_LOSE_BLOBS > 1:
raise ValueError("You cannot have a grid larger than 10 with move than a single lose blob.")
start = time.time()
run_q_learning_simulation()
end = time.time()
print(f"Simulation Time: {(end-start)}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment