PythonJogaPong — Parte 3 Criando e treinando uma Neural Network

Esse post faz parte da série PythonJogaPong, no qual explico como criar um sistema que aprende com dados de treino a jogar o jogo pong sozinho. Veja também as outras partes: Parte 1 Parte 2 Parte 4

Nesta etapa criaremos uma rede neural utilizando tensorflow e treinaremos ela com os dados obtidos da gravação dos nossos movimentos (assunto tratado na parte 2 dessa série). Com a neural network treinada, podemos então dar entrada de novos dados para que ela nos diga qual ação tomar no jogo. O código desta parte está em:

https://github.com/pilotorobo/pongplay/blob/master/neural_net.py

Vamos ao código:

import tensorflow as tf
import numpy as np
from collections import Counter
from sklearn.model_selection import train_test_split

Para esta etapa precisamos de algumas bibliotecas:

  • tensorflow é a biblioteca que usaremos para criar e treinar as neural networks
  • numpy é usada para tratar vetores e matrizes
  • Counter é uma classe utilizada para contar objetos
  • train_test_split é uma função importada da biblioteca scikit-learn para misturar e separar nossos dados em dados de treino e dados de teste

A maioria das funcionalidades dessa parte está contida na classe NeuralNetwork e como ela acaba sendo um pouco extensa, vamos dividir os códigos da classe em três partes.

Aqui no construtor do objeto criamos nossa rede neural. Importante notar que temos 6 features:

  1. Posição horizontal da bola
  2. Posição vertical da bola
  3. Posição horizontal da barra 1
  4. Posição vertical da barra 1
  5. Posição horizontal da barra 1
  6. Posição vertical da barra 1

Porém, no código definimos que temos 8, isso é devido pelo fato que temos que adicionar mais duas features para que a rede aprenda corretamente: a velocidade horizontal e vertical da bola. Mais na frente mostramos como fazemos isso.

Quando captamos os dados, preenchemos o valor corresponde as teclas apertadas com 0 (nenhuma tecla), 1 (tecla cima) e 2 (tecla baixo). Como precisamos de variáveis categoricas, fazemos isso com a função tf.one_hot.

Poderíamos criar as conexões da rede no “braço”, multiplicando e adicionando matrizes, mas ao invés disso utilizamos a função tf.layer.dense que já faz tudo isso por nós, bastando passar os parâmetros de cada camada da rede. Utilizamos a camada de entrada com 8 unidades, 2 intermediárias com 20 unidades cada uma e a camada de saída com 3 unidades. Embora não tenha feito muitos experimentos, esse tamanho se mostrou suficiente para bons resultados.

Criamos um tensor para calcular o erro (cost) com cross-entropy para utilizarmos para otimizar a rede e outro de performance (accuracy) para medir o desempenho. O algoritmo de otimização Adam se mostrou melhor que o tradicional Gradient Descent então o utilizamos com um learning rate de 0.01 para treinar a rede.

Criamos a função fit para treinarmos nossa neural network. Utilizamos a função train_test_split para separamos os inputs e outputs coletados em inputs/ouputs de treino (para ensinarmos a rede) e inputs/outputs de teste (para medirmos a performance da rede). Iremos utilizar 25000 epochs, isso é, iremos treinar nossa rede com os mesmo dados de treino 25 mil vezes. Criamos a nossa sessão para executar as operações com tf.Session e iniciamos todas as variáveis declaradas (basicamente os weights e biases criados por tf.layers.dense) com a função tf.global_variables_initializer. Executamos as operações de otimização da rede (optimizer) e cálculo de erro (cost) com os dados de treino ao mesmo tempo que medimos a performance da rede com a operação accuracy com os dados de validação. A cada 500 epochs, exibimos a situação atual da rede (performance e erro). Por fim, verificamos a performance da rede com a mesma operação accuracy mas dessa vez com os dados de teste.

Aqui temos três funções:

  • predict: Utilizamos para inferência. Executa a operação logits na rede treinada (utilizando a mesma sessão) para retornar a ação que deve ser tomada para o conjunto de dados X passado.
  • save: Salva o estado atual da sessão da rede (utilizando a classe tf.train.Saver), salvando várias informações, inclusive o valor dos pesos (weights) que foram calculados quando fizemos o treinamento. Utilizamos isso para não ter que treinar a rede toda vez que o projeto for fechado e aberto.
  • load: Carrega uma rede neural previamente salva utilizando a função save.

Aqui utilizamos a função set_ball_speed para inserir no dataset a velocidade da bola em cada instante. Por que precisamos desta informação? Vejamos a imagem abaixo:

Para qual direção a bola circulada está indo? Para cima ou para baixo? Para direita ou para a esquerda?

Como saber se devemos subir ou não nossa barra para defender? Se a bola estiver vindo em nossa direção, devemos subir, caso contrário podemos ficar parados. Logo precisamos saber para onde a bola está se movimentando, isto é sua atual variação no espaço, informação conhecida como velocidade.

Como cada ponto dos dados de treino contém a posição atual da bola, basta para cada frame verificarmos a posição anterior da bola e subtrair da atual: speed_datapoints = dataset[1:,2] — dataset[0:-1,2] . Como para o primeiro frame não temos a posição anterior, começamos a contar a partir do segundo datapoint e teremos um datapoint a menos, o que não influencia em nada. Por fim, inserimos os dados calculados em novo dataset e retornamos ele.

Chegando ao final do arquivo, carregamos os dados de treino gravados no post anterior e verificamos a frequência de cada label do dataset, isso é, quantas vezes apertamos para cima, para baixo ou ficamos parados nos dados de treino gravados. Isso é importante pois ficamos muito mais tempo parados do que movimentando a barra, logo se ficarmos 70% do tempo parado e nossa rede tiver um desempenho de 70%, existem grandes chances dela ter decidido simplesmente ficar parada o tempo todo, então o ideal é que tenhamos um desempenho maior que 70%. Calculamos então a velocidade da bola com set_ball_speed, separamos os dados de treino em inputs e output e finalmente treinamos e salvamos nossa rede para uso posterior utilizando as funções comentadas anteriormente.


Siga o blog e fique ligado para os próximos posts sobre como jogar python com pong! Veja também as outras partes: Parte 1 Parte 2 Parte 4

Qualquer dúvida ou sugestão poste nos comentários!

Se gostou, bata algumas palmas 👏 para nós ! e ajude outros encontrar este artigo!