← Caderno de laboratório

Um jogo determinístico consegue ensinar uma rede a correr pelos telhados?

Skyline Run tem um distrito de telhados determinístico, mas uma conclusão perfeita por uma máquina só prova que as regras e a geometria estão de acordo. Ela não mostra se um jogador consegue entender a lição. O registro de construção anterior mediu essa conclusão; agora preciso de algo que jogue sem receber instruções de como fazê-lo.

Por isso, estou criando um laboratório de IA para Skyline Run: redes neurais que aprendem o distrito no navegador, em níveis de habilidade selecionáveis, treinadas por um algoritmo genético ou por aprendizado Q profundo, observáveis da velocidade 1× até a máxima ou completamente ocultas para treinar mais rápido.

Antes que a primeira rede pudesse correr, porém, o ambiente mudou. O checkpoint, o vão mais difícil e a regeneração de energia foram alterados, transformando um plano de treinamento organizado em um teste: as observações e recompensas ainda descrevem o jogo?

Escolhi todos os números abaixo antes da primeira execução de treinamento, exceto quando os identifico como medidos. A seção posterior sobre o reajuste do distrito é composta inteiramente de medições. Essa distinção mostra onde o plano termina e as evidências começam.

Corredores translúcidos em wireframe ciano espalhados por uma paisagem neon de telhados a diferentes distâncias, alguns no meio do salto e outros caídos, representam uma população de agentes tentando atravessar o mesmo distrito.
Uma geração percorre o distrito em conjunto; a maioria fracassa nos primeiros segundos.

Eu sou o proprietário e desenvolvo este site e o canal AI Maker Lab — este é um registro de construção, não uma análise independente.

O motor já era um ambiente de treinamento

O motor já tem por acaso a propriedade mais importante para aprendizado de máquina. step(state, input, level) é uma função pura com DT = 1/60 fixo. Ela retorna um novo estado, nunca altera seu argumento e não toca no DOM. Todos os temporizadores de jogabilidade são contadores inteiros de ticks.

Isso é um ambiente de aprendizado por reforço. Ele é determinístico, não depende da renderização e pode avançar tão rápido quanto a CPU permitir. O renderizador é um módulo separado que desenha um estado; ele nunca é necessário para produzir um. Nada no jogo precisa mudar para que seja possível treinar com ele — um fato que vale a pena declarar sem rodeios, porque a tentação de “adicionar um gancho para a IA” é como os motores apodrecem.

Os requisitos ficam sobre essa base:

  • Agentes em vários níveis de habilidade, porque o objetivo é simular jogadores, não produzir um único corredor ideal.
  • Treinamento executado no navegador, em um Web Worker, para que a página continue responsiva.
  • Dois métodos de treinamento — aprendizado por reforço e um algoritmo genético — porque eles falham de maneiras diferentes, e compará-los é a parte interessante.
  • Um modelo que possa ser baixado como arquivo e carregado novamente.
  • Um modo visual com controle de velocidade e um modo headless que não renderize nada.
  • Análises ao vivo: perda, fitness, recompensa, progresso.

A pesquisa eliminou três palpites

Valia a pena pesquisar três perguntas em vez de adivinhar: como estabilizar o Q-learning, como dimensionar uma população de neuroevolução e com que frequência um agente deveria poder tomar uma decisão.

O Q-learning precisa de duas muletas, e ambas são inegociáveis. A primeira é a repetição de experiências: um buffer de transições passadas que a rede reamostra para não aprender apenas com o passo mais recente. A amostragem de minibatches aleatórios desse buffer, segundo as implementações de referência, “descorrelaciona os dados e leva a uma eficiência de dados melhor”. A segunda é uma rede-alvo: uma cópia atualizada lentamente que produz o alvo da previsão para que o treinamento não persiga a si mesmo. Ela fica congelada entre as atualizações realizadas a cada C passos. Dois outros detalhes vieram da mesma leitura e não custam nada para adotar: não dar um passo de gradiente até que o buffer contenha pelo menos um batch e usar Double DQN — escolher a ação com a rede ativa e avaliá-la com a cópia-alvo —, o que elimina uma superestimação sistemática com uma linha de código.

A neuroevolução é uma alternativa legítima aqui, não um brinquedo. Já se demonstrou que algoritmos genéticos sem gradiente evoluem os pesos de redes profundas de forma competitiva no Atari e na locomoção humanoide. O ponto estrutural que tornou essa abordagem atraente para esta construção: na neuroevolução, “a rede só é treinada entre episódios, e não a cada quadro” — uma abordagem filogenética, e não ontogenética, ou seja, aprendizado ao longo de gerações em vez de dentro de uma única vida. Para um laboratório no navegador, isso é um presente, pois uma geração inteira pode ser simulada em sincronia e renderizada como uma multidão. As heurísticas de população se agrupam em torno de 50–200 indivíduos; o elitismo deve manter os melhores desempenhos sem mutação para que um genoma bom nunca seja perdido por uma moeda que caiu do lado errado; e a probabilidade de mutação tem um teto — um estudo relata melhora de desempenho à medida que a taxa de mutação cresce e, depois, queda ao passar de aproximadamente 0.03, enquanto esquemas adaptativos ficam em torno de 5%.

A frequência de decisão é uma aceleração gratuita até deixar de ser. O frame skip faz o agente observar a cada k quadros e repetir sua ação durante o intervalo. Ele “encurta proporcionalmente o horizonte efetivo”, torna manobras de alto nível alcançáveis por exploração aleatória em vez de exponencialmente improváveis e reduz o número de passagens para a frente. Também tem um modo de falha conhecido: pular demais causa excesso de compromisso, e tarefas que exigem mira precisa pioram. k = 4 é a solução de compromisso citada com mais frequência.

O único lugar em que fui contra a escolha óbvia foi a biblioteca matemática. TensorFlow.js é a resposta padrão para aprendizado de máquina em um navegador, e seu backend WebGL é genuinamente poderoso — tensores como texturas, operações como shaders. Essa vantagem é irrelevante nesta escala. Uma execução posterior do construtor real mediu a rede [119, 48, 8] em exatamente 6,152 parâmetros: 119×48 + 48 = 5,760, depois 48×8 + 8 = 392. O overhead de despacho por operação excederia a aritmética, e uma viagem de ida e volta à GPU por passagem para a frente, dezenas de vezes por segundo simulado, seria uma pessimização. O laboratório usa operações matriciais feitas à mão com Float32Array, retropropagação manual e Adam: mais rápidas para esse tamanho, sem dependência e trivialmente portáveis para um worker. O modelo salvo armazena os pesos brutos, então essa decisão é reversível — trocar por uma biblioteca mais tarde muda dois arquivos e nenhum formato de arquivo.

O contrato de treinamento, número por número

O contrato de observação oferece uma visão local útil sem revelar o distrito inteiro: 119 floats combinam uma janela de tiles voltada para a frente com o estado do movimento.

Observação. 119 floats, reconstruídos em um buffer reutilizado a cada decisão. 108 deles formam uma janela de tiles de 12×9 ao redor do tile central do jogador, abrangendo 2 tiles atrás, 9 à frente, 4 acima e 4 abaixo. Os tiles são codificados como um escalar cada: sólido 1, atravessável em um sentido 0.5, checkpoint 0.25, objetivo 0.75, perigo -1, vazio 0. Tudo abaixo da borda inferior da fase é lido como -1, porque um fosso e um espinho merecem o mesmo sinal. A janela não é espelhada conforme a direção do jogador — neste distrito, o objetivo está sempre à direita, e fingir o contrário acrescentaria uma simetria que a fase não tem.

Os 11 restantes são o estado que a janela de tiles não consegue expressar: velocidade horizontal e vertical, indicador de contato com o chão, contador de coyote time, temporizador do dash, cooldown do dash, energia do dash, direção, deslocamentos x e y dentro do tile e distância normalizada até o objetivo. Os dois deslocamentos dentro do tile existem porque um jogador de 12×20 px em uma grade de 16 px pode ficar em posições materialmente diferentes dentro de uma célula.

Um pequeno jogador retangular sobre uma grade de tiles, com uma janela de sensoriamento translúcida sobreposta, alcança dois tiles atrás e nove à frente, quatro acima e quatro abaixo.
O agente enxerga 12×9 tiles ao redor de si, com peso para a frente, mais onze escalares que a grade não consegue expressar.

Ações. Oito quadros discretos em vez de quatro botões independentes: parado, direita, direita+salto, direita+dash, salto, esquerda, esquerda+salto, direita+salto+dash. Um conjunto discreto é o que uma rede Q quer, e esses oito cobrem o vocabulário do distrito. Uma consequência é deliberada: como o motor detecta a borda do salto em relação ao quadro anterior, um agente precisa soltar o salto entre as decisões para saltar novamente. Ele não pode manter o botão pressionado para sempre e esperar saltos repetidos. Essa é exatamente a restrição de uma mão humana.

Intervalo de decisão: 3 ticks. Um pouco mais apertado que o k = 4 citado. As constantes são COYOTE_TICKS = 6 e BUFFER_TICKS = 7, mas uma execução posterior mediu 5 ticks de tolerância utilizável para ambas: um salto de coyote time funciona 5 ticks depois de sair de uma beirada e falha em 6, enquanto uma entrada armazenada no buffer funciona 5 ticks antes da aterrissagem e falha em 6. Um intervalo de 3-tick pode se atrasar no máximo 2 ticks em relação a qualquer tick ideal, deixando uma margem de 3-tick dentro de qualquer uma das janelas medidas. 20 decisões por segundo se aproxima da frequência de entrada humana e ainda reduz as passagens para a frente em dois terços.

Os níveis de habilidade são desvantagens incorporadas ao treinamento. Esta é a decisão que espero que seja questionada, então aqui está o raciocínio. A maneira preguiçosa de criar uma IA fraca é treinar uma política forte e depois corromper sua saída. Isso produz um bom jogador tendo uma convulsão, não um jogador ruim. Em vez disso, cada nível é uma propriedade do ambiente presente durante o treinamento: um atraso de reação implementado como uma fila FIFO de entradas e uma probabilidade de substituir a ação por uma aleatória.

NívelAtraso de reaçãoRuído de ação
Iniciante9 ticks10%
Casual5 ticks5%
Habilidoso2 ticks2%
Especialista0 ticks0%

Uma rede iniciante aprende a jogar enquanto reage com 150 ms de atraso e erra uma entrada em cada dez. Ela deve desenvolver o comportamento que sobrevive a essas restrições — mais cautela, pior timing em vãos — em vez do comportamento de um especialista corrompido. Se isso realmente acontece é a primeira coisa que vale a pena medir.

Algoritmo genético. População 64, dentro da faixa citada de 50–200 e pequena o bastante para renderizar como uma multidão. Os 4 melhores genomas sobrevivem sem mutação. Os pais são escolhidos por torneio de 3; 75% dos filhos são cruzamentos uniformes, e os demais são clones; cada peso dos filhos então sofre mutação com probabilidade 0.08 pela adição de uma gaussiana de σ 0.12, limitada a ±4. A taxa de mutação fica acima do teto de 0.03 relatado por um estudo e próxima dos 5% dos esquemas adaptativos — um viés deliberado em favor da exploração para uma primeira execução em um problema inexplorado e o primeiro hiperparâmetro que espero reduzir.

O fitness é o maior progresso mais 250 por checkpoint, mais 1.500 por uma conclusão, mais metade de cada tick não gasto sob o limite de 3.600 ticks, menos 100 por morte. O progresso é medido como o melhor progresso, nunca como a posição atual, para que morrer e reaparecer não possa ser pontuado como movimento para trás.

Deep Q-Network. Aqui, a estabilidade tem precedência sobre atualizações imediatas. γ 0.99, taxa de aprendizado 1e-3, batch 64, capacidade de repetição 20,000, nenhum passo de gradiente até que 1,000 transições estejam armazenadas, sincronização rígida da rede-alvo a cada 500 passos de treinamento, ε reduzido linearmente de 1.0 para 0.05 ao longo de 50,000 decisões, perda de Huber com δ 1 apenas na saída da ação selecionada. A perda de Huber usa o erro quadrático perto de zero e passa a ser linear quando o erro cresce, para que valores atípicos não dominem. Ela já limita o gradiente, portanto não há uma etapa separada de clipping.

Recompensa por decisão: tiles de novo progresso, mais 2 por checkpoint, mais 20 por uma conclusão, menos 2 por morte, menos 0.01 por decisão como custo de tempo. Os episódios terminam em uma conclusão, em 3,600 ticks, em 8 mortes ou quando o melhor progresso não melhora em 4 px durante 360 ticks. Esse último é o detector de bloqueio, e é por isso que uma geração de 64 agentes sem esperança custa segundos, e não minutos.

Arquitetura. Um worker é responsável pelo treinador. Ele sempre envia eventos de métricas e só envia snapshots de quadros quando o modo visual está ativado, limitados a um a cada 16 ms. O controle de velocidade é uma cadência no worker: em multiplicadores fixos, ele busca velocidade × 60 ticks por segundo contra um acumulador em tempo real; no máximo, simplesmente executa fatias de 12 ms em sequência. O modo headless não é “renderização ignorada” — o worker não envia quadro algum, e o teste de ponta a ponta verifica uma contagem de quadros exatamente igual a zero, porque um modo de desempenho que silenciosamente ainda serializa o estado é uma mentira.

Dois processos abstratos de treinamento lado a lado: uma onda de muitas figuras idênticas avançando em conjunto e uma única figura com um loop de feedback retornando a si mesma.
O algoritmo genético avalia uma geração inteira em sincronia; a rede Q aprende com suas próprias transições repetidas.

Os modelos são salvos como JSON com a tag aml.skyline-run-ai, versão 1, contendo o algoritmo, o nível de habilidade e seus parâmetros, a geometria da observação, a quantidade de ações, os tamanhos das camadas, os pesos brutos e os totais de treinamento. O carregamento valida tudo isso e rejeita uma incompatibilidade com um erro, em vez de uma resposta errada de aparência plausível — uma janela de observação com outro formato seria carregada normalmente como números e se comportaria como ruído.

O distrito mudou antes do início do treinamento

As decisões acima foram fixadas antes da primeira execução de treinamento. O jogo mudou depois que esse registro foi escrito e antes do início do treinamento. Tudo nesta seção é uma medição feita a posteriori, não outro valor escolhido. Agora, regenera-se um SP a cada 48 ticks no chão em corrida, contra 96 ticks no chão parado. Agora há um checkpoint na coluna 116. O vão de dashes encadeados estreitou de 10 tiles para 9 tiles. A conclusão determinística ideal medida caiu de 2218 para 1304 ticks.

O resultado referente ao horizonte de observação é o mais importante. A janela de tiles alcança 9 tiles à frente. A partir do tile de lançamento, onde o tile central do jogador está na coluna 121, ela cobre até a coluna 130. A primeira coluna sólida da plataforma de aterrissagem é a 131, fora da janela. A coluna mais à frente, 130, codifica 0 em todas as 9 células, exatamente como o ar livre.

A plataforma entra na janela pela primeira vez quando o tile central chega à coluna 122: um tile dentro do vão, com o jogador já no ar. Antes do reajuste, a plataforma começava na coluna 132 e aparecia pela primeira vez com o tile central na coluna 123, dois tiles dentro do vão. O agente se compromete às cegas com o movimento mais difícil do distrito. O reajuste reduziu essa margem às cegas de 2 tiles para 1. Aumentar o alcance para a frente não é uma correção gratuita: ele está incorporado aos metadados de observação do modelo salvo, portanto mudá-lo invalida todos os modelos já treinados com esse contrato.

O checkpoint também muda o significado do detector de bloqueio. Voltar do antigo checkpoint da coluna 80 ao tile de lançamento leva 276 ticks, ou 76.7% da janela de bloqueio de 360-tick; com o reaparecimento, leva 294 ticks, ou 81.7%. A partir do novo checkpoint da coluna 116, o retorno leva 36 ticks, ou 10.0%; com o reaparecimento, são 54 ticks, ou 15.0%. maxPx é o melhor progresso de todos os tempos, portanto não avança em nenhum ponto do retorno. O temporizador de bloqueio corre o tempo inteiro enquanto o agente faz exatamente a coisa certa. No movimento mais difícil do distrito, um detector criado para eliminar agentes sem esperança esteve perto o bastante de eliminar agentes que estavam aprendendo esse movimento.

MediçãoAntes do reajusteDepois do reajuste
Fitness do algoritmo genético em uma conclusão perfeita6,315.677,022.67
Componente de velocidade do algoritmo genético6911,148
Limite do episódio / rota ideal1.62×2.76×
Decisões da DQN por conclusão na rota ideal740435
Custo de tempo acumulado da DQN na rota ideal-7.40-4.35

O fitness do algoritmo genético em uma conclusão perfeita aumentou em 707, ou 11.2%: 250 vêm do checkpoint acrescentado, e 457, da conclusão mais rápida. O checkpoint acrescentado vale +2 por conclusão da DQN, enquanto a quantidade menor de decisões produz o custo de tempo mais baixo da tabela. Os valores de fitness anteriores e posteriores ao reajuste não são comparáveis. Qualquer linha de base anterior ao reajuste perdeu a validade.

A mudança na regeneração introduz um estado oculto. Os 11 escalares carregam a energia do dash como sp / SP_MAX, mas não o acumulador de regeneração. Sob a antiga regra uniforme de 72-tick, essa omissão escondia um relógio fixo. Sob a regra de duas taxas, o acumulador avança em uma velocidade determinada pelas entradas recentes do próprio agente. A execução produziu estados com valores de acumulador 0 e 94, mas observações 119-float idênticas bit a bit. O codificador nunca lê esse campo, portanto quaisquer dois estados que difiram apenas no valor do acumulador produzem a mesma observação. No extremo, faltam 96 ticks no chão parado para o próximo SP quando o acumulador vale 0, contra 1 tick quando vale 95: uma diferença de 95 engine ticks. Essa é uma observabilidade parcial introduzida por uma mudança no design do jogo, invisível para uma rede feedforward sem memória nem empilhamento de quadros. As escolhas honestas são acrescentar o acumulador como um décimo segundo escalar e quebrar o contrato de observação do modelo salvo, ou deixá-lo de fora e aceitar que o timing do SP precisa ser inferido a partir de um comportamento que a rede não consegue enxergar.

O que o projeto prova — e o que o treinamento precisa testar

Nada foi treinado ainda. Os valores originais das decisões continuam sendo um ponto de partida tirado da literatura e das constantes medidas do motor, e o registro de construção anterior existe principalmente porque três valores cuidadosamente fundamentados se mostraram errados ao entrar em contato com o sistema em execução. As correções esperadas continuam sendo a taxa de mutação, o intervalo de decisão de 3-tick e o custo de tempo da recompensa. O alcance para a frente da janela de observação agora se junta à lista, com evidências medidas já contrárias a ele.

A afirmação específica que quero testar primeiro é a dos níveis de habilidade: que treinar sob uma desvantagem produz maneiras de jogar reconhecidamente diferentes, em vez de uma versão uniformemente pior da mesma maneira de jogar. Se as execuções de uma rede iniciante parecerem as de uma especialista com soluços, o design está errado, e os níveis precisam diferir estruturalmente, não apenas no ruído.

Duas limitações conhecidas já foram aceitas em vez de resolvidas. Episódios encerrados por timeout ou pela detecção de bloqueio são armazenados como transições terminais, o que não é rigorosamente correto — são truncamentos, e uma estimativa de valor construída a partir deles é ligeiramente pessimista. E existe uma única fase. Um agente que conclui um distrito memorizou um distrito; a generalização não entra em pauta até que exista um segundo no qual ele possa falhar.

O laboratório também não é um estudo com jogadores. Ele mede se as regras podem ser aprendidas, uma pergunta mais restrita e mecânica do que saber se uma pessoa consegue compreendê-las. As medições continuam em vigor pelo mesmo motivo da última vez: dizer o que mudou quando a próxima premissa falhar.

Portanto, a decisão não é que esses valores iniciais estejam certos. Decidi que o motor determinístico, os contratos explícitos de treinamento e as ressalvas medidas tornam esses valores testáveis. A seguir, Ensinando o treinador: evoluir a recompensa em vez de ajustá-la à mão pergunta se a própria evolução deveria ajustar a recompensa em vez de deixar seus coeficientes nas minhas mãos.

Fontes

Eu sou o proprietário e desenvolvo este site e o canal AI Maker Lab — este é um registro de construção, não uma análise independente.

Continue lendo