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: Criando o wrapper do 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. Para isso, criaremos um wrapper no ambiente original e então vamos introduzir as modificações necessárias.

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

Crie um arquivo chamado safedriving_wrapper.py, e adicione o seguinte código:

import gymnasium as gym
from shapely import affinity
from shapely.geometry import Point, Polygon


class SafeDrivingWrapper(gym.Wrapper):
    """
    A wrapper to create a 'safe zone' around the track and penalize the agent
    for going too far off-road.
    """
    def __init__(self, env, border_width=0.5):
        super(SafeDrivingWrapper, self).__init__(env)
        self.border_width = border_width

    def car_on_track(self):
        car_on_track = False
        x, y = self.unwrapped.car.hull.position
        point = Point(x, y)
        for poly in self.unwrapped.road_poly:
            polygon = Polygon(poly[0])
            # Create a larger polygon representing the track + safe border
            if self.border_width > 0:
                border_scale = 1 + self.border_width
                polygon = affinity.scale(polygon, xfact=border_scale, yfact=border_scale)

            if polygon.contains(point):
                car_on_track = True
                break
        return car_on_track

    def step(self, action):
        next_state, reward, terminated, truncated, info = self.env.step(action)

        # Apply a heavy penalty if the car is outside the safe zone
        if not self.car_on_track():
            reward -= 100
            terminated = True # End the episode immediately

        return next_state, reward, terminated, truncated, info

Este wrapper é responsável por criar uma espécie de cerca de segurança ao redor da pista. Desta forma, se o carro se afastar demais, podemos reiniciar a simulação, e agilizar o aprendizado.

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 safedriving_wrapper import SafeDrivingWrapper
from tensorflow.keras import layers

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

# 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
  • 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. Em seguinda, instanciamos o SafeDrivingWrapper utilizando o ambiente criado.
  • 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.
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):
        """Builds the CNN model."""
        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') # Q-values
        ])
        return model

    def update_target_model(self):
        """Copies weights from the main model to the target model."""
        self.target_model.set_weights(self.model.get_weights())

    def act(self, state):
        """Chooses an action using an epsilon-greedy policy."""
        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):
        """Stores an experience in the replay buffer."""
        self.memory.append((state, action, reward, next_state, done))

    def replay(self):
        """Trains the model using a random batch of experiences from memory."""
        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)

        # Predict Q-values for current states and next states
        q_values_current = self.predict(self.model, states).numpy()
        q_values_next = self.predict(self.target_model, next_states).numpy()

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

        self.train_step(states, q_values_current)

        # Decay epsilon
        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.update_target_model()
                self.epsilon = EPSILON_MIN
                print("Pre-trained weights found. Resuming training...")
            except FileNotFoundError:
                print("No pre-trained weights found. Starting from scratch...")

    @tf.function
    def predict(self, model, states):
        return model(states, training=False)

    @tf.function
    def train_step(self, states, targets):
        with tf.GradientTape() as tape:
            q_values = self.model(states, training=True)
            loss = tf.keras.losses.MeanSquaredError()(targets, q_values)
        grads = tape.gradient(loss, self.model.trainable_variables)
        self.optimizer.apply_gradients(zip(grads, self.model.trainable_variables))
  • 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.
def preprocess_state(state):
    """Converts the image to grayscale, resizes, and normalizes it."""
    # [Nota do revisor: A conversão para TF tensor é mais eficiente]
    state_tensor = tf.convert_to_tensor(state)
    state_tensor = tf.image.rgb_to_grayscale(state_tensor)
    return tf.cast(state_tensor, tf.float32) / 255.0

def train_agent(episodes=1000):
    agent = DQNAgent()
    agent.load_weights()

    for episode in range(episodes):
        state, _ = env.reset()
        state = preprocess_state(state)

        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)

            # Custom Reward Shaping
            car_on_track = env.car_on_track()
            if car_on_track:
                speed = np.linalg.norm(env.unwrapped.car.hull.linearVelocity)
                # Reward for high speed, penalize for low speed
                speed_bonus = max(0, speed * 0.1)
                low_speed_penalty = -1 if speed < 1.0 else 0
                reward += speed_bonus + low_speed_penalty
            else:
                # This penalty is now handled by the wrapper, but we can add more
                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"Episode {episode+1}, Total Reward: {total_reward:.2f}, Epsilon: {agent.epsilon:.4f}")

# Let's train!
train_agent()
  • 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! 🏆

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

Deixe um comentário