Обучение с подкреплением с Tensorflow 2.0

Обучение с подкреплением - увлекательная область искусственного интеллекта, которая действительно находится на грани взлома настоящего интеллекта. С новым обновлением Tensorflow это стало более ясным, чем когда-либо. В этой серии я постараюсь поделиться самой минимальной и понятной реализацией алгоритмов глубокого обучения с подкреплением.

Прочтите этот пост, чтобы начать работу с Tensorflow 2.0.

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

Обучение с подкреплением - это часть машинного обучения. Хотя у него есть некоторые сходства как с обучением с учителем, так и с обучением без учителя, это третья отдельная область. Что отличает его, так это отсутствие необходимости во внешнем контроле и концепция цели.

Градиент политики

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

Нейронная сеть - одна из лучших практик для контролируемого обучения. Он может сопоставлять объекты с надписями, одновременно извлекая новые полезные функции. Но проблема с обучением с подкреплением в том, что мы не знаем, какой результат является хорошим. Значит, агент (игрок) должен во всем разбираться. Несмотря на то, что это большое дело, у нас есть кое-что, что может дать нам подсказки для определения того, какие действия являются хорошими. которая является функцией вознаграждения. Теперь наша цель - сделать действия с хорошей наградой более поощряемыми, а действия с отрицательной наградой - не поощрять. В большинстве ванильных подходов мы можем сделать это, настроив градиент в соответствии с получаемой наградой. Если вы хотите узнать больше о методах градиента политики, посмотрите это видео.

Проблема присвоения кредита

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

# gamma: discount rate 
def discount_rewards(r, gamma = 0.8):
    discounted_r = np.zeros_like(r)
    running_add = 0
    for t in reversed(range(0, r.size)):
        running_add = running_add * gamma + r[t]
        discounted_r[t] = running_add
    return discounted_r

Реализация с Tensorflow 2.0

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

model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(32, input_dim = 4, activation='relu'))
model.add(tf.keras.layers.Dense(2, activation = "softmax"))
model.build()
optimizer = tf.keras.optimizers.Adam(learning_rate = 0.01)
compute_loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

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

gradBuffer = model.trainable_variables
for ix,grad in enumerate(gradBuffer):
  gradBuffer[ix] = grad * 0

Теперь наш цикл обучения:

for e in range(n_episodes):
  # reset the enviroment
  s = env.reset()
  ep_memory = []
  ep_score = 0
  done = False 
  while not done: 
s = s.reshape([1,4])
    with tf.GradientTape() as tape:
      #forward pass
      logits = model(s)
      a_dist = logits.numpy()
      # Choose random action with p = action dist
      a = np.random.choice(a_dist[0],p=a_dist[0])
      a = np.argmax(a_dist == a)
      loss = compute_loss([a], logits)
    grads = tape.gradient(loss, model.trainable_variables)
    # make the choosen action 
    s, r, done, _ = env.step(a)
    ep_score +=r
    if done: r-=10 # small trick to make training faster
    
    ep_memory.append([grads,r])
  scores.append(ep_score)
  # Discound the rewards 
  ep_memory = np.array(ep_memory)
  ep_memory[:,1] = discount_rewards(ep_memory[:,1])
  
  for grads, r in ep_memory:
    for ix,grad in enumerate(grads):
      gradBuffer[ix] += grad * r
  
  if e % update_every == 0:
    optimizer.apply_gradients(zip(gradBuffer, model.trainable_variables))
    for ix,grad in enumerate(gradBuffer):
      gradBuffer[ix] = grad * 0
      
  if e % 100 == 0:
    print("Episode  {}  Score  {}".format(e, np.mean(scores[-100:])))

Давайте немного поясним код:

s = s.reshape([1,4])
with tf.GradientTape() as tape:
  logits = model(s)
  a_dist = logits.numpy()
  # Choose random action with p = action dist
  a = np.random.choice(a_dist[0],p=a_dist[0])
  a = np.argmax(a_dist == a)
  loss = compute_loss([a], logits)
grads = tape.gradient(loss, model.trainable_variables)

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

Следующая часть применяет градиент:

  # Discound the rewards 
  ep_memory = np.array(ep_memory)
  ep_memory[:,1] = discount_rewards(ep_memory[:,1])
  
  for grads, r in ep_memory:
    for ix,grad in enumerate(grads):
      gradBuffer[ix] += grad * r
  
  if e % update_every == 0:
    optimizer.apply_gradients(zip(gradBuffer, model.trainable_variables))
    for ix,grad in enumerate(gradBuffer):
      gradBuffer[ix] = grad * 0

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

Наконец, мы проверяем средний балл за 100 эпизодов:

Вот и все, ребята! Это действительно просто, и вы можете применить это к любой проблеме, какой захотите. Вы можете найти код по ссылке ниже. До скорого!