Source code for d2c.data.data

"""Loading data and constructing the buffer."""

import os
import logging
import numpy as np
from abc import ABC, abstractmethod
from collections import OrderedDict
from typing import Dict, Callable, Tuple, Union, List
from d2c.envs import BaseEnv, LeaEnv
from d2c.utils.utils import add_gaussian_noise
from d2c.utils.replaybuffer import ReplayBuffer
from d2c.utils.dataloader import D4rlDataLoader, BaseDataLoader, BaseBMLoader


[docs]class BaseData(ABC): """The basic class of the replay buffer. Inherit this class to build data loaders for practical application and benchmark experiments. The API methods the user should implement are: * :meth:`_build_data`: Construct the data through adding the transitions into the replay buffer. * :meth:`_build_data_loader`: Construct the data loader. :param str data_path: the file path of the dataset. """ def __init__( self, data_path: Union[str, List[str]], ) -> None: self._data_path = data_path self._data_loader = None self._data = None self._buffer_size = None self._build_data() @abstractmethod def _build_data(self) -> None: """Construct the data through adding the transitions into the replay buffer.""" pass @abstractmethod def _build_data_loader(self) -> None: """Construct the data loader.""" pass @property def data(self) -> ReplayBuffer: """Return the replay buffer.""" logging.info('='*20 + f'Get a data of size {self._data.size}' + '='*20) return self._data
[docs]class Data(BaseData): """Create the replay buffer with batch data that is from the benchmarks or real-world applications dataset. It uses different data loaders to get the transitions and adds them into the replay buffer. This class generates the dataset for the offline RL training. The main methods that users of this class need to know are: * :meth:`data`: Return the constructed replay buffer; * :meth:`_build_data_loader`: Read the batch data and create a data loader. \ It builds different data loader according to the corresponding parameters \ in the configuration. * :meth:`_build_data`: Create a replay buffer and add the data into it. .. note:: To add a data loader for a new benchmark, you should implement a new method `_xxx_data_loader`(like :meth:`_app_data_loader`, :meth:`_d4rl_data_loader`) and add this method into :meth:`_data_loader_list`. :param config: the configuration. """ def __init__(self, config) -> None: app_config = config.app_config env_config = config.model_config.env.external data_loader_name = config.model_config.train.data_loader_name self._split_ratio = config.model_config.train.data_split_ratio # Data split ratio self._device = config.model_config.train.device if data_loader_name == 'app': # For real-world application experiments. try: data_path = app_config.data_path except: raise AttributeError('There is no data file for real-world application experiments!') elif env_config.data_file_path is not None: data_path = env_config.data_file_path data_loader_name = env_config.benchmark_name else: raise ValueError('There is no data file to load!') assert isinstance(data_path, str) logging.info(f'Loading the offline dataset file from {data_path}.') if data_loader_name not in self._data_loader_list.keys(): raise NotImplementedError(f'There is no data loader for {data_loader_name}!') self._app_cfg = app_config self._env_cfg = env_config self._data_loader_name = data_loader_name super(Data, self).__init__(data_path) def _build_data(self) -> None: self._build_data_loader() transitions = self._data_loader.get_transitions(split_ratio=self._split_ratio) s1, a1, s2, a2, reward, cost, done = [transitions[k] for k in transitions.keys()] self._buffer_size = s1.shape[0] state_dim = s1.shape[-1] action_dim = a1.shape[-1] self._data = ReplayBuffer( state_dim, action_dim, self._buffer_size, self._device, ) self._data.add_transitions( state=s1, action=a1, next_state=s2, next_action=a2, reward=reward, done=done, cost=cost, ) def _build_data_loader(self) -> None: self._data_loader = self._data_loader_list[self._data_loader_name]() def _app_data_loader(self): raise NotImplementedError def _d4rl_data_loader(self) -> D4rlDataLoader: state_normalize = self._env_cfg.state_normalize reward_normalize = self._env_cfg.reward_normalize return D4rlDataLoader( self._data_path, state_normalize, reward_normalize, ) @property def _data_loader_list(self) -> Dict[str, Callable[..., BaseDataLoader]]: """A dict of the alternative Data loaders.""" return dict( app=self._app_data_loader, d4rl=self._d4rl_data_loader, ) @property def state_shift_scale(self) -> Tuple[np.ndarray, np.ndarray]: return self._data_loader.state_shift_scale
[docs]class DataNoise(Data): """Construct a dataset that with noised action. :param config: the configuration. :param BaseEnv env: the env to provide the action information(minimum and maximum). """ def __init__(self, config, env: LeaEnv) -> None: self._action_noise = config.model_config.train.action_noise self._action_space = env.action_space super(DataNoise, self).__init__(config) def _build_data(self) -> None: self._build_data_loader() transitions = self._data_loader.get_transitions() s1, a1, s2, a2, reward, cost, done = [transitions[k] for k in transitions.keys()] # Add noise to the action. a1 = add_gaussian_noise(a1, self._action_space, self._action_noise) a2 = add_gaussian_noise(a2, self._action_space, self._action_noise) self._buffer_size = s1.shape[0] state_dim = s1.shape[-1] action_dim = a1.shape[-1] self._data = ReplayBuffer( state_dim, action_dim, self._buffer_size, self._device, ) self._data.add_transitions( state=s1, action=a1, next_state=s2, next_action=a2, reward=reward, done=done, cost=cost, ) def _build_data_loader(self) -> None: super(DataNoise, self)._build_data_loader()
[docs]class DataMix(BaseData): """Construct a dataset by mixing several data.""" def __init__(self, config) -> None: env_config = config.model_config.env.external self._device = config.model_config.train.device if env_config.data_file_path is not None: data_path = env_config.data_file_path data_loader_name = env_config.benchmark_name else: raise ValueError('There is no data file to load!') assert isinstance(data_path, list) if isinstance(data_path[-1], str): # There is no mix ratio in the input. mix_ratio = [1] * len(data_path) data_files = data_path # The data file list. else: data_files = data_path[:-1] # The data file list. mix_ratio = data_path[-1] # The ratio of each data that chosen to be mixed. assert isinstance(mix_ratio, list) assert len(data_files) == len(mix_ratio) for _r in mix_ratio: assert 0 < _r <= 1, 'The ratio of the data should between (0, 1]!' self._mix_ratio = mix_ratio if data_loader_name not in self._data_loader_list.keys(): raise NotImplementedError(f'There is no data loader for {data_loader_name}!') self._env_cfg = env_config self._data_loader_name = data_loader_name self._obs_shift, self._obs_scale = None, None super(DataMix, self).__init__(data_files) def _build_data(self) -> None: transitions = self._multi_data_load() s1, a1, s2, a2, reward, cost, done = [transitions[k] for k in transitions.keys()] if self._env_cfg.state_normalize: s1, s2, self._obs_shift, self._obs_scale = BaseBMLoader.norm_state(s1, s2) if self._env_cfg.reward_normalize: reward = BaseBMLoader.norm_reward(reward) self._buffer_size = s1.shape[0] logging.info(f'The mix data size is {self._buffer_size}.') state_dim = s1.shape[-1] action_dim = a1.shape[-1] self._data = ReplayBuffer( state_dim, action_dim, self._buffer_size, self._device, ) self._data.add_transitions( state=s1, action=a1, next_state=s2, next_action=a2, reward=reward, done=done, cost=cost, ) def _build_data_loader(self) -> None: self._data_loader = self._data_loader_list[self._data_loader_name] @staticmethod def _d4rl_data_loader(data_path: str) -> D4rlDataLoader: state_normalize = False reward_normalize = False return D4rlDataLoader( data_path, state_normalize, reward_normalize, ) @property def _data_loader_list(self) -> Dict[str, Callable[..., BaseDataLoader]]: """A dict of the alternative Data loaders.""" return dict( d4rl=self._d4rl_data_loader, ) def _multi_data_load(self) -> OrderedDict: self._build_data_loader() logging.info('='*10+'Loading the mix datasets'+'='*10) transitions = None for i, file in enumerate(self._data_path): logging.info(f'Mix data {i}: Loading {os.path.basename(file)} | mix ratio: {self._mix_ratio[i]}.') data_loader = self._data_loader(file) trans = data_loader.get_transitions() _num = int(trans['s1'].shape[0] * self._mix_ratio[i]) logging.info(f'The number of the chosen transitions is {_num}.') if i == 0: for k, v in trans.items(): trans[k] = v[:_num] transitions = trans else: for k, v in trans.items(): transitions[k] = np.concatenate([transitions[k], v[:_num]]) return transitions @property def state_shift_scale(self) -> Tuple[np.ndarray, np.ndarray]: return self._obs_shift, self._obs_scale