# LightZero 中如何自定义环境? - 在使用 LightZero 进行强化学习的研究或应用时,可能需要创建自定义的环境。创建自定义环境可以更好地适应特定的问题或任务,使得强化学习算法能够在特定环境中进行有效的训练。 - 一个典型的 LightZero 中的环境,请参考 [atari_lightzero_env.py](https://github.com/opendilab/LightZero/blob/main/zoo/atari/envs/atari_lightzero_env.py) 。LightZero的环境设计大致基于DI-engine的`BaseEnv`类。在创建自定义环境时,我们遵循了与DI-engine相似的基本步骤。以下是 DI-engine 中创建自定义环境的文档 - https://di-engine-docs.readthedocs.io/zh_CN/latest/04_best_practice/ding_env_zh.html ## 与 BaseEnv 的主要差异 在 LightZero 中,有很多棋类环境。棋类环境由于存在玩家交替执行动作,合法动作在变化的情况,所以环境的观测状态除了棋面信息,还应包含动作掩码,当前玩家等信息。因此,LightZero 中的 `obs` 不再像 DI-engine 中那样是一个数组,而是一个字典。字典中的 `'observation'` 对应于 DI-engine 中的 `obs`,此外字典中还包含了 `'action_mask'`、`'to_play'` 等信息。为了代码的兼容性,对于非棋类环境,LightZero 同样要求环境返回的 `obs` 包含`'action_mask'`、`'to_play'` 等信息。 在具体的方法实现中,这种差异主要体现在下面几点: - 在 `reset` 方法中,LightZeroEnv 返回的是一个字典 `lightzero_obs_dict = {'observation': obs, 'action_mask': action_mask, 'to_play': -1}` 。 - 对于非棋类环境 - `to_play` 的设置:由于非棋类环境一般只有一个玩家,因此设置 `to_play` =-1 。(我们在算法中根据该值,判断执行单player的算法逻辑 (`to_play` =-1) ,还是多player的算法逻辑 (`to_play`=N) ) - 对于 `action_mask` 的设置 - 离散动作空间: `action_mask`= np.ones(self.env.action_space.n, 'int8') 是一个全1的numpy数组,表示所有动作都是合法动作。 - 连续动作空间: `action_mask` = None ,特殊的 None 表示环境是连续动作空间。 - 对于棋类环境:为了方便后续 MCTS 流程, `lightzero_obs_dict ` 中可能还会增加棋面信息 `board` 和当前玩家 `curren_player_index` 等变量。 - 在 `step` 方法中,返回的是 `BaseEnvTimestep(lightzero_obs_dict, rew, done, info)` ,其中的 `lightzero_obs_dict` 包含了更新后的观察结果。 ## 基本步骤 以下是创建自定义 LightZero 环境的基本步骤: ### 1. 创建环境类 首先,需要创建一个新的环境类,该类需要继承自 DI-engine 的 BaseEnv 类。例如: ```Python from ding.envs import BaseEnv class MyCustomEnv(BaseEnv): pass ``` ### 2. __init__方法 在自定义环境类中,需要定义一个初始化方法 `__init__` 。在这个方法中,需要设置一些环境的基本属性,例如观察空间、动作空间、奖励空间等。例如: ```Python def __init__(self, cfg=None): self.cfg = cfg self._init_flag = False # set other properties... ``` ### 3. Reset 方法 `reset` 方法用于重置环境到一个初始状态。这个方法应该返回环境的初始观察。例如: ```Python def reset(self): # reset the environment... obs = self._env.reset() # get the action_mask according to the legal action ... lightzero_obs_dict = {'observation': obs, 'action_mask': action_mask, 'to_play': -1} return lightzero_obs_dict ``` ### 4. Step 方法 `step` 方法接受一个动作作为输入,执行这个动作,并返回一个元组,包含新的观察、奖励、是否完成和其他信息。例如: ```Python def step(self, action): # The core original env step. obs, rew, done, info = self.env.step(action) if self.cfg.continuous: action_mask = None else: # get the action_mask according to the legal action action_mask = np.ones(self.env.action_space.n, 'int8') lightzero_obs_dict = {'observation': obs, 'action_mask': action_mask, 'to_play': -1} self._eval_episode_return += rew if done: info['eval_episode_return'] = self._eval_episode_return return BaseEnvTimestep(lightzero_obs_dict, rew, done, info) ``` ### 5. 观察空间和动作空间 在自定义环境中,需要提供观察空间和动作空间的属性。这些属性是 `gym.Space` 对象,描述了观察和动作的形状和类型。例如: ```Python @property def observation_space(self): return self._observation_space @property def action_space(self): return self._action_space @property def legal_actions(self): # get the actual legal actions return np.arange(self._action_space.n) ``` ### 6. render 方法 `render` 方法会将游戏的对局演示出来,供用户查看。对于实现了 `render` 方法的环境,用户可以选择是否在执行 `step` 函数时调用 `render` 来实现每一步游戏状态的渲染。 ```Python def render(self, mode: str = 'image_savefile_mode') -> None: """ Overview: Renders the game environment. Arguments: - mode (:obj:`str`): The rendering mode. Options are 'state_realtime_mode', 'image_realtime_mode', or 'image_savefile_mode'. """ # In 'state_realtime_mode' mode, print the current game board for rendering. if mode == "state_realtime_mode": ... # In other two modes, use a screen for rendering. # Draw the screen. ... if mode == "image_realtime_mode": # Render the picture to user's window. ... elif mode == "image_savefile_mode": # Save the picture to frames. ... self.frames.append(self.screen) return None ``` 在 `render` 中,有三种不同的模式。 - 在 `state_realtime_mode` 下,`render` 会直接打印当前状态。 - 在 `image_realtime_mode` 下, `render` 会根据一些图形素材将环境状态渲染出来,形成可视化的界面,并弹出实时的窗口展示。 - 在 `image_savefile_mode` 下, `render` 会将渲染的图像保存在 `self.frames` 中,并在对局结束时通过 `save_render_output` 将其转化为文件保存下来。 在运行时, `render` 所采取的模式取决于 `self.render_mode` 的取值。当 `self.render_mode` 取值为 `None` 时,环境不会调用 `render` 方法。 ### 7. 其他方法 根据需要,可能还需要定义其他方法,例如 `close` (用于关闭环境并进行清理)等。 ### 8. 注册环境 最后,需要使用 `ENV_REGISTRY.register` 装饰器来注册新的环境,使得可以在配置文件中使用它。例如: ```Python from ding.utils import ENV_REGISTRY @ENV_REGISTRY.register('my_custom_env') class MyCustomEnv(BaseEnv): # ... ``` 当环境注册好之后,可以在配置文件中的 `create_config` 部分指定生成相应的环境: ```Python create_config = dict( env=dict( type='my_custom_env', import_names=['zoo.board_games.my_custom_env.envs.my_custom_env'], ), ... ) ``` 其中 `type` 要设定为所注册的环境名, `import_names` 则设置为环境包的位置。 创建自定义环境可能需要对具体的任务和强化学习有深入的理解。在实现自定义环境时,可能需要进行一些试验和调整,以使环境能够有效地支持强化学习的训练。 ## 棋类环境的特殊方法 以下是创建自定义 LightZero 棋类环境的额外步骤: 1. LightZero中的棋类环境有三种不同的模式: `self_play_mode` , `play_with_bot_mode` , `eval_mode` 。这三种模式的说明如下: - `self_play_mode`:该模式下,采取棋类环境的经典设置,每调用一次 `step` 函数,会根据传入的动作在环境中落子一次。在分出胜负的时间步,会返回+1的 reward 。在没有分出胜负的所有时间步, reward 均为0。 - `play_with_bot_mode`:该模式下,每调用一次 `step` 函数,会根据传入的动作在环境中落子一次,随后调用环境中的 bot 产生一个动作,并根据 bot 的动作再落子一次。也就是说, agent 扮演了1号玩家的角色,而 bot 扮演了2号玩家的角色和 agent 对抗。在对局结束时,如果 agent 胜利,则返回+1的 reward ,如果 bot 胜利,则返回-1的 reward ,平局则 reward 为0。在其余没有分出胜负的时间步, reward 均为0。 - `eval_mode`:该模式用于评估当前的 agent 的水平。具体有 bot 和 human 两种评估方法。采取 bot 评估时,和 play_with_bot_mode 中一样,会让 bot 扮演2号玩家和 agent 对抗,并根据结果计算 agent 的胜率。采取 human 模式时,则让用户扮演2号玩家,在命令行输入动作和 agent 对打。 每种模式下,在棋局结束后,都会从1号玩家的视角记录本局的 `eval_episode_return` 信息(如果1号玩家赢了,则 `eval_episode_return` 为1,如果输了为-1,平局为0),并记录在最后一个时间步中。 2. 在棋类环境中,随着对局的推进,可以采取的动作会不断变少,因此还需要实现 `legal_action` 方法。该方法可以用于检验玩家输入的动作是否合法,以及在 MCTS 过程中根据合法动作生成子节点。以 Connect4 环境为例,该方法会检查棋盘中的每一列是否下满,然后返回一个列表。该列表在可以落子的列取值为1,其余位置取值为0。 ```Python def legal_actions(self) -> List[int]: return [i for i in range(7) if self.board[i] == 0] ``` 3. LightZero的棋类环境中,还需要实现一些动作生成方法,例如 `bot_action` 和 `random_action` 。其中 `bot_action` 会根据 `self.bot_action_type` 的值调取相应种类的 bot ,通过 bot 中预实现的算法生成一个动作。而 `random_action` 则会从当前的合法动作列表中随机选取一个动作返回。 `bot_action` 用于实现环境的 `play_with_bot_mode` ,而 `random_action` 则会在 agent 和 bot 选取动作时依一定概率被调用,来增加对局样本的随机性。 ```Python def bot_action(self) -> int: if np.random.rand() < self.prob_random_action_in_bot: return self.random_action() else: if self.bot_action_type == 'rule': return self.rule_bot.get_rule_bot_action(self.board, self._current_player) elif self.bot_action_type == 'mcts': return self.mcts_bot.get_actions(self.board, player_index=self.current_player_index) ``` ## LightZeroEnvWrapper 我们在 lzero/envs/wrappers 中提供了一个 [LightZeroEnvWrapper](https://github.com/opendilab/LightZero/blob/main/lzero/envs/wrappers/lightzero_env_wrapper.py)。它能够将经典的 `classic_control`, `box2d` 环境包装成 LightZero 所需要的环境格式。在初始化实例时,会传入一个原始环境,这个原始环境通过父类 `gym.Wrapper` 被初始化,这使得实例可以调用原始环境中的 `render` , `close` , `seed` 等方法。在此基础上, `LightZeroEnvWrapper` 类重写了 `step` 和 `reset` 方法,将其输出封装成符合 LightZero 要求的字典 `lightzero_obs_dict` 。这样一来,封装后的新环境实例就满足了 LightZero 自定义环境的要求。 ```Python class LightZeroEnvWrapper(gym.Wrapper): # overview comments def __init__(self, env: gym.Env, cfg: EasyDict) -> None: # overview comments super().__init__(env) ... ``` 具体使用时,使用下面的函数,将一个 gym 环境,通过 `LightZeroEnvWrapper` 包装成 LightZero 所需要的环境格式。 `get_wrappered_env` 会返回一个匿名函数,该匿名函数每次调用都会产生一个 `DingEnvWrapper` 实例,该实例会将 `LightZeroEnvWrapper` 作为匿名函数传入,并在实例内部将原始环境封装成 LightZero 所需的格式。 ```Python def get_wrappered_env(wrapper_cfg: EasyDict, env_name: str): # overview comments ... if wrapper_cfg.manually_discretization: return lambda: DingEnvWrapper( gym.make(env_name), cfg={ 'env_wrapper': [ lambda env: ActionDiscretizationEnvWrapper(env, wrapper_cfg), lambda env: LightZeroEnvWrapper(env, wrapper_cfg) ] } ) else: return lambda: DingEnvWrapper( gym.make(env_name), cfg={'env_wrapper': [lambda env: LightZeroEnvWrapper(env, wrapper_cfg)]} ) ``` 然后在算法的主入口处中调用 `train_muzero_with_gym_env` 方法,即可使用上述包装后的 env 用于训练: ```Python if __name__ == "__main__": """ Overview: The ``train_muzero_with_gym_env`` entry means that the environment used in the training process is generated by wrapping the original gym environment with LightZeroEnvWrapper. Users can refer to lzero/envs/wrappers for more details. """ from lzero.entry import train_muzero_with_gym_env train_muzero_with_gym_env([main_config, create_config], seed=0, max_env_step=max_env_step) ``` ## 注意事项 - 状态表示:思考如何将环境状态表示为观察空间。对于简单的环境,可以直接使用低维连续状态;对于复杂的环境,可能需要使用图像或其他高维离散状态表示。 - 观察空间预处理:根据观察空间的类型,对输入数据进行适当的预处理操作,例如缩放、裁剪、灰度化、归一化等。预处理可以减少输入数据的维度,加速学习过程。 - 奖励设计:设计合理的符合目标的的奖励函数。例如,环境给出的外在奖励尽量归一化在[0, 1]。通过归一化环境给出的外在奖励,能更好的确定 RND 算法中的内在奖励权重等超参数。