DEV Community

Cover image for Reinforcement Learning with TF2 and Gym: Actor-Critic
Z. QIU
Z. QIU

Posted on • Updated on

Reinforcement Learning with TF2 and Gym: Actor-Critic

Today I started to learn a new RL method: Actor-Critic. The basic idea is quite similar to GAN network. Unlike Policy Gradient in which a single neural network is trained, Actor-Critic uses two neural networks: an "actor" for performing actions and a "critic" for evaluating action of the actor. I refer to this page for details of this method.

Model presentation

Given a state s_t at time t, the network calculates the action probabilities and chooses an action a_t to apply on env:
Alt Text

After the application of action a_t, then env updated accordingly, yields reward r_t, we observe at next time step t+1 a new state s_t+1 which results in a new sampled action a_t+1:
Alt Text

Steps of the basic Actor-Critic method

Unlike the Policy Gradient which needs to gather all data of an episode to calculate gradients, Actor-Critic method performs model update in every agent-env interaction loop :

Alt Text

Implementation with CartPole game

I followed this youtube video for my first Actor-Critic exercise. It was initially based on Keras thus I have modified a little bit of his code for using tensorflow. I also disabled eager mode because of a runtime error (I am using Tensorflow v2 on my laptop).

import tensorflow as tf
tf.compat.v1.disable_eager_execution()

from tensorflow.keras import backend as K
from tensorflow.keras.layers import Activation, Dense, Input

from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

import numpy as np
import gym

class Agent(object):
    def __init__(self, alpha, beta, gamma=0.99, n_actions=2, \
                layer1_size=1024, layer2_size=512, input_dims=4):
        self.gamma = gamma
        self.alpha = alpha
        self.beta = beta
        self.input_dims = input_dims
        self.fc1_dims = layer1_size
        self.fc2_dims = layer2_size
        self.n_actions = n_actions

        self.actor, self.critic, self.policy = self.build_network()

        self.action_space = [i for i in range(self.n_actions)]


    def build_network(self):
        input = Input(shape=(self.input_dims, ))
        delta = Input(shape=[1])
        dense1 = Dense(self.fc1_dims, activation="relu")(input)
        dense2 = Dense(self.fc2_dims, activation="relu")(dense1)
        probs = Dense(self.n_actions, activation="softmax")(dense2)
        values = Dense(1, activation="linear")(dense2)

        def custom_loss(y_true, y_pred):
            out = K.clip(y_pred, 1e-8, 1-1e-8)  
            log_lik = y_true * K.log(out)            
            return K.sum(-log_lik*delta)

        actor = Model(inputs=[input, delta], outputs=[probs])
        actor.compile(optimizer=Adam(lr=self.alpha), loss=custom_loss)

        critic = Model(inputs=[input], outputs=[values])
        critic.compile(optimizer=Adam(lr=self.beta), loss="mse")

        policy = Model(inputs=[input], outputs=[probs])

        return actor, critic, policy

    def choose_action(self, observation):

        state = observation[np.newaxis, :]
        probs = self.policy.predict(state)[0]

        action = np.random.choice(self.action_space, p=probs)

        return action

    def learn(self, state, action, reward, state_, done):
        state = state[np.newaxis, :]
        state_ = state_[np.newaxis, :]
        critic_value_ = self.critic.predict(state_)
        critic_value = self.critic.predict(state)

        target = reward + self.gamma*critic_value_*(1-int(done))
        delta = target - critic_value

        actions = np.zeros([1, self.n_actions])
        actions[np.arange(1), action] = 1.0

        self.actor.fit([state, delta], actions, verbose=0)
        self.critic.fit(state, target, verbose=0)


if __name__ == "__main__":
    agent=Agent(alpha=0.0001, beta=0.0005)
    env = gym.make("CartPole-v0")
    score_history = []
    num_episodes = 2000

    for i in range(num_episodes):
        done = False

        score = 0
        observation = env.reset()

        while not done:
            env.render()
            action = agent.choose_action(observation)
            observation_, reward, done, info= env.step(action)
            agent.learn(observation, action, reward, observation_, done)
            observation = observation_
            score+=reward

        score_history.append(score)
        avg_score = np.mean(score_history[-100:])
        print("episode ", i, "score %.2f average score %.2f" % (score, avg_score))
Enter fullscreen mode Exit fullscreen mode

A newer version

I have updated the program shown above to allow eager mode. The main difference is the introduction of the custom loss function for actor. It works well, however, the running speed is much slower after my modification (I am looking forward to Refactoring it later).


import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = "2"


import tensorflow as tf

if tf.__version__.startswith("1."):    
    raise RuntimeError("Error!! You are using tensorflow-v1")

from tensorflow.keras import backend as K
from tensorflow.keras.layers import Activation, Dense, Input

from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

import numpy as np
import gym

class Agent(object):
    def __init__(self, alpha, beta, gamma=0.99, n_actions=2, \
                layer1_size=1024, layer2_size=512, input_dims=4):
        self.gamma = gamma
        self.alpha = alpha
        self.beta = beta
        self.input_dims = input_dims
        self.fc1_dims = layer1_size
        self.fc2_dims = layer2_size
        self.n_actions = n_actions

        self.actor, self.critic, self.policy = self.build_network()

        self.action_space = [i for i in range(self.n_actions)]

        self.optimizer = tf.keras.optimizers.Adam(learning_rate=self.alpha)

    def build_network(self):

        input = Input(shape=(self.input_dims, ))
        delta = Input(shape=[1])
        dense1 = Dense(self.fc1_dims, activation="relu")(input)
        dense2 = Dense(self.fc2_dims, activation="relu")(dense1)
        probs = Dense(self.n_actions, activation="softmax")(dense2)
        values = Dense(1, activation="linear")(dense2)

        actor = Model(inputs=[input, delta], outputs=[probs])

        critic = Model(inputs=[input], outputs=[values])
        critic.compile(optimizer=Adam(lr=self.beta), loss="mse")

        policy = Model(inputs=[input], outputs=[probs])

        return actor, critic, policy

    def choose_action(self, observation):

        state = observation[np.newaxis, :]
        probs = self.policy.predict(state)[0]

        action = np.random.choice(self.action_space, p=probs)

        return action

    def learn(self, state, action, reward, state_, done):
        state = state[np.newaxis, :]
        state_ = state_[np.newaxis, :]
        critic_value_ = self.critic.predict(state_)
        critic_value = self.critic.predict(state)

        target = reward + self.gamma*critic_value_*(1-int(done))
        delta = target - critic_value

        actions = np.zeros([1, self.n_actions])
        actions[np.arange(1), action] = 1.0

        self.critic.fit(state, target, verbose=0)

        with tf.GradientTape() as tape:
            y_pred = self.actor(state)
            out = K.clip(y_pred, 1e-8, 1-1e-8)  
            log_lik = actions * K.log(out)            
            myloss = K.sum(-log_lik*delta)
        grads = tape.gradient(myloss, self.actor.trainable_variables)

        self.optimizer.apply_gradients(zip(grads, self.actor.trainable_variables))

if __name__ == "__main__":
    agent=Agent(alpha=0.0001, beta=0.0005)

    ENV_SEED = 1024  ## Reproducibility of the game
    NP_SEED = 1024  ## Reproducibility of numpy random
    env = gym.make('CartPole-v0')
    env = env.unwrapped    # use unwrapped version, otherwise episodes will terminate after 200 steps
    env.seed(ENV_SEED)  
    np.random.seed(NP_SEED)


    ### The Discrete space allows a fixed range of non-negative numbers, so in this case valid actions are either 0 or 1. 
    print(env.action_space)
    ### The Box space represents an n-dimensional box, so valid observations will be an array of 4 numbers. 
    print(env.observation_space)
    ### We can also check the Box’s bounds:
    print(env.observation_space.high)
    print(env.observation_space.low)

    score_history = []
    num_episodes = 2000

    for i in range(num_episodes):
        done = False

        score = 0
        observation = env.reset()

        while not done:
            env.render()
            action = agent.choose_action(observation)
            observation_, reward, done, info= env.step(action)
            agent.learn(observation, action, reward, observation_, done)
            observation = observation_
            score+=reward

        score_history.append(score)
        avg_score = np.mean(score_history[-100:])
        print("episode ", i, "score %.2f average score %.2f" % (score, avg_score))
Enter fullscreen mode Exit fullscreen mode

The training converges ideally. Below is a snapshot of the execution output:

Alt Text

References:

*https://github.com/wangshusen/DRL
*https://www.youtube.com/watch?v=2vJtbAha3To

Top comments (1)

Collapse
 
jana170897 profile image
Jana170897

Hi! I have the same implementation however it takes forever to converge. It does converge but it takes more than 24 hours often. Do you have the same? I have the same code als your first example here :) .

Thanks in advance!