Voltar ao blog

Aprendizado por Reforço com JavaScript: Crie do Zero um Agente Q-Learning com TensorFlow.js

Aprendizado por Reforço com JavaScript: Crie do Zero um Agente Q-Learning com TensorFlow.js

O Aprendizado por Reforço (RL) é um ramo do aprendizado de máquina em que um agente aprende a tomar decisões ótimas interagindo com um ambiente e recebedo recompensas (feedback) por suas ações. Diferente do aprendizado supervisionado, o RL não requer um conjunto de dados pré-definido; em vez disso, o agente descobre, por tentativa e erro, como maximizar suas recompensas (www.datacamp.com). Podemos imaginar isso como treinar um animal de estimação: a cada ação bem-sucedida, ele recebe um petisco (recompensa) e, com o tempo, aprende qual comportamento traz mais petiscos.

Neste artigo você vai aprender, passo a passo, a implementar do zero um agente de Q-Learning em JavaScript, usando o TensorFlow.js para gerenciar os cálculos. Veremos desde a definição dos conceitos básicos até um exemplo prático de treinamento. Ao final, teremos um agente capaz de resolver um desafio simples de tomada de decisão, encontrando as ações que levam à maior recompensa possível. Para contextualizar, temos alguns termos importantes:

  • Agente: Entidade que toma decisões (escolhe ações) com base em observações (estados) do ambiente.
  • Ambiente: Mundo ou simulação com a qual o agente interage. Contém estados e regras de transição e recompensa.
  • Estado: Descrição da configuração atual do ambiente (ex.: posição do agente).
  • Ação: Escolha que o agente faz em cada estado (ex.: mover para cima, baixo, etc.).
  • Recompensa: Feedback numérico dado ao agente após cada ação. Pode ser positiva (bônus) ou negativa (punição).
  • Episódio: Uma sequência de passos do agente começa num estado inicial e termina num estado terminal (por exemplo, ao alcançar a meta).

Segundo especialistas, o RL pode ser descrito em cinco passos simples (www.datacamp.com). De forma resumida:

  • O agente começa em um estado inicial.
  • Ele escolhe uma ação baseada em sua política (estratégia).
  • O ambiente retorna para um novo estado e fornece uma recompensa.
  • O agente atualiza seu conhecimento (função de valor) com base nessa recompensa.
  • O processo se repete até atingir um estado final ou critério de parada.

No Q-Learning, um algoritmo de RL, usamos uma estrutura baseada em valores e off-policy (que não requer um modelo interno do ambiente) para aprender a melhor série de ações a partir de cada estado (www.datacamp.com). A letra “Q” significa qualidade: cada par estado-ação recebe um valor Q que estima quão boa é essa ação para maximizar recompensas futuras (www.datacamp.com). O objetivo do agente é descobrir a política ótima, ou seja, escolher em cada estado a ação com o maior valor Q possível.

Em nosso exemplo prático, vamos usar um ambiente simples (um grid ou tabuleiro) e um agente discreto, implementando as etapas acima operacionalmente em JavaScript. Para auxiliar nos cálculos, usaremos a biblioteca TensorFlow.js, uma biblioteca JavaScript para aprendizado de máquina, que permite executar operações numéricas aceleradas (no navegador ou Node.js) (www.tensorflow.org).

O que é Aprendizado por Reforço?

Antes de codificar, vamos entender melhor os conceitos. No Aprendizado por Reforço, o agente e o ambiente formam um laço de feedback:

  1. O agente observa o estado atual do ambiente (por exemplo, sua posição em um grid).
  2. O agente escolhe uma ação (por exemplo, mover para a direita ou esquerda).
  3. O ambiente responde levando o agente a um novo estado e fornecendo uma recompensa numérica.
  4. Com base nessa recompensa, o agente atualiza sua política (estratégia) ou sua estimativa de valor para aquela ação.
  5. Esse ciclo se repete em múltiplos passos dentro de um episódio, e em vários episódios de treino.

Como analogia, pense em ensinar uma criança a andar de bicicleta: no começo, ela tenta várias coisas (calibrar o equilíbrio, acelerar, frear), recebe recompensas (elogios, pedalar sem cair) ou punições (quedas, perda de equilíbrio), e gradualmente aprende quais ações (equilíbrio, pedalar mais) aumentam suas recompensas (sucesso em pedalar). Essa exploração inicial (tentar ações aleatórias) vs. exploração das melhores ações aprendidas é o dilema clássico exploração vs. exploração no RL.

O agente deve explorar ações novas para descobrir recompensas potencialmente maiores, mas também deve explorar (aproveitar) o conhecimento atual, selecionando as ações que já acredita serem boas. Balancear exploração e exploração geralmente é feito com a estratégia epsilon-greedy: o agente escolhe uma ação aleatória com probabilidade ε (exploração), e com probabilidade 1−ε escolhe a melhor ação conhecida (exploração dos valores Q).

Termos principais em Q-Learning

  • Estado(s): Configuração atual do ambiente (por exemplo, a posição do agente no tabuleiro).
  • Ação(a): Movimento ou decisão executada pelo agente em um estado.
  • Recompensa: Feedback numérico recebido após cada ação. Pode ser positivo (indica uma boa ação) ou negativo (mau resultado).
  • Tabela Q: Estrutura (por exemplo, matriz ou tabela) onde mantemos o valor estimado Q(s,a) para cada par estado-ação.
  • Taxa de aprendizado (α): Hiperparâmetro que controla o quanto atualizamos os valores Q após cada experiência.
  • Fator de desconto (γ): Define a importância de recompensas futuras. Valores entre 0 e 1.
  • Epsilon (ε): Probabilidade de escolher ações aleatórias para explorar o ambiente.
  • Episódios: Uma "partida" completa no ambiente, que termina quando se atinge um estado final (meta ou falha).

Com esses conceitos, podemos partir para a implementação prática. Usaremos JavaScript como linguagem de programação e TensorFlow.js para facilitar cálculos (embora nosso exemplo principal use uma tabela Q simples, mostraremos como incluir a biblioteca).

TensorFlow.js e Configuração do Ambiente

TensorFlow.js é uma biblioteca de aprendizado de máquina em JavaScript que permite criar e treinar modelos de IA diretamente no navegador ou em Node.js (www.tensorflow.org). Embora nosso foco seja uma solução tradicional de Q-Learning (que não precisa de rede neural), usaremos TF.js para demonstrar sua integração e acelerar eventuais cálculos matriciais.

Para começar, precisamos incluir a biblioteca em nosso projeto. Podemos usar o Node.js ou um ambiente de navegador. Abaixo temos exemplos de configuração:

  • Node.js: instale a biblioteca via npm e importe no código JavaScript:

    npm install @tensorflow/tfjs # ou @tensorflow/tfjs-node para backend otimizado
    // Importa o TensorFlow.js em Node.js const tf = require('@tensorflow/tfjs'); tf.setBackend('cpu'); // Ou 'tensorflow' se estiver usando tfjs-node-gpu
  • Navegador (Web): inclua o script direto em um arquivo HTML:

    <!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script> </head> <body> <script src="q_learning_agent.js"></script> </body> </html>

Depois de configurar o ambiente, definiremos nosso jogo ou desafio de decisão. Para ilustrar, vamos criar um ambiente em grid (por exemplo, um tabuleiro 5x5) onde o agente deve encontrar um caminho até um ponto de destino. Podemos ter:

  • Estados: Cada célula do grid, numeradas de forma única ou usando coordenadas (linha, coluna).
  • Ações possíveis: mover para cima, baixo, esquerda ou direita (respeitando limites do grid).
  • Recompensas: Ao atingir a posição final (meta), o agente recebe uma grande recompensa (por exemplo, +10). Em outras células intermediárias, a recompensa pode ser -1 por movimento (para incentivar caminhos curtos) e possivelmente penalidades (por exemplo, -5 ao cair em um obstáculo).
  • Estado inicial: Podemos definir o agente começando, por exemplo, no canto superior esquerdo (0,0).
  • Estados terminais: O agente chega ao destino ou após um número máximo de passos (fim do episódio).

Em código JavaScript, podemos representar o ambiente de várias formas. Uma forma simples é usar arrays para o grid e funções auxiliares. Por exemplo:

// Parâmetros do ambiente const numRows = 5; const numCols = 5; const startState = [0, 0]; const goalState = [4, 4]; // Gera recompensas: -1 por movimento, +10 no objetivo function getReward(state) { const [row, col] = state; if (row === goalState[0] && col === goalState[1]) { return 10; // Recompensa do objetivo } else { return -1; // Custo ou incentivo para terminar logo } } // Transiciona para um novo estado baseado na ação function nextState(state, action) { let [row, col] = state; if (action === 'up') row = Math.max(row - 1, 0); if (action === 'down') row = Math.min(row + 1, numRows - 1); if (action === 'left') col = Math.max(col - 1, 0); if (action === 'right') col = Math.min(col + 1, numCols - 1); return [row, col]; }

Nesse exemplo, temos funções auxiliares para obter a próxima posição (nextState) e a recompensa associada (getReward). Não há modelagem explícita de obstáculos ou estados inválidos, mas isso poderia ser adicionado (por exemplo, retornando uma recompensa negativa grande se o agente tentar entrar num "obstáculo" ou se sair dos limites).

Agora que descrevemos o ambiente, vamos focar na construção do agente Q-Learning propriamente dito.

Construindo o Agente Q-Learning

O coração do Q-Learning é a atualização dos valores Q. O agente mantém uma Tabela Q que mapeia cada par estado-ação para um valor. No nosso grid de 5x5 com 4 ações possíveis, a tabela Q terá 25 linhas (estados) e 4 colunas (ações). Inicialmente, podemos começar todos os valores Q em zero, indicando que ainda não sabemos o valor de cada ação.

Inicializando a Tabela Q e Hiperparâmetros

Antes de tudo, definimos os hiperparâmetros principais do algoritmo:

  • Taxa de aprendizado (α, alpha): velocidade de atualização, por exemplo 0.1.
  • Fator de desconto (γ, gamma): entre 0 e 1, por exemplo 0.95, que define quanto das recompensas futuras são consideradas nas decisões.
  • Exploração (ε, epsilon): chance inicial de cabine uma ação aleatória (por exemplo, 1.0, ou 100% inicial), que depois pode decair até um mínimo (por exemplo, 0.01).
  • Número de episódios: quantas vezes o agente resetará e tentará alcançar o objetivo (por exemplo, 500 episódios).
  • Número de estados: no caso do grid 5x5, 25 estados diferentes.
  • Número de ações: 4 ações possíveis (cima, baixo, esquerda, direita).

Em código, inicializamos a tabela Q e esses parâmetros. Uma forma em JavaScript é:

const numStates = numRows * numCols; const numActions = 4; // ['up', 'down', 'left', 'right'] // Criando a Tabela Q (todos os valores iniciados em 0) const qTable = Array.from({ length: numStates }, () => Array(numActions).fill(0) ); // Hiperparâmetros let alpha = 0.1; // taxa de aprendizado let gamma = 0.95; // fator de desconto let epsilon = 1.0; // exploração inicial const epsilonMin = 0.01; const epsilonDecay = 0.995; // decaimento de epsilon por episódio const numEpisodes = 500; // total de episódios de treino

Aqui usamos Array.from para criar uma matriz 2D de zeros, onde cada linha representa um estado (ex.: estado indexado de 0 a 24) e cada coluna uma ação.

Estratégia Epsilon-Greedy

Para escolher ações durante o treinamento, o agente seguirá a estratégia epsilon-greedy. Em resumo:

  • Com probabilidade ε (epsilon), o agente explora: escolhe uma ação aleatória uniformemente.
  • Com probabilidade 1 − ε, o agente exploita: escolhe a melhor ação conhecida (aquela que tem o maior valor Q para o estado atual).

Conforme o treinamento progride, vamos reduzindo ε (decay), de forma que o agente comece muito exploratório e vá se tornando mais explorador à medida que aprende sobre o ambiente. Por exemplo, definimos um epsilonDecay = 0.995 e no final de cada episódio fazemos:

epsilon = Math.max(epsilonMin, epsilon * epsilonDecay);

Para implementar a seleção de ação:

const actions = ['up', 'down', 'left', 'right']; function chooseAction(stateIndex) { if (Math.random() < epsilon) { // Explorar: escolhe ação aleatória return Math.floor(Math.random() * numActions); } else { // Exploitar: escolhe ação com maior valor Q no estado atual const qs = qTable[stateIndex]; const maxQ = Math.max(...qs); // Em caso de empate, pega o primeiro. Poderíamos randomizar entre os maiores. return qs.indexOf(maxQ); } }

Explicação do código:

  • stateIndex é um número de 0 a 24 que representará a linha da tabela Q correspondente. Se necessário, você pode converter entre coordenadas (linha, coluna) e índice usando, por exemplo, estado = row * numCols + col.
  • Com probabilidade epsilon, usa-se Math.random() para decidir exploração.
  • Se não exploramos, buscamos o maior valor em qTable[stateIndex] (vetor de valores Q para as ações) e retornamos o índice daquela ação.
  • Math.max(...qs) encontra o maior Q, e indexOf retorna o índice da primeira ocorrência.

Atualização dos Valores Q (Equação de Bellman)

A atualização central do Q-Learning segue a fórmula de Bellman:

Q(s,a) ← Q(s,a) + α * [ r + γ * max_a' Q(s', a') − Q(s,a) ]

Onde:

  • s é o estado atual,
  • a é a ação tomada,
  • r é a recompensa recebida,
  • s’ é o próximo estado após a ação,
  • α é a taxa de aprendizado,
  • γ é o fator de desconto,
  • max_a' Q(s’, a’) é o valor máximo de Q entre todas ações possíveis em s’.

Em código JavaScript, dado um estado, ação e transição para o próximo estado com recompensa, fazemos:

function updateQ(stateIndex, actionIndex, reward, nextStateIndex) { const qPredict = qTable[stateIndex][actionIndex]; const qNext = qTable[nextStateIndex]; const maxFutureQ = Math.max(...qNext); const qTarget = reward + gamma * maxFutureQ; // Atualiza Q usando a fórmula qTable[stateIndex][actionIndex] = qPredict + alpha * (qTarget - qPredict); }

Explicando:

  • qPredict é o valor Q atual de (s,a).
  • maxFutureQ é o melhor valor Q esperado a partir do próximo estado s’.
  • Calculamos qTarget = r + γ * maxFutureQ.
  • Atualizamos qTable[s][a] somando uma fração (α) da diferença temporal (qTarget - qPredict).

Esse ajuste faz com que valores Q cresçam se a recompensa + futuro esperado for maior do que o atual.

Loop de Treinamento do Agente

Com a seleção de ação e atualização definidas, vamos colocar tudo junto no loop de treinamento. Para cada episódio de 0 até numEpisodes, fazemos:

  1. Iniciar o estado do agente no estado inicial (por exemplo, state = startState e calcular seu índice stateIndex = 0*5 + 0).

  2. Repetir passos até atingir um estado terminal (por exemplo, alcançar goalState):

    • Escolher uma ação indexada (actionIndex) com a função chooseAction(stateIndex).
    • Calcular o próximo estado físico: nextState = nextState(state, actions[actionIndex]).
    • Calcular a recompensa: reward = getReward(nextState).
    • Determinar o índice de próximo estado: nextIndex = nextState[0]*numCols + nextState[1].
    • Atualizar a tabela Q: updateQ(stateIndex, actionIndex, reward, nextIndex).
    • Passar para o próximo estado: stateIndex = nextIndex.
    • Checar se nextState é terminal (por exemplo, se chegou ao objetivo, então fim do episódio).
  3. Depois de terminar um episódio, ajustar epsilon para reduzir exploração futura.

Exemplo de código do loop principal:

for (let episode = 1; episode <= numEpisodes; episode++) { let [row, col] = startState; let stateIndex = row * numCols + col; let totalReward = 0; let done = false; // Por segurança, poderíamos limitar o número de passos por episódio for (let step = 0; step < 1000; step++) { // Escolhe ação (índice 0..3) const actionIndex = chooseAction(stateIndex); const action = actions[actionIndex]; // Executa ação no ambiente const newState = nextState([row, col], action); const reward = getReward(newState); const nextIndex = newState[0] * numCols + newState[1]; // Atualiza Q para (stateIndex, actionIndex) updateQ(stateIndex, actionIndex, reward, nextIndex); // Atualiza estado atual [row, col] = newState; stateIndex = nextIndex; totalReward += reward; // Verifica fim de episódio (atingiu objetivo) if (newState[0] === goalState[0] && newState[1] === goalState[1]) { console.log(`Episódio ${episode} finalizado em ${step+1} passos. Recompensa total: ${totalReward}.`); done = true; break; } } // Decai a taxa de exploração if (epsilon > epsilonMin) { epsilon *= epsilonDecay; } // (Opcional) Relatório periódico de desempenho if (episode % 100 === 0) { console.log(`Após ${episode} episódios, epsilon = ${epsilon.toFixed(2)}`); } }

Esse loop realiza o treino completo. Algumas observações:

  • O laço interno limita a 1000 passos (por segurança), mas normalmente terminamos mais cedo ao alcançar o objetivo.
  • Acumulamos totalReward para ter um histórico de desempenho do agente.
  • No final de cada episódio, decrescemos epsilon para reduzir gradualmente a exploração.
  • Podemos imprimir logs periódicos para acompanhar (por exemplo, a cada 100 episódios).

Ao treinar, o agente vai atualizando sua Tabela Q. Inicialmente, como tudo é desconhecido, ele explora mais e vai acumulando recompensas baixas (muitas punições -1 ou sem alcançar a meta). Com o tempo, ele descobre caminhos que levem ao objetivo e ajusta os valores Q das ações desses estados para valores mais altos (por exemplo, uma ação que eventualmente leva ao objetivo e acumula +10). Assim, a política implícita do agente (o que ele escolhe no futuro) torna-se cada vez melhor para alcançar recompensas máximas.

Exemplos Práticos de Código

Para ilustrar, vejamos alguns trechos-chave do código completo:

  • Inicialização da Tabela Q:

    const qTable = Array.from({ length: numStates }, () => Array(numActions).fill(0) );

    Aqui criamos uma matriz 2D preenchida com zeros. Cada linha corresponde a um estado, cada coluna a uma ação.

  • Seleção epsilon-greedy:

    function chooseAction(stateIndex) { if (Math.random() < epsilon) { // Explora: ação aleatória return Math.floor(Math.random() * numActions); } else { // Exploita: melhor ação atual const qs = qTable[stateIndex]; const maxQ = Math.max(...qs); return qs.indexOf(maxQ); } }

    Esse código alterna entre exploração (qualquer ação aleatória) e exploração (melhor ação segundo Q).

  • Função de atualização Q:

    function updateQ(stateIndex, actionIndex, reward, nextStateIndex) { const qPredict = qTable[stateIndex][actionIndex]; const maxFutureQ = Math.max(...qTable[nextStateIndex]); const qTarget = reward + gamma * maxFutureQ; qTable[stateIndex][actionIndex] = qPredict + alpha * (qTarget - qPredict); }

    A equação de Bellman em prática: incrementamos o valor Q atual em uma fração da diferença para o novo alvo (qTarget).

  • Simulação de um episódio:

    let stateIndex = 0; // estado inicial (0,0) no exemplo do grid 5x5 let totalReward = 0; for (let t = 0; t < 1000; t++) { const actionIndex = chooseAction(stateIndex); const [row, col] = [Math.floor(stateIndex/numCols), stateIndex % numCols]; const newState = nextState([row, col], actions[actionIndex]); const reward = getReward(newState); const nextIndex = newState[0]*numCols + newState[1]; updateQ(stateIndex, actionIndex, reward, nextIndex); stateIndex = nextIndex; totalReward += reward; if (newState[0] === goalState[0] && newState[1] === goalState[1]) { break; // alcançou objetivo } }

    Nesse loop interno, o agente executa ações até chegar à meta. Repetimos isso várias vezes em novos episódios, sempre iniciando do estado inicial.

Após completar o treinamento, a tabela Q terá valores aprendidos. Por exemplo, poderia ficar algo assim (ilustrativo):

EstadoSubirDescerEsquerdaDireita
(0,0)-1-1-15
(0,1)-12010
...............

Nesse exemplo fictício, no estado (0,0) a ação Direita (5) tem o maior valor Q (sinalizando que mover para a direita leva a caminho melhor), enquanto outras ações têm valor baixo. No estado (0,1), talvez Direita (10) é ideal, enquanto Descer (2) também pode ser razoável.

Conclusão

Neste artigo vimos como implementar um agente Q-Learning do zero em JavaScript, usando o TensorFlow.js para potencialmente acelerar operações e mantê-lo preparado para extensões com redes neurais. Recapitulando os pontos principais:

  • Aprendizado por Reforço (RL) é um paradigma no qual um agente aprende ações ótimas por tentativa e erro, interagindo com um ambiente e recebendo recompensas (www.datacamp.com). É comparável a como crianças ou animais aprendem novas tarefas por feedback (elogiando acertos e corrigindo erros) (www.dqdigitals.com).
  • Q-Learning é um algoritmo de RL off-policy baseado em valores. Ele mantém uma tabela Q com valores de qualidade para cada par (estado, ação) (www.datacamp.com). A cada experiência, a tabela é atualizada pela fórmula de Bellman para refletir novas descobertas (recompensas) no ambiente.
  • TensorFlow.js permite integrar modelos de aprendizado de máquina em projetos JavaScript (web ou Node.js) (www.tensorflow.org). No nosso exemplo, usamos TF.js principalmente para mostrar como importar/biblioteca, mas mantivemos o Q-Learning tradicional. Numa versão avançada, poderíamos substituir a tabela Q por uma rede neural (Deep Q-Network) treinada com TF.js para ambientes maiores ou contínuos.
  • Implementamos um exemplo prático: um agente em um grid aprendendo a alcançar um objetivo. Definimos estados, ações, recompensas, executamos episódios de treino e observamos a métrica de recompensa total. Com o tempo, o agente aprendeu a navegar de forma eficiente até o destino.

Como próximos passos, você pode:

  • Adicionar obstáculos ao ambiente e penalidades maiores.
  • Experimentar diferentes hiperparâmetros (α, γ, ε) para ver o impacto na aprendizagem.
  • Explorar Deep Q-Learning (DQN): usar o TensorFlow.js para criar uma rede neural que aproxima a função Q, permitindo resolver problemas com muitos estados ou estados contínuos.
  • Integrar o agente em uma interface visual (canvas ou jogos simples) para observar diretamente o comportamento aprendido.

O aprendizado por reforço abre portas para muitos desafios interativos e jogos. Com os fundamentos adquiridos aqui, você está pronto para explorar implementações mais avançadas em JavaScript e aproveitar toda a flexibilidade do TensorFlow.js para levar seu agente a novos patamares de inteligência.

Boa sorte em sua jornada de aprendizado por reforço!

Quer aprender mais sobre Programação?

Você acabou de ganhar 1 hora com a minha equipe para uma call exclusiva! Vamos entender o seu momento e te mostrar o caminho para se tornar um programador de sucesso. Clique no botão abaixo e agende agora mesmo!