""" Generate predicted latent z for offline evaluation. This script uses an MDM model trained on preprocessed_posterior data to generate predicted z values and saves them as .pt files for later decoding and evaluation in the SAME project. It loads text and length information from preprocessed posterior files, using only the 'text' and 'm_len' fields. Workflow: 1. Load the MDM model 2. Iterate over all test samples and generate predicted z from text (512-dim latent) 3. Save each sample's z_pred, text information, and length Output format: { 'z_pred': torch.Tensor, # shape (m_len, 512), predicted feature z 'text': { 'caption': str, # text description 'tokens': list, # token list 'start_time': float, 'end_time': float }, 'm_len': int # motion length } Output directory structure: output_dir/ repeat_1/ posterior_000000.pt posterior_000001.pt ... repeat_2/ ... config.json # saved generation config Usage example: python -m eval.eval_humanml_preproc \ --model_path ./save/first_exp_train/model000100000.pt \ --posterior_dir /path/to/val_posteriors \ --output_dir ./output/eval_first_exp \ --num_repetitions 20 \ --guidance_param 2.5 """ import os import sys import json import torch import numpy as np from datetime import datetime from argparse import ArgumentParser from tqdm import tqdm from pathlib import Path from torch.utils.data import Dataset, DataLoader # MDM imports from utils.fixseed import fixseed from utils.model_util import create_model_and_diffusion, load_saved_model from utils import dist_util from diffusion import logger from utils.sampler_util import ClassifierFreeSampleModel torch.multiprocessing.set_sharing_strategy('file_system') def evaluation_preproc_args(): """Parse evaluation arguments.""" parser = ArgumentParser(description='Generate predicted z for offline evaluation') # MDM model arguments parser.add_argument("--model_path", required=True, type=str, help="MDM model checkpoint path") parser.add_argument("--guidance_param", default=2.5, type=float, help="Classifier-free guidance scale") parser.add_argument("--use_ema", action='store_true', help="Use the EMA model") # Data source arguments parser.add_argument("--posterior_dir", required=True, type=str, help="Directory containing preprocessed posterior files") # Output arguments parser.add_argument("--output_dir", required=True, type=str, help="Output directory") # Evaluation arguments parser.add_argument("--num_repetitions", default=20, type=int, help="Number of repetitions") parser.add_argument("--seed", default=10, type=int, help="Random seed") parser.add_argument("--device", default=0, type=int, help="GPU device ID") parser.add_argument("--batch_size", default=32, type=int, help="Batch size") args = parser.parse_args() return args def load_mdm_args(model_path): """Load training arguments from the checkpoint directory.""" args_path = os.path.join(os.path.dirname(model_path), 'args.json') with open(args_path, 'r') as f: args_dict = json.load(f) # Create a simple object to store arguments. class Args: pass args = Args() for k, v in args_dict.items(): setattr(args, k, v) return args class PosteriorTextDataset(Dataset): """ Dataset that loads text and length information from preprocessed posterior files. Uses only the 'text' and 'm_len' fields and ignores 'mean', 'logvar', and 'sample'. """ def __init__(self, posterior_dir): """ Args: posterior_dir: directory containing preprocessed posterior files """ self.posterior_dir = posterior_dir # Scan the directory and collect all posterior file paths. all_files = os.listdir(posterior_dir) posterior_files = sorted([ f for f in all_files if f.endswith('.pt') and f not in ['meta_info.pt', 'preprocess_config.pt'] ]) self.file_paths = [os.path.join(posterior_dir, f) for f in posterior_files] print(f"[PosteriorTextDataset] Loaded {len(self.file_paths)} samples from {posterior_dir}") def __len__(self): return len(self.file_paths) def __getitem__(self, idx): """ Load text and length information for sample idx. Returns: dict: contains fields such as inp, text, and lengths """ posterior_data = torch.load(self.file_paths[idx]) # Get text information. text_info = posterior_data['text'] m_len = posterior_data['m_len'] # Handle text information across multiple formats. if isinstance(text_info, dict): caption = text_info.get('caption', '') tokens = text_info.get('tokens', []) else: caption = str(text_info) tokens = [] # Convert the token list to a string joined with '_'. tokens_str = '_'.join(tokens) if tokens else '' # Create a dummy inp; only shape information is needed. # Shape: (C, 1, T), where C=512 and T=m_len. inp = torch.zeros(512, 1, m_len) # Extract the filename without path. filename = os.path.basename(self.file_paths[idx]) return { 'inp': inp, 'text': caption, 'tokens': tokens_str, 'lengths': m_len, 'text_info': text_info, # Save full text_info for output. 'filename': filename, # Add filename. } def collate_posterior_text(batch): """ Collate function for PosteriorTextDataset Output format is compatible with MDM collate. """ # Find the maximum length. max_len = max(item['lengths'] for item in batch) batch_size = len(batch) # Create padded tensors. # inp: (B, C, 1, T_max) inp_padded = torch.zeros(batch_size, 512, 1, max_len) # mask: (B, 1, 1, T_max) mask = torch.zeros(batch_size, 1, 1, max_len).bool() lengths = [] texts = [] tokens_list = [] text_infos = [] filenames = [] for i, item in enumerate(batch): m_len = item['lengths'] lengths.append(m_len) texts.append(item['text']) tokens_list.append(item['tokens']) text_infos.append(item['text_info']) filenames.append(item['filename']) # Set mask; valid positions are True. mask[i, :, :, :m_len] = True lengths = torch.tensor(lengths) # Build model_kwargs. cond = { 'y': { 'mask': mask, 'lengths': lengths, 'text': texts, 'tokens': tokens_list, 'text_infos': text_infos, # Also save full text_info. 'filenames': filenames, # Add filename list. } } return inp_padded, cond def get_posterior_text_loader(posterior_dir, batch_size=32, shuffle=False, num_workers=4): """ Create a DataLoader that loads text information from posterior files. """ dataset = PosteriorTextDataset(posterior_dir) dataloader = DataLoader( dataset, batch_size=batch_size, shuffle=shuffle, num_workers=num_workers, collate_fn=collate_posterior_text, pin_memory=True, drop_last=False, ) return dataloader class MDMLatentGenerator: """ Latent generator for the MDM model. Wraps the MDM model and provides an interface for generating z. """ def __init__(self, model, diffusion, args, scale=2.5): self.model = model self.diffusion = diffusion self.args = args self.scale = scale self.sample_fn = diffusion.p_sample_loop def generate(self, model_kwargs, motion_shape): """ Generate latent z. Args: model_kwargs: dictionary containing conditioning information motion_shape: output shape (B, njoints, nfeats, T) Returns: sample: generated latent z with shape (B, T, C) """ # Add CFG scale. if self.scale != 1.: model_kwargs['y']['scale'] = torch.ones(motion_shape[0], device=dist_util.dev()) * self.scale # Encode text once. if 'text' in model_kwargs['y'].keys() and 'text_embed' not in model_kwargs['y'].keys(): model_kwargs['y']['text_embed'] = self.model.encode_text(model_kwargs['y']['text']) # Generate. sample = self.sample_fn( self.model, motion_shape, clip_denoised=False, model_kwargs=model_kwargs, skip_timesteps=0, init_image=None, progress=False, dump_steps=None, noise=None, const_noise=False, ) # Convert shape: (B, C, 1, T) -> (B, T, C). sample = sample.squeeze(2).permute(0, 2, 1) # (B, T, C) return sample def generate_and_save_latents(mdm_generator, dataloader, output_dir, repeat_idx, progress=True): """ Generate latent z with MDM and save it. Args: mdm_generator: MDM model generator dataloader: data loader that provides text conditions output_dir: output directory repeat_idx: current repetition index, starting from 1 progress: whether to show a progress bar Returns: num_generated: number of generated samples """ # Create the repetition directory. repeat_dir = os.path.join(output_dir, f'repeat_{repeat_idx}') os.makedirs(repeat_dir, exist_ok=True) real_num_batches = len(dataloader) desc = f'Generating repeat_{repeat_idx}' iterator = tqdm(enumerate(dataloader), total=real_num_batches, desc=desc) if progress else enumerate(dataloader) sample_idx = 0 with torch.no_grad(): for i, (motion, model_kwargs) in iterator: # Move to device. model_kwargs['y'] = { key: val.to(dist_util.dev()) if torch.is_tensor(val) else val for key, val in model_kwargs['y'].items() } # Get tokens. tokens = [t.split('_') for t in model_kwargs['y']['tokens']] # Determine generation shape. batch_size = motion.shape[0] n_frames = motion.shape[-1] motion_shape = (batch_size, mdm_generator.model.njoints, mdm_generator.model.nfeats, n_frames) # Generate latent z. latent_z = mdm_generator.generate(model_kwargs, motion_shape) # (B, T, C) # Get valid frame counts. lengths = model_kwargs['y']['lengths'].cpu().numpy() # Save each sample. for bs_i in range(batch_size): m_len = int(lengths[bs_i]) # Slice the valid part of z_pred. z_pred = latent_z[bs_i, :m_len, :].cpu() # (m_len, 512) # Use the full text_info from the posterior file. text_info = model_kwargs['y']['text_infos'][bs_i] # Get the original filename. original_filename = model_kwargs['y']['filenames'][bs_i] # Build the save dictionary. save_dict = { 'z_pred': z_pred, 'text': text_info, 'm_len': m_len } # Save the file using the original filename. save_path = os.path.join(repeat_dir, original_filename) torch.save(save_dict, save_path) sample_idx += 1 return sample_idx def main(): args = evaluation_preproc_args() fixseed(args.seed) replication_times = args.num_repetitions print(f'Number of repetitions: {replication_times}') # Create the output directory. os.makedirs(args.output_dir, exist_ok=True) # Set up distributed environment. dist_util.setup_dist(args.device) logger.configure() # ========================================= # 1. Load the MDM model # ========================================= logger.log("Loading MDM model arguments...") mdm_args = load_mdm_args(args.model_path) # Ensure required arguments exist. if not hasattr(mdm_args, 'pred_len'): mdm_args.pred_len = 0 mdm_args.context_len = 0 if not hasattr(mdm_args, 'autoregressive'): mdm_args.autoregressive = False logger.log("Creating data loader...") # Use preprocessed posterior files, using only text and m_len. logger.log(f"Using posterior files from: {args.posterior_dir}") gen_loader = get_posterior_text_loader( posterior_dir=args.posterior_dir, batch_size=args.batch_size, shuffle=False, num_workers=4 ) logger.log("Creating MDM model and diffusion...") # Create the model directly; no dummy loader is needed. model, diffusion = create_model_and_diffusion(mdm_args, None) logger.log(f"Loading MDM checkpoints from [{args.model_path}]...") load_saved_model(model, args.model_path, use_avg=args.use_ema) if args.guidance_param != 1: model = ClassifierFreeSampleModel(model) model.to(dist_util.dev()) model.eval() # ========================================= # 2. Create generator # ========================================= mdm_generator = MDMLatentGenerator(model, diffusion, mdm_args, scale=args.guidance_param) # ========================================= # 3. Save configuration information # ========================================= num_samples = len(gen_loader.dataset) config = { 'model_path': args.model_path, 'guidance_param': args.guidance_param, 'use_ema': args.use_ema, 'num_samples': num_samples, 'num_repetitions': replication_times, 'seed': args.seed, 'batch_size': args.batch_size, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'njoints': mdm_args.njoints, 'latent_dim': mdm_args.latent_dim, 'posterior_dir': args.posterior_dir, } config_path = os.path.join(args.output_dir, 'config.json') with open(config_path, 'w') as f: json.dump(config, f, indent=4) print(f'Config saved to {config_path}') # ========================================= # 4. Generate and save latent z # ========================================= logger.log("Starting generation...") for repeat_idx in range(1, replication_times + 1): print(f'\n{"="*60}') print(f'Repetition {repeat_idx}/{replication_times}') print(f'{"="*60}') # Set a different random seed for each repetition. fixseed(args.seed + repeat_idx - 1) num_generated = generate_and_save_latents( mdm_generator=mdm_generator, dataloader=gen_loader, output_dir=args.output_dir, repeat_idx=repeat_idx, progress=True ) print(f'Generated {num_generated} samples for repeat_{repeat_idx}') print(f'\n{"="*60}') print(f'Generation completed!') print(f'Output directory: {args.output_dir}') print(f'Total repetitions: {replication_times}') print(f'Samples per repetition: {num_generated}') print(f'{"="*60}') if __name__ == '__main__': main()