mirror of
https://github.com/wassname/pyrobolearn.git
synced 2026-09-09 11:31:38 +08:00
add approximators, policies, processors, recorders
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
|
||||
# import function approximators
|
||||
from approximator import *
|
||||
@@ -0,0 +1,558 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the basic (Function) Approximator class.
|
||||
|
||||
This file describes the `Approximator` class that wraps a learning model and connects it with its inputs, and outputs.
|
||||
The inputs/outputs can be states, actions, arrays/tensors, etc.
|
||||
|
||||
Dependencies:
|
||||
- `pyrobolearn.models`
|
||||
- `pyrobolearn.states`
|
||||
- `pyrobolearn.actions`
|
||||
"""
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from pyrobolearn.states import State
|
||||
from pyrobolearn.actions import Action
|
||||
|
||||
from pyrobolearn.models import Model
|
||||
from pyrobolearn.models.linear import Linear
|
||||
from pyrobolearn.models.nn import NN, MLP, NEATModel
|
||||
|
||||
__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 Approximator(object):
|
||||
r"""Function Approximator (abstract) class
|
||||
|
||||
The function approximator is a wrapper around the inner learning base model, and connects a learning model to
|
||||
its state/action inputs and outputs. That is, this class described how to connect a learning model with
|
||||
states/actions, and thus allows the inner models to be independent from the notions of states and actions.
|
||||
|
||||
This class and all the children inheriting from this one will be used internally by several classes such as
|
||||
policies, dynamic models, value estimators, and others. This enables to not duplicate and write the same code
|
||||
for these various different concepts which share similar features.
|
||||
For instance, policies are function approximators where the inputs are states, and the outputs are actions.
|
||||
Dynamic models are extended models where the inputs are states and actions, and the outputs are the next state.
|
||||
|
||||
Often the learning model can be constructed automatically by knowing the input and output dimensions, along with
|
||||
few other optional parameters. This is useful if one does not wish to change the learning model but just to scale
|
||||
it to different input/output sizes.
|
||||
|
||||
This class is used by the following classes:
|
||||
* Policy: approximator mapping states to actions
|
||||
* Value estimator: approximator mapping states (or states and actions) to a value, or mapping states to actions
|
||||
(in the case of discrete actions)
|
||||
* Actor-Critic:
|
||||
* Dynamic: approximator mapping states and actions to the next states
|
||||
* Transformation mappings: approximator mapping states to states
|
||||
|
||||
Example::
|
||||
|
||||
states = JntPosState(robot) + JntVelState(robot)
|
||||
actions = JntPosAction(robot)
|
||||
model = ...
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs, model=None, preprocessors=None, postprocessors=None):
|
||||
r"""Initialize the outer model.
|
||||
|
||||
Args:
|
||||
inputs (State, Action, array): inputs of the inner models (instance of State/Action)
|
||||
outputs (State, Action, array): outputs of the inner models (instance of Action/State)
|
||||
model (Model, None): inner model which will be wrapped if not an instance of Model
|
||||
preprocessors (None, Processor): the inputs are first given to the preprocessors then to the model.
|
||||
postprocessors (None, Processor): the predicted outputs by the model are given to the processors before
|
||||
being returned.
|
||||
"""
|
||||
|
||||
# Check inputs and outputs, and convert to the correct format
|
||||
self._model = None
|
||||
self.inputs = inputs
|
||||
self.outputs = outputs
|
||||
|
||||
# preprocessors and postprocessors
|
||||
self.preprocessors = preprocessors if preprocessors is not None else lambda x: x
|
||||
self.postprocessors = postprocessors if postprocessors is not None else lambda x: x
|
||||
|
||||
# Check the given model: check if correct input/output sizes wrt the previous arguments, and check
|
||||
# the model type and wrap it if necessary. That is, if the type is from the original module/library,
|
||||
# wrap it with the corresponding inner model
|
||||
self.model = model
|
||||
|
||||
##############
|
||||
# Properties #
|
||||
##############
|
||||
|
||||
@property
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
@inputs.setter
|
||||
def inputs(self, inputs):
|
||||
if inputs is not None:
|
||||
if isinstance(inputs, (int, float)):
|
||||
inputs = np.array([inputs])
|
||||
elif not isinstance(inputs, (State, Action, torch.Tensor, np.ndarray)):
|
||||
raise TypeError("Expecting the inputs to be a State, Action, torch.Tensor, or np.ndarray.")
|
||||
if self._model is not None:
|
||||
pass # TODO: check that the dimensions agree with the model
|
||||
# set inputs
|
||||
self._inputs = inputs
|
||||
|
||||
@property
|
||||
def outputs(self):
|
||||
return self._outputs
|
||||
|
||||
@outputs.setter
|
||||
def outputs(self, outputs):
|
||||
if outputs is not None:
|
||||
if isinstance(outputs, (int, float)):
|
||||
outputs = np.array([outputs])
|
||||
elif not isinstance(outputs, (State, Action, torch.Tensor, np.ndarray)):
|
||||
raise TypeError("Expecting the outputs to be a State, Action, torch.Tensor, or np.ndarray.")
|
||||
if self._model is not None:
|
||||
pass # TODO: check that the dimensions agree with the model
|
||||
# set outputs
|
||||
self._outputs = outputs
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self._model
|
||||
|
||||
@model.setter
|
||||
def model(self, model):
|
||||
if model is not None:
|
||||
# check model type
|
||||
# if not isinstance(model, Model):
|
||||
# raise TypeError("Expecting the model to be an instance of Model, instead received: "
|
||||
# "{}".format(type(model)))
|
||||
# TODO
|
||||
|
||||
# check model input/output shape
|
||||
if self._inputs is None:
|
||||
raise ValueError("Inputs have not been set.")
|
||||
if self._outputs is None:
|
||||
raise ValueError("Outputs have not been set.")
|
||||
shape = model.input_shape
|
||||
|
||||
# TODO
|
||||
|
||||
# set model
|
||||
self._model = model
|
||||
|
||||
@property
|
||||
def num_parameters(self):
|
||||
"""Return the total number of parameters"""
|
||||
return self.model.num_parameters
|
||||
|
||||
###########
|
||||
# Methods #
|
||||
###########
|
||||
|
||||
def is_parametric(self):
|
||||
"""
|
||||
Return True if the model is parametric.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is parametric.
|
||||
"""
|
||||
return self.model.is_parametric()
|
||||
|
||||
def is_linear(self):
|
||||
"""
|
||||
Return True if the model is linear (wrt the parameters). This can be for instance useful for some learning
|
||||
algorithms (some only works on linear models).
|
||||
|
||||
Returns:
|
||||
bool: True if it is a linear model
|
||||
"""
|
||||
return self.model.is_linear()
|
||||
|
||||
def is_recurrent(self):
|
||||
"""
|
||||
Return True if the model is recurrent. This can be for instance useful for some learning algorithms which
|
||||
change their behavior when they deal with recurrent learning models.
|
||||
|
||||
Returns:
|
||||
bool: True if it is a recurrent model.
|
||||
"""
|
||||
return self.model.is_recurrent()
|
||||
|
||||
def is_deterministic(self):
|
||||
"""
|
||||
Return True if the model is deterministic.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is deterministic.
|
||||
"""
|
||||
return self.model.is_deterministic()
|
||||
|
||||
def is_probabilistic(self):
|
||||
"""
|
||||
Return True if the model is probabilistic/stochastic.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is probabilistic.
|
||||
"""
|
||||
return self.model.is_probabilistic()
|
||||
|
||||
# alias
|
||||
is_stochastic = is_probabilistic
|
||||
|
||||
def is_discriminative(self):
|
||||
"""
|
||||
Return True if the model is discriminative, that is, if the model estimates the conditional probability
|
||||
:math:`p(y|x)`.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is discriminative.
|
||||
"""
|
||||
return self.model.is_discriminative()
|
||||
|
||||
def is_generative(self):
|
||||
"""
|
||||
Return True if the model is generative, that is, if the model estimates the joint distribution of the input
|
||||
and output :math:`p(x,y)`. A generative model allows to sample from it.
|
||||
|
||||
Returns:
|
||||
bool: True if the model is generative.
|
||||
"""
|
||||
return self.model.is_generative()
|
||||
|
||||
def reset(self):
|
||||
pass
|
||||
|
||||
def predict(self, x, to_numpy=True):
|
||||
# x = self.preprocessors(x)
|
||||
x = self.model(x.data[0])
|
||||
# x = self.postprocessors(x)
|
||||
return x
|
||||
|
||||
def parameters(self):
|
||||
return self.model.parameters()
|
||||
|
||||
def get_params(self):
|
||||
return list(self.parameters())
|
||||
|
||||
def hyperparameters(self):
|
||||
return self.model.hyperparameters()
|
||||
|
||||
def get_hyperparams(self):
|
||||
return list(self.hyperparameters())
|
||||
|
||||
def get_vectorized_parameters(self, to_numpy=True):
|
||||
return self.model.get_vectorized_parameters(to_numpy=to_numpy)
|
||||
|
||||
def set_vectorized_parameters(self, vector):
|
||||
self.model.set_vectorized_parameters(vector=vector)
|
||||
|
||||
def get_input_dims(self):
|
||||
return self.model.input_dims
|
||||
|
||||
def get_output_dims(self):
|
||||
return self.model.output_dims
|
||||
|
||||
def set_exploration(self):
|
||||
pass
|
||||
|
||||
def save(self, filename):
|
||||
self.model.save(filename)
|
||||
|
||||
def load(self, filename):
|
||||
self.model.load(filename)
|
||||
|
||||
#############
|
||||
# Operators #
|
||||
#############
|
||||
|
||||
def __call__(self, x):
|
||||
return self.predict(x)
|
||||
|
||||
def __str__(self):
|
||||
return self.model.__str__()
|
||||
|
||||
|
||||
class RandomApproximator(Approximator):
|
||||
r"""Random Approximator
|
||||
"""
|
||||
|
||||
class Random(object):
|
||||
|
||||
def __init__(self, num_outputs, seed=None):
|
||||
self.num_outputs = num_outputs
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
def __init__(self, outputs, preprocessors=None, postprocessors=None):
|
||||
# call parent class
|
||||
model = self.Random(num_outputs=self._size(outputs), seed=None)
|
||||
super(RandomApproximator, self).__init__(inputs=None, outputs=outputs, model=model,
|
||||
preprocessors=preprocessors, postprocessors=postprocessors)
|
||||
|
||||
def _size(self, x):
|
||||
size = 0
|
||||
if isinstance(x, (State, Action)):
|
||||
if x.isDiscrete():
|
||||
size = x.space[0].n
|
||||
else:
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
# def predict(self, x):
|
||||
# if isinstance(self.outputs, (State, Action)):
|
||||
# # get the space of each output
|
||||
# spaces = self.outputs.space
|
||||
#
|
||||
# # sample from each space
|
||||
# output_data = [space.sample() for space in spaces]
|
||||
#
|
||||
# # set the data for each action
|
||||
# self.outputs.data = output_data
|
||||
#
|
||||
# return self.outputs
|
||||
|
||||
def predict(self, x):
|
||||
# x = self.preprocessors(x)
|
||||
x = self.model.predict(x.data[0])
|
||||
if isinstance(self.outputs, (State, Action)) and self.outputs.isDiscrete():
|
||||
x = np.argmax(x)
|
||||
# x = self.postprocessors(x)
|
||||
return x
|
||||
|
||||
|
||||
class LinearApproximator(Approximator):
|
||||
r"""Linear Function Approximator
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs, preprocessors=None, postprocessors=None):
|
||||
# call parent class
|
||||
model = Linear(num_inputs=self._size(inputs), num_outputs=self._size(outputs), add_bias=True)
|
||||
super(LinearApproximator, self).__init__(inputs, outputs, model=model, preprocessors=preprocessors,
|
||||
postprocessors=postprocessors)
|
||||
|
||||
def _size(self, x):
|
||||
size = 0
|
||||
if isinstance(x, (State, Action)):
|
||||
if x.isDiscrete():
|
||||
size = x.space[0].n
|
||||
else:
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
def predict(self, x, to_numpy=True):
|
||||
# x = self.preprocessors(x)
|
||||
x = self.model.predict(x.data[0], to_numpy=to_numpy)
|
||||
if isinstance(self.outputs, (State, Action)) and self.outputs.isDiscrete():
|
||||
if to_numpy:
|
||||
x = np.array([np.argmax(x)])
|
||||
else:
|
||||
x = torch.argmax(x, dim=0, keepdim=True)
|
||||
# x = self.postprocessors(x)
|
||||
return x
|
||||
|
||||
|
||||
class NNApproximator(Approximator):
|
||||
r"""Neural Network Function Approximator
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs, model, preprocessors=None, postprocessors=None):
|
||||
|
||||
# call parent class
|
||||
super(NNApproximator, self).__init__(inputs, outputs, model, preprocessors=preprocessors,
|
||||
postprocessors=postprocessors)
|
||||
|
||||
# convert/wrap the model
|
||||
if not isinstance(model, NN):
|
||||
model = NN(model, input_dims=inputs.shape, output_dims=outputs.shape) # TODO
|
||||
self.model = model
|
||||
|
||||
|
||||
class MLPApproximator(NNApproximator):
|
||||
r"""Multi-Layer Perceptron Function Approximator
|
||||
|
||||
It creates a feed-forward and fully-connected neural network, where linear layers are followed by non-linear
|
||||
activation functions. The input and output dimensions are inferred from the inputs and outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs, hidden_units=(),
|
||||
activation_fct='Linear', last_activation_fct=None, dropout_prob=None,
|
||||
preprocessors=None, postprocessors=None):
|
||||
|
||||
# check that the inputs and ouputs are 1D
|
||||
# if not self._check1D(inputs):
|
||||
# raise ValueError("Length of input shape should be 1! Instead, got {}".format(inputs.shape))
|
||||
# print(outputs)
|
||||
# print(outputs.shape)
|
||||
# if not self._check1D(outputs):
|
||||
# raise ValueError("Length of output shape should be 1! Instead, got {}".format(outputs.shape))
|
||||
|
||||
input_size = self._size(inputs)
|
||||
output_size = self._size(outputs)
|
||||
|
||||
# create model
|
||||
num_units = [input_size] + list(hidden_units) + [output_size]
|
||||
model = MLP(num_units=num_units, activation_fct=activation_fct, last_activation_fct=last_activation_fct,
|
||||
dropout_prob=dropout_prob)
|
||||
|
||||
# call superclass
|
||||
super(MLPApproximator, self).__init__(inputs, outputs, model, preprocessors=preprocessors,
|
||||
postprocessors=postprocessors)
|
||||
|
||||
def _size(self, x):
|
||||
if isinstance(x, (State, Action)):
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
def _check1D(self, arg):
|
||||
"""Check that the given argument is a 1D vector, or simple array"""
|
||||
# if isinstance(arg, np.ndarray):
|
||||
shapes = arg.shape
|
||||
# else:
|
||||
# shape = arg.shape()
|
||||
for shape in shapes:
|
||||
if not (len(shape) == 1 and isinstance(shape[0], int)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def predict(self, x):
|
||||
# convert given input to torch tensor
|
||||
if isinstance(x, (State, Action)):
|
||||
x = np.concatenate((x. data))
|
||||
x = torch.from_numpy(x).float()
|
||||
|
||||
# feed it to the model and get predicted output
|
||||
x = self.model(x)
|
||||
|
||||
# check output
|
||||
if isinstance(self.outputs, (State, Action)):
|
||||
# data
|
||||
self.outputs.train_data = x
|
||||
# convert back output from torch tensor to np array
|
||||
x = x.detach().numpy()
|
||||
self.outputs.data = x
|
||||
|
||||
return self.outputs
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class NEATApproximator(Approximator):
|
||||
r"""NEAT Approximator
|
||||
|
||||
See Also: `neat_model.py`, `neat_policy`, `neat_algo.py`
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs, num_hidden=0, activation_fct='relu', network_type='feedforward',
|
||||
aggregation='sum', weights_limits=(-20, 20), bias_limits=(-20, 20),
|
||||
preprocessors=None, postprocessors=None):
|
||||
|
||||
# call parent class
|
||||
model = NEATModel(num_inputs=self._size(inputs), num_outputs=self._size(outputs), num_hidden=num_hidden,
|
||||
activation_fct=activation_fct, network_type=network_type, aggregation=aggregation,
|
||||
weights_limits=weights_limits, bias_limits=bias_limits)
|
||||
super(NEATApproximator, self).__init__(inputs, outputs, model=model, preprocessors=preprocessors,
|
||||
postprocessors=postprocessors)
|
||||
|
||||
##############
|
||||
# Properties #
|
||||
##############
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return the config object"""
|
||||
return self.model.config
|
||||
|
||||
@config.setter
|
||||
def config(self, config):
|
||||
"""Set the config file (str) or object."""
|
||||
self.model.config = config
|
||||
|
||||
@property
|
||||
def genome(self):
|
||||
return self.model.genome
|
||||
|
||||
@genome.setter
|
||||
def genome(self, genome):
|
||||
self.model.genome = genome
|
||||
|
||||
@property
|
||||
def network(self):
|
||||
return self.model.network
|
||||
|
||||
@property
|
||||
def population(self):
|
||||
return self.model.population
|
||||
|
||||
###########
|
||||
# Methods #
|
||||
###########
|
||||
|
||||
def _size(self, x):
|
||||
size = 0
|
||||
if isinstance(x, (State, Action)):
|
||||
if x.isDiscrete():
|
||||
size = x.space[0].n
|
||||
else:
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
def predict(self, x, to_numpy=True):
|
||||
# x = self.preprocessors(x)
|
||||
x = self.model.predict(x.merged_data[0])
|
||||
|
||||
if isinstance(self.outputs, (State, Action)):
|
||||
if self.outputs.isDiscrete():
|
||||
x = np.argmax(x)
|
||||
elif self.outputs.isContinuous():
|
||||
x = 2 * np.array(x) - 1
|
||||
else:
|
||||
raise NotImplementedError("The outputs are not discrete or continuous...")
|
||||
self.outputs.data = x
|
||||
# x = self.postprocessors(x)
|
||||
# return x
|
||||
return self.outputs
|
||||
|
||||
def set_network(self, genome=None, config=None):
|
||||
self.model.set_network(genome, config)
|
||||
|
||||
def update_config(self, config):
|
||||
self.model.update_config(config)
|
||||
|
||||
|
||||
# Tests
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
|
||||
class Processor(object):
|
||||
r"""Processor
|
||||
|
||||
This class describes pre- and post- processors. Specifically, it describes how to process the data before and
|
||||
after the policy.
|
||||
"""
|
||||
def __init__(self, inputs, outputs):
|
||||
self.inputs = inputs
|
||||
self.outputs = outputs
|
||||
|
||||
|
||||
class PreProcessor(Processor):
|
||||
r"""Preprocessor
|
||||
|
||||
It processes the input data before giving it to the policy/controller. For instance, it can be a state estimator
|
||||
such as a kalman filter.
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs):
|
||||
super(PreProcessor, self).__init__(inputs, outputs)
|
||||
|
||||
|
||||
class PostProcessor(Processor):
|
||||
r"""Postprocessor
|
||||
|
||||
It processes the data outputted by the policy. For instance, the policy could output cartesian positions,
|
||||
and we could process this data using an inverse kinematic scheme to output joint data.
|
||||
"""
|
||||
|
||||
def __init__(self, inputs, outputs):
|
||||
super(PostProcessor, self).__init__(inputs, outputs)
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python
|
||||
"""Defines the various metrics used in different learning paradigms.
|
||||
|
||||
Dependencies:
|
||||
- `pyrobolearn.tasks`
|
||||
"""
|
||||
|
||||
__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 Metric(object):
|
||||
r"""Metric (abstract) class
|
||||
|
||||
The metric class contains the various metrics used to evaluate a certain learning paradigm (e.g. imitation
|
||||
learning, reinforcement learning, transfer learning, active learning, and so on).
|
||||
|
||||
It notably contains the functionalities to evaluate a certain task using the metric, and different to plot them.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class ILMetric(Metric):
|
||||
r"""Imitation Learning Metric
|
||||
|
||||
Metrics used in imitation learning.
|
||||
|
||||
References:
|
||||
[1] "Learning from Humans", Billard et al., 2016
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(ILMetric, self).__init__()
|
||||
|
||||
|
||||
class RLMetric(Metric):
|
||||
r"""Reinforcement Learning Metric
|
||||
|
||||
Metrics used in reinforcement learning.
|
||||
|
||||
References:
|
||||
[1] "Deep Reinforcement Learning that Matters", Henderson et al., 2018
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(RLMetric, self).__init__()
|
||||
|
||||
|
||||
class TLMetric(Metric):
|
||||
r"""Transfer Learning Metric
|
||||
|
||||
Metrics used in transfer learning.
|
||||
|
||||
References:
|
||||
[1] "A Survey on Transfer Learning", Pan et al., 2010
|
||||
[2] "Transfer Learning for Reinforcement Learning Domains: A Survey", Taylor et al., 2009
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(TLMetric, self).__init__()
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
# import general policy
|
||||
from policy import Policy
|
||||
|
||||
# import basic policies
|
||||
from basic_policy import *
|
||||
|
||||
# import nn policies
|
||||
from nn_policy import *
|
||||
|
||||
# import dmp policies
|
||||
from dmp_policy import *
|
||||
|
||||
# import cpg policies
|
||||
from cpg_policy import *
|
||||
|
||||
# import neat policies
|
||||
from neat_policy import *
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define various basic policies.
|
||||
|
||||
Define the various basic policies such as the random policy, linear policy, policies based on value functions, etc.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from policy import Policy
|
||||
from pyrobolearn.approximators import LinearApproximator
|
||||
|
||||
|
||||
__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 RandomPolicy(Policy):
|
||||
"""Random policy
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, seed=None, *args, **kwargs):
|
||||
super(RandomPolicy, self).__init__(states, actions, *args, **kwargs)
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
def act(self, state=None, deterministic=False, to_numpy=True):
|
||||
# get the space of each action
|
||||
spaces = self.actions.space
|
||||
|
||||
# sample from each space
|
||||
action_data = [space.sample() for space in spaces]
|
||||
|
||||
# set the data for each action
|
||||
self.actions.data = action_data
|
||||
|
||||
return self.actions
|
||||
|
||||
def sample(self, state):
|
||||
return self.act(state)
|
||||
|
||||
def reset(self, seed=None):
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
|
||||
class LinearPolicy(Policy):
|
||||
"""Linear Policy
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, *args, **kwargs):
|
||||
model = LinearApproximator(states, actions)
|
||||
super(LinearPolicy, self).__init__(states, actions, model, *args, **kwargs)
|
||||
|
||||
def act(self, state, deterministic=True, to_numpy=True):
|
||||
return self.model.predict(state, to_numpy=to_numpy)
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class PolicyFromValue(Policy):
|
||||
r"""Policy From Value Function Approximator
|
||||
|
||||
.. math::
|
||||
|
||||
a = argmax_a Q^\pi(s,a)
|
||||
|
||||
.. seealso::
|
||||
|
||||
* `value.py`
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, *args, **kwargs):
|
||||
super(PolicyFromValue, self).__init__(states, actions, *args, **kwargs)
|
||||
|
||||
def act(self, state=None, deterministic=True, to_numpy=True):
|
||||
pass
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the Central Pattern Generator (CPG) Policy.
|
||||
|
||||
Define the various CPG policies that can be used.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from pyrobolearn.models import CPGNetwork
|
||||
from policy import Policy
|
||||
from pyrobolearn.states import State
|
||||
from pyrobolearn.actions import Action, JointPositionAction
|
||||
|
||||
|
||||
__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 CPGPolicy(Policy):
|
||||
r"""Central Pattern Generator (CPG) Network policy
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, timesteps=100, rate=1, *args, **kwargs):
|
||||
super(CPGPolicy, self).__init__(states, actions, rate=rate, *args, **kwargs)
|
||||
|
||||
# check actions
|
||||
if not isinstance(actions, JointPositionAction):
|
||||
raise TypeError("Expecting the actions to be an instance of JointPositionAction, instead got: "
|
||||
"{}".format(type(actions)))
|
||||
|
||||
# create CPG network based on the robot kinematic structures
|
||||
|
||||
# get specified legs
|
||||
robot = actions.robot
|
||||
joints = set(actions.joints)
|
||||
legs = []
|
||||
for robot_leg in robot.legs:
|
||||
leg = []
|
||||
for joint in robot_leg:
|
||||
if joint in joints:
|
||||
leg.append(joint)
|
||||
legs.append(leg)
|
||||
|
||||
num_legs = len(legs)
|
||||
|
||||
# define few variables to initialize the CPG nodes
|
||||
# variables for the node
|
||||
init_phi = 0.
|
||||
offset = 0.
|
||||
amplitude = 1.
|
||||
freq = 1.
|
||||
# variables for coupling the nodes
|
||||
weight_legs = 1. / len(legs)
|
||||
if len(legs) > 0 and len(legs[0]) > 0:
|
||||
weight_leg = 1. / len(legs[0])
|
||||
else:
|
||||
weight_leg = 0.
|
||||
bias = 0.
|
||||
|
||||
# create the CPG network based on the robot kinematic structures
|
||||
nodes = {}
|
||||
for leg_idx, leg in enumerate(legs):
|
||||
for idx, joint in enumerate(leg):
|
||||
# proper node parameters
|
||||
node = {'phi': init_phi, 'offset': offset, 'amplitude': amplitude, 'freq': freq}
|
||||
|
||||
# coupling parameters
|
||||
|
||||
# if first upper joint in the leg, connect it with the other upper joints (in the other legs)
|
||||
if idx == 0:
|
||||
for l in legs:
|
||||
if len(l) > 0 and joint != l[0]: # i.e. not the same node
|
||||
coupling_params = {'id': l[0], 'weight': weight_legs, 'bias': bias}
|
||||
node.setdefault('nodes', []).append(coupling_params)
|
||||
|
||||
# add coupling to current joint (except the last one) with the next joint in the leg
|
||||
if idx < len(leg)-1:
|
||||
# add next node
|
||||
coupling_params = {'id': leg[idx + 1], 'weight': weight_leg, 'bias': bias}
|
||||
node.setdefault('nodes', []).append(coupling_params)
|
||||
|
||||
# add coupling to current joint (except the first one) with the previous joint in the leg
|
||||
if idx > 0:
|
||||
# add next node
|
||||
coupling_params = {'id': leg[idx - 1], 'weight': weight_leg, 'bias': bias}
|
||||
node.setdefault('nodes', []).append(coupling_params)
|
||||
|
||||
# add node in the CPG network dictionary
|
||||
nodes[joint] = node
|
||||
|
||||
# create learning model
|
||||
self.model = CPGNetwork(nodes=nodes, timesteps=timesteps)
|
||||
|
||||
def _size(self, x):
|
||||
size = 0
|
||||
if isinstance(x, (State, Action)):
|
||||
if x.isDiscrete():
|
||||
size = x.space[0].n
|
||||
else:
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
def act(self, state=None, deterministic=True, to_numpy=True):
|
||||
if (self.cnt % self.rate) == 0:
|
||||
self.last_action = self.model.step()
|
||||
self.cnt += 1
|
||||
# angles = self.model.step()
|
||||
# self.actions.data = angles
|
||||
self.actions.data = self.last_action
|
||||
return self.actions
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
|
||||
def phase_resetting(self):
|
||||
self.model.reset()
|
||||
pass
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the Dynamic Movement Primitive (DMP) Policy.
|
||||
|
||||
Define the various DMP policies that can be used.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from pyrobolearn.models import DMP, DiscreteDMP, RhythmicDMP, BioDiscreteDMP
|
||||
from policy import Policy
|
||||
from pyrobolearn.states import State
|
||||
from pyrobolearn.actions import Action, JointPositionAction, JointVelocityAction, JointAccelerationAction
|
||||
|
||||
__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 DMPPolicy(Policy):
|
||||
r"""Dynamic Movement Primitive (DMP) policy
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, model, rate=1, *args, **kwargs):
|
||||
if not isinstance(model, DMP):
|
||||
raise TypeError("Expecting model to be an instance of DMP")
|
||||
super(DMPPolicy, self).__init__(states, actions, model, rate=rate, *args, **kwargs)
|
||||
self.y, self.dy, self.ddy = 0, 0, 0
|
||||
|
||||
def _size(self, x):
|
||||
size = 0
|
||||
if isinstance(x, (State, Action)):
|
||||
if x.isDiscrete():
|
||||
size = x.space[0].n
|
||||
else:
|
||||
size = x.totalSize()
|
||||
elif isinstance(x, np.ndarray):
|
||||
size = x.size
|
||||
elif isinstance(x, torch.Tensor):
|
||||
size = x.numel()
|
||||
elif isinstance(x, int):
|
||||
size = x
|
||||
return size
|
||||
|
||||
def act(self, state, deterministic=True, to_numpy=True):
|
||||
# return self.model.predict(state, to_numpy=to_numpy)
|
||||
if (self.cnt % self.rate) == 0:
|
||||
self.y, self.dy, self.ddy = self.model.step(state.data[0][0])
|
||||
self.cnt += 1
|
||||
# y, dy, ddy = self.model.step()
|
||||
# return np.array([y, dy, ddy])
|
||||
if isinstance(self.actions, JointPositionAction):
|
||||
self.actions.data = self.y
|
||||
elif isinstance(self.actions, JointVelocityAction):
|
||||
self.actions.data = self.dy
|
||||
elif isinstance(self.actions, JointAccelerationAction):
|
||||
self.actions.data = self.ddy
|
||||
return self.actions
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
|
||||
def rollout(self):
|
||||
return self.model.rollout()
|
||||
|
||||
def imitate(self, data):
|
||||
if len(data) > 0:
|
||||
print("Imitating with :", data.shape)
|
||||
# y, dy, ddy = data
|
||||
y = data
|
||||
# if len(y.shape) == 1:
|
||||
# y = y.reshape(1, -1)
|
||||
# if len(dy.shape) == 1:
|
||||
# dy = dy.reshape(1, -1)
|
||||
# if len(ddy.shape) == 1:
|
||||
# ddy = ddy.reshape(1, -1)
|
||||
self.model.imitate(y, plot=True) # dy, ddy, plot=True) # dy, ddy)
|
||||
else:
|
||||
print("Nothing to imitate.")
|
||||
|
||||
|
||||
class DiscreteDMPPolicy(DMPPolicy):
|
||||
r"""Discrete DMP Policy
|
||||
"""
|
||||
|
||||
def __init__(self, actions, states=None, num_basis=20, dt=0.01, y0=0, goal=1, forcing_terms=None,
|
||||
stiffness=None, damping=None, rate=1):
|
||||
if not isinstance(actions, Action):
|
||||
raise TypeError("Expecting actions to be an instance of the 'Action' class.")
|
||||
model = DiscreteDMP(num_dmps=self._size(actions), num_basis=num_basis, dt=dt, y0=y0, goal=goal,
|
||||
forcing_terms=forcing_terms, stiffness=stiffness, damping=damping)
|
||||
super(DiscreteDMPPolicy, self).__init__(states, actions, model, rate=rate)
|
||||
|
||||
|
||||
class RhythmicDMPPolicy(DMPPolicy):
|
||||
r"""Rhythmic DMP Policy
|
||||
"""
|
||||
|
||||
def __init__(self, actions, states=None, num_basis=20, dt=0.01, y0=0, goal=1, forcing_terms=None,
|
||||
stiffness=None, damping=None, rate=1):
|
||||
model = RhythmicDMP(num_dmps=self._size(actions), num_basis=num_basis, dt=dt, y0=y0, goal=goal,
|
||||
forcing_terms=forcing_terms, stiffness=stiffness, damping=damping)
|
||||
super(RhythmicDMPPolicy, self).__init__(states, actions, model, rate=rate)
|
||||
|
||||
|
||||
class BioDiscreteDMPPolicy(DMPPolicy):
|
||||
r"""Bio Discrete DMP Policy
|
||||
"""
|
||||
|
||||
def __init__(self, actions, states=None, num_basis=20, dt=0.01, y0=0, goal=1, forcing_terms=None,
|
||||
stiffness=None, damping=None, rate=1):
|
||||
model = BioDiscreteDMP(num_dmps=self._size(actions), num_basis=num_basis, dt=dt, y0=y0, goal=goal,
|
||||
forcing_terms=forcing_terms, stiffness=stiffness, damping=damping)
|
||||
super(BioDiscreteDMPPolicy, self).__init__(states, actions, model, rate=rate)
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the NEAT policy class.
|
||||
|
||||
This uses the Neuro-Evolution through Augmenting topologies (NEAT) framework. It allows the evolution of not only the
|
||||
parameters/weights but also the topological structure of neural networks. Note that the model associated with
|
||||
this policy (i.e. the neural network) is tightly coupled with the algorithm that modifies it.
|
||||
"""
|
||||
|
||||
|
||||
import numpy as np
|
||||
import cPickle as pickle
|
||||
|
||||
try:
|
||||
# from neat import nn, population, config, statistics
|
||||
import neat
|
||||
except ImportError as e:
|
||||
raise ImportError(e.__str__() + "\n HINT: you can install NEAT directly via 'pip install neat-python'.")
|
||||
|
||||
from policy import Policy
|
||||
from pyrobolearn.approximators import NEATApproximator
|
||||
|
||||
|
||||
__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 NEATPolicy(Policy):
|
||||
r"""NEAT Policy
|
||||
|
||||
NEAT stands for "Neuro-Evolution through Augmenting Topologies" [1] and allows the evolution of not only the
|
||||
parameters/weights but also the topological structure of neural networks. The model associated with this
|
||||
policy (i.e. the neural network) is tightly coupled with the algorithm that modifies it.
|
||||
By the structure of the neural network, we mean the type (i.e. forward or recurrent) and number of connection,
|
||||
as well as the type (i.e. using non-linearity activation function) and number of nodes can change.
|
||||
|
||||
Exploration is thus carried out in the parameter and hyper-parameter spaces.
|
||||
|
||||
Warnings: The associated algorithm is a little bit special and currently only works with the corresponding
|
||||
policy/learning model.
|
||||
|
||||
References:
|
||||
[1] "Evolving Neural Networks through Augmenting Topologies", Stanley et al., 2002
|
||||
[2] NEAT-Python
|
||||
- documentation: https://neat-python.readthedocs.io/en/latest/index.html
|
||||
- github repo: https://github.com/CodeReclaimers/neat-python
|
||||
[3] PyTorch NEAT (built upon NEAT-Python): https://github.com/uber-research/PyTorch-NEAT
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, num_hidden=0, activation_fct='relu', network_type='feedforward',
|
||||
aggregation='sum', weights_limits=(-20, 20), bias_limits=(-20, 20), rate=1, *args, **kwargs):
|
||||
r"""Initialize the neural network policy for the NEAT algorithm.
|
||||
"""
|
||||
model = NEATApproximator(states, actions, num_hidden=num_hidden, activation_fct=activation_fct,
|
||||
network_type=network_type, aggregation=aggregation, weights_limits=weights_limits,
|
||||
bias_limits=bias_limits)
|
||||
super(NEATPolicy, self).__init__(states, actions, model, rate=rate, *args, **kwargs)
|
||||
|
||||
##############
|
||||
# Properties #
|
||||
##############
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return the config object"""
|
||||
return self.model.config
|
||||
|
||||
@config.setter
|
||||
def config(self, config):
|
||||
"""Set the config file (str) or object."""
|
||||
self.model.config = config
|
||||
|
||||
@property
|
||||
def genome(self):
|
||||
return self.model.genome
|
||||
|
||||
@genome.setter
|
||||
def genome(self, genome):
|
||||
self.model.genome = genome
|
||||
|
||||
@property
|
||||
def network(self):
|
||||
return self.model.network
|
||||
|
||||
@property
|
||||
def population(self):
|
||||
return self.model.population
|
||||
|
||||
###########
|
||||
# Methods #
|
||||
###########
|
||||
|
||||
def update_config(self, config):
|
||||
self.model.update_config(config)
|
||||
|
||||
def set_network(self, genome=None, config=None):
|
||||
self.model.set_network(genome, config)
|
||||
|
||||
def act(self, state, deterministic=True):
|
||||
if (self.cnt % self.rate) == 0:
|
||||
self.last_action = self.model.predict(state)
|
||||
self.cnt += 1
|
||||
return self.last_action
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class NEATFeedForwardPolicy(NEATPolicy):
|
||||
r"""NEAT feed-forward policy
|
||||
|
||||
This creates a feed-forward network policy.
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, genome):
|
||||
super(NEATFeedForwardPolicy, self).__init__(states, actions, genome, network_type='feedforward')
|
||||
|
||||
|
||||
class NEATRecurrentPolicy(NEATPolicy):
|
||||
r"""NEAT recurrent policy
|
||||
|
||||
This creates a recurrent network policy.
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, genome):
|
||||
super(NEATRecurrentPolicy, self).__init__(states, actions, genome, network_type='recurrent')
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the Neural Network (NN) Policies.
|
||||
|
||||
Define the various neural network policies that can be used.
|
||||
"""
|
||||
|
||||
from pyrobolearn.approximators import NNApproximator, MLPApproximator
|
||||
from policy import Policy
|
||||
|
||||
|
||||
__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 NNPolicy(Policy):
|
||||
r"""Neural Network Policy
|
||||
|
||||
Defines the neural network policy. If the model is not given,
|
||||
|
||||
Examples:
|
||||
simulator = Bullet()
|
||||
robot = Robot(simulator)
|
||||
policy = NNPolicy(Robot, states=['joint_positions', 'joint_velocities'], actions=['joint_positions'])
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, model=None, *args, **kwargs):
|
||||
if model is None:
|
||||
raise ValueError("Expecting a NN model for the NN policy")
|
||||
else:
|
||||
# checking the input dimension of the model and the dimension of states
|
||||
# checking the output dimension of the model and the dimension of actions
|
||||
pass
|
||||
|
||||
super(NNPolicy, self).__init__(states, actions, model, *args, **kwargs)
|
||||
|
||||
def act(self, state, deterministic=True):
|
||||
pass
|
||||
|
||||
def sample(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class MLPPolicy(NNPolicy):
|
||||
r"""Multi-Layer Perceptron (MLP) Policy
|
||||
|
||||
Defines a MLP policy, which is a feedforward fully-connected neural network with linear layers and nonlinear
|
||||
activation functions.
|
||||
"""
|
||||
|
||||
def __init__(self, states, actions, hidden_units=(),
|
||||
activation_fct='linear', last_activation_fct=None, dropout_prob=None,
|
||||
preprocessors=None, postprocessors=None):
|
||||
"""Initialize MLP policy.
|
||||
|
||||
Args:
|
||||
states (State): 1D-states that is feed to the policy (the input dimensions will be inferred from the
|
||||
states)
|
||||
actions (Action): 1D-actions outputted by the policy and will be applied in the simulator (the output
|
||||
dimensions will be inferred from the actions)
|
||||
hidden_units (list/tuple of int): number of hidden units in the corresponding layer
|
||||
activation_fct (None, str, or list/tuple of str/None): activation function to be applied after each layer.
|
||||
If list/tuple, then it has to match the
|
||||
last_activation_fct (None or str): last activation function to be applied. If not specified, it will check
|
||||
if it is in the list/tuple of activation functions provided for the
|
||||
previous argument.
|
||||
dropout_prob (None, float, or list/tuple of float/None): dropout probability.
|
||||
"""
|
||||
model = MLPApproximator(states, actions, hidden_units=hidden_units,
|
||||
activation_fct=activation_fct, last_activation_fct=last_activation_fct,
|
||||
dropout_prob=dropout_prob, preprocessors=preprocessors, postprocessors=postprocessors)
|
||||
super(MLPPolicy, self).__init__(states, actions, model)
|
||||
|
||||
def act(self, state, deterministic=True):
|
||||
return self.model.predict(state)
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the basic Policy class.
|
||||
|
||||
A policy couples one or several learning model(s), the state, and action together. In this framework, the policy
|
||||
usually represents the robot's "brain".
|
||||
|
||||
Dependencies:
|
||||
- `pyrobolearn.states`
|
||||
- `pyrobolearn.actions`
|
||||
- `pyrobolearn.approximators` (and thus `pyrobolearn.models`)
|
||||
"""
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import pickle
|
||||
import torch
|
||||
|
||||
from pyrobolearn.states import State
|
||||
from pyrobolearn.actions import Action
|
||||
|
||||
from pyrobolearn.models import Model
|
||||
from pyrobolearn.approximators import Approximator, NNApproximator
|
||||
|
||||
|
||||
__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 Policy(object):
|
||||
r"""Abstract `Policy` class.
|
||||
|
||||
A policy maps a state to an action, and is often denoted as :math:`\pi_{\theta}(a_t|s_t)`, where :math:`\theta`
|
||||
represents the policy parameters. It represents the cognition of the agent(s).
|
||||
In our framework, the policy groups the learning model, state, and action objects.
|
||||
|
||||
Specifically, the policy is dissociated from the learning model, as a learning model can be used for different
|
||||
purposes. For instance, a neural network can be used to represent a policy but also a value function approximator,
|
||||
thus we separate these 2 notions (policy and learning model).
|
||||
|
||||
The policy is also loosely dissociated from the simulator and more specifically from the agent's body, as this last
|
||||
one is seen as being part of the environment. The states and actions are what connects the policy with the
|
||||
environment (and thus the simulator). The states and actions are given to the policy, and allows to build
|
||||
automatically a learning model (if not given) by inferring the dimensions of the inputs and outputs of the model.
|
||||
|
||||
.. note::
|
||||
|
||||
Exploration can be carried out by the policy, by specifying the exploration strategy (that is, exploration
|
||||
in the parameter or action space).
|
||||
|
||||
Example::
|
||||
|
||||
# create simulator
|
||||
simulator = BulletSim(render=True)
|
||||
|
||||
# create world
|
||||
world = BasicWorld(simulator)
|
||||
|
||||
# create robot
|
||||
robot = world.loadRobot('robot_name')
|
||||
# or load the robot (via urdf) and spawns it in the simulator
|
||||
#robot = Robot(simulator)
|
||||
#world.loadRobot(robot) # a robot is part of the world (if not done, it will be done inside Env)
|
||||
|
||||
# create states / actions
|
||||
states = JntPositionState(robot) + JntVelocityState(robot)
|
||||
actions = JntPositionAction(robot)
|
||||
|
||||
# optional: create learning model (if defined, it has to agree with the dimensions of states/actions)
|
||||
model = NN(...)
|
||||
|
||||
# create policy (if learning model not defined, it will create it inside)
|
||||
policy = Policy(states, actions, model)
|
||||
|
||||
# create rewards/costs (i.e. r(s,a,s')): gives robot, or state/actions
|
||||
reward = ForWardProgressReward(robot) - FallenCost(robot) - PowerConsumptionCost(robot)
|
||||
|
||||
# create environment to interact with
|
||||
env = Env(world, states, rewards)
|
||||
|
||||
# create and run task
|
||||
task = Task(env, policy)
|
||||
task.run()
|
||||
|
||||
# Optional: create RL algo (see RL_Algo)
|
||||
|
||||
.. seealso::
|
||||
|
||||
* `state.py`: describes the various states
|
||||
* `action.py`: describes the various actions
|
||||
* `model.py`: describes the abstract learning model class
|
||||
* `exploration.py`: describes how to explore using the policy
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self, states, actions, model=None, rate=1, preprocessors=None, postprocessors=None,
|
||||
distribution=None, *args, **kwargs):
|
||||
r"""
|
||||
Initialize a policy, the learning model.
|
||||
|
||||
Args:
|
||||
states (State): By giving the `states` to the policy, it can automatically infer the type and size/shape of
|
||||
each state, and thus can be used to automatically build a policy. At each step, the `states`
|
||||
are filled by the environment, and read by the policy. The `state` connects the policy with
|
||||
one or several objects (including robots) in the environment.
|
||||
Note that some policies don't use any state information.
|
||||
actions (Action): At each step, by calling `policy.act(state)`, the `actions` are computed by the policy,
|
||||
and should be given to the environment. As with the `states`, the type and size/shape of
|
||||
each action can be inferred and could be used to automatically build a policy.
|
||||
The `action` connects the policy with a controllable object (such as a robot) in the
|
||||
environment.
|
||||
model (Model, Approximator, None): inner model or approximator
|
||||
rate (int): rate at which the policy operates
|
||||
distribution:
|
||||
args:
|
||||
kwargs:
|
||||
"""
|
||||
self.states = states
|
||||
self.actions = actions
|
||||
self.model = model
|
||||
self.train_mode = False
|
||||
self.rate = rate
|
||||
self.cnt = 0
|
||||
self.last_action = None
|
||||
|
||||
##############
|
||||
# Properties #
|
||||
##############
|
||||
|
||||
@property
|
||||
def states(self):
|
||||
return self._states
|
||||
|
||||
@states.setter
|
||||
def states(self, states):
|
||||
if states is not None:
|
||||
if not isinstance(states, State):
|
||||
raise TypeError("Expecting states to be an instance of State.")
|
||||
self._states = states
|
||||
|
||||
@property
|
||||
def actions(self):
|
||||
return self._actions
|
||||
|
||||
@actions.setter
|
||||
def actions(self, actions):
|
||||
if not isinstance(actions, Action):
|
||||
raise TypeError("Expecting actions to be an instance of Action.")
|
||||
self._actions = actions
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self._model
|
||||
|
||||
@model.setter
|
||||
def model(self, model):
|
||||
if model is not None and not isinstance(model, Approximator):
|
||||
# Try to wrap it with the corresponding Approximator
|
||||
if isinstance(model, Model):
|
||||
model = Approximator(inputs=self.states, outputs=self.actions, model=model)
|
||||
elif isinstance(model, torch.nn.Module):
|
||||
model = NNApproximator(inputs=self.states, outputs=self.actions, model=model)
|
||||
# else:
|
||||
# raise TypeError("Expecting the model to be an instance of Model.")
|
||||
self._model = model
|
||||
|
||||
@property
|
||||
def rate(self):
|
||||
"""Return the rate at which the policy operates."""
|
||||
return self._rate
|
||||
|
||||
@rate.setter
|
||||
def rate(self, rate):
|
||||
"""Set the rate at which the policy operates."""
|
||||
if not isinstance(rate, int):
|
||||
raise TypeError("Expecting the rate to be an integer.")
|
||||
self._rate = rate
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
"""
|
||||
Return an iterator over the learning model parameters.
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.parameters()
|
||||
|
||||
@property
|
||||
def hyperparameters(self):
|
||||
"""
|
||||
Return an iterator over the learning model hyperparameters.
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.hyperparameters()
|
||||
|
||||
@property
|
||||
def input_dims(self):
|
||||
"""
|
||||
Return the input dimension of the policy.
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.get_input_dims()
|
||||
|
||||
@property
|
||||
def output_dims(self):
|
||||
"""
|
||||
Return the output dimension of the policy.
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.get_output_dims()
|
||||
|
||||
@property
|
||||
def num_parameters(self):
|
||||
"""Return the total number of parameters"""
|
||||
return self.model.num_parameters
|
||||
|
||||
###########
|
||||
# Methods #
|
||||
###########
|
||||
|
||||
def is_deterministic(self):
|
||||
"""
|
||||
Return True if the policy is deterministic; that is, given the same states result in the same actions.
|
||||
.. math:: a_t = f(s_t)
|
||||
|
||||
Returns:
|
||||
bool: True if the policy is deterministic
|
||||
"""
|
||||
return self.model.is_deterministic()
|
||||
|
||||
def is_stochastic(self):
|
||||
"""
|
||||
Return True if the policy is stochastic; that is, given the same states can result in different actions.
|
||||
.. math:: a_t ~ p(a_t|s_t)
|
||||
|
||||
Returns:
|
||||
bool: True if the policy is stochastic
|
||||
"""
|
||||
return self.model.is_stochastic()
|
||||
|
||||
def is_parametric(self):
|
||||
"""
|
||||
Return True if the policy is parametric.
|
||||
|
||||
Returns:
|
||||
bool: True if the policy is parametric.
|
||||
"""
|
||||
return self.model.is_parametric()
|
||||
|
||||
def is_linear(self):
|
||||
"""
|
||||
Return True if the policy is linear (wrt the parameters). This can be for instance useful for some learning
|
||||
algorithms (some only works on linear models).
|
||||
|
||||
Returns:
|
||||
bool: True if it is a linear policy
|
||||
"""
|
||||
return self.model.is_linear()
|
||||
|
||||
def is_recurrent(self):
|
||||
"""
|
||||
Return True if the policy is recurrent. This can be for instance useful for some learning algorithms which
|
||||
change their behavior when they deal with recurrent learning models.
|
||||
|
||||
Returns:
|
||||
bool: True if it is a recurrent policy.
|
||||
"""
|
||||
raise self.model.is_recurrent()
|
||||
|
||||
def get_vectorized_parameters(self, to_numpy=True):
|
||||
return self.model.get_vectorized_parameters(to_numpy=to_numpy)
|
||||
|
||||
def set_vectorized_parameters(self, vector):
|
||||
self.model.set_vectorized_parameters(vector=vector)
|
||||
|
||||
@abstractmethod
|
||||
def act(self, state, deterministic=True, to_numpy=True):
|
||||
"""
|
||||
Perform the action given the state.
|
||||
|
||||
Args:
|
||||
state (State): current state
|
||||
deterministic (bool): True by default. It can only be set to False, if the policy is stochastic.
|
||||
to_numpy (bool): if True, return a np.array
|
||||
|
||||
Returns:
|
||||
Action: action
|
||||
"""
|
||||
if self.model is not None:
|
||||
if (self.cnt % self.rate) == 0:
|
||||
self.last_action = self.model.predict(state, to_numpy=to_numpy)
|
||||
self.cnt += 1
|
||||
return self.last_action
|
||||
# predict = act
|
||||
|
||||
@abstractmethod
|
||||
def sample(self, state):
|
||||
"""
|
||||
Given the state, sample from the policy. This only works if the inner model of the policy is stochastic.
|
||||
|
||||
Args:
|
||||
state (State, array): current state
|
||||
|
||||
Returns:
|
||||
array: sample
|
||||
"""
|
||||
pass
|
||||
|
||||
def train(self, mode=True):
|
||||
"""
|
||||
Set the policy to train mode.
|
||||
|
||||
Args:
|
||||
mode (bool): if True, set the policy in train mode.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.train_mode = mode
|
||||
|
||||
def reset(self, *args, **kwargs):
|
||||
"""
|
||||
Reset the policy.
|
||||
"""
|
||||
self.model.reset()
|
||||
|
||||
def get_params(self):
|
||||
"""
|
||||
Return the learning model parameters.
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.get_params()
|
||||
|
||||
def get_hyperparams(self):
|
||||
"""
|
||||
Return the learning model hyperparameters
|
||||
"""
|
||||
if self.model is None:
|
||||
return None
|
||||
return self.model.get_hyperparams()
|
||||
|
||||
def save(self, filename):
|
||||
"""
|
||||
Save the policy in the given filename.
|
||||
|
||||
Args:
|
||||
filename (str): file to save the policy into
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# self.model.save(filename)
|
||||
pickle.dump(self, open(filename, 'wb'))
|
||||
|
||||
@staticmethod
|
||||
def load(filename):
|
||||
"""
|
||||
Load the policy from the given file.
|
||||
|
||||
Args:
|
||||
filename (str): file to load the policy from
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# self.model.load(filename)
|
||||
return pickle.load(open(filename, 'rb'))
|
||||
|
||||
#############
|
||||
# Operators #
|
||||
#############
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.act(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.model.__str__()
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
# import processors
|
||||
from processor import *
|
||||
|
||||
# import linear processors
|
||||
from linear_processor import
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the Linear Processor class.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from processor import Processor
|
||||
|
||||
|
||||
class LinearProcessor(Processor):
|
||||
r"""Linear Processor
|
||||
|
||||
Linear processor is a linear model: :math:`y = ax + b` where :math:`x` is the input, :math:`y` is the output,
|
||||
and :math:`a` and :math:`b` are given and fixed coefficients (slope and bias).
|
||||
"""
|
||||
|
||||
def __init__(self, a, b):
|
||||
super(LinearProcessor, self).__init__()
|
||||
self.a = torch.Tensor(a)
|
||||
self.b = torch.Tensor(b)
|
||||
|
||||
def compute(self, x):
|
||||
if isinstance(x, np.ndarray):
|
||||
x = torch.from_numpy(x).float()
|
||||
y = self.a * x + self.b
|
||||
return y.numpy()
|
||||
return self.a * x + self.b
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the Processor class.
|
||||
|
||||
Processors are rules that are applied to the inputs and outputs of a learning model before being processed by the
|
||||
model or after. Processors might have parameters but they do not have trainable/optimizable parameters; the parameters
|
||||
are fixed and given at the beginning.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
class Processor(object):
|
||||
r"""Processor
|
||||
|
||||
Processors are rules that are applied to the inputs and outputs of a model before being processed by the model
|
||||
or after. Processors might have parameters but they do not have trainable/optimizable parameters; the parameters
|
||||
are fixed and given at the beginning.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def compute(self, x):
|
||||
pass
|
||||
|
||||
def __call__(self, x):
|
||||
return self.compute(x)
|
||||
|
||||
|
||||
class CenterProcessor(Processor):
|
||||
r"""Center Processor
|
||||
|
||||
Center the data by the given mean; that is, it returned: :math:`\hat{x} = x - \mu` where :math:`\mu` is the mean.
|
||||
"""
|
||||
|
||||
def __init__(self, mean):
|
||||
super(CenterProcessor, self).__init__()
|
||||
self.mean = torch.Tensor(mean)
|
||||
|
||||
def compute(self, x):
|
||||
if isinstance(x, np.ndarray):
|
||||
x = torch.from_numpy(x).float()
|
||||
x -= self.mean
|
||||
return x.numpy()
|
||||
return x - self.mean
|
||||
|
||||
|
||||
class StandardizerProcessor(Processor):
|
||||
r"""Standardizer Processor
|
||||
|
||||
Processor that standardize the given data; the returned data is centered around 0 with a standard deviation of 1.
|
||||
That is, it returned :math:`\hat{x} = \frac{x - \mu}{\sigma}`, where :math:`\mu` is the mean, and :math:`\sigma`
|
||||
is the standard deviation.
|
||||
"""
|
||||
|
||||
def __init__(self, mean, std):
|
||||
super(StandardizerProcessor, self).__init__()
|
||||
self.mean = torch.Tensor(mean)
|
||||
self.std = torch.Tensor(std)
|
||||
|
||||
def compute(self, x):
|
||||
if isinstance(x, np.ndarray):
|
||||
x = torch.from_numpy(x).float()
|
||||
x = (x - self.mean) / (self.std + 1.e-13)
|
||||
return x.numpy()
|
||||
return (x - self.mean) / (self.std + 1.e-13)
|
||||
|
||||
|
||||
class NormalizerProcessor(Processor):
|
||||
r"""Normalizer Processor
|
||||
|
||||
Processor that normalize the given data; the returned data will be between 0 and 1.
|
||||
That is, it returned :math:`\hat{x} = \frac{x - x_{min}}{x_{max} - x_{min}}`, where
|
||||
:math:`x \in [x_{min}, x_{max}]`.
|
||||
"""
|
||||
|
||||
def __init__(self, xmin, xmax):
|
||||
super(NormalizerProcessor, self).__init__()
|
||||
self.xmin = torch.Tensor(xmin)
|
||||
self.xmax = torch.Tensor(xmax)
|
||||
if torch.allclose(self.xmin, self.xmax):
|
||||
raise ValueError("The given arguments 'xmin' and 'xmax' are the same.")
|
||||
|
||||
def compute(self, x):
|
||||
if isinstance(x, np.ndarray):
|
||||
x = torch.from_numpy(x).float()
|
||||
x = (x - self.xmin) / (self.xmax - self.xmin)
|
||||
return x.numpy()
|
||||
return (x - self.xmin) / (self.xmax - self.xmin)
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# import recorders
|
||||
from recorder import *
|
||||
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python
|
||||
"""Define the recorder classes.
|
||||
|
||||
The recorders allow to record the data from a source at a certain rate.
|
||||
For instance, it can record the robot states and actions. This can be useful for imitation learning tasks where
|
||||
the user demonstrates a certain skill through teleoperation or kinesthetic teaching. Using the recorder, you can
|
||||
record the data to be replayed later to the learning model.
|
||||
|
||||
Dependencies:
|
||||
- `pyrobolearn.states`
|
||||
- `pyrobolearn.actions`
|
||||
"""
|
||||
|
||||
# Memory vs Storage vs Recorder vs Sampler
|
||||
|
||||
import pickle
|
||||
import copy
|
||||
import time
|
||||
|
||||
from pyrobolearn.states import State
|
||||
from pyrobolearn.actions import Action
|
||||
|
||||
__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 Recorder(object):
|
||||
"""Recorder
|
||||
|
||||
This class allows to record the given data, and save it to a file.
|
||||
"""
|
||||
|
||||
def __init__(self, filename=None):
|
||||
"""
|
||||
Initialize the recorder.
|
||||
|
||||
Args:
|
||||
filename (str, None): file to save/load the data. If None, it will generate a filename based on the class
|
||||
name and the current local time.
|
||||
"""
|
||||
if filename is None:
|
||||
filename = self.__class__.__name__ + time.strftime("_%d-%m-%Y_%Hh%Mm%Ss", time.localtime())
|
||||
self.filename = filename
|
||||
# data row
|
||||
self.data = []
|
||||
# data "matrix" (it is not really a matrix because each row may have different dimension)
|
||||
self.all_data = []
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the current number of data points recorded.
|
||||
"""
|
||||
return len(self.data)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Return iterator over the data.
|
||||
|
||||
Returns:
|
||||
iterator
|
||||
"""
|
||||
return iter(self.data)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Access the data from the recorder.
|
||||
|
||||
Args:
|
||||
key (int, slice): data index
|
||||
|
||||
Returns:
|
||||
np.array: data
|
||||
"""
|
||||
return self.data[key]
|
||||
|
||||
def __contains__(self, item):
|
||||
"""
|
||||
Check if the given item is in the recorder.
|
||||
|
||||
Warnings: Currently, this is an O(N) operation. Need to use OrderedSet.
|
||||
|
||||
Args:
|
||||
item: item to check if it is in the recorder
|
||||
|
||||
Returns:
|
||||
bool: True if the item is in the recorder
|
||||
"""
|
||||
return item in self.data
|
||||
|
||||
def add(self, data):
|
||||
"""
|
||||
Add the given data to the recorder.
|
||||
|
||||
Args:
|
||||
data: data to add to the recorder
|
||||
"""
|
||||
self.data.append(data)
|
||||
|
||||
# alias
|
||||
__lshift__ = add
|
||||
|
||||
def add_row(self):
|
||||
"""
|
||||
Add a new data row in the list of data.
|
||||
"""
|
||||
self.all_data.append(self.data)
|
||||
self.data = []
|
||||
|
||||
def remove(self, key=0):
|
||||
"""
|
||||
Remove and return the specified data from the recorded data.
|
||||
|
||||
Args:
|
||||
key (int): data index
|
||||
"""
|
||||
return self.data.pop(key)
|
||||
|
||||
def remove_last_entry(self):
|
||||
"""
|
||||
Remove and return the last recorded datum.
|
||||
"""
|
||||
return self.remove(key=-1)
|
||||
|
||||
def remove_first_entry(self):
|
||||
"""
|
||||
Remove and return the first recorded datum.
|
||||
"""
|
||||
return self.remove(key=0)
|
||||
|
||||
def remove_row(self, key=0):
|
||||
"""
|
||||
Remove and return a data row in the list of all data.
|
||||
|
||||
Args:
|
||||
key: data row index
|
||||
"""
|
||||
return self.all_data.pop(key)
|
||||
|
||||
def remove_last_row(self):
|
||||
"""
|
||||
Remove and return the last data row from the data "matrix".
|
||||
"""
|
||||
return self.remove_row(key=-1)
|
||||
|
||||
def remove_first_row(self):
|
||||
"""
|
||||
Remove and return the first data row from the data "matrix".
|
||||
"""
|
||||
return self.remove_row(key=0)
|
||||
|
||||
def save(self, filename=None, append=True):
|
||||
"""
|
||||
Save the recorded data into the specified filename.
|
||||
|
||||
Args:
|
||||
filename (str, None): filename to save the data. If None, use the default one provided at the beginning.
|
||||
append (bool): If True, it will append the data to the end of the file
|
||||
"""
|
||||
if filename is None:
|
||||
filename = self.filename
|
||||
mode = "wba" if append else "wb"
|
||||
with open(filename, mode) as f:
|
||||
pickle.dump(self.data, f)
|
||||
|
||||
def load(self, filename=None):
|
||||
"""
|
||||
Load the recorded data from the specified filename.
|
||||
|
||||
Args:
|
||||
filename (str, None): filename to load the data from. If None, use the default one provided at the
|
||||
beginning.
|
||||
"""
|
||||
if filename is None:
|
||||
filename = self.filename
|
||||
with open(filename, "rb") as f:
|
||||
self.data = pickle.load(f)
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Return a generator over the recorded data.
|
||||
|
||||
Returns:
|
||||
generator
|
||||
"""
|
||||
for data in self.data:
|
||||
yield data
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the recorder; empty it.
|
||||
"""
|
||||
self.data = []
|
||||
|
||||
|
||||
class DataRecorder(Recorder):
|
||||
r"""Data Recorder
|
||||
|
||||
This class is useful to record data from a source. Basically, we specify what we wish to record.
|
||||
"""
|
||||
|
||||
def __init__(self, source, filename=None, rate=1, update=True):
|
||||
"""Initialize the recorder.
|
||||
|
||||
Args:
|
||||
source (object): instance that needs to have the property variable `data`. At each acquisition step,
|
||||
we append/save `source.data`
|
||||
filename (str, None): file to save/load the data. If None, it will generate a filename based on the class
|
||||
name and the current local time.
|
||||
rate (int): sampling rate
|
||||
update (bool): update the source by calling it if callable.
|
||||
"""
|
||||
|
||||
# Get world and simulator
|
||||
super(DataRecorder, self).__init__(filename)
|
||||
|
||||
# useful variables
|
||||
if not hasattr(source, 'data'):
|
||||
raise AttributeError("The given source doesn't have the 'data' attribute")
|
||||
self.src = source
|
||||
self.rate = rate
|
||||
self.cnt = 0
|
||||
self.update = update
|
||||
|
||||
def record(self):
|
||||
"""
|
||||
Record the data.
|
||||
"""
|
||||
if (self.cnt % self.rate) == 0:
|
||||
self.cnt = 0
|
||||
self.update_source()
|
||||
self.data.append(copy.deepcopy(self.src.merged_data))
|
||||
self.cnt += 1
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.record()
|
||||
|
||||
def update_source(self):
|
||||
if self.update and callable(self.src):
|
||||
self.src()
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Return a generator over the recorded data.
|
||||
|
||||
Returns:
|
||||
generator
|
||||
"""
|
||||
for data in self.data:
|
||||
self.src.data = data
|
||||
yield data
|
||||
|
||||
|
||||
class StateRecorder(DataRecorder):
|
||||
r"""State recorder
|
||||
|
||||
Record the state.
|
||||
"""
|
||||
|
||||
def __init__(self, states, filename=None, rate=1, update=True):
|
||||
"""
|
||||
Record the given states at each `rate` time steps.
|
||||
|
||||
Args:
|
||||
states (State): states to save
|
||||
filename (str, None): file to save/load the states. If None, it will generate a filename based on the class
|
||||
name and the current local time.
|
||||
rate (int): sampling rate
|
||||
update (bool): update the states by calling it if callable.
|
||||
"""
|
||||
if not isinstance(states, State):
|
||||
raise TypeError("Expecting 'states' to be an instance of State")
|
||||
super(StateRecorder, self).__init__(source=states, filename=filename, rate=rate, update=update)
|
||||
|
||||
|
||||
class ActionRecorder(DataRecorder):
|
||||
r"""Action recorder
|
||||
|
||||
Record the action.
|
||||
"""
|
||||
|
||||
def __init__(self, actions, filename=None, rate=1, update=True):
|
||||
"""
|
||||
Record the given actions at each `rate` time steps.
|
||||
|
||||
Args:
|
||||
actions (Action): actions to save
|
||||
filename (str, None): file to save/load the states. If None, it will generate a filename based on the class
|
||||
name and the current local time.
|
||||
rate (int): sampling rate
|
||||
update (bool): update the actions by calling it if callable.
|
||||
"""
|
||||
if not isinstance(actions, Action):
|
||||
raise TypeError("Expecting 'actions' to be an instance of Action")
|
||||
super(ActionRecorder, self).__init__(source=actions, filename=filename, rate=rate, update=update)
|
||||
Reference in New Issue
Block a user