사용자 지정 환경 생성

이 페이지는 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})