Ensinando uma Rede Neural a jogar Flappy Bird com Pytorch

Utilizando Aprendizado por Reforço com aprendizagem profunda e redes neurais, podemos criar uma IA que derrota o frustrante jogo Flappy Bird

Fernando Matsumoto
Turing Talks
10 min readJan 24, 2021

--

Texto escrito por Ariel Guerreiro, Nelson Yamashita e Fernando Matsumoto.

Introdução

Bem vindos entusiastas da ciência de dados e inteligência artificial a mais um Turing Talks! Dessa vez iremos continuar nossa jornada pelos conceitos da emergente área do aprendizado por reforço! Há algum tempo publicamos um texto sobre um exemplo mais simples desse tipo de aprendizado, o Problema dos K-Armed Bandits, porém, hoje iremos fazer um estudo mais aprofundado sobre o assunto. Baseando-nos no algoritmo de Q-Learning, que já explicamos neste post e neste workshop, iremos implementar e explicar um dos algoritmos mais importantes da área — responsável por resultados incríveis, como o de superar humanos em uma variedade de jogos — o Deep Q-Learning.

O código desse post está disponível no GitHub e pode ser executado no Google Colab.

O ambiente de Flappy Bird

Porém, antes de pularmos direto para o algoritmo, vamos entender um pouco mais sobre o ambiente com o qual nossa IA irá interagir (o jogo Flappy Bird) e como iremos instalá-lo para nossa simulação.

Sobre o jogo: Flappy Bird é um jogo de celular lançado em 2013 pelo desenvolvedor vietnamita Dong Nguyen. O jogo alcançou um rápido sucesso por ser incrivelmente simples e frustrante: sua única ação disponível é tocar na tela e fazer o passarinho subir um pouco enquanto ele está constantemente caindo e se movendo para direita. Com isso o jogador deve desviar de canos vindo em sua direção, a cada cano desviado o jogador aumenta sua pontuação.

Como o jogo é simulado no Python: Por conta de sua simplicidade e natureza Arcade o jogo demonstra um comportamento ideal para ser aprendido por uma IA. Por isso, criou-se para a biblioteca PyGame Learning Environment (PLE) uma versão do jogo que podemos utilizar como ambiente de aprendizagem para algoritmos de Aprendizado por Reforço.

Como iremos interagir com o ambiente: Uma das bibliotecas mais utilizadas em Aprendizado por Reforço é o gym desenvolvido pela OpenAI. Ela fornece uma série de ambientes de rápida e simples implementação, porém já que em quase toda implementação de algoritmos de Aprendizado por Reforço acaba-se usando seus ambientes, o resultado acaba ficando meio repetitivo e pouco emocionante, por isso optamos por um ambiente mais divertido e inusitado. Assim, utilizaremos uma adaptação do PLE, o gym_ple, que adapta ambientes PLE para o gym, mantendo sua praticidade com ambientes diferentes.

Sobre o ambiente: O ambiente está configurado da seguinte forma:

  • Ações válidas: as únicas ações são fazer nada ou “Up” que causa uma aceleração para cima do passarinho;
  • Estado Terminal: quando o passarinho bate no chão/teto ou em algum cano;
  • Recompensas coletadas: +1 para toda vez que o passarinho passa por um cano e -5 toda vez que chega em algum estado terminal;
  • Observações coletadas: posição y do pássaro, velocidade, distância do próximo cano, distância y do cano de cima, distância y do cano de baixo, e a mesma coisa para o próximo próximo cano.

Instalando as bibliotecas necessárias

Para isso recomendamos que seu python esteja numa versão igual ou inferior à 3.8.5. Também é necessário que você tenha git em seu computador.

Caso você esteja no Windows e tenha instalado seu python pela Anaconda basta você abrir o “terminal anaconda” e executar os seguintes comandos:

pip install pygame
pip install gym
pip install git+https://github.com/GrupoTuring/PyGame-Learning-Environment
pip install git+https://github.com/lusob/gym-ple

Em OSX e GNU/Linux é só executar esses comandos em um terminal comum. Caso você tenha encontrado algum erro ou problema na instalação, basta entrar em nosso Discord e mandar sua pergunta no canal de Aprendizado por Reforço, ficaremos muito felizes em te ajudar :).

Testando as bibliotecas e criando um agente aleatório

Para checar se tudo foi instalado corretamente iremos criar uma espécie de “hello world” do Aprendizado por Reforço: um agente que toma ações aleatórias.

Para isso abra um Ipython Notebook e crie as seguinte célula:

Provavelmente aparecerão alguns warnings mas se nenhum erro ocorrer, por enquanto está tudo certo e a biblioteca está pronta para uso!

Vamos então criar nosso agente aleatório:

Se tudo tiver dado certo, você verá o passarinho “se jogando” para o alto, morrendo inúmeras vezes. Isto mostra que o ambiente está sendo renderizado corretamente, e você estará pronto para o próximo passo: entender e implementar o algoritmo de Deep Q-Learning.

Obs: se você estiver rodando o código no colab, é necessário comentar a linha env.render. O resto do código continua funcionando, mas não é possível visualizar o jogo.

Explicação teórica

Q-Learning (tabular)

Para entender deep Q-Learning, vamos primeiramente fazer uma breve revisão de Q-Learning. Se você quiser uma explicação mais aprofundada, é só conferir as referências linkadas no começo desse post.

Primeiramente, precisamos de algum lugar para armazenar os q-valores. Isso será feito numa tabela, em que cada linha corresponde a um estado, cada coluna a uma ação e cada célula guarda o valor Q(s,a). É dessa tabela que vem o adjetivo “tabular” em Q-Learning tabular.

O algoritmo de Q-Learning consiste, principalmente, em dois passos: inicialmente, calcula-se o chamado “valor de bootstrap”, Qbootstrap(s,a), que é uma estimativa bem grosseira do q-valor do estado/ação atual. Uma vez que essa estimativa é feita, utilizamos o valor de bootstrap para atualizar a nossa estimativa de Q(s,a). A nossa atualização vai levar em conta tanto a nossa estimativa antiga quanto o valor de bootstrap, caso o valor não seja o mais representativo do real.

Para calcular o valor de bootstrap, lembramos da relação G(t) = R(t) + γ G(t+1). O valor de bootstrap é calculado de forma análoga:

onde s’, a’ são o próximo estado e ação. Como estamos interessados na política ótima, a’ é sempre a ação que produz o maior q-valor:

Por fim, precisamos incorporar Qbootstrap na nossa estimativa de Q(s,a). Em Q-Learning tabular, isso é feito com a fórmula abaixo (ela não será necessária no resto do post):

Deep Q-Networks (DQN)

O Q-Learning tradicional é uma forma de gerar um agente de qualidade, mas, na prática, só pode ser usado para problemas simples. Para problemas mais complexos, se torna inviável guardar todas as combinações de estados e ações em uma tabela. Por exemplo, um videogame, com imagens preto e branco de dimensões 250 x 250 pixels, onde os pixels possuem valores entre 0 e 255 e as ações possíveis são as 4 direções, precisaria de mais de 10¹⁵⁰⁵¹⁵ entradas em uma tabela.

Outro aspecto negativo do Q-Learning tabular é a falta de generalização para estados em que o agente não visitou ou teve pouco treinamento, pois o agente usa somente as informações que coletou diretamente sobre aquele estado para tomar suas decisões. Em problemas complexos, é difícil garantir uma boa exploração de todas as possibilidades de estado-ação do ambiente.

Buscando solucionar estes problemas, surge a DQN. No lugar da tabela, é utilizada uma rede neural que, dado um estado, calcula os q-valores das ações possíveis. É dessa mudança que surge o nome do algoritmo. Para treinar o agente, não atualizamos os q-valores em uma tabela, mas sim os pesos da rede neural, para gerar os q-valores que melhor representam o ambiente. Assim como na versão tabular, o agente, uma vez treinado, busca tomar as ações que possuem maior valor.

As mudanças mencionadas acima consistem na essência do algoritmo. No entanto, para obter uma performance aceitável, é necessário fazer mais uma alteração. O modo como as redes neurais aprendem significa que a ideia de “receber uma recompensa, calcular o Qbootstrap, treinar o agente, e repetir” não funciona muito bem.

Para que a rede neural aprenda bem, vamos utilizar um replay buffer, que é basicamente uma memória com todas as transições (estado, ação, recompensa, proximo estado, done).

A cada passo dado pelo agente, armazenamos uma transição nova no buffer e treinamos o agente com algumas transições aleatórias do buffer.

As vantagens que o uso do replay buffer oferecem são importantes para o aprendizado do agente. Uma delas é que o agente vai ver cada transição várias vezes, reduzindo a chance do agente se “esquecer” de alguma transição antiga. Outra é de que as transições usadas são de diferentes instantes de tempo, reduzindo a influência de uma transição sobre outra, melhorando a performance da rede neural.

O pseudocódigo da DQN pode ser observado na imagem a seguir:

Retirado do nosso repositório de aprendizado por reforço

Código

Inicialmente, vamos importar as bibliotecas necessárias para o nosso programa:

A primeira parte do código que vamos ver é o Replay Buffer, que usamos para guardar as transições que o agente observa e para pegar aleatoriamente algumas transições para treinar a rede neural:

Ao criar o buffer, delimitamos um tamanho máximo que ele pode ter e criamos listas para armazenar as transições (estado, ação, recompensa, proximo estado, done). O método update é utilizado para armazenar uma transição, onde as entradas de cada lista no valor do index são atualizadas para os valores fornecidos, e o index é atualizado. Por fim, o método sample pega uma quantidade de transições de forma aleatória, determinada pelo batch_size.

A próxima parte do código é a rede neural. Nesta implementação utilizamos a biblioteca pytorch. Possuímos alguns textos sobre a biblioteca, que podem ser conferidos neste link

A implementação da rede em si é simples: na inicialização, precisamos fornecer o tamanho da dimensão dos estados (input_dim) e o tamanho da dimensão de ações (output_dim). A rede possui uma camada de input, uma hidden layer e uma camada de saída. A ativação é feita pela função ReLU. O método forward é utilizado para calcular os q-valores de cada ação dado um estado, representando por x.

Chegamos então no nosso agente em si, que possui algumas variáveis necessárias para o funcionamento do algoritmo, como o valor de gamma, que desconta as recompensas futuras, a taxa de aprendizado, a memória, que é o Replay Buffer como mencionado acima, o número de épocas que a rede deve treinar, a rede em si e seu otimizador e os valores de epsilon.

O epsilon é usado para incentivar a exploração de novos estados ao forçar o agente a tomar ações aleatórias. Inicialmente, queremos que o agente tome várias ações aleatórias, para explorar o ambiente. Com o passar do treinamento, o agente já possui um conhecimento do ambiente, então não precisa tomar tantas ações aleatórias. Dessa forma, decaímos o valor de epsilon até chegar em um valor mínimo.

O método act é responsável por tomar uma ação. É sorteado um número aleatório entre 0 e 1 e, caso seja inferior a epsilon, será tomada uma ação aleatória, fornecida por action_space.sample(). Caso o valor seja superior, o agente irá calcular os q-valores do estado por meio da rede neural e retornar a ação de maior valor.

O método eps_decay cumpre o papel de decaimento, ou seja, diminuição do valor de epsilon, como comentado acima. O método remember é usado para guardar as transições observadas na memória do agente.

O método train, por sua vez, é o responsável pelo aprendizado do agente. Nele, são selecionadas aleatoriamente transições guardadas na memória e, comparando a resposta fornecida pela rede neural e a armazenada na memória, é feito o “backpropagation” da rede neural, de forma semelhante à fórmula do Q-Learning tabular.

A função train, que não faz parte do agente, é utilizada para a comunicação do agente com o ambiente. O ambiente é reiniciado e entra-se no looping de treinamento. Para um número de timesteps (transições) definido, o agente irá tomar decisões (agir) e o ambiente irá retornar as consequências das ações: a recompensa, o próximo estado e a indicação de se o episódio terminou. Estas transições são armazenadas na memória do agente e em seguida o agente treina, pegando uma amostra aleatória da memória. Se o episódio termina, é calculado o retorno do episódio e o ciclo se reinicia.

Finalmente, para executarmos o treinamento, precisamos definir os valores que queremos para os parâmetros, criar o agente e treiná-lo.

Resultados

Aqui plotamos a média móvel dos retornos do nosso agente, podemos ver que ele conseguiu alguns resultados bem impressionantes como pontuações acima de 120:

Conclusão

Porém, mesmo com essa alta pontuação, podemos notar uma inconsistência e certa instabilidade das recompensas. Isso se deve há uma deficiência do algoritmo quanto a estimação dos Q-valores, já que estamos criando uma estimativa com base em um estimativa… Tal problema pode ser resolvido, com uma técnica relativamente simples, criando o algoritmo de Double DQN, mas isso fica para outro post!

Esperamos que tenha gostado! Se quiser explorar mais assuntos do Aprendizado por Reforço você pode checar nosso repositório no Github! Você também pode entrar em nosso Discord onde estamos constantemente realizando aulas abertas e publicando outros assuntos interessantes :). Você também pode nos acompanhar em nossas redes sociais: Facebook, Linkedin, Instagram, Medium!

--

--