浏览代码

Allow curricula to be created without files (#3145)

Previously the Curriculum and MetaCurriculum classes required file / folder
paths for initialization.  These methods loaded the configuration for the
curricula from the filesystem.  Requiring files for configuring curricula
makes testing and updating our config format more difficult.

This change moves the file loading into static methods, so that Curricula /
MetaCurricula can be initialized from dictionaries only.
/asymm-envs
GitHub 5 年前
当前提交
c6152459
共有 7 个文件被更改,包括 131 次插入149 次删除
  1. 58
      ml-agents/mlagents/trainers/curriculum.py
  2. 4
      ml-agents/mlagents/trainers/learn.py
  3. 118
      ml-agents/mlagents/trainers/meta_curriculum.py
  4. 32
      ml-agents/mlagents/trainers/tests/test_curriculum.py
  5. 56
      ml-agents/mlagents/trainers/tests/test_meta_curriculum.py
  6. 6
      ml-agents/mlagents/trainers/trainer_controller.py
  7. 6
      ml-agents/mlagents/trainers/trainer_util.py

58
ml-agents/mlagents/trainers/curriculum.py


import os
import json
import math
from typing import Dict, Any, TextIO

logger = logging.getLogger("mlagents.trainers")
class Curriculum(object):
def __init__(self, location):
class Curriculum:
def __init__(self, brain_name: str, config: Dict):
:param location: Path to JSON defining curriculum.
:param brain_name: Name of the brain this Curriculum is associated with
:param config: Dictionary of fields needed to configure the Curriculum
# The name of the brain should be the basename of the file without the
# extension.
self._brain_name = os.path.basename(location).split(".")[0]
self.data = Curriculum.load_curriculum_file(location)
self.brain_name = brain_name
self.config = config
self.smoothing_value = 0
self.smoothing_value = 0.0
for key in [
"parameters",
"measure",

]:
if key not in self.data:
if key not in self.config:
"{0} does not contain a " "{1} field.".format(location, key)
f"{brain_name} curriculum config does not contain a {key} field."
self.measure = self.data["measure"]
self.min_lesson_length = self.data["min_lesson_length"]
self.max_lesson_num = len(self.data["thresholds"])
self.measure = self.config["measure"]
self.min_lesson_length = self.config["min_lesson_length"]
self.max_lesson_num = len(self.config["thresholds"])
parameters = self.data["parameters"]
parameters = self.config["parameters"]
"The parameter {0} in Curriculum {1} must have {2} values "
"but {3} were found".format(
key, location, self.max_lesson_num + 1, len(parameters[key])
)
f"The parameter {key} in {brain_name}'s curriculum must have {self.max_lesson_num + 1} values "
f"but {len(parameters[key])} were found"
)
@property

steps completed).
:return Whether the lesson was incremented.
"""
if not self.data or not measure_val or math.isnan(measure_val):
if not self.config or not measure_val or math.isnan(measure_val):
if self.data["signal_smoothing"]:
if self.config["signal_smoothing"]:
if measure_val > self.data["thresholds"][self.lesson_num]:
if measure_val > self.config["thresholds"][self.lesson_num]:
parameters = self.data["parameters"]
parameters = self.config["parameters"]
self._brain_name,
self.brain_name,
self.lesson_num,
", ".join([str(x) + " -> " + str(config[x]) for x in config]),
)

current lesson is returned.
:return: The configuration of the reset parameters.
"""
if not self.data:
if not self.config:
parameters = self.data["parameters"]
parameters = self.config["parameters"]
def load_curriculum_file(location: str) -> None:
def load_curriculum_file(config_path: str) -> Dict:
with open(location) as data_file:
with open(config_path) as data_file:
"The file {0} could not be found.".format(location)
"The file {0} could not be found.".format(config_path)
"There was an error decoding {}".format(location)
"There was an error decoding {}".format(config_path)
def _load_curriculum(fp: TextIO) -> None:
def _load_curriculum(fp: TextIO) -> Dict:
try:
return json.load(fp)
except json.decoder.JSONDecodeError as e:

4
ml-agents/mlagents/trainers/learn.py


return None
else:
meta_curriculum = MetaCurriculum(curriculum_folder)
meta_curriculum = MetaCurriculum.from_directory(curriculum_folder)
meta_curriculum.set_all_curriculums_to_lesson_num(lesson)
meta_curriculum.set_all_curricula_to_lesson_num(lesson)
return meta_curriculum

118
ml-agents/mlagents/trainers/meta_curriculum.py


logger = logging.getLogger("mlagents.trainers")
class MetaCurriculum(object):
"""A MetaCurriculum holds curriculums. Each curriculum is associated to a
class MetaCurriculum:
"""A MetaCurriculum holds curricula. Each curriculum is associated to a
def __init__(self, curriculum_folder: str):
def __init__(self, curricula: Dict[str, Curriculum]):
Args:
curriculum_folder (str): The relative or absolute path of the
folder which holds the curriculums for this environment.
The folder should contain JSON files whose names are the
brains that the curriculums belong to.
default_reset_parameters (dict): The default reset parameters
of the environment.
:param curriculum_folder: Dictionary of brain_name to the
Curriculum for each brain.
self._brains_to_curriculums: Dict[str, Curriculum] = {}
self._brains_to_curricula: Dict[str, Curriculum] = {}
for brain_name, curriculum in curricula.items():
self._brains_to_curricula[brain_name] = curriculum
config_keys: Set[str] = set(curriculum.get_config().keys())
try:
for curriculum_filename in os.listdir(curriculum_folder):
# This process requires JSON files
brain_name, extension = os.path.splitext(curriculum_filename)
if extension.lower() != ".json":
continue
curriculum_filepath = os.path.join(
curriculum_folder, curriculum_filename
# Check if any two curricula use the same reset params.
if config_keys & used_reset_parameters:
logger.warning(
"Two or more curricula will "
"attempt to change the same reset "
"parameter. The result will be "
"non-deterministic."
curriculum = Curriculum(curriculum_filepath)
config_keys: Set[str] = set(curriculum.get_config().keys())
# Check if any two curriculums use the same reset params.
if config_keys & used_reset_parameters:
logger.warning(
"Two or more curriculums will "
"attempt to change the same reset "
"parameter. The result will be "
"non-deterministic."
)
used_reset_parameters.update(config_keys)
self._brains_to_curriculums[brain_name] = curriculum
except NotADirectoryError:
raise MetaCurriculumError(
curriculum_folder + " is not a "
"directory. Refer to the ML-Agents "
"curriculum learning docs."
)
used_reset_parameters.update(config_keys)
def brains_to_curriculums(self):
def brains_to_curricula(self):
return self._brains_to_curriculums
return self._brains_to_curricula
for brain_name, curriculum in self.brains_to_curriculums.items():
for brain_name, curriculum in self.brains_to_curricula.items():
lesson_nums[brain_name] = curriculum.lesson_num
return lesson_nums

for brain_name, lesson in lesson_nums.items():
self.brains_to_curriculums[brain_name].lesson_num = lesson
self.brains_to_curricula[brain_name].lesson_num = lesson
def _lesson_ready_to_increment(
self, brain_name: str, reward_buff_size: int

Whether the curriculum of the specified brain should attempt to
increment its lesson.
"""
if brain_name not in self.brains_to_curriculums:
if brain_name not in self.brains_to_curricula:
self.brains_to_curriculums[brain_name].min_lesson_length
self.brains_to_curricula[brain_name].min_lesson_length
"""Attempts to increments all the lessons of all the curriculums in this
"""Attempts to increments all the lessons of all the curricula in this
MetaCurriculum. Note that calling this method does not guarantee the
lesson of a curriculum will increment. The lesson of a curriculum will
only increment if the specified measure threshold defined in the

for brain_name, buff_size in reward_buff_sizes.items():
if self._lesson_ready_to_increment(brain_name, buff_size):
measure_val = measure_vals[brain_name]
ret[brain_name] = self.brains_to_curriculums[
ret[brain_name] = self.brains_to_curricula[
ret[brain_name] = self.brains_to_curriculums[
brain_name
].increment_lesson(measure_val)
ret[brain_name] = self.brains_to_curricula[brain_name].increment_lesson(
measure_val
)
def set_all_curriculums_to_lesson_num(self, lesson_num):
"""Sets all the curriculums in this meta curriculum to a specified
def set_all_curricula_to_lesson_num(self, lesson_num):
"""Sets all the curricula in this meta curriculum to a specified
lesson_num (int): The lesson number which all the curriculums will
lesson_num (int): The lesson number which all the curricula will
for _, curriculum in self.brains_to_curriculums.items():
for _, curriculum in self.brains_to_curricula.items():
"""Get the combined configuration of all curriculums in this
"""Get the combined configuration of all curricula in this
Returns:
A dict from parameter to value.
:return: A dict from parameter to value.
for _, curriculum in self.brains_to_curriculums.items():
for _, curriculum in self.brains_to_curricula.items():
@staticmethod
def from_directory(folder_path: str) -> "MetaCurriculum":
"""
Creates a MetaCurriculum given a folder full of curriculum config files.
:param folder_path: The path to the folder which holds the curriculum configs
for this environment. The folder should contain JSON files whose names
are the brains that the curricula belong to.
"""
try:
curricula = {}
for curriculum_filename in os.listdir(folder_path):
# This process requires JSON files
brain_name, extension = os.path.splitext(curriculum_filename)
if extension.lower() != ".json":
continue
curriculum_filepath = os.path.join(folder_path, curriculum_filename)
curriculum_config = Curriculum.load_curriculum_file(curriculum_filepath)
curricula[brain_name] = Curriculum(brain_name, curriculum_config)
return MetaCurriculum(curricula)
except NotADirectoryError:
raise MetaCurriculumError(
f"{folder_path} is not a directory. Refer to the ML-Agents "
"curriculum learning docs."
)

32
ml-agents/mlagents/trainers/tests/test_curriculum.py


}
"""
dummy_curriculum_config = json.loads(dummy_curriculum_json_str)
bad_curriculum_json_str = """
{

"""
@pytest.fixture
def location():
return "TestBrain.json"
dummy_curriculum_config_path = "TestBrain.json"
@pytest.fixture

@patch("builtins.open", new_callable=mock_open, read_data=dummy_curriculum_json_str)
def test_init_curriculum_happy_path(mock_file, location, default_reset_parameters):
curriculum = Curriculum(location)
def test_init_curriculum_happy_path():
curriculum = Curriculum("TestBrain", dummy_curriculum_config)
assert curriculum._brain_name == "TestBrain"
assert curriculum.brain_name == "TestBrain"
def test_init_curriculum_bad_curriculum_raises_error(
mock_file, location, default_reset_parameters
):
def test_load_bad_curriculum_file_raises_error(mock_file):
Curriculum(location)
Curriculum(
"TestBrain", Curriculum.load_curriculum_file(dummy_curriculum_config_path)
)
@patch("builtins.open", new_callable=mock_open, read_data=dummy_curriculum_json_str)
def test_increment_lesson(mock_file, location, default_reset_parameters):
curriculum = Curriculum(location)
def test_increment_lesson():
curriculum = Curriculum("TestBrain", dummy_curriculum_config)
assert curriculum.lesson_num == 0
curriculum.lesson_num = 1

assert curriculum.lesson_num == 3
@patch("builtins.open", new_callable=mock_open, read_data=dummy_curriculum_json_str)
def test_get_config(mock_file):
curriculum = Curriculum("TestBrain.json")
def test_get_parameters():
curriculum = Curriculum("TestBrain", dummy_curriculum_config)
assert curriculum.get_config() == {"param1": 0.7, "param2": 100, "param3": 0.2}
curriculum.lesson_num = 2

# Test json loading and error handling. These examples don't need to valid config files.
def test_curriculum_load_good():
expected = {"x": 1}
value = json.dumps(expected)

56
ml-agents/mlagents/trainers/tests/test_meta_curriculum.py


_check_environment_trains,
BRAIN_NAME,
)
from mlagents.trainers.tests.test_curriculum import dummy_curriculum_json_str
class MetaCurriculumTest(MetaCurriculum):
"""This class allows us to test MetaCurriculum objects without calling
MetaCurriculum's __init__ function.
"""
def __init__(self, brains_to_curriculums):
self._brains_to_curriculums = brains_to_curriculums
from mlagents.trainers.tests.test_curriculum import (
dummy_curriculum_json_str,
dummy_curriculum_config,
)
@pytest.fixture

@patch("mlagents.trainers.curriculum.Curriculum.get_config", return_value={})
@patch("mlagents.trainers.curriculum.Curriculum.__init__", return_value=None)
@patch(
"mlagents.trainers.curriculum.Curriculum.load_curriculum_file",
return_value=dummy_curriculum_config,
)
meta_curriculum = MetaCurriculum("test/")
meta_curriculum = MetaCurriculum.from_directory("test/")
assert len(meta_curriculum.brains_to_curriculums) == 2
assert len(meta_curriculum.brains_to_curricula) == 2
assert "Brain1" in meta_curriculum.brains_to_curriculums
assert "Brain2.test" in meta_curriculum.brains_to_curriculums
assert "Brain1" in meta_curriculum.brains_to_curricula
assert "Brain2.test" in meta_curriculum.brains_to_curricula
calls = [call("test/Brain1.json"), call("test/Brain2.test.json")]

@patch("os.listdir", side_effect=NotADirectoryError())
def test_init_meta_curriculum_bad_curriculum_folder_raises_error(listdir):
with pytest.raises(MetaCurriculumError):
MetaCurriculum("test/")
MetaCurriculum.from_directory("test/")
meta_curriculum = MetaCurriculumTest(
{"Brain1": curriculum_a, "Brain2": curriculum_b}
)
meta_curriculum = MetaCurriculum({"Brain1": curriculum_a, "Brain2": curriculum_b})
meta_curriculum.lesson_nums = {"Brain1": 1, "Brain2": 3}

@patch("mlagents.trainers.curriculum.Curriculum")
@patch("mlagents.trainers.curriculum.Curriculum")
def test_increment_lessons(curriculum_a, curriculum_b, measure_vals):
meta_curriculum = MetaCurriculumTest(
{"Brain1": curriculum_a, "Brain2": curriculum_b}
)
meta_curriculum = MetaCurriculum({"Brain1": curriculum_a, "Brain2": curriculum_b})
meta_curriculum.increment_lessons(measure_vals)

):
curriculum_a.min_lesson_length = 5
curriculum_b.min_lesson_length = 10
meta_curriculum = MetaCurriculumTest(
{"Brain1": curriculum_a, "Brain2": curriculum_b}
)
meta_curriculum = MetaCurriculum({"Brain1": curriculum_a, "Brain2": curriculum_b})
meta_curriculum.increment_lessons(measure_vals, reward_buff_sizes=reward_buff_sizes)

@patch("mlagents.trainers.curriculum.Curriculum")
@patch("mlagents.trainers.curriculum.Curriculum")
def test_set_all_curriculums_to_lesson_num(curriculum_a, curriculum_b):
meta_curriculum = MetaCurriculumTest(
{"Brain1": curriculum_a, "Brain2": curriculum_b}
)
meta_curriculum = MetaCurriculum({"Brain1": curriculum_a, "Brain2": curriculum_b})
meta_curriculum.set_all_curriculums_to_lesson_num(2)
meta_curriculum.set_all_curricula_to_lesson_num(2)
assert curriculum_a.lesson_num == 2
assert curriculum_b.lesson_num == 2

):
curriculum_a.get_config.return_value = default_reset_parameters
curriculum_b.get_config.return_value = default_reset_parameters
meta_curriculum = MetaCurriculumTest(
{"Brain1": curriculum_a, "Brain2": curriculum_b}
)
meta_curriculum = MetaCurriculum({"Brain1": curriculum_a, "Brain2": curriculum_b})
assert meta_curriculum.get_config() == default_reset_parameters

with patch(
"builtins.open", new_callable=mock_open, read_data=dummy_curriculum_json_str
):
curriculum = Curriculum("TestBrain.json")
mc = MetaCurriculumTest({curriculum_brain_name: curriculum})
curriculum_config = Curriculum.load_curriculum_file("TestBrain.json")
curriculum = Curriculum("TestBrain", curriculum_config)
mc = MetaCurriculum({curriculum_brain_name: curriculum})
_check_environment_trains(env, META_CURRICULUM_CONFIG, mc, -100.0)

6
ml-agents/mlagents/trainers/trainer_controller.py


for (
brain_name,
curriculum,
) in self.meta_curriculum.brains_to_curriculums.items():
) in self.meta_curriculum.brains_to_curricula.items():
# Skip brains that are in the metacurriculum but no trainer yet.
if brain_name not in self.trainers:
continue

delta_train_start = time() - self.training_start_time
if (
self.meta_curriculum
and brain_name in self.meta_curriculum.brains_to_curriculums
and brain_name in self.meta_curriculum.brains_to_curricula
lesson_num = self.meta_curriculum.brains_to_curriculums[
lesson_num = self.meta_curriculum.brains_to_curricula[
brain_name
].lesson_num
trainer.stats_reporter.add_stat("Environment/Lesson", lesson_num)

6
ml-agents/mlagents/trainers/trainer_util.py


min_lesson_length = 1
if meta_curriculum:
if brain_name in meta_curriculum.brains_to_curriculums:
min_lesson_length = meta_curriculum.brains_to_curriculums[
if brain_name in meta_curriculum.brains_to_curricula:
min_lesson_length = meta_curriculum.brains_to_curricula[
f"Brains with curricula: {meta_curriculum.brains_to_curriculums.keys()}. "
f"Brains with curricula: {meta_curriculum.brains_to_curricula.keys()}. "
)
trainer: Trainer = None # type: ignore # will be set to one of these, or raise

正在加载...
取消
保存