In this notebook we show the results of a Leaky Integrator Echo State Network Reinforcement Learning algorithm closely followed from the work by Zhang et al. on the cartpole environment:
Reservoir nodes are initialized to 0 and updated according to $\bf{h}_t^{(k)} = \bf{h}_t^{(k-1)} + max(0,\bf{W}_{in} \cdot \bf{x}_t^{(k)} + \bf{b})$, where input weights $\bf{W}_{in}$ and the bias are initialized randomly. 't' is the time step until the environment returns 'done' and k stands for the $k^{th}$ entry of the $i^{th}$ series element $(\bf{s}_{t,i}^{(k)},\bf{a}_{t,i}^{(k)},\bf{s'}_{t,i}^{(k)},\bf{r}_{t,i}^{(k)})$ in the minibatch $\{(\bf{s}_{t,i}^{(k)},\bf{a}_{t,i}^{(k)},\bf{s'}_{t,i}^{(k)},\bf{r}_{t,i}^{(k)})\}_{i=1,...,M;k=1,...,T}$. Batch size $M$ is set to 64 and length of each series $T$ is 5
The rest of the algorithm is analogous to the well-known DQN algorithm. The Q values of the action space are calculated by the policy network $Q(\bf{S_t},\mathcal{A},\Theta_{t-1}) = \Theta_{t-1} \cdot \bf{U}_t^{k}$ and the optimal Q values are approximated by the target network $Q^{\pi}(\bf{S'_t},\mathcal{A},\tilde{\Theta}) = \tilde{\Theta} \cdot \bf{U'}_t^{k}$, where $\Theta_{t-1}$ and $\tilde{\Theta}$ consist of the randomly initialized output weights and output bias $[W_{out};b^{out}]$ and $\bf{U}_t^{k} = [\bf{S}_{t}^{(k)};\bf{H}_{t}^{(k)};\bf{1}]$ with $\bf{S}_{t}^{(k)}$ containing the states in the batch as column vectors. $Q^{\pi}$ is later updated according to Bellman equation with the rewards in the batch $r_t^{(k)}$ : $Q^{\pi}(\bf{S}_t^{(k)},\mathcal{A}) = r_t^{(k)} + \gamma \cdot Q^{\pi}(\bf{S'_t},\mathcal{A},\tilde{\Theta})$
Another key aspect of the algorithm is the mean approximation in the gradient calculation. We average over the series elements and batch elements in the following to acquire one $Q^{\pi}$ and $Q$ at each iteration: \begin{align} \bf{\bar{u}}_t &= \frac{1}{MT} \sum_{k=1}^{T} \sum_{i=1}^{M} \bf{U}_{t,i}^{k}, \text{where $\bf{U}_{t,i}^{k}$ is the $i^{th}$ column vector of $\bf{U}_t^{k}$} \\ \bar{Q}^{\pi}_t &= \frac{1}{MT} \sum_{k=1}^{T} \sum_{i=1}^{M} Q^{\pi}(\bf{s}_t^{(k)},\mathcal{A}) \\ \bar{Q}_t &= \frac{1}{MT} \sum_{k=1}^{T} \sum_{i=1}^{M} Q(\bf{s}_{t,i}^{(k)},\mathcal{A},\Theta_{t-1}) \end{align}
The output weights are then updated with the learning rate \begin{align} \alpha_t &= \frac{P_{t-1}^T}{\lambda + \bf{v}_t^T \bf{\bar{u}}_t} \end{align} and the gradient \begin{align} \nabla_{t-1} &= (\bar{Q}^{\pi}_t - \bar{Q}_t) \bf{\bar{u}}_t^T \end{align} and the regularization term \begin{align} L_1 &= - \kappa \cdot sgn(\Theta_{t-1})P_{t-1}^T \end{align}
yielding
\begin{equation} \Theta_{t} = \Theta_{t-1} + \nabla_{t-1} \alpha_t + L_1 \end{equation}, where
\begin{align} P_{t} &= \frac{1}{\lambda}(P_{t-1} - \bf{g_t}\bf{v}_t^T) \\ \bf{v}_t &= P_{t-1}\bf{\bar{u}}_t \\ \bf{g}_t &= \frac{\bf{v}_t}{\lambda + \bf{v}_t^T \bf{\bar{u}}_t} \end{align}import sys,os
import pandas as pd
import numpy as np
import gym
import numpy as np
import torch
from IPython.display import clear_output
from tqdm.notebook import tqdm,trange
sys.path.append("./../../../")
from collections import namedtuple, deque
from itertools import count
import random,math
from matplotlib import pyplot as plt
def at_least_2d(arr):
if len(arr.shape)==1:
return arr[:,None]
elif len(arr.shape)==2:
return arr
else:
raise Exception("Unsupported array shape.")
env = gym.make("CartPole-v0")
n_actions = env.action_space.n
relu = np.vectorize(lambda x: x if x>=0 else 0*x)#,otypes=[np.float64])
random.seed(42)
np.random.seed(42)
env.seed(42);
BATCH_SIZE = 64
MEMORY_SIZE = int(1e5)
GAMMA = 0.99
EPS_START = 0.01
EPS_END = 0.01
EPS_DECAY = 256
TARGET_UPDATE = 1
resSize = 256
bias=1
T_transient = 5
T_opt = 5
T = T_transient+T_opt
forgetting_factor = 0.99999
kappa = 1e-5
max_episodes=100
omega = 1
P_alpha = 0.4
As the agent chooses actions the states, actions, next states and rewards will be recorded into replay memory.
Transition = namedtuple('Transition',
('state', 'action', 'next_state', 'reward', 'done'))
class ReplayMemory(object):
def __init__(self, capacity,series_length):
self.series_memory = deque([],maxlen=series_length)
self.memory = deque([],maxlen=capacity)
self.length = 0
self.series_length = series_length
def push(self, *args):
"""Save a transition"""
if args[4]:
while len(self.series_memory)<self.series_length:
self.series_memory.append(Transition(*args))
self.length +=1
else:
self.series_memory.append(Transition(*args))
self.length +=1
if len(self.series_memory)==self.series_length:
self.memory.append(self.series_memory)
self.series_memory = deque([],maxlen=self.series_length)
def sample(self, batch_size):
return random.sample(self.memory, batch_size)
def __len__(self):
return len(self.memory)
At each iteration the policy network weights are optimized via gradient ascent.
def optimize_model():
global P
global policy_train_res_layer
global policy_train_Wout
global target_train_res_layer
if len(memory) < BATCH_SIZE:
return False
transition_series = memory.sample(BATCH_SIZE)
batch_series = pd.DataFrame(transition_series)
q_diff = np.zeros((env.action_space.n,BATCH_SIZE))
u = np.zeros((env.observation_space.shape[0]+resSize+bias,BATCH_SIZE))
for i in range(T):
batch = Transition(*zip(*batch_series[i].to_list()))
state_batch = np.array(batch.state).T
action_batch = np.array(batch.action)
reward_batch = np.array(batch.reward)
next_state_batch = np.array([s if s is not None else env.observation_space.shape[0]*[0] for s in batch.next_state]).T
done_batch = np.array(batch.done)
policy_train_res_layer = update_f(policy_train_res_layer,state_batch)
target_train_res_layer = update_f(target_train_res_layer,next_state_batch)
if i > T_transient:
U = get_U(policy_train_res_layer,state_batch)
state_action_values = np.dot(policy_train_Wout,U)
u += U
#target_train_res_layer = update_f(target_train_res_layer,next_state_batch)
next_state_values = np.dot(target_train_Wout,get_U(target_train_res_layer,next_state_batch))
next_state_values = (1-done_batch)*next_state_values
# Compute the expected Q values
expected_state_action_values = (next_state_values * GAMMA) + reward_batch
q_diff += expected_state_action_values - state_action_values
### UPDATE SECTION ###
#Necessary for updates
q_diff = q_diff.sum(1,keepdims=1)/BATCH_SIZE/T
u = u.sum(1,keepdims=1)/BATCH_SIZE/T
v = np.dot(P,u)
g = v/(forgetting_factor+np.dot(v.T,u))
# Updates
policy_train_Wout+= np.dot(q_diff,g.T) - kappa*np.dot(np.sign(policy_train_Wout),P.T)
P = (P-np.dot(g,v.T))/forgetting_factor
policy_train_res_layer = np.zeros((resSize,BATCH_SIZE))
target_train_res_layer = np.zeros((resSize,1))
#####################
return True
Wout_0 = np.random.rand(n_actions,env.observation_space.shape[0]+resSize+bias)
Win = np.random.rand(resSize,bias+env.observation_space.shape[0]) - 0.5
def update_f(x,in_):
_in_ = at_least_2d(in_)
_bias = at_least_2d(np.array(_in_.shape[1]*[bias]))
return x + relu(np.dot(Win, np.vstack((_bias.T,_in_))))
def get_U(x,in_):
_in_ = at_least_2d(in_)
_bias = at_least_2d(np.array(_in_.shape[1]*[bias]))
return np.hstack((_in_.T,x.T,_bias)).T
policy_res_layer = np.zeros((resSize,1))
policy_train_res_layer = np.zeros((resSize,BATCH_SIZE))
target_train_res_layer = np.zeros((resSize,1))
policy_Wout = Wout_0.copy()
policy_train_Wout = Wout_0.copy()
target_train_Wout = Wout_0.copy()
# P from Sherman-Morrison formula
P_0 = np.identity(env.observation_space.shape[0]+resSize+1)*P_alpha
P = P_0.copy()
# Parameter to track no of ALL steps taken during the WHOLE training.
steps_done = 0
episode_durations = []
highest_score = 0
def select_action(state):
global steps_done
global policy_res_layer
sample = random.random()
eps_threshold = EPS_END + (EPS_START - EPS_END) * \
math.exp(-1. * steps_done / EPS_DECAY)
steps_done += 1
if sample > eps_threshold:
policy_res_layer = update_f(policy_res_layer,state)
return np.argmax(np.dot(policy_Wout,get_U(policy_res_layer,state)))
else:
return random.randrange(n_actions)
memory = ReplayMemory(MEMORY_SIZE,T)
i_episode = tqdm(total=max_episodes)
while i_episode.last_print_n < max_episodes:
assert np.all(policy_Wout == Wout_0.copy())
assert np.all(policy_train_Wout == Wout_0.copy())
assert np.all(policy_res_layer == np.zeros((resSize,1)))
assert np.all(policy_train_res_layer == np.zeros((resSize,BATCH_SIZE)))
assert np.all(target_train_res_layer == np.zeros((resSize,1)))
assert np.all(P_0 == np.identity(env.observation_space.shape[0]+resSize+1)*P_alpha)
assert np.all(P==P_0)
# Initialize the environment and state
state = env.reset()
for t in count():
assert np.all(policy_train_Wout == policy_Wout)
# Select and perform an action
action = select_action(state)
next_state, reward, done, _ = env.step(action)
# Observe new state
if done:
next_state = None
if t < 199:
reward=-10
# Store the transition in memory
memory.push(state, action, next_state, reward, done)
# Move to the next state
state = next_state
# Perform one step of the optimization (on the policy network)
optimized = optimize_model()
if optimized:
policy_Wout = policy_train_Wout.copy()
if i_episode.last_print_n % TARGET_UPDATE == 0:
target_train_Wout = policy_train_Wout.copy()
if done:
policy_res_layer = np.zeros((resSize,1))
if optimized:
episode_durations.append(t + 1)
i_episode.update()
P = P_0.copy()
policy_train_Wout = Wout_0.copy()
policy_Wout = Wout_0.copy()
break
print('Complete')
plt.plot(episode_durations)
plt.show()
We let the agent play the game 100 times and collect the results.
ep_lengths = []
target_res_layer = np.zeros((resSize,1))
target_Wout = target_train_Wout.copy()
for i in range(100):
state = env.reset()
done=0
rewards=[]
target_res_layer = np.zeros((resSize,1))
while not done:
target_res_layer = update_f(target_res_layer,state)
action = np.argmax(np.dot(target_Wout,get_U(target_res_layer,state)))
next_state, reward, done, _ = env.step(action)
state = next_state
rewards.append(reward)
ep_lengths.append(len(rewards))
np.mean(ep_lengths)
The average episode length is ~194. More than 80% of the time, the agent is able to complete the game succesfully.
plt.hist(ep_lengths,align='right')
plt.show()