Usando Deep Learning para jogar Super Mario Bros.

Criando uma rede neural que aprende a jogar Super Mario.

Bernardo Coutinho
Turing Talks
11 min readJun 21, 2020

--

Nosso agente em ação

Todo o código do programa está neste repositório.

Se você se interessa por programação, com certeza já deve ter ouvido falar de Deep Learning, um dos assuntos mais badalados dos últimos tempos. Esse aprendizado é utilizado nas mais variadas aplicações, desde reconhecimento de imagens até processamento de linguagem. No Turing Talks de hoje, vamos aplicar Deep Learning de uma das maneiras mais interessantes: ensinando um programa a jogar Super Mario Bros.

O texto de hoje não precisa de tanto conhecimento prévio, estamos colocando todas as explicações mais complexas ao final do texto em um apêndice.

Introdução — Aprendizado por Reforço

Aprendizado por Reforço é o nome da área que estuda programas que aprendem a realizar tarefas complexas por tentativa e erro, de forma similar aos seres humanos. Esses programas são jogados em um mundo e vão experimentando diferentes ações, recebendo um feedback depois de cada uma. Assim, eles se tornam capazes de entender quais ações costumam ter um melhor resultado, aprendendo a agir com um comportamento ideal.

Exemplo de um ser humano aprendendo o que não fazer por tentativa e erro.

Esses programas que devem aprender a tomar decisões são chamados de agentes, e o mundo com o qual eles interagem é chamado de ambiente. Essa interação acontece por meio de ações, e cada uma destas recebe uma recompensa diferente, que informa quão boa foi aquela ação naquele determinado estado — o estado é a configuração do ambiente naquele instante.

Tomando o gif acima como exemplo, podemos dizer que o ser humano, o agente, tenta interagir com a mangueira, que faz parte do seu ambiente, por meio de uma ação: ligar ela em sua direção. Depois de tomar essa ação, o ser humano recebe uma recompensa bem negativa, seu estado vai de seco para molhado, e ele aprende a nunca mais tomar aquela ação de novo — assim espero.

Resumidamente, o nosso agente interage com o ambiente por meio de uma ação, e recebe de volta uma recompensa e um novo estado, no qual ele deve se basear para tomar a próxima ação.

Diagrama do Aprendizado por Reforço

No caso do jogo de Super Mario Bros., o nosso agente é o jogador, o programa que controla o Mario, enquanto o ambiente é todo o mundo do jogo com o qual o Mario interage: os blocos, os canos, os Goombas, etc, e o estado do ambiente, sua configuração atual, é o frame atual do jogo. É com base nesse frame que o agente deve decidir a melhor ação a se tomar.

As ações que o nosso agente pode tomar são os controles do Mario: pular, andar, ficar parado, etc. E, a cada frame do jogo, o agente recebe uma recompensa indicando quão bem ele agiu: se ele morrer, ele recebe uma recompensa negativa; se ele chegar mais perto do final, ele recebe uma recompensa positiva.

Ufa! Depois de toda essa teoria, finalmente entendemos como o nosso programa aprende a tomar as melhores decisões em um determinado ambiente.

Deep Q-Learning

Em 2013, a Deepmind, uma das maiores empresa de desenvolvimento de Inteligência Artificial do mundo, desenvolveu a chamada Deep Q-Network (DQN), uma rede neural capaz de aprender a jogar jogos de Atari melhor que seres humanos. Essa invenção revolucionou o Aprendizado por Reforço, tornando possíveis resolver problemas muito mais complexos do que conseguíamos anteriormente, até mesmo aqueles em que o nosso programa precisa aprender a interpretar os frames de um jogo.

DQN jogando Breakout (Atari)

No passado, utilizamos uma DQN para ensinar um módulo lunar a pousar na Lua. Desta vez, utilizando alguns avanços nessa técnica, vamos desenvolver uma rede neural que aprende sozinha a jogar a primeira fase de Super Mario Bros.

O Ambiente de Super Mario Bros.

Antes de pensar em como aplicar uma IA para jogar Super Mario Bros., precisamos de um ambiente, um emulador, com o qual nosso agente irá interagir. O gym-super-mario-bros é exatamente isso, um ambiente de Aprendizado por Reforço no qual podemos criar programas que jogam quaisquer fases dos dois primeiros Super Mario Bros.

O ambiente em ação

Nesse ambiente, nós recebemos uma imagem do frame atual do jogo (esse é o nosso estado), e devemos escolher uma ação para tomar com base nesse frame, como pular, andar para a esquerda, ficar parado, etc.

Com o gym-super-mario-bros, podemos escolher diferentes fases e visuais para o jogo, dependendo do nosso objetivo. Nesse caso, vamos utilizar o ambiente SuperMarioBros-1–1–v1, em que jogamos uma versão da primeira fase do Mario com um plano de fundo mais simplificado, para facilitar o reconhecimento de imagem.

SuperMarioBros-1–1–v1

Para entender melhor como funciona esse ambiente, vamos programar um agente simples que toma ações aleatórias para jogar essa primeira fase.

Agente Aleatório

O primeiro passo para criar qualquer agente é criar o nosso ambiente, nesse caso, o gym-super-mario-bros. Esse ambiente vem por padrão com 256 ações possíveis para controlar o Mario, sendo que a maioria delas não movimenta o jogador. Para restringir as nossas ações apenas às ações importantes, que controlam o nosso personagem, o gym-super-mario-bros recomenda usar um JoypadSpace e um SIMPLE_MOVEMENT, como fizemos abaixo.

Agora, vamos entender como funciona para rodar um episódio dessa fase, criando uma função run_episode.

Primeiro, usamos o método .reset() no nosso ambiente, que reseta ele para a posição inicial e retorna o primeiro frame do jogo, que guardaremos em state. Depois, vamos criar um loop que toma ações aleatórias no ambiente até que a fase termine — quando o Mario morre, o tempo acaba, ou ele chega no final.

Para escolher uma ação aleatória, o ambiente possui um atributo .action_space que contém todas as ações possíveis. Se utilizarmos um .sample() nesse action_space, ele retorna aleatoriamente uma dessas ações possíveis.

Por fim, usamos o método .step(action) do nosso ambiente, que toma a ação escolhida e retorna o novo frame do jogo, a recompensa recebida, e uma variável que diz se a fase terminou ou não.

Pronto! Agora temos um Mario que toma ações aleatórias, mas que não consegue aprender com seus acertos e erros, como você pode ver no gif a seguir.

Agente aleatório em ação

Agora que já sabemos como trabalhar com o nosso ambiente, podemos finalmente programar o agente que aprenderá a jogar a primeira fase.

O Agente

Deep Q-Network (DQN)

Como já explicamos o que é uma DQN mais a fundo nesse post passado, vamos explicar mais uma intuição de como ela funciona.

De maneira simples, uma rede neural nada mais é que uma função que recebe algo como entrada e vai aprendendo a calcular uma saída com base nos exemplos que ela recebe. Em um caso de reconhecimento de imagem, uma rede neural pode receber uma imagem como entrada e aprender a classificar o que está representado nela com base em exemplos.

Exemplo de uma rede neural que classifica imagens

Como o objetivo do nosso problema é receber uma imagem do estado do jogo e definir qual ação devemos tomar, a entrada da nossa rede vai ser uma imagem do frame atual do jogo, e as nossas saídas vão ser as Qualidades das ações que podem ser tomadas, sendo que a ação com maior Qualidade deve corresponder à melhor decisão.

A Qualidade (Q) de uma ação é definida como o retorno esperado daquela ação, que equivale à média da recompensa total que recebemos ao tomar aquela ação naquele estado.

Simplificadamente, é um número que representa quão bom é tomar uma determinada ação em um estado. Explicamos melhor esses conceitos no post de Introdução ao Aprendizado por Reforço.

Dessa forma, a arquitetura da rede neural que utilizamos está representada simplificadamente no diagrama abaixo:

Esquema simplificado da Deep Q-Network utilizada

Ela funciona da seguinte forma:

  • Passamos como entrada os quatro últimos frames do jogo. Esta é uma técnica chamada Frame Stacking, em que passamos mais que o último frame para a rede para que ela tenha uma noção de tempo.
  • Essas imagens passam pelas camadas de Convolução da rede neural, que tentam interpretar e reconhecer padrões nessas imagens. Se você quiser entender um pouco melhor como essas camadas fazem isso, temos uma explicação nesse post.
  • O resultado da interpretação das camadas de Convolução é passado para as camadas interconectadas da rede, que tentam calcular a Qualidade de cada uma das ações.
  • Com base nessas Qualidades que calculamos, decidimos qual é a melhor ação escolhendo aquela que possui a maior Qualidade.

No início, nossa DQN será péssima tanto em interpretar as imagens que ela recebe quanto em calcular a Qualidade de cada ação com base nessa interpretação. Entretanto, após ser treinada, ela entenderá melhor o que deve fazer, e será capaz de distinguir bem as melhores ações em cada estado.

A implementação dessa rede não é tão simples, mas se você quiser dar uma olhada, ela está presente no início deste arquivo. Também estamos colocando ela em um Apêndice ao final do texto com várias explicações mais avançadas!

Agora que explicamos o funcionamento da nossa rede neural, só precisamos mostrar como ela interage com o ambiente.

Como o Agente interage com o Ambiente

Lembra de quando criamos um agente que interagia com o ambiente com ações aleatórias? Agora vamos fazer a mesma coisa, só que com um agente que realmente vai aprender as melhores ações a se tomar.

Antes de tudo, vamos criar uma função state_reshape(state) que normaliza e modifica os nossos frames para facilitar as contas da rede. Depois, podemos criar a função que treina o nosso agente:

Temos algumas diferenças com relação à função do agente aleatório.

  • Agora, estamos escolhendo as ações com o método .act(state) do agente, em vez de escolher uma ação aleatória.
  • Também estamos usando o método .remember(state, actions, rewards, next_state, dones) para guardar na memória as informações de cada ação que tomamos. Precisamos guardar essas informações na nossa memória para posteriormente treinar nosso agente, ela faz com que seja possível usar várias experiências do passado no treino!
  • Dessa vez, estamos treinando a nossa rede a cada instante que passa com o método .train(), que melhorará nosso resultado a cada episódio.
  • Estamos usando um atributo epsilon do agente, resetando ele a cada 50.000 instantes. Esse atributo controla a exploração do agente, fazendo com que ele sempre tente experimentar novas ações.

Deixamos a explicação mais a fundo de cada um desses método no apêndice ao final do post

Finalmente! Agora é só rodar o código e o nosso agente aprenderá sozinho a jogar Super Mario Bros.

Depois de várias horas treinando, o nosso melhor resultado com a DQN foi este, que você viu no início do post:

Nosso melhor resultado com a DQN

Mas quando utilizamos algoritmos mais complexos de Aprendizado por Reforço, o nosso resultado ficou melhor ainda!

Ele já termina a primeira fase!

Conclusão

Esperamos que você tenha gostado do nosso projeto! O Aprendizado por Reforço é uma das áreas mais interessantes de Inteligência Artificial, e nosso objetivo é interessar mais pessoas nela apresentando os nossos projetos.

Caso se interesse por aprofundar mais no projeto, estamos deixando um Apêndice com maiores explicações sobre ele, e você sempre pode dar uma olhada no próprio código.

Se quiser conhecer um pouco mais sobre o Grupo Turing, não deixem de seguir as nossas redes sociais: Facebook, Instagram, LinkedIn e, claro, acompanhar nossos posts no Medium.

Até a próxima!

Apêndice

Durante o texto, simplificamos ou evitamos falar de algumas partes um pouco mais complicadas do texto, as quais iremos elaborar mais sobre a seguir:

A Rede Neural

A rede que utilizamos no projeto foi escrita em Torch, e possui três camadas de convolução e duas interconectadas.

Nesse caso, usamos um tipo específico de DQN chamado Dueling Deep Q-Network, em que dividimos a nossa rede em duas partes no final: uma que calcula o valor (V) do estado e outra que calcula a vantagem (advantage) de cada ação. Esse é um avanço que melhora a performance da rede dividindo a Qualidade que ela deve aprender em duas partes distintas.

Esquema de uma Dueling DQN

A Classe do Agente

Deixamos essa parte mais complicada do post pro final. Se você quiser entender melhor o agente, recomendamos ler nosso post de Q-Learning, em que ensinamos uma IA a jogar Pong, e o de DQN que mencionamos anteriormente.

Primeiro, vamos criar uma classe Agent que vai ter todos os atributos e métodos importantes do nosso agente:

Não se preocupe se você não entender muito do código do agente, ele é bem complexo

  • O atributo gamma é o nosso fator de desconto, um hiperparâmetro que define quanto vamos priorizar as recompensas futuras. Explicamos mais sobre ele no nosso post introdutório do assunto.
  • O atributo epsilon é um hiperparâmetro que incentiva a exploração de mais ações.
  • O atributo memory é a memória do nosso agente, um Prioritized Experience Replay, onde vamos guardar as informações necessárias das ações que tomamos para treinar a nossa rede neural, possibilitando treinar nossa rede com várias experiências passadas. O beta é um parâmetro utilizado pelo Replay.
  • O atributo dqn guarda a nossa rede neural, para que possamos treiná-la e usá-la para calcular a Qualidade das ações.
  • O atributo dqn_target é como um backup da nossa dqn, uma cópia atrasada, que utilizamos em algumas contas para aumentar a estabilidade do treinamento, impedindo que a nossa rede piore muito por causa de uma otimização ruim.

Escolhendo uma Ação

Como mencionamos antes, nosso agente deve escolher a ação de maior qualidade se ele quiser agir de acordo com o que ele acredita ser o melhor comportamento. Entretanto, como nosso agente começa sem saber nada, é possível que ele esteja bem confiante de que uma determinada ação ruim é a melhor.

Se, por exemplo, nosso agente acreditar que ficar parado é a melhor ação a se tomar, ele nunca sairá do lugar e nunca experimentará as outras ações do jogo. Para impedir isso, fazemos com que o nosso agente escolha uma ação aleatória com uma probabilidade epsilon, variando as ações que ele toma.

Conforme o nosso agente vai conhecendo melhor o ambiente, vamos decrescendo essa probabilidade epsilon exponencialmente, já que ele não precisará mais explorar tanto o ambiente. Contudo, ainda deixamos um epsilon mínimo para que o agente sempre busque experimentar novas ações.

Dessa forma, o nosso agente agiria por meio do seguinte método:

Memória

Depois de cada ação, vamos guardar suas informações na memória, para depois conseguirmos calcular a qualidade de todas as ações reutilizando experiências do passado. Nesse método, guardamos o estado atual, a ação tomada, a recompensa da nossa ação, o estado seguinte a nossa ação e se a fase terminou. O nome de cada conjunto dessas informações é chamado experiência.

Treinamento

Depois de guardar muitas experiências na nossa memória, podemos finalmente treinar a nossa rede neural. O método a seguir pega as experiências na nossa memória e otimiza nossa rede neural com base nelas.

O código dessa otimização é um tanto quanto complexo, já que além da otimização normal de uma DQN ainda são usadas as técnicas de Double Q-Learning e Prioritized Experience Replay. Para entender a otimização de uma DQN, recomendamos o nosso texto sobre o assunto.

Muito obrigado por ler o nosso texto até o final!

--

--

Bernardo Coutinho
Turing Talks

Computer Engineering undergraduate at Poli-USP. Interested in Machine Learning and Robotics. https://github.com/Berbardo