update storages + ER

This commit is contained in:
Brian Delhaisse
2019-04-06 04:48:00 +02:00
parent 94a0f00822
commit bc8720476b
7 changed files with 500 additions and 17 deletions
+5 -5
View File
@@ -14,7 +14,7 @@ The framework has been tested with Python 2.7 and Ubuntu 16.04 and 18.04. We als
## Installation
1. First download the `pip` Python package manager and create a virtual environment for Python as described in the following link: https://packaging.python.org/guides/installing-using-pip-and-virtualenv/
On Ubuntu, in the terminal, you can type to download and install `pip` and `virtualenv`:
On Ubuntu, you can install `pip` and `virtualenv` by typing in the terminal:
- In Python 2.7:
```bash
@@ -34,14 +34,14 @@ virtualenv -p /usr/bin/python<version> <virtualenv_name>
# activate the virtual environment
source <virtualenv_name>/bin/activate
```
where `<version>` is the python version you want to use (select between `2.7` or `3.5`), and `<virtualenv_name>` is a name of your choice for the virtual environment. For instance, it can be `py2.7` or `py3.7`.
where `<version>` is the python version you want to use (select between `2.7` or `3.5`), and `<virtualenv_name>` is a name of your choice for the virtual environment. For instance, it can be `py2.7` or `py3.5`.
To deactivate the virtual environment, just type:
```bash
deactivate
```
2. clone this repository and install the requirements and the setup.py
2. clone this repository and install the requirements by executing the setup.py
In Python 2.7:
```bash
@@ -63,8 +63,8 @@ pip install -e . # this will install pyrobolearn as well as the required packag
Depending on your computer configuration and the python version you use, you might need to install also the following packages through `apt-get`:
```bash
sudo apt-get install python-tk # if python 2.7
sudo apt-get install pytho3-tk # if python 3.5
sudo apt install python-tk # if python 2.7
sudo apt install python3-tk # if python 3.5
```
## How to use it?
+7
View File
@@ -5,3 +5,10 @@ This folder contains storages / replay memories that are used in reinforcement l
## What to look/check next?
Check the `algos` folder (especially the `algos/rl_algo` file) and the `samplers` folder.
## References
[1] "Reinforcement Learning for robots using neural networks", Lin, 1993
[2] "Playing Atari with Deep Reinforcement Learning", Mnih et al., 2013
[3] "Prioritized Experience Replay", Schaul, 2015
[4] "Hindsight Experience Replay", Andrychowicz et al., 2017
+1 -1
View File
@@ -1,3 +1,3 @@
# import memory
from storage import *
from .storage import *
+123 -4
View File
@@ -1,4 +1,123 @@
# ER: Experience Replay (memory)
# Refs:
# 1. 'Reinforcement Learning for robots using neural networks', Lin, 1993
# 2. 'Playing Atari with Deep Reinforcement Learning', Mnih et al., 2013
#!/usr/bin/env python
"""Provides the experience replay (ER) storage.
References:
[1] "Reinforcement Learning for robots using neural networks", Lin, 1993
[2] "Playing Atari with Deep Reinforcement Learning", Mnih et al., 2013
"""
import random
import torch
from pyrobolearn.storages.storage import Storage, ListStorage
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class ExperienceReplay(Storage):
r"""Experience replay storage
The experience replay storage returns a transition tuple :math:`(s_t, a_t, s_{t+1}, r_t, d)`, where is :math:`s_t`
is the state at time :math:`t`, :math:`a_t` is the action outputted by the policy in response to the state
:math:`s_t`, :math:`s_{t+1}` is the next state returned by the environment due to the policy's action :math:`a_t`
and the current state :math:`s_t`, :math:`r_t` is the reward signal returned by the environment, and :math:`d`
is a boolean value that specifies if the task is over or not (i.e. if it has failed or succeeded).
The experience replay storage is often used in conjunction with off-policy RL algorithms.
The following code is inspired by [3] but modified such that it uses a PyTorch list storage.
References:
[1] "Reinforcement Learning for robots using neural networks", Lin, 1993
[2] "Playing Atari with Deep Reinforcement Learning", Mnih et al., 2013
"""
def __init__(self, capacity=10000, device=None, dtype=torch.float):
"""
Initialize the Experience Replay Storage.
Args:
capacity (int): maximum size of the experience replay storage.
device (torch.device, str, None): the device to put the data on (e.g. `torch.device("cuda:0")` or
`torch.device("cpu")`). If string, it can be 'cpu' or 'cuda'. If None, it will keep the original device
to which the tensor is allocated.
dtype (torch.dtype, None): convert the `torch.Tensor` to the specified data type. If None, it will keep
the original dtype
"""
super(ExperienceReplay, self).__init__(device, dtype)
self.capacity = capacity
self.memory = ListStorage(device=device, dtype=dtype)
self.position = 0
##############
# Properties #
##############
@property
def device(self):
"""Return the memory's device instance."""
return self.memory.device
@device.setter
def device(self, device):
"""Set the memory's device instance."""
self.memory.device = device
@property
def dtype(self):
"""Return the memory's data type."""
return self.memory.dtype
@dtype.setter
def dtype(self, dtype):
"""Set the memory's data type."""
self.memory.dtype = dtype
###########
# Methods #
###########
def push(self, *items):
r"""
Push new transition :math:`(s_t, a_t, s_{t+1}, r_t, d, \gamma)` in the experience replay.
Args:
*items (list, tuple of torch.Tensor): transition tuple
"""
# add new item or update previous one
if len(self.memory) < self.capacity:
self.memory.append(items)
else:
self.memory[self.position] = items
# update head position (cyclic)
self.position = (self.position + 1) % self.capacity
def to(self, device=None, dtype=None):
"""
Put all the tensors to the specified device and convert them to the specified data type.
Args:
device (torch.device, str, None): the device to put the data on (e.g. `torch.device("cuda:0")` or
`torch.device("cpu")`). If string, it can be 'cpu' or 'cuda'. If None, it will keep the original device
to which the tensor is allocated.
dtype (torch.dtype, None): convert the `torch.Tensor` to the specified data type. If None, it will keep
the original dtype
"""
self.memory.to(device=device, dtype=dtype)
def sample(self, batch_size):
"""Sample uniformly a batch from the experience replay."""
return random.sample(self, batch_size)
# alias
ER = ExperienceReplay
+73 -2
View File
@@ -1,2 +1,73 @@
# HER: Hindsight Experience Replay
# Ref: 'Hindsight Experience Replay', Andrychowicz et al., 2017
#!/usr/bin/env python
"""Provides the hindsight experience replay (HER) storage.
References:
[1] "Hindsight Experience Replay", Andrychowicz et al., 2017
"""
from pyrobolearn.storages.er import ExperienceReplay
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class HindsightExperienceReplay(ExperienceReplay):
r"""Hindsight Experience replay storage
One of the main challenges in RL is to shape the reward function such that the agent can successfully learned to
perform the specified task. This often requires expert knowledge to engineer this reward function.
To address this, the authors from [1] proposes to use a hindsight experience replay, which enables learning from
sparse and binary rewards, and can be combined with any off-policy RL algorithms. This notably improves the
sample efficiency.
In this setting, one or several goals have to be defined. They are concatenated with the state and feed to the
policy and value approximators. Additionally, they are included in the transition tuple sampled from the
experience replay storage.
Pseudo-algo
-----------
Pseudo-algorithm (taken from [1] and reproduce here for completeness)::
1. Given:
- an off-policy RL algorithm A (e.g. DQN, DDPG, NAF, SDQN)
- a strategy S for sampling goals for replay (e.g. S(s_0, ..., s_T) = m(s_T))
- a reward function r : S x A x G \rightarrow \mathbb{R} (e.g. r(s, a, g) = -[f_g(s) = 0])
2. Initialize A (e.g. initialize neural networks)
3. Initialize replay buffer R
4. for episode = 1 to M do
5. Sample a goal g and an initial state s0.
6. for t = 0 to T - 1 do
7. Sample an action at using the behavioral policy from A: a_t \leftarrow \pi_b([s_t,g])
8. Execute the action a_t and observe a new state s_{t+1}
9. end for
10. for t = 0 to T - 1 do
11. r_t := r(s_t, a_t, g)
12. Store the transition ([s_t,g], a_t, r_t, [s_{t+1},g]) in R (standard experience replay)
13. Sample a set of additional goals for replay G := S(current episode)
14. for g' \in G do
15. r' := r(s_t, a_t, g')
16. Store the transition ([s_t,g'], a_t, r', [s_{t+1},g']) in R (HER)
17. end for
18. end for
19. for t = 1 to N do
20. Sample a minibatch B from the replay buffer R
21. Perform one step of optimization using A and minibatch B
22. end for
23. end for
References:
[1] "Hindsight Experience Replay", Andrychowicz et al., 2017
"""
pass
# alias
HER = HindsightExperienceReplay
+77 -2
View File
@@ -1,2 +1,77 @@
# PER: Prioritized Experience Replay
# Ref: 'Prioritized Experience Replay', Schaul, 2015
#!/usr/bin/env python
"""Provides the prioritized experience replay (PER) storage.
The PER works by prioritizing transitions based on the magnitude of their TD error. In order to overcome over-fitting
by sampling the same transitions, a stochastic sampling method is used based on a kind of softmax function on TD
errors. In order to correct the bias induced by this sampling method, importance sampling weights (which are
normalized for stability reasons) are used.
In summary, PER can be seen as a stochastic prioritization ER which uses importance sampling.
References:
[1] "Prioritized Experience Replay", Schaul, 2015
"""
from pyrobolearn.storages.storage import PriorityQueueStorage
__author__ = "Brian Delhaisse"
__copyright__ = "Copyright 2018, PyRoboLearn"
__credits__ = ["Brian Delhaisse"]
__license__ = "MIT"
__version__ = "1.0.0"
__maintainer__ = "Brian Delhaisse"
__email__ = "briandelhaisse@gmail.com"
__status__ = "Development"
class PrioritizedExperienceReplay(PriorityQueueStorage):
r"""Prioritized Experience Replay storage
The PER works by prioritizing transitions based on the magnitude of their TD error. In order to overcome
over-fitting by sampling the same transitions, a stochastic sampling method is used based on a kind of softmax
function on TD errors. In order to correct the bias induced by this sampling method, importance sampling weights
(which are normalized for stability reasons) are used.
In summary, PER can be seen as a stochastic prioritization ER which uses importance sampling.
There are 2 stochastic prioritization schemes used in [1].
- proportional prioritization: :math:`p_i = |\delta_i| + \epsilon`, where :math:`\epsilon` is a small positive
constant to avoid the transition to have a probability of 0.
- rank-based prioritization: math:`p_i = \frac{1}{rank(i)}`, where rank(i) is the rank of transition i (that is
they are i other keys in the priority queue that are smaller than the current key i) when the replay memory is
sorted according to :math:`|\delta_i|`
Pseudo-algo:
-----------
Pseudo-algorithm (taken from [1] and reproduce here for completeness)::
1. Input: minibatch k, step-size \eta, replay period K and size N, exponents a and b, budget T
2. Initialize replay memory H = {}, \Delta = 0, p_1 = 1
3. Observe s_0 and choose a_1 \sim \pi_\theta(s_0)
4. for t = 0 to T-1 do
5. choose action a_t \sim \pi_\theta(s_t)
6. Observe s_{t+1}, r_t, \gamma_t
7. Store transition (s_t, a_t, r_t, s_{t+1}, \gamma_t) in H with maximal priority p_t = max_{i<t} p_i
8. if (t % K) = 0 then
9. for j = 1 to k do
10. Sample transition j \sim P(j) = \frac{ p_j^a }{ \sum_i p_i^a }
11. Compute importance-sampling weight w_j = \frac{ (N P(j))^{-b} }{ \max_i w_i }
12. Compute TD-error \delta_j = r_j + \gamma_j Q_{target}(s_j, argmax_a Q(s_j, a)) - Q(s_{j-1}, a_{j-1})
13. Update transition priority p_j \leftarrow |\delta_j|
14. Accumulate weight-change \Delta \leftarrow \Delta + w_j \delta_j \nabla_\theta Q(s_{j-1},a_{j-1})
15. end for
16. Update weights \theta \leftarrow \theta + \eta \Delta, reset \Delta = 0
17. From time to time copy weights into target network \theta_{target} \leftarrow \theta
18. end if
19. end for
References:
[1] "Prioritized Experience Replay", Schaul, 2015
"""
pass
# alias
PER = PrioritizedExperienceReplay
+214 -3
View File
@@ -12,9 +12,9 @@ See Also:
import collections
import copy
import pickle
import queue
import numpy as np
import torch
from torch.utils.data.sampler import BatchSampler, SubsetRandomSampler
from pyrobolearn import logger
@@ -118,6 +118,11 @@ class PyTorchStorage(Storage):
if isinstance(item, torch.Tensor):
logger.debug('setting tensor of size {} to {} with dtype={}'.format(item.size(), device, dtype))
item = item.to(device=device, dtype=dtype)
elif isinstance(item, np.ndarray):
if item.dtype != object: # double, float, float16, int64, int32, and uint8
item = torch.from_numpy(item).to(device=device, dtype=dtype)
elif isinstance(item, (float, int, np.generic)):
item = torch.tensor(item).to(device=device, dtype=dtype)
elif isinstance(item, dict):
for key, value in item.items():
item[key] = self._to(value, device=device, dtype=dtype)
@@ -127,8 +132,9 @@ class PyTorchStorage(Storage):
value = self._to(value, device=device, dtype=dtype)
item.add(value)
elif isinstance(item, collections.Iterable):
for idx, value in enumerate(item):
item[idx] = self._to(value, device=device, dtype=dtype)
item = [self._to(value, device=device, dtype=dtype) for value in item]
# for idx, value in enumerate(item):
# item[idx] = self._to(value, device=device, dtype=dtype)
return item
@staticmethod
@@ -232,6 +238,211 @@ class ListStorage(list, PyTorchStorage):
iterable = self._to(iterable, device=self.device, dtype=self.dtype)
super(ListStorage, self).append(iterable)
def __setitem__(self, key, value):
"""Set the specified value at the specified key."""
super(ListStorage, self).__setitem__(key, self._to(value, device=self.device, dtype=self.dtype))
class FIFOQueueStorage(queue.Queue, PyTorchStorage):
r"""FIFO Queue Storage
FIFO queue storage (data structure) which allocates the given tensor(s) to the specified device.
"""
def __init__(self, maxsize=0):
"""Initialize the FIFO Queue storage.
Args:
maxsize (int): maximum size of the queue. If :attr:`maxsize` is <= 0, the queue size is infinite.
"""
super(FIFOQueueStorage, self).__init__(maxsize)
def put(self, item, block=False, timeout=None):
"""Put an item into the queue.
If optional args 'block' is true and 'timeout' is None (the default), block if necessary until a free slot
is available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises
the Full exception if no free slot was available within that time.
Otherwise ('block' is false), put an item on the queue if a free slot is immediately available, else raise
the Full exception ('timeout' is ignored in that case).
"""
if not self.full():
item = self._to(item, device=self.device, dtype=self.dtype)
super(FIFOQueueStorage, self).put(item, block=block, timeout=timeout)
def put_nowait(self, item):
"""
Put an item into the queue without blocking. Only enqueue the item if a free slot is immediately available.
Otherwise raise the Full exception.
"""
item = self._to(item, device=self.device, dtype=self.dtype)
super(FIFOQueueStorage, self).put_nowait(item)
def __len__(self):
"""Return the size of the Queue."""
return self.qsize()
def __iter__(self):
"""Return the iterator object itself."""
self.cnt = 0
return self
def __next__(self): # only valid in Python 3
"""Return the next item in the sequence."""
if self.cnt < self.qsize():
self.cnt += 1
return self.queue[self.cnt-1]
else:
raise StopIteration
def next(self): # for Python 2
"""Return the next item in the sequence."""
return self.__next__()
class LIFOQueueStorage(queue.LifoQueue, PyTorchStorage):
r"""LIFO Queue Storage
LIFO queue storage (data structure) which allocates the given tensor(s) to the specified device.
"""
def __init__(self, maxsize=0):
"""Initialize the LIFO Queue storage.
Args:
maxsize (int): maximum size of the queue. If :attr:`maxsize` is <= 0, the queue size is infinite.
"""
super(LIFOQueueStorage, self).__init__(maxsize)
def put(self, item, block=False, timeout=None):
"""Put an item into the queue.
If optional args 'block' is true and 'timeout' is None (the default), block if necessary until a free slot
is available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises
the Full exception if no free slot was available within that time.
Otherwise ('block' is false), put an item on the queue if a free slot is immediately available, else raise
the Full exception ('timeout' is ignored in that case).
"""
if not self.full():
item = self._to(item, device=self.device, dtype=self.dtype)
super(LIFOQueueStorage, self).put(item, block=block, timeout=timeout)
def put_nowait(self, item):
"""
Put an item into the queue without blocking. Only enqueue the item if a free slot is immediately available.
Otherwise raise the Full exception.
"""
item = self._to(item, device=self.device, dtype=self.dtype)
super(LIFOQueueStorage, self).put_nowait(item)
def __len__(self):
"""Return the size of the Queue."""
return self.qsize()
def __iter__(self):
"""Return the iterator object itself."""
self.cnt = 0
return self
def __next__(self): # only valid in Python 3
"""Return the next item in the sequence."""
if self.cnt < self.qsize():
self.cnt += 1
return self.queue[self.cnt-1]
else:
raise StopIteration
def next(self): # for Python 2
"""Return the next item in the sequence."""
return self.__next__()
class PriorityQueueStorage(queue.PriorityQueue, PyTorchStorage):
r"""Priority Queue Storage
Priority queue storage (data structure) which allocates the given tensor(s) to the specified device.
Note that `queue.PriorityQueue` is a thread-safe class that use the `heapq` module (which is initially not thread
safe) under the hood.
"""
def __init__(self, maxsize=0, ascending=True):
"""Initialize the LIFO Queue storage.
Args:
maxsize (int): maximum size of the queue. If :attr:`maxsize` is <= 0, the queue size is infinite.
ascending (bool): if True, the item with the lowest priority will be the first one to be retrieved.
"""
super(PriorityQueueStorage, self).__init__(maxsize)
self.ascending = ascending
def put(self, item, block=False, timeout=None):
"""Put an item into the queue.
If optional args 'block' is true and 'timeout' is None (the default), block if necessary until a free slot
is available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises
the Full exception if no free slot was available within that time.
Otherwise ('block' is false), put an item on the queue if a free slot is immediately available, else raise
the Full exception ('timeout' is ignored in that case).
"""
if not self.full():
if not isinstance(item, tuple) or len(item) != 2:
raise TypeError("Expecting the item to be a tuple of length 2 with (priority number, data), instead "
"got: {}".format(item))
if self.ascending:
item = (item[0], self._to(item[1], device=self.device, dtype=self.dtype))
else:
item = (-item[0], self._to(item[1], device=self.device, dtype=self.dtype))
super(PriorityQueueStorage, self).put(item, block=block, timeout=timeout)
def put_nowait(self, item):
"""
Put an item into the queue without blocking. Only enqueue the item if a free slot is immediately available.
Otherwise raise the Full exception.
"""
if not isinstance(item, tuple) or len(item) != 2:
raise TypeError("Expecting the item to be a tuple of length 2 with (priority number, data), instead "
"got: {}".format(item))
if self.ascending:
item = (item[0], self._to(item[1], device=self.device, dtype=self.dtype))
else:
item = (-item[0], self._to(item[1], device=self.device, dtype=self.dtype))
super(PriorityQueueStorage, self).put_nowait(item)
def get(self, block=True, timeout=None):
"""Remove and return an item from the queue.
If optional args 'block' is true and 'timeout' is None (the default), block if necessary until an item is
available. If 'timeout' is a non-negative number, it blocks at most 'timeout' seconds and raises the Empty
exception if no item was available within that time. Otherwise ('block' is false), return an item if one is
immediately available, else raise the Empty exception ('timeout' is ignored in that case).
"""
item = super(PriorityQueueStorage, self).get(block=block, timeout=timeout)
if not self.ascending:
item = (-item[0], item[1])
return item
def __len__(self):
"""Return the size of the Queue."""
return self.qsize()
def __iter__(self):
"""Return the iterator object itself."""
self.cnt = 0
return self
def __next__(self): # only valid in Python 3
"""Return the next item in the sequence."""
if self.cnt < self.qsize():
self.cnt += 1
return self.queue[self.cnt-1]
else:
raise StopIteration
def next(self): # for Python 2
"""Return the next item in the sequence."""
return self.__next__()
class SetStorage(set, PyTorchStorage):
r"""PyTorch set storage