Reinforcement Learning - Q-tables

Q-Learning.

By Matthew Millar R&D Scientist at ユニファ

Q -learning is a model-free approach to reinforcement learning. This means that Q-learning does not rely on a model to make decisions based on an input, but instead uses a policy that shows the agent what actions to perform given a certain circumstance. Q-learning should find the best policy which gives an optimal solution to any finite Markov decision process. The FMDP can be described as:

Where X is the state space, A is the action space, and r is the reward function. The optimal solution is obtained by maximizing the total reward for any action and any change in state, starting with the current state. The best or optimal solution can be found for any finite problem given an unlimited exploration time combined with an exploration adaption to a policy. This optimal Q-function can be calculated as such:

The full equation and explanation can be found in Melo’s Convergence of Q-Learning: A Simple Proof [1]:
Influences on how the agent will learn using Q-learning comes down to three real factors. Learning rate controls how much, or the rate in which new information replaces older information. If the learning rate is set to 0, then the agent will never learn anything new and will rely on previous knowledge to choose its actions. If the rate is set to 1, then the agent will only look at the most recent data and completely discard previous data.

The Discount factor is another variable that controls learning. The discount factor determines the weight of future rewards and how important or not these rewards will be. If it is set to 0, then the model will only consider instant rewards or the current reward and not look towards potential rewards of future actions. If the discount factor is set to 1, then the agent will work towards achieving the long-term highest reward.

The third and final variable used for learning the control is the initial conditions or Q0. Q-learning is an iterative program, it must have a starting condition before any changes take place to any state or environment. To help an agent explore options better, high initial values should be used regardless of which action is used. The update rules can control the values compared to other alternatives which can increase its total probability of being chosen. This will allow for each option to be fully explored and to aid in finding an optimal solution to each problem.

Q-Tables:

Q-Learning runs off a table or matrix of state and actions. This Q-table starts out all values as 0 and after each episode, these values are updated accordingly. The matrix should take the size of both the state as well as the action size with a very basic form of this is a simple array [state, action]. The simplest method for creating a Q-table in python is this:

```import numpy as np
# Initial Q-table
Q_table = np.zeros((state_size, action_size))
```

This Q-table will store every action and state that the agent will be in. Exploitation is done when an action that has been performed before and in a certain state is chosen which results in a reward (most likely the max possible future reward). Exploration is choosing a random action at any point in time and in any state. A basic method for this could be defined as below:

```import random
epli = 0.4
def choose_action (epli)::
ran_float = random.random() # Generates a random float between [0,1]
if ran_float < epli:
#preform a random action
else:
get_action_from_qtable(current_state, q_table)
```

After choosing an action to perform and receiving a reward, the Q-table must be updated. The updating must be done after each action is performed. The updating phase will end at the conclusion of each episode. The basic flow of the update function will be:

Step 1: Get the current state of the agent.
Step 2: The agent takes action. (Picks random action or Pick action from Q-table)
Step 3: Update q-value.
An update rule for Q-values can be described as:

```Q[state, action] = Q[state, action] + learning_rate * (reward + gamma * np.max(Q[new_state, :])) – Q[state, action]
```

This is a basic approach to adjust and update q-values in a Q-table.

Now for a simple example:
First lest look at the agent in the game:

```class Agent:
def __init__(self, learning_rate = 0.1,
discount = 0.95,
exploration_rate = 1.0,
iterations = 1000):
# Q talbe for holding rewards
self.q_table = [[0,0,0,0,0], [0,0,0,0,0]]
self.learning_rate = learning_rate
self.discount = discount # Future rewards value to current rewards
self.exploration_rate = exploration_rate # Exploration rate
# This controls sift from exploration to exploation
self.exploration_delta = 1.0 / iterations

# Chose between explotation or exploration
def get_next_action(self, state):
if random.random() > self.exploration_rate:
return self.greedy_action(state)
else:
return self.random_action()

def greedy_action(self, state):
# Check to see if forward is best reward
if self.q_table[FORWARD][state] > self.q_table[BACKWARD][state]:
return FORWARD
elif self.q_table[BACKWARD][state] > self.q_table[FORWARD][state]:
return BACKWARD
# Rewards are equal, take random action
return FORWARD if random.random() < 0.5 else BACKWARD

def random_action(self):
return FORWARD if random.random() < 0.5 else BACKWARD

def update(self, old_state, new_state, action, reward):
old_value = self.q_table[action][old_state]
# best next action
future_action = self.greedy_action(new_state)
# reward for the best next action
future_reward = self.q_table[future_action][new_state]

# Main Q-table updating algorithm
new_value = old_value + self.learning_rate * (reward + self.discount * future_reward - old_value)
self.q_table[action][old_state] = new_value

# Finally shift our exploration_rate toward zero (less gambling)
if self.exploration_rate > 0:
self.exploration_rate -= self.exploration_delta
```

Then lets built the environment that he can walk through.

```# Build the Environment
class EnvironmentSimulator:
def __init__(self, length=5, small=2, large=10):
self.length = length # Length of the environment
self.small = small  # reward for going back to the start
self.large = large  # reward for reaching the end
self.state = 0  # environment entry point

def take_action(self, action):
if action == BACKWARD:
reward = self.small
self.state = 0
elif action == FORWARD:
if self.state < self.length - 1:
self.state += 1
reward = 0
else:
reward = self.large
return self.state, reward
# Reset the environment
def reset(self):
self.state = 0
return self.state
```

This is a very simple array of 5 spaces. So basically, the agent can walk back a forth and get a reward for either reaching one end or the other.
Next, we will set up the environment and the main loop for running the simulation.

```# Set up environment
environment = EnvironmentSimulator()
environment.reset()
# Scores
total_reward = 0
last_total = 0

for step in range(iterations):
# Store the current state in old state
old_state = environment.state
action = agent.get_next_action(old_state)
new_state, reward = environment.take_action(action)
agent.update(old_state, new_state, action, reward)

total_reward += reward
if step % 250 ==0:
performance = (total_reward - last_total) / 250.0
print("Step:{} Performance:{} Total-reward:{}".format(step, performance, total_reward))
```

RI Hello World Revisited

Let's take a look back at the Cart Pole example from the previous blog,
tech.unifa-e.com
We did that with a Keras model, now we will do it with a q-table approach.

First we need to set up the Q-learning class

```class QLearn:
def __init__(self, actions, epsilon, alpha, gamma):
self.q = {}
self.epsilon = epsilon
self.alpha = alpha
self.gamma = gamma
self.actions = actions
```

Next, we need a few methods for choosing actions and updating the tables:

```    # Q learning function
def learnQ(self, state, action, reward, value):
old_value = self.q.get((state, action), None)
# If there are no values in the table add the reward
if old_value is None:
self.q[(state, action)] = reward
else:
self.q[(state, action)] = old_value + self.alpha * (value - old_value)

def chooseAction(self, state):
q = [self.getQ(state, a) for a in self.actions]
maxQ = max(q)

if random.random() < self.epsilon:
minQ = min(q); mag = max(abs(minQ), abs(maxQ))
q = [q[i] + random.random() * mag - .5 * mag for i in range(len(self.actions))]
maxQ = max(q)

count = q.count(maxQ)
if count > 1:
best = [i for i in range(len(self.actions)) if q[i] == maxQ]
i = random.choice(best)
else:
i = q.index(maxQ)
action = self.actions[i]
return action, q

def learn(self, s1, action, reward, s2):
maxqnew = max([self.getQ(s2, a) for a in self.actions])
self.learnQ(s1, action, reward, reward + self.gamma*maxqnew)
```

And now let's define the main method:

```if __name__ == '__main__':
# Load the cart pole game from gym ai
env = gym.make('CartPole-v0')
# Vriables needed
max_number_of_steps = 200
last_time_steps = numpy.ndarray(0)
n_bins = 8
n_bins_angle = 10
max_number_episodes = 200
episode_number = 0
render = False

number_of_features = env.observation_space.shape[0]
last_time_steps = numpy.ndarray(0)

# Simplfy the number of states as it is too large
cart_position = pandas.cut([-2.4, 2.4], bins=n_bins, retbins=True)[1][1:-1]
pole_angle = pandas.cut([-2, 2], bins=n_bins_angle, retbins=True)[1][1:-1]
cart_velocity = pandas.cut([-1, 1], bins=n_bins, retbins=True)[1][1:-1]
angle_rate = pandas.cut([-3.5, 3.5], bins=n_bins_angle, retbins=True)[1][1:-1]

qlearn = QLearn(actions=range(env.action_space.n),
alpha=0.5, gamma=0.90, epsilon=0.1)

while episode_number < max_number_episodes:
observation = env.reset()

cart_position, pole_angle, cart_velocity, angle_rate_of_change = observation
state = build_state([to_bin(cart_position, cart_position),
to_bin(pole_angle, pole_angle),
to_bin(cart_velocity, cart_velocity),
to_bin(angle_rate_of_change, angle_rate)])

for t in range(0, max_number_of_steps):
if render:
env.render()

action = qlearn.chooseAction(state)
observation, reward, done, info = env.step(action)

cart_position, pole_angle, cart_velocity, angle_rate_of_change = observation
nextState = build_state([to_bin(cart_position, cart_position),
to_bin(pole_angle, pole_angle),
to_bin(cart_velocity, cart_velocity),
to_bin(angle_rate_of_change, angle_rate)])

if not(done):
qlearn.learn(state, action, reward, nextState)
state = nextState
else:
# Penalize it for failing
reward = -200
qlearn.learn(state, action, reward, nextState)
last_time_steps = numpy.append(last_time_steps, [int(t + 1)])
break
```

And that is really the only difference between the Keras and q-learning ways of solving the Cart Pole problem.
Instead of the model deciding what to do, the values in the q-table are making the decision.

Conclusion

This shows how to simply set up an environment and use a q-table to learn RI. This was a simple example the next logical step would be to have a negative reward which would encourage the AI to stop moving backward and to only move forward. This could help shape how the AI will learn and help move it towards the desired actions. This program is really a bare-bones approach with no bells and whistles. It shows you what is the minimum to have for an agent, environment, and simulation for training up a RI agent.
We also revisited the cart pole example to see how Q-Learning can be done compared to using a Keras model. The Keras model needs less code and can solve the issue a little better than the Q-learning approach. This may be because I had to simplify the states that the pole can take which may lead to the poorer results.
Q-learning has its limits as the table can become very complex as the states and actions that are a possible increase in the total number. This can greatly increase the complexity of the Q-table which can become very difficult to manage. By simplifying the dimensions of the state/action sets can help make it more manageable, but you sacrifice granularity which can lead to mistakes in the results.

Resources:

[1]: Melo, F. Convergence of Q-learning: a simple proof. Retrieved 2019 from