File size: 13,975 Bytes
079c32c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# 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 算法中的内在奖励权重等超参数。