ФЕВРАЛЯ 2016

ТОМ 31 НОМЕР 2

Microsoft Azure Azure Service Fabric, Q-обучение и крестики-нолики

Хесус Агилар

Продукты и технологии:

Azure Service Fabric, Q-обучение

В статье рассматриваются:

  • разновидность метода обучения с подкреплением (reinforcement learning) — Q-обучение (Q-learning);
  • платформа Azure Service Fabric и ее модель программирования акторов;
  • предоставление функциональности актора через контроллер API;
  • реализация игры в крестики-нолики.

Исходный код можно скачать по ссылке aka.ms/servicefabricqlearning.

Инновации в облачных вычислениях понизили барьеры вхождения в использование распределенных вычислений и машинного обучения, которые превратились из нишевых технологий, требующих специализированной и дорогостоящей инфраструктуры, в предложение для массового спроса, доступное любому разработчику ПО или архитектору решения. В этой статье я опишу реализацию метода обучения с подкреплением, которая использует возможности Azure Service Fabric (следующей итерации предложения Azure Platform-as-a-Service [PaaS]) в области распределенных вычислений и хранения данных. Чтобы продемонстрировать потенциал этого подхода, я покажу, как можно задействовать Service Fabric и ее модель программирования Reliable Actors, чтобы создать интеллектуальную серверную часть, способную предсказывать следующий ход в игре «крестики-нолики».

Знакомьтесь с Q-обучением

Сегодня мы повсюду встречаемся с инновационными решениями, управляемыми данными, такими как выдача рекомендаций, распознавание лиц и обнаружение попыток мошенничества. Группы инженерии ПО используют для реализации этих решений как контролируемые, так и неконтролируемые методы обучения. Несмотря на широчайшие возможные этих подходов, бывают случаи, где их трудно применить.

Обучение с подкреплением (reinforcement learning) — это метод, обрабатывающий сценарии, которые можно представить как последовательность состояний и переходов между ними. В противоположность другим методам машинного обучения при этом подходе не делается попыток обобщения закономерностей, обучая модель на основе размеченной информации (labeled information) (контролируемое обучение) или неразмеченных данных (unlabeled data) (неконтролируемое обучение). Вместо этого метод фокусируется на задачах, которые можно смоделировать как последовательность состояний и переходов.

Скажем, ваш сценарий можно представить как последовательность состояний, ведущих к конечному состоянию (известное как поглощающее [absorbing state]). В качестве примера подумайте о роботе, принимающем решения для предотвращения столкновений с препятствиями, или искусственном интеллекте (artificial intelligence, AI) в игре, который стремится победить оппонента. Во многих случаях последовательность состояний, ведущих к конкретной ситуации, как раз и является тем, что определяет следующий лучший шаг для агента/робота/AI-персонажа.

Q-обучение (Q-learning) — метод обучения с подкреплением, который использует итеративный механизм подкрепления (iterative reward mechanism) для нахождения оптимальных промежуточных путей (transitional pathways) в модели конечного автомата (state machine model); он работает замечательно хорошо, когда количество состояний и число переходов между ними конечно. В этой статье я представлю, как я использовал Service Fabric для создания комплексного решения в области Q-обучения, и покажу, как можно создать интеллектуальную серверную часть, которая «учится» играть в крестики-нолики. (Заметьте, что сценарии с использованием конечных автоматов также называют Марковскими процессами принятия решений [Markov Decision Processes, MDP]).

Для начала немного базовой теории Q-обучения. Рассмотрим состояния и переходы, приведенные на рис. 1. Допустим, вы хотите находить в любом состоянии, в какое состояние должен перейти агент, чтобы прибыть к состоянию gold с минимальным числом переходов. Один из способов справиться с этой задачей — присвоить каждому состоянию значение подкрепления (reward value). Подкрепление предполагает значимость перехода в состояние в направлении вашей цели: к получению золота (gold).

A Sequence of States Leading to the Gold State
Рис. 1. Последовательность состояний, ведущих к состоянию Gold

Просто, да? Проблема в том, как определить подкрепление для каждого состояния. Алгоритм Q-обучения идентифицирует подкрепления (rewards), рекурсивно выполняя итерации и назначение подкреплений состояниям, ведущим к поглощающему состоянию (золоту). Алгоритм вычисляет подкрепление для состояния уменьшая значение подкрепления из последующего состояния. Если состояние имеет два значения подкрепления (что возможно, если состояние встречается более чем на одном пути), приоритет имеет наивысшее значение. Уменьшение значения подкрепления дает важный эффект для системы. За счет этого алгоритм понижает значение подкрепления для состояний, которые далеки от золота, и присваивает большее весовое значение состояниям, ближайшим к золоту.

В качестве примера того, как алгоритм вычисляет подкрепление, посмотрите на диаграмму состояний на рис. 1. Как видите, к золоту ведут три пути:

1→5→4→G

1→5→3→4→G

1→5→3→2→4→G

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

1(R=72)→5(R=81)→4(R=90)→G (R=100) 

1(R=64)→5(R=72)→3(R=81)→4(R=90)→ G(R=100)

1(R=58)→5(R=64)→3(R=72)→2(R=81)→4(R=90)→ G(R=100)

Поскольку у некоторых состояний более одного значения подкрепления, используются наивысшие значения. На рис. 2 изображено конечное распределение подкреплений.

Final Rewards
Рис. 2. Конечные значения подкреплений

Благодаря этой информации агент может определить оптимальный путь к золоту из любого состояния, переходя в состояние с наивысшим подкреплением. Например, если агент находится в состоянии 5, ему предоставляется выбор перейти в состояние 3 или 4, и, поскольку состояние 4 имеет более высокое подкрепление, выбирается именно оно.

Azure Service Fabric

Service Fabric, следующая итерация предложения Azure Platform-as-a-Service, позволяет разработчикам создавать распределенные приложения, используя две разные модели высокоуровневого программирования: Reliable Actors и Reliable Services. Эти модели программирования дают возможность максимально эффективно задействовать инфраструктурные ресурсы распределенной платформы. Сама платформа обрабатывает наиболее сложные задачи, связанные с поддержкой и выполнением распределенного приложения, в частности: восстановление после сбоев, распределение сервисов для обеспечения эффективного использования ресурсов, развертывание обновлений и контроль сосуществующих версий (side-to-side versioning).

Service Fabric предоставляет вам кластер, обеспечивая использование более высокого уровня абстракции, избавляя от заботы о нижележащей инфраструктуре. Ваш код выполняется на узлах кластера Service Fabric, и вы можете размещать многоузловые кластеры на одной машине для целей разработки или на нескольких серверах (виртуальных или физических машинах) для производственных нагрузок. Платформа управляет жизненным циклом ваших акторов и сервисов, а также восстановлением после сбоев инфраструктуры.

Service Fabric вводит концепции Reliable Actors и Reliable Services с семантикой поддержки состояний. Эта возможность транслируется в полностью интегрированную среду разработки, в которой можно создавать приложения, сохраняющие данные в распределенной, а значит, и высокодоступной среде хранения без необходимости включения в вашу архитектуру внешнего уровня хранения (например, введения зависимости от внешнего хранилища или уровня кеширования).

Обучение с подкреплением — это метод, обрабатывающий сценарии, которые можно представить как последовательность состояний и переходов между ними.

Реализуя алгоритм Q-обучения как сервис внутри Service Fabric, вы получаете преимущества распределенных вычислений и хранилища состояний с низкой латентностью, что позволяет вам выполнять алгоритм, сохранять результаты и предоставлять всю систему как надежные конечные точки для клиентского доступа. Все эти возможности предлагаются в одном решении с унифицированным стеком программирования и управления. Никакой необходимости добавлять дополнительные компоненты (вроде внешнего хранилища, кеша или системы обмена сообщениями) в вашу архитектуру нет. Если коротко, то вы получаете решение, в котором вычислительные ресурсы, данные и сервисы находятся в рамках одной интегрированной платформы. Это элегантное решение, по моему мнению!

Q-обучение и Reliable Actors

Модель акторов упрощает архитектуру приложений с интенсивной параллельной обработкой. В этой модели акторы являются фундаментальными вычислительными единицами. Актор представляет границы функциональности и состояние. Актор можно рассматривать как объектную сущность, «живущую» в распределенной системе. Жизненным циклом актора управляет Service Fabric. В случае аварии Service Fabric автоматически заново создает экземпляр актора на работоспособном узле. Например, если актор с состоянием рухнет по какой-то причине или же произойдет авария на узле (считайте — в VM), где он выполняется, этот актор автоматически создается заново на другой машине со всеми данными своего состояния в целости и невредимости.

Service Fabric также управляет тем, как происходит обращение к экземпляру актора. Платформа гарантирует, что в любой момент единовременно выполняется только один метод конкретного актора. Если появляется два одновременных вызова одного и того же актора, Service Fabric ставит в очередь один из них и разрешает обработку другого. Вследствие этого вашему коду внутри актора нет нужды беспокоиться об условиях гонок, блокировке или синхронизации.

Как только алгоритм идентифицирует поглощающее состояние, он вычислит подкрепления для всех состояний, ведущих к этому состоянию.

Как я уже рассказывал, алгоритм Q-обучения перебирает состояния с целью нахождения поглощающих состояний и состояний с подкреплениями. Как только алгоритм идентифицирует поглощающее состояние, он вычислит подкрепления для всех состояний, ведущих к этому состоянию.

Используя модель акторов, я могу моделировать эту функциональность в виде актора, который представляет состояние в контексте алгоритма Q-обучения. В моей реализации тип актора, представляющего эти состояния, — QState. Когда появляется переход в актор QState, содержащий подкрепление, актор QState создает другой экземпляр актора другого типа (QTrainedState) для каждого из акторов QState на пути. Акторы QTrainedState поддерживают максимальное значение подкрепления и список последующих состояний, которые ведут к награде. Список содержит маркеры последующих состояний (которые уникально идентифицируют их в графе).

На рис. 3 я изобразил логику алгоритма, использующего акторы, для очень простого сценария, где состояние с маркером 3 является поглощающим, содержит подкрепление 100 и имеет лишь один путь к себе по двум предыдущим состояниям (под маркерами 1 и 2). Каждая окружность представляет экземпляр актора, типы QState показаны светло-серым цветом, а QTrainedState — темно-серым. Когда процесс перехода достигает QState с маркером состояния 3, актор QState создает два QTrainedState, по одному на каждый из предыдущих QState. Для актора QTrainedState, представляющего состояние с маркером 2, предлагаемый переход (за подкрепление 90) — в состояние с маркером 3, а для актора QTrainedState, представляющего состояние с маркером 1, предлагаемый переход (за подкрепление 81) — в состояние с маркером 2.


Рис. 3. Определение и сохранение подкреплений

No Reward/Not Absorbant Подкрепления нет/поглощения нет
R:81 Children States: 2 R:81 Дочерние состояния: 2
R:90 Children States: 3 R:90 Дочерние состояния: 3
Reward = 100 Absorbant = True Discount = .9 R:100 Поглощающее состояние: True Коэффициент дисконта: .9
QStates QStates
QTrainedStates

QTrainedStates

 

Есть вероятность, что несколько состояний дадут одно и то же подкрепление, поэтому актор QTrainedState сохраняет набор маркеров как дочерние состояния.

В следующем коде приведена реализация интерфейсов для акторов QState и QTrainedState: IQState и IQTrainedState. QState имеет два поведения: переход в другие QState и запуск процесса перехода, когда предыдущего перехода не было:

public interface IQState : IActor
{
  Task StartTrainingAsync(int initialTransitionValue);
  Task TransitionAsync(int? previousStateToken, int transitionValue);
}
public interface IQTrainedState:IActor
{
 .Task AddChildQTrainedStateAsync(int stateToken, double reward);
 .Task<List<int>> GetChildrenQTrainedStatesAsync();
}

Заметьте, что реализация IQTrainedState предоставляет метод GetChildrenQTrainedStatesAsync. Через этот метод актор QTrainedState будет предоставлять обучающие данные, содержащие состояния с наивысшим значением подкрепления для любого состояния в системе. (Заметьте, что все акторы в Service Fabric должны реализовать интерфейс, производный от IActor.)

Актор QState

После определения интерфейсов я могу перейти к реализации акторов. Начну с актора QState и метода TransitionAsync, который является краеугольным камнем алгоритма и где выполняется большая часть работы. TransitionAsync выполняет переход в другое состояние, создавая новый экземпляр актора QState и вновь вызывая тот же метод.

По-видимому, вас заинтересовало, можно ли рекурсивным вызовом этого метода избежать издержек вызова метода через другой экземпляр актора. Рекурсивный вызов метода — операция, требующая интенсивных вычислений на одном узле. И напротив, создавая экземпляр другого актора, вы используете преимущества возможностей Service Fabric, позволяя платформе распределить обработку по горизонтальным вычислительным ресурсам.

Напоминания являются новыми конструкциями, введенными в модель программирования акторов, которые позволяют вам планировать асинхронную работу без блокирования выполнения какого-либо метода.

Для управления назначением подкрепления я зарегистрирую напоминание (reminder). Напоминания являются новыми конструкциями, введенными в модель программирования акторов, которые позволяют вам планировать асинхронную работу без блокирования выполнения какого-либо метода.

Напоминания доступны только для акторов с состоянием (stateful actors). Для акторов как с состоянием, так и без него платформа предоставляет таймеры, которые поддерживают похожие шаблоны. Одно из важных соображений заключается в том, что, когда используется актор, процесс сбора мусора откладывается; тем не менее, платформа не считает обратные вызовы таймера как нагрузку. Если начинает работать сборщик мусора, таймеры будут остановлены. Акторы не подлежат сбору мусора, пока выполняется какой-либо их метод. Чтобы обеспечить рекурсивное выполнение, используйте напоминания. Подробнее см. по ссылке bit.ly/1RmzKfr.

Цель, относящаяся к алгоритму, выполнить назначение подкрепления, не блокируя процесс перехода. Как правило, планирования рабочего элемента в пуле потоков с обратным вызовом будет достаточно; но в модели программирования акторов этот подход не считается хорошей идеей, так как вы потеряете преимущества параллельной обработки, предлагаемые платформой.

Платформа гарантирует, что в любой момент выполняется только один метод. Это позволяет писать код без учета параллельной обработки, т. е. не заботясь о безопасности в многопоточной среде. Как и следовало ожидать, существует компромисс: вы должны избегать создания задач или потоков, чтобы обертывать операции внутри методов актора. Напоминания дают возможность реализовать фоновую обработку с гарантиями платформы в отношении параллельной обработки, как показано на рис. 4.

Рис. 4. TransitionAsync в классе QState

public abstract class QState : StatefulActor, IQState, IRemindable
{
  // ...
  public Task TransitionAsync(int? previousStateToken, int transitionValue)
  {
    var rwd = GetReward(previousStateToken, transitionValue);
    var stateToken = transitionValue;
    if (previousStateToken != null)
        stateToken = int.Parse(previousStateToken.Value + stateToken.ToString());
    var ts = new List<Task>();
    if (rwd == null || !rwd.IsAbsorbent)
      ts.AddRange(GetTransitions(stateToken).Select(p =>
        ActorProxy.Create<IQState>(ActorId.NewId(),
        "fabric:/QLearningServiceFab").TransitionAsync(stateToken, p)));
    if (rwd != null)
      ts.Add(RegisterReminderAsync("SetReward",
        Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(rwd))
        , TimeSpan.FromMilliseconds(0)
        , TimeSpan.FromMilliseconds(-1), ActorReminderAttributes.Readonly));
      return Task.WhenAll(ts);
  }
  // ...
}

(Заметьте, что установка dueTime в TimeSpan.FromMilliseconds(0)) указывает на немедленное выполнение.)

Чтобы закончить реализацию IQState, следующий код реализует метод StartTransitionAsync, где я использую напоминание, чтобы избежать блокирующего вызова с длительным выполнением:

public Task StartTrainingAsync(int initialTransitionValue)
  {
    return RegisterReminderAsync("StartTransition",
      Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { TransitionValue =
      initialTransitionValue })), TimeSpan.FromMilliseconds(0),
      TimeSpan.FromMilliseconds(-1),
      ActorReminderAttributes.Readonly);
  }

Чтобы закончить с классом QState, я опишу реализацию методов SetRewardAsync и ReceiveReminderAsync (рис. 5). Метод SetReward создает или обновляет актор с состоянием (реализация IQTrainedState). Для нахождения актора при последующих вызовах я использую маркер состояния как идентификатор актора — акторы являются адресуемыми сущностями.

Рис. 5. Методы SetRewardAsync и ReceiveReminderAsync

public Task SetRewardAsync(int stateToken, double stateReward, double discount)
  {
    var t = new List<Task>();
    var reward = stateReward;
    foreach (var pastState in GetRewardingQStates(stateToken))
    {
      t.Add(ActorProxy
        .Create<IQTrainedState>(new ActorId(pastState.StateToken),
          "fabric:/QLearningServiceFab")
        .AddChildQTrainedStateAsync(pastState.NextStateToken, reward));
      reward = reward * discount;
    }
    return Task.WhenAll(t);
  }
public async Task ReceiveReminderAsync(string reminderName,
  byte[] context, TimeSpan dueTime, TimeSpan period)
  {
    await UnregisterReminderAsync(GetReminder(reminderName));
    var state = JsonConvert.DeserializeObject<JObject>(
      Encoding.UTF8.GetString(context));
    if (reminderName == "SetReward")
    {
      await SetRewardAsync(state["StateToken"].ToObject<int>(),
        state["Value"].ToObject<double>(),
        state["Discount"].ToObject<double>());
    }
    if (reminderName == "StartTransition")
    {
      await TransitionAsync(null, state["TransitionValue"].ToObject<int>());
    }
  }

Актор QTrainedState

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

В Service Fabric вы реализуете актор с состоянием, наследуя свой класс от базового класса StatefulActor или StatefulActor<T> и реализуя интерфейс, производный от IActor. T — это тип экземпляра состояния, которое должно быть сериализуемым и иметь ссылочный тип. Когда вы вызываете какой-либо метод класса, производного от StatefulActor<T>, платформа загружает состояние из провайдера состояний и, как только вызов завершается, автоматически сохраняет это состояние. В случае QTrainedState я смоделировал состояние (отказоустойчивые данные), используя следующий класс:

[DataContract]
public class QTrainedStateState
{
  [DataMember]
  public double MaximumReward { get; set; }
  [DataMember]
  public HashSet<int> ChildrenQTrainedStates { get; set; }
}

На рис. 6 показана полная реализация класса QTrainedState, который реализует два метода интерфейса IQTrainedState.

Рис. 6. Класс QTrainedState

public class QTrainedState : StatefulActor<QTrainedStateState>, IQTrainedState
{
  protected async override Task OnActivateAsync()
  {
    this.State =
      await ActorService.StateProvider.LoadStateAsync<QTrainedStateState>(
      Id, "qts") ??
      new QTrainedStateState() { ChildrenQTrainedStates = new HashSet<int>() };
    await base.OnActivateAsync();
  }
  protected async override Task OnDeactivateAsync()
  {
    await ActorService.StateProvider.SaveStateAsync(Id, "qts", State);
    await base.OnDeactivateAsync();
  }
  [Readonly]
  public  Task AddChildQTrainedStateAsync(int stateToken, double reward)
  {
    if (reward < State.MaximumReward)
    {
      return Task.FromResult(true);
    }
    if (Math.Abs(reward - State.MaximumReward) < 0.10)
    {
      State.ChildrenQTrainedStates.Add(stateToken);
      return Task.FromResult(true);
    }
      State.MaximumReward = reward;
      State.ChildrenQTrainedStates.Clear();
      State.ChildrenQTrainedStates.Add(stateToken);
      return Task.FromResult(true);
  }
  [Readonly]
  public Task<List<int>> GetChildrenQTrainedStatesAsync()
  {
    return Task.FromResult(State.ChildrenQTrainedStates.ToList());
  }
}

Доступ к функциональности акторов

К этому моменту в решении есть все необходимое, чтобы начать процесс обучения и сохранять данные. Но я еще не рассказал, как клиенты будут взаимодействовать с этими акторами. На высоком уровне это взаимодействие заключается в запуске процесса обучения и запроса сохраненных данных. Каждая из таких операций взаимодействия коррелирует с API-операцией, и RESTful-реализация облегчает интеграцию с клиентами.

Service Fabric не только предоставляет две модели программирования, но и является полной платформой координации и управления процессами.

Service Fabric не только предоставляет две модели программирования, но и является полной платформой координации и управления процессами. Восстановление после сбоев и управление ресурсами, существующие для акторов и сервисов, доступны и другим процессам. Например, вы можете выполнять процессы Node.js или ASP.NET 5, управляемые Service Fabric, и получать все эти возможности без дополнительных усилий. Поэтому я просто использую стандартные приложения ASP.NET 5 Web API и создаю контроллер API, который предоставляет релевантную функциональность актора, как показано на рис. 7.

Рис. 7. Контроллер API

[Route("api/[controller]")]
public class QTrainerController : Controller
{
  [HttpGet()]
  [Route("[action]/{startTrans:int}")]
  public  async Task<IActionResult>  Start(int startTrans)
  {
    var actor = ActorProxy.Create<IQState>(ActorId.NewId(),
      "fabric:/QLearningServiceFab/");
    await actor.StartTrainingAsync(startTrans);
    return Ok(startTrans); 
  }
  [HttpGet()]
  [Route("[action]/{stateToken}")]
  public async Task<int> NextValue(int stateToken)
  {
    var actor = ActorProxy.Create<IQTrainedState>(new ActorId(stateToken),
      "fabric:/QLearningServiceFab");
    var qs = await actor.GetChildrenQTrainedStatesAsync();
    return qs.Count == 0 ? 0 : qs[new Random().Next(0, qs.Count)];
  }
}

А крестики-нолики?

Теперь остается лишь задействовать решение в каком-либо конкретном сценарии. Для этого я воспользуюсь простой игрой в крестики-нолики.

Цель — обучить набор QTrainedState, чтобы вы могли делать запрос на прогнозирование следующего хода в крестиках-ноликах. Один из способов заключается в том, что машина выступает в роли обоих игроков и обучается на основе результатов.

Вернитесь к реализации и обратите внимание на то, что QState является абстрактным классом. Идея в том, чтобы инкапсулировать базовые аспекты алгоритма и помещать логику конкретного сценария в производный класс. От сценария зависят три части алгоритма: как происходит переход между состояниями (политика), какие состояния являются поглощающими и имеют начальное подкрепление, а также каким состояниям алгоритм будет назначать подкрепление с коэффициентом дисконта. Для каждых из этих частей класс QState имеет метод, где можно реализовать соответствующую семантику для решения конкретной задачи. Эти методы называются GetTransitions, GetReward и GetRewardingQStates.

Итак, вопрос трансформируется в следующее: как смоделировать игру в крестики-нолики в виде последовательности состояний и переходов?

Рассмотрим игру, изображенную на рис. 8, где каждой ячейке присвоен номер. Вы можете считать каждый ход как переход из одного состояния в другое, где значением перехода является ячейка, которой стремится достичь игрок. Каждый маркер состояния — это комбинация предыдущих ходов (ячеек) и значения перехода. Например, на рис. 8 переход из 1 в 14, а затем в 142 и т. д. моделирует этапы игры, где игрок, делающий первый ход, выигрывает. И в этом случае всем состояниям, ведущим к 14273 (поглощающему состоянию), следует назначать подкрепление 1 и 142.

The Tic-Tac-Toe Scenario
Рис. 8. Сценарий с игрой в крестики-нолики

Вернемся к Q-обучению. Я должен предоставить все конечные (поглощающие) состояния, каждое с начальным подкреплением. В случае игры в крестики-нолики три типа состояний дадут награду: выигрыш, ничья или блок (ваш оппонент вот-вот выиграет, поэтому вы вынуждены использовать свой ход, чтобы заблокировать ему этот выигрышный путь). Выигрыш или ничья — поглощающие состояния, означающие конец игры, но блок таковым не является, и игра продолжается. На рис. 9 показана реализация метода GetReward для игры в крестики-нолики.

Рис. 9. Метод GetReward

internal override IReward GetReward(int? previousStateToken, int transitionValue)
{
  var game = new TicTacToeGame(previousStateToken,transitionValue);
  IReward rwd = null;
  if (game.IsBlock)
  {
    rwd = new TicTacToeReward() { Discount = .5, Value = 95, IsAbsorbent = false,
      StateToken = game.StateToken};
  }
  if (game.IsWin)
  {
    rwd = new TicTacToeReward() { Discount = .9, Value = 100, IsAbsorbent = true,
      StateToken = game.StateToken };
  }
  if (game.IsTie)
  {
    rwd = new TicTacToeReward() { Discount = .9, Value = 50, IsAbsorbent = true,
      StateToken = game.StateToken };
  }
  return rwd;
}

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

internal override IEnumerable<IPastState> GetRewardingQStates(int stateToken)
{
  var game = new TicTacToeGame(stateToken);
  if (game.IsTie)           
    return game.GetAllStateSequence();           
  return game.GetLastPlayersStateSequence();
}

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

internal override IEnumerable<int> GetTransitions(int stateToken)
{
  var game = new TicTacToeGame(stateToken);
  return game.GetPossiblePlays();
}

Игра против машины

К этому моменту я могу опубликовать решение и начать обучение, вызвав REST API и передав начальные переходы: 1 в 9.

По окончании обучения можно использовать этот API для создания приложения, которое просто передает маркер состояния и принимает предлагаемое значение. В пакете исходного кода для этой статьи содержится UWP-приложение, использующее данную серверную часть. Рис. 10 демонстрирует игру.

A Game of Tic-Tac-Toe
Рис. 10. Игра в крестики-нолики

Заключение

Применяя Q-обучение и Service Fabric, я сумел создать комплексную инфраструктуру, использующую распределенную платформу для вычислений и сохранения данных. Чтобы продемонстрировать этот подход, я использовал игру в крестики-нолики, чтобы создать серверную часть, которая обучается тому, как играть в эту игру, и делает это на приемлемом уровне.


Хесус Агилар  (Jesus Aguilar) — старший архитектор облака Microsoft в группе Technical Evangelism and Development, где он сотрудничает с крупными компаниями, «рожденными» в облаке, и помогает им добиваться конкурентных преимуществ. Увлекается инженерией программного обеспечения и проектированием решений, и вы сразу же завладеете его вниманием, если произнесете такие термины, как прогнозная аналитика, масштабируемость, параллельная обработка, проектировочные шаблоны и <Выберите любую букву>aaS. Следите за его заметками в Twitter (@giventocode) и публикациями в блоге giventocode.com.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Робу Бэгби (Rob Bagby), Майку Ланцетти (Mike Lanzetta) и Мэтью Шнайдеру (Matthew Snider).