사용자 지정 환경 생성¶
이 페이지는 Gymnasium으로 사용자 지정 환경을 생성하는 방법에 대한 간략한 개요를 제공합니다. 렌더링을 포함한 더 완전한 튜토리얼은 이 페이지를 읽기 전에 기본 사용법을 읽어보시기 바랍니다.
우리는 고정된 크기의 2차원 정사각형 격자로 구성된 GridWorldEnv
라는 매우 단순한 게임을 구현할 것입니다. 에이전트는 매 타임스텝마다 격자 셀 사이를 수직 또는 수평으로 이동할 수 있으며, 에이전트의 목표는 에피소드 시작 시 무작위로 배치된 격자의 목표 지점까지 이동하는 것입니다.
게임에 대한 기본 정보
관찰은 목표 지점과 에이전트의 위치를 제공합니다.
환경에는 “오른쪽”, “위”, “왼쪽”, “아래” 이동에 해당하는 4개의 이산적인 행동이 있습니다.
환경은 에이전트가 목표 지점이 있는 격자 셀로 이동하면 종료(terminate)됩니다.
에이전트는 목표에 도달했을 때만 보상받습니다. 즉, 에이전트가 목표에 도달하면 보상은 1이고, 그렇지 않으면 0입니다.
환경 __init__
¶
모든 환경과 마찬가지로, 우리의 사용자 지정 환경은 환경의 구조를 정의하는 gymnasium.Env`를 상속받습니다. 환경의 요구 사항 중 하나는 환경의 가능한 입력(행동) 및 출력(관찰)의 일반적인 집합을 선언하는 관찰 및 행동 공간을 정의하는 것입니다. 게임에 대한 기본 정보에서 설명했듯이, 우리 에이전트는 4개의 이산적인 행동을 가지므로, 우리는 4개의 옵션이 있는 ``Discrete(4)`
공간을 사용할 것입니다.
우리의 관찰에 대해서는 몇 가지 옵션이 있습니다. 이 튜토리얼에서는 관찰이 ``{“agent”: array([1, 0]), “target”: array([0, 3])}``와 같이 보인다고 상상할 것입니다. 여기서 배열 요소는 에이전트 또는 목표의 x 및 y 위치를 나타냅니다. 관찰을 표현하는 다른 옵션으로는 에이전트와 목표를 격자 위에 값으로 나타내는 2D 격자 또는 각 “레이어”에 에이전트 또는 목표 정보만 포함하는 3D 격자가 있습니다. 따라서 우리는 관찰 공간을 에이전트 및 목표 공간이 int 타입의 배열 출력을 허용하는 :class:`Box`인 :class:`Dict`로 선언할 것입니다.
환경에서 사용할 수 있는 가능한 모든 공간 목록은 spaces를 참조하십시오.
from typing import Optional
import numpy as np
import gymnasium as gym
class GridWorldEnv(gym.Env):
def __init__(self, size: int = 5):
# 정사각형 격자의 크기
self.size = size
# 에이전트 및 목표 위치 정의; `reset`에서 무작위로 선택되고 `step`에서 업데이트됨
self._agent_location = np.array([-1, -1], dtype=np.int32)
self._target_location = np.array([-1, -1], dtype=np.int32)
# 관찰은 에이전트와 목표의 위치를 포함하는 딕셔너리입니다.
# 각 위치는 {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),
}
)
# 우리는 "right", "up", "left", "down"에 해당하는 4가지 행동을 가지고 있습니다.
self.action_space = gym.spaces.Discrete(4)
# 추상적인 행동을 격자 방향으로 매핑하는 딕셔너리
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`과 :meth:`Env.step()
모두에서 관찰을 계산해야 하므로, 환경의 상태를 관찰로 변환하는 _get_obs
메소드를 갖는 것이 종종 편리합니다. 그러나 이것은 필수는 아니며, :meth:`Env.reset`과 :meth:`Env.step`에서 관찰을 별도로 계산할 수도 있습니다.
def _get_obs(self):
return {"agent": self._agent_location, "target": self._target_location}
:meth:`Env.reset`과 :meth:`Env.step`이 반환하는 부가 정보에 대해서도 유사한 메소드를 구현할 수 있습니다. 우리의 경우, 에이전트와 목표 사이의 맨해튼 거리를 제공하고자 합니다:
def _get_info(self):
return {
"distance": np.linalg.norm(
self._agent_location - self._target_location, ord=1
)
}
종종 info에는 Env.step()
메소드 내에서만 사용 가능한 데이터도 포함될 수 있습니다(예: 개별 보상 항). 이 경우, :meth:`Env.step`에서 ``_get_info``가 반환하는 딕셔너리를 업데이트해야 합니다.
리셋(Reset) 함수¶
reset`의 목적은 환경에 대한 새 에피소드를 시작하는 것이며, ``seed``와 ``options`()
두 가지 매개변수를 갖습니다. seed는 결정론적인 상태로 난수 생성기를 초기화하는 데 사용할 수 있으며, options는 reset 내에서 사용되는 값을 지정하는 데 사용할 수 있습니다. reset의 첫 번째 줄에서는 ``super().reset(seed=seed)``를 호출해야 하며, 이는 reset의 나머지 부분에서 사용할 난수 생성기(np_random
)를 초기화합니다.
우리의 사용자 지정 환경 내에서 reset`은 에이전트와 목표의 위치를 무작위로 선택해야 합니다(같은 위치이면 다시 선택합니다). :meth:`reset`의 반환 타입은 초기 관찰과 부가 정보의 튜플입니다. 따라서 이전에 구현한 ``_get_obs``와 ``_get_info`()
메소드를 사용할 수 있습니다:
def reset(self, seed: Optional[int] = None, options: Optional[dict] = None):
# self.np_random을 시드하기 위해 다음 줄이 필요합니다.
super().reset(seed=seed)
# 에이전트의 위치를 균일하게 무작위로 선택합니다.
self._agent_location = self.np_random.integers(0, self.size, size=2, dtype=int)
# 목표의 위치는 에이전트의 위치와 겹치지 않을 때까지 무작위로 샘플링합니다.
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) 함수¶
step()
메소드는 일반적으로 환경의 대부분의 로직을 포함하며, ``action``을 인수로 받아 행동을 적용한 후 환경의 상태를 계산합니다. 이 메소드는 다음 관찰, 결과 보상, 환경 종료 여부, 환경 중단 여부, 그리고 부가 정보의 튜플을 반환합니다.
우리의 환경에서는 step 함수 중에 몇 가지 일들이 발생해야 합니다:
우리는 discrete 행동(예: 2)을 grid 방향으로 변환하기 위해 self._action_to_direction을 에이전트 위치와 함께 사용합니다. 에이전트가 격자 범위를 벗어나지 않도록, 에이전트의 위치를 범위 내로 클립합니다.
에이전트의 현재 위치가 목표 위치와 같은지 확인하여 에이전트의 보상을 계산합니다.
환경은 내부적으로 중단되지 않으므로(
make()
중에 환경에 시간 제한 래퍼를 적용할 수 있습니다), truncated는 영구적으로 False로 설정합니다.에이전트의 관찰과 부가 정보를 얻기 위해 _get_obs와 _get_info를 다시 사용합니다.
def step(self, action):
# 행동({0,1,2,3}의 요소)을 이동할 방향에 매핑합니다.
direction = self._action_to_direction[action]
# `np.clip`을 사용하여 격자 경계를 벗어나지 않도록 합니다.
self._agent_location = np.clip(
self._agent_location + direction, 0, self.size - 1
)
# 에이전트가 목표에 도달했을 때만 환경이 완료됩니다.
terminated = np.array_equal(self._agent_location, self._target_location)
truncated = False
reward = 1 if terminated else 0 # 에피소드 끝에만 에이전트에게 보상이 주어집니다.
observation = self._get_obs()
info = self._get_info()
return observation, reward, terminated, truncated, info
환경 등록 및 생성하기¶
새로운 사용자 지정 환경을 지금 바로 사용하는 것도 가능하지만, :meth:`gymnasium.make`를 사용하여 환경을 초기화하는 것이 더 일반적입니다. 이 섹션에서는 사용자 지정 환경을 등록한 다음 초기화하는 방법을 설명합니다.
환경 ID는 세 가지 구성 요소로 구성되며, 그 중 두 가지는 선택 사항입니다: 선택 사항인 네임스페이스(여기서는 gymnasium_env
), 필수인 이름(여기서는 GridWorld
), 그리고 선택 사항이지만 권장되는 버전(여기서는 v0). 또한 GridWorld-v0``(권장 접근 방식), ``GridWorld
또는 ``gymnasium_env/GridWorld``로 등록될 수도 있으며, 환경 생성 시에는 적절한 ID가 사용되어야 합니다.
진입점(entry point)은 문자열 또는 함수일 수 있습니다. 이 튜토리얼은 파이썬 프로젝트의 일부가 아니므로 문자열을 사용할 수 없지만, 대부분의 환경에서는 이것이 진입점을 지정하는 일반적인 방법입니다.
Register에는 환경에 대한 키워드 인수를 지정하는 데 사용할 수 있는 추가 매개변수가 있습니다. 예를 들어, 시간 제한 래퍼를 적용할지 여부 등입니다. 자세한 내용은 :meth:`gymnasium.register`를 참조하십시오.
gym.register(
id="gymnasium_env/GridWorld-v0",
entry_point=GridWorldEnv,
)
사용자 지정 환경 등록(문자열 진입점을 포함하여)에 대한 더 완전한 가이드는 전체 환경 생성 튜토리얼을 읽어보십시오.
환경이 등록되면, 등록된 모든 환경을 출력하는 :meth:`gymnasium.pprint_registry`를 통해 확인할 수 있으며, 그 후 :meth:`gymnasium.make`를 사용하여 환경을 초기화할 수 있습니다. 동일한 환경의 여러 인스턴스가 병렬로 실행되는 환경의 벡터화된 버전은 :meth:`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)
래퍼(Wrappers) 사용하기¶
종종 우리는 사용자 지정 환경의 다른 변형을 사용하거나, Gymnasium 또는 다른 곳에서 제공하는 환경의 동작을 수정하고자 합니다. 래퍼를 사용하면 환경 구현을 변경하거나 상용구(boilerplate) 코드를 추가하지 않고도 이를 수행할 수 있습니다. 래퍼 사용 방법에 대한 자세한 내용과 자신만의 래퍼 구현 방법은 래퍼 문서를 확인하십시오. 우리의 예제에서는 관찰이 딕셔너리이기 때문에 학습 코드에서 직접 사용할 수 없습니다. 그러나 이를 해결하기 위해 환경 구현을 건드릴 필요가 없습니다! 단순히 환경 인스턴스 위에 래퍼를 추가하여 관찰을 단일 배열로 평탄화할 수 있습니다:
>>> 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})