| """ |
| 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 |
|
|
| |
| 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') |
| |
| |
| 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") |
| |
| |
| parser.add_argument("--posterior_dir", required=True, type=str, |
| help="Directory containing preprocessed posterior files") |
| |
| |
| parser.add_argument("--output_dir", required=True, type=str, |
| help="Output directory") |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| 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]) |
| |
| |
| text_info = posterior_data['text'] |
| m_len = posterior_data['m_len'] |
| |
| |
| if isinstance(text_info, dict): |
| caption = text_info.get('caption', '') |
| tokens = text_info.get('tokens', []) |
| else: |
| caption = str(text_info) |
| tokens = [] |
| |
| |
| tokens_str = '_'.join(tokens) if tokens else '' |
| |
| |
| |
| inp = torch.zeros(512, 1, m_len) |
| |
| |
| filename = os.path.basename(self.file_paths[idx]) |
| |
| return { |
| 'inp': inp, |
| 'text': caption, |
| 'tokens': tokens_str, |
| 'lengths': m_len, |
| 'text_info': text_info, |
| 'filename': filename, |
| } |
|
|
|
|
| def collate_posterior_text(batch): |
| """ |
| Collate function for PosteriorTextDataset |
| Output format is compatible with MDM collate. |
| """ |
| |
| max_len = max(item['lengths'] for item in batch) |
| |
| batch_size = len(batch) |
| |
| |
| |
| inp_padded = torch.zeros(batch_size, 512, 1, max_len) |
| |
| |
| 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']) |
| |
| |
| mask[i, :, :, :m_len] = True |
| |
| lengths = torch.tensor(lengths) |
| |
| |
| cond = { |
| 'y': { |
| 'mask': mask, |
| 'lengths': lengths, |
| 'text': texts, |
| 'tokens': tokens_list, |
| 'text_infos': text_infos, |
| 'filenames': filenames, |
| } |
| } |
| |
| 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) |
| """ |
| |
| if self.scale != 1.: |
| model_kwargs['y']['scale'] = torch.ones(motion_shape[0], |
| device=dist_util.dev()) * self.scale |
| |
| |
| 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']) |
| |
| |
| 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, |
| ) |
| |
| |
| sample = sample.squeeze(2).permute(0, 2, 1) |
| |
| 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 |
| """ |
| |
| 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: |
| |
| model_kwargs['y'] = { |
| key: val.to(dist_util.dev()) if torch.is_tensor(val) else val |
| for key, val in model_kwargs['y'].items() |
| } |
| |
| |
| tokens = [t.split('_') for t in model_kwargs['y']['tokens']] |
| |
| |
| batch_size = motion.shape[0] |
| n_frames = motion.shape[-1] |
| motion_shape = (batch_size, mdm_generator.model.njoints, mdm_generator.model.nfeats, n_frames) |
| |
| |
| latent_z = mdm_generator.generate(model_kwargs, motion_shape) |
| |
| |
| lengths = model_kwargs['y']['lengths'].cpu().numpy() |
| |
| |
| for bs_i in range(batch_size): |
| m_len = int(lengths[bs_i]) |
| |
| |
| z_pred = latent_z[bs_i, :m_len, :].cpu() |
| |
| |
| text_info = model_kwargs['y']['text_infos'][bs_i] |
| |
| |
| original_filename = model_kwargs['y']['filenames'][bs_i] |
| |
| |
| save_dict = { |
| 'z_pred': z_pred, |
| 'text': text_info, |
| 'm_len': m_len |
| } |
| |
| |
| 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}') |
| |
| |
| os.makedirs(args.output_dir, exist_ok=True) |
| |
| |
| dist_util.setup_dist(args.device) |
| logger.configure() |
| |
| |
| |
| |
| logger.log("Loading MDM model arguments...") |
| mdm_args = load_mdm_args(args.model_path) |
| |
| |
| 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...") |
| |
| |
| 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...") |
| |
| 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() |
| |
| |
| |
| |
| mdm_generator = MDMLatentGenerator(model, diffusion, mdm_args, scale=args.guidance_param) |
| |
| |
| |
| |
| 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}') |
| |
| |
| |
| |
| 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}') |
| |
| |
| 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() |
|
|