[Python] PPO clip AI issues

Discussão em 'Python' iniciado por Stack, Dezembro 1, 2024.

  1. Stack

    Stack Membro Participativo

    I have been trying to make a PPO clip AI using openai's pseudocode from a while ago however it is not very good. I am only trying a simple environment (cartpole) and the issue appears to be that after it runs for a little bit it develops a bias towards the move it originally takes meaning it moves in one direction more than the other.

    I have tried changing the hyper parameters in many ways and it hasn't changed much so I believe there is a fundamental flaw in the code that is causing the issue, that or I missed something with the parameters.

    My code is two files a main.py and a PPO_discrete.py:

    main.py

    import gymnasium as gym
    from PPO_discrete import PPOAgent

    def main():
    env = gym.make('CartPole-v1', render_mode="rgb_array") # Create CartPole environment
    state_dim = 4 # Dimension of state space
    action_dim = 2 # Number of discrete actions in the action space

    # Initialize PPO agent
    agent = PPOAgent(state_dim, action_dim, env)

    agent.learn(20500, True) # Train the agent
    print("Done training")

    env.close() # Close the environment after training

    # Reopen the environment for testing
    env = gym.make('CartPole-v1', render_mode="human")

    for _ in range(1000): # Test for 100 episodes
    obs, _ = env.reset(seed=49)
    done = False

    while not done:
    action, _ = agent.get_action(obs) # Extract only the action
    next_obs, _, terminated, truncated, _ = env.step(action) # Pass only action to step()
    obs = next_obs
    done = terminated or truncated

    env.close() # Properly close the environment after testing

    if __name__ == "__main__":
    main()


    PPO_discrete.py

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import torch.optim as optim
    import numpy as np

    # Policy Network for Discrete Environments
    class DiscretePolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
    super(DiscretePolicyNetwork, self).__init__()
    self.fc1 = nn.Linear(state_dim, 64)
    self.fc2 = nn.Linear(64, 64)
    self.logits = nn.Linear(64, action_dim)

    def forward(self, state):
    x = F.relu(self.fc1(state))
    x = F.relu(self.fc2(x))
    logits = self.logits(x) # Output logits for each action
    return logits


    class ValueNetwork(nn.Module):
    def __init__(self, state_dim):
    super(ValueNetwork, self).__init__()
    self.fc1 = nn.Linear(state_dim, 64)
    self.fc2 = nn.Linear(64, 64)
    self.fc3 = nn.Linear(64, 1)

    def forward(self, state):
    x = F.relu(self.fc1(state))
    x = F.relu(self.fc2(x))
    value = self.fc3(x)
    return value


    # PPOAgent with implemented optimizer parameter values
    class PPOAgent:
    def __init__(self, state_dim, action_dim, env):
    # Initialize policy and value networks
    self.policy_net = DiscretePolicyNetwork(state_dim, action_dim)
    self.value_net = ValueNetwork(state_dim)

    # Initialize optimizers with the specified parameter values
    self.policy_optimizer = optim.Adam(self.policy_net.parameters(), lr=0.0003, eps=1e-5)
    self.value_optimizer = optim.Adam(self.value_net.parameters(), lr=0.0003, eps=1e-5)

    # Manually set the optimizer state values
    for optimizer in [self.policy_optimizer, self.value_optimizer]:
    for state in optimizer.state.values():
    state['step'] = torch.tensor(1600.)
    state['exp_avg'] = torch.tensor([-0.0153])
    state['exp_avg_sq'] = torch.tensor([0.0024])

    # Environment and other hyperparameters
    self.env = env
    self.mean = np.zeros(env.observation_space.shape)
    self.std = np.ones(env.observation_space.shape)
    self.batch_size = 64
    self.epoch_count = 4
    self.gamma = 0.99 # Discount factor
    self.lambda_ = 0.95 # GAE parameter
    self.entropy_bonus = True
    self.epsilon = 0.2 # Clipping range
    self.beta = 0.01 # Entropy coefficient
    self.n_steps = 1024 # Number of steps per rollout


    def learn(self, k, frames):
    timesteps_perm = 0
    timesteps = 0
    states, actions, rewards, log_probs = [], [], [], []
    playing = True
    episode_rewards = [] # Track rewards per episode
    episode_reward = 0 # Track reward for the current episode

    while playing:
    obs, _ = self.env.reset(seed=49)
    done = False

    while not done:
    timesteps_perm += 1
    timesteps += 1
    action, log_prob = self.get_action(obs)

    next_obs, reward, terminated, truncated, _ = self.env.step(action)

    # Collect data
    states.append(obs)
    actions.append(action)
    rewards.append(reward)
    log_probs.append(log_prob)

    episode_reward += reward
    obs = next_obs
    done = terminated or truncated

    if done:
    episode_rewards.append(episode_reward)
    episode_reward = 0

    # Process batch if enough timesteps collected
    if timesteps >= self.n_steps:
    timesteps = 0
    self._process_batch(states, actions, rewards, log_probs)
    avg_action = np.mean(actions)
    states, actions, rewards, log_probs = [], [], [], [] # Reset batch

    # Print stats after each batch
    avg_reward = np.mean(episode_rewards[-10:]) if episode_rewards else 0
    print(f"Batch Processed: Avg Reward (last 10 eps): {avg_reward:.2f}, Timesteps: {self.n_steps}, Total: {timesteps_perm}, Average action: {avg_action}")
    episode_rewards = []

    if timesteps_perm >= k: # Stop training after collecting enough states
    playing = False

    def get_action(self, obs):
    state_tensor = torch.tensor(obs, dtype=torch.float32).unsqueeze(0)
    logits = self.policy_net(state_tensor)
    distribution = torch.distributions.Categorical(logits=logits)
    action = distribution.sample() # Get action as a tensor
    log_prob = distribution.log_prob(action)

    return action.item(), log_prob.detach()

    def _process_batch(self, states, actions, rewards, log_probs):
    rewards = (rewards - np.mean(rewards)) / (np.std(rewards) + 1e-8)
    states_tensor = torch.tensor(states, dtype=torch.float32)

    # Compute rewards-to-go (RTGs)
    rtgs = self._compute_rtgs(rewards)

    # Compute values from the value network
    values = self.value_net(states_tensor).squeeze(-1).detach()

    # Compute advantage estimates using GAE
    advantages = self._compute_advantage_estimates(rtgs, rewards, values)

    # Normalize advantages for stability
    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    self._update_policy(advantages, actions, log_probs, states)

    # Fit the value function to RTGs
    self._fit_value_function(rtgs, states_tensor)

    def _compute_rtgs(self, rewards):
    rtgs = []
    running_rtg = 0

    for r in reversed(rewards):
    running_rtg = r + self.gamma * running_rtg
    rtgs.insert(0, running_rtg)

    return torch.tensor(rtgs, dtype=torch.float32)

    def _compute_advantage_estimates(self, rtgs, rewards, values):
    advantages = torch.zeros_like(torch.tensor(rewards, dtype=torch.float32))

    gae = 0
    for t in reversed(range(len(rewards))):
    next_value = 0 if t == len(rewards) - 1 else values[t + 1]
    delta_t = rewards[t] + self.gamma * next_value - values[t]
    gae = delta_t + self.gamma * self.lambda_ * gae
    advantages[t] = gae

    return advantages

    def _update_policy(self, advantages, actions, log_probs, states):
    states_tensor = torch.tensor(states, dtype=torch.float32)
    actions_tensor = torch.tensor(actions, dtype=torch.int64) # Actions are now integers
    old_log_probs_tensor = torch.tensor(log_probs, dtype=torch.float32)

    for _ in range(self.epoch_count): # Use multiple epochs for stability
    logits = self.policy_net(states_tensor)
    distribution = torch.distributions.Categorical(logits=logits)
    new_log_probs = distribution.log_prob(actions_tensor)

    # Compute the probability ratio
    ratios = torch.exp(new_log_probs - old_log_probs_tensor)

    # Compute the surrogate objective
    surrogate_1 = ratios * advantages
    surrogate_2 = torch.clamp(ratios, 1 - self.epsilon, 1 + self.epsilon) * advantages
    clip_loss = torch.min(surrogate_1, surrogate_2).mean()

    # Add entropy bonus
    entropy_bonus = distribution.entropy().mean() if self.entropy_bonus else 0
    loss = -clip_loss - self.beta * entropy_bonus

    self.policy_optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(self.policy_net.parameters(), max_norm=0.5)
    torch.nn.utils.clip_grad_norm_(self.value_net.parameters(), max_norm=0.5)
    self.policy_optimizer.step()

    def _fit_value_function(self, rtgs, states_tensor, reps=2):
    rtgs = rtgs.detach() # Detach the targets, they don't require gradients
    for _ in range(reps):
    pred_vals = self.value_net(states_tensor).squeeze(-1)
    mse_loss = torch.mean((rtgs - pred_vals) ** 2)

    self.value_optimizer.zero_grad()
    mse_loss.backward()
    self.value_optimizer.step()

    Continue reading...

Compartilhe esta Página