engineportf's picture
Upload folder using huggingface_hub
558db1e verified
Raw
History Blame Contribute Delete
10.9 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
import torch.optim as optim
from .rssm import RSSMState, RSSM
from .networks import Encoder, Decoder, RewardModel, Actor, Critic
def compute_lambda_returns(rewards: torch.Tensor, values: torch.Tensor, gamma: float = 0.99, lambda_: float = 0.95):
"""
Computes TD(lambda) returns over an imagined trajectory.
rewards: [horizon, batch]
values: [horizon, batch]
"""
horizon = rewards.shape[0]
returns = []
last_return = values[-1]
for t in reversed(range(horizon - 1)):
# TD(lambda) recursive calculation
ret = rewards[t] + gamma * ((1 - lambda_) * values[t+1] + lambda_ * last_return)
returns.insert(0, ret)
last_return = ret
returns.append(values[-1])
return torch.stack(returns)
class AgenticForecaster(nn.Module):
def __init__(self, obs_dim: int, action_dim: int, device: torch.device):
super().__init__()
self.device = device
self.action_dim = action_dim
self.encoder = Encoder(obs_dim=obs_dim).to(device)
self.rssm = RSSM(action_dim=action_dim).to(device)
feature_dim = self.rssm.stoch_dim + self.rssm.deter_dim
self.decoder = Decoder(feature_dim=feature_dim, obs_dim=obs_dim).to(device)
self.reward_model = RewardModel(feature_dim=feature_dim).to(device)
self.actor = Actor(feature_dim=feature_dim, action_dim=action_dim).to(device)
self.critic = Critic(feature_dim=feature_dim).to(device)
self.target_critic = Critic(feature_dim=feature_dim).to(device)
self.target_critic.load_state_dict(self.critic.state_dict())
# Optimizers
self.model_optimizer = optim.Adam(
list(self.encoder.parameters()) +
list(self.rssm.parameters()) +
list(self.decoder.parameters()) +
list(self.reward_model.parameters()),
lr=3e-4
)
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=1e-4)
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-4)
def train_world_model(self, observations: torch.Tensor, actions: torch.Tensor, rewards: torch.Tensor, free_nats: float = 3.0, kl_scale: float = 1.0):
"""
observations: [batch_size, seq_len, obs_dim]
actions: [batch_size, seq_len, action_dim]
rewards: [batch_size, seq_len]
"""
batch_size, seq_len, _ = observations.shape
state = self.rssm.initial_state(batch_size, self.device)
kl_losses = []
recon_losses = []
reward_losses = []
for t in range(seq_len):
obs_embed = self.encoder(observations[:, t, :])
# Actions align such that action[t-1] led to observation[t]. For t=0, assume zero action
action_t = actions[:, t-1, :] if t > 0 else torch.zeros(batch_size, self.action_dim, device=self.device)
state, post_stats, prior_stats = self.rssm.observe_step(state, action_t, obs_embed)
features = state.get_features()
recon_obs = self.decoder(features)
pred_reward = self.reward_model(features)
recon_losses.append(F.mse_loss(recon_obs, observations[:, t, :], reduction='none').sum(-1).mean())
reward_losses.append(F.mse_loss(pred_reward.squeeze(-1), rewards[:, t], reduction='mean'))
post_dist = Normal(post_stats[0], post_stats[1])
prior_dist = Normal(prior_stats[0], prior_stats[1])
# Analytical KL divergence summed over latent dimension, averaged over batch
kl_loss = torch.mean(torch.sum(torch.distributions.kl.kl_divergence(post_dist, prior_dist), dim=-1))
# Free bits (nats) implementation
kl_loss = torch.max(kl_loss, torch.tensor(free_nats).to(self.device))
kl_losses.append(kl_loss)
model_loss = sum(recon_losses) + sum(reward_losses) + kl_scale * sum(kl_losses)
self.model_optimizer.zero_grad()
model_loss.backward()
nn.utils.clip_grad_norm_(self.parameters(), 100.0)
self.model_optimizer.step()
return {"model_loss": model_loss.item(), "recon_loss": sum(recon_losses).item(), "kl_loss": sum(kl_losses).item()}
def train_actor_critic(self, start_states: RSSMState, horizon: int = 15, gamma: float = 0.99, lambda_: float = 0.95):
"""
Trains Actor and Critic purely in latent imagination.
start_states is sampled from the posterior distribution during world model training.
"""
# 1. Rollout imagined trajectory
state = start_states
features_list = []
rewards_list = []
for _ in range(horizon):
features = state.get_features()
action = self.actor(features.detach(), explore=True)
# Imagine next state driven by the actor's action
state = self.rssm.imagine_step(state, action)
next_features = state.get_features()
reward = self.reward_model(next_features)
features_list.append(next_features)
rewards_list.append(reward.squeeze(-1))
features_stack = torch.stack(features_list) # [horizon, batch, feature_dim]
rewards_stack = torch.stack(rewards_list) # [horizon, batch]
# 2. Compute TD(lambda) values using the target critic
with torch.no_grad():
values_stack = self.target_critic(features_stack).squeeze(-1)
returns = compute_lambda_returns(rewards_stack, values_stack, gamma, lambda_)
# 3. Train Actor (Maximize expected lambda return)
actor_loss = -returns.mean()
self.actor_optimizer.zero_grad()
actor_loss.backward()
nn.utils.clip_grad_norm_(self.actor.parameters(), 100.0)
self.actor_optimizer.step()
# 4. Train Critic (Predict lambda returns)
# Detach features so we don't backprop into the world model
pred_values = self.critic(features_stack.detach()).squeeze(-1)
critic_loss = F.mse_loss(pred_values, returns.detach())
self.critic_optimizer.zero_grad()
critic_loss.backward()
nn.utils.clip_grad_norm_(self.critic.parameters(), 100.0)
self.critic_optimizer.step()
# 5. Soft update target critic
tau = 0.05
for param, target_param in zip(self.critic.parameters(), self.target_critic.parameters()):
target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)
return {"actor_loss": actor_loss.item(), "critic_loss": critic_loss.item()}
def act(self, obs: torch.Tensor, prev_state: RSSMState, prev_action: torch.Tensor, explore: bool = False):
"""Used during data collection or evaluation to select an action."""
with torch.no_grad():
obs_embed = self.encoder(obs.unsqueeze(0))
new_state, _, _ = self.rssm.observe_step(prev_state, prev_action.unsqueeze(0), obs_embed)
features = new_state.get_features()
action = self.actor(features, explore=explore)
return action.squeeze(0), new_state
class HomeostaticAgenticForecaster(AgenticForecaster):
"""
Dreamer agent with homeostatic critic.
Maintains a stable value baseline while learning deviations.
"""
def __init__(self, obs_dim: int, action_dim: int, device: torch.device):
super().__init__(obs_dim, action_dim, device)
# Replace critic with homeostatic version
feature_dim = self.rssm.stoch_dim + self.rssm.deter_dim
from .networks import HomeostaticCritic
self.critic = HomeostaticCritic(feature_dim=feature_dim).to(device)
self.target_critic = HomeostaticCritic(feature_dim=feature_dim).to(device)
self.target_critic.load_state_dict(self.critic.state_dict())
# Reinitialize optimizers with new parameters
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-4)
self.training_step = 0
def train_actor_critic(self, start_states: RSSMState, horizon: int = 15,
gamma: float = 0.99, lambda_: float = 0.95):
"""Same as base, but critic updates its setpoint periodically"""
# After computing returns, update homeostasis
if self.training_step % 100 == 0: # Every 100 steps
update_homeostasis = True
else:
update_homeostasis = False
self.training_step += 1
# 1. Rollout imagined trajectory
state = start_states
features_list = []
rewards_list = []
for _ in range(horizon):
features = state.get_features()
action = self.actor(features.detach(), explore=True)
state = self.rssm.imagine_step(state, action)
next_features = state.get_features()
reward = self.reward_model(next_features)
features_list.append(next_features)
rewards_list.append(reward.squeeze(-1))
features_stack = torch.stack(features_list)
rewards_stack = torch.stack(rewards_list)
# 2. Compute TD(lambda) values using the target critic
with torch.no_grad():
values_stack = self.target_critic(features_stack).squeeze(-1)
returns = compute_lambda_returns(rewards_stack, values_stack, gamma, lambda_)
# 3. Train Actor
actor_loss = -returns.mean()
self.actor_optimizer.zero_grad()
actor_loss.backward()
nn.utils.clip_grad_norm_(self.actor.parameters(), 100.0)
self.actor_optimizer.step()
# 4. Train Critic (Predict lambda returns AND update setpoint optionally)
pred_values = self.critic(features_stack.detach(), update_homeostasis=update_homeostasis).squeeze(-1)
critic_loss = F.mse_loss(pred_values, returns.detach())
self.critic_optimizer.zero_grad()
critic_loss.backward()
nn.utils.clip_grad_norm_(self.critic.parameters(), 100.0)
self.critic_optimizer.step()
# 5. Soft update target critic
tau = 0.05
for param, target_param in zip(self.critic.parameters(), self.target_critic.parameters()):
target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)
return {"actor_loss": actor_loss.item(), "critic_loss": critic_loss.item()}