Создание настраиваемой среды
Для применения алгоритмов в 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 долларов и вам на выбор предлагается одно из трех действий:
-
Купить лотерейный билет PowerRich (выигрыш 100 млн долларов с вероятностью 0,01; других выигрышей нет).
-
Купить лотерейный билет MegaHaul (выигрыш 1 млн долларов с вероятностью 0,05; других выигрышей нет).
-
Не покупать лотерейный билет.
Эта игра разовая. Она завершается сразу после выбора действия и получения награды. Сначала определим конкретный подтип 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
Добавление оболочки среды
Теперь предположим, что мы хотим использовать табличный метод Монте-Карло для оценки значения «состояние-действие».
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}
Если вы были достаточно наблюдательны, то заметили, что политика вообще не |
обновляется! На самом деле, она выполняется в режиме исполнителя. Для обновления политики не забудьте заключить ее в объект 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)
Один агент | Несколько агентов |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Обратите внимание, что все интерфейсы 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. Если вы все же не понимаете, как описать вашу задачу с помощью определенных в этом пакете интерфейсов, смело создавайте проблему в репозитории.