Coletânea Python do ZERO às Redes Neurais Artificiais
Agente Autônomo que joga BreakOut (Atari)
Uma das aplicações que vem crescendo exponencialmente, quando tratamos sobre redes neurais artificiais, são as áreas que englobam visão computacional e inteligência artificial. Tenha em mente que nunca teremos um contexto totalmente isolado de aplicação, mas o contrário disso, combinando diversos recursos dos mais diversos modelos de redes neurais artificiais.
Ao meu ver, é um grande desperdício de tempo tentar classificar os
modelos separando-os conforme suas funcionalidades específicas, uma vez que sempre haverão estruturas compartilhadas em mais de um modelo.
Sendo assim, raciocine que treinar um Agente para que o mesmo, por exemplo, consiga jogar um jogo de videogame, envolve o uso de redes neurais artificiais convolucionais (que lê dados a partir de imagens), deep learning (aprendizado de máquina profundo) e redes neurais artificiais intuitivas (haja visto que o propósito geral aqui é não somente aprender a jogar o jogo, mas dominá-lo e usar do mesmo aprendizado para outros jogos do mesmo tipo).
Então, apenas separando o joio do trigo, lembre-se que a área da visão
computacional trabalha sobre estruturas de sensores que abstraem nosso sentido da visão, realizando um mapeamento de um ambiente 2D ou 3D, gerando dados a serem transformados para matrizes em um processo de convolução até chegar no cruzamento de dados em busca de padrões, prática comum a praticamente todos modelos de redes neurais artificiais. O pulo do gato aqui se dará pela capacidade de nosso Agente de realizar a leitura de frames em tempo real e reagir aos mesmos, inicialmente testando todas as possíveis ações, posteriormente baseado em sua memória de replay e aprendizado de máquina (retroalimentada e processada em ciclos) conseguir progredir reagindo da maneira correta as mais diversas situações.
Apenas como exemplo implementaremos uma simples rede neural
artificial intuitiva que por sua vez aprenderá a jogar o famoso BreakOut do Atari, porém esse modelo pode ser adaptado para qualquer fim 2D.
Talvez você esteja se perguntando sobre a complexidade da criação de uma IA para um carro autônomo, por exemplo. Novamente, tenha em mente que por baixo dos panos tudo se trata em realizar a leitura do ambiente, em tempo real, frame a frame, transliterando tais dados para arrays e realizando seu devido processamento.
De forma resumida, e novamente com propósito de organizar nossa sequência lógica dos fatos, basicamente o que faremos é:
- Instalação das bibliotecas e módulos que serão utilizados em nosso código.
- Importação das bibliotecas e módulos específicos.- Construção da arquitetura de rede neural artificial convolucional- Construção da arquitetura de rede neural artificial intuitiva.- Criação da base de memória e pré-processamento dos dados.- Carregamento da memória de curto prazo.- Criação dos estados iniciais de nosso agente.- Treinamento da IA para seu devido propósito.- Definição final do modelo.
- Execução de nossa rede neural artificial intuitiva.- Interpretação dos dados obtidos em tempo real.
BreakOut
BreakOut é um jogo da Atari, desenvolvido em 1976,
inicialmente para fliperamas tendo um port para o próprio console da Atari, o 2600. O objetivo do jogo é destruir todos os blocos/tijolos rebatendo uma bola sobre os mesmos ao mesmo evitando que a bola toque a margem inferior da tela. O jogador por sua vez controla uma palheta para rebater a bola que segue a trajetória respeitando a física da angulação sobre a mesma.
Instalação das Dependências
Para esse projeto em particular, optei por criar um ambiente
isolado apenas com o núcleo Python e as bibliotecas que usaremos para este exemplo.
Sendo assim, via prompt Anaconda por meio do Código acima é criado o
ambiente virtual isolado AIBreakout.
Da mesma forma como fizemos com o exemplo anterior, é necessário ativar este ambiente para que possamos instalar no mesmo as bibliotecas, módulos e ferramentas que faremos uso.
Para nosso exemplo faremos o uso das bibliotecas Numpy, que oferecerá uma série de ferramentas para que possamos trabalhar com dados em forma matricial; TensorFlow, que permitirá a criação de forma simples das arquiteturas de nossas redes neurais artificiais; gym que por sua vez nos permitirá implementar nossa AI diretamente nos processos do jogo BreakOut; imageio que nos será útil para o carregamento dos dados a partir de imagens/frames e por fim o modulo skimage da biblioteca SciKit-Learn, que nos permitirá a manipulação das imagens obtidas.
As instalações são feitas como de praxe, via pip.
pip install numpy
pip install tensorflow
pip install gym==0.7.4
pip install imageio
pip install scikit-image
Partindo para a prática!!!
Como sempre, todo processo se inicia com as devidas importações das bibliotecas, módulos e ferramentas que usaremos ao longo de nosso código. Única especificação diferente do que fizemos anteriormente é que agora, do módulo transform da biblioteca skimage importaremos resize, que por sua vez permitirá, como o próprio nome sugere, o escalonamento da imagem obtida para um tamanho menor.
Lembre-se que uma prática comum em redes neurais convolucionais é sempre que possível, converter a escala de cores da imagem para preto e branco, assim como reduzir e padronizar os tamanhos das imagens que passarão pelo processo de convolução, ganhando assim, performance nesta camada de processamento.
Em seguida é necessário realizar um processo um pouco diferente do convencional, repare que criamos um objeto de nome ENV_NAME, e pela forma como o mesmo foi declarado podemos deduzir que trata-se de uma palavra reservada do sistema certo? Não necessariamente do sistema, mas essa é uma palavra reservada da biblioteca gym, por meio desse objeto realizaremos o carregamento do jogo BreakOut, nesse caso, em sua versão 3.
Internamente a biblioteca gym este é o modelo de exemplo que vem na mesma para que possamos usar de seus frames como dados de entrada para nossa rede neural artificial. No site da mesma é possível encontrar outros exemplos para diferentes propósitos.
Uma vez realizadas corretamente as devidas importações e o carregamento da estrutura do jogo BreakOut da biblioteca gym, podemos dar início a criação de nossa arquitetura de código a ser usada por nosso Agente.
Para isso inicialmente criamos uma classe de nome FrameProcessor( ) que por sua vez receberá um atributo de classe object. Essa estrutura será responsável pela conversão da matriz RGB de cada frame para uma escala de cinza, assim como o escalonamento das imagens para um tamanho menor e padrão. Esse processo é uma prática comum de ser realizada em diversos modelos de redes neurais artificiais, pois com pequenos ajustes como este obtemos um ganho de performance que justifica os mesmos.
Tenha em mente que uma matriz de cores convertida para escala de cinza torna o processo mais eficiente, escalonar as imagens para um tamanho menor padronizado também, assim como os dados de nossas matrizes em uma janela de 0 a 1 também. Sempre que possível serão realizados processos dessa natureza, puramente a fim de ganho de performance.
Internamente a estrutura dessa classe criamos nosso método construtor __init__ parametrizado com frame_height = 84 e frame_width = 84, o que em outras palavras nos diz que assim que carregado um frame o mesmo já será escalonado para uma matriz 84x84.
Em seguida são criados alguns objetos, como frame_height e
frame_width que respectivamente recebem o valor definido anteriormente 84; frame, que via função placeholder( ) cria e aloca um objeto de tamanhos [210,160 3], esta estrutura funciona internamente reservando um invólucro a ser preenchido por dados posteriormente, guardando-os como matriz nas dimensões especificadas.
É de suma importância esse tipo de conversão pois internamente, quando houverem os respectivos cruzamentos dos dados em forma matricial ou vetorial os mesmos devem estar padronizados em um só formato.
Também é criado o objeto processed, que recebe como atributos os
dados da conversão de cor para escala de cinza via função rgb_to_gray( ) onde será repassado um frame, assim como pela função crop_to_bounding_box( ) esses mesmos dados obtidos até então da matriz da imagem sofrerão um corte, mantendo apenas a região de maior interesse.
Para quem está familiarizado com visão computacional, uma bounding box é uma marcação que cria uma caixa destacando, por exemplo, o rosto de uma pessoa em uma imagem, essa caixa por sua vez tem dados em forma das posições x e y onde esse rosto se encontra, em nosso exemplo, os parâmetros 34, 0, 160, 160 dizem respeito a área de maior interesse, enquanto tudo o que está fora dessa marcação é desconsiderado, tudo a fim de obter melhor performance.
Por fim, via função resize_images( ) parametrizada com os dados obtidos até então, é aplicado o método NEAREST_NEIGHBOR, que por sua vez, agrupará os dados com base em um parâmetro em comum, neste caso, empilhará frame a frame respeitando sua bounding box.
Finalizando o bloco de código dessa classe, é criado um método chamado __call__ que recebe uma session e um frame. Se você já é familiarizado com TensorFlow deve se lembrar que cada pacote de um processamento é rodado dentro de uma sessão.
Aqui, esta camada de processamento deverá ser realizada a cada frame lido. Repare que a nível de código simplesmente é criada e executada uma sessão via session.run( ) onde serão processados os dados de processes que alimentarão um dicionário com dados de cada frame.
Apenas relembrando que o propósito aqui, pós leitura e cruzamento
desses dados, é que os dados de entrada (cada frame obtido, em tamanho [210, 160, 3], colorido) seja convertido para um novo frame de tamanho [84, 84, 1] em escala de cinza.
Em seguida damos início a criação de nossa estrutura de rede neural convolucional, que aqui terá algumas particularidades haja visto que os dados processados pela mesma retroalimentarão uma rede neural intuitiva.
Apenas relembrando, conceitualmente uma rede neural artificial convolucional é o tipo de rede neural artificial onde os dados de entrada são obtidos a partir de imagens, que passam pelo processo de convolução (onde o mapeamento de cada pixel que compõe a imagem é decodificado e transliterado para uma matriz) e finalmente terão seus dados convertidos como os neurônios de entrada de nossa rede neural.
A nomenclatura DQN costuma ser utilizada por convenção para toda e
qualquer rede neural artificial intuitiva profunda, um belo nome não, encontrado na literatura como Deep Q Network.
Retomando a codificação, é criada a classe DQN( ) que receberá um object. Dentro de sua estrutura criamos um método construtor __init__ que por sua vez recebe uma série de parâmetros, o primeiro deles n_actions definirá um tamanho específico de possíveis ações a serem tomadas (apenas abstraindo nosso exemplo, a palheta que o Agente controlará pode “não fazer nada”, assim como mover-se para direita ou para esquerda); hidden = 1024 define manualmente que a primeira camada oculta dessa rede neural terá o número fixo inicial de 1024 neurônios; learning_rate = 0,00001 define manualmente também a taxa de aprendizado, ou de quantas em quantas amostras os pesos serão atualizados; frame_height e frame_width que como visto anteriormente, definem a altura e largura do frame, respectivamente; Finalizando com agent_history_length = 4, que define que serão levadas em consideração os últimos 4 valores de estado e ação de nosso Agente.
Na sequência é criado um objeto de nome inputscaled que recebe como atributo o valor de input dividido por 255, esta é uma simples forma de escalonar os dados entre 0 e 1, ganhando assim um pouco em performance.
Logo após é criado o bloco com a estrutura convolucional em si, repare
que serão 4 camadas de convolução com diferentes parâmetros, essa prática se dá porque assim “forçamos” nossa rede a encontrar os padrões certos.
A primeira camada de convolução, instanciada em objeto como conv1,
por meio da função conv2d( ) recebe os parâmetros inputs com os valores atribuídos de inputscaled; filters = 32, mecanismo interno que aplicará 32 filtros de polimento; kernel_size, aqui de tamanho 8x8, realizará a leitura dos blocos separada e isoladamente em busca de padrões, strides = 4 que pegará essas informações dos blocos obtidos por kernel_size e armazenará em vetores isolados; kernel_initializer que chama a função variance_scaling_initializer( ) parametrizada com scale = 2, o que em outras palavras significa que amostras intercaladas escolhidas de strides serão agrupadas de acordo com sua semelhança e da semelhança com um parâmetro arbitrário, dando mais pesos aos erros na identificação dos blocos de mapeamento; padding = “valid” estipula que os dados de saída desta camada de processamento sejam validados somente se estiverem todos no mesmo padrão; activation = tf.nn.relu que é o nosso velho conhecido ReLU, função de ativação que retorna valores de classificação binária; use_bias = False, prática comum em redes neurais convolucionais, o descarte da camada de processamento de bias, apenas a fim de melhor performance; encerrando com name = ‘conv1’ apenas para melhor organização e sumarização de nossa rede.
O processo é repetido para outras 3 camadas de convolução apenas
alterando os tamanhos dos vetores a serem processados, isso força a rede a detectar e reforçar os padrões corretos encontrados pela mesma nesse processo de mapeamento de cada bloco de dados que compõe cada pixel da composição da imagem.
Ainda em nossa classe DQN( ) damos sequência criando uma cadeia de objetos que por sua vez irão gerar aquele mecanismo de dependência abordado no exemplo anterior, onde os dados serão cruzados de forma a que sempre haverá um novo estado ou uma tomada de ação por nosso Agente, e isso se dá pela interligação de uma série de objetos e funções a serem executadas a partir do gatilho inicial. Para isso, criamos um objeto de nome valuestream, que recebe como atributo a função split( ) que separa cada dado lido a partir das saídas apresentadas, transformando cada valor desse em um neurônio empilhado do que seria a camada de saída dessa rede (porém lembrando, aqui a saída deve se comportar diferente, ao invés de encerrar o processo, a mesma retroalimentará a rede continuamente, gerando ciclos de processamento).
Da mesma forma é criado um objeto de nome advantagestream que por sua vez recebe esses dados da camada flatten obtidos anteriormente. Em seguida é criado o objeto advantage que por sua vez será mais uma camada de rede neural artificial, dessa vez, densa, com todos neurônios interconectados.
Repare que estamos realizando a interconexão entre duas redes neurais, de forma a ler dados a partir da imagem, convertê-los de forma que possam alimentar uma rede neural densa para que seja realizada as etapas de aprendizado de máquina.
Logo, advantage por sua vez recebe como atribuição a camada de entrada da rede neural densa via dense( ), por sua vez parametrizado com inputs que conterá os dados de advantagestream como número de neurônios para essa camada de entrada; units aqui recebendo n_actions, em outras palavras, o número de possíveis ações a serem tomadas por nosso Agente; kernel_initializer e name definidos da mesma forma que na estrutura da rede neural convolucional.
Por fim, encerrando essa cadeia de código, é criado um objeto de nome value que atuará como a conhecida camada de saída de uma rede neural convencional, repare que sua única diferença em relação a camada anterior é que a mesma retornará apenas um valor. Lembre-se que todo esse processamento segue a lógica de ler uma imagem e a partir dela tomar a decisão de realizar uma ação.
Seguindo com o código, hora de criarmos aquela estrutura compreendida anteriormente pela equação de Bellman, onde criaremos uma série de objetos que representarão para nosso agente parâmetros como seus estados, assim como suas tomadas de decisão com base em sua memória de curta duração.
Sendo assim é criado o objeto q_values, por sua vez atribuído com o
resultado da função subtract( ) parametrizada com os valores de advantage subtraídos da média dos próprios valores via reduce_mean( ).
Em seguida é criado o objeto best_action, que chama a função argmax( )
parametrizada de forma a obter o último valor, ou o valor mais alto obtido a partir de q_values.
Da mesma forma é criado o objeto target_q, inicialmente criando um
objeto vazio e sem forma, a receber um dado no formato float. O mesmo processo é realizado para o objeto action, alterando apenas que o mesmo espera um dado em formato int. Na sequência para action é feito o mesmo.
Logo após chegamos no objeto Q, codificado aqui de forma a receber
como atributo o resultado da multiplicação das somas entre q_values, action e n_actions. Entenda que essa fórmula por si só está realizando uma versão reduzida da equação de Bellman, a fim de definir o estado atual de nosso Agente com base no estado passado (Q).
Encerrando essa parte é definida nossa função de custo loss que por sua
vez nada mais está fazendo do que recebendo a média dos valores da diferença entre os valores de Q encontrados se comparado com os valores previstos para o mesmo. Obviamente quanto maior a proximidade desses dados significa melhor performance de nosso algoritmo, em função de que temos um objetivo a alcançar, um alvo a atingir e a avaliação da performance se dá pela proximidade desse alvo, quanto mais próximo, mais eficiente, mais distante, menos eficiente é nosso algoritmo.
Ainda no encerramento é instanciado o objeto optimizer, agora definido
para treinar nosso agente a partir do otimizador Adam para que os valores de leaning_rate, ou seja, da taxa de aprendizado seja equivalente ao valor definido manualmente anteriormente.
Por fim é criado um objeto muito importante, senão o mais importante
deles nesse contexto que é o update, com base na função minimize( ) parametrizada com os valores de loss, é definido uma espécie de estrutura condicional onde enquanto o aprendizado de máquina não atinge um nível de excelência, o ciclo todo se repete, novamente, trabalhando com aquela ideia de estrutura de dependência, estrutura lógica que “motivará” nosso Agente a continuar vivo, realizando suas funções de forma contínua.
Importante destacar aqui que ao visualizar esses conceitos é comum imaginar que quando finalmente atingido um determinado objetivo por nosso Agente o mesmo perde seu propósito e poderia ficar estagnado ou até mesmo ser desligado. Porém, raciocine que essa continuidade simulada, por parte de processamento de nosso agente, não necessariamente tem uma estimativa de acabar, porque na estrutura que estamos elaborando, sempre haverão novas situações a serem exploradas, o ambiente inicial assim como os primeiros padrões aprendidos não apenas o equivalente a fase de treino de uma rede neural artificial, a ideia é que uma vez dominado este primeiro ambiente ou atingido um determinado objetivo, nosso Agente seja utilizado em outros contextos.
Partindo para nossa próxima classe, denominada ExplorationExploitationScheduler( ) que recebe um object para si, dentro de sua estrutura é criado um método construtor __init__ que recebe DQN, n_actions, eps_initial, eps_final, eps_final_frame, eps_evaluation, eps_annealing_frames, replay_memory_start_size e max_frames. Vamos entender o que cada objeto desses realiza dentro dessa classe:
Inicialmente precisamos ter de forma clara o que essa classe em
particular realiza, e nesse caso, essa classe ficará responsável por determinar uma ação a ser tomada por nosso Agente com base nos dados obtidos até então. O conceito de exploration e exploitation para alguns autores determina a maneira como nosso Agente se portará em seu ambiente, que tipo de abordagem o mesmo terá para realizar o reconhecimento e o mapeamento do mesmo, realizando a cada passo uma série de avaliações de suas tomadas de decisão assim como planejando quais serão seus próximos passos de acordo com a tendência/probabilidade de maior chance de estar correta.
Sendo assim vamos aos objetos. Toda vez que essa classe for
instanciada, a mesma será alimentada com uma série de dados definidos até então, o primeiro deles n_actions, que como visto anteriormente, define um intervalo de quantas são as possíveis ações a serem tomadas por nosso Agente, ou em outras palavras, o número de possíveis ações; eps_initial por sua vez receberá um valor de probabilidade de exploração/tomada de decisão para o primeiro frame processado na memória de replay; eps_final receberá um dado probabilístico a ser usado para definir qual a próxima ação; eps_final_frames receberá um valor obtido após o processamento de todos os frames envolvidos nesta etapa; eps_evaluation receberá valores oriundos de testes de performance, para comparação com os demais valores de epsilon, avaliando assim, a performance do aprendizado por reforço; eps_annealing_frames terá o número de frames agrupados e levados em consideração para a nova tomada de decisão; replay_memory_start_size define manualmente o número de frames que o Agente usará como referência para sua primeira leitura em busca de reconhecimento de padrões; Por fim, max_frames determina o número máximo de frames a serem usados para alimentar nosso Agente assim como serem armazenados em memória.
Repare em alguns pontos importantes, esses parâmetros, assim como
tantos outros, são definidos aqui manualmente para um contexto padrão, para avaliação de nosso Agente é inclusive recomendados testes com outros valores para tais parâmetros.
Apenas realizando uma rápida leitura destes parâmetros, note que os
valores de epsilon variam entre 0 e 1, porém, assim como em uma descida de gradiente de outros modelos, aqui a ideia é que estes valores de aproximem de 0.1, indicando que existe um número muito pequeno de probabilidade de ações a serem tomadas, porém essas ações serão as corretas. Raciocine que em um estado inicial nosso Agente não terá discernimento para saber se suas ações estão certas ou erradas, a medida que o mesmo vai via tentativa e erro recebendo recompensas ou penalizações, o mesmo tende a começar a guardar os padrões tidos como corretos, quanto mais treinado, maior será a taxa de acertos do mesmo, até o ponto onde ele não precisa mais tomar muitas decisões erradas, mas se basear na melhor probabilidade dentre as corretas.
Da mesma forma repare o número de frames a serem processados, frame
a frame por ciclo, em processo de aprendizagem por reforço, onde para cada frame serão realizadas diversas camadas de processamento para obtenção dos seus respectivos estados e ações.
Retomando as últimas linhas desse bloco de código, é criado o objeto
slope, que para alguns autores é a nomenclatura usual para uma tendência de ação. Logo, slope recebe como atributo o resultado da divisão entre a subtração de eps_initial e eps_final pelo valor de eps_annealing_frames.
Também é criado o objeto intercept que recebe atribuído para si o
resultado da multiplicação da subtração entre eps_initial e slope pelo valor em replay_memory_start_size.
Do mesmo modo é criado slope_2 que recebe o resultado da subtração
entre eps_final e eps_final_frame dividido pela subtração entre os valores de max_frames, eps_annealing_frames e replay_memory_start_size.
Da mesma maneira é criado intercept_2 que recebe como atributos os
valores obtidos pela subtração entre eps_final_frame e slope_2, multiplicado pelo valor de max_frames.
Entendendo este cruzamento de dados em particular, note que aqui
estamos definindo um valor de probabilidade mínima e máxima para tomada de decisões de nosso agente, e este intervalo é construído com base no número de frames processados, uma vez que não teremos uma janela fixa, mas de tamanho variável, estreitando essa janela diretamente proporcional aos valores dos estados e tomadas de decisão de nosso agente.
Novamente, como mencionado nos parágrafos anteriores, o número de possíveis ações a serem tomadas por nosso agente no início de sua “vida” é grande, porém, conforme o mesmo aprende o que é errado e vai descartando tais ações de sua memória, proporcionalmente o mesmo terá uma janela menor de número de ações a serem tomadas.
Finalizando esse bloco, é criado uma espécie de laço de herança da
classe atual com DQN( ) criado anteriormente e aqui instanciado para o objeto de classe DQN.
Encerrando essa classe, é criada a função get_action( ) parametrizada com session, frame_number e state, além de já definir evaluation como False. Dentro dessa estrutura é criado uma condicional onde para evaluation é criada a variável eps que recebe como atributo eps_evaluation. Isso pode parecer um tanto quanto confuso, mas raciocine que ao gatilho inicial de nosso Agente, o mesmo não terá dados passados, de feedback, para saber avaliar sua própria performance, logo, este parâmetro deve ainda ser construído, em função disso criamos essa estrutura que inicia dessa forma.
Uma tomada de ação normalmente se dará a partir da leitura de estados passados, porém a primeira tomada de decisão será totalmente aleatória por parte de nosso agente, haja visto que o mesmo apenas reconhece um ambiente a ser explorado.
Dessa forma, caso não haja este dado inicial (e não existe mesmo), é
levado em consideração que se o valor de frame_number for menor que o valor de replay_memory_start_size, eps recebe os dados de eps_initial. Este é o gatilho inicial de processamento de nosso Agente em para sua primeira ação e que se repetirá inúmeras vezes.
Caso essa condição não seja válida, é realizada uma terceira verificação
onde na verdade estamos realizando uma validação onde se os valores de frame_number forem iguais ou maiores que os valores de replay_memory_start_size e frame_number for menor que os valores da soma entre replay_memory_start_size e eps_annealing_frames, eps recebe atribuído a si o resultado da multiplicação das somas entre frame_number e intercept por slope. O mesmo é feito em uma segunda validação onde se o valor de frame_number for maior ou igual ao valor da soma entre replay_memory_start_size com eps_annealing_frames, eps recebe o resultado da soma entre frame_number por intercept_2 multiplicado por slope_2.
Note que simplesmente estamos forçando a criação de um estado inicial
para que nosso agente tome sua primeira ação, uma vez que não existe uma codificação somente para a primeira ação e outra para as demais, logo, simplesmente realizamos esse cruzamento destes dados para criar um ambiente inicial a ser reconhecido por nosso agente, de forma que o mesmo consiga ler alguns valores, o suficiente para sua primeira tomada de decisão.
Finalizando esse bloco é criada uma última estrutura condicional onde se
um número inicial aleatório, gerado via função random.rand( ) for menor que o valor de eps, é retornado um número para n_actions. Em outras palavras, até então criamos a estrutura do gatilho que acionará pela primeira vez nosso Agente, essa função em particular agora irá gerar a semente que alimentará esse gatilho, logo nos primeiros ciclos de processamento essa estrutura será validada e o gatilho será acionado, “ligando” nosso Agente.
Como mencionado anteriormente, por uma questão de processamento,
toda camada de execução de uma rede neural criada via TensorFlow deve rodar em uma sessão, isso aqui é feito repassando para session.run( ) os primeiros valores para best_action e para input:[state] em sua posição 0 do índice, em outras palavras, é iniciada uma sessão com uma ação e um estado inicial.
Como esse conteúdo está se tornando mais extenso que o do modelo anterior, vamos dar uma pausa e revisar alguns pontos abordados até então em nosso atual exemplo.
Até o momento criamos a estrutura responsável por realizar a leitura e
conversão frame a frame de nosso jogo, assim como a rede neural convolucional responsável por converter tais dados de imagem para matrizes numéricas. Da mesma forma criamos uma estrutura de rede neural artificial densa, as primeiras estruturas dedicadas a criar a “consciência” de nosso agente baseada em sensores de leitura e interpretação do ambiente, assim como seus estados e capacidade de tomadas de decisão. Tudo até então, apesar de já ocupar um número considerável de linhas de código, é apenas a base de nosso Agente.
Dando prosseguimento, criaremos agora as estruturas lógicas que desencadearão a continuidade do processamento de nosso agente, simulando a temporalidade constante da inteligência artificial do mesmo.
Seguindo com nosso exemplo atual, é criada a classe ReplayMemory( )
que por sua vez recebe object. Dentro de si, como de costume, é criado um método construtor __init__ que recebe size, frame_height, frame_width, agente_history_length e batch_size. Alguns destes já vistos anteriormente, porém agora cruzando dados de forma a simular a temporalidade.
Este conceito pode parecer bastante abstrato, e de fato, é complexo traduzirmos em palavras a passagem do tempo, mas tenha em mente que em uma rede neural convencional todo processo tem um início, meio e fim, sendo que ao fim do mesmo, não existe mais o conceito de tempo de processamento.
Já para uma rede neural artificial intuitiva esse conceito é entendido de outra forma, uma vez “ligado” nosso agente o mesmo estará sempre em processamento. Assim como nossos cérebro está sempre operante (mesmo enquanto dormimos) aqui já que estamos abstraindo o funcionamento do mesmo, estamos criando estruturas lógicas que, em seu processo de simulação de uma inteligência, estarão realizando diversas camadas de processamento, seja em primeiro ou segundo plano, onde não há intervalos de tempo definidos, mas processamento constante em decorrência do tempo.
Dessa maneira, são instanciados os objetos size, frame_height,
frame_width, agente_history_length e batch_size com suas variáveis homônimas, porém temos agora alguns objetos novos, o primeiro deles count, inicialmente com valor 0 atribuído e current também atribuído da mesma forma, com 0.
Em seguida é criada a estrutura que pre-alocará blocos de memória para
armazenamento de estados. Isso é feito para actions, rewards, frames e terminal_flags por meio da função np.empty( ), apenas diferenciando que para cada objeto destes o valor atribuído estará em um formato específico. Para actions em formato int32, para rewards, float32, para frames uint8 e para terminal_flags em formato booleano.
Criada a estrutura básica de pre-alocação de memória, podemos também
realizar algumas alocações para os novos estados em uma mini batch a ser sempre atualizada pelos ciclos de processamento. Então, states aqui recebe como atribuição espaços vazios alocados para batch_size, agente_history_length, frame_height e frame_width. Exatamente a mesma estrutura de código é criada para new_states. Por fim é atribuído o valor de batch_size para índices.
Na sequência é criada a função add_experience( ) parametrizada com action, frame, reward e terminal. Dentro de seu bloco é criada uma estrutura condicional onde se o tamanho do frame for diferente daqueles valores estabelecidos anteriormente para frame_height e frame_width (84x84), é levantada uma exceção, apontando para o usuário que houve algum erro por parte do escalonamento da imagem.
Também são realizados alguns cruzamentos de dados utilizando agora de
nosso objeto current anteriormente criado mas até então sem uso. Repare que count (também inutilizado até então) receberá atribuído para si o valor máximo encontrado si próprio e em current somado de 1. O objeto current por sua vez ao final receberá atribuído para si o valor do módulo de current + 1 e size.
Quando estamos desenvolvendo algum programa que dentre suas funções, muitas delas dependem de interação com o usuário, é normal criar estruturas de validação, tentando contornar todos os erros previstos a serem cometidos pelo usuário. Em um quadro onde o usuário é humano, tentamos prever as poucas possíveis prováveis ações que o mesmo pode realizar e criamos validadores para isso. Em nosso exemplo atual, nosso Agente por sua vez testará todas as possíveis possibilidades e ao mínimo erro de cruzamento dos dados toda a rede irá suspender sua atividade.
Pensando nisso, precisamos aqui também criar estruturas validadoras, a primeira delas por meio da função _get_state( ). Note que essa função por sua vez está fora do escopo global de acordo com a sintaxe utilizada no momento de sua declaração.
Dentro da mesma criamos uma estrutura condicional onde se o valor de count for 0, será levantado um erro de memória de replay vazia. Do mesmo modo criamos uma segunda estrutura condicional onde se o valor de index for menor que o valor de agente_history_length subtraído de 1, é levantado um erro dizendo que o valor mínimo para que se obtenha um índice é 3.
Por fim, é retornado para frames uma estrutura de autopreenchimento que nada mais fará do que validar os requisitos mínimos de count e index.
Da mesma forma criaremos uma estrutura para validação apenas do índice, caso possa haver alguma inconsistência por parte dele. Para isso criamos a função _get_valid_indices( ) sem parâmetros mesmo. Dentro dela inicialmente é criado um laço de repetição que fará a leitura dos valores em batch_size. A partir desse ponto é criada uma estrutura condicional que estipula que enquanto a condição for verdadeira, o objeto index recebe como atributo um número a ser gerado aleatoriamente via random.randint( ) respeitando a métrica de agente_history_length e do valor de count subtraído 1.
Em seguida é criado dentro da estrutura condicional atual uma nova
estrutura condicional onde se o valor de index for menor que o valor de agente_history_length é para ir para próxima condição (algo muito parecido com as estruturas de switch/case de outras linguagens de programação).
A próxima estrutura condicional por sua vez verifica se o valor de index
é igual ou maior ao valor de current e se o valor de agente_history_length é igual ou menor ao valor de current. Caso válida essa condição é dito ao interpretador que pule para a próxima condicional.
A última estrutura condicional verifica se terminal_flags em seu
conteúdo possui dados para index (de qualquer tipo). Assim que atingido esse estado a estrutura validadora cessa e é retornado para índices o valor atribuído a index.
Na sequência também é criada uma função dedicada a ser o gatilho para que nossa memória de curta duração de nosso Agente seja alimentada. Isso é feito simplesmente criando uma função de nome get_minibatch( ) onde dentro da mesma é criada uma estrutura condicional que verifica se o valor de count é menor do que o valor de agente_history_length. Raciocine que essa estrutura está diretamente ligada aos validadores criados anteriormente, de forma que caso haja alguma inconsistência é levantado um erro de alocação de espaço na memória.
Indentado no bloco de código da função get_minibatch( ) instanciamos _get_valid_indices( ) realizando alguns cruzamentos de dados. Repare que neste momento o que faremos é criar uma estrutura que retroalimentará esses dados em forma de ciclo, uma vez que queremos continuidade de processamento de dados por parte de nosso Agente.
Sendo assim, criamos um laço de repetição onde a cada interação com
índices, states em sua posição i recebe os dados de _get_state( ) parametrizado com o valor de idx subtraído de 1, em outras palavras estamos retroalimentando o último valor de states com o último valor de índice. Do mesmo modo new_states em sua posição i recebe os dados de _get_state( ) parametrizado com idx. De forma rápida podemos entender que essa estrutura por si realiza a atualização necessária para que se obtenha um novo estado que substituirá o estado atual.
Em seguida é gerado um retorno bastante incomum, haja visto que
comumente retornamos um dado ou valor simples, aqui nesse caso estamos retornando toda uma gama de parâmetros para retroalimentará as estruturas de validação. Isso se dá pelo retorno da função transpose( ) parametrizada com states, action e, rewards, recursivamente aplicando transpose( ) também para new_states e para os valores de índices de terminal_flags.
Terminados os procedimentos validadores e de retroalimentação, podemos finalmente criar o mecanismo de aprendizado de nosso Agente. Isso se dará criando a função learn( ) por sua vez parametrizada com dados de session, replay_memory, main_dqn, target_dqn, batch_size e gamma.
Dentro de sua estrutura os objetos states, actions, rewards, new_states e
terminal_flags recebem como atributo replay_memory repassando seus dados para get_minibatch( ). Também é criado o objeto arg_q_max que em uma sessão própria roda main_dqn.best_action alimentado com os dados de new_states. Da mesma forma q_vals roda em sessão q_values alimentado com os dados de new_states.
Na sequência é criado o objeto double_q que por sua vez recebe os
dados de q_vals, porém apenas os melhores valores encontrados, já que esses nessa situação são oriundos de arg_q_max.
O objeto target_q agora finalmente instancia rewards somado do valor
de gamma multiplicado pelo valor de double_q, multiplicado pelo valor de terminal_flags subtraído de 1. Por mais confuso que isso possa parecer de momento, preste bastante atenção, estamos simplesmente transformando alguns dados obtido em todas as ações anteriores e convertendo-os agora em forma de uma recompensa a ser aplicada sobre nosso Agente.
Encerrando essa parte, loss e uma variável por hora vazia _ rodam uma
sessão onde é realizado o cruzamento dos dados de input em relação a states, target_q em relação a ele mesmo e action em relação a actions. Entendendo esta etapa, aqui são criados os laços de processamento cíclico de nossa rede neural, já que a mesma estará sempre lendo estados passados e atuais, realizando tomadas de ação e retroalimentando a rede atualizando sequencialmente esses dados.
Por fim, apenas para obtermos um retorno, é retornado um dado/valor
para loss, podemos usar desta informação posteriormente para avaliação da performance do modelo.
Dando prosseguimento em nosso código, vamos criar uma estrutura de validação para a atualização da rede neural em si. Para este propósito inicialmente criamos a classe TargetNetworkUpdater( ) que recebe um object. Como de costume é criado um método construtor __init__ que dessa vez receberá main_dqn_vars e target_dqn_vars, nas linhas abaixo são criados os respectivos objetos.
Também é criada para essa classe a função _update_target_vars( ). Dentro de seu bloco é criado o objeto update_ops que por hora possui como atributo apenas uma lista vazia. Na sequência é criado um laço de repetição onde para cada repetição do mesmo é realizada a contagem de main_dqn_vars. Dentro desse laço é criado o objeto copy_op que recebe os valores de target_dqn_vars como valor. Finalizando, acrescentamos os valores de copy_op em update_ops, retornando o valor de update_ops.
Agora é chamada uma função padrão do Python chamada __call__( ) parametrizada com sess. Dentro de seu código é determinado que os valores de update_ops devem ser equiparados aos de _update_target_vars( ). Como você já deve ter percebido, boa parte desses códigos criam estruturas de dependência, onde sempre há um dado a se buscar, a se atualizar, a continuar buscando... para que possamos simular a continuidade de nosso Agente.
Encerrando esse bloco é criado um laço de repetição que simplesmente
percorre os valores de update_ops e os insere em copy_op, repassando os mesmos para uma sessão a ser executada. O que fará uma espécie de sincronização entre as redes neurais quando carregadas e processadas.
Logo após o bloco de código anterior criamos (novamente fora da última classe) a função generate_gif( ) que recebe um frame_number, frames_for_gif, reward e path. Esta por sua vez será a função que com base nas imagens processadas até o momento irá gerar como retorno um gif mostrando diferentes etapas do andamento de nosso Agente, em outras palavras, será gerado um gif animado com amostras de sequências de jogadas sendo realizadas por nosso Agente para que possamos acompanhar o mesmo “jogando”.
Então, é criado um laço de repetição em frame_idx a ser preenchido com
os dados contados de frames_for_gif. O que esse laço por sua vez realiza é a equiparação dos dados idx de frames_for_gif com frame_idx, em cada dimensão de sua matriz, mantendo seu formato original por meio do parâmetro preserve_range = True e codificando esses dados em formato uint8.
Finalizando, é criada a estrutura que salvará esse gif animado, isso é
feito por meio da função mimsave( ) da biblioteca imageio. Aqui simplesmente é repassado o diretório onde esse gif será salvo, assim como o seu nome contendo o número do frame e sua respectiva recompensa, além é claro dos parâmetros de onde virão esses dados, nesse caso de frames_for_gif, numa amostragem de 1 frame a cada 30.
Chegou a hora de finalmente criamos nossa classe Atari( ) que recebe um object e que a partir desse ponto começaremos a de fato instanciar objetos do jogo, carregando certos dados dos jogos amostra da biblioteca gym.
Inicialmente é, como sempre, criado um método construtor __init__ que
recebe envName, no_op_steps e agente_history_length para que se faça o cruzamento desses dados com os demais.
Criando seus respectivos objetos é criado env que recebe como atributo
gym.make por sua vez parametrizado com envName. Note que aqui, estamos importando o ambiente do jogo BreakOut, em sua versão 3 como havíamos especificado lá no início de nosso código logo após as importações das bibliotecas. Por convenção, essa variável fica responsável por carregar e instanciar qual jogo da biblioteca gym estamos usando para exemplo.
Em seguida é criado process_frame que agora é chama a função
FrameProcessor( ); state inicialmente é parametrizada em None, ou seja, sem dados iniciais aqui predefinidos, uma vez que serão importados de outra classe;
last_lives aqui é um objeto padrão da biblioteca gym, basicamente sempre virá parametrizado com 0 pois a ideia é que independente do jogo, há um número de vidas de nosso personagem, quando essas chegam a 0 o jogador perde; no_op_steps e agente_history_length instanciam suas respectivas variáveis.
Dando sequência é criada uma função padrão para a biblioteca gym que é a função step( ), que por sua vez recebe sess e action, e age diretamente sobre a passagem de tempo simulada no jogo. Tenha em mente que sempre é feita uma determinada programação interna ao jogo, seja por sua engine ou por algum parâmetro específico onde é determinado que no espaço de tempo de 1 segundo serão processados um número x de frames de forma uniforme e sequencial.
Aqui inicialmente são criadas as variáveis new_frame, reward, terminal
e info, todas elas recebendo os dados de env.step(action), uma vez que como dito anteriormente, há uma programação implícita sobre a passagem do tempo no jogo, normalmente referenciada como step.
Na sequência é criado uma estrutura condicional onde se o dado/valor de
ale.lives de info for menor que last_lives, terminal_life_lost é setada como True. Em outras palavras, cada vez que o Agente erra em sua jogada e, nesse jogo, deixa a bolinha tocar a base da tela, uma vida é descontada, quando ele perde sua última vida, terminal_life_lost é acionada sinalizando isso.
Caso a condição acima não seja alcançada, terminal_life_lost tem seu
valor equiparado com o de terminal, haja visto que o número de vidas iniciais de nosso personagem no jogo pode ser parametrizada manualmente.
Também é atribuído para last_lives o valor atual de ale.lives de info, da
mesma forma processed_new_frame agora é atualizada com os valores oriundos de process_frame, por sua vez instanciando dados de sess e new_frame.
Em seguida para new_state, objeto que de acordo com sua nomenclatura, está criando dados para o novo estado de nosso Agente, recebe os dados de toda matriz de state para processed_new_frame em seu eixo 2.
Finalmente, após cruzados e processados os dados dessa estrutura, é
retornado os novos dados/valores para processed_new_frame, reward, terminal, terminal_life_lost e new_frame.
Na sequência é criada a função clip_reward( ) que recebe uma recompensa via reward, porém, raciocine que essa função será o gatilho a ser acionado onde de acordo com o contexto, nosso Agente será recompensado ou penalizado.
Para isso, simplesmente criamos uma estrutura condicional onde se o
valor de reward for maior que 0 é retornado o valor 1, se o valor de reward for igual a 0 é retornado o 0 e caso nenhuma das condições seja alcançada é retornado o valor -1. Esses três estados se dão porque nosso Agente de acordo com sua ação pode ser recompensado, penalizado ou pode simplesmente não acontecer nada com o mesmo caso ele realize alguma tomada de decisão computada como irrelevante para o contexto.
Apenas relembrando os conceitos teóricos iniciais, uma recompensa positiva se dará quando o agente der um passo em direção ao objetivo, quando o mesmo conseguir ficar mais próximo de seu objetivo. Da mesma forma uma penalização é aplicada sempre que de acordo com a ação ele acaba se afastando de seu objetivo, e existirão também situações onde a ação de nosso agente simplesmente não irá interferir nesse progresso.
Seguindo com nosso código podemos agora, caso você venha testando o mesmo etapa após etapa de criação, resetar as ações realizadas até o momento que muito provavelmente ainda estão carregadas na memória, para que possamos agora inserir alguns parâmetros específicos para o núcleo de nossa biblioteca gym. O reset em si é feito simplesmente rodando a função tf.reset_default_graph( ) e na sequência vamos começar a definir alguns parâmetros de controle para nosso Agente. Tais parâmetros podem serem alterados normalmente, inclusive é recomendado que se realizem testes com diferentes parâmetros para avaliar a performance do modelo.
Partindo para a parametrização, inicialmente temos
MAX_EPISODE_LENGTH com valor atribuído 18000, o que em outras palavras nos diz que esse número de ciclos equivale a mais ou menos 5 minutos jogando; EVAL_FREQUENCY setado em 200000 define de quantos em quantos frames processados será realizada uma avaliação da performance do modelo; EVAL_STEPS com valor 10000 atribuído para si define o número de frames a serem usados para avaliação; NETW_UPDATE_FREQ com valor 10000 define um tamanho de dados das ações usadas por nosso agente que serão levadas em conta para o processo de aprendizado.
Lembre-se que em outros momentos criamos as estruturas de código para isso e lá entendemos que como parte do processo de aprendizado é realizado essa verificação, onde de acordo com os padrões encontrados, os mais corretos e eficientes são guardados enquanto os incorretos aos poucos vão sendo descartados, de modo que a ideia é que nosso Agente domine uma determinada função quando o mesmo tiver uma bagagem de conhecimento acumulada as formas corretas de realizar tais feitos; DISCOUNT_FACTOR aqui definido como 0.99 nada mais é do que o gamma da equação e Bellman, equivalente a taxa de aprendizado de outros modelos de rede neural artificial convencional; REPLAY_MEMORY_START_SIZE com valor atribuído de 50000, aqui define o número de ações aleatórias iniciais que nosso Agente terá, lembre-se que em seu estado inicial o mesmo não tem nenhuma informação de aprendizado passado, começando totalmente de forma aleatória, em um processo de tentativa e erro; MAX_FRAMES definido como 30000000 delimita o tamanho de frames que nosso Agente será capaz de “ver” em seu estado inicial, tenha em mente que esse número alto inicial se dá porque aqui é levadas em consideração não somente as possibilidades de sua primeira ação, mas toda a árvore de possibilidades de todos os passos até chegar muito próximo ou alcançar seu objetivo, esse número é astronomicamente maior em sistemas de carro autônomo, por exemplo, onde as possibilidades são praticamente infinitas; MEMORY_SIZE, aqui com valor 1000000 define o número de informações que serão mantidas na memória de replay; NO_OP_STEPS = 10 delimita o número de ações iniciais que podem ser tomadas a cada ciclo de execução assim como de avaliação; UPDATE_FREQ aqui define que a cada 4 das 10 ações iniciais será realizado o reajuste dos pesos.
Lembre-se que estamos trabalhando sobre uma rede neural artificial intuitiva porém temos mecanismos equivalentes aos de outras redes, nesse contexto, esse parâmetro equivale aos ajustes dos pesos para atualização da descida do gradiente em modelos convencionais; HIDDEN aqui setado em 1024 simplesmente define o número de neurônios na primeira camada oculta da rede neural densa; LEARNING_RATE com valor 0.00001 define a taxa de aprendizado, que nesse contexto, está diretamente relacionado com UPDATE_FREQ; BS aqui simplesmente configura um valor de 32 para batch; PATH especifica o caminho onde será salvo o gif; SUMMARIES especifica onde ficarão salvos os arquivos de sumário, que nesse caso registram todas as ações corretas tomadas por nosso Agente, desde o começo até o ponto onde alcança seu objetivo, em forma de todo o caminho de processamento realizado em um ciclo; RUNID está diretamente ligado com o parâmetro anterior, pois é possível, obviamente, salvar diferentes execuções para que se realizem comparações e avaliações das mesmas; Via makedirs( ), função da biblioteca os, são criados os respectivos diretórios.
Por fim, o objeto atari, aqui instanciando a classe Atari, repassa para a mesma os dados de ENV_NAME e NO_OP_STEPS. Finalizando é exibida via console uma mensagem do estado inicial do Agente.
Anteriormente em determinados blocos de códigos de classes foram necessárias as criações de algumas sessões específicas, agora dando continuidade criaremos algumas no escopo global de nosso código. Porém antes disso precisamos criar algumas instancias no TensorFlow para que, literalmente, seus tensores saibam onde, em que ordem e quais dados buscar.
Sendo assim, via método variable_scope( ) parametrizado com
‘mainDQN’, chamamos da função homônima, nossa rede neural DQN, passando para ela os parâmetros para atari.env.action_space.n (o jogo em si), HIDDEN e LEARNING_RATE. Da mesma maneira para ‘targetDQN’ é repassado a instância do jogo e HIDDEN, repare que um escopo é para a rede base e outra para a rede que processará os dados alvo, os dados a serem atingidos com o cruzamento de todos aqueles objetos criados anteriormente sob a ótica de sistema de dependência.
Logo após é criado o objeto init que chama a função
global_variables_initializer( ), da biblioteca TensorFlow, sem parâmetros mesmo.
Também é criado o objeto saver, por sua vez chamando a função
train.Saver( ), também sem nenhum parâmetro específico além dos parâmetros padrão.
Prosseguindo é instanciada a variável reservada da biblioteca gym
MAIN_DQN_VARS que chama a função da biblioteca TensorFlow trainable_variables( ) alimentando a mesma com ‘mainDQN’. Note que essa função em particular equivale a função .fit( ) de alguns modelos convencionais. O mesmo é feito para TARGET_DQN_VARS com a respectiva ‘targetDQN’.
Apenas em uma pequena codificação adicional, para LAYERS_IDS são repassados todos os identificadores de objetos onde existem camadas de frames processados, note que alguns deles definimos manualmente nas camadas da rede neural densa enquanto outros (Bias) antes ignorados agora são instanciados no processo.
Eis que finalmente chegamos no momento mais crítico de nosso código, o de criar a função treino de nosso Agente, que realizará a execução de todas as instancias, assim como o cruzamento de todos os seus dados nas respectivas sessões.
Indentado para a função train( ) criamos uma estrutura de sessão via Session( ) referenciada como sess onde a sessão inicialmente é inicializada com os dados de init, passados como parâmetro para sess.run( ). Também é especificado o frame inicial a ser lido, neste caso, o frame 0 para o objeto frame_number. Também são instanciados os objetos rewards e loss_list com suas respectivas listas inicialmente vazias.
Peço perdão pela proporção da imagem, a mesma acabou ficando para esse bloco muito reduzida, porém é necessário mostrar nesse ponto todos os espaços de endentação. Ao final desse capítulo o código inteiro estará disponível em outro formato onde será possível realizar uma melhor leitura deste bloco.
Dando sequência, indentado para a sessão criada anteriormente criamos
uma estrutura condicional que define que enquanto frame_number tiver seu valor menor que o de MAX_FRAMES será realizado uma série de ações. Tenha em mente que este gatilho inicial simplesmente está definindo um limite de frames a serem processados nesse contexto. Logo, é criado um objeto epoch_frame que inicialmente tem seu valor nulo.
Em seguida é criada uma estrutura condicional dentro da condicional
anterior que define que enquanto o valor de epoch_frame for menor do que o de EVAL_FREQUENCY, uma série de ações serão tomadas. Aqui está um gatilho dos sistemas de dependência de nosso Agente.
Na estrutura dessa condicional é instanciado terminal_life_lost que por sua vez recebe como atributo a função reset( ) parametrizada com sess. Em outras palavras uma vez que o número de vidas chega a zero o jogo recomeça do zero. Note que em seguida é instanciado o objeto episode_reward_sum que nesse caso recebe como valor 0, pois pela lógica se nosso Agente perdeu o jogo, sua pontuação deve ser resetada também.
Na sequência é criado um laço de repetição que usa uma variável vazia
para percorrer todos os dados em MAX_EPISODE_LENGTH, dentro dessa estrutura o objeto action agora recebe atribuído para si os dados de explore_exploit_sched.get_action( ) parametrizado com sess, frame_number e atari.state. Vamos entender o que está acontecendo aqui, a ação que nosso Agente toma é com base em um estado, no código isso se dá pela leitura dos dados da sessão atual, último frame processado e o estado atual de atari (que instancia a classe Atari( )).
Em seguida os objetos processed_new_frame, reward, terminal,
terminal_life_lost e a variável vazia _ de nosso laço de repetição recebem como atributos os dados de atari.step( ) por sua vez lendo os dados de sess e de action. Como dito anteriormente, é de suma importância entender esses cruzamentos de dados assim como as dependências que estamos criando. Aqui, nesse momento estamos basicamente realizando a atualização dessas variáveis com os últimos dados da última etapa de processamento do estado anterior do Agente.
Também é definido que frame_number e epoch_frame recebem como
atributo o valor atual deles acrescentados em 1. Da mesma forma episode_reward_sum recebe como atributo a soma do seu valor atual com o valor de reward, atualizando assim essas variáveis.
Seguindo a mesma lógica, clipped_reward recebe para si os valores de
clip_reward( ) parametrizado com os valores de reward.
Em seguida é criada uma estrutura que atualiza também o estado de
nosso Agente com base nos últimos cruzamentos de dados, raciocine que isso é feito porque nosso agente não somente terá seus estados atualizados quando for recompensado ou penalizado por uma ação, lembre-se que existe também um terceiro estado onde a ação tomada surtiu um efeito nulo, e mesmo aqui não havendo qualquer alteração, recompensa ou penalização, esse estado tem que ser também atualizado. Para isso my_replay_memory chama a função add_experience( ) parrando alguns parâmetros como action=action, frame = o valor da matriz de processed_new_frame, reward = último valor atribuído para clipped_reward e terminal = o último valor lido para terminal_life_lost. Dessa forma é acionado os gatilhos de atualização de estado de nosso Agente mesmo quando o último estado é nulo.
Continuando na mesma endentação é criada outra estrutura condicional onde se o resultado da divisão entre o valor de frame_number e UPDATE_FREQ for 0, ou seja, sem resto nesta divisão, e o valor de frame_number for maior que o de REPLAY_MEMORY_START_SIZE, o objeto loss chama a função learn( ) repassando como parâmetros sess, my_replay_memory, MAIN_QDN, TARGET_DQN, BS e gama com o valor de DISCOUNT_FACTOR atribuído para si.
Note que o que este código está fazendo é dando continuidade para a ação de nosso Agente com base em um último estado lido como nulo (ação sem recompensa ou penalidade / ação com resultado 0 visto anteriormente). Executado este bloco, loss_list por meio da função append( ) tem seus valores atualizados com os dados de loss.
Mais uma estrutura condicional é criada, agora verificando se a diferença
da divisão dos valores de frame_number e NETW_UPDATE_FREQ foram igual a 0 e o valor de frame_number for maior que o de REPLAY_MEMORY_START_SIZE, a função update_networks é
instanciada tendo sess como parâmetro.
Ainda nesse contexto, finalizando o mesmo é criada uma última
estrutura condicional onde se terminal tiver seu estado definido como False, é interrompida a execução.
Dando continuidade agora realizamos a codificação do sistema que atualiza as recompensas dadas para nosso Agente, inicialmente instanciando rewards que chama a função append( ) por meio dela repassando para si os valores de episode_reward_sum.
Em seguida é criada uma estrutura condicional que percorre o tamanho
de rewards e se o resto da divisão deste valor por 10 for igual a zero, é criada uma estrutura condicional dentro dessa estrutura atual.
Nessa nova estrutura condicional é realizada a verificação onde se o
valor de frame_number for maior que o valor de REPLAY_MEMORY_START_SIZE, o objeto summ cria e roda uma sessão onde é instanciado PERFORMANCE_SUMMARIES, também é realizada a atualização de nosso dicionário por meio de feed_dict que por sua vez recebe para suas chaves os dados de LOSS_PH e para seus valores o valor médio de loss_list por meio da função np.mean( ).
Também é criada outra camada em nosso dicionário com dados chave de REWARD_PH e com dados de valor a média dos últimos 100 valores de rewards. Lembre-se que alguns blocos atrás criamos essas estruturas, porém elas tinham apenas um tamanho delimitado, mas sem dados as preenchendo, uma vez que não haviam dados de estados anteriores inicialmente, e nessa construção esses valores devem não só alimentar esses espaços como constantemente os atualizar.
Para o objeto da biblioteca gym SUM_WRITER é executada a função
add_summary( ) repassando para mesma os dados de summ e frame_number.
Na mesma lógica loss_list recebe como valor inicial uma lista vazia a ser alimentada com os respectivos valores de loss a partir dos primeiros ciclos de execução de nosso Agente.
Na sequência summ_param recebe como atributo os dados da sessão que
instancia e executa PARAM_SUMMARIES.
Complementar a isso SUMM_WRITER roda a função add_summary( )
com os dados de summ_param e frame_number.
Repare que todo esse processo é equivalente a quando em uma rede neural convencional guardamos os melhores dados dos pesos para reutilização, aqui, nesse contexto de uma rede neural artificial intuitiva, tais valores são salvos de maneira parecida, com a particularidade de que os mesmos são constantemente atualizados, salvos, lidos, em um ciclo praticamente interminável. O fato de ter tais dados salvos em algum estado faz com que possamos “desligar” nosso agente assim como “religar” o mesmo de forma que ele retome seu último estado sem necessidade de recomeçar do zero.
Em seguida é criada uma estrutura para mostrar a leitura destes números e exibir em console durante a execução.
Para isso também é realizada uma pequena codificação adicional onde habilitamos a exibição do terminal e do gif que nos mostrará nosso agente “jogando” BreakOut. Para essa visualização também deve ser instanciado frames_for_gif e eval_rewards como listas vazias.
Por fim, como aqui estamos apenas visualizando a execução de nosso Agente, é necessário definir que evaluate_frame_number terá atribuído para si o valor 0, em outras palavras isso sincroniza o valor de console com o frame que você está vendo.
Para alguns exemplos, de algumas versões da biblioteca gym é necessário criar uma codificação adicional para que de fato a visualização de nosso Agente jogando BreakOut se dê de forma correta, caso contrário pode ocorrer de o gif ficar com a imagem apenas do primeiro frame, erro que como mencionado anteriormente ocorre para alguns exemplos.
Sendo assim, é necessário criar um laço de repetição que usa uma
variável vazia _ para percorrer os dados de EVAL_STEPS, onde para cada leitura é realizada uma verificação onde se estamos trabalhando fazendo o uso do terminal, em terminal o objeto terminal_life_lost recebe como atributo atari.reset( ) tendo agora sess e evaluation = True, o que em outras palavras nos diz que o que for apresentado em terminal começa realmente do estado zero de nosso Agente e do jogo em si, contornando um bug que faz com que o jogo fosse iniciado de forma aleatória, sem seus valores resetados.
Em seguida episode_reward_sum = 0 apenas condiz com a explicação
do parágrafo anterior. Também é alterado o estado de terminal para False quando essa condição for alcançada. Note que aqui estamos deixando bem específico que estamos trabalhando aplicando essas condições apenas para os dados visualizados, todo o resto que é executado em segundo plano segue normalmente.
Agora muita atenção para esta etapa, repare que aqui o que estamos
instanciando para action é um valor 1 seguido de uma estrutura condicional, onde este valor deve coincidir com o valor de terminal_life_lost, caso contrário por meio de explore_exploit_sched.get_action( ) é acionado o gatilho para que o mesmo tome alguma decisão. Para a função get_action( ) são repassados como parâmetros frame_number, atari.state e evaluation = True.
Em seguida os dados de processed_new_frame, reward, terminal,
terminal_life_lost e new_frame são atualizados com os dados obtidos de atari.step( ) parametrizado com action.
Note que o que estamos fazendo aqui é contornando o fato de que nosso Agente precisa esperar a reação do ambiente para suas ações, cada vez que o mesmo dispara seu tiro em direção aos blocos a serem destruídos existe um tempo ocioso do mesmo esperando o projétil chegar até os blocos e retornar, onde em boa parte desse trajeto o mesmo pode simplesmente ficar parado, apenas realizando a leitura do ambiente. A partir de um certo ponto que o projétil está voltando aí sim ele precisa ir em direção ao projétil. Você verá que nas primeiras execuções ele fica se movimentando de forma aleatória durante esse período, à medida que ele aprende ele começa a “poupar energia” realizando ação apenas quando necessário.
Da mesma forma como as outras atualizações realizadas no passado para
estas variáveis, evaluate_frame_number tem seu valor atualizado com a soma de seu valor atual somado de 1. Da mesma forma episode_reward_sum tem seu valor atualizado com seu valor somado ao valor de reward.
Logo após é crida mais uma estrutura condicional, dessa vez verificando
nosso gif, para o mesmo não ficar estagnado exibindo apenas o primeiro frame, frames_for_gif é atualizado também acrescentando para si o valor de new_frame.
Do mesmo jeito em nosso terminal eval_rewards receber um novo valor oriundo de episode_reward_sum, gif tem seu estado alterado para False. Lembre-se que já que estamos realizando essa visualização, queremos acompanhar apenas em terminal o que é exibido em tela por nosso gif.
Finalizando, é criada uma simples mensagem exibindo também a
pontuação alcançada no jogo.
Lembre-se que uma prática comum quando estamos criando qualquer software é criar sistemas de validação, onde iremos tentar prever todos erros ou exceções cometidas pelo usuário do programa, contornando-as evitando que o programa pare de funcional. Aqui nosso Agente é programado por um humano, logo, cometerá erros que não foram previstos no momento da codificação de suas estruturas de código.
Sendo assim, é interessante criar um sistema de validação também para
nossa rede neural artificial intuitiva, como se bem sabe, isso em Python é feito por meio de try e except.
Para a tentativa de contornar a não exibição correta de nosso gif
chamamos manualmente a função generate_gif( ) passando como parâmetro para a mesmo frame_number, frames_for_gif, eval_rewards em sua posição 0 e PATH. Isso irá forçar a criação e leitura do gif.
Caso não seja possível realizar a ação anterior, muito provavelmente o
que está ocorrendo é que no cruzamento dos dados nenhum valor está sendo repassado ou atualizado para eval_rewards. Então é exibida uma mensagem em console elucidando esse erro. Na sequência serão tomadas uma série de medidas para forçar que as devidas ações sejam feitas.
Como estamos trabalhando com objetos reservados da biblioteca gym
cruzando dados com nossos objetos criados para esse exemplo, não que seja comum, mas é perfeitamente possível haver alguma incompatibilidade entre o cruzamento desses dados. Agora iremos forçar o salvamento dos dados dos primeiros ciclos processados.
Para isso instanciando o objeto saver, executando a função save( )
parrando como parâmetro PATH concatenado com ‘/my_model’, também parametrizando manualmente global_step com frame_number.
Em seguida frames_for_gif é instanciado com uma nova lista vazia. Nos
mesmos moldes summ novamente abre e executa uma sessão parametrizada com os dados de EVAL_SCORE_SUMMARY e o mesmo dicionário usado sempre, com os dados em chaves:valores de EVAL_SCORE_PH e o valor médio de eval_rewards.
Também é executado novamente todo o processo de SUMM_WRITER,
atualizando o sumário com os dados de summ e frame_number.
Por fim, encerrando esse bloco validador, é aberto o arquivo criado pelo
validador, ‘rewardsEval.dat’ como atributo de eval_reward_file, também é exibido em tela os dados de frame_number, média de eval_rewards e os dados lidos a partir de eval_reward_file, caso, obviamente, todo processo anterior tenha sido executado com sucesso.
Atingindo a marca de 400 linhas de código, mas partindo para o fim do
modelo de nossa rede artificial intuitiva, podemos criar o gatilho final que é por meio de TRAIN, finalmente instanciar e executar a função train( ). Caso não haja nenhum erro de sintaxe, a partir desse momento nosso agente é ativado e começa a trabalhar de forma contínua. Porém, vamos criar uma última estrutura que funcionará como validadora, serão apenas mais algumas linhas de código para quem já escreveu 400, que irão forçar todos os gatilhos serem acionados, da maneira correta, para que a inteligência artificial de nosso Agente funciona corretamente.
Para isso criamos uma estrutura condicional que verifica se train( ) está
mesmo sendo executada por TRAIN, supondo que você está executando o código pela primeira vez, não havendo estados anteriores, não havendo nenhum dado ou valor de objeto nenhum alocado em memória, etc... Realmente aqui estamos criando o validador que é o gatilho inicial para que tudo comece a entrar em processamento.
Inicialmente gif_path recebe como atributo um diretório a ser criado
caso ainda não exista um específico para nosso gif. Pela função do sistema makedirs( ) é validado o caminho de gif_path para que seja reconhecido.
Em seguida para trained_path e save_file são repassados os dados de save_files_dict, neste caso, todas as instâncias de ‘BreakoutDeterministic-v3’.
Na sequência explore_exploit_sched recebe como atributo a classe
ExplorationExploitationScheduler( ) que por sua vez instancia a rede neural MAIN_DQN, atari.env.action_space.n, que pré-carrega o jogo BreakOut a partir da biblioteca gym e define manualmente para esse estado atual que replay_memory_start_size obtém dados de REPLAY_MEMORY_START_SIZE, assim como max_frames obtém dados de MAX_FRAMES.
Seguindo com a parte final do código, note que agora abrimos uma sessão que englobará todas as outras em seu escopo, pois instanciando Session( ) repassando cada sessão de cada classe, por meio de sess, estamos agora de certa forma importando tudo para essa sessão em particular.
Dentro de sua estrutura saver recebe por meio da função
import_meta_graph( ) o valor da soma dos dados de trained_path e save_file. Do mesmo modo via função restore( ) saver realiza a execução da função latest_checkpoint( ) parametrizada por sua vez com trained_path. Consegue perceber que agora também existe o cruzamento e verificação dos dados de diferentes escopos, finalmente temos uma cadeia de retroalimentação ativa para nossa inteligência artificial.
Novamente frames_for_gif tem seus dados atualizados com uma lista
vazia, pois terminal_life_lost valida o estado inicial que o jogo deve ter a esse momento, o que pode ser confirmado no objeto seguinte pois episode_reward_sum volta a ter valor inicial 0.
Na sequência é criada mais uma estrutura condicional, onde, enquanto o estado dessa sessão for True, é feito o processamento contínuo por meio de atari.env.render( ) de nosso Agente sobre o jogo carregado.
Também é reforçado que é necessária a realização da primeira ação para
que sejam acionados todos gatilhos iniciais em seu efeito cascata. Isso é feito por meio de action, como feito anteriormente, executando uma ação ou pronto para execução de uma ação a qualquer momento, mesmo esperando o tempo de resposta do ambiente.
Mais uma vez é realizadas as atualizações de processed_new_frame,
reward, terminal, terminal_life_lost, new_frame, episode_reward_sum e frames_for_gif como realizado anteriormente.
Por fim, é feita a verificação de que quando o conteúdo de terminal
chega ao fim de processamento, essa sessão pode ficar suspensa.
Encerrando nosso exemplo, uma vez que nosso agente dominou o jogo por completo, terminando-o, o mesmo pode agora encerrar esse processo e partir para uma nova ação diferente de jogar BreakOut. Isso é feito encerrando o ambiente via atari.env.close( ).
Como de costume, podemos exibir em tela algumas mensagens nos
dando o feedback de nosso Agente. Por fim é salvo no referente pasta um gif que mostra diferentes estados do progresso de nosso Agente.
Observando por alguns momentos é possível, como esperado, notar que nas primeiras execuções nosso Agente erra bastante seus movimentos, perdendo no jogo, porém à medida que o mesmo realiza os primeiros acertos é impressionante a velocidade como o mesmo evolui no jogo, não cometendo mais erros e dominando o mesmo.
À esquerda o estado inicial do jogo, ao centro mais ou menos metade
dos blocos já destruídos e à direita uma imagem do final do jogo.
Apenas lembrando que o tempo de processamento está diretamente
ligado com o seu hardware em questão, nos testes feitos para esse livro, o código acima foi executado em um Intel i7 3770k de 3.5Ghz, com 16gb de ram e uma placa de vídeo GeForce GTX 1060 de 6gb, tendo um tempo médio de 50 segundos para que fosse completada toda a tarefa.
Código Completo:
import os
import random
import gym
import tensorflow as tf
import numpy as np
import imageio
from skimage.transform import resize
ENV_NAME = 'BreakoutDeterministic-v3'
class FrameProcessor(object):
def __init__(self, frame_height=84, frame_width=84):
self.frame_height = frame_height
self.frame_width = frame_width
self.frame = tf.placeholder(shape=[210, 160, 3], dtype=tf.uint8)
self.processed = tf.image.rgb_to_grayscale(self.frame)
self.processed = tf.image.crop_to_bounding_box(self.processed, 34, 0, 160, 160)
self.processed = tf.image.resize_images(self.processed,
[self.frame_height, self.frame_width],
method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
def __call__(self, session, frame):
return session.run(self.processed, feed_dict={self.frame:frame})
class DQN(object):
def __init__(self, n_actions, hidden=1024, learning_rate=0.00001,
frame_height=84, frame_width=84, agent_history_length=4):
self.n_actions = n_actions
self.hidden = hidden
self.learning_rate = learning_rate
self.frame_height = frame_height
self.frame_width = frame_width
self.agent_history_length = agent_history_length
self.input = tf.placeholder(shape=[None, self.frame_height,
self.frame_width,
self.agent_history_length], dtype=tf.float32)
self.inputscaled = self.input/255
self.conv1 = tf.layers.conv2d(inputs=self.inputscaled,
filters=32, kernel_size=[8, 8], strides=4,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
padding="valid", activation=tf.nn.relu, use_bias=False, name='conv1')
self.conv2 = tf.layers.conv2d(
inputs=self.conv1, filters=64, kernel_size=[4, 4], strides=2,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
padding="valid", activation=tf.nn.relu, use_bias=False, name='conv2')
self.conv3 = tf.layers.conv2d(
inputs=self.conv2, filters=64, kernel_size=[3, 3], strides=1,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
padding="valid", activation=tf.nn.relu, use_bias=False, name='conv3')
self.conv4 = tf.layers.conv2d(
inputs=self.conv3, filters=hidden, kernel_size=[7, 7], strides=1,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
padding="valid", activation=tf.nn.relu, use_bias=False, name='conv4')
self.valuestream, self.advantagestream = tf.split(self.conv4, 2, 3)
self.valuestream = tf.layers.flatten(self.valuestream)
self.advantagestream = tf.layers.flatten(self.advantagestream)
self.advantage = tf.layers.dense(
inputs=self.advantagestream, units=self.n_actions,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
name="advantage")
self.value = tf.layers.dense(
inputs=self.valuestream, units=1,
kernel_initializer=tf.variance_scaling_initializer(scale=2),
name='value')
self.q_values = self.value + tf.subtract(self.advantage,tf.reduce_mean(self.advantage,
axis=1, keepdims=True))
self.best_action = tf.argmax(self.q_values, 1)
self.target_q = tf.placeholder(shape=[None], dtype=tf.float32)
self.action = tf.placeholder(shape=[None], dtype=tf.int32)
self.Q = tf.reduce_sum(tf.multiply(self.q_values, tf.one_hot(self.action,
self.n_actions, dtype=tf.float32)),
axis=1)
self.loss = tf.reduce_mean(tf.losses.huber_loss(labels=self.target_q,
predictions=self.Q))
self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
self.update = self.optimizer.minimize(self.loss)
class ExplorationExploitationScheduler(object):
def __init__(self, DQN, n_actions, eps_initial=1, eps_final=0.1, eps_final_frame=0.01,
eps_evaluation=0.0, eps_annealing_frames=1000000,
replay_memory_start_size=50000, max_frames=25000000):
self.n_actions = n_actions
self.eps_initial = eps_initial
self.eps_final = eps_final
self.eps_final_frame = eps_final_frame
self.eps_evaluation = eps_evaluation
self.eps_annealing_frames = eps_annealing_frames
self.replay_memory_start_size = replay_memory_start_size
self.max_frames = max_frames
self.slope = -(self.eps_initial - self.eps_final)/self.eps_annealing_frames
self.intercept = self.eps_initial - self.slope*self.replay_memory_start_size
self.slope_2 = -(self.eps_final - self.eps_final_frame)/
(self.max_frames - self.eps_annealing_frames - self.replay_memory_start_size)
self.intercept_2 = self.eps_final_frame - self.slope_2*self.max_frames
self.DQN = DQN
def get_action(self, session, frame_number, state, evaluation=False):
if evaluation:
eps = self.eps_evaluation
elif frame_number < self.replay_memory_start_size:
eps = self.eps_initial
elif frame_number >= self.replay_memory_start_size and frame_number <
self.replay_memory_start_size + self.eps_annealing_frames:
eps = self.slope*frame_number + self.intercept
elif frame_number >= self.replay_memory_start_size + self.eps_annealing_frames:
eps = self.slope_2*frame_number + self.intercept_2
if np.random.rand(1) < eps:
return np.random.randint(0, self.n_actions)
return session.run(self.DQN.best_action, feed_dict={self.DQN.input:[state]})[0]
class ReplayMemory(object):
def __init__(self, size=1000000, frame_height=84, frame_width=84,
agent_history_length=4, batch_size=32):
self.size = size
self.frame_height = frame_height
self.frame_width = frame_width
self.agent_history_length = agent_history_length
self.batch_size = batch_size
self.count = 0
self.current = 0
self.actions = np.empty(self.size, dtype=np.int32)
self.rewards = np.empty(self.size, dtype=np.float32)
self.frames = np.empty((self.size, self.frame_height,
self.frame_width), dtype=np.uint8)
self.terminal_flags = np.empty(self.size, dtype=np.bool)
self.states = np.empty((self.batch_size, self.agent_history_length,
self.frame_height, self.frame_width),
dtype=np.uint8)
self.new_states = np.empty((self.batch_size, self.agent_history_length,
self.frame_height, self.frame_width), dtype=np.uint8)
self.indices = np.empty(self.batch_size, dtype=np.int32)
def add_experience(self, action, frame, reward, terminal):
if frame.shape != (self.frame_height, self.frame_width):
raise ValueError('Dimensão do frame está incorreta!')
self.actions[self.current] = action
self.frames[self.current, ...] = frame
self.rewards[self.current] = reward
self.terminal_flags[self.current] = terminal
self.count = max(self.count, self.current+1)
self.current = (self.current + 1) % self.size
def _get_state(self, index):
if self.count is 0:
raise ValueError("A memória de replay está vazia!")
if index < self.agent_history_length - 1:
raise ValueError("Indice mínimo deve ser 3")
return self.frames[index-self.agent_history_length+1:index+1, ...]
def _get_valid_indices(self):
for i in range(self.batch_size):
while True:
index = random.randint(self.agent_history_length, self.count - 1)
if index < self.agent_history_length:
continue
if index >= self.current and index - self.agent_history_length <= self.current:
continue
if self.terminal_flags[index - self.agent_history_length:index].any():
continue
break
self.indices[i] = index
def get_minibatch(self):
if self.count < self.agent_history_length:
raise ValueError('ERRO: Não foi possível alocar espaço na memória.')
self._get_valid_indices()
for i, idx in enumerate(self.indices):
self.states[i] = self._get_state(idx - 1)
self.new_states[i] = self._get_state(idx)
return np.transpose(self.states, axes=(0, 2, 3, 1)),
self.actions[self.indices],
self.rewards[self.indices],
np.transpose(self.new_states,
axes=(0, 2, 3, 1)),
self.terminal_flags[self.indices]
def learn(session, replay_memory, main_dqn, target_dqn, batch_size, gamma):
states, actions, rewards, new_states, terminal_flags = replay_memory.get_minibatch()
arg_q_max = session.run(main_dqn.best_action, feed_dict={main_dqn.input:new_states})
q_vals = session.run(target_dqn.q_values, feed_dict={target_dqn.input:new_states})
double_q = q_vals[range(batch_size), arg_q_max]
target_q = rewards + (gamma*double_q * (1-terminal_flags))
loss, _ = session.run([main_dqn.loss, main_dqn.update],
feed_dict={main_dqn.input:states,
main_dqn.target_q:target_q,
main_dqn.action:actions})
return loss
class TargetNetworkUpdater(object):
def __init__(self, main_dqn_vars, target_dqn_vars):
self.main_dqn_vars = main_dqn_vars
self.target_dqn_vars = target_dqn_vars
def _update_target_vars(self):
update_ops = []
for i, var in enumerate(self.main_dqn_vars):
copy_op = self.target_dqn_vars[i].assign(var.value())
update_ops.append(copy_op)
return update_ops
def __call__(self, sess):
update_ops = self._update_target_vars()
for copy_op in update_ops:
sess.run(copy_op)
def generate_gif(frame_number, frames_for_gif, reward, path):
for idx, frame_idx in enumerate(frames_for_gif):
frames_for_gif[idx] = resize(frame_idx, (420, 320, 3),
preserve_range=True, order=0).astype(np.uint8)
imageio.mimsave(f'{path}{"ATARI_frame_{0}_reward_{1}.gif".format(frame_number, reward)}',
frames_for_gif, duration=1/30)
class Atari(object):
def __init__(self, envName, no_op_steps=10, agent_history_length=4):
self.env = gym.make(envName)
self.process_frame = FrameProcessor()
self.state = None
self.last_lives = 0
self.no_op_steps = no_op_steps
self.agent_history_length = agent_history_length
def reset(self, sess, evaluation=False):
frame = self.env.reset()
self.last_lives = 0
terminal_life_lost = True
if evaluation:
for _ in range(random.randint(1, self.no_op_steps)):
frame, _, _, _ = self.env.step(1)
processed_frame = self.process_frame(sess, frame)
self.state = np.repeat(processed_frame, self.agent_history_length, axis=2)
return terminal_life_lost
def step(self, sess, action):
new_frame, reward, terminal, info = self.env.step(action)
if info['ale.lives'] < self.last_lives:
terminal_life_lost = True
else:
terminal_life_lost = terminal
self.last_lives = info['ale.lives']
processed_new_frame = self.process_frame(sess, new_frame)
new_state = np.append(self.state[:, :, 1:], processed_new_frame, axis=2)
self.state = new_state
return processed_new_frame, reward, terminal, terminal_life_lost, new_frame
def clip_reward(reward):
if reward > 0:
return 1
elif reward == 0:
return 0
else:
return -1
tf.reset_default_graph()
MAX_EPISODE_LENGTH = 18000
EVAL_FREQUENCY = 200000
EVAL_STEPS = 10000
NETW_UPDATE_FREQ = 10000
DISCOUNT_FACTOR = 0.99
REPLAY_MEMORY_START_SIZE = 50000 MAX_FRAMES = 30000000
MEMORY_SIZE = 1000000
NO_OP_STEPS = 10
UPDATE_FREQ = 4
HIDDEN = 1024
LEARNING_RATE = 0.00001
BS = 32
PATH = "output/"
SUMMARIES = "summaries"
RUNID = 'run_1'
os.makedirs(PATH, exist_ok=True)
os.makedirs(os.path.join(SUMMARIES, RUNID), exist_ok=True)
SUMM_WRITER = tf.summary.FileWriter(os.path.join(SUMMARIES, RUNID)) atari = Atari(ENV_NAME, NO_OP_STEPS)
print(f'O ambiente gerado tem as respectivas {atari.env.action_space.n} ações: {atari.env.unwrapped.get_action_meanings()
with tf.variable_scope('mainDQN'):
MAIN_DQN = DQN(atari.env.action_space.n, HIDDEN, LEARNING_RATE)
with tf.variable_scope('targetDQN'):
TARGET_DQN = DQN(atari.env.action_space.n, HIDDEN)
init = tf.global_variables_initializer()
saver = tf.train.Saver()
MAIN_DQN_VARS = tf.trainable_variables(scope='mainDQN') TARGET_DQN_VARS = tf.trainable_variables(scope='targetDQN')
LAYER_IDS = ["conv1", "conv2", "conv3", "conv4", "denseAdvantage",
"denseAdvantageBias", "denseValue", "denseValueBias"]
with tf.name_scope('Performance'):
LOSS_PH = tf.placeholder(tf.float32, shape=None, name='loss_summary')
LOSS_SUMMARY = tf.summary.scalar('loss', LOSS_PH)
REWARD_PH = tf.placeholder(tf.float32, shape=None, name='reward_summary')
REWARD_SUMMARY = tf.summary.scalar('reward', REWARD_PH)
EVAL_SCORE_PH = tf.placeholder(tf.float32, shape=None, name='evaluation_summary')
EVAL_SCORE_SUMMARY = tf.summary.scalar('evaluation_score', EVAL_SCORE_PH)
PERFORMANCE_SUMMARIES = tf.summary.merge([LOSS_SUMMARY, REWARD_SUMMARY])
with tf.name_scope('Parameters'):
ALL_PARAM_SUMMARIES = []
for i, Id in enumerate(LAYER_IDS):
with tf.name_scope('mainDQN/'):
MAIN_DQN_KERNEL = tf.summary.histogram(Id, tf.reshape(MAIN_DQN_VARS[i], shape= [-1]))
ALL_PARAM_SUMMARIES.extend([MAIN_DQN_KERNEL])
PARAM_SUMMARIES = tf.summary.merge(ALL_PARAM_SUMMARIES)
def train():
my_replay_memory = ReplayMemory(size=MEMORY_SIZE, batch_size=BS)
update_networks = TargetNetworkUpdater(MAIN_DQN_VARS, TARGET_DQN_VARS)
explore_exploit_sched = ExplorationExploitationScheduler(
MAIN_DQN, atari.env.action_space.n,
replay_memory_start_size=REPLAY_MEMORY_START_SIZE,
max_frames=MAX_FRAMES)
with tf.Session() as sess:
sess.run(init)
frame_number = 0
rewards = []
loss_list = []
while frame_number < MAX_FRAMES:
epoch_frame = 0
while epoch_frame < EVAL_FREQUENCY:
terminal_life_lost = atari.reset(sess)
episode_reward_sum = 0
for _ in range(MAX_EPISODE_LENGTH):
action = explore_exploit_sched.get_action(sess, frame_number, atari.state)
processed_new_frame, reward, terminal, terminal_life_lost, _ = atari.step(sess, action)
frame_number += 1
epoch_frame += 1
episode_reward_sum += reward
clipped_reward = clip_reward(reward)
my_replay_memory.add_experience(action=action,
frame=processed_new_frame[:, :, 0], reward=clipped_reward,
terminal=terminal_life_lost)
if frame_number % UPDATE_FREQ == 0 and frame_number > REPLAY_MEMORY_START_SIZE:
loss = learn(sess, my_replay_memory, MAIN_DQN, TARGET_DQN,
BS, gamma = DISCOUNT_FACTOR)
loss_list.append(loss)
if frame_number % NETW_UPDATE_FREQ == 0 and frame_number > REPLAY_MEMORY_START_SIZE:
update_networks(sess)
if terminal:
terminal = False
break
rewards.append(episode_reward_sum)
if len(rewards) % 10 == 0:
if frame_number > REPLAY_MEMORY_START_SIZE:
summ = sess.run(PERFORMANCE_SUMMARIES,
feed_dict={LOSS_PH:np.mean(loss_list),
REWARD_PH:np.mean(rewards[-100:])})
SUMM_WRITER.add_summary(summ, frame_number)
loss_list = []
summ_param = sess.run(PARAM_SUMMARIES)
SUMM_WRITER.add_summary(summ_param, frame_number)
print(len(rewards), frame_number, np.mean(rewards[-100:]))
with open('rewards.dat', 'a') as reward_file:
print(len(rewards), frame_number,
np.mean(rewards[-100:]), file=reward_file)
terminal = True
gif = True
frames_for_gif = []
eval_rewards = []
evaluate_frame_number = 0
for _ in range(EVAL_STEPS):
if terminal:
terminal_life_lost = atari.reset(sess, evaluation=True)
episode_reward_sum = 0
terminal = False
action = 1 if terminal_life_lost else explore_exploit_sched.get_action(sess, frame_number,
atari.state,
evaluation=True)
processed_new_frame, reward, terminal, terminal_life_lost, new_frame = atari.step(sess, action)
evaluate_frame_number += 1
episode_reward_sum += reward
if gif:
frames_for_gif.append(new_frame)
if terminal:
eval_rewards.append(episode_reward_sum)
gif = False
print("Pontuação:\n", np.mean(eval_rewards))
try:
generate_gif(frame_number, frames_for_gif, eval_rewards[0], PATH)
except IndexError:
print("ERRO: Sem pontuação para esse jogo")
saver.save(sess, PATH+'/my_model', global_step=frame_number)
frames_for_gif = []
summ = sess.run(EVAL_SCORE_SUMMARY, feed_dict=
{EVAL_SCORE_PH:np.mean(eval_rewards)})
SUMM_WRITER.add_summary(summ, frame_number)
with open('rewardsEval.dat', 'a') as eval_reward_file:
print(frame_number, np.mean(eval_rewards), file=eval_reward_file)
TRAIN = train()
if TRAIN:
train()
if not TRAIN:
gif_path = "GIF/"
os.makedirs(gif_path, exist_ok=True)
trained_path, save_file = save_files_dict[ENV_NAME]
explore_exploit_sched = ExplorationExploitationScheduler(
MAIN_DQN, atari.env.action_space.n,
replay_memory_start_size=REPLAY_MEMORY_START_SIZE,
max_frames=MAX_FRAMES)
with tf.Session() as sess:
saver = tf.train.import_meta_graph(trained_path+save_file)
saver.restore(sess,tf.train.latest_checkpoint(trained_path))
frames_for_gif = []
terminal_life_lost = atari.reset(sess, evaluation = True)
episode_reward_sum = 0
while True:
atari.env.render()
action = 1 if terminal_life_lost else explore_exploit_sched.get_action(sess, 0, atari.state,
evaluation = True)
processed_new_frame, reward, terminal, terminal_life_lost, new_frame = atari.step(sess, action)
episode_reward_sum += reward
frames_for_gif.append(new_frame)
if terminal == True:
break
atari.env.close()
print("A recompensa total é: {}".format(episode_reward_sum))
print("Gerando GIF...")
generate_gif(0, frames_for_gif, episode_reward_sum, gif_path)
print("Gif criado, confira na pasta {}".format(gif_path))