Документация Engee

Создание настраиваемой среды

Для применения алгоритмов в ReinforcementLearning.jl сначала нужно определить решаемую задачу понятным образом. В этом разделе будет показано, как создавать различные среды на основе интерфейсов, определенных в

ReinforcementLearningBase.jl. Чаще всего для описания задач обучения с подкреплением применяются интерфейсы OpenAI/Gym. Опираясь на них, мы разработали несколько более расширенные версии, в которых используется возможность множественной диспетчеризации Julia и поддерживаются среды с несколькими агентами.

Минимальные интерфейсы для реализации

У многих интерфейсов в ReinforcementLearningBase.jl есть реализация по умолчанию. Поэтому в большинстве случаев для определения настраиваемой среды достаточно реализовать следующие функции:

action_space(env::YourEnv)
state(env::YourEnv)
state_space(env::YourEnv)
reward(env::YourEnv)
is_terminated(env::YourEnv)
reset!(env::YourEnv)
act!(env::YourEnv, action)

Пример: LotteryEnv

Здесь создание простой среды демонстрируется на примере из руководства по поиску по дереву Монте-Карло.

Условия игры такие: предположим, у вас в кармане 10 долларов и вам на выбор предлагается одно из трех действий:

  1. Купить лотерейный билет PowerRich (выигрыш 100 млн долларов с вероятностью 0,01; других выигрышей нет).

  2. Купить лотерейный билет MegaHaul (выигрыш 1 млн долларов с вероятностью 0,05; других выигрышей нет).

  3. Не покупать лотерейный билет.

Эта игра разовая. Она завершается сразу после выбора действия и получения награды. Сначала определим конкретный подтип AbstractEnv с именем LotteryEnv:

julia> using ReinforcementLearning


julia> Base.@kwdef mutable struct LotteryEnv <: AbstractEnv
           reward::Union{Nothing, Int} = nothing
       end
Main.LotteryEnv

У LotteryEnv есть только одно поле с именем reward; по умолчанию оно инициализируется со значением nothing. Теперь реализуем необходимые интерфейсы:

julia> struct LotteryAction{a}
           function LotteryAction(a)
               new{a}()
           end
       end


julia> RLBase.action_space(env::LotteryEnv) = LotteryAction.([:PowerRich, :MegaHaul, nothing])

Здесь RLBase — это просто псевдоним для ReinforcementLearningBase.

julia> RLBase.reward(env::LotteryEnv) = env.reward


julia> RLBase.state(env::LotteryEnv, ::Observation, ::DefaultPlayer) = !isnothing(env.reward)


julia> RLBase.state_space(env::LotteryEnv) = [false, true]


julia> RLBase.is_terminated(env::LotteryEnv) = !isnothing(env.reward)


julia> RLBase.reset!(env::LotteryEnv) = env.reward = nothing

Данная лотерея — это простая разовая игра. Поэтому если reward имеет значение nothing, значит игра еще не началась и находится в состоянии false. В противном случае игра завершилась и находится в состоянии true. Таким образом, результат state_space(env) описывает возможные состояния этой среды. Сбрасывая игру с помощью reset!, мы попросту устанавливаем reward в значение nothing, то есть снова переводим игру в начальное состояние.

Осталось лишь реализовать игровую логику:

julia> function RLBase.act!(x::LotteryEnv, action)
           if action == LotteryAction(:PowerRich)
               x.reward = rand() < 0.01 ? 100_000_000 : -10
           elseif action == LotteryAction(:MegaHaul)
               x.reward = rand() < 0.05 ? 1_000_000 : -10
           elseif action == LotteryAction(nothing)
               x.reward = 0
           else
               @error "unknown action of $action"
           end
       end

Тестирование среды

Для проведения нескольких моделирований и проверки работоспособности среды, которую мы определили, есть метод RLBase.test_runnable!.

julia> env = LotteryEnv()
# LotteryEnv

## Traits

| Trait Type        |                  Value |
|:----------------- | ----------------------:|
| NumAgentStyle     |          SingleAgent() |
| DynamicStyle      |           Sequential() |
| InformationStyle  | ImperfectInformation() |
| ChanceStyle       |           Stochastic() |
| RewardStyle       |           StepReward() |
| UtilityStyle      |           GeneralSum() |
| ActionStyle       |     MinimalActionSet() |
| StateStyle        |     Observation{Any}() |
| DefaultStateStyle |     Observation{Any}() |
| EpisodeStyle      |             Episodic() |

## Is Environment Terminated?

No

## State Space

`Bool[0, 1]`

## Action Space

`Main.LotteryAction[Main.LotteryAction{:PowerRich}(), Main.LotteryAction{:MegaHaul}(), Main.LotteryAction{nothing}()]`

## Current State

```
false
```

julia> RLBase.test_runnable!(env)
Test Summary:                 | Pass  Total  Time
random policy with LotteryEnv | 2000   2000  0.1s
Test.DefaultTestSet("random policy with LotteryEnv", Any[], 2000, false, false, true, 1.725026373550979e9, 1.725026373657591e9, false, "/root/.julia/packages/ReinforcementLearningBase/bZTn5/src/base.jl")

Это простой тест на очевидные проблемы в коде, который работает так:

n_episode = 10
for _ in 1:n_episode
    reset!(env)
    while !is_terminated(env)
        action = rand(action_space(env))
        act!(env, action)
    end
end

Далее необходимо проверить, работают ли остальные компоненты в ReinforcementLearning.jl. Как и в случае выше, давайте сначала протестируем RandomPolicy :

julia> run(RandomPolicy(action_space(env)), env, StopAfterNEpisodes(1_000))
EmptyHook()

Если ошибок не происходит, значит наша среда работает по крайней мере с RandomPolicy 🎉🎉🎉. Затем можно добавить обработчик для сбора награды в каждом эпизоде и проверки производительности RandomPolicy.

julia> hook = TotalRewardPerEpisode()
TotalRewardPerEpisode{Val{true}, Float64}(Float64[], 0.0, true)

julia> run(RandomPolicy(action_space(env)), env, StopAfterNEpisodes(1_000), hook)
TotalRewardPerEpisode{Val{true}, Float64}([0.0, 0.0, 0.0, -10.0, -10.0, -10.0, -10.0, 0.0, -10.0, -10.0  …  -10.0, 0.0, 0.0, -10.0, -10.0, -10.0, 0.0, 0.0, -10.0, -10.0], 0.0, true)

julia> using Plots



julia> plot(hook.rewards)
Plot{Plots.GRBackend() n=1}
]1337;ReportCellSizeGKS: cannot open display - headless operation mode active
custom env random policy reward

Добавление оболочки среды

Теперь предположим, что мы хотим использовать табличный метод Монте-Карло для оценки значения «состояние-действие».

julia> p = QBasedPolicy(
           learner = TDLearner(
               TabularQApproximator(
                   n_state = length(state_space(env)),
                   n_action = length(action_space(env)),
               ), :SARS
           ),
           explorer = EpsilonGreedyExplorer(0.1)
       )
QBasedPolicy{TDLearner{:SARS, TabularQApproximator{Matrix{Float64}}}, EpsilonGreedyExplorer{:linear, false, TaskLocalRNG}}(TDLearner{:SARS, TabularQApproximator{Matrix{Float64}}}(TabularQApproximator{Matrix{Float64}}([0.0 0.0; 0.0 0.0; 0.0 0.0]), 1.0, 0.01, 0), EpsilonGreedyExplorer{:linear, false, TaskLocalRNG}(0.1, 1.0, 0, 0, 1, TaskLocalRNG()))

julia> plan!(p, env)
ERROR: MethodError: no method matching forward(::TDLearner{:SARS, TabularQApproximator{Matrix{Float64}}}, ::Bool)

Closest candidates are:
  forward(!Matched::TargetNetwork, ::Any...)
   @ ReinforcementLearningCore ~/.julia/packages/ReinforcementLearningCore/BYdWk/src/policies/learners/target_network.jl:65
  forward(::L, !Matched::E) where {L<:AbstractLearner, E<:AbstractEnv}
   @ ReinforcementLearningCore ~/.julia/packages/ReinforcementLearningCore/BYdWk/src/policies/learners/abstract_learner.jl:15
  forward(!Matched::TabularApproximator{R}, ::I) where {R<:AbstractArray, I}
   @ ReinforcementLearningCore ~/.julia/packages/ReinforcementLearningCore/BYdWk/src/policies/learners/tabular_approximator.jl:45
  ...

Но здесь мы получаем ошибку. Что же это означает?

Прежде чем отвечать на этот вопрос, давайте немного разберемся в политике, которая была определена выше. QBasedPolicy состоит из двух частей: learner (обучатель) и explorer (исследователь). Компонент learner изучает функцию значений «состояние-действие» (Q-функцию) во время взаимодействий с env. Компонент explorer служит для выбора действия на основе Q-значения, возвращенного компонентом learner. Внутри TDLearner объект TabularQApproximator используется для оценки Q-значения.

Вот в чем проблема! TabularQApproximator принимает только состояния типа Int.

julia> RLCore.forward(p.learner.approximator, 1, 1)  # Q(s, a)
0.0

julia> RLCore.forward(p.learner.approximator, 1)     # [Q(s, a) for a in action_space(env)]
3-element view(::Matrix{Float64}, :, 1) with eltype Float64:
 0.0
 0.0
 0.0

julia> RLCore.forward(p.learner.approximator, false)
ERROR: ArgumentError: invalid index: false of type Bool

Хорошо, теперь мы знаем, где кроется проблема. Но как ее устранить?

Первое, что приходит в голову, — переписать функцию RLBase.state(env::LotteryEnv, ::Observation, ::DefaultPlayer) так, чтобы она обязательно возвращала значение типа Int. Это работает. Но иногда могут использоваться среды, созданные другими разработчиками, и изменить код напрямую может быть непросто. К счастью, существуют оболочки сред, которые позволяют преобразовывать среды.

julia> wrapped_env = ActionTransformedEnv(
           StateTransformedEnv(
               env;
               state_mapping=s -> s ? 1 : 2,
               state_space_mapping = _ -> Base.OneTo(2)
           );
           action_mapping = i -> action_space(env)[i],
           action_space_mapping = _ -> Base.OneTo(3),
       )
# LotteryEnv |> StateTransformedEnv |> ActionTransformedEnv

## Traits

| Trait Type        |                  Value |
|:----------------- | ----------------------:|
| NumAgentStyle     |          SingleAgent() |
| DynamicStyle      |           Sequential() |
| InformationStyle  | ImperfectInformation() |
| ChanceStyle       |           Stochastic() |
| RewardStyle       |           StepReward() |
| UtilityStyle      |           GeneralSum() |
| ActionStyle       |     MinimalActionSet() |
| StateStyle        |     Observation{Any}() |
| DefaultStateStyle |     Observation{Any}() |
| EpisodeStyle      |             Episodic() |

## Is Environment Terminated?

Yes

## State Space

`Base.OneTo(2)`

## Action Space

`Base.OneTo(3)`

## Current State

```
1
```

julia> plan!(p, wrapped_env)
1

Отлично! Теперь можно провести эксперимент:

julia> h = TotalRewardPerEpisode()
TotalRewardPerEpisode{Val{true}, Float64}(Float64[], 0.0, true)

julia> run(p, wrapped_env, StopAfterNEpisodes(1_000), h)
TotalRewardPerEpisode{Val{true}, Float64}([-10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0  …  -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0, -10.0], 0.0, true)

julia> plot(h.rewards)
Plot{Plots.GRBackend() n=1}
custom env random policy reward wrapped env

Если вы были достаточно наблюдательны, то заметили, что политика вообще не

обновляется! На самом деле, она выполняется в режиме исполнителя. Для обновления политики не забудьте заключить ее в объект Agent.

Более сложные среды

Приведенная выше среда LotteryEnv весьма простая. Многие среды, с которыми мы будем иметь дело, относятся к этой же категории. Однако, помимо этого, есть и множество других сред. Посмотреть, какие типы сред поддерживаются, можно в разделе Built-in Environments.

Чтобы можно было различать различные виды сред, в ReinforcementLearningBase.jl определен ряд их характерных черт. Разберем их поочередно.

StateStyle

В приведенном выше примере LotteryEnv функция state(env::LotteryEnv) возвращает просто логическое значение. Однако в некоторых других средах имя функции state может иметь более расплывчатый смысл. Специалисты из разных областей часто называют одно и то же по-разному. Вам может быть интересно следующее обсуждение: В чем различие между наблюдением и состоянием в обучении с подкреплением? Чтобы избежать путаницы при выполнении state(env), разработчик среды может определить state(::AbstractStateStyle, env::YourEnv) явным образом. Это позволит пользователям получать нужную информацию по запросу. Вот некоторые встроенные стили состояний:

julia> using InteractiveUtils


julia> subtypes(RLBase.AbstractStateStyle)
4-element Vector{Any}:
 GoalState
 InformationSet
 InternalState
 Observation

Имейте в виду, что у каждого стиля состояния может быть множество разных представлений: String, Array, Graph и т. д. Все перечисленные выше стили могут принимать тип данных в качестве параметра. Например:

julia> RLBase.state(::Observation{String}, env::LotteryEnv) = is_terminated(env) ? "Game Over" : "Game Start"

Для сред, поддерживающих состояния множества разных типов, разработчики должны указывать все поддерживаемые стили состояний. Например:

julia> tp = TigerProblemEnv();


julia> StateStyle(tp)
(Observation{Int64}(), InternalState{Int64}())

julia> state(tp, Observation{Int64}())
1

julia> state(tp, InternalState{Int64}())
1

julia> state(tp)
1

DefaultStateStyle

DefaultStateStyle по умолчанию возвращает первый элемент из результата StateStyle.

Разработчики алгоритмов обычно не заботятся о стилях состояний. Они могут принимать как само собой разумеющееся, что стиль по умолчанию всегда четко определен, и просто вызывать state(env) для получения правильного представления. Поэтому для сред со множеством различных представлений state(env) будет диспетчеризоваться в state(DefaultStateStyle(env), env). И мы можем использовать DefaultStateStyleEnv оболочку для переопределения предварительно определенного стиля DefaultStateStyle(::YourEnv).

RewardStyle

Во многих играх, таких как шахматы, го и многие карточные игры, получение награды происходит только в конце игры. Такие игры относятся к типу TerminalReward, а все остальные — к типу StepReward. На самом деле, TerminalReward — это частный случай StepReward (на всех шагах, кроме последнего, награда равна 0). Причина, по которой эти случаи все же следует различать, заключается в том, что для игр в стиле TerminalReward некоторые алгоритмы можно реализовать эффективнее.

julia> RewardStyle(tp)
StepReward()

julia> RewardStyle(MontyHallEnv())
TerminalReward()

ActionStyle

Для некоторых сред допустимые действия на каждом шаге могут быть разными. Такие среды относятся к стилю FullActionSet. В противном случае среда относится к стилю MinimalActionSet. Типичной встроенной средой со стилем FullActionSet является среда TicTacToeEnv. Необходимо определить еще два метода:

julia> ttt = TicTacToeEnv();


julia> ActionStyle(ttt)
FullActionSet()

julia> legal_action_space(ttt)
9-element Vector{Int64}:
 1
 2
 3
 4
 5
 6
 7
 8
 9

julia> legal_action_space_mask(ttt)
9-element BitVector:
 1
 1
 1
 1
 1
 1
 1
 1
 1

Пространство действий некоторых простых сред может описываться просто кортежем (Tuple) или вектором (Vector). Однако иногда пространство действий не так легко описать с помощью какой-либо встроенной структуры данных. В таком случае можно определить настраиваемую структуру, реализовав для нее следующие интерфейсы:

  • Base.in

  • Random.rand

Например, так можно определить пространство действий для N-мерного симплекса:

julia> using Random


julia> struct SimplexSpace
           n::Int
       end


julia> function Base.in(x::AbstractVector, s::SimplexSpace)
           length(x) == s.n && all(>=(0), x) && isapprox(1, sum(x))
       end


julia> function Random.rand(rng::AbstractRNG, s::SimplexSpace)
           x = rand(rng, s.n)
           x ./= sum(x)
           x
       end

NumAgentStyle

В приведенной выше среде LotteryEnv игрок только один. Однако во многих настольных играх обычно участвует несколько игроков.

julia> NumAgentStyle(env)
SingleAgent()

julia> NumAgentStyle(ttt)
MultiAgent{2}()

Для сред с несколькими агентами вводится несколько новых интерфейсов API. Кроме того, расширяется смысл некоторых API, с которыми мы уже познакомились. Во-первых, разработчики сред с несколькими агентами должны реализовать players для различения игроков.

julia> players(ttt)
(Player(:Cross), Player(:Nought))

julia> current_player(ttt)
Player(:Cross)
Один агент Несколько агентов

state(env)

state(env, player)

reward(env)

reward(env, player)

env(action)

env(action, player)

action_space(env)

action_space(env, player)

state_space(env)

state_space(env, player)

is_terminated(env)

is_terminated(env, player)

Обратите внимание, что все интерфейсы API для одного агента по-прежнему действуют, но в контексте current_player(env).

UtilityStyle

Иногда в средах с несколькими агентами сумма наград всех игроков обязательно должна быть равна 0. Стиль UtilityStyle таких сред называется ZeroSum. ZeroSum — это частный случай ConstantSum. В кооперативных играх у всех игроков одинаковая награда. В этом случае стиль будет называться IdenticalUtility. В остальных случаях применяется стиль GeneralSum.

InformationStyle

Если все игроки видят одно и то же состояние, стиль InformationStyle среды называется PerfectInformation. Это частный случай сред ImperfectInformation .

DynamicStyle

Все встречавшиеся нам до сих пор среды относились к стилю Sequential: на каждом шаге выполнять действие было разрешено только ОДНОМУ игроку. Наряду с этим есть среды Simultaneous, в которых все игроки выполняют действия одновременно, причем не зная на этот момент, какие действия выбирают другие игроки. Одновременные среды должны принимать в качестве входных данных коллекцию действий разных игроков.

julia> rps = RockPaperScissorsEnv();


julia> action_space(rps)
(('💎', '💎'), ('💎', '📃'), ('💎', '✂'), ('📃', '💎'), ('📃', '📃'), ('📃', '✂'), ('✂', '💎'), ('✂', '📃'), ('✂', '✂'))

julia> action = plan!(RandomPolicy(), rps)
('📃', '📃')

julia> act!(rps, action)
true

ChanceStyle

Если в среде нет генератора rng, то есть последствия каждого действия детерминированы, стиль ChanceStyle таких сред называется Deterministic. В противном случае они имеют стиль Stochastic, что является возвращаемым по умолчанию значением. Особым случаем являются игры в развернутой форме, в который есть узел случая и для этого особого игрока определена вероятность действия. Стиль ChanceStyle таких сред называется EXPLICIT_STOCHASTIC. Для таких сред должны быть определены следующие методы:

julia> kp = KuhnPokerEnv();


julia> chance_player(kp)
ChancePlayer()

julia> prob(kp, chance_player(kp))
3-element Vector{Float64}:
 0.3333333333333333
 0.3333333333333333
 0.3333333333333333

julia> chance_player(kp) in players(kp)
true

Чтобы явным образом задать стиль случайности для пользовательской среды, можно определить для нее метод диспетчеризации ChanceStyle.

Примеры

Вы ознакомились со всеми необходимыми сведениями для создания настраиваемой среды. Рекомендуем также разобрать примеры в

ReinforcementLearningEnvironments.jl. Если вы все же не понимаете, как описать вашу задачу с помощью определенных в этом пакете интерфейсов, смело создавайте проблему в репозитории.