В предыдущей части серии мы использовали идеи 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!