建立自訂環境

本頁提供如何使用 Gymnasium 建立自訂環境的簡短概述,如需更完整的教學,包含渲染,請先閱讀基本用法,再閱讀本頁。

我們將實作一個非常簡單的遊戲,稱為 GridWorldEnv,由固定大小的二維正方形網格組成。智能體可以在每個時間步在網格單元之間垂直或水平移動,智能體的目標是導航到網格上的目標,目標在劇集開始時隨機放置。

關於遊戲的基本資訊

  • 觀察提供目標和智能體的位置。

  • 我們的環境中有 4 個離散動作,對應於「右」、「上」、「左」和「下」的移動。

  • 當智能體導航到目標所在的網格單元時,環境結束(終止)。

  • 智能體只有在到達目標時才會獲得獎勵,即當智能體到達目標時獎勵為 1,否則為零。

環境 __init__

與所有環境一樣,我們的自訂環境將繼承自 gymnasium.Env,它定義了環境的結構。環境的要求之一是定義觀察空間和動作空間,它們宣告環境可能的輸入(動作)和輸出(觀察)的通用集合。正如我們在關於遊戲的基本資訊中概述的那樣,我們的智能體有四個離散動作,因此我們將使用具有四個選項的 Discrete(4) 空間。

對於我們的觀察,有幾種選擇,在本教學中,我們將想像我們的觀察看起來像 {"agent": array([1, 0]), "target": array([0, 3])},其中陣列元素表示智能體或目標的 x 和 y 位置。表示觀察的替代選項是二維網格,其值表示網格上的智能體和目標,或三維網格,每個「層」僅包含智能體或目標資訊。因此,我們將觀察空間宣告為 Dict,其中智能體和目標空間是 Box,允許整數類型的陣列輸出。

如需可用於環境的可能空間的完整列表,請參閱 空間

from typing import Optional
import numpy as np
import gymnasium as gym


class GridWorldEnv(gym.Env):

    def __init__(self, size: int = 5):
        # The size of the square grid
        self.size = size

        # Define the agent and target location; randomly chosen in `reset` and updated in `step`
        self._agent_location = np.array([-1, -1], dtype=np.int32)
        self._target_location = np.array([-1, -1], dtype=np.int32)

        # Observations are dictionaries with the agent's and the target's location.
        # Each location is encoded as an element of {0, ..., `size`-1}^2
        self.observation_space = gym.spaces.Dict(
            {
                "agent": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),
                "target": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),
            }
        )

        # We have 4 actions, corresponding to "right", "up", "left", "down"
        self.action_space = gym.spaces.Discrete(4)
        # Dictionary maps the abstract actions to the directions on the grid
        self._action_to_direction = {
            0: np.array([1, 0]),  # right
            1: np.array([0, 1]),  # up
            2: np.array([-1, 0]),  # left
            3: np.array([0, -1]),  # down
        }

建構觀察

由於我們需要在 Env.reset()Env.step() 中計算觀察,因此通常方便使用方法 _get_obs 將環境的狀態轉換為觀察。但是,這不是強制性的,您可以在 Env.reset()Env.step() 中分別計算觀察。

    def _get_obs(self):
        return {"agent": self._agent_location, "target": self._target_location}

我們還可以為 Env.reset()Env.step() 返回的輔助資訊實作類似的方法。在我們的例子中,我們想提供智能體和目標之間的曼哈頓距離

    def _get_info(self):
        return {
            "distance": np.linalg.norm(
                self._agent_location - self._target_location, ord=1
            )
        }

通常,info 也會包含一些僅在 Env.step() 方法內部可用的資料(例如,個別獎勵項)。在這種情況下,我們必須更新 _get_infoEnv.step() 中返回的字典。

重置函數

reset() 的目的是為環境啟動新的劇集,並具有兩個參數:seedoptions。種子可用於將隨機數產生器初始化為確定性狀態,選項可用於指定重置中使用的值。在重置的第一行,您需要呼叫 super().reset(seed=seed),這將初始化隨機數產生器 (np_random) 以在 reset() 的其餘部分中使用。

在我們的自訂環境中,reset() 需要隨機選擇智能體和目標的位置(如果它們具有相同的位置,我們會重複此操作)。reset() 的返回類型是初始觀察和任何輔助資訊的元組。因此,我們可以使用我們先前為此實作的方法 _get_obs_get_info

    def reset(self, seed: Optional[int] = None, options: Optional[dict] = None):
        # We need the following line to seed self.np_random
        super().reset(seed=seed)

        # Choose the agent's location uniformly at random
        self._agent_location = self.np_random.integers(0, self.size, size=2, dtype=int)

        # We will sample the target's location randomly until it does not coincide with the agent's location
        self._target_location = self._agent_location
        while np.array_equal(self._target_location, self._agent_location):
            self._target_location = self.np_random.integers(
                0, self.size, size=2, dtype=int
            )

        observation = self._get_obs()
        info = self._get_info()

        return observation, info

步進函數

step() 方法通常包含環境的大部分邏輯,它接受一個 action 並計算應用動作後環境的狀態,返回下一個觀察、結果獎勵、環境是否終止、環境是否截斷和輔助資訊的元組。

對於我們的環境,在步進函數期間需要發生幾件事

  • 我們使用 self._action_to_direction 將離散動作(例如,2)轉換為具有智能體位置的網格方向。為了防止智能體超出網格邊界,我們將智能體的位置裁剪為保持在邊界內。

  • 我們透過檢查智能體的目前位置是否等於目標的位置來計算智能體的獎勵。

  • 由於環境不會在內部截斷(我們可以在 make() 期間將時間限制包裝器應用於環境),因此我們永久將截斷設定為 False。

  • 我們再次使用 _get_obs 和 _get_info 來獲取智能體的觀察和輔助資訊。

    def step(self, action):
        # Map the action (element of {0,1,2,3}) to the direction we walk in
        direction = self._action_to_direction[action]
        # We use `np.clip` to make sure we don't leave the grid bounds
        self._agent_location = np.clip(
            self._agent_location + direction, 0, self.size - 1
        )

        # An environment is completed if and only if the agent has reached the target
        terminated = np.array_equal(self._agent_location, self._target_location)
        truncated = False
        reward = 1 if terminated else 0  # the agent is only reached at the end of the episode
        observation = self._get_obs()
        info = self._get_info()

        return observation, reward, terminated, truncated, info

註冊和建立環境

雖然現在可以立即使用新的自訂環境,但更常見的是使用 gymnasium.make() 初始化環境。在本節中,我們將說明如何註冊自訂環境,然後初始化它。

環境 ID 由三個組件組成,其中兩個是可選的:一個可選的命名空間(此處:gymnasium_env)、一個強制性名稱(此處:GridWorld)和一個可選但建議的版本(此處:v0)。它也可能已註冊為 GridWorld-v0(建議的方法)、GridWorldgymnasium_env/GridWorld,然後應在環境建立期間使用適當的 ID。

入口點可以是字串或函數,由於本教學不是 python 專案的一部分,因此我們無法使用字串,但對於大多數環境而言,這是指定入口點的正常方式。

Register 還有其他參數可用於指定環境的關鍵字引數,例如,是否應用時間限制包裝器等。請參閱 gymnasium.register() 以取得更多資訊。

gym.register(
    id="gymnasium_env/GridWorld-v0",
    entry_point=GridWorldEnv,
)

如需關於註冊自訂環境(包括使用字串入口點)的更完整指南,請閱讀完整的建立環境 教學。

環境註冊後,您可以透過 gymnasium.pprint_registry() 檢查,它將輸出所有已註冊的環境,然後可以使用 gymnasium.make() 初始化環境。可以使用 gymnasium.make_vec() 實例化環境的向量化版本,其中多個相同的環境實例並行執行。

import gymnasium as gym
>>> gym.make("gymnasium_env/GridWorld-v0")
<OrderEnforcing<PassiveEnvChecker<GridWorld<gymnasium_env/GridWorld-v0>>>>
>>> gym.make("gymnasium_env/GridWorld-v0", max_episode_steps=100)
<TimeLimit<OrderEnforcing<PassiveEnvChecker<GridWorld<gymnasium_env/GridWorld-v0>>>>>
>>> env = gym.make("gymnasium_env/GridWorld-v0", size=10)
>>> env.unwrapped.size
10
>>> gym.make_vec("gymnasium_env/GridWorld-v0", num_envs=3)
SyncVectorEnv(gymnasium_env/GridWorld-v0, num_envs=3)

使用包裝器

通常,我們想要使用自訂環境的不同變體,或者我們想要修改 Gymnasium 或其他方提供的環境的行為。包裝器允許我們在不更改環境實作或新增任何樣板程式碼的情況下執行此操作。查看包裝器文件,以取得關於如何使用包裝器的詳細資訊以及實作您自己的包裝器的說明。在我們的範例中,觀察無法直接在學習程式碼中使用,因為它們是字典。但是,我們實際上不需要接觸我們的環境實作來解決這個問題!我們可以簡單地在環境實例之上新增一個包裝器,將觀察扁平化為單個陣列

>>> from gymnasium.wrappers import FlattenObservation

>>> env = gym.make('gymnasium_env/GridWorld-v0')
>>> env.observation_space
Dict('agent': Box(0, 4, (2,), int64), 'target': Box(0, 4, (2,), int64))
>>> env.reset()
({'agent': array([4, 1]), 'target': array([2, 4])}, {'distance': 5.0})
>>> wrapped_env = FlattenObservation(env)
>>> wrapped_env.observation_space
Box(0, 4, (4,), int64)
>>> wrapped_env.reset()
(array([3, 0, 2, 1]), {'distance': 2.0})