*** This tutorial is two years old and may no longer work properly. You can find an updated tutorial for object recognition at this link***
In this tutorial, we will develop a program that can recognize objects in a real-time video stream on a built-in laptop webcam using deep learning.
Object recognition involves two main
tasks:
Object Detection (Where are the objects?): Locate objects in a photo or video frame
Image Classification (What are the objects?): Predict the type of each object in a photo or video frame
Humans can do both tasks effortlessly, but computers cannot.
Computers require a lot of processing power to take full advantage of the state-of-the-art algorithms that enable object recognition in real time. However, in recent years, the technology has matured, and real-time object recognition is now possible with only a laptop computer and a webcam.
Real-time object recognition systems
are currently being used in a number of real-world applications, including the
following:
Self-driving cars: detection of pedestrians, cars, traffic lights, bicycles, motorcycles, trees, sidewalks, etc.
Sports: ball tracking in baseball, golf, and football.
Agriculture: disease detection in fruits.
Food: food identification.
There are a lot of steps in this tutorial. Have fun, be patient, and be persistent. Don’t give up! If something doesn’t work the first time around, try again. You will learn a lot more by fighting through to the end of this project. Stay relentless!
By the end of this tutorial, you will have the rock-solid confidence to detect and recognize objects in real time on your laptop’s GPU (Graphics Processing Unit) using deep learning.
We need to get all the required software set up on our computer. I will be following this really helpful tutorial.
Open an Anaconda command prompt terminal.
Type the command below to create a virtual environment named tensorflow_cpu that has Python 3.6 installed.
conda create -n tensorflow_cpu pip python=3.6
Press y and then ENTER.
A virtual environment is like an independent Python workspace which has its own set of libraries and Python version installed. For example, you might have a project that needs to run using an older version of Python, like Python 2.7. You might have another project that requires Python 3.7. You can create separate virtual environments for these projects.
Now, let’s activate the virtual environment by using this command:
conda activate tensorflow_cpu
Type the following command to install TensorFlow CPU.
You should see a message that says: “Your CPU supports instructions that this TensorFlow binary….”. Just ignore that. Your TensorFlow will still run fine.
Now run this command to complete the test of the installation:
print(sess.run(hello))
Press CTRL+Z. Then press ENTER to exit.
Type:
exit
That’s it for TensorFlow CPU. Now let’s install TensorFlow GPU.
Here is a good tutorial that walks through the installation, but I’ll outline all the steps below.
Install CUDA Toolkit v9.0
The first thing we need to do is to install the CUDA Toolkit v9.0. Go to this link.
Select your operating system. In my case, I will select Windows, x86_64, Version 10, and exe (local).
Download the Base Installer as well as all the patches. I downloaded all these files to my Desktop. It will take a while to download, so just wait while your computer downloads everything.
Open the folder where the downloads were saved to.
Double-click on the Base Installer program, the largest of the files that you downloaded from the website.
Click Yes to allow the program to make changes to your device.
Click OK to extract the files to your computer.
I saw this error window. Just click Continue.
Click Agree and Continue.
If you saw that error window earlier… “…you may not be able to run CUDA applications with this driver…,” select the Custom (Advanced) install option and click Next. Otherwise, do the Express installation and follow all the prompts.
Uncheck the Driver components, PhysX, and Visual Studio Integration options. Then click Next.
Install the NVIDIA CUDA Deep Neural Network library (cuDNN)
Now that we installed the CUDA 9.0 base installer and its four patches, we need to install the NVIDIA CUDA Deep Neural Network library (cuDNN). Official instructions for installing are on this page, but I’ll walk you through the process below.
Agree to the terms of the cuDNN Software License Agreement.
We have CUDA 9.0, so we need to click cuDNN v7.6.4 (September 27, 2019), for CUDA 9.0.
I have Windows 10, so I will download cuDNN Library for Windows 10.
In my case, the zip file downloaded to my Desktop. I will unzip that zip file now, which will create a new folder of the same name…just without the .zip part. These are your cuDNN files. We’ll come back to these in a second.
Before we get going, let’s double check what GPU we have. If you are on a Windows machine, search for the “Device Manager.”
Once you have the Device Manager open, you should see an option near the top for “Display Adapters.” Click the drop-down arrow next to that, and you should see the name of your GPU. Mine is NVIDIA GeForce GTX 1060.
If you are on Windows, you can also check what NVIDIA graphics driver you have by right-clicking on your Desktop and clicking the NVIDIA Control Panel. My version is 430.86. This version fits the requirements for cuDNN.
Ok, now that we have verified that our system meets the requirements, lets navigate to C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0, your CUDA Toolkit directory.
Now go to your cuDNN files, that new folder that was created when you did the unzipping. Inside that folder, you should see a folder named cuda. Click on it.
Click bin.
Copy cudnn64_7.dll to C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\bin. Your computer might ask you to allow Administrative Privileges. Just click Continue when you see that prompt.
Now go back to your cuDNN files. Inside the cuda folder, click on include. You should see a file named cudnn.h.
Copy that file to C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\include. Your computer might ask you to allow Administrative Privileges. Just click Continue when you see that prompt.
Now go back to your cuDNN files. Inside the cuda folder, click on lib -> x64. You should see a file named cudnn.lib.
Copy that file to C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\lib\x64. Your computer might ask you to allow Administrative Privileges. Just click Continue when you see that prompt.
If you are using Windows, do a search on your computer for Environment Variables. An option should pop up to allow you to edit the Environment Variables on your computer.
Click on Environment Variables.
Make sure you CUDA_PATH variable is set to C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0.
Now let’s test the installation. Launch the Python interpreter.
python
Type this command.
import tensorflow as tf
If you don’t see an error, TensorFlow GPU is successfully installed.
Now type this:
hello = tf.constant('Hello, TensorFlow!')
And run this command. It might take a few minutes to run, so just wait until it finishes:
sess = tf.Session()
Now type this command to complete the test of the installation:
print(sess.run(hello))
You can further confirm whether TensorFlow can access the GPU, by typing the following into the Python interpreter (just copy and paste into the terminal window while the Python interpreter is running).
Now that we have everything setup, let’s install some useful libraries. I will show you the steps for doing this in my TensorFlow GPU virtual environment, but the steps are the same for the TensorFlow CPU virtual environment.
Open a new Anaconda terminal window. Let’s take a look at the list of virtual environments that we can activate.
conda env list
I’m going to activate the TensorFlow GPU virtual environment.
Once that is finished, you need to create a folder somewhere that has the TensorFlow Models (e.g. C:\Users\addis\Documents\TensorFlow). If you have a D drive, you can also save it there as well.
In your Anaconda terminal window, move to the TensorFlow directory you just created. You will use the cd command to change to that directory. For example:
Download the latest *-win32.zip release (assuming you are on a Windows machine).
Create a folder in C:\Program Files named it Google Protobuf.
Extract the contents of the downloaded *-win32.zip, inside C:\Program Files\Google Protobuf
Search for Environment Variables on your system. A window should pop up that says System Properties.
Click Environment Variables.
Go down to the Path variable and click Edit.
Click New.
Add C:\Program Files\Google Protobuf\bin
You can also add it the Path System variable.
Click OK a few times to close out all the windows.
Open a new Anaconda terminal window.
I’m going to activate the TensorFlow GPU virtual environment.
conda activate tensorflow_gpu
cd into your \TensorFlow\models\research\ directory and run the following command:
for /f %i in ('dir /b object_detection\protos\*.proto') do protoc object_detection\protos\%i --python_out=.
Now go back to the Environment Variables on your system. Create a New Environment Variable named PYTHONPATH (if you don’t have one already). Replace C:\Python27amd64 if you don’t have Python installed there. Also, replace <your_path> with the path to your TensorFlow folder.
Note: This section gets really technical. If you know the basics of computer vision and deep learning, it will make sense. Otherwise, it will not. You can skip this section and head straight to the Implementation section if you are not interested in what is going on under the hood of the object recognition application we are developing.
In this project, we use OpenCV and TensorFlow to create a system capable of automatically recognizing objects in a webcam. Each detected object is outlined with a bounding box labeled with the predicted object type as well as a detection score.
The detection score is the probability that a bounding box contains the object of a particular type (e.g. the confidence a model has that an object identified as a “backpack” is actually a backpack).
The particular SSD with Inception v2
model used in this project is the ssd_inception_v2_coco model. The ssd_inception_v2_coco model uses
the Single Shot MultiBox Detector (SSD) for its architecture and the Inception
v2 framework for feature extraction.
Single Shot MultiBox Detector (SSD)
Most state-of-the-art object detection methods involve the
following stages:
Hypothesize
bounding boxes
Resample
pixels or features for each box
Apply
a classifier
The Single Shot MultiBox Detector (SSD) eliminates the multi-stage process above and performs all object detection computations using just a single deep neural network.
Inception v2
Most state-of-the-art object detection
methods based on convolutional neural networks at the time of the invention of
Inception v2 added increasingly more convolution layers or neurons per layer in
order to achieve greater accuracy. The problem with this approach is that it is
computationally expensive and prone to overfitting. The Inception v2
architecture (as well as the Inception v3 architecture) was proposed in order
to address these shortcomings.
Rather than stacking multiple kernel
filter sizes sequentially within a convolutional neural network, the approach
of the inception-based model is to perform a convolution on an input with
multiple kernels all operating at the same layer of the network. By factorizing
convolutions and using aggressive regularization, the authors were able to
improve computational efficiency. Inception v2 factorizes the traditional 7 x 7
convolution into 3 x 3 convolutions.
Szegedy, Vanhoucke, Ioffe, Shlens, & Wojna, (2015) conducted an empirically-based demonstration in their landmark Inception v2 paper, which showed that factorizing convolutions and using aggressive dimensionality reduction can substantially lower computational cost while maintaining accuracy.
Data Set
The ssd_inception_v2_coco model used in this project is pretrained on the Common Objects in Context (COCO) data set (COCO data set), a large-scale data set that contains 1.5 million object instances and more than 200,000 labeled images. The COCO data required 70,000 crowd worker hours to gather, annotate, and organize images of objects in natural environments.
Software Dependencies
The
following libraries form the object recognition backbone of the application
implemented in this project:
OpenCV, a library of programming functions for computer vision.
Pillow, a library for manipulating images.
Numpy, a library for scientific computing.
Matplotlib, a library for creating graphs and visualizations.
TensorFlow Object Detection API, an open source framework developed by Google that enables the development, training, and deployment of pre-trained object detection models.
Now to the fun part, we will now recognize objects using our computer webcam.
Copy the following program, and save it to your TensorFlow\models\research\object_detection directory as object_detection_test.py .
# Import all the key libraries
import numpy as np
import os
import six.moves.urllib as urllib
import sys
import tarfile
import tensorflow as tf
import zipfile
import cv2
from collections import defaultdict
from io import StringIO
from matplotlib import pyplot as plt
from PIL import Image
from utils import label_map_util
from utils import visualization_utils as vis_util
# Define the video stream
cap = cv2.VideoCapture(0)
# Which model are we downloading?
# The models are listed here: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
MODEL_NAME = 'ssd_inception_v2_coco_2018_01_28'
MODEL_FILE = MODEL_NAME + '.tar.gz'
DOWNLOAD_BASE = 'http://download.tensorflow.org/models/object_detection/'
# Path to the frozen detection graph.
# This is the actual model that is used for the object detection.
PATH_TO_CKPT = MODEL_NAME + '/frozen_inference_graph.pb'
# List of the strings that is used to add the correct label for each box.
PATH_TO_LABELS = os.path.join('data', 'mscoco_label_map.pbtxt')
# Number of classes to detect
NUM_CLASSES = 90
# Download Model
opener = urllib.request.URLopener()
opener.retrieve(DOWNLOAD_BASE + MODEL_FILE, MODEL_FILE)
tar_file = tarfile.open(MODEL_FILE)
for file in tar_file.getmembers():
file_name = os.path.basename(file.name)
if 'frozen_inference_graph.pb' in file_name:
tar_file.extract(file, os.getcwd())
# Load a (frozen) Tensorflow model into memory.
detection_graph = tf.Graph()
with detection_graph.as_default():
od_graph_def = tf.GraphDef()
with tf.gfile.GFile(PATH_TO_CKPT, 'rb') as fid:
serialized_graph = fid.read()
od_graph_def.ParseFromString(serialized_graph)
tf.import_graph_def(od_graph_def, name='')
# Loading label map
# Label maps map indices to category names, so that when our convolution network
# predicts `5`, we know that this corresponds to `airplane`. Here we use internal
# utility functions, but anything that returns a dictionary mapping integers to
# appropriate string labels would be fine
label_map = label_map_util.load_labelmap(PATH_TO_LABELS)
categories = label_map_util.convert_label_map_to_categories(
label_map, max_num_classes=NUM_CLASSES, use_display_name=True)
category_index = label_map_util.create_category_index(categories)
# Helper code
def load_image_into_numpy_array(image):
(im_width, im_height) = image.size
return np.array(image.getdata()).reshape(
(im_height, im_width, 3)).astype(np.uint8)
# Detection
with detection_graph.as_default():
with tf.Session(graph=detection_graph) as sess:
while True:
# Read frame from camera
ret, image_np = cap.read()
# Expand dimensions since the model expects images to have shape: [1, None, None, 3]
image_np_expanded = np.expand_dims(image_np, axis=0)
# Extract image tensor
image_tensor = detection_graph.get_tensor_by_name('image_tensor:0')
# Extract detection boxes
boxes = detection_graph.get_tensor_by_name('detection_boxes:0')
# Extract detection scores
scores = detection_graph.get_tensor_by_name('detection_scores:0')
# Extract detection classes
classes = detection_graph.get_tensor_by_name('detection_classes:0')
# Extract number of detectionsd
num_detections = detection_graph.get_tensor_by_name(
'num_detections:0')
# Actual detection.
(boxes, scores, classes, num_detections) = sess.run(
[boxes, scores, classes, num_detections],
feed_dict={image_tensor: image_np_expanded})
# Visualization of the results of a detection.
vis_util.visualize_boxes_and_labels_on_image_array(
image_np,
np.squeeze(boxes),
np.squeeze(classes).astype(np.int32),
np.squeeze(scores),
category_index,
use_normalized_coordinates=True,
line_thickness=8)
# Display output
cv2.imshow('object detection', cv2.resize(image_np, (800, 600)))
if cv2.waitKey(25) & 0xFF == ord('q'):
cv2.destroyAllWindows()
break
print("We are finished! That was fun!")
Open a new terminal window.
Activate the TensorFlow GPU virtual environment.
conda activate tensorflow_gpu
cd into your TensorFlow\models\research\object_detection directory.
At the time of this writing, we need to use Numpy version 1.16.4. Type the following command to see what version of Numpy you have on your system.
pip show numpy
If it is not 1.16.4, execute the following commands:
pip uninstall numpy
pip install numpy==1.16.4
Now run, your program:
python object_detection_test.py
In about 30 to 90 seconds, you should see your webcam power up and object recognition take action. That’s it! Congratulations for making it to the end of this tutorial!
In this post, I will walk you through how to implement the value iteration and Q-learning reinforcement learning algorithms from scratch, step-by-step. We will not use any fancy machine learning libraries, only basic Python libraries like Pandas and Numpy.
Our end goal is to implement and compare the performance of the value iteration and Q-learning reinforcement learning algorithms on the racetrack problem (Barto, Bradtke, & Singh, 1995).
In the racetrack problem, the goal is to control the movement of a race car along a predefined racetrack so that the race car travels from the starting position to the finish line in a minimum amount of time. The race car must stay on the racetrack and avoid crashing into walls along the way. The racetrack problem is analogous to a time trial rather than a competitive race since there are no other cars on the track to avoid.
Note: Before you deep dive into a section below, I recommend you check out my introduction to reinforcement learning post so you can familiarize yourself with what reinforcement learning is and how it works. Once you skim over that blog post, the rest of this article will make a lot more sense. If you are already familiar with how reinforcement learning works, no need to read that post. Just keep reading this one.
The two reinforcement learning
algorithms implemented in this project were value iteration and Q-learning.
Both algorithms were tested on two different racetracks: an R-shaped racetrack
and an L-shaped racetrack. The number of timesteps the race car needed to take
from the starting position to the finish line was calculated for each
algorithm-racetrack combination.
Using the implementations of
value iteration and Q-learning, three hypotheses will be tested.
Hypothesis 1: Both Algorithms Will Enable the Car to Finish the Race
I hypothesize that value
iteration and Q-learning will both generate policies that will enable the race
car to successfully finish the race on all racetracks tested (i.e. move from
the starting position of the racetracks to the finish line).
Hypothesis 2: Value Iteration Will Learn Faster Than Q-Learning
I hypothesize that value
iteration will generate a learning policy faster than Q-learning because it has
access to the transition and reward functions (explained in detail in the next
section “Algorithms Implemented”).
Hypothesis 3: Bad Crash Version 1 Will Outperform Bad Crash Version 2
I hypothesize that it will take
longer for the car to finish the race for the crash scenario in which the race
car needs to return to the original starting position each time it crashes into
a wall. In other words, Bad Crash Version 1 (return to nearest open position) performance
will be better than Bad Crash Version 2 (return to start) performance.
In the case of the value
iteration reinforcement learning algorithm, the race car (i.e. agent) knows two
things before taking a specific action (i.e. accelerate in x and/or y
directions) (Alpaydin, 2014):
The probabilities of ending up in other new states given a particular action is taken from a current state.
More formally, the race car knows the transition function.
As discussed in the previous section, the transition function takes as input the current state s and selected action a and outputs the probability of transitioning to some new state s’.
The immediate reward (e.g. race car gets -1 reward each time it moves to a new state) that would be received for taking a particular action from a current state.
More formally, this is known as the reward function.
The reward function takes as input the current state s and selected action a and outputs the reward.
Value iteration is known as a
model-based reinforcement learning technique because it tries to learn a
model of the environment where the model has two components:
Transition Function
The race car looks at each time it was in a particular state s and took a particular action a and ended up in a new state s’. It then updates the transition function (i.e. transition probabilities) according to these frequency counts.
Reward Function
This function answers the question: “Each time the race car was in a particular state s and took a particular action a, what was the reward?”
In short, in value iteration, the
race car knows the transition probabilities and reward function (i.e. the model
of the environment) and uses that information to govern how the race car should
act when in a particular state. Being able to look ahead and calculate
the expected rewards of a potential action gives value iteration a distinct
advantage over other reinforcement learning algorithms when the set of states
is small in size.
Let us take a look at an example.
Suppose the following:
The race car is in state so, where s0
= <x0, y0, vx0, vy0>, corresponding
to the x-position on a racetrack, y-position on a race track, velocity in the x
direction, and velocity in the y direction, at timestep 0.
The race car takes action a0,
where a0 = (ax0, ay0)
= (change in velocity in x-direction, change in
velocity in y-direction)
The race car then observes the new state, where
the new state is s1, where s1 = <x1, y1, vx1, vy1>.
The race car receives a reward r0
for taking action a0 in state s0.
Putting the bold-faced variables together, we get the following expression which is known as an experience tuple. Tuples are just like lists except the data inside of them cannot be changed.
Experience Tuple = <s0, a0, s1, r0>
What the experience tuple above says is that if the race car
is in state s0 and takes action a0, the race car will
observe a new state s1 and will receive reward r0.
Then at the next time step, we generate another experience
tuple that is represented as follows.
Experience Tuple = <s1, a1, s2, r1>
This process of collecting experience tuples as the race car
explores the race track (i.e. environment) happens repeatedly.
Because value iteration is a model based approach, it builds
a model of the transition function T[s, a, s’] and reward
function R[s,a,s’] using the experience tuples.
The transition function can be built and updated
by adding up how many times the race car was in state s and took a particular
action a and then observed a new state s’. Recall that T[s, a, s’] stands for
the probability the race car finds itself in a new state s’ (at the next
timestep) given that it was in state s and took action a.
The reward function can be built by examining
how many times the race car was in state s and took a particular action a and
received a reward r. From that information the average reward for that
particular scenario can be calculated.
Once these models are built, the race
car can then can use value iteration to determine the optimal values of each
state (hence the name value iteration). In some texts, values are referred
to as utilities (Russell, Norvig, & Davis, 2010).
What are optimal values?
Each state s in the environment (denoted
as <xt, yt, vxt, vyt > in this racetrack project) has a
value V(s). Different actions can be taken in a given state s. The optimal
values of each state s are based on the action a that generates the best expected
cumulative reward.
Expected cumulative reward is defined
as the immediate reward that the race car would receive if it takes action a
and ends up in the state s + the discounted reward that the race car
would receive if it always chose the best action (the one that maximizes total
reward) from that state onward to the terminal state (i.e. the finish line of
the racetrack).
V*(s) =
best possible (immediate reward + discounted future reward)
where the * means “optimal.”
The reason why those future rewards
are discounted (typically by a number in the range [0,1), known as gamma γ) is
because rewards received far into the future are worth less than rewards
received immediately. For all we know, the race car might have a gas-powered
engine, and there is always the risk of running out of gas. After all, would
you rather receive $10 today or $10 ten years from now? $10 received today is
worth more (e.g. you can invest it to generate even more cash). Future rewards
are more uncertain so that is why we incorporate the discount rate γ.
It is common in control applications
to see state-action pairs denoted as Q*(s, a) instead of V*(s) (Alpaydin, 2014). Q*(s,a) is the [optimal]
expected cumulative reward when the race car takes an action a in state s and
always takes the best action after that timestep. All of these values are
typically stored in a table. The table maps state-action pairs to the optimal Q-values
(i.e. Q*(s,a)).
Each row in the table corresponds to
a state-action-value
combination. So in this racetrack problem, we have the following entries into the
value table:
[x, y, vx, vy, ax, ay, Q(s,a)] = [x-coordinate, y-coordinate, velocity in
x-direction, velocity in y-direction, acceleration in
x-direction, acceleration in y-direction, value when taking that action in that state]
Note that Q(s,a) above is not labeled
Q*(s,a). Only once value iteration is done can we label it Q*(s,a) because it
takes time to find the optimal Q values for each state-action pair.
With the optimal Q values for each
state-action pair, the race car can calculate the best action to take given a
state. The best action to take given a state is the one with the highest Q
value.
At this stage, the race car is ready
to start the engine and leave the starting position.
At each timestep of the race, the
race car observes the state (i.e. position and velocity) and decides what
action to apply by looking at the value table. It finds the action that
corresponds to the highest Q value for that state. The car then takes that
action.
The pseudocode for calculating
the optimal value for each state-action pair (denoted as Q*(s,a)) in the
environment is below. This algorithm is the value iteration algorithm because
it iterates over all the state-action pairs in the environment and gives them a
value based on the cumulative expected reward of that state-action pair (Alpaydin, 2014; Russell, Norvig, & Davis, 2010; Sutton & Barto,
2018):
Value Iteration Algorithm Pseudocode
Inputs
States:
List of all possible values of x
List of all possible values of y
List of all possible values of vx
List of all possible values of vy
Actions:
List of all the possible values of ax
List of all possible values of ay
Model:
Model = Transition Model T[s, a, s’] + Reward Model R[s, a, s’]
Where Model is a single table with the following row entries
[s, a, s’, probability of ending up in a new state s’ given state s and action a, immediate reward for ending up in new state s’ given state s and action a]
= [s, a, s’, T(s, a, s’), R(s, a, s’)]
Note that the reward will be -1 for each state except the finish line states (i.e. absorbing or terminal states), which will have a reward of 0.
Discount Rate:
γ
Where 0 ≤ γ < 1
If γ = 0, rewards received immediately are the only thing that counts.
As γ gets close to 1, rewards received further in the future count more.
Error Threshold
Ɵ
Ɵ is a small number that helps us to determine when we have optimal values for each state-action pair (also known as convergence).
Ɵ helps us know when the values of each state-action pair, denoted as Q(s,a), stabilize (i.e. stop changing a lot from run to run of the loop).
Process
Create a table V(s) that will store the optimal
Q-value for each state. This table will help us determine when we should stop
the algorithm and return the output. Initialize all the values of V(s) to
arbitrary values, except the terminal state (i.e. finish line state) that has a
value of 0.
Initialize all Q(s,a) to arbitrary values,
except the terminal state (i.e. finish line states) that has a value of 0.
Initialize a Boolean variable called done that
is a flag to indicate when we are done building the model (or set a fixed
number of training iterations using a for loop).
While (Not Done)
Initialize a value called Δ
to 0. When this value gets below the error threshold Ɵ, we exit the main loop and
return the output.
For all possible states of the environment
v := V(s) // Extract and store the most
recent value of the state
For all possible actions the race car (agent)
can take
Q(s,a) = (expected immediate reward given the
state s and an action a) + (discount rate) * [summation_over_all_new_states(probability
of ending up in a new state s’given a state s and action a * value of the new
state s’)]
More formally,
where
= expected
immediate return when taking action a from state s
V(s) := maximum value of Q(s,a) across all the
different actions that the race car can take from state s
Δ := maximum(Δ, |v – V(s)|)
Is Δ < Ɵ? If yes, we are done (because the
values of V(s) have converged i.e. stabilized), otherwise we are not done.
Output
The output of the value iteration
algorithm is the value table, the table that maps state-action pairs to the
optimal Q-values (i.e. Q*(s,a)).
Each row in the table corresponds to
a state-action-value
combination. So in this racetrack problem, we have the following entries into
the value table:
[x, y, vx, vy, ax, ay, Q(s,a)] = [x-coordinate, y-coordinate, velocity in
x-direction, velocity in y-direction, acceleration in
x-direction, acceleration in y-direction, value when taking that action in that state]
Policy Determination
With the optimal Q values for each
state-action pair, the race car can calculate the best action to take given a
state. The best action to take given a state is the one with the highest Q
value.
At this stage, the race car is ready
to start the engine and leave the starting position.
At each timestep t of the race, the
race car observes the state st (i.e. position and velocity) and
decides what action at to apply by looking at the value table. It
finds the action that corresponds to the highest Q value for that state (the action
that will generate the highest expected cumulative reward). The car then takes
that action at*, where at* means
the optimal action to take at time t.
More formally, the optimal policy for
a given states s at timestep t is π*(s) where:
For each state s do
Value Iteration Summary
So, to sum up, on a high level,
the complete implementation of an autonomous race car using value iteration has
two main steps:
Step 1: During the training phase, calculate the value of each state-action pair.
Step 2: At each timestep of the time trial, given a current state s, select the action a where the state-action pair value (i.e. Q*(s,a)) is the highest.
In most real-world cases, it is not computationally efficient to build a complete model (i.e. transition and reward function) of the environment beforehand. In these cases, the transition and reward functions are unknown. To solve this issue, model-free reinforcement learning algorithms like Q-learning were invented (Sutton & Barto, 2018) .
In
the case of model-free learning, the race car (i.e. agent) has to interact with
the environment and learn through trial-and-error. It cannot look ahead to
inspect what the value would be if it were to take a particular action from
some state. Instead, in Q-learning, the race car builds a table of Q-values
denoted as Q(s,a) as it takes an action a from a state s and then receives a
reward r. The race car uses the Q-table at each timestep to determine the best
action to take given what it has learned in previous timesteps.
One
can consider Q[s,a] as a two-dimensional table that has the states s as the
rows and the available actions a as the columns. The value in each cell of this
table (i.e. the Q-value) represents the value of taking action a in state s.
Just
like in value iteration, value has two components, the immediate reward and the
discounted future reward.
Where
immediate reward is the reward you get when taking action a from state s, and
discounted future reward is the reward you get for taking actions in the
future, discounted by some discount rate γ. The reason why future rewards are
discounted is because rewards received immediately are more certain than
rewards received far into the future (e.g. $1 received today is worth more than
$1 received fifty years from now).
So, how
does the race car use the table Q[s,a] to determine what action to take at each
timestep of the time trial (i.e. what is the policy given a state…π(s))?
Each
time the race car observes state s, it consults the Q[s,a] table. It takes a
look to see which action generated the highest Q value given that observed state
s. That action is the one that it takes.
More
formally,
π(s) = argmaxa(Q[s,a])
argmax
is a function that returns the action a that maximizes the expression Q[s,a].
In other words, the race car looks across all position actions given a state
and selects the action that has the highest Q-value.
After
Q-learning is run a number of times, an optimal policy will eventually be
found. The optimal policy is denoted as π*(s).
Similarly the optimal Q-value is Q*[s,a].
How the Q[s,a] Table Is Built
Building the Q[s,a]
table can be broken down into the following steps:
Choose the environment we want to train on.
In this race car problem, the racetrack environment is what we want the race car to explore and train on.
The racetrack provides the x and y position data that constitutes a portion of the state s that will be observed at each timestep by the racecar (agent).
Starting from the starting position, the race car moves through the environment, observing the state s at a given timestep
The race car consults the policy π(s), the function that takes as input the current state and outputs an action a.
The action a taken by the race car changes the state of the environment from s to s’. The environment also generates a reward r that is passed to the race car.
Steps 2-4 generate a complete experience tuple (i.e. a tuple is a list that does not change) of the form <s, a, s’, r> = <state, action, new state, reward>.
The experience tuple in 5 is used to update the Q[s,a] table.
Repeat steps 3-6 above until the race car gets to the terminal state (i.e. the finish line of the racetrack).
With
the Q[s,a] table built, the race car can now test the policy. Testing the policy
means starting at the starting position and consulting the Q[s,a] table at each
timestep all the way until the race car reaches the finish line. We count how
many timesteps it took for the race car to reach the finish line. If the time
it took the race car to complete the time trial is not improving, we are done.
Otherwise, we make the race car go back through the training process and test
its policy again with an updated Q[s,a] table.
We expect that
eventually, the performance of the race car will reach a plateau.
How the Q[s,a] Table Is Updated
With those high
level steps, let us take a closer look now at how the Q[s,a] table is updated in
step 6 in the previous section.
Each
time the race car takes an action, the environment transitions to a new state
s’ and gives the race car a reward r. This information is used by the race car
to update its Q[s,a] table.
The
update rule consists of two parts, where Q’[s,a] is the new updated Q-value for
an action a taken at state s.
The
learning rate is a number 0 < α ≤ 1
(commonly 0.2). Thus, the new updated Q-value is a blend of the old Q-value and
the improved estimate of the Q value for a given state-action pair. The higher
the learning rate, the more new information is considered when updating the
Q-value. A learning rate of 1 means that the new updated Q-value is only
considering the new information.
Where
γ is the discount rate. It is typically by a number in the range
[0,1). It means that rewards received by the race car (agent) in the far
future are worth less than an immediate reward.
Continuing
with the expansion of the equation, we have:
Q’[s,a]
= Q[s,a] + α *
(r + γ * Q[s’, optimal action a’ at new state s’] – Q[s,a])
Note
in the above equation that we assume that the race car reaches state s’ and
takes the best action from there, where best action is the action a’ that has
the highest Q-value for that given new state s’.
Where
argmaxa’(Q[s’,a’]) returns the action a’ that has the highest Q
value for a given state s’.
How an Action Is Selected at Each Step
The performance of
Q-learning is improved the more the race car explores different states and
takes different actions in those states. In other words, the more state-action
pairs the race car explores, the better Q[s,a] table the race car is able to
build.
One strategy for
forcing the race car to explore as much of the state-action space as possible
is to add randomness into the Q-learning procedure. This is called exploration.
More specifically, at the step of Q-learning where the race car selects an
action based on the Q[s,a] table:
There is a 20% chance ε that the race car will
do some random action, and a 80% chance the race car will do the action with
the highest Q-value (as determined by the Q[s,a] table). The latter is known as
exploitation.
If the race car does some random action, the
action is chosen randomly from the set of possible actions the race car can perform.
I
chose 20% as the starting probability (i.e. ε) of doing some random action, but
it could be another number (e.g. 30%) As the race car gains more experience and
the Q[s,a] table gets better and better, we want the race car to take fewer
random actions and follow its policy more often. For this reason, it is common
to reduce ε with each successive iteration, until we get to a point where the
race car stops exploring random actions.
Q-Learning Algorithm Pseudocode
Below
are is the Q-learning algorithm pseudocode on a high level (Alpaydin,
2014; Sutton & Barto, 2018).
Inputs
Learning rate α, where 0 < α ≤ 1 (e.g.
0.2)
Discount rate γ (e.g. 0.9)
Exploration probability ε, corresponding to the
probability that the race car will take a random action given a state (e.g. 0.2)
Reduction of exploration probability,
corresponding to how much we want to decrease ε at each training iteration (defined
as a complete trip from the starting position to the finish line terminal state)
of the Q-learning process. (e.g. 0.5)
Number of training iterations (e.g. 1,000,000)
States:
List of all possible values of x
List of all possible values of y
List of all possible values of vx
List of all possible values of vy
Actions:
List of all the possible values of ax
List of all possible values of ay
Process
Initialize the Q[s,a] table to small random
values, except for the terminal state which will have a value of 0.
For a fixed number of training iterations
Initialize the state to the starting position of
the race track
Initialize a Boolean variable to see if the race
car has crossed the finish line (i.e. reached the terminal state)
While the race car has not reached the finish
line
Select the action a using the Q[s,a] table that
corresponds to the highest Q value given state s
Algorithm returns
the Q[s,a] table which is used to determine the optimal policy.
Policy Determination
Each
time the race car observes state s, it consults the Q[s,a] table. It takes a
look to see which action generated the highest Q value given that observed
state s. That action is the one that it takes.
More
formally,
π(s) = argmaxa(Q[s,a])
argmax
is a function that returns the action a that maximizes the expression Q[s,a].
In other words, the race car looks across all position actions given a state
and selects the action that has the highest Q-value.
Helpful Video
Here is a good video where Professor Tucker Balch from Georgia Tech goes through Q-learning, step-by-step:
The Q-learning and value
iteration algorithms were implemented for the racetrack problem and then tested
on two different racetracks: an R-shaped racetrack and an L-shaped racetrack.
The number of timesteps the race car needed to find the finish line was
calculated for each algorithm-racetrack combination. The number of training
iterations was also monitored. 10 experiments for each algorithm-racetrack
combination were performed to create data to graph a learning curve.
At each timestep t of the
racetrack problem, we have the following variables (below). We assume the
racetrack is a Cartesian grid with an x-axis and a y-axis, and that the system
is governed by the standard laws of kinematics:
State = <xt, yt,
vxt, vyt> = variables that describe the current
situation (i.e. environment)
xt = x coordinate of the location of
the car at timestep t
yt = y coordinate of the location of
the car at timestep t
vxt = velocity in the x direction at
time step t, where vxt = xt – xt-1
Maximum vxt = 5
Minimum vxt = -5
vyt = velocity in the y direction at
timestep t, where vyt = yt – yt-1
Action = <axt, ayt>
= control variables = what the race car (i.e. agent) can do to influence the
state
axt = accelerate in the x direction
at timestep t, where axt = vxt – vx t-1
-1 (decrease velocity in x-direction by 1)
0 (keep same velocity), or
1 (increase velocity in x-direction by 1)
ayt = acceleration in the y direction
at timestep t, where ayt = vyt – vy t-1
-1 (decrease velocity in y-direction by 1)
0 (keep same velocity)
1 (increase velocity in y-direction by 1)
Example Calculation
At t = 0, the race car observes
the current state. Location is (2,2) and
velocity is (1,0).
Position (i.e. x and y coordinates) is now (x0 +vx1, y0 + vy1)
= (2 + 2, 2 + 1) = (x1, y1)
= (4, 3)
Thus, putting it all together, the new state is
now <x1, y1, vx1, vy1> = <4, 3, 2, 1>
Acceleration is Not Guaranteed
There is another twist with the
racetrack problem. At any timestep t, when the race car attempts to accelerate,
there is a 20% chance that the attempt will fail. In other words, each time the
race car selects an action, there is a:
20% chance that the attempt fails, and velocity
remains unchanged at the next timestep; i.e. (axt,
ayt) = (0, 0)
80% chance velocity changes at the next timestep;
(axt, ayt) = (selected
acceleration in the x-direction, selected
acceleration in y-direction)
Must Avoid Crashing Into Walls
The race car must stay on the
track. Crashing into walls is bad. Two versions of a “bad crash” will be
implemented in this project:
Bad Crash Version 1: If the car crashes
into a wall,
The car is placed at the nearest position
on the track to the place where the car crashed.
The velocity is set to (0, 0).
Bad Crash Version 2: If the car crashes
into a wall,
The car must go back to the original starting
position.
The velocity is set to (0, 0).
In this project, the performance
of both versions of bad crash will be compared side by side for the
reinforcement learning algorithms implemented in this project.
Reward Function
Because the goal is for the race
car to go from the starting position to the finish line in the minimum number
of timesteps, we assign a reward of -1 for each move the car makes. Because the
reward is negative, this means that the race car incurs a cost (i.e. penalty)
of 1 each time the race car moves to another position. The goal is to get to
the finish line in as few moves (i.e. time steps) as possible in order to
minimize the total cost (i.e. maximize the total reward).
The finish line is the final
state (often referred to as terminal or absorbing state). This state has a
reward of 0. There is no requirement to stop right on the finish position (i.e.
finish line). It is sufficient to cross it.
Description of Any Tuning Process Applied
In order to compare the performance of the algorithm for the
different crash scenarios, the principal hyperparameters were kept the same for
all runs. These hyperparameters are listed below.
Learning Rate
The Learning rate α was set to 0.2.
Discount Rate
The discount rate γ was set to 0.9.
Exploration Probability
The exploration probability ε was set to 0.2 as dictated by
the problem statement.
Number of Training Iterations
The number of training iterations was varied in order to
computer the learning curve (see Results section below)
Here is the full code for value iteration. Don’t be scared by how long this code is. I included a lot of comments so that you know what is going on at each step of the code. This and the racetrack text files above are all you need to run the program (just copy and paste into your favorite IDE!):
import os # Library enables the use of operating system dependent functionality
import time # Library handles time-related tasks
from random import shuffle # Import shuffle() method from the random module
from random import random # Import random() method from the random module
from copy import deepcopy # Enable deep copying
import numpy as np # Import Numpy library
# File name: value_iteration.py
# Author: Addison Sears-Collins
# Date created: 8/14/2019
# Python version: 3.7
# Description: Implementation of the value iteration reinforcement learning
# algorithm for the racetrack problem.
# The racetrack problem is described in full detail in:
# Barto, A. G., Bradtke, S. J., Singh, S. P. (1995). Learning to Act Using
# Real-time Dynamic Programming. Artificial Intelligence, 72(1-2):81–138.
# and
# Sutton, Richard S., and Andrew G. Barto. Reinforcement learning :
# An Introduction. Cambridge, Massachusetts: The MIT Press, 2018. Print.
# (modified version of Exercise 5.12 on pg. 111)
# Define constants
ALGORITHM_NAME = "Value_Iteration"
FILENAME = "R-track.txt"
THIS_TRACK = "R_track"
START = 'S'
GOAL = 'F'
WALL = '#'
TRACK = '.'
MAX_VELOCITY = 5
MIN_VELOCITY = -5
DISC_RATE = 0.9 # Discount rate, also known as gamma. Determines by how much
# we discount the value of a future state s'
ERROR_THRES = 0.001 # Determine when Q-values stabilize (i.e.theta)
PROB_ACCELER_FAILURE = 0.20 # Probability car will try to take action a
# according to policy pi(s) = a and fail.
PROB_ACCELER_SUCCESS = 1 - PROB_ACCELER_FAILURE
NO_TRAINING_ITERATIONS = 10 # A single training iteration runs through all
# possible states s
NO_RACES = 10 # How many times the race car does a single time trial from
# starting position to the finish line
FRAME_TIME = 0.3 # How many seconds between frames printed to the console
MAX_STEPS = 500 # Maximum number of steps the car can take during time trial
MAX_TRAIN_ITER = 50 # Maximum number of training iterations
# Range of the velocity of the race car in both y and x directions
vel_range = range(MIN_VELOCITY, MAX_VELOCITY + 1)
# All actions A the race car can take
# (acceleration in y direction, acceleration in x direction)
actions = [(-1,-1), (0,-1), (1,-1),
(-1,0) , (0,0), (1,0),
(-1,1) , (0,1), (1,1)]
def read_environment(filename):
"""
This method reads in the environment (i.e. racetrack)
:param str filename
:return environment: list of lists
:rtype list
"""
# Open the file named filename in READ mode.
# Make the file an object named 'file'
with open(filename, 'r') as file:
# Read until end of file using readline()
# readline() then returns a list of the lines
# of the input file
environment_data = file.readlines()
# Close the file
file.close()
# Initialize an empty list
environment = []
# Adds a counter to each line in the environment_data list,
# i is the index of each line and line is the actual contents.
# enumerate() helps get not only the line of the environment but also
# the index of that line (i.e. row)
for i,line in enumerate(environment_data):
# Ignore the first line that contains the dimensions of the racetrack
if i > 0:
# Remove leading or trailing whitespace if applicable
line = line.strip()
# If the line is empty, ignore it
if line == "": continue
# Creates a list of lines. Within each line is a list of
# individual characters
# The stuff inside append(stuff) first creates a new_list = []
# It then appends all the values in a given line to that new
# list (e.g. new_list.append(all values inside the line))
# Then we append that new list to the environment list.
# Therefoer, environment is a list of lists.
environment.append([x for x in line])
# Return the environment (i.e. a list of lists/lines)
return environment
def print_environment(environment, car_position = [0,0]):
"""
This method reads in the environment and current
(y,x) position of the car and prints the environment to the console
:param list environment
:param list car_position
"""
# Store value of current grid square
temp = environment[car_position[0]][car_position[1]]
# Move the car to current grid square
environment[car_position[0]][car_position[1]] = "X"
# Delay
time.sleep(FRAME_TIME)
# Clear the printed output
clear()
# For each line in the environment
for line in environment:
# Initialize a string
text = ""
# Add each character to create a line
for character in line:
text += character
# Print the line of the environment
print(text)
# Retstore value of current grid square
environment[car_position[0]][car_position[1]] = temp
def clear():
"""
This method clears the print output
"""
os.system( 'cls' )
def get_random_start_position(environment):
"""
This method reads in the environment and selects a random
starting position on the racetrack (y, x). Note that
(0,0) corresponds to the upper left corner of the racetrack.
:param list environment: list of lines
:return random starting coordinate (y,x) on the racetrack
:rtype tuple
"""
# Collect all possible starting positions on the racetrack
starting_positions = []
# For each row in the environment
for y,row in enumerate(environment):
# For each column in each row of the environment
for x,col in enumerate(row):
# If we are at the starting position
if col == START:
# Add the coordiante to the list of available
# starting positions in the environment
starting_positions += [(y,x)]
# Random shuffle the list of starting positions
shuffle(starting_positions)
# Select a starting position
return starting_positions[0]
def get_new_velocity(old_vel,accel,min_vel=MIN_VELOCITY,max_vel=MAX_VELOCITY):
"""
Get the new velocity values
:param tuple old_vel: (vy, vx)
:param tuple accel: (ay, ax)
:param int min_vel: Minimum velocity of the car
:param int max_vel: Maximum velocity of the car
:return new velocities in y and x directions
"""
new_y = old_vel[0] + accel[0]
new_x = old_vel[1] + accel[1]
if new_x < min_vel: new_x = min_vel
if new_x > max_vel: new_x = max_vel
if new_y < min_vel: new_y = min_vel
if new_y > max_vel: new_y = max_vel
# Return the new velocities
return new_y, new_x
def get_new_position(old_loc, vel, environment):
"""
Get a new position using the old position and the velocity
:param tuple old_loc: (y,x) position of the car
:param tuple vel: (vy,vx) velocity of the car
:param list environment
:return y+vy, x+vx: (new_y,new_x)
"""
y,x = old_loc[0], old_loc[1]
vy, vx = vel[0], vel[1]
# new_y = y+vy, new_x = x + vx
return y+vy, x+vx
def get_nearest_open_cell(environment, y_crash, x_crash, vy = 0, vx = (
0), open = [TRACK, START, GOAL]):
"""
Locate the nearest open cell in order to handle crash scenario.
Distance is calculated as the Manhattan distance.
Start from the crash grid square and expand outward from there with
a radius of 1, 2, 3, etc. Forms a diamond search pattern.
For example, a Manhattan distance of 2 would look like the following:
.
...
..#..
...
.
If velocity is provided, search in opposite direction of velocity so that
there is no movement over walls
:param list environment
:param int ycrash: y coordinate where crash happened
:param int xcrash: x coordinate where crash happened
:param int vy: velocity in y direction when crash occurred
:param int vx: velocity in x direction when crash occurred
:param list of strings open: Contains environment types
:return tuple of the nearest open y and x position on the racetrack
"""
# Record number of rows (lines) and columns in the environment
rows = len(environment)
cols = len(environment[0])
# Add expanded coverage for searching for nearest open cell
max_radius = max(rows,cols)
# Generate a search radius for each scenario
for radius in range(max_radius):
# If car is not moving in y direction
if vy == 0:
y_off_range = range(-radius, radius + 1)
# If the velocity in y-direction is negative
elif vy < 0:
# Search in the positive direction
y_off_range = range(0, radius + 1)
else:
# Otherwise search in the negative direction
y_off_range = range(-radius, 1)
# For each value in the search radius range of y
for y_offset in y_off_range:
# Start near to crash site and work outwards from there
y = y_crash + y_offset
x_radius = radius - abs(y_offset)
# If car is not moving in x direction
if vx == 0:
x_range = range(x_crash - x_radius, x_crash + x_radius + 1)
# If the velocity in x-direction is negative
elif vx < 0:
x_range = range(x_crash, x_crash + x_radius + 1)
# If the velocity in y-direction is positive
else:
x_range = range(x_crash - x_radius, x_crash + 1)
# For each value in the search radius range of x
for x in x_range:
# We can't go outside the environment(racetrack) boundary
if y < 0 or y >= rows: continue
if x < 0 or x >= cols: continue
# If we find and open cell, return that (y,x) open cell
if environment[y][x] in open:
return(y,x)
# No open grid squares found on the racetrack
return
def act(old_y, old_x, old_vy, old_vx, accel, environment, deterministic=(
False),bad_crash = False):
"""
This method generates the new state s' (position and velocity) from the old
state s and the action a taken by the race car. It also takes as parameters
the two different crash scenarios (i.e. go to nearest
open position on the race track or go back to start)
:param int old_y: The old y position of the car
:param int old_x: The old x position of the car
:param int old_vy: The old y velocity of the car
:param int old_vx: The old x velocity of the car
:param tuple accel: (ay,ax) - acceleration in y and x directions
:param list environment: The racetrack
:param boolean deterministic: True if we always follow the policy
:param boolean bad_crash: True if we return to start after crash
:return s' where s' = new_y, new_x, new_vy, and new_vx
:rtype int
"""
# This method is deterministic if the same output is returned given
# the same input information
if not deterministic:
# If action fails (car fails to take the prescribed action a)
if random() > PROB_ACCELER_SUCCESS:
#print("Car failed to accelerate!")
accel = (0,0)
# Using the old velocity values and the new acceleration values,
# get the new velocity value
new_vy, new_vx = get_new_velocity((old_vy,old_vx), accel)
# Using the new velocity values, update with the new position
temp_y, temp_x = get_new_position((old_y,old_x), (new_vy, new_vx),(
environment))
# Find the nearest open cell on the racetrack to this new position
new_y, new_x = get_nearest_open_cell(environment, temp_y, temp_x, new_vy,
new_vx)
# If a crash happens (i.e. new position is not equal to the nearest
# open position on the racetrack
if new_y != temp_y or new_x != temp_x:
# If this is a crash in which we would have to return to the
# starting position of the racetrack and we are not yet
# on the finish line
if bad_crash and environment[new_y][new_x] != GOAL:
# Return to the start of the race track
new_y, new_x = get_random_start_position(environment)
# Velocity of the race car is set to 0.
new_vy, new_vx = 0,0
# Return the new state
return new_y, new_x, new_vy, new_vx
def get_policy_from_Q(cols, rows, vel_range, Q, actions):
"""
This method returns the policy pi(s) based on the action taken in each state
that maximizes the value of Q in the table Q[s,a]. This is pi*(s)...the
best action that the race car should take in each state is the one that
maximizes the value of Q. (* means optimal)
:param int cols: Number of columns in the environment
:param int rows: Number of rows (i.e. lines) in the environment
:param list vel_range: Range of the velocity of the race car
:param list of tuples actions: actions = [(ay,ax),(ay,ax)....]
:return pi : the policy
:rtype: dictionary: key is the state tuple, value is the
action tuple (ay,ax)
"""
# Create an empty dictionary called pi
pi = {}
# For each state s in the environment
for y in range(rows):
for x in range(cols):
for vy in vel_range:
for vx in vel_range:
# Store the best action for each state...the one that
# maximizes the value of Q.
# argmax looks across all actions given a state and
# returns the index ai of the maximum Q value
pi[(y,x,vy,vx)] = actions[np.argmax(Q[y][x][vy][vx])]
# Return pi(s)
return(pi)
def value_iteration(environment, bad_crash = False, reward = (
0.0), no_training_iter = NO_TRAINING_ITERATIONS):
"""
This method is the value iteration algorithm.
:param list environment
:param boolean bad_crash
:param int reward of the terminal states (i.e. finish line)
:param int no_training_iter
:return policy pi(s) which maps a given state to an optimal action
:rtype dictionary
"""
# Calculate the number of rows and columns of the environment
rows = len(environment)
cols = len(environment[0])
# Create a table V(s) that will store the optimal Q-value for each state.
# This table will help us determine when we should stop the algorithm and
# return the output. Initialize all the values of V(s) to arbitrary values,
# except the terminal state (i.e. finish line state) that has a value of 0.
# values[y][x][vy][vx]
# Read from left to right, we create a list of vx values. Then for each
# vy value we assign the list of vx values. Then for each x value, we assign
# the list of vy values (which contain a list of vx values), etc.
# This is called list comprehension.
values = [[[[random() for _ in vel_range] for _ in vel_range] for _ in (
line)] for line in environment]
# Set the finish line states to 0
for y in range(rows):
for x in range(cols):
# Terminal state has a value of 0
if environment[y][x] == GOAL:
for vy in vel_range:
for vx in vel_range:
values[y][x][vy][vx] = reward
# Initialize all Q(s,a) to arbitrary values, except the terminal state
# (i.e. finish line states) that has a value of 0.
# Q[y][x][vy][vx][ai]
Q = [[[[[random() for _ in actions] for _ in vel_range] for _ in (
vel_range)] for _ in line] for line in environment]
# Set finish line state-action pairs to 0
for y in range(rows):
for x in range(cols):
# Terminal state has a value of 0
if environment[y][x] == GOAL:
for vy in vel_range:
for vx in vel_range:
for ai, a in enumerate(actions):
Q[y][x][vy][vx][ai] = reward
# This is where we train the agent (i.e. race car). Training entails
# optimizing the values in the tables of V(s) and Q(s,a)
for t in range(no_training_iter):
# Keep track of the old V(s) values so we know if we reach stopping
# criterion
values_prev = deepcopy(values)
# When this value gets below the error threshold, we stop training.
# This is the maximum change of V(s)
delta = 0.0
# For all the possible states s in S
for y in range(rows):
for x in range(cols):
for vy in vel_range:
for vx in vel_range:
# If the car crashes into a wall
if environment[y][x] == WALL:
# Wall states have a negative value
# I set some arbitrary negative value since
# we want to train the car not to hit walls
values[y][x][vy][vx] = -9.9
# Go back to the beginning
# of this inner for loop so that we set
# all the other wall states to a negative value
continue
# For each action a in the set of possible actions A
for ai, a in enumerate(actions):
# The reward is -1 for every state except
# for the finish line states
if environment[y][x] == GOAL:
r = reward
else:
r = -1
# Get the new state s'. s' is based on the current
# state s and the current action a
new_y, new_x, new_vy, new_vx = act(
y,x,vy,vx,a,environment,deterministic = True,
bad_crash = bad_crash)
# V(s'): value of the new state when taking action
# a from state s. This is the one step look ahead.
value_of_new_state = values_prev[new_y][new_x][
new_vy][new_vx]
# Get the new state s'. s' is based on the current
# state s and the action (0,0)
new_y, new_x, new_vy, new_vx = act(
y,x,vy,vx,(0,0),environment,deterministic = (
True), bad_crash = bad_crash)
# V(s'): value of the new state when taking action
# (0,0) from state s. This is the value if for some
# reason the race car attemps to accelerate but
# fails
value_of_new_state_if_action_fails = values_prev[
new_y][new_x][new_vy][new_vx]
# Expected value of the new state s'
# Note that each state-action pair has a unique
# value for s'
expected_value = (
PROB_ACCELER_SUCCESS * value_of_new_state) + (
PROB_ACCELER_FAILURE * (
value_of_new_state_if_action_fails))
# Update the Q-value in Q[s,a]
# immediate reward + discounted future value
Q[y][x][vy][vx][ai] = r + (
DISC_RATE * expected_value)
# Get the action with the highest Q value
argMaxQ = np.argmax(Q[y][x][vy][vx])
# Update V(s)
values[y][x][vy][vx] = Q[y][x][vy][vx][argMaxQ]
# Make sure all the rewards to 0 in the terminal state
for y in range(rows):
for x in range(cols):
# Terminal state has a value of 0
if environment[y][x] == GOAL:
for vy in vel_range:
for vx in vel_range:
values[y][x][vy][vx] = reward
# See if the V(s) values are stabilizing
# Finds the maximum change of any of the states. Delta is a float.
delta = max([max([max([max([abs(values[y][x][vy][vx] - values_prev[y][
x][vy][vx]) for vx in vel_range]) for vy in (
vel_range)]) for x in range(cols)]) for y in range(rows)])
# If the values of each state are stabilizing, return the policy
# and exit this method.
if delta < ERROR_THRES:
return(get_policy_from_Q(cols, rows, vel_range, Q, actions))
return(get_policy_from_Q(cols, rows, vel_range, Q, actions))
def do_time_trial(environment, policy, bad_crash = False, animate = True,
max_steps = MAX_STEPS):
"""
Race car will do a time trial on the race track according to the policy.
:param list environment
:param dictionary policy: A dictionary containing the best action for a
given state. The key is the state y,x,vy,vx and value is the action
(ax,ay) acceleration
:param boolean bad_crash: The crash scenario. If true, race car returns to
starting position after crashes
:param boolean animate: If true, show the car on the racetrack at each
timestep
:return i: Total steps to complete race (i.e. from starting position to
finish line)
:rtype int
"""
# Copy the environment
environment_display = deepcopy(environment)
# Get a starting position on the race track
starting_pos = get_random_start_position(environment)
y,x = starting_pos
vy,vx = 0,0 # We initialize velocity to 0
# Keep track if we get stuck
stop_clock = 0
# Begin time trial
for i in range(max_steps):
# Show the race car on the racetrack
if animate:
print_environment(environment_display, car_position = [y,x])
# Get the best action given the current state
a = policy[(y,x,vy,vx)]
# If we are at the finish line, stop the time trial
if environment[y][x] == GOAL:
return i
# Take action and get new a new state s'
y,x,vy,vx = act(y, x, vy, vx, a, environment, bad_crash = bad_crash)
# Determine if the car gets stuck
if vy == 0 and vx == 0:
stop_clock += 1
else:
stop_clock = 0
# We have gotten stuck as the car has not been moving for 5 timesteps
if stop_clock == 5:
return max_steps
# Program has timed out
return max_steps
def main():
"""
The main method of the program
"""
print("Welcome to the Racetrack Control Program!")
print("Powered by the " + ALGORITHM_NAME +
" Reinforcement Learning Algorithm\n")
print("Track: " + THIS_TRACK)
print()
print("What happens to the car if it crashes into a wall?")
option_1 = """1. Starts from the nearest position on the track to the
place where it crashed."""
option_2 = """2. Returns back to the original starting position."""
print(option_1)
print(option_2)
crash_scenario = int(input("Crash scenario (1 or 2): "))
no_training_iter = int(input(
"Enter the initial number of training iterations (e.g. 5): "))
print("\nThe race car is training. Please wait...")
# Directory where the racetrack is located
#racetrack_name = input("Enter the path to your input file: ")
racetrack_name = FILENAME
racetrack = read_environment(racetrack_name)
# How many times the race car will do a single time trial
races = NO_RACES
while(no_training_iter < MAX_TRAIN_ITER):
# Keep track of the total number of steps
total_steps = 0
# Record the crash scenario
bad_crash = False
if crash_scenario == 1:
bad_crash = False
else:
bad_crash = True
# Retrieve the policy
policy = value_iteration(racetrack, bad_crash = bad_crash,
no_training_iter=no_training_iter)
for each_race in range(races):
total_steps += do_time_trial(racetrack, policy, bad_crash = (
bad_crash), animate = True)
print("Number of Training Iterations: " + str(no_training_iter))
if crash_scenario == 1:
print("Bad Crash Scenario: " + option_1)
else:
print("Bad Crash Scenario: " + option_2)
print("Average Number of Steps the Race Car Needs to Take Before " +
"Finding the Finish Line: " + str(total_steps/races) + " steps\n")
print("\nThe race car is training. Please wait...")
# Delay
time.sleep(FRAME_TIME + 4)
# Testing statistics
test_stats_file = THIS_TRACK
test_stats_file += "_"
test_stats_file += ALGORITHM_NAME + "_iter"
test_stats_file += str(no_training_iter)+ "_cr"
test_stats_file += str(crash_scenario) + "_stats.txt"
## Open a test_stats_file
outfile_ts = open(test_stats_file,"w")
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write(ALGORITHM_NAME + " Summary Statistics\n")
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write("Track: ")
outfile_ts.write(THIS_TRACK)
outfile_ts.write("\nNumber of Training Iterations: " + str(no_training_iter))
if crash_scenario == 1:
outfile_ts.write("\nBad Crash Scenario: " + option_1 + "\n")
else:
outfile_ts.write("Bad Crash Scenario: " + option_2 + "\n")
outfile_ts.write("Average Number of Steps the Race Car Took " +
"Before Finding the Finish Line: " + str(total_steps/races) +
" steps\n")
# Show functioning of the program
trace_runs_file = THIS_TRACK
trace_runs_file += "_"
trace_runs_file += ALGORITHM_NAME + "_iter"
trace_runs_file += str(no_training_iter) + "_cr"
trace_runs_file += str(crash_scenario) + "_trace.txt"
if no_training_iter <= 5:
## Open a new file to save trace runs
outfile_tr = open(trace_runs_file,"w")
# Print trace runs that demonstrate proper functioning of the code
outfile_tr.write(str(policy))
outfile_tr.close()
## Close the files
outfile_ts.close()
no_training_iter += 5
main()
Q-Learning Code in Python
And here is the code for Q-Learning. Again, don’t be scared by how long this code is. I included a lot of comments so that you know what is going on at each step of the code (just copy and paste this into your favorite IDE!):
import os # Library enables the use of operating system dependent functionality
import time # Library handles time-related tasks
from random import shuffle # Import shuffle() method from the random module
from random import random # Import random() method from the random module
from copy import deepcopy # Enable deep copying
import numpy as np # Import Numpy library
# File name: q_learning.py
# Author: Addison Sears-Collins
# Date created: 8/14/2019
# Python version: 3.7
# Description: Implementation of the Q-learning reinforcement learning
# algorithm for the racetrack problem
# The racetrack problem is described in full detail in:
# Barto, A. G., Bradtke, S. J., Singh, S. P. (1995). Learning to Act Using
# Real-time Dynamic Programming. Artificial Intelligence, 72(1-2):81–138.
# and
# Sutton, Richard S., and Andrew G. Barto. Reinforcement learning :
# An Introduction. Cambridge, Massachusetts: The MIT Press, 2018. Print.
# (modified version of Exercise 5.12 on pg. 111)
# Define constants
ALGORITHM_NAME = "Q_Learning"
FILENAME = "L-track.txt"
THIS_TRACK = "L_track"
START = 'S'
GOAL = 'F'
WALL = '#'
TRACK = '.'
MAX_VELOCITY = 5
MIN_VELOCITY = -5
DISC_RATE = 0.9 # Discount rate, also known as gamma. Determines by how much
# we discount the value of a future state s'
ERROR_THRES = 0.001 # Determine when Q-values stabilize (i.e.theta)
PROB_ACCELER_FAILURE = 0.20 # Probability car will try to take action a
# according to policy pi(s) = a and fail.
PROB_ACCELER_SUCCESS = 1 - PROB_ACCELER_FAILURE
NO_TRAINING_ITERATIONS = 500000 # A single training iteration runs through all
# possible states s
TRAIN_ITER_LENGTH = 10 # The maximum length of each training iteration
MAX_TRAIN_ITER = 10000000 # Maximum number of training iterations
NO_RACES = 10 # How many times the race car does a single time trial from
# starting position to the finish line
LEARNING_RATE = 0.25 # If applicable, also known as alpha
MAX_STEPS = 500 # Maximum number of steps the car can take during time trial
FRAME_TIME = 0.1 # How many seconds between frames printed to the console
# Range of the velocity of the race car in both y and x directions
vel_range = range(MIN_VELOCITY, MAX_VELOCITY + 1)
# Actions the race car can take
# (acceleration in y direction, acceleration in x direction)
actions = [(-1,-1), (0,-1), (1,-1),
(-1,0) , (0,0), (1,0),
(-1,1) , (0,1), (1,1)]
def read_environment(filename):
"""
This method reads in the environment (i.e. racetrack)
:param str filename
:return environment: list of lists
:rtype list
"""
# Open the file named filename in READ mode.
# Make the file an object named 'file'
with open(filename, 'r') as file:
# Read until end of file using readline()
# readline() then returns a list of the lines
# of the input file
environment_data = file.readlines()
# Close the file
file.close()
# Initialize an empty list
environment = []
# Adds a counter to each line in the environment_data list,
# i is the index of each line and line is the actual contents.
# enumerate() helps get not only the line of the environment but also
# the index of that line (i.e. row)
for i,line in enumerate(environment_data):
# Ignore the first line that contains the dimensions of the racetrack
if i > 0:
# Remove leading or trailing whitespace if applicable
line = line.strip()
# If the line is empty, ignore it
if line == "": continue
# Creates a list of lines. Within each line is a list of
# individual characters
# The stuff inside append(stuff) first creates a new_list = []
# It then appends all the values in a given line to that new
# list (e.g. new_list.append(all values inside the line))
# Then we append that new list to the environment list.
# Therefoer, environment is a list of lists.
environment.append([x for x in line])
# Return the environment (i.e. a list of lists/lines)
return environment
def print_environment(environment, car_position = [0,0]):
"""
This method reads in the environment and current
(y,x) position of the car and prints the environment to the console
:param list environment
:param list car_position
"""
# Store value of current grid square
temp = environment[car_position[0]][car_position[1]]
# Move the car to current grid square
environment[car_position[0]][car_position[1]] = "X"
# Delay
time.sleep(FRAME_TIME)
# Clear the printed output
clear()
# For each line in the environment
for line in environment:
# Initialize a string
text = ""
# Add each character to create a line
for character in line:
text += character
# Print the line of the environment
print(text)
# Retstore value of current grid square
environment[car_position[0]][car_position[1]] = temp
def clear():
"""
This method clears the print output
"""
os.system( 'cls' )
def get_random_start_position(environment):
"""
This method reads in the environment and selects a random
starting position on the racetrack (y, x). Note that
(0,0) corresponds to the upper left corner of the racetrack.
:param list environment: list of lines
:return random starting coordinate (y,x) on the racetrack
:rtype tuple
"""
# Collect all possible starting positions on the racetrack
starting_positions = []
# For each row in the environment
for y,row in enumerate(environment):
# For each column in each row of the environment
for x,col in enumerate(row):
# If we are at the starting position
if col == START:
# Add the coordiante to the list of available
# starting positions in the environment
starting_positions += [(y,x)]
# Random shuffle the list of starting positions
shuffle(starting_positions)
# Select a starting position
return starting_positions[0]
def act(old_y, old_x, old_vy, old_vx, accel, environment, deterministic=(
False),bad_crash = False):
"""
This method generates the new state s' (position and velocity) from the old
state s and the action a taken by the race car. It also takes as parameters
the two different crash scenarios (i.e. go to nearest
open position on the race track or go back to start)
:param int old_y: The old y position of the car
:param int old_x: The old x position of the car
:param int old_vy: The old y velocity of the car
:param int old_vx: The old x velocity of the car
:param tuple accel: (ay,ax) - acceleration in y and x directions
:param list environment: The racetrack
:param boolean deterministic: True if we always follow the policy
:param boolean bad_crash: True if we return to start after crash
:return s' where s' = new_y, new_x, new_vy, and new_vx
:rtype int
"""
# This method is deterministic if the same output is returned given
# the same input information
if not deterministic:
# If action fails (car fails to take the prescribed action a)
if random() > PROB_ACCELER_SUCCESS:
#print("Car failed to accelerate!")
accel = (0,0)
# Using the old velocity values and the new acceleration values,
# get the new velocity value
new_vy, new_vx = get_new_velocity((old_vy,old_vx), accel)
# Using the new velocity values, update with the new position
temp_y, temp_x = get_new_position((old_y,old_x), (new_vy, new_vx),(
environment))
# Find the nearest open cell on the racetrack to this new position
new_y, new_x = get_nearest_open_cell(environment, temp_y, temp_x, new_vy,
new_vx)
# If a crash happens (i.e. new position is not equal to the nearest
# open position on the racetrack
if new_y != temp_y or new_x != temp_x:
# If this is a crash in which we would have to return to the
# starting position of the racetrack and we are not yet
# on the finish line
if bad_crash and environment[new_y][new_x] != GOAL:
# Return to the start of the race track
new_y, new_x = get_random_start_position(environment)
# Velocity of the race car is set to 0.
new_vy, new_vx = 0,0
# Return the new state
return new_y, new_x, new_vy, new_vx
def get_new_position(old_loc, vel, environment):
"""
Get a new position using the old position and the velocity
:param tuple old_loc: (y,x) position of the car
:param tuple vel: (vy,vx) velocity of the car
:param list environment
:return y+vy, x+vx: (new_y,new_x)
"""
y,x = old_loc[0], old_loc[1]
vy, vx = vel[0], vel[1]
# new_y = y+vy, new_x = x + vx
return y+vy, x+vx
def get_new_velocity(old_vel,accel,min_vel=MIN_VELOCITY,max_vel=MAX_VELOCITY):
"""
Get the new velocity values
:param tuple old_vel: (vy, vx)
:param tuple accel: (ay, ax)
:param int min_vel: Minimum velocity of the car
:param int max_vel: Maximum velocity of the car
:return new velocities in y and x directions
"""
new_y = old_vel[0] + accel[0]
new_x = old_vel[1] + accel[1]
if new_x < min_vel: new_x = min_vel
if new_x > max_vel: new_x = max_vel
if new_y < min_vel: new_y = min_vel
if new_y > max_vel: new_y = max_vel
# Return the new velocities
return new_y, new_x
def get_nearest_open_cell(environment, y_crash, x_crash, vy = 0, vx = (
0), open = [TRACK, START, GOAL]):
"""
Locate the nearest open cell in order to handle crash scenario.
Distance is calculated as the Manhattan distance.
Start from the crash grid square and expand outward from there with
a radius of 1, 2, 3, etc. Forms a diamond search pattern.
For example, a Manhattan distance of 2 would look like the following:
.
...
..#..
...
.
If velocity is provided, search in opposite direction of velocity so that
there is no movement over walls
:param list environment
:param int ycrash: y coordinate where crash happened
:param int xcrash: x coordinate where crash happened
:param int vy: velocity in y direction when crash occurred
:param int vx: velocity in x direction when crash occurred
:param list of strings open: Contains environment types
:return tuple of the nearest open y and x position on the racetrack
"""
# Record number of rows (lines) and columns in the environment
rows = len(environment)
cols = len(environment[0])
# Add expanded coverage for searching for nearest open cell
max_radius = max(rows,cols)
# Generate a search radius for each scenario
for radius in range(max_radius):
# If car is not moving in y direction
if vy == 0:
y_off_range = range(-radius, radius + 1)
# If the velocity in y-direction is negative
elif vy < 0:
# Search in the positive direction
y_off_range = range(0, radius + 1)
else:
# Otherwise search in the negative direction
y_off_range = range(-radius, 1)
# For each value in the search radius range of y
for y_offset in y_off_range:
# Start near to crash site and work outwards from there
y = y_crash + y_offset
x_radius = radius - abs(y_offset)
# If car is not moving in x direction
if vx == 0:
x_range = range(x_crash - x_radius, x_crash + x_radius + 1)
# If the velocity in x-direction is negative
elif vx < 0:
x_range = range(x_crash, x_crash + x_radius + 1)
# If the velocity in y-direction is positive
else:
x_range = range(x_crash - x_radius, x_crash + 1)
# For each value in the search radius range of x
for x in x_range:
# We can't go outside the environment(racetrack) boundary
if y < 0 or y >= rows: continue
if x < 0 or x >= cols: continue
# If we find and open cell, return that (y,x) open cell
if environment[y][x] in open:
return(y,x)
# No open grid squares found on the racetrack
return
def get_policy_from_Q(cols, rows, vel_range, Q, actions):
"""
This method returns the policy pi(s) based on the action taken in each state
that maximizes the value of Q in the table Q[s,a]. This is pi*(s)...the
best action that the race car should take in each state is the one that
maximizes the value of Q. (* means optimal)
:param int cols: Number of columns in the environment
:param int rows: Number of rows (i.e. lines) in the environment
:param list vel_range: Range of the velocity of the race car
:param list of tuples actions: actions = [(ay,ax),(ay,ax)....]
:return pi : the policy
:rtype: dictionary: key is the state tuple, value is the
action tuple (ay,ax)
"""
# Create an empty dictionary called pi
pi = {}
# For each state s in the environment
for y in range(rows):
for x in range(cols):
for vy in vel_range:
for vx in vel_range:
# Store the best action for each state...the one that
# maximizes the value of Q.
# argmax looks across all actions given a state and
# returns the index ai of the maximum Q value
pi[(y,x,vy,vx)] = actions[np.argmax(Q[y][x][vy][vx])]
# Return pi(s)
return(pi)
def q_learning(environment, bad_crash = False, reward = 0.0, no_training_iter =(
NO_TRAINING_ITERATIONS), train_iter_length = TRAIN_ITER_LENGTH):
"""
Return a policy pi that maps states to actions
Each episode uses a different initial state. This forces the agent to fully
explore the environment to create a more informed Q[s,a] table.
:param list environment
:param boolean bad_crash
:param int reward of the terminal states (i.e. finish line)
:param int no_training_iter
:param int train_iter_length
:return policy pi(s) which maps a given state to an optimal action
:rtype dictionary
"""
rows = len(environment)
cols = len(environment[0])
# Initialize all Q(s,a) to arbitrary values, except the terminal state
# (i.e. finish line states) that has a value of 0.
# Q[y][x][vy][vx][ai]
Q = [[[[[random() for _ in actions] for _ in vel_range] for _ in (
vel_range)] for _ in line] for line in environment]
# Set finish line state-action pairs to 0
for y in range(rows):
for x in range(cols):
# Terminal state has a value of 0
if environment[y][x] == GOAL:
for vy in vel_range:
for vx in vel_range:
for ai, a in enumerate(actions):
Q[y][x][vy][vx][ai] = reward
# We run many training iterations for different initial states in order
# to explore the environment as much as possible
for iter in range(no_training_iter):
# Reset all the terminal states to the value of the goal
for y in range(rows):
for x in range(cols):
if environment[y][x] == GOAL:
Q[y][x] = [[[reward for _ in actions] for _ in (
vel_range)] for _ in vel_range]
# Select a random initial state
# from anywhere on the racetrack
y = np.random.choice(range(rows))
x = np.random.choice(range(cols))
vy = np.random.choice(vel_range)
vx = np.random.choice(vel_range)
# Do a certain number of iterations for each episode
for t in range(train_iter_length):
if environment[y][x] == GOAL:
break
if environment[y][x] == WALL:
break
# Choose the best action for the state s
a = np.argmax(Q[y][x][vy][vx])
# Act and then observe a new state s'
new_y, new_x, new_vy, new_vx = act(y, x, vy, vx, actions[
a], environment, bad_crash = bad_crash)
r = -1
# Update the Q table based on the immediate reward received from
# taking action a in state s plus the discounted future reward
Q[y][x][vy][vx][a] = ((1 - LEARNING_RATE)*Q[y][x][vy][vx][a] +
LEARNING_RATE*(r + DISC_RATE*max(Q[new_y][new_x][
new_vy][new_vx])))
# The new state s' now becomes s
y, x, vy, vx = new_y, new_x, new_vy, new_vx
return(get_policy_from_Q(cols, rows, vel_range, Q, actions))
def do_time_trial(environment, policy, bad_crash = False, animate = True,
max_steps = MAX_STEPS):
"""
Race car will do a time trial on the race track according to the policy.
:param list environment
:param dictionary policy: A dictionary containing the best action for a
given state. The key is the state y,x,vy,vx and value is the action
(ax,ay) acceleration
:param boolean bad_crash: The crash scenario. If true, race car returns to
starting position after crashes
:param boolean animate: If true, show the car on the racetrack at each
timestep
:return i: Total steps to complete race (i.e. from starting position to
finish line)
:rtype int
"""
# Copy the environment
environment_display = deepcopy(environment)
# Get a starting position on the race track
starting_pos = get_random_start_position(environment)
y,x = starting_pos
vy,vx = 0,0 # We initialize velocity to 0
# Keep track if we get stuck
stop_clock = 0
# Begin time trial
for i in range(max_steps):
# Show the race car on the racetrack
if animate:
print_environment(environment_display, car_position = [y,x])
# Get the best action given the current state
a = policy[(y,x,vy,vx)]
# If we are at the finish line, stop the time trial
if environment[y][x] == GOAL:
return i
# Take action and get new a new state s'
y,x,vy,vx = act(y, x, vy, vx, a, environment, bad_crash = bad_crash)
# Determine if the car gets stuck
if vy == 0 and vx == 0:
stop_clock += 1
else:
stop_clock = 0
# We have gotten stuck as the car has not been moving for 5 timesteps
if stop_clock == 5:
return max_steps
# Program has timed out
return max_steps
def main():
"""
The main method of the program
"""
print("Welcome to the Racetrack Control Program!")
print("Powered by the " + ALGORITHM_NAME +
" Reinforcement Learning Algorithm\n")
print("Track: " + THIS_TRACK)
print()
print("What happens to the car if it crashes into a wall?")
option_1 = """1. Starts from the nearest position on the track to the
place where it crashed."""
option_2 = """2. Returns back to the original starting position."""
print(option_1)
print(option_2)
crash_scenario = int(input("Crash scenario (1 or 2): "))
no_training_iter = int(input(
"Enter the initial number of training iterations (e.g. 500000): "))
print("\nThe race car is training. Please wait...")
# Directory where the racetrack is located
#racetrack_name = input("Enter the path to your input file: ")
racetrack_name = FILENAME
racetrack = read_environment(racetrack_name)
# How many times the race car will do a single time trial
races = NO_RACES
while(no_training_iter <= MAX_TRAIN_ITER):
# Keep track of the total number of steps
total_steps = 0
# Record the crash scenario
bad_crash = False
if crash_scenario == 1:
bad_crash = False
else:
bad_crash = True
# Retrieve the policy
policy = q_learning(racetrack, bad_crash = (
bad_crash),no_training_iter=no_training_iter)
for each_race in range(races):
total_steps += do_time_trial(racetrack, policy, bad_crash = (
bad_crash), animate = False)
print("Number of Training Iterations: " + str(no_training_iter))
if crash_scenario == 1:
print("Bad Crash Scenario: " + option_1)
else:
print("Bad Crash Scenario: " + option_2)
print("Average Number of Steps the Race Car Needs to Take Before " +
"Finding the Finish Line: " + str(total_steps/races) + " steps\n")
print("\nThe race car is training. Please wait...")
# Delay
time.sleep(FRAME_TIME + 4)
# Testing statistics
test_stats_file = THIS_TRACK
test_stats_file += "_"
test_stats_file += ALGORITHM_NAME + "_iter"
test_stats_file += str(no_training_iter)+ "_cr"
test_stats_file += str(crash_scenario) + "_stats.txt"
## Open a test_stats_file
outfile_ts = open(test_stats_file,"w")
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write(ALGORITHM_NAME + " Summary Statistics\n")
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write("Track: ")
outfile_ts.write(THIS_TRACK)
outfile_ts.write("\nNumber of Training Iterations: " + str(no_training_iter))
if crash_scenario == 1:
outfile_ts.write("\nBad Crash Scenario: " + option_1 + "\n")
else:
outfile_ts.write("Bad Crash Scenario: " + option_2 + "\n")
outfile_ts.write("Average Number of Steps the Race Car Took " +
"Before Finding the Finish Line: " + str(total_steps/races) +
" steps\n")
# Show functioning of the program
trace_runs_file = THIS_TRACK
trace_runs_file += "_"
trace_runs_file += ALGORITHM_NAME + "_iter"
trace_runs_file += str(no_training_iter) + "_cr"
trace_runs_file += str(crash_scenario) + "_trace.txt"
if no_training_iter <= 5:
## Open a new file to save trace runs
outfile_tr = open(trace_runs_file,"w")
# Print trace runs that demonstrate proper functioning of the code
outfile_tr.write(str(policy))
outfile_tr.close()
## Close the files
outfile_ts.close()
if no_training_iter == 500000:
no_training_iter += 500000
else:
no_training_iter += 1000000
main()
Hypothesis 1: Both Algorithms Will Enable the Car to Finish the Race
Both the value iteration and
Q-learning algorithms were successfully able to finish the race for both crash
scenarios and for both the R-track and the L-track. The race car completed the
R-track in fewer time steps than the L-track, which was expected.
The
crash policy in which the car had to return to the starting position after it
crashed versus going to the nearest open position negatively impacted
performance for both algorithms. Time trial performance was superior for the
value iteration algorithm compared to the Q learning algorithm. This result was
expected given that the value iteration algorithm had access to the transition
and the reward probabilities during the training phase. In other words, value
iteration it gets a complete model of the entire track, whereas Q-learning has
to be forced to explore the environment in order to learn.
Hypothesis 2: Value Iteration Will Learn Faster Than Q-Learning
My hypothesis was correct. Value
iteration generated a learning policy faster than Q-learning because it had
access to the transition and reward functions. Q-learning required more than
6,000,000 training iterations on the R-track to achieve the time trial
performance that the value iteration algorithm was able to produce in only 45
training iterations (holding all other hyperparameters constant). And whereas
the Q-learning algorithm took 2-3 hours to train to get good time trial
results, value iteration never took more than 10 minutes for both the R-track
and L-track under both of the bad crash scenarios.
Hypothesis 3: Bad Crash Version 1 Will Outperform Bad Crash Version 2
The results show that my
hypothesis was correct. It took longer for the car to finish the race for the
crash scenario in which the race car needed to return to the original starting
position each time it crashed into a wall. In other words, Bad Crash Version 1
(return to start) performance was better than Bad Crash Version 2 (return to
nearest open position) performance. These results are what I expected since
always returning to the starting position after a crash hampers the ability of
the race car to continually explore new states.
Summary and Conclusions
My hypotheses were correct. Here
are the following conclusions:
Value iteration and Q-learning are powerful
reinforcement learning algorithms that can enable an agent to learn
autonomously.
Value iteration led to faster learning than the
Q-learning algorithm.
A crash policy in which the race car always
returns to the starting position after a crash negatively impacts performance.
In this post, I will walk you through how to build an artificial feedforward neural network trained with backpropagation, step-by-step. We will not use any fancy machine learning libraries, only basic Python libraries like Pandas and Numpy.
Our end goal is to evaluate the performance of an artificial feedforward neural network trained with backpropagation and to compare the performance using no hidden layers, one hidden layer, and two hidden layers. Five different data sets from the UCI Machine Learning Repository are used to compare performance: Breast Cancer, Glass, Iris, Soybean (small), and Vote.
We will use our neural network to do the following:
Predict if someone has breast cancer
Identify glass type
Identify flower species
Determine soybean disease type
Classify a representative as either a Democrat or Republican based on their voting patterns
I hypothesize that the neural networks with no hidden layers will outperform the networks with two hidden layers. My hypothesis is based on the notion that the simplest solutions are often the best solutions (i.e. Occam’s Razor).
What is an Artificial Feedforward Neural Network Trained with Backpropagation?
Background
An artificial feed-forward neural network (also known as multilayer perceptron) trained with backpropagation is an old machine learning technique that was developed in order to have machines that can mimic the brain. Neural networks were the focus of a lot of machine learning research during the 1980s and early 1990s but declined in popularity during the late 1990s.
Since 2010, neural networks have experienced a resurgence in popularity due to improvements in computing speed and the availability of massive amounts of data with which to train large-scale neural networks. In the real world, neural networks have been used to recognize speech, caption images, and even help self-driving cars learn how to park autonomously.
The Brain as Inspiration for Artificial Neural Networks
In
order to understand neural networks, it helps to first take a look at the basic
architecture of the human brain. The brain has 1011 neurons (Alpaydin,
2014). Neurons
are cells inside the brain that process information.
Each neuron contains a number of input wires called dendrites. Each neuron also has one output wire called an axon. The axon is used to send messages to other neurons. The axon of a sending neuron is connected to the dendrites of the receiving neuron via a synapse.
So,
in short, a neuron receives inputs from dendrites, performs a computation, and
sends the output to other neurons via the axon. This process is how information
flows through the brain. The messages sent between neurons are in the form of
electric pulses.
An
artificial neural network, the one used in machine learning, is a simplified
model of the actual human neural network explained above. It is typically
composed of zero or more layers.
Each
layer of the neural network is made up of nodes (analogous to neurons in the
brain). Nodes of one layer are connected to nodes in another layer by
connection weights, which are typically just floating-point numbers (e.g.
0.23342341). These numbers represent the strength of the connection between two
nodes.
The
job of a node in a hidden layer is to:
Receive
input values from each node in a preceding layer
Compute
a weighted sum of those input values
Send
that weighted sum through some activation function (e.g. logistic sigmoid
function or hyperbolic tangent function)
Send
the result of the computation in #3 to each node in the next layer of the
neural network.
Thus,
the output from the nodes in a given layer becomes the input for all nodes in the
next layer.
The
output layer of a network does steps 1-3 above. However, the result of the
computation from step #3 is a class prediction instead of an input to another
layer (since the output layer is the final layer).
Here is a diagram of the process I explained above:
Here is a diagram showing a single layer neural network:
b stands for the bias term. This is a constant. It is like the b in the equation for a line, y = mx + b. It enables the model to have flexibility because, without that bias term, you cannot as easily adapt the weighted sum of inputs (i.e. mx) to fit the data (i.e. in the example of a simple line, the line cannot move up and down the y-axis without that b term).
w in the diagram above stands for the weights, and x stands for the input values.
Here is a similar diagram, but now it is a two-layer neural network instead of single layer.
And here is one last way to look at the same thing I explained above:
Note that the yellow circles on the left represent the input values. w represents the weights. The sigma inside the box means that we calculated the weighted sum of the input values. We run that through the activation function f(S)…e.g. sigmoid function. And then out of that, pops the output, which is passed on to the nodes in the following layer.
Neural networks that contain many layers, for example more than 100, are called deep neural networks. Deep neural networks are the cornerstone of the rapidly growing field known as deep learning.
Training Phase
The objective during the training phase of a neural network is to determine all the connection weights. At the start of training, the weights of the network are initialized to small random values close to 0. After this step, training proceeds to the two main phases of the algorithm: forward propagation and backpropagation.
Forward Propagation
During
the forward propagation phase of a neural network, we process one instance (i.e.
one set of inputs) at a time. Hidden layers extract important features
contained in the input data by computing a weighted sum of the inputs and
running the result through the logistic sigmoid activation function. This
output relays to nodes in the next hidden layer where the data is transformed
yet again. This process continues until the data reaches the output layer.
The
output of the output layer is a predicted class value, which in this project is
a one-hot encoded class prediction vector. The index of the vector corresponds
to each class. For example, if a 1 is in the 0 index of the vector (and a 0 is in
all other indices of the vector), the class prediction is class 0. Because we
are dealing with 0s and 1s, the output vector can also be considered the
probability that an instance is in a given class.
Backpropagation
After
the input signal produced by a training instance propagates through the network
one layer at a time to the output layer, the backpropagation phase commences.
An error value is calculated at the output layer. This error corresponds to the
difference between the class predicted by the network and the actual (i.e.
true) class of the training instance.
The
prediction error is then propagated backward from the output layer to the input
layer. Blame for the error is assigned to each node in each layer, and then the
weights of each node of the neural network are updated accordingly (with the
goal to make more accurate class predictions for the next instance that flows
through the neural network) using stochastic gradient descent for the weight
optimization procedure.
Note
that weights of the neural network are adjusted on a training instance by
training instance basis. This online learning method is the preferred one for
classification problems of large size (Ĭordanov
& Jain, 2013).
The
forward propagation and backpropagation phases continue for a certain number of
epochs. A single epoch finishes when each training instance has been processed
exactly once.
Testing Phase
Once the neural network has been trained, it can be used to make predictions on new, unseen test instances. Test instances flow through the network one-by-one, and the resulting output (which is a vector of class probabilities) determines the classification.
Helpful Video
Below is a helpful video by Andrew Ng, a professor at Stanford University, that explains neural networks and is helpful for getting your head around the math. The video gets pretty complicated in some spots (esp. where he starts writing all sorts of mathematical notation and derivatives). My advice is to lookup anything that he explains that isn’t clear. Take it slow as you are learning about neural networks. There is no rush. This stuff isn’t easy to understand on your first encounter with it. Over time, the fog will begin to lift, and you will be able to understand how it all works.
Required Data Set Format for Feedforward Neural Network Trained with Backpropagation
Columns (0 through N)
0: Instance ID
1: Attribute 1
2: Attribute 2
3: Attribute 3
…
N: Actual Class
The program then adds two
additional columns for the testing set.
N + 1: Predicted Class
N + 2: Prediction Correct? (1 if yes, 0 if no)
Breast Cancer Data Set
This breast cancer data set
contains 699 instances, 10 attributes, and a class – malignant or benign (Wolberg,
1992).
Modification of Attribute Values
The actual class value was changed to “Benign”
or “Malignant.”
Attribute values were normalized to be in the
range 0 to 1.
Class values were vectorized using one-hot
encoding.
Missing Data
There were 16 missing attribute values, each denoted with a “?”. I
chose a random number between 1 and 10 (inclusive) to fill in the data.
Glass Data Set
This glass data set contains 214
instances, 10 attributes, and 7 classes (German, 1987). The purpose of the data set is to
identify the type of glass.
Modification of Attribute Values
Attribute values were normalized to be in the
range 0 to 1.
Class values were vectorized using one-hot
encoding.
Missing Data
There are no missing values in this data set.
Iris Data Set
This data set contains 3 classes
of 50 instances each (150 instances in total), where each class refers to a
different type of iris plant (Fisher, 1988).
Modification of Attribute Values
Attribute values were normalized to be in the
range 0 to 1.
Class values were vectorized using one-hot
encoding.
Missing Data
There were no missing attribute values.
Soybean Data Set (small)
This soybean (small) data set
contains 47 instances, 35 attributes, and 4 classes (Michalski, 1980). The purpose of the data set is to
determine the disease type.
Modification of Attribute Values
Attribute values were normalized to be in the
range 0 to 1.
Class values were vectorized using one-hot
encoding.
Attribute values that were all the same value were
removed.
Missing Data
There are no missing values in this data set.
Vote Data Set
This data set includes votes for
each of the U.S. House of Representatives Congressmen (435 instances) on the 16
key votes identified by the Congressional Quarterly Almanac (Schlimmer, 1987). The purpose of the data set is to identify
the representative as either a Democrat or Republican.
267 Democrats
168 Republicans
Modification of Attribute Values
I did the following modifications:
Changed all “y” to 1 and all “n” to 0.
Class values were vectorized using one-hot
encoding.
Missing Data
Missing values were denoted as “?”. To fill in
those missing values, I chose random number, either 0 (“No”) or 1 (“Yes”).
Stochastic Gradient Descent
I used stochastic gradient descent for optimizing the
weights.
In normal gradient descent, we need
to calculate the partial derivative of the cost function with respect to each
weight. For each partial derivative, we have to tally up the terms for each training
instance to compute the partial derivative of the cost function with respect to
that weight. What this means is that, if we have a lot of attributes and a
large dataset, gradient descent is slow. For this reason, stochastic gradient
descent was chosen since weights are updated after each training instance (as
opposed to after all training instances).
Here is a good video that explains stochastic gradient descent.
Some tuning was performed in this
project. The learning rate was set to 0.1, which was different than the 0.01
value that is often used for multi-layer feedforward neural networks (Montavon, 2012). Lower values resulted in much
longer training times and did not result in large improvements in
classification accuracy.
Epochs
The number of epochs chosen for
the main runs of the algorithm on the data sets was chosen to be 1000. Other
values were tested, but the number of epochs did not have a large impact on classification
accuracy.
Number of Nodes per Hidden Layer
In order to tune the number of
nodes per hidden layer, I used a constant learning rate and constant number of
epochs. I then calculated the classification accuracy for each data set for a
set number of nodes per hidden layer. I performed this process using networks
with one hidden layer and networks with two hidden layers. The results of this
tuning process are below.
Note that the mean classification
accuracy across all data sets when one hidden layer was used for the neural
network reached a peak at eight nodes per hidden layer. This value of eight nodes
per hidden layer was used for the actual runs on the data sets.
For two hidden layers, the peak
mean classification accuracy was attained at five nodes per hidden layer. Thus,
when the algorithm was run on the data sets for two hidden layers, I used five
nodes per hidden layer for each data set to compare the classification accuracy
across the data sets.
Here is the full code for the neural network. This is all you need to run the program:
import pandas as pd # Import Pandas library
import numpy as np # Import Numpy library
from random import shuffle # Import shuffle() method from the random module
from random import seed # Import seed() method from the random module
from random import random # Import random() method from the random module
from collections import Counter # Used for counting
from math import exp # Import exp() function from the math module
# File name: neural_network.py
# Author: Addison Sears-Collins
# Date created: 7/30/2019
# Python version: 3.7
# Description: An artificial feedforward neural network trained
# with backpropagation (also called multilayer perceptron)
# Required Data Set Format
# Columns (0 through N)
# 0: Attribute 0
# 1: Attribute 1
# 2: Attribute 2
# 3: Attribute 3
# ...
# N: Actual Class
# 2 additional columns are added for the test set.
# N + 1: Predicted Class
# N + 2: Prediction Correct?
ALGORITHM_NAME = "Feedforward Neural Network With Backpropagation"
SEPARATOR = "," # Separator for the data set (e.g. "\t" for tab data)
def normalize(dataset):
"""
Normalize the attribute values so that they are between 0 and 1, inclusive
:param pandas_dataframe dataset: The original dataset as a Pandas dataframe
:return: normalized_dataset
:rtype: Pandas dataframe
"""
# Generate a list of the column names
column_names = list(dataset)
# For every column except the actual class column
for col in range(0, len(column_names) - 1):
temp = dataset[column_names[col]] # Go column by column
minimum = temp.min() # Get the minimum of the column
maximum = temp.max() # Get the maximum of the column
# Normalized all values in the column so that they
# are between 0 and 1.
# x_norm = (x_i - min(x))/(max(x) - min(x))
dataset[column_names[col]] = dataset[column_names[col]] - minimum
dataset[column_names[col]] = dataset[column_names[col]] / (
maximum - minimum)
normalized_dataset = dataset
return normalized_dataset
def get_five_stratified_folds(dataset):
"""
Implementation of five-fold stratified cross-validation. Divide the data
set into five random folds. Make sure that the proportion of each class
in each fold is roughly equal to its proportion in the entire data set.
:param pandas_dataframe dataset: The original dataset as a Pandas dataframe
:return: five_folds
:rtype: list of folds where each fold is a list of instances(i.e. examples)
"""
# Create five empty folds
five_folds = list()
fold0 = list()
fold1 = list()
fold2 = list()
fold3 = list()
fold4 = list()
# Get the number of columns in the data set
class_column = len(dataset[0]) - 1
# Shuffle the data randomly
shuffle(dataset)
# Generate a list of the unique class values and their counts
classes = list() # Create an empty list named 'classes'
# For each instance in the dataset, append the value of the class
# to the end of the classes list
for instance in dataset:
classes.append(instance[class_column])
# Create a list of the unique classes
unique_classes = list(Counter(classes).keys())
# For each unique class in the unique class list
for uniqueclass in unique_classes:
# Initialize the counter to 0
counter = 0
# Go through each instance of the data set and find instances that
# are part of this unique class. Distribute them among one
# of five folds
for instance in dataset:
# If we have a match
if uniqueclass == instance[class_column]:
# Allocate instance to fold0
if counter == 0:
# Append this instance to the fold
fold0.append(instance)
# Increase the counter by 1
counter += 1
# Allocate instance to fold1
elif counter == 1:
# Append this instance to the fold
fold1.append(instance)
# Increase the counter by 1
counter += 1
# Allocate instance to fold2
elif counter == 2:
# Append this instance to the fold
fold2.append(instance)
# Increase the counter by 1
counter += 1
# Allocate instance to fold3
elif counter == 3:
# Append this instance to the fold
fold3.append(instance)
# Increase the counter by 1
counter += 1
# Allocate instance to fold4
else:
# Append this instance to the fold
fold4.append(instance)
# Reset the counter to 0
counter = 0
# Shuffle the folds
shuffle(fold0)
shuffle(fold1)
shuffle(fold2)
shuffle(fold3)
shuffle(fold4)
# Add the five stratified folds to the list
five_folds.append(fold0)
five_folds.append(fold1)
five_folds.append(fold2)
five_folds.append(fold3)
five_folds.append(fold4)
return five_folds
def initialize_neural_net(
no_inputs, no_hidden_layers, no_nodes_per_hidden_layer, no_outputs):
"""
Generates a new neural network that is ready to be trained.
Network (list of layers): 0+ hidden layers, and output layer
Input Layer (list of attribute values): A row from the training set
Hidden Layer (list of dictionaries): A set of nodes (i.e. neurons)
Output Layer (list of dictionaries): A set of nodes, one node per class
Node (dictionary): Contains a set of weights, one weight for each input
to the layer containing that node + an additional weight for the bias.
Each node is represented as a dictionary that stores key-value pairs
Each key corresponds to a property of that node (e.g. weights).
Weights will be initialized to small random values between 0 and 1.
:param int no_inputs: Numper of inputs (i.e. attributes)
:param int no_hidden_layers: Numper of hidden layers (0 or more)
:param int no_nodes_per_hidden_layer: Numper of nodes per hidden layer
:param int no_outputs: Numper of outputs (one output node per class)
:return: network
:rtype:list (i.e. list of layers: hidden layers, output layer)
"""
# Create an empty list
network = list()
# Create the the hidden layers
hidden_layer = list()
hl_counter = 0
# Create the output layer
output_layer = list()
# If this neural network contains hidden layers
if no_hidden_layers > 0:
# Build one hidden layer at a time
for layer in range(no_hidden_layers):
# Reset to an empty hidden layer
hidden_layer = list()
# If this is the first hidden layer
if hl_counter == 0:
# Build one node at a time
for node in range(no_nodes_per_hidden_layer):
initial_weights = list()
# Each node in the hidden layer has no_inputs + 1 weights,
# initialized to a random number in the range [0.0, 1.0)
for i in range(no_inputs + 1):
initial_weights.append(random())
# Add the node to the first hidden layer
hidden_layer.append({'weights':initial_weights})
# Finished building the first hidden layer
hl_counter += 1
# Add this first hidden layer to the front of the neural
# network
network.append(hidden_layer)
# If this is not the first hidden layer
else:
# Build one node at a time
for node in range(no_nodes_per_hidden_layer):
initial_weights = list()
# Each node in the hidden layer has
# no_nodes_per_hidden_layer + 1 weights, initialized to
# a random number in the range [0.0, 1.0)
for i in range(no_nodes_per_hidden_layer + 1):
initial_weights.append(random())
hidden_layer.append({'weights':initial_weights})
# Add this newly built hidden layer to the neural network
network.append(hidden_layer)
# Build the output layer
for outputnode in range(no_outputs):
initial_weights = list()
# Each node in the output layer has no_nodes_per_hidden_layer
# + 1 weights, initialized to a random number in
# the range [0.0, 1.0)
for i in range(no_nodes_per_hidden_layer + 1):
initial_weights.append(random())
# Add this output node to the output layer
output_layer.append({'weights':initial_weights})
# Add the output layer to the neural network
network.append(output_layer)
# A neural network has no hidden layers
else:
# Build the output layer
for outputnode in range(no_outputs):
initial_weights = list()
# Each node in the hidden layer has no_inputs + 1 weights,
# initialized to a random number in the range [0.0, 1.0)
for i in range(no_inputs + 1):
initial_weights.append(random())
# Add this output node to the output layer
output_layer.append({'weights':initial_weights})
network.append(output_layer)
# Finished building the initial neural network
return network
def weighted_sum_of_inputs(weights, inputs):
"""
Calculates the weighted sum of inputs plus the bias
:param list weights: A list of weights. Each node has a list of weights.
:param list inputs: A list of input values. These can be a single row
of attribute values or the output from nodes from the previous layer
:return: weighted_sum
:rtype: float
"""
# We assume that the last weight is the bias value
# The bias value is a special weight that does not multiply with an input
# value (or we could assume its corresponding input value is always 1)
# The bias is similar to the intercept constant b in y = mx + b. It enables
# a (e.g. sigmoid) curve to be shifted to create a better fit
# to the data. Without the bias term b, the line always goes through the
# origin (0,0) and cannot adapt as well to the data.
# In y = mx + b, we assume b * x_0 where x_0 = 1
# Initiate the weighted sum with the bias term. Assume the last weight is
# the bias term
weighted_sum = weights[-1]
for index in range(len(weights) - 1):
weighted_sum += weights[index] * inputs[index]
return weighted_sum
def sigmoid(weighted_sum_of_inputs_plus_bias):
"""
Run the weighted sum of the inputs + bias through
the sigmoid activation function.
:param float weighted_sum_of_inputs_plus_bias: Node summation term
:return: sigmoid(weighted_sum_of_inputs_plus_bias)
"""
return 1.0 / (1.0 + exp(-weighted_sum_of_inputs_plus_bias))
def forward_propagate(network, instance):
"""
Instances move forward through the neural network from one layer
to the next layer. At each layer, the outputs are calculated for each
node. These outputs are the inputs for the nodes in the next layer.
The last set of outputs is the output for the nodes in the output
layer.
:param list network: List of layers: 0+ hidden layers, 1 output layer
:param list instance (a single training/test instance from the data set)
:return: outputs
:rtype: list
"""
inputs = instance
# For each layer in the neural network
for layer in network:
# These will store the outputs for this layer
new_inputs = list()
# For each node in this layer
for node in layer:
# Calculate the weighted sum + bias term
weighted_sum = weighted_sum_of_inputs(node['weights'], inputs)
# Run the weighted sum through the activation function
# and store the result in this node's dictionary.
# Now the node's dictionary has two keys, weights and output.
node['output'] = sigmoid(weighted_sum)
# Used for debugging
#print(node)
# Add the output of the node to the new_inputs list
new_inputs.append(node['output'])
# Update the inputs list
inputs = new_inputs
# We have reached the output layer
outputs = inputs
return outputs
def sigmoid_derivative(output):
"""
The derivative of the sigmoid activation function with respect
to the weighted summation term of the node.
Formally (after a lot of calculus), this derivative is:
derivative = sigmoid(weighted_sum_of_inputs_plus_bias) *
(1 - sigmoid(weighted_sum_of_inputs_plus_bias))
= node_ouput * (1 - node_output)
This method is used during the backpropagation phase.
:param list output: Output of a node (generated during the forward
propagation phase)
:return: sigmoid_der
:rtype: float
"""
return output * (1.0 - output)
def back_propagate(network, actual):
"""
In backpropagation, the error is computed between the predicted output by
the network and the actual output as determined by the data set. This error
propagates backwards from the output layer to the first hidden layer. The
weights in each layer are updated along the way in response to the error.
The goal is to reduce the prediction error for the next training instance
that forward propagates through the network.
:param network list: The neural network
:param actual list: A list of the actual output from the data set
"""
# Iterate in reverse order (i.e. starts from the output layer)
for i in reversed(range(len(network))):
# Work one layer at a time
layer = network[i]
# Keep track of the errors for the nodes in this layer
errors = list()
# If this is a hidden layer
if i != len(network) - 1:
# For each node_j in this hidden layer
for j in range(len(layer)):
# Reset the error value
error = 0.0
# Calculate the weighted error.
# The error values come from the error (i.e. delta) calculated
# at each node in the layer just to the "right" of this layer.
# This error is weighted by the weight connections between the
# node in this hidden layer and the nodes in the layer just
# to the "right" of this layer.
for node in network[i + 1]:
error += (node['weights'][j] * node['delta'])
# Add the weighted error for node_j to the
# errors list
errors.append(error)
# If this is the output layer
else:
# For each node in the output layer
for j in range(len(layer)):
# Store this node (i.e. dictionary)
node = layer[j]
# Actual - Predicted = Error
errors.append(actual[j] - node['output'])
# Calculate the delta for each node_j in this layer
for j in range(len(layer)):
node = layer[j]
# Add an item to the node's dictionary with the
# key as delta.
node['delta'] = errors[j] * sigmoid_derivative(node['output'])
def update_weights(network, instance, learning_rate):
"""
After the deltas (errors) have been calculated for each node in
each layer of the neural network, the weights can be updated.
new_weight = old_weight + learning_rate * delta * input_value
:param list network: List of layers: 0+ hidden layers, 1 output layer
:param list instance: A single training/test instance from the data set
:param float learning_rate: Controls step size in the stochastic gradient
descent procedure.
"""
# For each layer in the network
for layer_index in range(len(network)):
# Extract all the attribute values, excluding the class value
inputs = instance[:-1]
# If this is not the first hidden layer
if layer_index != 0:
# Go through each node in the previous layer and add extract the
# output from that node. The output from the previous layer
# is the input to this layer.
inputs = [node['output'] for node in network[layer_index - 1]]
# For each node in this layer
for node in network[layer_index]:
# Go through each input value
for j in range(len(inputs)):
# Update the weights
node['weights'][j] += learning_rate * node['delta'] * inputs[j]
# Updating the bias weight
node['weights'][-1] += learning_rate * node['delta']
def train_neural_net(
network, training_set, learning_rate, no_epochs, no_outputs):
"""
Train a neural network that has already been initialized.
Training is done using stochastic gradient descent where the weights
are updated one training instance at a time rather than after the
entire training set (as is the case with gradient descent).
:param list network: The neural network, which is a list of layers
:param list training_set: A list of training instances (i.e. examples)
:param float learning_rate: Controls step size of gradient descent
:param int no_epochs: How many passes we will make through training set
:param int no_outputs: The number of output nodes (equal to # of classes)
"""
# Go through the entire training set a fixed number of times (i.e. epochs)
for epoch in range(no_epochs):
# Update the weights one instance at a time
for instance in training_set:
# Forward propagate the training instance through the network
# and produce the output, which is a list.
outputs = forward_propagate(network, instance)
# Vectorize the output using one hot encoding.
# Create a list called actual_output that is the same length
# as the number of outputs. Put a 1 in the place of the actual
# class.
actual_output = [0 for i in range(no_outputs)]
actual_output[int(instance[-1])] = 1
back_propagate(network, actual_output)
update_weights(network, instance, learning_rate)
def predict_class(network, instance):
"""
Make a class prediction given a trained neural network and
an instance from the test data set.
:param list network: The neural network, which is a list of layers
:param list instance: A single training/test instance from the data set
:return class_prediction
:rtype int
"""
outputs = forward_propagate(network, instance)
# Return the index that has the highest probability. This index
# is the class value. Assume class values begin at 0 and go
# upwards by 1 (i.e. 0, 1, 2, ...)
class_prediction = outputs.index(max(outputs))
return class_prediction
def calculate_accuracy(actual, predicted):
"""
Calculates the accuracy percentages
:param list actual: Actual class values
:param list predicted: predicted class values
:return: classification_accuracy
:rtype: float (as a percentage)
"""
number_of_correct_predictions = 0
for index in range(len(actual)):
if actual[index] == predicted[index]:
number_of_correct_predictions += 1
classification_accuracy = (
number_of_correct_predictions / float(len(actual))) * 100.0
return classification_accuracy
def get_test_set_predictions(
training_set, test_set, learning_rate, no_epochs,
no_hidden_layers, no_nodes_per_hidden_layer):
"""
This method is the workhorse.
A new neutal network is initialized.
The network is trained on the training set.
The trained neural network is used to generate predictions on the
test data set.
:param list training_set
:param list test_set
:param float learning_rate
:param int no_epochs
:param int no_hidden_layers
:param int no_nodes_per_hidden_layer
:return network, class_predictions
:rtype list, list
"""
# Get the number of attribute values
no_inputs = len(training_set[0]) - 1
# Calculate the number of unique classes
no_outputs = len(set([instance[-1] for instance in training_set]))
# Build a new neural network
network = initialize_neural_net(
no_inputs, no_hidden_layers, no_nodes_per_hidden_layer, no_outputs)
train_neural_net(
network, training_set, learning_rate, no_epochs, no_outputs)
# Store the class predictions for each test instance
class_predictions = list()
for instance in test_set:
cl_prediction = predict_class(network, instance)
class_predictions.append(cl_prediction)
# Return the learned model as well as the class predictions
return network, class_predictions
###############################################################
def main():
"""
The main method of the program
"""
LEARNING_RATE = 0.1 # Used for stochastic gradient descent procedure
NO_EPOCHS = 1000 # Epoch is one complete pass through training data
NO_HIDDEN_LAYERS = 1 # Number of hidden layers
NO_NODES_PER_HIDDEN_LAYER = 8 # Number of nodes per hidden layer
# Welcome message
print("Welcome to the " + ALGORITHM_NAME + " Program!")
print()
# Directory where data set is located
#data_path = input("Enter the path to your input file: ")
data_path = "vote.txt"
# Read the full text file and store records in a Pandas dataframe
pd_data_set = pd.read_csv(data_path, sep=SEPARATOR)
# Show functioning of the program
#trace_runs_file = input("Enter the name of your trace runs file: ")
trace_runs_file = "vote_nn_trace_runs.txt"
## Open a new file to save trace runs
outfile_tr = open(trace_runs_file,"w")
# Testing statistics
#test_stats_file = input("Enter the name of your test statistics file: ")
test_stats_file = "vote_nn_test_stats.txt"
## Open a test_stats_file
outfile_ts = open(test_stats_file,"w")
# Generate a list of the column names
column_names = list(pd_data_set)
# The input layer in the neural network
# will have one node for each attribute value
no_of_inputs = len(column_names) - 1
# Make a list of the unique classes
list_of_unique_classes = pd.unique(pd_data_set["Actual Class"])
# The output layer in the neural network
# will have one node for each class value
no_of_outputs = len(list_of_unique_classes)
# Replace all the class values with numbers, starting from 0
# in the Pandas dataframe.
for cl in range(0, len(list_of_unique_classes)):
pd_data_set["Actual Class"].replace(
list_of_unique_classes[cl], cl ,inplace=True)
# Normalize the attribute values so that they are all between 0
# and 1, inclusive
normalized_pd_data_set = normalize(pd_data_set)
# Convert normalized Pandas dataframe into a list
dataset_as_list = normalized_pd_data_set.values.tolist()
# Set the seed for random number generator
seed(1)
# Get a list of 5 stratified folds because we are doing
# five-fold stratified cross-validation
fv_folds = get_five_stratified_folds(dataset_as_list)
# Keep track of the scores for each of the five experiments
scores = list()
experiment_counter = 0
for fold in fv_folds:
print()
print("Running Experiment " + str(experiment_counter) + " ...")
print()
outfile_tr.write("Running Experiment " + str(
experiment_counter) + " ...\n")
outfile_tr.write("\n")
# Get all the folds and store them in the training set
training_set = list(fv_folds)
# Four folds make up the training set
training_set.remove(fold)
# Combined all the folds so that all we have is a list
# of training instances
training_set = sum(training_set, [])
# Initialize a test set
test_set = list()
# For each instance in this test fold
for instance in fold:
# Create a copy and store it
copy_of_instance = list(instance)
test_set.append(copy_of_instance)
# Get the trained neural network and the predicted values
# for each test instance
neural_net, predicted_values = get_test_set_predictions(
training_set, test_set,LEARNING_RATE,NO_EPOCHS,
NO_HIDDEN_LAYERS,NO_NODES_PER_HIDDEN_LAYER)
actual_values = [instance[-1] for instance in fold]
accuracy = calculate_accuracy(actual_values, predicted_values)
scores.append(accuracy)
# Print the learned model
print("Experiment " + str(
experiment_counter) + " Trained Neural Network")
print()
for layer in neural_net:
print(layer)
print()
outfile_tr.write("Experiment " + str(
experiment_counter) + " Trained Neural Network")
outfile_tr.write("\n")
outfile_tr.write("\n")
for layer in neural_net:
outfile_tr.write(str(layer))
outfile_tr.write("\n")
outfile_tr.write("\n\n")
# Print the classifications on the test instances
print("Experiment " + str(
experiment_counter) + " Classifications on Test Instances")
print()
outfile_tr.write("Experiment " + str(
experiment_counter) + " Classifications on Test Instances")
outfile_tr.write("\n\n")
test_df = pd.DataFrame(test_set, columns=column_names)
# Add 2 additional columns to the testing dataframe
test_df = test_df.reindex(
columns=[*test_df.columns.tolist(
), 'Predicted Class', 'Prediction Correct?'])
# Add the predicted values to the "Predicted Class" column
# Indicate if the prediction was correct or not.
for pre_val_index in range(len(predicted_values)):
test_df.loc[pre_val_index, "Predicted Class"] = predicted_values[
pre_val_index]
if test_df.loc[pre_val_index, "Actual Class"] == test_df.loc[
pre_val_index, "Predicted Class"]:
test_df.loc[pre_val_index, "Prediction Correct?"] = "Yes"
else:
test_df.loc[pre_val_index, "Prediction Correct?"] = "No"
# Replace all the class values with the name of the class
for cl in range(0, len(list_of_unique_classes)):
test_df["Actual Class"].replace(
cl, list_of_unique_classes[cl] ,inplace=True)
test_df["Predicted Class"].replace(
cl, list_of_unique_classes[cl] ,inplace=True)
# Print out the test data frame
print(test_df)
print()
print()
outfile_tr.write(str(test_df))
outfile_tr.write("\n\n")
# Go to the next experiment
experiment_counter += 1
print("Experiments Completed.\n")
outfile_tr.write("Experiments Completed.\n\n")
# Print the test stats
print("------------------------------------------------------------------")
print(ALGORITHM_NAME + " Summary Statistics")
print("------------------------------------------------------------------")
print("Data Set : " + data_path)
print()
print("Learning Rate: " + str(LEARNING_RATE))
print("Number of Epochs: " + str(NO_EPOCHS))
print("Number of Hidden Layers: " + str(NO_HIDDEN_LAYERS))
print("Number of Nodes Per Hidden Layer: " + str(
NO_NODES_PER_HIDDEN_LAYER))
print()
print("Accuracy Statistics for All 5 Experiments: %s" % scores)
print()
print()
print("Classification Accuracy: %.3f%%" % (
sum(scores)/float(len(scores))))
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write(ALGORITHM_NAME + " Summary Statistics\n")
outfile_ts.write(
"------------------------------------------------------------------\n")
outfile_ts.write("Data Set : " + data_path +"\n\n")
outfile_ts.write("Learning Rate: " + str(LEARNING_RATE) + "\n")
outfile_ts.write("Number of Epochs: " + str(NO_EPOCHS) + "\n")
outfile_ts.write("Number of Hidden Layers: " + str(
NO_HIDDEN_LAYERS) + "\n")
outfile_ts.write("Number of Nodes Per Hidden Layer: " + str(
NO_NODES_PER_HIDDEN_LAYER) + "\n")
outfile_ts.write(
"Accuracy Statistics for All 5 Experiments: %s" % str(scores))
outfile_ts.write("\n\n")
outfile_ts.write("Classification Accuracy: %.3f%%" % (
sum(scores)/float(len(scores))))
## Close the files
outfile_tr.close()
outfile_ts.close()
main()
The breast cancer data set results
were in line with what I expected. The simpler model, the one with no hidden
layers, ended up generating the highest classification accuracy. Classification
accuracy was just short of 97%. In other words, the neural network that had no
hidden layers successfully classified a patient as either malignant or benign
with an almost 97% accuracy.
These results also suggest that
the amount of training data has a direct impact on performance. Higher amounts
of data (699 instances in this case) can lead to better learning and better
classification accuracy on new, unseen instances.
Glass Data Set
The performance of the neural
network on the glass data set was the worst out of all of the data sets. The
ability of the network to correctly identify the type of glass given the
attribute values never exceeded 70%.
I hypothesize that the poor
performance on the glass data set is due to the high numbers of classes
combined with a relatively smaller data set.
Iris Data Set
Classification accuracy was
superb on the iris dataset, attaining a classification accuracy around 97%. The
results of the iris dataset were surprising given that the more complicated
neural network with two hidden layers and five nodes per hidden layer outperformed
the simpler neural network that had no hidden layers. In this case, it appears
that the iris dataset benefited from the increasing layers of abstraction
provided by a higher number of layers.
Soybean Data Set (small)
Performance on the soybean data set
was stellar and was the highest of all of the data sets but also had the
largest standard deviation for the classification accuracy. Note that
classification accuracy reached a peak of 100% using one hidden layer and eight
nodes for the hidden layer. However, when I added an additional hidden layer,
classification accuracy dropped to under 70%.
The reason for the high standard
deviation of the classification accuracy is unclear, but I hypothesize it has
to do with the relatively small number of training instances. Future work would
need to be performed with the soybean large dataset available from the UCI Machine
Learning Repository to see if these results remain consistent.
The results of the soybean runs suggest
that large numbers of relevant attributes can help a machine learning algorithm
create more accurate classifications.
Vote Data Set
The vote data set did not yield the
stellar performance of the soybean data set, but classification accuracy was
still solid at ~96% using one hidden layer and eight nodes per hidden layer.
These results are in line with what I expected because voting behavior should
provide a powerful predictor of whether a candidate is a Democrat or
Republican. I would have been surprised had I observed classification
accuracies that were lower since members of Congress tend to vote along party
lines on most issues.
Summary and Conclusions
My hypothesis was incorrect. In
some cases, simple neural networks with no hidden layers outperformed more
complex neural networks with 1+ hidden layers. However, in other cases, more
complex neural networks with multiple hidden layers outperformed the network
with no hidden layers. The reason why some data is more amenable to networks
with hidden layers instead of without hidden layers is unclear.
Other conclusions include the
following:
Higher amounts of data can lead to better
learning and better classification accuracy on new, unseen instances.
Large numbers of relevant attributes can help a neural
network create more accurate classifications.
Neural networks are powerful and can achieve
excellent results on both binary and multi-class classification problems.
Alpaydin, E. (2014). Introduction to Machine
Learning. Cambridge, Massachusetts: The MIT Press.
Fisher, R. (1988, July 01). Iris Data Set.
Retrieved from Machine Learning Repository: https://archive.ics.uci.edu/ml/datasets/iris
German, B. (1987, September 1). Glass
Identification Data Set. Retrieved from UCI Machine Learning Repository:
https://archive.ics.uci.edu/ml/datasets/Glass+Identification
Ĭordanov, I., & Jain, L. C. (2013). Innovations
in Intelligent Machines –3 : Contemporary Achievements in Intelligent
Systems. Berlin: Springer.
Michalski, R. (1980). Learning by being told and
learning from examples: an experimental comparison of the two methodes of
knowledge acquisition in the context of developing an expert system for
soybean disease diagnosis. International Journal of Policy Analysis and
Information Systems, 4(2), 125-161.
Montavon, G. O. (2012). Neural Networks : Tricks
of the Trade. New York: Springer.
Schlimmer, J. (1987, 04 27). Congressional Voting
Records Data Set. Retrieved from Machine Learning Repository:
https://archive.ics.uci.edu/ml/datasets/Congressional+Voting+Records
Wolberg, W. (1992, 07 15). Breast Cancer Wisconsin
(Original) Data Set. Retrieved from Machine Learning Repository:
https://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+%28Original%25