Sunday, August 14, 2016

"Neuronlike Adaptive Elements That Can Solve Difficult Learning Control Problems"

This is a Python implementation of the method presented in the paper "Neuronlike Adaptive Elements That Can Solve Difficult Learning Control Problems", published in 1983.

The original C implementation written by the authors can be found here.

The "ASE" and "ACE" are neurons which:

  1. have a set of weights which determins the output of the element
    • ASE's weight determine the outputed action
    • ACE's weights determine the corrected reward
  2. update weights according to a
    • reward input
    • exponentially-decaying eligibility trace

In the paper the raw observation vectors are first passed to a decoder which creates the $x$ seen below. $x$ is one-hot encoded.

So even though the below formulas are written in terms of vectors and dot products, anything involving $x$ is really just an index.

e.g. $\mathbf{w}^\intercal \mathbf{x}$

is equivalent to

"Let i be the index where there is a 1 in w. Return x[i]"

Decision function of the ASE:

$ y(t) = \text{sign}(\mathbf{w}^\intercal \mathbf{x} + \text{noise}(t))$

Update rules:

$\mathbf{w}(t+1) = \mathbf{w} + \alpha r(t) \mathbf{e}(t) $

$\mathbf{e}(t+1) = \delta \mathbf{e}(t) + (1-\delta) y(t)\mathbf{x}(t) $

ACE Output (an improved prediction):

$\hat{r}(t) = r(t) + \gamma p(t) - p(t-1)$

where $p$ is the prediction of the reward $p(t) = \mathbf{v}^\intercal \mathbf{x}$

Update rules:

$\mathbf{v}(t+1) = \mathbf{v}(t) + \beta{\hat{r}}\mathbf{\bar{x}}(t)$

$\mathbf{\bar{x}}(t+1) = \lambda\mathbf{\bar{x}}(t) + (1-\lambda) \mathbf{x}$

In [10]:
import numpy as np
In [11]:
import numpy as np
from sklearn.utils import check_random_state

class ASE(object):
    def __init__(self, n_input_dim, learning_rate, decay_rate, sigma=0.01, random_state=None):
        self.n_input_dim = n_input_dim
        self.learning_rate = learning_rate
        self.decay_rate = decay_rate
        self.sigma = sigma
        self.random_state = random_state
        
        self.reset_values()
    
    def reset_values(self):
        # Weights
        self.w = np.zeros(self.n_input_dim)

        # Eligibility
        self.e = np.zeros(self.n_input_dim)
        
        self.rs = check_random_state(self.random_state)
        
    def step(self, x, reward=None):
        '''
        Updates the weights and eligibility trace.
        
        Parameters
        ----------
        'x' : the state vector 
        'reward' : the reward value
        
        Returns
        -------
        Action : 0 or 1
        '''
        # Probabilistic action
        action = self.w.dot(x) + self.rs.randn()*self.sigma
        action = np.clip(action, -50, 50)
        action = int((1 + np.exp(-action)) ** (-1) > 0.5)

        if reward is not None:
            self.w += self.learning_rate * reward * self.e       

            self.e *= self.decay_rate
            self.e += (1. - self.decay_rate) * (action*2 - 1) * x
        
        return action
        
In [20]:
class ACE(object):

    def __init__(self, n_input_dim, learning_rate, decay_rate, discount_factor, random_state=None):
        self.n_input_dim = n_input_dim
        self.learning_rate = learning_rate
        self.decay_rate = decay_rate
        self.discount_factor = discount_factor
        self.random_state = random_state
        
        self.reset_values()
        
    def reset_values(self):
        self.rs = check_random_state(self.random_state)
        self.w = np.zeros(self.n_input_dim)
        self.trace = np.zeros(self.n_input_dim)
        self.prev_p = 0.
        
    def step(self, x, reward=None, done=False):
        if done:
            p = 0.
        else:
            p = self.w.dot(x)
        
        if reward is None:
            self.prev_p = p
            return None
        
        else:
            revised_reward = reward + self.discount_factor * p - self.prev_p
            
            self.w += self.learning_rate * revised_reward * self.trace
            self.trace *= self.decay_rate
            self.trace += (1. - self.decay_rate) * x
            
            self.prev_p = p
            return revised_reward
In [21]:
ONE_DEGREE = 1. * np.pi / 180
SIX_DEGREES = 6. * np.pi / 180
FIFTY_DEGREES = 50. * np.pi / 180

def get_box(observation):
    x, x_dot, theta, theta_dot = observation
    box=0

    bin_edges = [[-0.8, 0.8],
                 [-0.5, 0.5],
                 [-SIX_DEGREES, -ONE_DEGREE, 0, ONE_DEGREE, SIX_DEGREES],
                 [-FIFTY_DEGREES, FIFTY_DEGREES]]
    
    box = 0
    for s, edges in zip(observation, bin_edges):

        i = np.digitize([s], edges)[0]
        box = box * (len(edges)+1) + i
    
    vec = np.zeros(162)
    vec[box] = 1.
    
    return vec
In [22]:
import gym

N_TRIALS = 150
MAX_STEPS = 100000
TERMINATE_ON_MAX_STEPS = True
SEED = 12345


env = gym.make('CartPole-v0')
ase = ASE(n_input_dim=162, learning_rate=1000, decay_rate=0.9, random_state=SEED)
ace = ACE(n_input_dim=162, learning_rate=0.5, decay_rate=0.8, discount_factor=0.95, random_state=SEED)

for trial in range(1, N_TRIALS+1):
    obs = env.reset()
    reward = None
    done = False
    
    for t in range(1, MAX_STEPS+1):
        x = get_box(obs)
        revised_reward = ace.step(x, reward, done)
        #print reward, revised_reward
        action = ase.step(x, revised_reward)
        
        if done:
            break
        
        obs, _, done, _ = env.step(action)

        if done:
            reward = -1
        else:
            reward = 0
    
    if trial % 10 == 0 :
        print "Trial {}: survived {} steps".format(trial, t)
    if TERMINATE_ON_MAX_STEPS and t == MAX_STEPS:
        print "Terminated after {} trials. Successfully balanced pole for MAX_STEPS={}".format(trial, MAX_STEPS)
        break
INFO:gym.envs.registration:Making new env: CartPole-v0
[2016-08-14 18:48:41,473] Making new env: CartPole-v0
Trial 10: survived 23 steps
Trial 20: survived 60 steps
Trial 30: survived 268 steps
Trial 40: survived 815 steps
WARNING:gym.core:Observation '[ 2.40528917  0.38981101  0.01398455  0.26120958]' is not contained within observation space 'Box(4,)'.
[2016-08-14 18:48:44,502] Observation '[ 2.40528917  0.38981101  0.01398455  0.26120958]' is not contained within observation space 'Box(4,)'.
Trial 50: survived 354 steps
Trial 60: survived 1194 steps
Trial 70: survived 657 steps
Trial 80: survived 587 steps
Trial 90: survived 1967 steps
Terminated after 91 trials. Successfully balanced pole for MAX_STEPS=100000

This method succesfully solves Cart-Pole in under 100 iterations!

What if I didn't use any boxes? What if I just used the raw 4-element observation vector??

The formulas are all writen in terms of vectors and dot products so maybe I can skip the state-space, one-hot encoding and just pass the raw observation vector?

In [23]:
import gym

N_TRIALS = 1500
MAX_STEPS = 100000
TERMINATE_ON_MAX_STEPS = True
SEED = 12345


env = gym.make('CartPole-v0')
ase = ASE(n_input_dim=4, learning_rate=10, decay_rate=0.5, random_state=SEED)
ace = ACE(n_input_dim=4, learning_rate=0.01, decay_rate=0.5, discount_factor=0.05, random_state=SEED)

for trial in range(1,N_TRIALS+1):
    obs = env.reset()
    reward = None
    done = False
    
    for t in range(1, MAX_STEPS+1):
        x = obs
        revised_reward = ace.step(x, reward, done)
        #print reward, revised_reward
        action = ase.step(x, revised_reward)
        
        if done:
            break
        
        obs, _, done, _ = env.step(action)

        if done:
            reward = -1
        else:
            reward = 0
            
    if trial % 10 == 0 :
        print "Trial {}: survived {} steps".format(trial, t)
    if TERMINATE_ON_MAX_STEPS and t == MAX_STEPS:
        print "Terminated after {} trials. Successfully balanced pole for MAX_STEPS={}".format(trial, MAX_STEPS)
        break
INFO:gym.envs.registration:Making new env: CartPole-v0
[2016-08-14 18:49:36,056] Making new env: CartPole-v0
Trial 10: survived 64 steps
Trial 20: survived 47 steps
Trial 30: survived 11 steps
Trial 40: survived 32 steps
Trial 50: survived 23 steps
Trial 60: survived 53 steps
Trial 70: survived 33 steps
Trial 80: survived 74 steps
Trial 90: survived 25 steps
Trial 100: survived 25 steps
Trial 110: survived 86 steps
Trial 120: survived 25 steps
Trial 130: survived 31 steps
Trial 140: survived 17 steps
Trial 150: survived 61 steps
Trial 160: survived 54 steps
Trial 170: survived 22 steps
Trial 180: survived 81 steps
Trial 190: survived 17 steps
Trial 200: survived 42 steps
Trial 210: survived 32 steps
Trial 220: survived 13 steps
Trial 230: survived 50 steps
Trial 240: survived 23 steps
Trial 250: survived 18 steps
Trial 260: survived 55 steps
Trial 270: survived 11 steps
Trial 280: survived 49 steps
Trial 290: survived 18 steps
Trial 300: survived 67 steps
Trial 310: survived 43 steps
Trial 320: survived 31 steps
Trial 330: survived 52 steps
Trial 340: survived 47 steps
Trial 350: survived 33 steps
Trial 360: survived 34 steps
Trial 370: survived 30 steps
Trial 380: survived 29 steps
Trial 390: survived 107 steps
Trial 400: survived 11 steps
Trial 410: survived 63 steps
Trial 420: survived 58 steps
Trial 430: survived 84 steps
Trial 440: survived 42 steps
Trial 450: survived 39 steps
Trial 460: survived 32 steps
Trial 470: survived 50 steps
Trial 480: survived 59 steps
Trial 490: survived 33 steps
Trial 500: survived 11 steps
Trial 510: survived 37 steps
Trial 520: survived 11 steps
Trial 530: survived 21 steps
Trial 540: survived 22 steps
Trial 550: survived 87 steps
Trial 560: survived 67 steps
Trial 570: survived 67 steps
Trial 580: survived 44 steps
Trial 590: survived 64 steps
Trial 600: survived 26 steps
Trial 610: survived 11 steps
Trial 620: survived 15 steps
Trial 630: survived 48 steps
Trial 640: survived 39 steps
Trial 650: survived 52 steps
Trial 660: survived 31 steps
Trial 670: survived 72 steps
Trial 680: survived 30 steps
Trial 690: survived 10 steps
Trial 700: survived 49 steps
Trial 710: survived 29 steps
Trial 720: survived 40 steps
Trial 730: survived 44 steps
Trial 740: survived 42 steps
Trial 750: survived 36 steps
Trial 760: survived 39 steps
Trial 770: survived 31 steps
Trial 780: survived 28 steps
Trial 790: survived 43 steps
Trial 800: survived 50 steps
Trial 810: survived 44 steps
Trial 820: survived 75 steps
Trial 830: survived 29 steps
Trial 840: survived 45 steps
Trial 850: survived 31 steps
Trial 860: survived 64 steps
Trial 870: survived 11 steps
Trial 880: survived 29 steps
Trial 890: survived 25 steps
Trial 900: survived 31 steps
Trial 910: survived 31 steps
Trial 920: survived 23 steps
Trial 930: survived 42 steps
Trial 940: survived 33 steps
Trial 950: survived 41 steps
Trial 960: survived 25 steps
Trial 970: survived 45 steps
Trial 980: survived 27 steps
Trial 990: survived 45 steps
Trial 1000: survived 41 steps
Trial 1010: survived 42 steps
Trial 1020: survived 33 steps
Trial 1030: survived 52 steps
Trial 1040: survived 67 steps
Trial 1050: survived 26 steps
Trial 1060: survived 47 steps
Trial 1070: survived 40 steps
Trial 1080: survived 58 steps
Trial 1090: survived 31 steps
Trial 1100: survived 31 steps
Trial 1110: survived 31 steps
Trial 1120: survived 98 steps
Trial 1130: survived 30 steps
Trial 1140: survived 74 steps
Trial 1150: survived 59 steps
Trial 1160: survived 43 steps
Trial 1170: survived 38 steps
Trial 1180: survived 39 steps
Trial 1190: survived 51 steps
Trial 1200: survived 56 steps
Trial 1210: survived 50 steps
Trial 1220: survived 35 steps
Trial 1230: survived 43 steps
Trial 1240: survived 49 steps
Trial 1250: survived 50 steps
Trial 1260: survived 32 steps
Trial 1270: survived 27 steps
Trial 1280: survived 50 steps
Trial 1290: survived 28 steps
Trial 1300: survived 25 steps
Trial 1310: survived 50 steps
Trial 1320: survived 45 steps
Trial 1330: survived 26 steps
Trial 1340: survived 33 steps
Trial 1350: survived 41 steps
Trial 1360: survived 51 steps
Trial 1370: survived 11 steps
Trial 1380: survived 29 steps
Trial 1390: survived 35 steps
Trial 1400: survived 40 steps
Trial 1410: survived 34 steps
Trial 1420: survived 34 steps
Trial 1430: survived 41 steps
Trial 1440: survived 85 steps
Trial 1450: survived 45 steps
Trial 1460: survived 56 steps
Trial 1470: survived 40 steps
Trial 1480: survived 25 steps
Trial 1490: survived 20 steps
Trial 1500: survived 28 steps

Nope. It doesn't work. It seems this method is too strongly reliant on the input vector being a one-hot encoding of discrete states. This is not surprising since the paper was written as a successor to another method which had the name "Boxes".

What about a less sophisticated discretization of the space?

np.linspace

In [4]:
from itertools import product
import numpy as np

import gym

GRANULARITY = 8

env = gym.make('CartPole-v0')

# Split each dimension up into `GRANULARITY` number of buckets
# store these buckets in `linspaces`
bucket_edges = []
ranges = zip(env.observation_space.low, env.observation_space.high)
for low, high in ranges:
    if np.isneginf(low):
        low = -5
    if np.isinf(high):
        high = 5
    bucket_edges.append(np.linspace(low, high, num=GRANULARITY+1, endpoint=True)[1:-1])
INFO:gym.envs.registration:Making new env: CartPole-v0
[2016-08-14 18:35:24,993] Making new env: CartPole-v0
In [6]:
bucket_edges
Out[6]:
[array([-1.8, -1.2, -0.6,  0. ,  0.6,  1.2,  1.8]),
 array([-3.75, -2.5 , -1.25,  0.  ,  1.25,  2.5 ,  3.75]),
 array([-0.31415927, -0.20943951, -0.10471976,  0.        ,  0.10471976,
         0.20943951,  0.31415927]),
 array([-3.75, -2.5 , -1.25,  0.  ,  1.25,  2.5 ,  3.75])]
In [25]:
def get_box(observation):
    x, x_dot, theta, theta_dot = observation
    box=0

    bin_edges = bucket_edges
    
    box = 0
    for s, edges in zip(observation, bin_edges):

        i = np.digitize([s], edges)[0]
        box = box * (len(edges)+1) + i
    
    vec = np.zeros(GRANULARITY**observation.shape[0])
    vec[box] = 1.
    
    return vec
In [26]:
import gym

N_TRIALS = 1000
MAX_STEPS = 100000
TERMINATE_ON_MAX_STEPS = True
SEED = 12345


env = gym.make('CartPole-v0')
ase = ASE(n_input_dim=GRANULARITY**4, learning_rate=1000, decay_rate=0.9, random_state=SEED)
ace = ACE(n_input_dim=GRANULARITY**4, learning_rate=0.5, decay_rate=0.8, discount_factor=0.95, random_state=SEED)

for trial in range(1, N_TRIALS):
    obs = env.reset()
    reward = None
    done = False
    
    for t in range(1, MAX_STEPS+1):
        x = get_box(obs)
        revised_reward = ace.step(x, reward, done)
        #print reward, revised_reward
        action = ase.step(x, revised_reward)
        
        if done:
            break
        
        obs, _, done, _ = env.step(action)

        if done:
            reward = -1
        else:
            reward = 0
    
    if trial % 10 == 0 :
        print "Trial {}: survived {} steps".format(trial, t)
    if TERMINATE_ON_MAX_STEPS and t == MAX_STEPS:
        print "Terminated after {} trials. Successfully balanced pole for MAX_STEPS={}".format(trial, MAX_STEPS)
        break
INFO:gym.envs.registration:Making new env: CartPole-v0
[2016-08-14 18:50:37,159] Making new env: CartPole-v0
Trial 10: survived 10 steps
Trial 20: survived 72 steps
Trial 30: survived 133 steps
Trial 40: survived 129 steps
WARNING:gym.core:Observation '[ 2.4481542   2.44331325 -0.01609566 -0.74575892]' is not contained within observation space 'Box(4,)'.
[2016-08-14 18:50:37,834] Observation '[ 2.4481542   2.44331325 -0.01609566 -0.74575892]' is not contained within observation space 'Box(4,)'.
Trial 50: survived 116 steps
Trial 60: survived 285 steps
Trial 70: survived 11 steps
Trial 80: survived 165 steps
Trial 90: survived 219 steps
Trial 100: survived 191 steps
Trial 110: survived 254 steps
Trial 120: survived 169 steps
Trial 130: survived 196 steps
Trial 140: survived 102 steps
Trial 150: survived 211 steps
Trial 160: survived 152 steps
Trial 170: survived 129 steps
Trial 180: survived 174 steps
Trial 190: survived 299 steps
Trial 200: survived 130 steps
Trial 210: survived 165 steps
Trial 220: survived 184 steps
Trial 230: survived 184 steps
Trial 240: survived 242 steps
Trial 250: survived 419 steps
Trial 260: survived 151 steps
Trial 270: survived 166 steps
Trial 280: survived 372 steps
Trial 290: survived 319 steps
Trial 300: survived 155 steps
Trial 310: survived 165 steps
Trial 320: survived 234 steps
Trial 330: survived 362 steps
Trial 340: survived 143 steps
Trial 350: survived 409 steps
Trial 360: survived 773 steps
Trial 370: survived 405 steps
Trial 380: survived 386 steps
Trial 390: survived 320 steps
Trial 400: survived 393 steps
Trial 410: survived 124 steps
Trial 420: survived 128 steps
Trial 430: survived 359 steps
Trial 440: survived 155 steps
Trial 450: survived 979 steps
Trial 460: survived 229 steps
Trial 470: survived 370 steps
Trial 480: survived 361 steps
Trial 490: survived 1150 steps
Trial 500: survived 168 steps
Trial 510: survived 411 steps
Trial 520: survived 751 steps
Trial 530: survived 187 steps
Trial 540: survived 5854 steps
Terminated after 541 trials. Successfully balanced pole for MAX_STEPS=100000

Their method works with this very rough discretization but (unsurprisingly) it takes much longer to learn.

No comments :

Post a Comment