Skip to content

Commit 76db0b9

Browse files
author
Ervin T
committed
[debug] Require all behavior names to have a matching YAML entry (#5210)
* Add strict check to settings.py * Remove warning from trainer factory, add test * Add changelog * Fix test * Update changelog * Remove strict CLI options * Remove strict option, rename, make strict default * Remove newline * Update comments * Set default dict to actually default to a default dict * Fix tests * Fix tests again * Default trainer dict to requiring all fields * Fix settings typing * Use logger * Add default_settings to error (cherry picked from commit 86a4070)
1 parent cacfc8c commit 76db0b9

File tree

5 files changed

+66
-13
lines changed

5 files changed

+66
-13
lines changed

com.unity.ml-agents/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ This results in much less memory being allocated during inference with `CameraSe
6161

6262
#### ml-agents / ml-agents-envs / gym-unity (Python)
6363
- Some console output have been moved from `info` to `debug` and will not be printed by default. If you want all messages to be printed, you can run `mlagents-learn` with the `--debug` option or add the line `debug: true` at the top of the yaml config file. (#5211)
64+
- When using a configuration YAML, it is required to define all behaviors found in a Unity
65+
executable in the trainer configuration YAML, or specify `default_settings`. (#5210)
6466
- The embedding size of attention layers used when a BufferSensor is in the scene has been changed. It is now fixed to 128 units. It might be impossible to resume training from a checkpoint of a previous version. (#5272)
6567

6668
### Bug Fixes

ml-agents/mlagents/trainers/settings.py

+29-6
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ def _check_batch_size_seq_length(self, attribute, value):
661661
)
662662

663663
@staticmethod
664-
def dict_to_defaultdict(d: Dict, t: type) -> DefaultDict:
664+
def dict_to_trainerdict(d: Dict, t: type) -> "TrainerSettings.DefaultTrainerDict":
665665
return TrainerSettings.DefaultTrainerDict(
666666
cattr.structure(d, Dict[str, TrainerSettings])
667667
)
@@ -718,12 +718,26 @@ def __init__(self, *args):
718718
super().__init__(*args)
719719
else:
720720
super().__init__(TrainerSettings, *args)
721+
self._config_specified = True
722+
723+
def set_config_specified(self, require_config_specified: bool) -> None:
724+
self._config_specified = require_config_specified
721725

722726
def __missing__(self, key: Any) -> "TrainerSettings":
723727
if TrainerSettings.default_override is not None:
724-
return copy.deepcopy(TrainerSettings.default_override)
728+
self[key] = copy.deepcopy(TrainerSettings.default_override)
729+
elif self._config_specified:
730+
raise TrainerConfigError(
731+
f"The behavior name {key} has not been specified in the trainer configuration. "
732+
f"Please add an entry in the configuration file for {key}, or set default_settings."
733+
)
725734
else:
726-
return TrainerSettings()
735+
logger.warn(
736+
f"Behavior name {key} does not match any behaviors specified "
737+
f"in the trainer configuration file. A default configuration will be used."
738+
)
739+
self[key] = TrainerSettings()
740+
return self[key]
727741

728742

729743
# COMMAND LINE #########################################################################
@@ -788,7 +802,7 @@ class TorchSettings:
788802
@attr.s(auto_attribs=True)
789803
class RunOptions(ExportableSettings):
790804
default_settings: Optional[TrainerSettings] = None
791-
behaviors: DefaultDict[str, TrainerSettings] = attr.ib(
805+
behaviors: TrainerSettings.DefaultTrainerDict = attr.ib(
792806
factory=TrainerSettings.DefaultTrainerDict
793807
)
794808
env_settings: EnvironmentSettings = attr.ib(factory=EnvironmentSettings)
@@ -800,7 +814,8 @@ class RunOptions(ExportableSettings):
800814
# These are options that are relevant to the run itself, and not the engine or environment.
801815
# They will be left here.
802816
debug: bool = parser.get_default("debug")
803-
# Strict conversion
817+
818+
# Convert to settings while making sure all fields are valid
804819
cattr.register_structure_hook(EnvironmentSettings, strict_to_cls)
805820
cattr.register_structure_hook(EngineSettings, strict_to_cls)
806821
cattr.register_structure_hook(CheckpointSettings, strict_to_cls)
@@ -816,7 +831,7 @@ class RunOptions(ExportableSettings):
816831
)
817832
cattr.register_structure_hook(TrainerSettings, TrainerSettings.structure)
818833
cattr.register_structure_hook(
819-
DefaultDict[str, TrainerSettings], TrainerSettings.dict_to_defaultdict
834+
TrainerSettings.DefaultTrainerDict, TrainerSettings.dict_to_trainerdict
820835
)
821836
cattr.register_unstructure_hook(collections.defaultdict, defaultdict_to_dict)
822837

@@ -839,8 +854,12 @@ def from_argparse(args: argparse.Namespace) -> "RunOptions":
839854
"engine_settings": {},
840855
"torch_settings": {},
841856
}
857+
_require_all_behaviors = True
842858
if config_path is not None:
843859
configured_dict.update(load_config(config_path))
860+
else:
861+
# If we're not loading from a file, we don't require all behavior names to be specified.
862+
_require_all_behaviors = False
844863

845864
# Use the YAML file values for all values not specified in the CLI.
846865
for key in configured_dict.keys():
@@ -868,6 +887,10 @@ def from_argparse(args: argparse.Namespace) -> "RunOptions":
868887
configured_dict[key] = val
869888

870889
final_runoptions = RunOptions.from_dict(configured_dict)
890+
# Need check to bypass type checking but keep structure on dict working
891+
if isinstance(final_runoptions.behaviors, TrainerSettings.DefaultTrainerDict):
892+
# configure whether or not we should require all behavior names to be found in the config YAML
893+
final_runoptions.behaviors.set_config_specified(_require_all_behaviors)
871894
return final_runoptions
872895

873896
@staticmethod

ml-agents/mlagents/trainers/tests/test_settings.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def test_no_configuration():
7777
Verify that a new config will have a PPO trainer with extrinsic rewards.
7878
"""
7979
blank_runoptions = RunOptions()
80+
blank_runoptions.behaviors.set_config_specified(False)
8081
assert isinstance(blank_runoptions.behaviors["test"], TrainerSettings)
8182
assert isinstance(blank_runoptions.behaviors["test"].hyperparameters, PPOSettings)
82-
8383
assert (
8484
RewardSignalType.EXTRINSIC in blank_runoptions.behaviors["test"].reward_signals
8585
)
@@ -508,7 +508,7 @@ def test_default_settings():
508508
default_settings_cls = cattr.structure(default_settings, TrainerSettings)
509509
check_if_different(default_settings_cls, run_options.behaviors["test2"])
510510

511-
# Check that an existing beehavior overrides the defaults in specified fields
511+
# Check that an existing behavior overrides the defaults in specified fields
512512
test1_settings = run_options.behaviors["test1"]
513513
assert test1_settings.max_steps == 2
514514
assert test1_settings.network_settings.hidden_units == 2000
@@ -519,6 +519,37 @@ def test_default_settings():
519519
check_if_different(test1_settings, default_settings_cls)
520520

521521

522+
def test_config_specified():
523+
# Test require all behavior names to be specified (or not)
524+
# Remove any pre-set defaults
525+
TrainerSettings.default_override = None
526+
behaviors = {"test1": {"max_steps": 2, "network_settings": {"hidden_units": 2000}}}
527+
run_options_dict = {"behaviors": behaviors}
528+
ro = RunOptions.from_dict(run_options_dict)
529+
# Don't require all behavior names
530+
ro.behaviors.set_config_specified(False)
531+
# Test that we can grab an entry that is not in the dict.
532+
assert isinstance(ro.behaviors["test2"], TrainerSettings)
533+
534+
# Create strict RunOptions with no defualt_settings
535+
run_options_dict = {"behaviors": behaviors}
536+
ro = RunOptions.from_dict(run_options_dict)
537+
# Require all behavior names
538+
ro.behaviors.set_config_specified(True)
539+
with pytest.raises(TrainerConfigError):
540+
# Variable must be accessed otherwise Python won't query the dict
541+
print(ro.behaviors["test2"])
542+
543+
# Create strict RunOptions with default settings
544+
default_settings = {"max_steps": 1, "network_settings": {"num_layers": 1000}}
545+
run_options_dict = {"default_settings": default_settings, "behaviors": behaviors}
546+
ro = RunOptions.from_dict(run_options_dict)
547+
# Require all behavior names
548+
ro.behaviors.set_config_specified(True)
549+
# Test that we can grab an entry that is not in the dict.
550+
assert isinstance(ro.behaviors["test2"], TrainerSettings)
551+
552+
522553
def test_pickle():
523554
# Make sure RunOptions is pickle-able.
524555
run_options = RunOptions()

ml-agents/mlagents/trainers/tests/test_trainer_util.py

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def test_handles_no_config_provided():
7171
"""
7272
brain_name = "testbrain"
7373
no_default_config = RunOptions().behaviors
74+
# Pretend this was created without a YAML file
75+
no_default_config.set_config_specified(False)
7476

7577
trainer_factory = TrainerFactory(
7678
trainer_config=no_default_config,

ml-agents/mlagents/trainers/trainer/trainer_factory.py

-5
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@ def __init__(
5656
self.ghost_controller = GhostController()
5757

5858
def generate(self, behavior_name: str) -> Trainer:
59-
if behavior_name not in self.trainer_config.keys():
60-
logger.warning(
61-
f"Behavior name {behavior_name} does not match any behaviors specified"
62-
f"in the trainer configuration file: {sorted(self.trainer_config.keys())}"
63-
)
6459
trainer_settings = self.trainer_config[behavior_name]
6560
return TrainerFactory._initialize_trainer(
6661
trainer_settings,

0 commit comments

Comments
 (0)