В предыдущей части серии мы использовали идеи Q-Learning совместно с TensorFlow. Мы получили более общее решение для нашего агента, которому не нужна таблица для каждого состояния игры.
На этой неделе мы сделаем последний шаг и реализуем этот подход TensorFlow в Haskell. Мы увидим, как интегрировать эту библиотеку с нашей существующей системой Environment
. Это работает довольно гладко, с хорошим разделением между нашей логикой TensorFlow и нашей обычной логикой среды!
Эта статья требует практических знаний интеграции Haskell TensorFlow. Если вы новичок в этом, вам следует скачать наше Руководство, в котором показано, как работать с этим фреймворком. Вы также можете прочитать нашу оригинальную серию Машинное обучение, чтобы узнать больше! В частности, во второй части будут рассмотрены основы тензоров.
Построение нашей модели TF
Первое, что мы хотим сделать, это построить «модель». Этот тип модели будет хранить три элемента. Первым будет тензор для имеющихся у нас весов. Тогда вторые две будут функциями в монаде TensorFlow Session
. Первая функция предоставит баллы за различные ходы в позиции, поэтому мы можем выбрать свой ход. Второй позволит нам обучить модель и обновить веса.
data Model = Model
{ weightsT :: Variable Float
, chooseActionStep :: TensorData Float -> Session (Vector Float)
, learnStep :: TensorData Float -> TensorData Float -> Session ()
}
Входными данными для выбора действия является наше состояние наблюдения за миром, преобразованное в Float
и помещенное в вектор размера 16. Результатом будут 4 значения с плавающей запятой для баллов. Затем наш шаг обучения будет включать наблюдение, а также набор из 4 значений. Это «целевые» значения, на которых мы обучаем нашу модель.
Мы можем построить нашу модель внутри монады Session
. В первой части этого процесса мы определяем наши веса и используем их для определения счета каждого хода (результатов).
createModel :: Session Model
createModel = do
-- Choose Action
inputs <- placeholder (Shape [1, 16])
weights <- truncatedNormal (vector [16, 4]) >>= initializedVariable
let results = inputs `matMul` readValue weights
returnedOutputs <- render results
...
Теперь делаем наш «тренажер». Наша функция «потери» представляет собой уменьшенную квадратную разницу между нашими результатами и «целевыми» результатами. Мы будем использовать оптимизатор adam
, чтобы узнать значения наших весов, чтобы минимизировать эту потерю.
createModel :: Session Model createModel = do -- Choose Action ...
-- Train Nextwork (nextOutputs :: Tensor Value Float) <- placeholder (Shape [4, 1]) let (diff :: Tensor Build Float) = nextOutputs `sub` results let (loss :: Tensor Build Float) = reduceSum (diff `mul` diff) trainer_ <- minimizeWith adam loss [weights] ...
Наконец, мы оборачиваем эти тензоры в функции, которые можем вызывать с помощью runWithFeeds
. Напомним, что каждый feed
дает нам возможность заполнить один из наших тензоров-заполнителей.
createModel :: Session Model createModel = do -- Choose Action ...
-- Train Network ...
-- Create Model let chooseStep = \inputFeed -> runWithFeeds [feed inputs inputFeed] returnedOutputs let trainStep = \inputFeed nextOutputFeed -> runWithFeeds [ feed inputs inputFeed , feed nextOutputs nextOutputFeed ] trainer_ return $ Model weights chooseStep trainStep
Теперь наша модель включает в себя все необходимые тензорные операции! Все, что нам нужно сделать, это указать правильный TensorData
. Чтобы увидеть, как это работает, давайте начнем интеграцию с нашим EnvironmentMonad
!
Интеграция с EnvironmentMonad
Функции нашей модели существуют внутри монады TensorFlow Session
. Так как же нам интегрировать это с нашим существующим кодом Environment
? Ответ, конечно же, построить новую монаду! Эта монада будет обертывать Session
, при этом давая нам наше FrozenLakeEnvironment
! Мы сохраним окружение внутри State
, но мы также сохраним ссылку на наш Model
.
newtype FrozenLake a = FrozenLake (StateT (FrozenLakeEnvironment, Model) Session a) deriving (Functor, Applicative, Monad)
instance (MonadState FrozenLakeEnvironment) FrozenLake where get = FrozenLake (fst <$> get) put fle = FrozenLake $ do (_, model) <- get put (fle, model)
Теперь мы можем приступить к реализации фактического экземпляра EnvironmentMonad
. Большинство наших существующих типов и функций будут работать с тривиальной модификацией. Единственное реальное изменение заключается в том, что runEnv
нужно будет запустить сеанс TensorFlow и создать модель. Тогда он может использовать evalStateT
.
instance EnvironmentMonad FrozenLake where
type (Observation FrozenLake) = FrozenLakeObservation
type (Action FrozenLake) = FrozenLakeAction
type (EnvironmentState FrozenLake) = FrozenLakeEnvironment
baseEnv = basicEnv
currentObservation = currentObs <$> get
resetEnv = resetFrozenLake
stepEnv = stepFrozenLake
runEnv env (FrozenLake action) = runSession $ do
model <- createModel
evalStateT action (env, model)
Это все, что нам нужно для определения первого класса. Но с TensorFlow наша среда полезна только в том случае, если мы используем тензорную модель! Это означает, что нам также нужно заполнить LearningEnvironment
. У этого есть две функции, chooseActionBrain
и learnEnv
, использующие наши тензоры. Давайте посмотрим, как это работает.
Выбор действия
Выбор действия прост. Мы снова начнем с того же формата для иногда выбора случайного хода:
chooseActionTensor :: FrozenLake FrozenLakeAction
chooseActionTensor = FrozenLake $ do
(fle, model) <- get
let (exploreRoll, gen') = randomR (0.0, 1.0) (randomGenerator fle)
if exploreRoll < flExplorationRate fle
then do
let (actionRoll, gen'') = Rand.randomR (0, 3) gen'
put $ (fle { randomGenerator = gen'' }, model)
return (toEnum actionRoll)
else do
...
Как и в Python, нам нужно преобразовать наблюдение в тензорный тип. На этот раз мы создадим TensorData
. Этот тип оборачивает вектор, и наш ввод должен иметь размер 1x16. Он имеет формат тензора oneHot
. Но проще сделать это чистой функцией, чем использовать монаду TensorFlow.
obsToTensor :: FrozenLakeObservation -> TensorData Float
obsToTensor obs = encodeTensorData (Shape [1, 16]) (V.fromList asList)
where
asList = replicate (fromIntegral obs) 0.0 ++
[1.0] ++
replicate (fromIntegral (15 - obs)) 0.0
Поскольку мы уже определили наш шаг chooseAction
в модели, его легко использовать! Мы преобразуем текущее наблюдение, получаем значения результата, а затем выбираем лучший индекс!
chooseActionTensor :: FrozenLake FrozenLakeAction chooseActionTensor = FrozenLake $ do (fle, model) <- get -- Random move ... else do let obs1 = currentObs fle let obs1Data = obsToTensor obs1
-- Use model! results <- lift ((chooseActionStep model) obs1Data) let bestMoveIndex = V.maxIndex results put $ (fle { randomGenerator = gen' }, model) return (toEnum bestMoveIndex)
Учимся у окружающей среды
Одна неудачная часть нашего текущего дизайна заключается в том, что нам приходится повторять некоторую работу в нашей функции обучения. Чтобы извлечь урок из нашего действия, нам нужно использовать все значения, а не только выбранное действие. Итак, чтобы начать нашу функцию обучения, мы снова вызовем chooseActionStep
. На этот раз мы получим лучший индекс И максимальный балл.
learnTensor :: FrozenLakeObservation -> FrozenLakeObservation -> Reward -> FrozenLakeAction -> FrozenLake () learnTensor obs1 obs2 (Reward reward) action = FrozenLake $ do model <- snd <$> get let obs1Data = obsToTensor obs1
-- Use the model! results <- lift ((chooseActionStep model) obs1Data) let (bestMoveIndex, maxScore) = (V.maxIndex results, V.maximum results) ...
Теперь мы можем получить наши «целевые» значения, подставив награду и максимальное количество очков в соответствующий индекс. Затем мы преобразуем второе наблюдение в тензор, и у нас есть все входные данные для вызова нашего этапа обучения!
learnTensor :: FrozenLakeObservation -> FrozenLakeObservation -> Reward -> FrozenLakeAction -> FrozenLake () learnTensor obs1 obs2 (Reward reward) action = FrozenLake $ do ... let (bestMoveIndex, maxScore) = (V.maxIndex results, V.maximum results) let targetActionValues = results V.// [(bestMoveIndex, double2Float reward + (gamma * maxScore))] let obs2Data = obsToTensor obs2 let targetActionData = encodeTensorData (Shape [4, 1]) targetActionValues
-- Use the model! lift $ (learnStep model) obs2Data targetActionData
where gamma = 0.81
Используя эти две функции, теперь мы можем заполнить наш класс LearningEnvironment
!
instance LearningEnvironment FrozenLake where
chooseActionBrain = chooseActionTensor
learnEnv = learnTensor
-- Same as before
explorationRate = ..
reduceExploration = ...
Затем мы сможем запустить этот код так же, как и другие наши примеры Q-обучения!
Вывод
На этом мы завершаем часть этой серии, посвященную машинному обучению. На следующей неделе у нас будет еще одна статья об Open Gym. Мы сравним нашу текущую настройку и библиотеку Gloss. Gloss предлагает гораздо более широкие возможности для рендеринга нашей игры и приема ввода. Таким образом, его использование расширило бы диапазон игр, в которые мы могли бы играть!
Мы определенно продолжим расширять концепцию Open Gym в будущем! Ожидайте более формального подхода к этому в какой-то момент! А пока взгляните на наш репозиторий Github для этой серии! Код этой статьи находится в ветке tensorflow
!