建立自定義環境¶
編碼前:環境設計¶
建立強化學習 (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-v0、GridWorld 或 gymnasium_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)
實際環境設計技巧¶
從簡單開始,逐步增加複雜性¶
首先:讓基本的移動和目標達成功能正常工作
然後:新增障礙物、多個目標或時間壓力
最後:新增複雜動力學、部分可觀測性或多智慧體互動
為學習而設計¶
明確的成功標準:智慧體應該知道它何時表現良好
合理的難度:不要太簡單(微不足道)或太難(不可能)
一致的規則:在相同狀態下采取相同動作應該產生相同效果
資訊豐富的觀測:包含最佳決策所需的一切
思考你的研究問題¶
導航:側重於空間推理和路徑規劃
控制:強調動力學、穩定性和連續動作
策略:包括部分資訊、對手建模或長期規劃
最佳化:設計明確的權衡和資源限制
下一步¶
恭喜!你現在知道如何建立自定義強化學習環境了。接下來可以探索以下內容:
新增渲染以視覺化你的環境(完整教程)
在你的自定義環境中訓練一個智慧體(訓練指南)
嘗試不同的獎勵函式,看看它們如何影響學習
嘗試包裝器組合來修改環境的行為
建立更復雜的環境,包括障礙物、多個智慧體或連續動作
良好環境設計的關鍵在於迭代——從簡單開始,徹底測試,並根據研究或應用目標逐步增加複雜性。