Novembro de 2018

Volume 33 - Número 11

Inteligência artificial - Uma análise detalhada sobre o Aprendizado de Reforço

De Frank La La

Frank La VigneNa coluna do mês passado, explorei alguns conceitos básicos sobre o aprendizado de reforço (Reinforcement Learning - RL), tentando uma abordagem estritamente aleatória para navegar em um ambiente simples e, em seguida, implementar uma Q-Table para lembrar as últimas ações e quais ações levaram a quais recompensas. Na demonstração, um agente trabalhando aleatoriamente foi capaz de alcançar o estado da meta em aproximadamente 1% do tempo e cerca da metade do tempo ao usar uma Q-Table para lembrar das ações anteriores. No entanto, esse experimento apenas arranhou a superfície do campo promissor e em expansão da RL.

Lembre-se de que na coluna anterior (msdn.com/magazine/mt830356), um espaço de problema de RL consiste em um ambiente, um agente, ações, estados e recompensas. Um agente examina o estado de um ambiente e executa uma ação. A ação, então, altera o estado do agente e/ou do ambiente. O agente recebe uma recompensa e examina o estado atualizado de seu ambiente. Em seguida, o ciclo é reiniciado e executado por várias iterações até que o agente tenha êxito ou falhe em uma meta predefinida. Quando um agente tiver êxito ou falhar, a simulação é encerrada. Com uma Q-Table, um agente lembra quais ações renderam recompensas positivas e recorre a elas ao tomar decisões em simulações subsequentes.

Problema Multi-Armed Bandit

Um dos problemas clássicos em RL é a tensão entre exploração e aproveitamento. As máquinas caça-níqueis, muitas vezes chamadas de "one-armed bandits", são a inspiração para esse problema. Logo, um banco de máquinas caça-níqueis cria um "multi-armed bandit". Cada uma dessas máquinas caça-níqueis tem uma probabilidade de pagar um superprêmio ou não. A probabilidade de cada rodada resultando em um superprêmio pode ser representada como P e a probabilidade de não pagar é 1 - P.  Se uma máquina tem uma probabilidade de superprêmio (JP) de 0,5, cada puxada da alavanca tem a mesma chance de ganhar ou perder. Por outro lado, uma máquina com uma JP de 0,1 renderia um resultado de perda 90% do tempo.

Agora, imagine um banco de cinco máquinas caça-níqueis e o jogador (ou agente) tem o objetivo de maximizar as vitórias e minimizar as perdas. Sem o conhecimento prévio de qualquer probabilidade de superprêmio (JP) das máquinas, o agente deve assumir alguns riscos no início. Com a primeira puxada da alavanca, o agente ganha e recebe um pagamento. No entanto, as tentativas subsequentes revelam que esta máquina paga cerca de metade do tempo, uma JP de 0,54. Em se tratando de máquinas caça-níqueis, isso é bastante generoso. O agente deve decidir se ele deve aproveitar o atual recurso conhecido ou explorar uma nova máquina. Se a probabilidade de pagamento da primeira máquina caça-níqueis é generosa, valeria a pena tentar qualquer uma das máquinas no banco para ver se as chances de pagamento são melhores?

A melhor maneira de explorar ainda mais o espaço de problema é com um código Python em um Jupyter Notebook. Crie um bloco de anotações do Python 3 em sua plataforma preferida. Eu abordei sobre os Jupyter Notebooks em um artigo anterior (msdn.com/magazine/mt829269). Crie uma célula vazia e digite o seguinte código e execute a célula.

import numpy as np
import matplotlib.pyplot as plt
number_of_slot_machines = 5
np.random.seed(100)
JPs =  np.random.uniform(0,1, number_of_slot_machines)
print(JPs)
plt.bar(np.arange(len(JPs)),JPs)
plt.show()

A saída deve ler e mostrar um gráfico dos valores, conforme mostrado na Figura 1.

[0.54340494 0.27836939 0.42451759 0.84477613 0.00471886]

Probabilidades de superprêmio das cinco máquinas caça-níqueis
Figura 1 - Probabilidades de superprêmio das cinco máquinas caça-níqueis

O código cria uma matriz de valores JP para uma série de cinco máquinas caça-níqueis, variando de 0,004 a 0,844. No entanto, a primeira máquina em que o agente tentou, embora generosa, não é a melhor. Claramente, a quarta máquina caça-níqueis com uma taxa de pagamento de 84,4% é a melhor máquina pagadora do ambiente. Também vale a pena observar que a máquina caça-níqueis final tem as piores chances de pagamento de um superprêmio. Lembre-se de que o agente não tem nenhum conhecimento prévio das taxas de pagamento e ele deve descobrir por conta própria. Se o agente tivesse ficado na primeira máquina, optando por aproveitar ao invés de explorar, ele nunca teria localizado a melhor máquina caça-níqueis pagadora.

Para representar o que o agente sabe no início de uma simulação, adicione o seguinte código a uma nova célula:

known_JPs = np.zeros(number_of_slot_machines)

Isso cria uma matriz de zeros, o que significa que o agente pressupõe que a JP de cada máquina caça-níqueis seja zero. Embora isso possa não ser o melhor valor inicial de todos os casos, será suficiente para nossos objetivos aqui. Para criar uma simulação de uma máquina caça-níqueis, adicione o seguinte código a uma nova célula e execute-o:

def play_machine(slot_machine):
  x = np.random.uniform(0, 1)
  if (x <= JPs[slot_machine]):
    return(10)
  else:
    return(-1)

Este snippet de código simula uma máquina caça-níqueis pagando uma recompensa de 10, se a máquina pagar, e uma recompensa negativa de -1, se a máquina não pagar. As chances de um pagamento baseiam-se na probabilidade definida na matriz numpy das JPs. Para testar o código, insira o seguinte código Python em uma nova célula e execute-o:

# Test Slot Machine 4
for machines in range(10):
  print(play_machine(3))
print ("------")      
# Test Slot Machine 5
for machines in range(10):
  print(play_machine(4))

Esse código coloca a máquina de melhor desempenho contra a máquina de pior desempenho. Como tudo se baseia em chance, não há nenhuma garantia dos resultados de saída. Os resultados devem refletir isso, com uma maioria de valores 10 para a máquina 4 e quase todos os valores -1 para a máquina 5. Com o código simulado de máquina caça-níqueis se comportando conforme o esperado, agora é hora de examinar um algoritmo comum em RL: Épsilon-Greedy.

O algoritmo épsilon-Greedy

O principal dilema que o agente enfrenta aqui é se priorizar a ganância, o desejo de aproveitar-se de um recurso conhecido, ou a curiosidade, o desejo de explorar outras máquinas caça-níqueis na esperança de uma melhor chance de recompensas. Um dos algoritmos mais simples para resolver esse dilema é conhecido como o algoritmo épsilon-Greedy, onde o agente escolhe aleatoriamente entre o uso da máquina caça-níqueis com as melhores chances de pagamento, observado até agora, ou a tentativa de outra máquina na esperança de que ela possa fornecer um pagamento melhor. Com um valor baixo de épsilon, esse algoritmo segue o algoritmo greedy, mas, ocasionalmente, tentará outra máquina caça-níqueis. Por exemplo, se o valor de épsilon for 0,1, o algoritmo optará por aproveitar 90% do tempo e explorar apenas 10% do tempo. Normalmente, os valores padrões de épsilon costumam ficar entre 0,05 e 0,1. Em suma, o agente jogará principalmente a melhor máquina caça-níqueis que ele conhece e, às vezes, experimentará uma nova máquina. Lembre-se de que cada puxada da alavanca tem um custo e o agente não sabe o que sabemos: a máquina 4 paga o melhor.

Isso ressalta a noção sobre RL. Inicialmente, o agente não sabe nada sobre o ambiente, por isso ele precisa primeiro explorar e, posteriormente, aproveitar. O aprendizado continua ao longo de todo o processo. Essencialmente, esta é a noção de gratificação atrasada, e é do interesse do agente não ser totalmente ganancioso, por isso deixa algum espaço para a exploração.

Testando a hipótese de épsilon-Greedy

Para testar essa hipótese, adicione o código na Figura 2 a uma nova célula e execute-o. Esse código cria a função multi_armed_bandit, que simula uma série de execuções em relação a uma coleção de máquinas caça-níqueis. A função armazena as chances observadas de um pagamento de superprêmio. Em cada iteração, o agente jogará aleatoriamente a máquina caça-níqueis com o melhor pagamento observado até o momento, ou arbitrariamente tentará outra máquina. A função argmax retorna o valor mais alto na matriz numpy. Aqui, isso significa a máquina caça-níqueis com as melhores chances de acertar um superprêmio. Os parâmetros da função permitem o controle sobre o número de máquinas caça-níqueis, a quantidade de iterações a serem executadas e o valor de épsilon. 

Figura 2 - Código de Aprendizado de Reforço

def multi_armed_bandit(arms, iterations, epsilon):
  total_reward, optimal_action = [], []
  estimated_payout_odds = np.zeros(arms)
  count = np.zeros(arms)
  for i in range(0, iterations):
    epsilon_random = np.random.uniform(0, 1)
    if epsilon_random > epsilon :
      # exploit
      action = np.argmax(estimated_payout_odds)
    else :
      # explore
      action = np.random.choice(np.arange(arms))
    reward = play_machine(action)
    estimated_payout_odds[action] = estimated_payout_odds[action] +
      (1/(count[action]+1)) *
      (reward - estimated_payout_odds[action])
    total_reward.append(reward)
    optimal_action.append(action == np.argmax(estimated_payout_odds))
    count[action] += 1
  return(estimated_payout_odds, total_reward)

Com o código RL no lugar, agora é hora de testar o algoritmo épsilon-Greedy. Insira o código da Figura 3 em uma célula vazia e execute-o. Os resultados mostram o gráfico da Figura 1 para facilitar a referência, seguido pelas chances que o código RL observou.

Figura 3 - Código para Comparar as Chances Reais das Máquinas Caça-Níqueis com as Observações do Agente

print ("Actual Odds")
plt.bar(np.arange(len(JPs)),JPs)
plt.show()
print (JPs)
print("----------------------------------")
iterations = 1000
print("\n----------------------------------")
print ("Learned Odds with epsilon of .1")
print("----------------------------------")
learned_payout_odds, reward = multi_armed_bandit(number_of_slot_machines, iterations, .1)
plt.bar(np.arange(len(learned_payout_odds)),learned_payout_odds)
plt.show()
print (learned_payout_odds)
print ("Reward: ", sum(reward))

Como você pode ver na Figura 4 , o algoritmo fez um excelente trabalho, não apenas de determinar a máquina caça-níqueis com as chances mais favoráveis, mas também de produzir probabilidades de pagamento bastante precisas para as outras quatro máquinas caça-níqueis. Os gráficos se alinham bastante bem. A exceção é a quinta máquina caça-níqueis, que tem chances tão baixas de um pagamento que marcou negativamente nas observações do agente.

Resultados com um Valor épsilon de 0,1
Figura 4 - Resultados com um valor épsilon de 0,1

Agora, com a linha de base estabelecida, é hora de experimentar mais. O que aconteceria se épsilon fosse definido como zero, o que significa que o algoritmo nunca iria explorar? Insira o código a seguir em uma nova célula e execute-o para realizar esse experimento:

print("\n----------------------------------")
print ("Learned Odds with epsilon of 0")
print("----------------------------------")
learned_payout_odds, reward =
  multi_armed_bandit(number_of_slot_machines, iterations, 0)
plt.bar(np.arange(len(learned_payout_odds)),learned_payout_odds)
plt.show()
print (learned_payout_odds)
print ("Reward: ", sum(reward))

O gráfico resultante mostra um valor maior que zero. Uma máquina domina as outras, deixando claro que o agente encontrou uma máquina e ficou com ela. No entanto, execute o código várias vezes e você pode perceber que, ocasionalmente, um padrão interessante se desenvolve. Haverá uma ou mais máquinas com valores negativos, com uma máquina com um valor maior que zero. Nesses casos, o agente perdeu em uma determinada máquina e, em seguida, ganhou em outra máquina. Depois que o agente descobre uma máquina vencedora, ele ficará com essa máquina, pois ela será a máquina em que a função argmax escolherá. Se épsilon estiver definido como zero, o agente ainda poderá explorar, mas não será intencional. Dessa forma, as chances das máquinas caça-níqueis observadas estão longe das chances reais. Também é importante observar que o método “greedy” produz uma pontuação de recompensa menor do que quando o épsilon foi definido como 0,1. A ganância, pelo menos a ganância absoluta, parece ser contraproducente.

E se o épsilon fosse definido como 1, fazendo o agente explorar todas as vezes e não aproveitar nada? Insira o código a seguir em uma nova célula e execute-o:

print("\n----------------------------------")
print ("Learned Odds with epsilon of 1")
print("----------------------------------")
learned_payout_odds, reward  = multi_armed_bandit(number_of_slot_machines, iterations, 1)
plt.bar(np.arange(len(learned_payout_odds)),learned_payout_odds)
plt.show()
print (learned_payout_odds)
print ("Reward: ", sum(reward))

Os resultados mostrarão que o agente fez um excelente trabalho de observar as chances semelhantes às das chances reais, e o gráfico se alinha muito de perto com a Figura 1. Na verdade, os resultados da definição de épsilon como 1 parecem muito semelhantes quando o valor foi 0,1. Observe o valor de Recompensa e, no entanto, há uma diferença gritante. O valor de recompensa quando épsilon foi definido como 0,1 será quase sempre maior do que quando definido como 1. Quando o agente está configurado para explorar apenas, ele tentará uma máquina aleatoriamente em cada iteração. Embora ele possa estar aprendendo com a observação, ele não está atuando nessas observações.

Conclusão

RL continua sendo um dos espaços mais interessantes em inteligência artificial. Neste artigo, explorei o algoritmo épsilon-Greedy com o clássico problema "Multi-Armed Bandit", especificamente aprofundando no dilema explorar ou aproveitar que os agentes enfrentam. Recomendo que você explore ainda mais as compensações, experimentando com diferentes valores de épsilon e uma quantidade maior de máquinas caça-níqueis.


Frank La Vigne trabalha na Microsoft como um profissional de Soluções de Tecnologia de Inteligência Artificial, em que ele ajuda as empresas a alcançar mais obtendo o máximo proveito de seus dados com análise e IA. Ele também co-hospeda o podcast DataDriven. Ele escreve regularmente em seu blog FranksWorld.com e você pode assisti-lo em seu canal no YouTube “Frank’s World TV” (FranksWorld.TV).

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Andy Leonard


Discuta esse artigo no fórum do MSDN Magazine