建立自定義環境

編碼前:環境設計

建立強化學習 (RL) 環境就像設計一個影片遊戲或模擬。在編寫任何程式碼之前,你需要仔細思考想要解決的學習問題。這個設計階段至關重要——一個設計不佳的環境將使學習變得困難甚至不可能,無論你的演算法有多好。

關鍵設計問題

問自己這些基本問題:

🎯 智慧體應該學習什麼技能?

  • 穿越迷宮?

  • 平衡和控制一個系統?

  • 最佳化資源分配?

  • 玩策略遊戲?

👀 智慧體需要什麼資訊?

  • 位置和速度?

  • 系統當前狀態?

  • 歷史資料?

  • 部分可觀測性還是完全可觀測性?

🎮 智慧體可以採取什麼動作?

  • 離散選擇(向上/向下/向左/向右移動)?

  • 連續控制(轉向角、油門)?

  • 多個同時動作?

🏆 我們如何衡量成功?

  • 達到特定目標?

  • 最小化時間或能量?

  • 最大化得分?

  • 避免失敗?

⏰ 何時結束回合?

  • 任務完成(成功/失敗)?

  • 時間限制?

  • 安全約束?

網格世界 (GridWorld) 示例設計

對於我們的教程示例,我們將建立一個簡單的網格世界 (GridWorld) 環境

  • 🎯 技能:高效導航到目標位置

  • 👀 資訊:智慧體在網格上的位置和目標位置

  • 🎮 動作:向上、向下、向左或向右移動

  • 🏆 成功:以最少步數到達目標

  • ⏰ 結束:當智慧體到達目標時(或可選的時間限制)

這提供了一個清晰的學習問題,它足夠簡單易懂,但要最優地解決卻並非微不足道。


本頁提供了使用 Gymnasium 建立自定義環境的完整實現。有關包含渲染功能的完整教程

我們建議你在閱讀本頁之前,先熟悉基本用法

我們將把我們的網格世界遊戲實現為一個固定大小的二維方格。智慧體在每個時間步可以在網格單元之間垂直或水平移動,目標是在回合開始時隨機放置的目標位置導航。

環境 __init__ 函式

像所有環境一樣,我們的自定義環境將繼承自 gymnasium.Env,它定義了所有環境必須遵循的結構。其中一個要求是定義觀測空間和動作空間,它們聲明瞭此環境的有效輸入(動作)和輸出(觀測)。

正如我們的設計所概述的,我們的智慧體有四個離散動作(向四個基本方向移動),所以我們將使用 Discrete(4) 空間。

對於我們的觀測,我們有幾種選擇。我們可以將整個網格表示為二維陣列,或者使用座標位置,甚至使用帶有獨立“層”的 3D 陣列來表示智慧體和目標。對於本教程,我們將使用簡單的字典格式,例如 {"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 (5x5 by default)
        self.size = size

        # Initialize positions - will be set randomly in reset()
        # Using -1,-1 as "uninitialized" state
        self._agent_location = np.array([-1, -1], dtype=np.int32)
        self._target_location = np.array([-1, -1], dtype=np.int32)

        # Define what the agent can observe
        # Dict space gives us structured, human-readable observations
        self.observation_space = gym.spaces.Dict(
            {
                "agent": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),   # [x, y] coordinates
                "target": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),  # [x, y] coordinates
            }
        )

        # Define what actions are available (4 directions)
        self.action_space = gym.spaces.Discrete(4)

        # Map action numbers to actual movements on the grid
        # This makes the code more readable than using raw numbers
        self._action_to_direction = {
            0: np.array([1, 0]),   # Move right (positive x)
            1: np.array([0, 1]),   # Move up (positive y)
            2: np.array([-1, 0]),  # Move left (negative x)
            3: np.array([0, -1]),  # Move down (negative y)
        }

構建觀測

由於我們需要在 Env.reset()Env.step() 中計算觀測,因此擁有一個輔助方法 _get_obs 來將環境的內部狀態轉換為觀測格式會很方便。這使我們的程式碼 DRY (不要重複自己),並使其更容易在以後修改觀測格式。

    def _get_obs(self):
        """Convert internal state to observation format.

        Returns:
            dict: Observation with agent and target positions
        """
        return {"agent": self._agent_location, "target": self._target_location}

我們還可以為 Env.reset()Env.step() 返回的輔助資訊實現類似的方法。在我們的例子中,我們將提供智慧體和目標之間的曼哈頓距離——這對於除錯和理解智慧體進度可能很有用,但不應被學習演算法本身使用。

    def _get_info(self):
        """Compute auxiliary information for debugging.

        Returns:
            dict: Info with distance between agent and target
        """
        return {
            "distance": np.linalg.norm(
                self._agent_location - self._target_location, ord=1
            )
        }

有時 info 中會包含只在 Env.step() 內部才可用的資料(如單個獎勵分量、動作成功/失敗等)。在這些情況下,我們會在 step 方法中直接更新 _get_info 返回的字典。

重置函式

reset() 方法啟動一個新的回合。它接受兩個可選引數:用於可復現隨機生成的 seed 和用於附加配置的 options。在第一行,你必須呼叫 super().reset(seed=seed) 以正確初始化隨機數生成器。

在我們的網格世界環境中,reset() 隨機地將智慧體和目標放置在網格上,確保它們不會在同一個位置開始。我們以元組的形式返回初始觀測和資訊。

    def reset(self, seed: Optional[int] = None, options: Optional[dict] = None):
        """Start a new episode.

        Args:
            seed: Random seed for reproducible episodes
            options: Additional configuration (unused in this example)

        Returns:
            tuple: (observation, info) for the initial state
        """
        # IMPORTANT: Must call this first to seed the random number generator
        super().reset(seed=seed)

        # Randomly place the agent anywhere on the grid
        self._agent_location = self.np_random.integers(0, self.size, size=2, dtype=int)

        # Randomly place target, ensuring it's different from agent position
        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() 方法包含核心環境邏輯。它接收一個動作,更新環境狀態,並返回結果。物理、遊戲規則和獎勵邏輯都在這裡。

對於網格世界,我們需要:1. 將離散動作轉換為移動方向 2. 更新智慧體的位置(帶邊界檢查)3. 根據是否達到目標計算獎勵 4. 判斷回合是否應該結束 5. 返回所有所需資訊

    def step(self, action):
        """Execute one timestep within the environment.

        Args:
            action: The action to take (0-3 for directions)

        Returns:
            tuple: (observation, reward, terminated, truncated, info)
        """
        # Map the discrete action (0-3) to a movement direction
        direction = self._action_to_direction[action]

        # Update agent position, ensuring it stays within grid bounds
        # np.clip prevents the agent from walking off the edge
        self._agent_location = np.clip(
            self._agent_location + direction, 0, self.size - 1
        )

        # Check if agent reached the target
        terminated = np.array_equal(self._agent_location, self._target_location)

        # We don't use truncation in this simple environment
        # (could add a step limit here if desired)
        truncated = False

        # Simple reward structure: +1 for reaching target, 0 otherwise
        # Alternative: could give small negative rewards for each step to encourage efficiency
        reward = 1 if terminated else 0

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

        return observation, reward, terminated, truncated, info

常見環境設計陷阱

現在你已經瞭解了基本結構,讓我們討論初學者常犯的錯誤

獎勵設計問題

問題:僅在最後才給予獎勵(稀疏獎勵)

# This makes learning very difficult!
reward = 1 if terminated else 0

更好:提供中間反饋

# Option 1: Small step penalty to encourage efficiency
reward = 1 if terminated else -0.01

# Option 2: Distance-based reward shaping
distance = np.linalg.norm(self._agent_location - self._target_location)
reward = 1 if terminated else -0.1 * distance

狀態表示問題

問題:包含不相關資訊或缺少關鍵細節

# Too much info - agent doesn't need grid size in every observation
obs = {"agent": self._agent_location, "target": self._target_location, "size": self.size}

# Too little info - agent can't distinguish different positions
obs = {"distance": distance}  # Missing actual positions!

更好:精確包含最佳決策所需的一切

# Just right - positions are sufficient for navigation
obs = {"agent": self._agent_location, "target": self._target_location}

動作空間問題

問題:動作不合理或無法執行

# Bad: Agent can move diagonally but environment doesn't support it
self.action_space = gym.spaces.Discrete(8)  # 8 directions including diagonals

# Bad: Continuous actions for discrete movement
self.action_space = gym.spaces.Box(-1, 1, shape=(2,))  # Continuous x,y movement

邊界處理錯誤

問題:允許無效狀態或模糊的邊界行為

# Bad: Agent can go outside the grid
self._agent_location = self._agent_location + direction  # No bounds checking!

# Unclear: What happens when agent hits wall?
if np.any(self._agent_location < 0) or np.any(self._agent_location >= self.size):
    # Do nothing? Reset episode? Give penalty? Unclear!

更好:清晰、一致的邊界處理

# Clear: Agent stays in place when hitting boundaries
self._agent_location = np.clip(
    self._agent_location + direction, 0, self.size - 1
)

註冊和建立環境

雖然你可以立即使用你的自定義環境,但將其註冊到 Gymnasium 中會更方便,這樣你就可以像建立內建環境一樣使用 gymnasium.make() 來建立它。

環境 ID 包含三個組成部分:可選的名稱空間(此處為:gymnasium_env)、強制名稱(此處為:GridWorld)以及可選但推薦的版本(此處為:v0)。你可以將其註冊為 GridWorld-v0GridWorldgymnasium_env/GridWorld,但為了清晰起見,推薦使用完整格式。

由於本教程不屬於 Python 包的一部分,我們直接將類作為入口點傳遞。在實際專案中,你通常會使用像 "my_package.envs:GridWorldEnv" 這樣的字串。

# Register the environment so we can create it with gym.make()
gym.register(
    id="gymnasium_env/GridWorld-v0",
    entry_point=GridWorldEnv,
    max_episode_steps=300,  # Prevent infinite episodes
)

有關注冊自定義環境(包括使用字串入口點)的更完整指南,請閱讀完整的建立環境教程。

註冊後,你可以使用 gymnasium.pprint_registry() 檢查所有可用環境,並使用 gymnasium.make() 建立例項。你還可以使用 gymnasium.make_vec() 建立向量化版本。

import gymnasium as gym

# Create the environment like any built-in environment
>>> env = gym.make("gymnasium_env/GridWorld-v0")
<OrderEnforcing<PassiveEnvChecker<GridWorld<gymnasium_env/GridWorld-v0>>>>

# Customize environment parameters
>>> env = gym.make("gymnasium_env/GridWorld-v0", size=10)
>>> env.unwrapped.size
10

# Create multiple environments for parallel training
>>> vec_env = gym.make_vec("gymnasium_env/GridWorld-v0", num_envs=3)
SyncVectorEnv(gymnasium_env/GridWorld-v0, num_envs=3)

除錯你的環境

當你的環境未按預期工作時,以下是常見的除錯策略

檢查環境有效性

from gymnasium.utils.env_checker import check_env

# This will catch many common issues
try:
    check_env(env)
    print("Environment passes all checks!")
except Exception as e:
    print(f"Environment has issues: {e}")

使用已知動作手動測試

# Test specific action sequences to verify behavior
env = gym.make("gymnasium_env/GridWorld-v0")
obs, info = env.reset(seed=42)  # Use seed for reproducible testing

print(f"Starting position - Agent: {obs['agent']}, Target: {obs['target']}")

# Test each action type
actions = [0, 1, 2, 3]  # right, up, left, down
for action in actions:
    old_pos = obs['agent'].copy()
    obs, reward, terminated, truncated, info = env.step(action)
    new_pos = obs['agent']
    print(f"Action {action}: {old_pos} -> {new_pos}, reward={reward}")

常見除錯問題

# Issue 1: Forgot to call super().reset()
def reset(self, seed=None, options=None):
    # super().reset(seed=seed)  # ❌ Missing this line
    # Results in: possibly incorrect seeding

# Issue 2: Wrong action mapping
self._action_to_direction = {
    0: np.array([1, 0]),   # right
    1: np.array([0, 1]),   # up - but is this really "up" in your coordinate system?
    2: np.array([-1, 0]),  # left
    3: np.array([0, -1]),  # down
}

# Issue 3: Not handling boundaries properly
# This allows agent to go outside the grid!
self._agent_location = self._agent_location + direction  # ❌ No bounds checking

使用包裝器

有時你希望修改環境的行為,而不更改核心實現。包裝器 (Wrappers) 是實現此目的的完美選擇——它們允許你新增功能,例如更改觀測格式、新增時間限制或修改獎勵,而無需觸及原始環境程式碼。

>>> from gymnasium.wrappers import FlattenObservation

>>> # Original observation is a dictionary
>>> env = gym.make('gymnasium_env/GridWorld-v0')
>>> env.observation_space
Dict('agent': Box(0, 4, (2,), int64), 'target': Box(0, 4, (2,), int64))

>>> obs, info = env.reset()
>>> obs
{'agent': array([4, 1]), 'target': array([2, 4])}

>>> # Wrap it to flatten observations into a single array
>>> wrapped_env = FlattenObservation(env)
>>> wrapped_env.observation_space
Box(0, 4, (4,), int64)

>>> obs, info = wrapped_env.reset()
>>> obs
array([3, 0, 2, 1])  # [agent_x, agent_y, target_x, target_y]

當與期望特定輸入格式(如需要一維陣列而不是字典的神經網路)的演算法一起工作時,這尤其有用。

高階環境特性

一旦基本功能正常,你可能想新增更復雜的功能

新增渲染

def render(self):
    """Render the environment for human viewing."""
    if self.render_mode == "human":
        # Print a simple ASCII representation
        for y in range(self.size - 1, -1, -1):  # Top to bottom
            row = ""
            for x in range(self.size):
                if np.array_equal([x, y], self._agent_location):
                    row += "A "  # Agent
                elif np.array_equal([x, y], self._target_location):
                    row += "T "  # Target
                else:
                    row += ". "  # Empty
            print(row)
        print()

引數化環境

def __init__(self, size: int = 5, reward_scale: float = 1.0, step_penalty: float = 0.0):
    self.size = size
    self.reward_scale = reward_scale
    self.step_penalty = step_penalty
    # ... rest of init ...

def step(self, action):
    # ... movement logic ...

    # Flexible reward calculation
    if terminated:
        reward = self.reward_scale  # Success reward
    else:
        reward = -self.step_penalty  # Step penalty (0 by default)

實際環境設計技巧

從簡單開始,逐步增加複雜性

  1. 首先:讓基本的移動和目標達成功能正常工作

  2. 然後:新增障礙物、多個目標或時間壓力

  3. 最後:新增複雜動力學、部分可觀測性或多智慧體互動

為學習而設計

  • 明確的成功標準:智慧體應該知道它何時表現良好

  • 合理的難度:不要太簡單(微不足道)或太難(不可能)

  • 一致的規則:在相同狀態下采取相同動作應該產生相同效果

  • 資訊豐富的觀測:包含最佳決策所需的一切

思考你的研究問題

  • 導航:側重於空間推理和路徑規劃

  • 控制:強調動力學、穩定性和連續動作

  • 策略:包括部分資訊、對手建模或長期規劃

  • 最佳化:設計明確的權衡和資源限制

下一步

恭喜!你現在知道如何建立自定義強化學習環境了。接下來可以探索以下內容:

  1. 新增渲染以視覺化你的環境(完整教程

  2. 在你的自定義環境中訓練一個智慧體訓練指南

  3. 嘗試不同的獎勵函式,看看它們如何影響學習

  4. 嘗試包裝器組合來修改環境的行為

  5. 建立更復雜的環境,包括障礙物、多個智慧體或連續動作

良好環境設計的關鍵在於迭代——從簡單開始,徹底測試,並根據研究或應用目標逐步增加複雜性。