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:
- Observa o estado: O algoritmo recebe uma imagem da tela do jogo (o estado).
- 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.
- 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).
- Recebe a recompensa: O algoritmo executa a ação no ambiente e recebe uma recompensa.
- 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
- 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))
- 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()
- 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:
- 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.
- 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.
- 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) 🚗💨”