Car Racing com Inteligência Artificial: Um guia completo para iniciantes (e não tão iniciantes) 🚗💨

Se você sempre sonhou em criar um piloto de corrida virtual que detona nas pistas do “CarRacing-v3” do Gymnasium, mas se sentiu como um carro atolado na lama, este post é para você! Domine o Car Racing do Gymnasium hoje mesmo.

Vamos juntos, passo a passo, transformar esse desafio em uma experiência super divertida e recompensadora. Prepare-se para acelerar seu conhecimento em Inteligência Artificial e Reinforcement Learning!

O que é o car racing do gymnasium?

Imagine um videogame onde você precisa dirigir um carro em uma pista gerada aleatoriamente. O objetivo? Completar a volta o mais rápido possível, sem sair da pista (ou pelo menos, não tanto a ponto de ser desclassificado!).

O Gymnasium (antigo OpenAI Gym) é uma biblioteca do Python que oferece diversos ambientes para testar algoritmos de Inteligência Artificial. O “CarRacing-v3” é um desses ambientes, e é um desafio clássico no mundo do Reinforcement Learning.

Por que o car racing é tão legal (e desafiador)?

  • Controle Contínuo vs. Discreto: No nosso caso, vamos usar o modo discreto, onde o carro tem um número limitado de ações (acelerar, frear, virar à esquerda, virar à direita, etc.). Isso simplifica um pouco o problema, mas ainda é um baita desafio!
  • Recompensas: O carro recebe recompensas por andar na pista e penalidades por sair dela. O objetivo do algoritmo é aprender a maximizar essas recompensas.
  • Visão: O algoritmo “vê” o mundo através de uma imagem da tela do jogo. Ele precisa aprender a interpretar essa imagem para tomar as melhores decisões.

Preparando o terreno: Modificando o ambiente do car racing

Aqui, vamos dar uma “turbinada” no ambiente original para facilitar o aprendizado do nosso piloto virtual. A ideia é criar uma “zona de segurança” ao redor da pista, para que o carro não fique andando sem rumo pelo mapa.

Instalando as bibliotecas

Primeiro, precisamos as bibliotecas necessárias e de uma biblioteca chamada shapely para trabalhar com geometria. Abra seu terminal e mande bala:

pip install numpy tensorflow shapely "gymnasium[all]"

Ajustando o ambiente

No arquivo car_racing.py do Gymnasium, adicione as seguintes linhas no início:

from shapely.geometry import Point
from shapely.geometry.polygon import Polygon
from shapely import affinity

Criando o método car_on_track

Este método vai verificar se o carro está na pista e dentro da nossa “zona de segurança”. Adicione o seguinte código à classe CarRacing:

    def car_on_track(self, border_width=0):
        car_in_track  = False
        x, y = self.car.hull.position
        point = Point(x, y)
        for poly in self.road_poly:
            polygon = Polygon(poly[0])

            if border_width > 0:
                border = 1 + border_width
                polygon = affinity.scale(polygon, xfact=border, yfact=border)

            if polygon.contains(point):
                car_in_track = True
                break

        return car_in_track

Modificando o Método step

Agora, vamos usar este método para ajustar as recompensas e as condições de término do episódio. Modifique o método step da seguinte forma:

# ... código anterior
        if action is not None:  # First step without action, called from reset()
            self.reward -= 0.1
            # We actually don't want to count fuel spent, we want car to be faster.
            # self.reward -=  10 * self.car.fuel_spent / ENGINE_POWER
            self.car.fuel_spent = 0.0
            step_reward = self.reward - self.prev_reward
            self.prev_reward = self.reward

            if not self.car_on_track(0.5):
                step_reward  -= 100
                terminated = True
                
                info["lap_finished"] = False

            x, y = self.car.hull.position
            if self.tile_visited_count == len(self.track) or self.new_lap:
                # Termination due to finishing lap
                terminated = True
                info["lap_finished"] = True
            if abs(x) > PLAYFIELD or abs(y) > PLAYFIELD:
                terminated = True
                info["lap_finished"] = False
                step_reward = -100
# restante do código...

Com essas alterações, podemos configurar o tamanho da zona de segurança e, se o carro sair dessa zona ao redor da pista, o episódio é reiniciado. No exemplo acima, estou utilizando 0.5 como valor da zona de segurança, e isso irá criar uma zona de metade do tamanho da pista para cada lado.

Construindo o piloto de IA: O algoritmo DQN

Agora que o ambiente está preparado, vamos construir nosso piloto virtual usando o algoritmo DQN (Deep Q-Network).

O Que é o DQN?

O DQN é um algoritmo de Reinforcement Learning que usa uma rede neural para aprender a melhor ação a ser tomada em cada situação. Ele funciona da seguinte forma:

  1. Observa o estado: O algoritmo recebe uma imagem da tela do jogo (o estado).
  2. Prevê os valores Q: A rede neural prevê os valores Q para cada ação possível. O valor Q representa a recompensa esperada ao tomar essa ação no estado atual.
  3. Escolhe a ação: O algoritmo escolhe a ação com o maior valor Q previsto (com uma pitada de aleatoriedade para explorar novas possibilidades).
  4. Recebe a recompensa: O algoritmo executa a ação no ambiente e recebe uma recompensa.
  5. Atualiza a rede neural: O algoritmo usa a recompensa recebida para ajustar os pesos da rede neural, de forma a prever valores Q mais precisos no futuro.

Mão na massa: código passo a passo

import random
from collections import deque

import gymnasium as gym
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers

# Configuração do ambiente
env = gym.make("CarRacing-v3", continuous=False, render_mode="human")

# Hiperparâmetros
STATE_SHAPE = (96, 96, 1)
ACTION_SIZE = env.action_space.n
MEMORY_SIZE = 10000
BATCH_SIZE = 32
GAMMA = 0.99
ALPHA = 0.00025
EPSILON = 1.0
EPSILON_DECAY = 0.99999
EPSILON_MIN = 0.01
TARGET_UPDATE = 25
WEIGHTS_FILE = "car_racing.weights.h5"
SAVE_WEIGHTS = False
LOAD_EXISTING_WEIGHTS = True
SAVE_WEIGHTS_INTERVAL = 100
Ver mais
  • Importando as bibliotecas: Aqui, importamos as bibliotecas necessárias para o nosso algoritmo, como gymnasium para o ambiente, numpy para manipulação de arrays, tensorflow e keras para construir a rede neural.
  • Configurando o ambiente: Criamos o ambiente “CarRacing-v3” com o modo discreto e renderização visual.
  • Hiperparâmetros: Essa é a “receita” do nosso algoritmo. Vamos detalhar cada um deles:
    • STATE_SHAPE: O formato da imagem que o algoritmo recebe (96×96 pixels em escala de cinza).
    • ACTION_SIZE: O número de ações possíveis (quantas opções de controle o carro tem).
    • MEMORY_SIZE: O tamanho da memória do algoritmo, onde ele armazena as experiências passadas.
    • BATCH_SIZE: O número de experiências usadas para atualizar a rede neural a cada passo.
    • GAMMA: O fator de desconto, que determina a importância das recompensas futuras.
    • ALPHA: A taxa de aprendizado, que controla a velocidade com que a rede neural se ajusta.
    • EPSILON: A taxa de exploração, que determina a probabilidade de o algoritmo escolher uma ação aleatória em vez da melhor ação prevista. Isso é importante para o algoritmo explorar novas possibilidades e não ficar preso em soluções subótimas.
    • EPSILON_DECAY: A taxa de decaimento do épsilon, que diminui a taxa de exploração ao longo do tempo.
    • EPSILON_MIN: O valor mínimo do épsilon.
    • TARGET_UPDATE: A frequência com que o modelo alvo é atualizado.
    • WEIGHTS_FILE: O nome do arquivo onde os pesos da rede neural são salvos.
    • SAVE_WEIGHTS: Se os pesos devem ser salvos.
    • LOAD_EXISTING_WEIGHTS: Se os pesos pré-treinados devem ser carregados.
    • SAVE_WEIGHTS_INTERVAL: A frequência com que os pesos são salvos.
def preprocess_state(state):
    """ Converte a imagem para escala de cinza e normaliza."""
    state = tf.image.rgb_to_grayscale(state)
    state = tf.image.resize(state, (96, 96))
    return state / 255.0
  • preprocess_state: Essa função pega a imagem da tela do jogo e a transforma em um formato que a rede neural consegue entender. Ela converte a imagem para escala de cinza, redimensiona para 96×96 pixels e normaliza os valores dos pixels para ficarem entre 0 e 1.
class DQNAgent:
    def __init__(self):
        self.memory = deque(maxlen=MEMORY_SIZE)
        self.epsilon = EPSILON
        self.model = self.build_model()
        self.target_model = self.build_model()
        self.update_target_model()
        self.optimizer = tf.keras.optimizers.Adam(learning_rate=ALPHA)

    def build_model(self):
        """ Cria a rede neural."""
        model = tf.keras.Sequential([
            layers.Input(STATE_SHAPE),
            layers.Conv2D(32, (8, 8), strides=4, activation='relu'),
            layers.Conv2D(64, (4, 4), strides=2, activation='relu'),
            layers.Conv2D(64, (3, 3), strides=1, activation='relu'),
            layers.Flatten(),
            layers.Dense(512, activation='relu'),
            layers.Dense(ACTION_SIZE, activation='linear')
        ])
        return model

    def update_target_model(self):
        """ Copia os pesos do modelo principal para o modelo alvo."""
        self.target_model.set_weights(self.model.get_weights())

    def act(self, state):
        """ Escolhe uma ação baseada na política epsilon-greedy."""
        if np.random.rand() <= self.epsilon:
            return random.randrange(ACTION_SIZE)
        q_values = self.predict(self.model, tf.expand_dims(state, axis=0))
        return np.argmax(q_values[0].numpy())

    def remember(self, state, action, reward, next_state, done):
        """ Armazena a experiência no replay buffer."""
        self.memory.append((state, action, reward, next_state, done))

    def replay(self):
        """ Treina o modelo com amostras da memória."""
        if len(self.memory) < BATCH_SIZE:
            return
        batch = random.sample(self.memory, BATCH_SIZE)
        states, actions, rewards, next_states, dones = zip(*batch)
        states = np.array(states)
        next_states = np.array(next_states)

        q_values = self.predict(self.model, states).numpy()
        next_q_values = self.predict(self.target_model, next_states).numpy()

        for i in range(BATCH_SIZE):
            if dones[i]:
                q_values[i, actions[i]] = rewards[i]
            else:
                q_values[i, actions[i]] = rewards[i] + GAMMA * np.amax(next_q_values[i])

        self.train_step(states, q_values)
        
        if self.epsilon > EPSILON_MIN:
            self.epsilon *= EPSILON_DECAY

    def save_weights(self):
        if SAVE_WEIGHTS:
            self.model.save_weights(WEIGHTS_FILE)

    def load_weights(self):
        if LOAD_EXISTING_WEIGHTS:
            try:
                self.model.load_weights(WEIGHTS_FILE)
                self.target_model.load_weights(WEIGHTS_FILE)
                self.epsilon = EPSILON_MIN
                print("Arquivo de pré-treino encontrado, continuando treinamento...")
            except FileNotFoundError:
                print("Arquivo de pré-treino não encontrado, iniciando treinamento...")

    @tf.function
    def predict(self, model, states):
        """ Faz a previsão dos valores Q para múltiplos estados."""
        return model(states, training=False)

    @tf.function
    def train_step(self, states, targets):
        """ Passo de treinamento otimizado com @tf.function."""
        with tf.GradientTape() as tape:
            q_values = self.model(states, training=True)
            loss = tf.keras.losses.MSE(targets, q_values)
        grads = tape.gradient(loss, self.model.trainable_variables)
        self.optimizer.apply_gradients(zip(grads, self.model.trainable_variables))
Ver mais
  • DQNAgent: Essa classe representa o nosso piloto virtual. Ela contém a rede neural, a memória e os métodos para escolher ações, armazenar experiências e treinar a rede.
    • __init__: O construtor da classe. Ele inicializa a memória, a taxa de exploração (epsilon), o modelo principal, o modelo alvo e o otimizador.
    • build_model: Essa função cria a rede neural. A rede é composta por camadas convolucionais, que são ótimas para processar imagens, e camadas densas, que são usadas para fazer a previsão final dos valores Q.
    • update_target_model: Essa função copia os pesos do modelo principal para o modelo alvo. O modelo alvo é usado para calcular os valores Q de destino durante o treinamento. Isso ajuda a estabilizar o treinamento e evitar que o algoritmo fique instável.
    • act: Essa função escolhe uma ação com base na política epsilon-greedy. Com probabilidade epsilon, a função escolhe uma ação aleatória. Caso contrário, ela escolhe a ação com o maior valor Q previsto pelo modelo.
    • remember: Essa função armazena a experiência na memória. A experiência é composta pelo estado atual, a ação escolhida, a recompensa recebida, o próximo estado e se o episódio terminou.
    • replay: Essa função treina o modelo com amostras da memória. A função seleciona aleatoriamente um lote de experiências da memória, calcula os valores Q de destino e atualiza os pesos do modelo para que ele preveja valores Q mais precisos no futuro.
    • save_weights: Essa função salva os pesos do modelo em um arquivo.
    • load_weights: Essa função carrega os pesos do modelo de um arquivo.
    • predict: Essa função faz a previsão dos valores Q para múltiplos estados.
    • train_step: Essa função realiza um passo de treinamento otimizado com @tf.function.
# Treinamento
def train_agent(episodes=1000):
    agent = DQNAgent()
    agent.load_weights()

    for episode in range(episodes):
        state, _ = env.reset()
        state = preprocess_state(state)
        real_reward = 0
        total_reward = 0
        done = False

        while not done:
            action = agent.act(state)
            next_state, reward, done, _, _ = env.step(action)
            next_state = preprocess_state(next_state)

            real_reward += reward

            # Modificação da recompensa
            car_on_track = env.unwrapped.car_on_track()
            if car_on_track:
                speed = np.linalg.norm(env.unwrapped.car.hull.linearVelocity)
                speed_bonus = speed * 0.1
                low_speed_penalty = -2 if speed < 2 else 0
                reward += speed_bonus + low_speed_penalty
            else:
                reward -= 2

            agent.remember(state, action, reward, next_state, done)
            state = next_state
            total_reward += reward
            agent.replay()

        if episode % TARGET_UPDATE == 0:
            agent.update_target_model()

        if episode > 0 and episode % SAVE_WEIGHTS_INTERVAL == 0:
            agent.save_weights()

        print(f"Episódio {episode+1}, Recompensa: {total_reward}, Recompensa real: {real_reward}, Epsilon: {agent.epsilon:.2f}")

train_agent()
Ver mais
  • train_agent: Essa função é responsável por treinar o nosso piloto virtual. Ela cria um objeto DQNAgent, carrega os pesos pré-treinados (se existirem) e executa o treinamento por um número determinado de episódios.
    • Para cada episódio, a função reseta o ambiente, preprocessa o estado inicial e entra em um loop que executa até o episódio terminar.
    • Dentro do loop, a função escolhe uma ação com base na política épsilon-greedy, executa a ação no ambiente, preprocessa o próximo estado, calcula a recompensa e armazena a experiência na memória.
    • A função também atualiza os pesos do modelo a cada passo, usando a função replay.
    • A cada TARGET_UPDATE episódios, a função atualiza o modelo alvo.
    • A cada SAVE_WEIGHTS_INTERVAL episódios, a função salva os pesos do modelo em um arquivo.
    • No final de cada episódio, a função imprime informações sobre o progresso do treinamento, como o número do episódio, a recompensa total e a taxa de exploração.

Para uma aprendizado mais rápido, eu modifiquei a recompensa do ambiente padrão do Gym para favorecer a velocidade do veículo se ele estiver na pista (usando o método car_on_track criado antes), e aplicar uma penalidade caso o veículo não esteja na pista.

Desta forma, o algoritmo aprende que é melhor andar na pista e ir mais rápido, aumentando a pontuação.

Execute o código e acompanhe o aprendizado do algoritmo em tempo real. Nos meus testes, por volta da época 500 já é possível notar que o carro busca permanecer na pista, e corre um pouco mais rápido. No repositódio do Github, você pode encontrar o código fonte completo e o arquivo com os pesos da rede pré treinada caso não queira passar por toda a etapa de treinamento novamente.

A rede neural

A rede neural é o cérebro do nosso piloto virtual. Ela recebe a imagem da tela do jogo como entrada e produz os valores Q para cada ação possível como saída.

A rede é composta por:

  • Camadas Convolucionais: Essas camadas são responsáveis por extrair características importantes da imagem, como as bordas da pista e a posição do carro.
  • Camadas Densas: Essas camadas são responsáveis por combinar as características extraídas pelas camadas convolucionais e fazer a previsão final dos valores Q.

O processo de treinamento

O processo de treinamento é onde o nosso piloto virtual aprende a dirigir. Ele funciona da seguinte forma:

  1. Exploração: No início do treinamento, o piloto explora o ambiente, experimentando diferentes ações e aprendendo o que funciona e o que não funciona.
  2. Exploitation: À medida que o treinamento avança, o piloto começa a explorar o ambiente com mais inteligência, escolhendo ações que maximizem a recompensa esperada.
  3. Convergência: Eventualmente, o piloto converge para uma política ótima, onde ele sabe exatamente qual ação tomar em cada situação para maximizar a recompensa.

Dicas extras para turbinar seu piloto

  • Ajuste os Hiperparâmetros: Experimente diferentes valores para os hiperparâmetros para ver o que funciona melhor para o seu caso.
  • Recompensa: Ajuste a função de recompensa para incentivar o comportamento desejado.
  • Arquitetura da Rede: Experimente diferentes arquiteturas de rede neural.
  • Aumente o Tempo de Treinamento: Quanto mais tempo você treinar o seu piloto, melhor ele vai ficar.

Conclusão

E aí, curtiu o guia completo para dominar o Car Racing do Gymnasium com Inteligência Artificial?

Agora é a sua vez de colocar a mão na massa e criar o seu próprio piloto virtual!

Compartilhe este post com seus amigos que também curtem programação e Inteligência Artificial. E não se esqueça de deixar um comentário aqui embaixo com suas dúvidas, sugestões e resultados!

🚀 Vamos juntos acelerar o aprendizado e dominar o mundo da Inteligência Artificial! 🏆

1 comentário em “Car Racing com Inteligência Artificial: Um guia completo para iniciantes (e não tão iniciantes) 🚗💨”

Deixe um comentário