575 lines
20 KiB
Python
575 lines
20 KiB
Python
import logging
|
|||
|
|
import math
|
||
|
|
import os
|
||
|
|
import random
|
||
|
|
import threading
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import psutil
|
||
|
|
import torch
|
||
|
|
import torch.nn.functional as F
|
||
|
|
from safetensors.torch import load_file
|
||
|
|
from torch.optim.lr_scheduler import LambdaLR
|
||
|
|
from transformers import AutoProcessor
|
||
|
|
|
||
|
|
from wall_x.model.core.action.normalizer import Normalizer
|
||
|
|
from wall_x.model.qact.tokenizer_mixin import get_action_tokenizer_mixin
|
||
|
|
from wall_x.utils.metrics import dtw_distance, frechet_distance, get_action_accuracy
|
||
|
|
|
||
|
|
_logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
def load_wallx_processors(
|
||
|
|
config,
|
||
|
|
normalizer=None,
|
||
|
|
action_statistic_dof=None,
|
||
|
|
device: str = "cpu",
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Load Wall-X processors, including tokenizer and action mapper.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
config: Configuration dictionary.
|
||
|
|
device: Tokenizer device. Training usually uses "cpu"; inference
|
||
|
|
usually uses "cuda".
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary containing:
|
||
|
|
- processor: HuggingFace processor
|
||
|
|
- train_action_tokenizer: action tokenizer for training
|
||
|
|
- val_action_tokenizer: action tokenizer for validation
|
||
|
|
- action_mapper: action mapper dictionary
|
||
|
|
- num_added_tokens: number of added tokens
|
||
|
|
- tokenizer_mixin: ActionTokenizerMixin instance
|
||
|
|
"""
|
||
|
|
processor = AutoProcessor.from_pretrained(config["processor_path"], use_fast=True)
|
||
|
|
# pad side = left
|
||
|
|
processor.tokenizer.padding_side = "left"
|
||
|
|
|
||
|
|
new_tokens = ["<|propri|>", "<|action|>"]
|
||
|
|
if config.get("new_special_tokens", None) is not None:
|
||
|
|
new_tokens.extend(config.get("new_special_tokens"))
|
||
|
|
|
||
|
|
action_tokenizer_type = config.get("action_tokenizer_type", None)
|
||
|
|
|
||
|
|
train_action_tokenizer = None
|
||
|
|
val_action_tokenizer = None
|
||
|
|
action_mapper = None
|
||
|
|
tokenizer_mixin = None
|
||
|
|
|
||
|
|
if action_tokenizer_type:
|
||
|
|
# Use tokenizer_mixin as the single action-tokenizer interface.
|
||
|
|
action_tokenizer_config = config.get("action_tokenizer", {})
|
||
|
|
# Backward compatibility: read top-level keys as fallback values.
|
||
|
|
action_tokenizer_config.setdefault(
|
||
|
|
"action_tokenizer_path", config.get("action_tokenizer_path")
|
||
|
|
)
|
||
|
|
action_tokenizer_config.setdefault(
|
||
|
|
"action_tokenizer_checkpoint_path",
|
||
|
|
config.get("action_tokenizer_checkpoint_path"),
|
||
|
|
)
|
||
|
|
action_tokenizer_config.setdefault(
|
||
|
|
"action_tokenizer_config_dir", config.get("action_tokenizer_config_dir")
|
||
|
|
)
|
||
|
|
# Pass action_horizon_ar to the tokenizer for DLLM.
|
||
|
|
data_config = config.get("data", {})
|
||
|
|
action_tokenizer_config.setdefault(
|
||
|
|
"action_horizon_ar", data_config.get("action_horizon_ar", 32)
|
||
|
|
)
|
||
|
|
# Fall back to dof_config when ar_dof_config is not provided.
|
||
|
|
ar_dof_config = config.get("ar_dof_config") or config.get("dof_config")
|
||
|
|
assert ar_dof_config is not None, "Missing ar_dof_config and dof_config"
|
||
|
|
if normalizer is None:
|
||
|
|
if action_statistic_dof is None:
|
||
|
|
raise ValueError(
|
||
|
|
"Action tokenizer setup requires an explicit normalizer or "
|
||
|
|
"action statistics. Public Wall-X builds do not bundle "
|
||
|
|
"default action statistics."
|
||
|
|
)
|
||
|
|
ar_normalizer = Normalizer(action_statistic_dof, ar_dof_config)
|
||
|
|
else:
|
||
|
|
ar_normalizer = normalizer
|
||
|
|
tokenizer_mixin = get_action_tokenizer_mixin(action_tokenizer_type)
|
||
|
|
tokenizer_mixin.load_tokenizer(
|
||
|
|
action_tokenizer_config, ar_normalizer, device=device
|
||
|
|
)
|
||
|
|
|
||
|
|
# Collect special tokens.
|
||
|
|
_new_tokens, special_tokens = tokenizer_mixin.get_all_special_tokens()
|
||
|
|
new_tokens += _new_tokens
|
||
|
|
|
||
|
|
# Add tokens to the vocabulary.
|
||
|
|
num_added_tokens = processor.tokenizer.add_tokens(new_tokens)
|
||
|
|
|
||
|
|
# Set placeholder_seq for discrete diffusion.
|
||
|
|
if special_tokens and action_tokenizer_config.get(
|
||
|
|
"input_placeholder_flag", False
|
||
|
|
):
|
||
|
|
processor.placeholder_seq = [
|
||
|
|
processor.tokenizer.convert_tokens_to_ids(token)
|
||
|
|
for token in special_tokens
|
||
|
|
]
|
||
|
|
|
||
|
|
# Backward compatibility: use the first added token when
|
||
|
|
# <|action_token_0|> does not exist.
|
||
|
|
ar_first_token_id = processor.tokenizer.convert_tokens_to_ids(
|
||
|
|
"<|action_token_0|>"
|
||
|
|
)
|
||
|
|
if (
|
||
|
|
ar_first_token_id is None
|
||
|
|
or ar_first_token_id == processor.tokenizer.unk_token_id
|
||
|
|
):
|
||
|
|
ar_first_token_id = processor.tokenizer.convert_tokens_to_ids(
|
||
|
|
_new_tokens[0]
|
||
|
|
)
|
||
|
|
processor.ar_first_token = ar_first_token_id
|
||
|
|
|
||
|
|
# Build action_mapper.
|
||
|
|
action_mapper = tokenizer_mixin.build_action_mapper(processor)
|
||
|
|
|
||
|
|
# Fetch the underlying tokenizer.
|
||
|
|
train_action_tokenizer = tokenizer_mixin.tokenizer
|
||
|
|
# Fast tokenizers need a separate validation instance; others share one.
|
||
|
|
val_action_tokenizer = tokenizer_mixin.get_val_tokenizer(config)
|
||
|
|
else:
|
||
|
|
num_added_tokens = processor.tokenizer.add_tokens(new_tokens)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"processor": processor,
|
||
|
|
"train_action_tokenizer": train_action_tokenizer,
|
||
|
|
"val_action_tokenizer": val_action_tokenizer,
|
||
|
|
"action_mapper": action_mapper,
|
||
|
|
"num_added_tokens": num_added_tokens,
|
||
|
|
"tokenizer_mixin": tokenizer_mixin,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def load_wallx_processors_from_cfg(
|
||
|
|
cfg,
|
||
|
|
normalizer=None,
|
||
|
|
action_statistic_dof=None,
|
||
|
|
device: str = "cpu",
|
||
|
|
):
|
||
|
|
"""Typed convenience wrapper around ``load_wallx_processors``.
|
||
|
|
|
||
|
|
Builds the flat dict that the legacy function expects from typed
|
||
|
|
TrainConfig sub-configs, then delegates. Callers using TrainConfig
|
||
|
|
can use this directly instead of hand-flattening.
|
||
|
|
"""
|
||
|
|
import dataclasses
|
||
|
|
|
||
|
|
flat = dataclasses.asdict(cfg.model)
|
||
|
|
flat["model_type"] = cfg.model_type
|
||
|
|
flat["data"] = dict(cfg._raw_data or {})
|
||
|
|
flat["dof_config"] = cfg.task.dof_config
|
||
|
|
flat["agent_pos_config"] = cfg.task.agent_pos_config
|
||
|
|
if cfg.task.ar_dof_config is not None:
|
||
|
|
flat["ar_dof_config"] = cfg.task.ar_dof_config
|
||
|
|
flat["batch_size_per_gpu"] = cfg.hyperparams.batch_size_per_gpu
|
||
|
|
return load_wallx_processors(
|
||
|
|
flat,
|
||
|
|
normalizer=normalizer,
|
||
|
|
action_statistic_dof=action_statistic_dof,
|
||
|
|
device=device,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def load_qwen_pretrain_weight(model, pretrain_weight_path):
|
||
|
|
weight_files = sorted(
|
||
|
|
[f for f in os.listdir(pretrain_weight_path) if f.endswith(".safetensors")]
|
||
|
|
)
|
||
|
|
# Initialize empty dictionary to store merged weights
|
||
|
|
merged_weights = {}
|
||
|
|
|
||
|
|
# Load and merge each file sequentially
|
||
|
|
for weight_file in weight_files:
|
||
|
|
file_path = os.path.join(pretrain_weight_path, weight_file)
|
||
|
|
weights = load_file(file_path)
|
||
|
|
merged_weights.update(weights)
|
||
|
|
|
||
|
|
renamed_weights = model.rename_vlm_weights_for_vla(merged_weights)
|
||
|
|
renamed_weights = {
|
||
|
|
k: v
|
||
|
|
for k, v in renamed_weights.items()
|
||
|
|
if "action_preprocessor.normalizer_" not in k
|
||
|
|
} # remove normalizer weights
|
||
|
|
if (
|
||
|
|
model.config.model_type == "qwen2_5_vl"
|
||
|
|
and model.model.embed_tokens.weight.shape[0]
|
||
|
|
!= renamed_weights["model.embed_tokens.weight"].shape[0]
|
||
|
|
):
|
||
|
|
_logger.info(
|
||
|
|
"resize_token_embeddings from %d to %d",
|
||
|
|
model.model.embed_tokens.weight.shape[0],
|
||
|
|
renamed_weights["model.embed_tokens.weight"].shape[0],
|
||
|
|
)
|
||
|
|
model.model.resize_token_embeddings(
|
||
|
|
renamed_weights["model.embed_tokens.weight"].shape[0]
|
||
|
|
)
|
||
|
|
|
||
|
|
err = model.load_state_dict(renamed_weights, strict=False)
|
||
|
|
|
||
|
|
return model, err
|
||
|
|
|
||
|
|
|
||
|
|
def update_model_config(train_config, model_config):
|
||
|
|
model_config.use_state_string_representation = train_config["data"].get(
|
||
|
|
"use_state_string_representation", False
|
||
|
|
)
|
||
|
|
model_config.ar_loss_weight = train_config.get("ar_loss_weight", 1.0)
|
||
|
|
|
||
|
|
model_config.dof_config = train_config["dof_config"]
|
||
|
|
model_config.agent_pos_config = train_config["agent_pos_config"]
|
||
|
|
|
||
|
|
model_config.action_horizon_flow = train_config["data"].get(
|
||
|
|
"action_horizon_flow", 32
|
||
|
|
)
|
||
|
|
|
||
|
|
if train_config.get("_attn_implementation", None) is not None:
|
||
|
|
model_config._attn_implementation = train_config["_attn_implementation"]
|
||
|
|
|
||
|
|
if train_config.get("attn_deterministic", None) is not None:
|
||
|
|
model_config.attn_deterministic = train_config["attn_deterministic"]
|
||
|
|
model_config.vision_config.attn_deterministic = train_config[
|
||
|
|
"attn_deterministic"
|
||
|
|
]
|
||
|
|
_logger.info("Attention is using deterministic kernel for this run")
|
||
|
|
else:
|
||
|
|
model_config.attn_deterministic = True
|
||
|
|
model_config.vision_config.attn_deterministic = True
|
||
|
|
|
||
|
|
if train_config.get("noise_scheduler", None) is not None:
|
||
|
|
model_config.noise_scheduler = train_config["noise_scheduler"]
|
||
|
|
|
||
|
|
return model_config
|
||
|
|
|
||
|
|
|
||
|
|
def update_data_config(config):
|
||
|
|
"""Keep the top-level model type aligned with the nested data config."""
|
||
|
|
config["data"]["model_type"] = config.get("model_type")
|
||
|
|
|
||
|
|
if config.get("use_state_string_representation", None) is not None:
|
||
|
|
config["data"]["use_state_string_representation"] = config[
|
||
|
|
"use_state_string_representation"
|
||
|
|
]
|
||
|
|
|
||
|
|
return config
|
||
|
|
|
||
|
|
|
||
|
|
def get_detailed_memory_usage():
|
||
|
|
"""Return process memory and thread-count diagnostics."""
|
||
|
|
process = psutil.Process()
|
||
|
|
memory_info = process.memory_info()
|
||
|
|
current_threads = threading.active_count()
|
||
|
|
return {
|
||
|
|
"rss": f"{memory_info.rss / 1024 / 1024:.2f}MB ",
|
||
|
|
"vms": f"{memory_info.vms / 1024 / 1024:.2f}MB ",
|
||
|
|
"threads_count": current_threads,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def is_last_rank():
|
||
|
|
return torch.distributed.get_rank() == (torch.distributed.get_world_size() - 1)
|
||
|
|
|
||
|
|
|
||
|
|
def print_rank_last(message):
|
||
|
|
"""If distributed is initialized, log only on last rank."""
|
||
|
|
if torch.distributed.is_initialized():
|
||
|
|
if is_last_rank():
|
||
|
|
_logger.info(message)
|
||
|
|
else:
|
||
|
|
_logger.info(message)
|
||
|
|
|
||
|
|
|
||
|
|
def seed_all(seed):
|
||
|
|
np.random.seed(seed)
|
||
|
|
random.seed(seed)
|
||
|
|
torch.manual_seed(seed)
|
||
|
|
torch.cuda.manual_seed_all(seed)
|
||
|
|
torch.backends.cudnn.deterministic = True
|
||
|
|
torch.backends.cudnn.benchmark = False
|
||
|
|
os.environ["PYTHONHASHSEED"] = str(seed)
|
||
|
|
|
||
|
|
|
||
|
|
def save_text_results_to_file(
|
||
|
|
current_step, all_input_texts, all_gt_texts, all_pred_texts, save_dir
|
||
|
|
):
|
||
|
|
"""Save text-generation results to a local JSON file."""
|
||
|
|
import json
|
||
|
|
|
||
|
|
os.makedirs(save_dir, exist_ok=True)
|
||
|
|
|
||
|
|
results = []
|
||
|
|
for i, (input_text, gt_text, pred_text) in enumerate(
|
||
|
|
zip(all_input_texts, all_gt_texts, all_pred_texts)
|
||
|
|
):
|
||
|
|
# Some callers pass one-item lists instead of plain strings.
|
||
|
|
input_clean = input_text[0] if isinstance(input_text, list) else input_text
|
||
|
|
gt_clean = gt_text[0] if isinstance(gt_text, list) else gt_text
|
||
|
|
pred_clean = pred_text[0] if isinstance(pred_text, list) else pred_text
|
||
|
|
|
||
|
|
results.append(
|
||
|
|
{
|
||
|
|
"sample_id": i,
|
||
|
|
"input": input_clean,
|
||
|
|
"ground_truth": gt_clean,
|
||
|
|
"prediction": pred_clean,
|
||
|
|
"step": current_step,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
filename = os.path.join(save_dir, f"text_predictions_step_{current_step}.json")
|
||
|
|
with open(filename, "w", encoding="utf-8") as f:
|
||
|
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
||
|
|
|
||
|
|
|
||
|
|
# Add accuracy metrics.
|
||
|
|
# Add ade, fde, dtw, and frechet metrics from Ryan.
|
||
|
|
def compute_action_metrics(all_preds, all_actions, config, step_log={}):
|
||
|
|
metrics_settings = config.get("metrics_settings", "default")
|
||
|
|
metrics_available = ["l1", "mse", "accuracy", "ade", "fde", "dtw", "frechet"]
|
||
|
|
metrics_default = ["l1", "mse", "accuracy"]
|
||
|
|
if metrics_settings == "all":
|
||
|
|
metrics_enabled = metrics_available
|
||
|
|
elif metrics_settings == "default":
|
||
|
|
metrics_enabled = metrics_default
|
||
|
|
elif isinstance(metrics_settings, str):
|
||
|
|
metrics_enabled = [
|
||
|
|
m.strip()
|
||
|
|
for m in metrics_settings.split(" ")
|
||
|
|
if m.strip() in metrics_available
|
||
|
|
]
|
||
|
|
elif isinstance(metrics_settings, list):
|
||
|
|
metrics_enabled = [m for m in metrics_settings if m in metrics_available]
|
||
|
|
else:
|
||
|
|
metrics_enabled = metrics_default
|
||
|
|
_logger.warning(
|
||
|
|
'Unknown metrics_settings "%s". Using default metrics: %s',
|
||
|
|
metrics_settings,
|
||
|
|
metrics_enabled,
|
||
|
|
)
|
||
|
|
|
||
|
|
overall_l1 = F.l1_loss(all_preds, all_actions)
|
||
|
|
overall_mse = F.mse_loss(all_preds, all_actions)
|
||
|
|
|
||
|
|
step_log["val_action_l1"] = overall_l1.item()
|
||
|
|
step_log["val_action_mse"] = overall_mse.item()
|
||
|
|
|
||
|
|
if "accuracy" in metrics_enabled:
|
||
|
|
accuracy_thresholds = [0.05, 0.1, 0.2, 0.4]
|
||
|
|
# The accuracy that all predicted action dimensions are within a certain range of the ground truth.
|
||
|
|
accuracies = get_action_accuracy(
|
||
|
|
all_preds, all_actions, thresholds=accuracy_thresholds
|
||
|
|
)
|
||
|
|
for th_idx, threshold in enumerate(accuracy_thresholds):
|
||
|
|
step_log[f"val_action_acc_thr{threshold}"] = accuracies[th_idx].item()
|
||
|
|
|
||
|
|
start_idx = 0
|
||
|
|
dof_config = config["dof_config"]
|
||
|
|
for dof_key, dof_dim in dof_config.items():
|
||
|
|
end_idx = start_idx + dof_dim
|
||
|
|
# all_preds.shape = (B, T, action_dim)
|
||
|
|
pred_dof = all_preds[..., start_idx:end_idx]
|
||
|
|
action_dof = all_actions[..., start_idx:end_idx]
|
||
|
|
dof_l1 = F.l1_loss(pred_dof, action_dof)
|
||
|
|
dof_mse = F.mse_loss(pred_dof, action_dof)
|
||
|
|
step_log[f"val_l1/{dof_key}"] = dof_l1.item()
|
||
|
|
step_log[f"val_mse/{dof_key}"] = dof_mse.item()
|
||
|
|
|
||
|
|
if "accuracy" in metrics_enabled:
|
||
|
|
accuracies = get_action_accuracy(
|
||
|
|
action_dof, pred_dof, thresholds=accuracy_thresholds
|
||
|
|
)
|
||
|
|
for th_idx, threshold in enumerate(accuracy_thresholds):
|
||
|
|
step_log[f"val_acc/{dof_key}_thr{threshold}"] = accuracies[
|
||
|
|
th_idx
|
||
|
|
].item()
|
||
|
|
|
||
|
|
if "ee_cartesian_pos" in dof_key:
|
||
|
|
if "ade" in metrics_enabled:
|
||
|
|
displacement_error = torch.norm(pred_dof - action_dof, dim=-1)
|
||
|
|
ade = torch.mean(displacement_error)
|
||
|
|
step_log[f"val_ade/{dof_key}"] = ade.item()
|
||
|
|
|
||
|
|
if "fde" in metrics_enabled:
|
||
|
|
final_pred = pred_dof[:, -1, :]
|
||
|
|
final_gt = action_dof[:, -1, :]
|
||
|
|
fde = torch.mean(torch.norm(final_pred - final_gt, dim=-1))
|
||
|
|
step_log[f"val_fde/{dof_key}"] = fde.item()
|
||
|
|
|
||
|
|
if "dtw" in metrics_enabled or "frechet" in metrics_enabled:
|
||
|
|
dtw_distances = []
|
||
|
|
frechet_distances = []
|
||
|
|
batch_size = pred_dof.shape[0]
|
||
|
|
for i in range(batch_size):
|
||
|
|
pred_seq = pred_dof[i] # shape: (T, D)
|
||
|
|
gt_seq = action_dof[i] # shape: (T, D)
|
||
|
|
dtw_dist = dtw_distance(pred_seq, gt_seq)
|
||
|
|
dtw_distances.append(dtw_dist)
|
||
|
|
|
||
|
|
frechet_dist = frechet_distance(pred_seq, gt_seq)
|
||
|
|
frechet_distances.append(frechet_dist)
|
||
|
|
|
||
|
|
avg_dtw = torch.mean(torch.stack(dtw_distances))
|
||
|
|
if "dtw" in metrics_enabled:
|
||
|
|
step_log[f"val_dtw/{dof_key}"] = avg_dtw.item()
|
||
|
|
|
||
|
|
avg_frechet = torch.mean(torch.stack(frechet_distances))
|
||
|
|
if "frechet" in metrics_enabled:
|
||
|
|
step_log[f"val_frechet/{dof_key}"] = avg_frechet.item()
|
||
|
|
|
||
|
|
start_idx = end_idx
|
||
|
|
|
||
|
|
return step_log
|
||
|
|
|
||
|
|
|
||
|
|
def get_warmup_cosine_schedule(
|
||
|
|
optimizer,
|
||
|
|
num_warmup_steps: int,
|
||
|
|
num_training_steps: int,
|
||
|
|
peak_lr: float = None,
|
||
|
|
end_lr: float = None,
|
||
|
|
last_epoch: int = -1,
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Create a schedule with linear warmup followed by cosine decay:
|
||
|
|
- Warmup: linearly increases from peak_lr/(warmup_steps+1) to peak_lr
|
||
|
|
- Decay: cosine decay from peak_lr to end_lr
|
||
|
|
|
||
|
|
Args:
|
||
|
|
optimizer: The optimizer for which to schedule the learning rate.
|
||
|
|
num_warmup_steps: The number of steps for the warmup phase.
|
||
|
|
num_training_steps: The total number of training steps.
|
||
|
|
peak_lr: The peak learning rate. If None, uses optimizer's initial lr.
|
||
|
|
end_lr: The minimum learning rate at the end. If None, defaults to peak_lr * 0.1.
|
||
|
|
last_epoch: The index of the last epoch when resuming training.
|
||
|
|
|
||
|
|
Return:
|
||
|
|
torch.optim.lr_scheduler.LambdaLR with the appropriate schedule.
|
||
|
|
"""
|
||
|
|
if peak_lr is None:
|
||
|
|
peak_lr = optimizer.defaults["lr"]
|
||
|
|
if end_lr is None:
|
||
|
|
end_lr = peak_lr * 0.1
|
||
|
|
|
||
|
|
def lr_lambda(current_step: int):
|
||
|
|
if current_step < num_warmup_steps:
|
||
|
|
# Start from peak_lr / (warmup_steps + 1).
|
||
|
|
init_lr = peak_lr / (num_warmup_steps + 1)
|
||
|
|
current_lr = init_lr + (peak_lr - init_lr) * current_step / num_warmup_steps
|
||
|
|
return current_lr / peak_lr # LambdaLR multiplies by base_lr
|
||
|
|
else:
|
||
|
|
# Cosine decay
|
||
|
|
decay_steps = num_training_steps - num_warmup_steps
|
||
|
|
progress = min(1.0, (current_step - num_warmup_steps) / max(1, decay_steps))
|
||
|
|
cos = 0.5 * (1 + math.cos(math.pi * progress))
|
||
|
|
current_lr = end_lr + (peak_lr - end_lr) * cos
|
||
|
|
return current_lr / peak_lr
|
||
|
|
|
||
|
|
return LambdaLR(optimizer, lr_lambda, last_epoch)
|
||
|
|
|
||
|
|
|
||
|
|
def plot_openloop(
|
||
|
|
action_pred_list,
|
||
|
|
action_gt_list,
|
||
|
|
l1_loss,
|
||
|
|
episode_index,
|
||
|
|
save_path,
|
||
|
|
is_static_list=None,
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Plot openloop action comparison visualization.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
action_pred_list: List of predicted actions, each with shape (horizon, action_dim)
|
||
|
|
action_gt_list: List of ground truth actions, each with shape (horizon, action_dim)
|
||
|
|
l1_loss: L1 loss array with shape (total_frames, action_dim)
|
||
|
|
episode_index: Index of the episode being visualized
|
||
|
|
save_path: Directory path to save the plot
|
||
|
|
is_static_list: Optional list of booleans indicating static frames
|
||
|
|
"""
|
||
|
|
import matplotlib
|
||
|
|
|
||
|
|
matplotlib.use("Agg")
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
|
||
|
|
assert len(action_pred_list) == len(
|
||
|
|
action_gt_list
|
||
|
|
), "Predicted action and ground truth action must have the same shape."
|
||
|
|
|
||
|
|
dim = action_pred_list[0].shape[1]
|
||
|
|
plt.figure(figsize=(12, 4 * dim))
|
||
|
|
|
||
|
|
for i in range(dim):
|
||
|
|
plt.subplot(dim, 1, i + 1)
|
||
|
|
|
||
|
|
has_labeled_static = False
|
||
|
|
for j in range(len(action_gt_list)):
|
||
|
|
gt_action = action_gt_list[j]
|
||
|
|
predict_action = action_pred_list[j]
|
||
|
|
|
||
|
|
x_vals_gt = np.linspace(j, j + 1, len(gt_action))
|
||
|
|
x_vals_pred = np.linspace(j, j + 1, len(predict_action))
|
||
|
|
|
||
|
|
if is_static_list is not None and is_static_list[j]:
|
||
|
|
label = None
|
||
|
|
if not has_labeled_static:
|
||
|
|
label = "Static GT"
|
||
|
|
has_labeled_static = True
|
||
|
|
plt.axvspan(j, j + 1, color="gray", alpha=0.2, label=label)
|
||
|
|
|
||
|
|
if j == 0:
|
||
|
|
plt.plot(
|
||
|
|
x_vals_gt,
|
||
|
|
gt_action[:, i],
|
||
|
|
label="Ground Truth",
|
||
|
|
color="blue",
|
||
|
|
linewidth=2,
|
||
|
|
linestyle="-",
|
||
|
|
marker="o",
|
||
|
|
markersize=3,
|
||
|
|
)
|
||
|
|
plt.plot(
|
||
|
|
x_vals_pred,
|
||
|
|
predict_action[:, i],
|
||
|
|
label="Model Output",
|
||
|
|
color="orange",
|
||
|
|
linewidth=2,
|
||
|
|
linestyle="--",
|
||
|
|
marker="x",
|
||
|
|
markersize=4,
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
plt.plot(
|
||
|
|
x_vals_gt,
|
||
|
|
gt_action[:, i],
|
||
|
|
color="blue",
|
||
|
|
linewidth=2,
|
||
|
|
linestyle="-",
|
||
|
|
marker="o",
|
||
|
|
markersize=3,
|
||
|
|
)
|
||
|
|
plt.plot(
|
||
|
|
x_vals_pred,
|
||
|
|
predict_action[:, i],
|
||
|
|
color="orange",
|
||
|
|
linewidth=2,
|
||
|
|
linestyle="--",
|
||
|
|
marker="x",
|
||
|
|
markersize=4,
|
||
|
|
)
|
||
|
|
|
||
|
|
plt.title(f"Action Dimension {i + 1}, L1 Loss: {l1_loss[:, i].mean():.6f}")
|
||
|
|
plt.xlabel("Number of Chunk")
|
||
|
|
plt.ylabel("Action Value")
|
||
|
|
plt.legend()
|
||
|
|
|
||
|
|
plt.suptitle(
|
||
|
|
f"Openloop Action Comparison for Episode {episode_index}, L1 Loss: {l1_loss.mean():.6f}"
|
||
|
|
)
|
||
|
|
plt.tight_layout(rect=[0, 0, 1, 0.98])
|
||
|
|
os.makedirs(save_path, exist_ok=True)
|
||
|
|
plt.savefig(f"{save_path}/{episode_index}.png")
|
||
|
|
plt.close()
|
||
|
|
_logger.info("Saved openloop plot to %s/%s.png", save_path, episode_index)
|