Created
October 18, 2020 15:53
-
-
Save youngsoul/b9b96a0a1874cbf3ea2beb1627ec137c to your computer and use it in GitHub Desktop.
QLearner for the Grid Reinforcement learning game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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