forked from openai/baselines
-
Notifications
You must be signed in to change notification settings - Fork 722
Expand file tree
/
Copy pathbase_class.py
More file actions
1088 lines (921 loc) · 47.5 KB
/
Copy pathbase_class.py
File metadata and controls
1088 lines (921 loc) · 47.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from abc import ABC, abstractmethod
import os
import glob
import warnings
from collections import OrderedDict
import json
import zipfile
import cloudpickle
import numpy as np
import gym
import tensorflow as tf
from stable_baselines.common import set_global_seeds
from stable_baselines.common.save_util import (
is_json_serializable, data_to_json, json_to_data, params_to_bytes, bytes_to_params
)
from stable_baselines.common.policies import get_policy_from_name, ActorCriticPolicy
from stable_baselines.common.vec_env import VecEnvWrapper, VecEnv, DummyVecEnv
from stable_baselines import logger
class BaseRLModel(ABC):
"""
The base RL model
:param policy: (BasePolicy) Policy object
:param env: (Gym environment) The environment to learn from
(if registered in Gym, can be str. Can be None for loading trained models)
:param verbose: (int) the verbosity level: 0 none, 1 training information, 2 tensorflow debug
:param requires_vec_env: (bool) Does this model require a vectorized environment
:param policy_base: (BasePolicy) the base policy used by this method
"""
def __init__(self, policy, env, verbose=0, *, requires_vec_env, policy_base, policy_kwargs=None):
if isinstance(policy, str) and policy_base is not None:
self.policy = get_policy_from_name(policy_base, policy)
else:
self.policy = policy
self.env = env
self.verbose = verbose
self._requires_vec_env = requires_vec_env
self.policy_kwargs = {} if policy_kwargs is None else policy_kwargs
self.observation_space = None
self.action_space = None
self.n_envs = None
self._vectorize_action = False
self.num_timesteps = 0
self.graph = None
self.sess = None
self.params = None
self._param_load_ops = None
self.initial_state = None
self.n_batch = None
self.nminibatches = None
self.n_steps = None
if env is not None:
if isinstance(env, str):
if self.verbose >= 1:
print("Creating environment from the given name, wrapped in a DummyVecEnv.")
self.env = env = DummyVecEnv([lambda: gym.make(env)])
self.observation_space = env.observation_space
self.action_space = env.action_space
if requires_vec_env:
if isinstance(env, VecEnv):
self.n_envs = env.num_envs
else:
raise ValueError("Error: the model requires a vectorized environment, please use a VecEnv wrapper.")
else:
if isinstance(env, VecEnv):
if env.num_envs == 1:
self.env = _UnvecWrapper(env)
self._vectorize_action = True
else:
raise ValueError("Error: the model requires a non vectorized environment or a single vectorized"
" environment.")
self.n_envs = 1
def get_env(self):
"""
returns the current environment (can be None if not defined)
:return: (Gym Environment) The current environment
"""
return self.env
def set_env(self, env):
"""
Checks the validity of the environment, and if it is coherent, set it as the current environment.
:param env: (Gym Environment) The environment for learning a policy
"""
if env is None and self.env is None:
if self.verbose >= 1:
print("Loading a model without an environment, "
"this model cannot be trained until it has a valid environment.")
return
elif env is None:
raise ValueError("Error: trying to replace the current environment with None")
# sanity checking the environment
assert self.observation_space == env.observation_space, \
"Error: the environment passed must have at least the same observation space as the model was trained on."
assert self.action_space == env.action_space, \
"Error: the environment passed must have at least the same action space as the model was trained on."
if self._requires_vec_env:
assert isinstance(env, VecEnv), \
"Error: the environment passed is not a vectorized environment, however {} requires it".format(
self.__class__.__name__)
assert not self.policy.recurrent or self.n_envs == env.num_envs, \
"Error: the environment passed must have the same number of environments as the model was trained on." \
"This is due to the Lstm policy not being capable of changing the number of environments."
self.n_envs = env.num_envs
else:
# for models that dont want vectorized environment, check if they make sense and adapt them.
# Otherwise tell the user about this issue
if isinstance(env, VecEnv):
if env.num_envs == 1:
env = _UnvecWrapper(env)
self._vectorize_action = True
else:
raise ValueError("Error: the model requires a non vectorized environment or a single vectorized "
"environment.")
else:
self._vectorize_action = False
self.n_envs = 1
self.env = env
def _init_num_timesteps(self, reset_num_timesteps=True):
"""
Initialize and resets num_timesteps (total timesteps since beginning of training)
if needed. Mainly used logging and plotting (tensorboard).
:param reset_num_timesteps: (bool) Set it to false when continuing training
to not create new plotting curves in tensorboard.
:return: (bool) Whether a new tensorboard log needs to be created
"""
if reset_num_timesteps:
self.num_timesteps = 0
new_tb_log = self.num_timesteps == 0
return new_tb_log
@abstractmethod
def setup_model(self):
"""
Create all the functions and tensorflow graphs necessary to train the model
"""
pass
def _setup_learn(self, seed):
"""
check the environment, set the seed, and set the logger
:param seed: (int) the seed value
"""
if self.env is None:
raise ValueError("Error: cannot train the model without a valid environment, please set an environment with"
"set_env(self, env) method.")
if seed is not None:
set_global_seeds(seed)
@abstractmethod
def get_parameter_list(self):
"""
Get tensorflow Variables of model's parameters
This includes all variables necessary for continuing training (saving / loading).
:return: (list) List of tensorflow Variables
"""
pass
def get_parameters(self):
"""
Get current model parameters as dictionary of variable name -> ndarray.
:return: (OrderedDict) Dictionary of variable name -> ndarray of model's parameters.
"""
parameters = self.get_parameter_list()
parameter_values = self.sess.run(parameters)
return_dictionary = OrderedDict((param.name, value) for param, value in zip(parameters, parameter_values))
return return_dictionary
def _setup_load_operations(self):
"""
Create tensorflow operations for loading model parameters
"""
# Assume tensorflow graphs are static -> check
# that we only call this function once
if self._param_load_ops is not None:
raise RuntimeError("Parameter load operations have already been created")
# For each loadable parameter, create appropiate
# placeholder and an assign op, and store them to
# self.load_param_ops as dict of variable.name -> (placeholder, assign)
loadable_parameters = self.get_parameter_list()
# Use OrderedDict to store order for backwards compatibility with
# list-based params
self._param_load_ops = OrderedDict()
with self.graph.as_default():
for param in loadable_parameters:
placeholder = tf.placeholder(dtype=param.dtype, shape=param.shape)
# param.name is unique (tensorflow variables have unique names)
self._param_load_ops[param.name] = (placeholder, param.assign(placeholder))
@abstractmethod
def _get_pretrain_placeholders(self):
"""
Return the placeholders needed for the pretraining:
- obs_ph: observation placeholder
- actions_ph will be population with an action from the environement
(from the expert dataset)
- deterministic_actions_ph: e.g., in the case of a gaussian policy,
the mean.
:return: ((tf.placeholder)) (obs_ph, actions_ph, deterministic_actions_ph)
"""
pass
def pretrain(self, dataset, n_epochs=10, learning_rate=1e-4,
adam_epsilon=1e-8, val_interval=None):
"""
Pretrain a model using behavior cloning:
supervised learning given an expert dataset.
NOTE: only Box and Discrete spaces are supported for now.
:param dataset: (ExpertDataset) Dataset manager
:param n_epochs: (int) Number of iterations on the training set
:param learning_rate: (float) Learning rate
:param adam_epsilon: (float) the epsilon value for the adam optimizer
:param val_interval: (int) Report training and validation losses every n epochs.
By default, every 10th of the maximum number of epochs.
:return: (BaseRLModel) the pretrained model
"""
continuous_actions = isinstance(self.action_space, gym.spaces.Box)
discrete_actions = isinstance(self.action_space, gym.spaces.Discrete)
assert discrete_actions or continuous_actions, 'Only Discrete and Box action spaces are supported'
# Validate the model every 10% of the total number of iteration
if val_interval is None:
# Prevent modulo by zero
if n_epochs < 10:
val_interval = 1
else:
val_interval = int(n_epochs / 10)
if self.policy.recurrent:
if self.nminibatches is None:
envs_per_batch = self.n_envs * self.n_steps
else:
batch_size = self.n_batch // self.nminibatches
envs_per_batch = batch_size // self.n_steps
with self.graph.as_default():
with tf.variable_scope('pretrain'):
if continuous_actions:
obs_ph, actions_ph, states_ph, snew_ph, dones_ph, \
deterministic_actions_ph = self._get_pretrain_placeholders()
loss = tf.reduce_mean(tf.square(actions_ph - deterministic_actions_ph))
else:
obs_ph, actions_ph, states_ph, snew_ph, dones_ph, \
actions_logits_ph = self._get_pretrain_placeholders()
# actions_ph has a shape if (n_batch,), we reshape it to (n_batch, 1)
# so no additional changes is needed in the dataloader
actions_ph = tf.expand_dims(actions_ph, axis=1)
one_hot_actions = tf.one_hot(actions_ph, self.action_space.n)
loss = tf.nn.softmax_cross_entropy_with_logits_v2(
logits=actions_logits_ph,
labels=tf.stop_gradient(one_hot_actions)
)
loss = tf.reduce_mean(loss)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate, epsilon=adam_epsilon)
optim_op = optimizer.minimize(loss, var_list=self.params)
self.sess.run(tf.global_variables_initializer())
if self.verbose > 0:
print("Pretraining with Behavior Cloning...")
for epoch_idx in range(int(n_epochs)):
train_loss = 0.0
if self.policy.recurrent:
state = self.initial_state[:envs_per_batch]
# Full pass on the training set
for _ in range(len(dataset.train_loader)):
expert_obs, expert_actions, expert_mask = dataset.get_next_batch('train')
feed_dict = {
obs_ph: expert_obs,
actions_ph: expert_actions,
}
if self.policy.recurrent:
feed_dict.update({states_ph: state, dones_ph: expert_mask})
state, train_loss_, _ = self.sess.run([snew_ph, loss, optim_op], feed_dict)
else:
train_loss_, _ = self.sess.run([loss, optim_op], feed_dict)
train_loss_, _ = self.sess.run([loss, optim_op], feed_dict)
train_loss += train_loss_
train_loss /= len(dataset.train_loader)
if self.verbose > 0 and (epoch_idx + 1) % val_interval == 0:
val_loss = 0.0
# Full pass on the validation set
for _ in range(len(dataset.val_loader)):
expert_obs, expert_actions, expert_mask = dataset.get_next_batch('val')
feed_dict = {
obs_ph: expert_obs,
actions_ph: expert_actions,
}
if self.policy.recurrent:
feed_dict.update({states_ph: state, dones_ph: expert_mask})
val_loss_, = self.sess.run([loss], feed_dict)
val_loss += val_loss_
val_loss /= len(dataset.val_loader)
if self.verbose > 0:
print("==== Training progress {:.2f}% ====".format(100 * (epoch_idx + 1) / n_epochs))
print('Epoch {}'.format(epoch_idx + 1))
print("Training loss: {:.6f}, Validation loss: {:.6f}".format(train_loss, val_loss))
print()
# Free memory
del expert_obs, expert_actions
if self.verbose > 0:
print("Pretraining done.")
return self
@abstractmethod
def learn(self, total_timesteps, callback=None, seed=None, log_interval=100, tb_log_name="run",
reset_num_timesteps=True):
"""
Return a trained model.
:param total_timesteps: (int) The total number of samples to train on
:param seed: (int) The initial seed for training, if None: keep current seed
:param callback: (function (dict, dict)) -> boolean function called at every steps with state of the algorithm.
It takes the local and global variables. If it returns False, training is aborted.
:param log_interval: (int) The number of timesteps before logging.
:param tb_log_name: (str) the name of the run for tensorboard log
:param reset_num_timesteps: (bool) whether or not to reset the current timestep number (used in logging)
:return: (BaseRLModel) the trained model
"""
pass
@abstractmethod
def predict(self, observation, state=None, mask=None, deterministic=False):
"""
Get the model's action from an observation
:param observation: (np.ndarray) the input observation
:param state: (np.ndarray) The last states (can be None, used in recurrent policies)
:param mask: (np.ndarray) The last masks (can be None, used in recurrent policies)
:param deterministic: (bool) Whether or not to return deterministic actions.
:return: (np.ndarray, np.ndarray) the model's action and the next state (used in recurrent policies)
"""
pass
@abstractmethod
def action_probability(self, observation, state=None, mask=None, actions=None, logp=False):
"""
If ``actions`` is ``None``, then get the model's action probability distribution from a given observation.
Depending on the action space the output is:
- Discrete: probability for each possible action
- Box: mean and standard deviation of the action output
However if ``actions`` is not ``None``, this function will return the probability that the given actions are
taken with the given parameters (observation, state, ...) on this model. For discrete action spaces, it
returns the probability mass; for continuous action spaces, the probability density. This is since the
probability mass will always be zero in continuous spaces, see http://blog.christianperone.com/2019/01/
for a good explanation
:param observation: (np.ndarray) the input observation
:param state: (np.ndarray) The last states (can be None, used in recurrent policies)
:param mask: (np.ndarray) The last masks (can be None, used in recurrent policies)
:param actions: (np.ndarray) (OPTIONAL) For calculating the likelihood that the given actions are chosen by
the model for each of the given parameters. Must have the same number of actions and observations.
(set to None to return the complete action probability distribution)
:param logp: (bool) (OPTIONAL) When specified with actions, returns probability in log-space.
This has no effect if actions is None.
:return: (np.ndarray) the model's (log) action probability
"""
pass
def load_parameters(self, load_path_or_dict, exact_match=True):
"""
Load model parameters from a file or a dictionary
Dictionary keys should be tensorflow variable names, which can be obtained
with ``get_parameters`` function. If ``exact_match`` is True, dictionary
should contain keys for all model's parameters, otherwise RunTimeError
is raised. If False, only variables included in the dictionary will be updated.
This does not load agent's hyper-parameters.
.. warning::
This function does not update trainer/optimizer variables (e.g. momentum).
As such training after using this function may lead to less-than-optimal results.
:param load_path_or_dict: (str or file-like or dict) Save parameter location
or dict of parameters as variable.name -> ndarrays to be loaded.
:param exact_match: (bool) If True, expects load dictionary to contain keys for
all variables in the model. If False, loads parameters only for variables
mentioned in the dictionary. Defaults to True.
"""
# Make sure we have assign ops
if self._param_load_ops is None:
self._setup_load_operations()
params = None
if isinstance(load_path_or_dict, dict):
# Assume `load_path_or_dict` is dict of variable.name -> ndarrays we want to load
params = load_path_or_dict
elif isinstance(load_path_or_dict, list):
warnings.warn("Loading model parameters from a list. This has been replaced " +
"with parameter dictionaries with variable names and parameters. " +
"If you are loading from a file, consider re-saving the file.",
DeprecationWarning)
# Assume `load_path_or_dict` is list of ndarrays.
# Create param dictionary assuming the parameters are in same order
# as `get_parameter_list` returns them.
params = dict()
for i, param_name in enumerate(self._param_load_ops.keys()):
params[param_name] = load_path_or_dict[i]
else:
# Assume a filepath or file-like.
# Use existing deserializer to load the parameters.
# We only need the parameters part of the file, so
# only load that part.
_, params = BaseRLModel._load_from_file(load_path_or_dict, load_data=False)
feed_dict = {}
param_update_ops = []
# Keep track of not-updated variables
not_updated_variables = set(self._param_load_ops.keys())
for param_name, param_value in params.items():
placeholder, assign_op = self._param_load_ops[param_name]
feed_dict[placeholder] = param_value
# Create list of tf.assign operations for sess.run
param_update_ops.append(assign_op)
# Keep track which variables are updated
not_updated_variables.remove(param_name)
# Check that we updated all parameters if exact_match=True
if exact_match and len(not_updated_variables) > 0:
raise RuntimeError("Load dictionary did not contain all variables. " +
"Missing variables: {}".format(", ".join(not_updated_variables)))
self.sess.run(param_update_ops, feed_dict=feed_dict)
@abstractmethod
def save(self, save_path, cloudpickle=False):
"""
Save the current parameters to file
:param save_path: (str or file-like) The save location
:param cloudpickle: (bool) Use older cloudpickle format instead of zip-archives.
"""
raise NotImplementedError()
@classmethod
@abstractmethod
def load(cls, load_path, env=None, custom_objects=None, **kwargs):
"""
Load the model from file
:param load_path: (str or file-like) the saved parameter location
:param env: (Gym Envrionment) the new environment to run the loaded model on
(can be None if you only need prediction from a trained model)
:param custom_objects: (dict) Dictionary of objects to replace
upon loading. If a variable is present in this dictionary as a
key, it will not be deserialized and the corresponding item
will be used instead. Similar to custom_objects in
`keras.models.load_model`. Useful when you have an object in
file that can not be deserialized.
:param kwargs: extra arguments to change the model when loading
"""
raise NotImplementedError()
@staticmethod
def _save_to_file_cloudpickle(save_path, data=None, params=None):
"""Legacy code for saving models with cloudpickle
:param save_path: (str or file-like) Where to store the model
:param data: (OrderedDict) Class parameters being stored
:param params: (OrderedDict) Model parameters being stored
"""
if isinstance(save_path, str):
_, ext = os.path.splitext(save_path)
if ext == "":
save_path += ".pkl"
with open(save_path, "wb") as file_:
cloudpickle.dump((data, params), file_)
else:
# Here save_path is a file-like object, not a path
cloudpickle.dump((data, params), save_path)
@staticmethod
def _save_to_file_zip(save_path, data=None, params=None):
"""Save model to a .zip archive
:param save_path: (str or file-like) Where to store the model
:param data: (OrderedDict) Class parameters being stored
:param params: (OrderedDict) Model parameters being stored
"""
# data/params can be None, so do not
# try to serialize them blindly
if data is not None:
serialized_data = data_to_json(data)
if params is not None:
serialized_params = params_to_bytes(params)
# We also have to store list of the parameters
# to store the ordering for OrderedDict.
# We can trust these to be strings as they
# are taken from the Tensorflow graph.
serialized_param_list = json.dumps(
list(params.keys()),
indent=4
)
# Check postfix if save_path is a string
if isinstance(save_path, str):
_, ext = os.path.splitext(save_path)
if ext == "":
save_path += ".zip"
# Create a zip-archive and write our objects
# there. This works when save_path
# is either str or a file-like
with zipfile.ZipFile(save_path, "w") as file_:
# Do not try to save "None" elements
if data is not None:
file_.writestr("data", serialized_data)
if params is not None:
file_.writestr("parameters", serialized_params)
file_.writestr("parameter_list", serialized_param_list)
@staticmethod
def _save_to_file(save_path, data=None, params=None, cloudpickle=False):
"""Save model to a zip archive or cloudpickle file.
:param save_path: (str or file-like) Where to store the model
:param data: (OrderedDict) Class parameters being stored
:param params: (OrderedDict) Model parameters being stored
:param cloudpickle: (bool) Use old cloudpickle format
(stable-baselines<=2.7.0) instead of a zip archive.
"""
if cloudpickle:
BaseRLModel._save_to_file_cloudpickle(save_path, data, params)
else:
BaseRLModel._save_to_file_zip(save_path, data, params)
@staticmethod
def _load_from_file_cloudpickle(load_path):
"""Legacy code for loading older models stored with cloudpickle
:param load_path: (str or file-like) where from to load the file
:return: (dict, OrderedDict) Class parameters and model parameters
"""
if isinstance(load_path, str):
if not os.path.exists(load_path):
if os.path.exists(load_path + ".pkl"):
load_path += ".pkl"
else:
raise ValueError("Error: the file {} could not be found".format(load_path))
with open(load_path, "rb") as file_:
data, params = cloudpickle.load(file_)
else:
# Here load_path is a file-like object, not a path
data, params = cloudpickle.load(load_path)
return data, params
@staticmethod
def _load_from_file(load_path, load_data=True, custom_objects=None):
"""Load model data from a .zip archive
:param load_path: (str or file-like) Where to load model from
:param load_data: (bool) Whether we should load and return data
(class parameters). Mainly used by `load_parameters` to
only load model parameters (weights).
:param custom_objects: (dict) Dictionary of objects to replace
upon loading. If a variable is present in this dictionary as a
key, it will not be deserialized and the corresponding item
will be used instead. Similar to custom_objects in
`keras.models.load_model`. Useful when you have an object in
file that can not be deserialized.
:return: (dict, OrderedDict) Class parameters and model parameters
"""
# Check if file exists if load_path is
# a string
if isinstance(load_path, str):
if not os.path.exists(load_path):
if os.path.exists(load_path + ".zip"):
load_path += ".zip"
else:
raise ValueError("Error: the file {} could not be found".format(load_path))
# Open the zip archive and load data.
try:
with zipfile.ZipFile(load_path, "r") as file_:
namelist = file_.namelist()
# If data or parameters is not in the
# zip archive, assume they were stored
# as None (_save_to_file allows this).
data = None
params = None
if "data" in namelist and load_data:
# Load class parameters and convert to string
# (Required for json library in Python 3.5)
json_data = file_.read("data").decode()
data = json_to_data(json_data, custom_objects=custom_objects)
if "parameters" in namelist:
# Load parameter list and and parameters
parameter_list_json = file_.read("parameter_list").decode()
parameter_list = json.loads(parameter_list_json)
serialized_params = file_.read("parameters")
params = bytes_to_params(
serialized_params, parameter_list
)
except zipfile.BadZipFile:
# load_path wasn't a zip file. Possibly a cloudpickle
# file. Show a warning and fall back to loading cloudpickle.
warnings.warn("It appears you are loading from a file with old format. " +
"Older cloudpickle format has been replaced with zip-archived " +
"models. Consider saving the model with new format.",
DeprecationWarning)
# Attempt loading with the cloudpickle format.
# If load_path is file-like, seek back to beginning of file
if not isinstance(load_path, str):
load_path.seek(0)
data, params = BaseRLModel._load_from_file_cloudpickle(load_path)
return data, params
@staticmethod
def _softmax(x_input):
"""
An implementation of softmax.
:param x_input: (numpy float) input vector
:return: (numpy float) output vector
"""
x_exp = np.exp(x_input.T - np.max(x_input.T, axis=0))
return (x_exp / x_exp.sum(axis=0)).T
@staticmethod
def _is_vectorized_observation(observation, observation_space):
"""
For every observation type, detects and validates the shape,
then returns whether or not the observation is vectorized.
:param observation: (np.ndarray) the input observation to validate
:param observation_space: (gym.spaces) the observation space
:return: (bool) whether the given observation is vectorized or not
"""
if isinstance(observation_space, gym.spaces.Box):
if observation.shape == observation_space.shape:
return False
elif observation.shape[1:] == observation_space.shape:
return True
else:
raise ValueError("Error: Unexpected observation shape {} for ".format(observation.shape) +
"Box environment, please use {} ".format(observation_space.shape) +
"or (n_env, {}) for the observation shape."
.format(", ".join(map(str, observation_space.shape))))
elif isinstance(observation_space, gym.spaces.Discrete):
if observation.shape == (): # A numpy array of a number, has shape empty tuple '()'
return False
elif len(observation.shape) == 1:
return True
else:
raise ValueError("Error: Unexpected observation shape {} for ".format(observation.shape) +
"Discrete environment, please use (1,) or (n_env, 1) for the observation shape.")
elif isinstance(observation_space, gym.spaces.MultiDiscrete):
if observation.shape == (len(observation_space.nvec),):
return False
elif len(observation.shape) == 2 and observation.shape[1] == len(observation_space.nvec):
return True
else:
raise ValueError("Error: Unexpected observation shape {} for MultiDiscrete ".format(observation.shape) +
"environment, please use ({},) or ".format(len(observation_space.nvec)) +
"(n_env, {}) for the observation shape.".format(len(observation_space.nvec)))
elif isinstance(observation_space, gym.spaces.MultiBinary):
if observation.shape == (observation_space.n,):
return False
elif len(observation.shape) == 2 and observation.shape[1] == observation_space.n:
return True
else:
raise ValueError("Error: Unexpected observation shape {} for MultiBinary ".format(observation.shape) +
"environment, please use ({},) or ".format(observation_space.n) +
"(n_env, {}) for the observation shape.".format(observation_space.n))
else:
raise ValueError("Error: Cannot determine if the observation is vectorized with the space type {}."
.format(observation_space))
class ActorCriticRLModel(BaseRLModel):
"""
The base class for Actor critic model
:param policy: (BasePolicy) Policy object
:param env: (Gym environment) The environment to learn from
(if registered in Gym, can be str. Can be None for loading trained models)
:param verbose: (int) the verbosity level: 0 none, 1 training information, 2 tensorflow debug
:param policy_base: (BasePolicy) the base policy used by this method (default=ActorCriticPolicy)
:param requires_vec_env: (bool) Does this model require a vectorized environment
"""
def __init__(self, policy, env, _init_setup_model, verbose=0, policy_base=ActorCriticPolicy,
requires_vec_env=False, policy_kwargs=None):
super(ActorCriticRLModel, self).__init__(policy, env, verbose=verbose, requires_vec_env=requires_vec_env,
policy_base=policy_base, policy_kwargs=policy_kwargs)
self.sess = None
self.initial_state = None
self.step = None
self.proba_step = None
self.params = None
@abstractmethod
def setup_model(self):
pass
@abstractmethod
def learn(self, total_timesteps, callback=None, seed=None,
log_interval=100, tb_log_name="run", reset_num_timesteps=True):
pass
def predict(self, observation, state=None, mask=None, deterministic=False):
if state is None:
state = self.initial_state
if mask is None:
mask = [False for _ in range(self.n_envs)]
observation = np.array(observation)
vectorized_env = self._is_vectorized_observation(observation, self.observation_space)
observation = observation.reshape((-1,) + self.observation_space.shape)
actions, _, states, _ = self.step(observation, state, mask, deterministic=deterministic)
clipped_actions = actions
# Clip the actions to avoid out of bound error
if isinstance(self.action_space, gym.spaces.Box):
clipped_actions = np.clip(actions, self.action_space.low, self.action_space.high)
if not vectorized_env:
if state is not None:
raise ValueError("Error: The environment must be vectorized when using recurrent policies.")
clipped_actions = clipped_actions[0]
return clipped_actions, states
def action_probability(self, observation, state=None, mask=None, actions=None, logp=False):
if state is None:
state = self.initial_state
if mask is None:
mask = [False for _ in range(self.n_envs)]
observation = np.array(observation)
vectorized_env = self._is_vectorized_observation(observation, self.observation_space)
observation = observation.reshape((-1,) + self.observation_space.shape)
actions_proba = self.proba_step(observation, state, mask)
if len(actions_proba) == 0: # empty list means not implemented
warnings.warn("Warning: action probability is not implemented for {} action space. Returning None."
.format(type(self.action_space).__name__))
return None
if actions is not None: # comparing the action distribution, to given actions
prob = None
logprob = None
actions = np.array([actions])
if isinstance(self.action_space, gym.spaces.Discrete):
actions = actions.reshape((-1,))
assert observation.shape[0] == actions.shape[0], \
"Error: batch sizes differ for actions and observations."
prob = actions_proba[np.arange(actions.shape[0]), actions]
elif isinstance(self.action_space, gym.spaces.MultiDiscrete):
actions = actions.reshape((-1, len(self.action_space.nvec)))
assert observation.shape[0] == actions.shape[0], \
"Error: batch sizes differ for actions and observations."
# Discrete action probability, over multiple categories
actions = np.swapaxes(actions, 0, 1) # swap axis for easier categorical split
prob = np.prod([proba[np.arange(act.shape[0]), act]
for proba, act in zip(actions_proba, actions)], axis=0)
elif isinstance(self.action_space, gym.spaces.MultiBinary):
actions = actions.reshape((-1, self.action_space.n))
assert observation.shape[0] == actions.shape[0], \
"Error: batch sizes differ for actions and observations."
# Bernoulli action probability, for every action
prob = np.prod(actions_proba * actions + (1 - actions_proba) * (1 - actions), axis=1)
elif isinstance(self.action_space, gym.spaces.Box):
actions = actions.reshape((-1, ) + self.action_space.shape)
mean, logstd = actions_proba
std = np.exp(logstd)
n_elts = np.prod(mean.shape[1:]) # first dimension is batch size
log_normalizer = n_elts/2 * np.log(2 * np.pi) + 1/2 * np.sum(logstd, axis=1)
# Diagonal Gaussian action probability, for every action
logprob = -np.sum(np.square(actions - mean) / (2 * std), axis=1) - log_normalizer
else:
warnings.warn("Warning: action_probability not implemented for {} actions space. Returning None."
.format(type(self.action_space).__name__))
return None
# Return in space (log or normal) requested by user, converting if necessary
if logp:
if logprob is None:
logprob = np.log(prob)
ret = logprob
else:
if prob is None:
prob = np.exp(logprob)
ret = prob
# normalize action proba shape for the different gym spaces
ret = ret.reshape((-1, 1))
else:
ret = actions_proba
if not vectorized_env:
if state is not None:
raise ValueError("Error: The environment must be vectorized when using recurrent policies.")
ret = ret[0]
return ret
def get_parameter_list(self):
return self.params
@abstractmethod
def save(self, save_path, cloudpickle=False):
pass
@classmethod
def load(cls, load_path, env=None, custom_objects=None, **kwargs):
"""
Load the model from file
:param load_path: (str or file-like) the saved parameter location
:param env: (Gym Envrionment) the new environment to run the loaded model on
(can be None if you only need prediction from a trained model)
:param custom_objects: (dict) Dictionary of objects to replace
upon loading. If a variable is present in this dictionary as a
key, it will not be deserialized and the corresponding item
will be used instead. Similar to custom_objects in
`keras.models.load_model`. Useful when you have an object in
file that can not be deserialized.
:param kwargs: extra arguments to change the model when loading
"""
data, params = cls._load_from_file(load_path, custom_objects=custom_objects)
if 'policy_kwargs' in kwargs and kwargs['policy_kwargs'] != data['policy_kwargs']:
raise ValueError("The specified policy kwargs do not equal the stored policy kwargs. "
"Stored kwargs: {}, specified kwargs: {}".format(data['policy_kwargs'],
kwargs['policy_kwargs']))
model = cls(policy=data["policy"], env=None, _init_setup_model=False)
model.__dict__.update(data)
model.__dict__.update(kwargs)
model.set_env(env)
model.setup_model()
model.load_parameters(params)
return model
class OffPolicyRLModel(BaseRLModel):
"""
The base class for off policy RL model
:param policy: (BasePolicy) Policy object
:param env: (Gym environment) The environment to learn from
(if registered in Gym, can be str. Can be None for loading trained models)
:param replay_buffer: (ReplayBuffer) the type of replay buffer
:param verbose: (int) the verbosity level: 0 none, 1 training information, 2 tensorflow debug
:param requires_vec_env: (bool) Does this model require a vectorized environment
:param policy_base: (BasePolicy) the base policy used by this method
"""
def __init__(self, policy, env, replay_buffer=None, _init_setup_model=False, verbose=0, *,
requires_vec_env=False, policy_base=None, policy_kwargs=None):
super(OffPolicyRLModel, self).__init__(policy, env, verbose=verbose, requires_vec_env=requires_vec_env,
policy_base=policy_base, policy_kwargs=policy_kwargs)
self.replay_buffer = replay_buffer
@abstractmethod
def setup_model(self):
pass
@abstractmethod
def learn(self, total_timesteps, callback=None, seed=None,
log_interval=100, tb_log_name="run", reset_num_timesteps=True, replay_wrapper=None):
pass
@abstractmethod
def predict(self, observation, state=None, mask=None, deterministic=False):
pass
@abstractmethod
def action_probability(self, observation, state=None, mask=None, actions=None, logp=False):
pass
@abstractmethod
def save(self, save_path, cloudpickle=False):
pass
@classmethod
def load(cls, load_path, env=None, custom_objects=None, **kwargs):
"""
Load the model from file
:param load_path: (str or file-like) the saved parameter location
:param env: (Gym Envrionment) the new environment to run the loaded model on
(can be None if you only need prediction from a trained model)
:param custom_objects: (dict) Dictionary of objects to replace
upon loading. If a variable is present in this dictionary as a
key, it will not be deserialized and the corresponding item
will be used instead. Similar to custom_objects in
`keras.models.load_model`. Useful when you have an object in
file that can not be deserialized.
:param kwargs: extra arguments to change the model when loading
"""
data, params = cls._load_from_file(load_path, custom_objects=custom_objects)
if 'policy_kwargs' in kwargs and kwargs['policy_kwargs'] != data['policy_kwargs']:
raise ValueError("The specified policy kwargs do not equal the stored policy kwargs. "
"Stored kwargs: {}, specified kwargs: {}".format(data['policy_kwargs'],
kwargs['policy_kwargs']))
model = cls(policy=data["policy"], env=None, _init_setup_model=False)
model.__dict__.update(data)
model.__dict__.update(kwargs)
model.set_env(env)
model.setup_model()
model.load_parameters(params)
return model
class _UnvecWrapper(VecEnvWrapper):
def __init__(self, venv):
"""
Unvectorize a vectorized environment, for vectorized environment that only have one environment
:param venv: (VecEnv) the vectorized environment to wrap
"""
super().__init__(venv)
assert venv.num_envs == 1, "Error: cannot unwrap a environment wrapper that has more than one environment."
def __getattr__(self, attr):
if attr in self.__dict__:
return getattr(self, attr)
return getattr(self.venv, attr)
def __set_attr__(self, attr, value):
if attr in self.__dict__:
setattr(self, attr, value)
else:
setattr(self.venv, attr, value)
def compute_reward(self, achieved_goal, desired_goal, _info):
return float(self.venv.env_method('compute_reward', achieved_goal, desired_goal, _info)[0])
@staticmethod
def unvec_obs(obs):
"""
:param obs: (Union[np.ndarray, dict])
:return: (Union[np.ndarray, dict])
"""
if not isinstance(obs, dict):
return obs[0]
obs_ = OrderedDict()
for key in obs.keys():
obs_[key] = obs[key][0]
del obs
return obs_