EMMA-CVPR2026 / Baseline /architecture_ablation.py
abanerj3's picture
Upload folder using huggingface_hub
b46439f verified
Raw
History Blame Contribute Delete
16.1 kB
# EMMA Architecture Ablation Study: LTC vs LSTM vs GRU vs Transformer
# Critical Ablation A.1 for Paper
import os
import csv
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from ncps.torch import LTC
# Set device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}")
Nloop = 0
class Custom_Pendulum_Loss(nn.Module):
"""Physics-informed loss identical to run-45 pipeline."""
def __init__(self, labels, logits, omega):
super().__init__()
self.y_true = labels
self.y_pred = logits
self.y_omega = omega
def forward(self):
dev = self.y_pred.device
T, B, _ = self.y_pred.shape
maxChange = 95.0
getp = lambda k: self.y_pred[:,:,k]
alpha_nominal = 0.45
beta_nominal = 0.05
gamma_nominal = 100.0
alpha = (1 + (0.5 - getp(0)) * maxChange / 100.0) * alpha_nominal
beta = (1 + (0.5 - getp(1)) * maxChange / 100.0) * beta_nominal
gamma = (1 + (0.5 - getp(2)) * maxChange / 100.0) * gamma_nominal
L = alpha
tau = beta
g = torch.tensor(9.81, device=dev)
thetaVal = self.y_true[:,:,0]
omegaVal = self.y_omega[:,:,0]
theta = thetaVal.clone().unsqueeze(2)
omega = omegaVal.clone().unsqueeze(2)
limitLoop = min(500, T)
tau_dt = 0.03
for i in range(1, limitLoop):
y1 = theta[:,:,i-1] + omega[:,:,i-1]*tau_dt
y0 = omega[:,:,i-1] + (-torch.mul(tau,omega[:,:,i-1]) - torch.mul(torch.div(g,L.clamp(min=0.0001)),torch.sin(theta[:,:,i-1])))*tau_dt
theta = torch.cat([theta, y1.unsqueeze(2)],dim=2)
omega = torch.cat([omega, y0.unsqueeze(2)],dim=2)
loss_Cal_theta = gamma * 0.01
loss_Cal_omega = gamma * 0.005
mse_loss = torch.abs(torch.sum(torch.square(self.y_true[:,:,0:limitLoop]-theta)/limitLoop, dim=2)-loss_Cal_theta) + \
torch.abs(torch.sum(torch.square(self.y_omega[:,:,0:limitLoop]-omega)/limitLoop, dim=2)-loss_Cal_omega)
param_penalty = 0.0
param_penalty += 10.0 * torch.mean(torch.relu(-alpha))
param_penalty += 10.0 * torch.mean(torch.relu(-beta))
param_penalty += 10.0 * torch.mean(torch.relu(-gamma))
param_penalty += 2.0 * torch.mean(torch.relu(alpha - 2.0))
param_penalty += 2.0 * torch.mean(torch.relu(beta - 1.0))
param_penalty += 1.0 * torch.mean(torch.relu(gamma - 500.0))
total_loss = mse_loss + 0.001 * param_penalty
self.L = L
self.tau = tau
return total_loss
def cut_in_sequences(x, y, seq_len, inc=1):
sequences_x, sequences_y = [], []
for s in range(0, x.shape[0] - seq_len, inc):
start, end = s, s + seq_len
sequences_x.append(x[start:end])
sequences_y.append(y[start:end])
return np.stack(sequences_x, axis=1), np.stack(sequences_y, axis=1)
class PendulumData:
def __init__(self, seq_len=16, data_dir="data"):
print(f"Loading data from {data_dir}...")
theta_data = np.loadtxt(os.path.join(data_dir, "thetaData.txt"))
omega_data = np.loadtxt(os.path.join(data_dir, "omegaData.txt"))
theta_traj = theta_data.T
omega_traj = omega_data.T
global Nloop
Nloop = theta_traj.shape[1]
train_x, train_y = cut_in_sequences(theta_traj, theta_traj, seq_len)
train_omega, train_omega_y = cut_in_sequences(omega_traj, omega_traj, seq_len)
self.train_x = torch.tensor(train_x, dtype=torch.float32)
self.train_y = torch.tensor(train_y, dtype=torch.float32)
self.train_omega = torch.tensor(train_omega, dtype=torch.float32)
self.train_omega_y = torch.tensor(train_omega_y, dtype=torch.float32)
print(f"Training sequences: {self.train_x.shape[1]}")
def iterate_train(self, batch_size=32):
total_seqs = self.train_x.shape[1]
total_batches = max(1, total_seqs // batch_size)
for i in range(total_batches):
start = i * batch_size
end = start + batch_size
yield (self.train_x[:, start:end], self.train_y[:, start:end],
self.train_omega[:, start:end], self.train_omega_y[:, start:end])
class ArchitectureModel(nn.Module):
"""Unified model supporting LTC, LSTM, GRU, and Transformer."""
def __init__(self, model_type="ltc", model_size=64, learning_rate=0.0003):
super().__init__()
self.model_type = model_type.lower()
self.model_size = model_size
input_size = Nloop if Nloop > 0 else 100
print(f"Building {model_type.upper()} model with {model_size} units...")
if self.model_type == "ltc":
self.wm = LTC(
input_size=input_size,
units=model_size,
return_sequences=True,
batch_first=False,
mixed_memory=False,
ode_unfolds=6,
epsilon=1e-8
)
self.rnn = self.wm
learning_rate = 0.005
elif self.model_type == "lstm":
self.rnn = nn.LSTM(input_size, model_size, batch_first=False)
elif self.model_type == "gru":
self.rnn = nn.GRU(input_size, model_size, batch_first=False)
elif self.model_type == "transformer":
nhead = 4
padded_input_size = ((input_size + nhead - 1) // nhead) * nhead
if input_size != padded_input_size:
self.input_projection = nn.Linear(input_size, padded_input_size)
else:
self.input_projection = None
encoder_layer = nn.TransformerEncoderLayer(
d_model=padded_input_size,
nhead=nhead,
dim_feedforward=model_size,
batch_first=False
)
self.rnn = nn.TransformerEncoder(encoder_layer, num_layers=2)
self.transformer_out = nn.Linear(padded_input_size, model_size)
else:
raise ValueError(f"Unknown model type: {model_type}")
self.dense = nn.Linear(model_size, 3)
self.sigmoid = nn.Sigmoid()
self.optimizer = optim.AdamW(self.parameters(), lr=learning_rate,
weight_decay=1e-4, betas=(0.9, 0.999), eps=1e-8)
self.to(device)
self.scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
self.optimizer, T_0=10, T_mult=2, eta_min=1e-6
)
def forward(self, x):
if self.model_type == "ltc":
out, _ = self.rnn(x)
elif self.model_type == "transformer":
if hasattr(self, 'input_projection') and self.input_projection is not None:
T, B, _ = x.shape
x = self.input_projection(x.reshape(T*B, -1)).reshape(T, B, -1)
out = self.rnn(x)
out = self.transformer_out(out)
else: # LSTM, GRU
out, _ = self.rnn(x)
T, B, H = out.shape
y = self.sigmoid(self.dense(out.reshape(T*B, H))).reshape(T, B, 3)
return y
def compute_loss(self, y_pred, target_y, omega):
loss_fn = Custom_Pendulum_Loss(target_y, y_pred, omega)
return loss_fn.forward()
def run_architecture_ablation():
"""Run architecture comparison: LTC vs LSTM vs GRU vs Transformer."""
# Get absolute path
script_dir = os.path.dirname(os.path.abspath(__file__))
# Configuration
pendulum_45_datasets = [
"Pendulum-EMMA/45_v1",
"Pendulum-EMMA/45_v2",
"Pendulum-EMMA/45_v3",
"Pendulum-EMMA/45_v4",
"Pendulum-EMMA/45_v5"
]
# Architecture comparison (all with 64 hidden units for fair comparison)
architectures = ["ltc", "lstm", "gru", "transformer"]
# Training parameters
seq_len = 16
batch_size = 2
num_epochs = 40
learning_rate = 0.0003
results = []
print("=" * 70)
print("EMMA ARCHITECTURE ABLATION STUDY")
print("Comparison: LTC vs LSTM vs GRU vs Transformer (NODE)")
print("=" * 70)
total_experiments = len(architectures) * len(pendulum_45_datasets)
experiment_num = 0
for dataset_path in pendulum_45_datasets:
dataset_name = os.path.basename(dataset_path)
print(f"\n{'='*70}")
print(f"DATASET: {dataset_name}")
print(f"{'='*70}")
# Load data
data_dir = os.path.join(script_dir, dataset_path, "data")
if not os.path.exists(data_dir):
print(f"[WARNING] Data directory not found: {data_dir}")
continue
dataset = PendulumData(seq_len=seq_len, data_dir=data_dir)
for arch_name in architectures:
experiment_num += 1
print(f"\n[{experiment_num}/{total_experiments}] Architecture: {arch_name.upper()}, Dataset: {dataset_name}")
# Set seed
seed = int(dataset_name.split('_v')[-1])
np.random.seed(seed)
torch.manual_seed(seed)
# Create model
model = ArchitectureModel(model_type=arch_name, model_size=64, learning_rate=learning_rate).to(device)
optimizer = model.optimizer
scheduler = model.scheduler
# Training loop with convergence tracking
best_loss = float('inf')
convergence_epoch = num_epochs
loss_history = []
import time
start_time = time.time()
patience = 5
patience_counter = 0
convergence_threshold = 1e-4 # Loss improvement threshold
for epoch in range(num_epochs):
model.train()
epoch_loss = 0.0
batch_count = 0
for batch_x, batch_y, batch_omega, batch_omega_y in dataset.iterate_train(batch_size=batch_size):
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
batch_omega = batch_omega.to(device)
optimizer.zero_grad()
predicted_params = model(batch_x)
loss_mat = model.compute_loss(predicted_params, batch_y, batch_omega)
loss = loss_mat.mean()
if torch.isnan(loss):
continue
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
batch_count += 1
if batch_count > 0:
avg_loss = epoch_loss / batch_count
loss_history.append(avg_loss)
scheduler.step()
# Track convergence (when loss improvement is minimal)
if avg_loss < best_loss - convergence_threshold:
best_loss = avg_loss
patience_counter = 0
convergence_epoch = epoch + 1
else:
patience_counter += 1
if (epoch + 1) % 10 == 0:
print(f' Epoch {epoch+1}/{num_epochs}, Loss: {avg_loss:.6f}')
# Early stopping if converged
if patience_counter >= patience and epoch >= 10:
print(f' Converged at epoch {convergence_epoch}')
break
training_time = time.time() - start_time
final_loss = loss_history[-1] if loss_history else best_loss
# Evaluate
model.eval()
with torch.no_grad():
sample_batch = next(iter(dataset.iterate_train(batch_size=1)))
sample_x, sample_y, sample_omega, sample_omega_y = sample_batch
sample_x = sample_x.to(device)
predicted_params = model(sample_x)
maxChange = 95.0
getp = lambda k: predicted_params[:,:,k].mean()
L = ((1 + (0.5 - getp(0)) * maxChange / 100.0) * 0.45).item()
tau = ((1 + (0.5 - getp(1)) * maxChange / 100.0) * 0.05).item()
# Calculate parameter recovery accuracy (how close to ground truth)
L_gt = 0.45 # Ground truth for 45° pendulum
tau_gt = 0.05 # Ground truth damping
L_error = abs(L - L_gt) / L_gt * 100 # Percentage error
tau_error = abs(tau - tau_gt) / tau_gt * 100
print(f" L: {L:.6f} m (error: {L_error:.2f}%), tau: {tau:.6f} 1/s (error: {tau_error:.2f}%)")
print(f" Time: {training_time:.2f}s, Converged: epoch {convergence_epoch}, Final loss: {final_loss:.6f}")
results.append({
'dataset': dataset_name,
'architecture': arch_name.upper(),
'hidden_units': 64,
'video_number': seed,
'best_loss': best_loss,
'final_loss': final_loss,
'training_time_s': training_time,
'convergence_epoch': convergence_epoch,
'L_estimated': L,
'tau_estimated': tau,
'L_error_percent': L_error,
'tau_error_percent': tau_error
})
# Save results
results_file = os.path.join(script_dir, "architecture_ablation_results.csv")
print(f"\n{'='*70}")
print("SAVING RESULTS")
print(f"{'='*70}")
with open(results_file, 'w', newline='') as f:
if results:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
print(f"Results saved to: {results_file}")
# Print summary with key metrics
print(f"\n{'='*70}")
print("RESULTS SUMMARY - KEY METRICS FOR PAPER")
print(f"{'='*70}")
for arch_name in architectures:
arch_results = [r for r in results if r['architecture'] == arch_name.upper()]
if arch_results:
L_values = [r['L_estimated'] for r in arch_results]
tau_values = [r['tau_estimated'] for r in arch_results]
L_errors = [r['L_error_percent'] for r in arch_results]
tau_errors = [r['tau_error_percent'] for r in arch_results]
time_values = [r['training_time_s'] for r in arch_results]
conv_epochs = [r['convergence_epoch'] for r in arch_results]
final_losses = [r['final_loss'] for r in arch_results]
print(f"\n{arch_name.upper()}:")
print(f" Parameter Recovery Accuracy:")
print(f" L error: {np.mean(L_errors):.2f}% ± {np.std(L_errors):.2f}%")
print(f" tau error: {np.mean(tau_errors):.2f}% ± {np.std(tau_errors):.2f}%")
print(f" Convergence Speed:")
print(f" Epochs to converge: {np.mean(conv_epochs):.1f} ± {np.std(conv_epochs):.1f}")
print(f" Stability (lower is better):")
print(f" L std: {np.std(L_values):.6f} m")
print(f" tau std: {np.std(tau_values):.6f} 1/s")
print(f" Loss std: {np.std(final_losses):.6f}")
print(f" Training Time:")
print(f" {np.mean(time_values):.2f} ± {np.std(time_values):.2f} s")
if __name__ == "__main__":
run_architecture_ablation()