| |
| """T2M-GPT stage 2 on Full_TriVis: Vietnamese gloss -> pose-token GPT. |
| |
| Same model and recipe as the paper's `train_t2m_trans.py`: the frozen stage-1 |
| VQ-VAE supplies discrete targets, a causal transformer is trained with CE on |
| next-token prediction, and inputs are corrupted with probability 1-pkeep |
| (the paper's "corruption" trick that lets the model recover from its own |
| generation errors). |
| |
| Two substitutions were unavoidable: |
| * CLIP's text tower is English-only, so conditioning comes from a frozen |
| Vietnamese encoder (PhoBERT by default). As in the paper, this is a single |
| pooled sentence vector prepended to the token sequence. |
| * The paper's validation uses HumanML3D FID / R-precision evaluators, which do |
| not exist for VSL. Validation here is teacher-forced CE + token accuracy, |
| plus free-running generation decoded through the VQ-VAE and scored with |
| MPJPE against ground truth (and a length-ratio, since over-generation was the |
| dominant failure of this project's earlier gloss->pose baseline). |
| """ |
| import argparse |
| import json |
| import os |
|
|
| import numpy as np |
| import torch |
| from torch.distributions import Categorical |
| from torch.utils.tensorboard import SummaryWriter |
|
|
| import models.t2m_trans as trans |
| import models.vqvae as vqvae |
| import options.option_vsl as option_vsl |
| import utils.utils_model as utils_model |
| from dataset import dataset_vsl |
| from models.text_encoder_vi import ViTextEncoder |
| from utils.losses_vsl import mpjpe_groups |
|
|
|
|
| def build_vqvae(resume_pth, device): |
| ckpt = torch.load(resume_pth, map_location='cpu') |
| targs = argparse.Namespace(**ckpt['args']) |
| net = vqvae.HumanVQVAE(targs, targs.nb_code, targs.code_dim, targs.output_emb_width, |
| targs.down_t, targs.stride_t, targs.width, targs.depth, |
| targs.dilation_growth_rate, targs.vq_act, targs.vq_norm, |
| input_dim=targs.input_dim) |
| net.load_state_dict(ckpt['net'], strict=True) |
| net.eval().to(device) |
| for p in net.parameters(): |
| p.requires_grad = False |
| return net, targs, ckpt |
|
|
|
|
| def masked_ce(logits, targets, tlen): |
| """Per-sample mean CE over real tokens (+ end token), then mean over batch. |
| |
| Vectorized equivalent of upstream's `for i in range(bs)` loop -- same value, |
| but ~4x faster per iteration, which matters at 60k iters. |
| Returns (loss, n_correct, n_tokens). |
| """ |
| B, L, V = logits.shape |
| valid = (torch.arange(L, device=logits.device)[None, :] < (tlen[:, None] + 1)).float() |
| |
| |
| targets = targets.clamp(max=V - 1) |
| ce = torch.nn.functional.cross_entropy( |
| logits.reshape(B * L, -1), targets.reshape(B * L), reduction='none').view(B, L) |
| loss = ((ce * valid).sum(1) / valid.sum(1).clamp(min=1)).mean() |
| with torch.no_grad(): |
| correct = int((((logits.argmax(-1) == targets).float()) * valid).sum()) |
| return loss, correct, int(valid.sum()) |
|
|
|
|
| @torch.no_grad() |
| def validate_teacher_forced(trans_encoder, loader, text_enc, device, max_batches=40): |
| """Teacher-forced CE + token accuracy on the val split.""" |
| trans_encoder.eval() |
| tot_loss, tot_right, tot_tok, nb = 0.0, 0, 0, 0 |
| for bi, (texts, tokens, tlen) in enumerate(loader): |
| if bi >= max_batches: |
| break |
| tokens, tlen = tokens.to(device), tlen.to(device) |
| feat = text_enc(list(texts)) |
| logits = trans_encoder(tokens[:, :-1], feat) |
| loss, correct, ntok = masked_ce(logits, tokens[:, :logits.shape[1]], tlen) |
| tot_loss += loss.item() * tokens.shape[0] |
| tot_right += correct |
| tot_tok += ntok |
| nb += tokens.shape[0] |
| trans_encoder.train() |
| return tot_loss / max(nb, 1), 100.0 * tot_right / max(tot_tok, 1) |
|
|
|
|
| @torch.no_grad() |
| def validate_generation(trans_encoder, net, store, items, text_enc, device, |
| max_tokens, nb_code, text_field='gloss', if_categorial=False): |
| """Free-running generation -> VQ decode -> MPJPE vs ground truth. |
| |
| Scored on the overlapping prefix of generated and reference motion, so a |
| length mismatch shows up in len_ratio rather than silently inflating MPJPE. |
| """ |
| trans_encoder.eval() |
| mean = torch.from_numpy(store.mean).to(device) |
| std = torch.from_numpy(store.std).to(device) |
| acc = {"all": 0.0, "body": 0.0, "face": 0.0, "hands": 0.0} |
| ratios, n_used, n_empty = [], 0, 0 |
|
|
| for i in items: |
| c = store.index[i] |
| feat = text_enc([c[text_field]]) |
| idx = trans_encoder.sample(feat, if_categorial=if_categorial) |
| if idx is None or idx.numel() == 0: |
| n_empty += 1 |
| continue |
| idx = idx.clamp(max=nb_code - 1) |
| pred = net.decode_batch(idx) |
|
|
| motion, mask = store.get(i) |
| gt = torch.from_numpy(motion).unsqueeze(0).to(device) |
| mk = torch.from_numpy(mask).unsqueeze(0).to(device) |
| ratios.append(pred.shape[1] / gt.shape[1]) |
|
|
| T = min(pred.shape[1], gt.shape[1]) |
| nk = store.layout.n_kpts |
| valid = mk[:, :T].view(1, T, nk, 2)[..., 0] |
| g = mpjpe_groups(pred[:, :T] * std + mean, gt[:, :T] * std + mean, valid, |
| groups=store.layout.metric_groups()) |
| for k in acc: |
| acc[k] += g[k] |
| n_used += 1 |
|
|
| trans_encoder.train() |
| n = max(n_used, 1) |
| return ({k: v / n for k, v in acc.items()}, |
| float(np.mean(ratios)) if ratios else 0.0, n_used, n_empty) |
|
|
|
|
| def main(): |
| args = option_vsl.get_trans_args() |
| torch.manual_seed(args.seed) |
| np.random.seed(args.seed) |
| device = torch.device(args.device) |
|
|
| args.out_dir = os.path.join(args.out_dir, args.exp_name) |
| os.makedirs(args.out_dir, exist_ok=True) |
| logger = utils_model.get_logger(args.out_dir) |
| writer = SummaryWriter(args.out_dir) |
| logger.info(json.dumps(vars(args), indent=4, sort_keys=True)) |
|
|
| |
| net, targs, ckpt = build_vqvae(args.resume_pth, device) |
| assert targs.nb_code == args.nb_code, \ |
| f'--nb-code {args.nb_code} != stage-1 codebook {targs.nb_code}' |
| logger.info(f"stage-1: iter {ckpt.get('iter')} val_recon {ckpt.get('val_recon'):.5f} " |
| f"hands MPJPE {ckpt['val_mpjpe']['hands']:.5f} " |
| f"codes {ckpt.get('codes_used')}/{targs.nb_code} <-- generation ceiling") |
|
|
| |
| text_enc = ViTextEncoder(args.text_model, device=args.device) |
| logger.info(f'text encoder {args.text_model}, dim {text_enc.dim}') |
|
|
| |
| train_set = dataset_vsl.VSLText2TokenDataset( |
| args.data_dir, os.path.join(args.token_dir, 'train'), 'train', |
| args.nb_code, max_tokens=args.max_tokens, text_field=args.text_field) |
| train_loader = torch.utils.data.DataLoader( |
| train_set, args.batch_size, shuffle=True, num_workers=args.num_workers, |
| drop_last=True, pin_memory=True, persistent_workers=args.num_workers > 0) |
| train_loader_iter = dataset_vsl.cycle(train_loader) |
|
|
| val_set = dataset_vsl.VSLText2TokenDataset( |
| args.data_dir, os.path.join(args.token_dir, 'val'), 'val', |
| args.nb_code, max_tokens=args.max_tokens, text_field=args.text_field, |
| augment_crop=False) |
| val_loader = torch.utils.data.DataLoader(val_set, args.batch_size, shuffle=False, |
| num_workers=4, drop_last=False) |
| rng = np.random.RandomState(0) |
| gen_items = sorted(rng.choice(len(val_set.store.index), |
| size=min(args.eval_samples, len(val_set.store.index)), |
| replace=False).tolist()) |
|
|
| |
| |
| block_size = args.max_tokens + 1 |
| trans_encoder = trans.Text2Motion_Transformer( |
| num_vq=args.nb_code, embed_dim=args.embed_dim_gpt, clip_dim=text_enc.dim, |
| block_size=block_size, num_layers=args.num_layers, n_head=args.n_head_gpt, |
| drop_out_rate=args.drop_out_rate, fc_rate=args.ff_rate) |
| if args.resume_trans: |
| logger.info(f'loading transformer checkpoint from {args.resume_trans}') |
| tck = torch.load(args.resume_trans, map_location='cpu') |
| trans_encoder.load_state_dict(tck['trans'], strict=True) |
| elif getattr(args, 'init_trans', None): |
| |
| |
| |
| |
| |
| |
| |
| src = torch.load(args.init_trans, map_location='cpu')['trans'] |
| tgt = trans_encoder.state_dict() |
| ok = {k: v for k, v in src.items() if k in tgt and tgt[k].shape == v.shape} |
| skipped = [k for k in tgt if k not in ok] |
| trans_encoder.load_state_dict({**tgt, **ok}, strict=True) |
| n_ok = sum(ok[k].numel() for k in ok) |
| n_all = sum(v.numel() for v in tgt.values()) |
| logger.info(f'warm start from {args.init_trans}: copied {len(ok)}/{len(tgt)} ' |
| f'tensors = {100*n_ok/n_all:.1f}% of params; ' |
| f'skipped (non-learned/shape): {len(skipped)}') |
| trans_encoder.train().to(device) |
| logger.info(f'GPT params: {sum(p.numel() for p in trans_encoder.parameters())/1e6:.2f}M, ' |
| f'block_size {block_size}') |
|
|
| optimizer = utils_model.initial_optim(args.decay_option, args.lr, args.weight_decay, |
| trans_encoder, args.optimizer) |
| scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=args.lr_scheduler, |
| gamma=args.gamma) |
|
|
| |
| best_hands, best_iter = 1e9, 0 |
| avg_loss, right_num, nb_sample = 0.0, 0, 0 |
|
|
| for nb_iter in range(1, args.total_iter + 1): |
| texts, tokens, tlen = next(train_loader_iter) |
| tokens, tlen = tokens.to(device), tlen.to(device) |
| bs = tokens.shape[0] |
| feat = text_enc(list(texts)) |
|
|
| input_index = tokens[:, :-1] |
| |
| if args.pkeep == -1: |
| proba = np.random.rand(1)[0] |
| mask = torch.bernoulli(proba * torch.ones_like(input_index, dtype=torch.float)) |
| else: |
| mask = torch.bernoulli(args.pkeep * torch.ones_like(input_index, dtype=torch.float)) |
| mask = mask.round().to(dtype=torch.int64) |
| r_indices = torch.randint_like(input_index, args.nb_code) |
| a_indices = mask * input_index + (1 - mask) * r_indices |
|
|
| logits = trans_encoder(a_indices, feat).contiguous() |
|
|
| |
| loss_cls, correct, ntok = masked_ce(logits, tokens[:, :logits.shape[1]], tlen) |
|
|
| optimizer.zero_grad() |
| loss_cls.backward() |
| optimizer.step() |
| scheduler.step() |
|
|
| avg_loss += loss_cls.item() |
| right_num += correct |
| nb_sample += ntok |
|
|
| if nb_iter % args.print_iter == 0: |
| avg_loss /= args.print_iter |
| acc = 100.0 * right_num / max(nb_sample, 1) |
| writer.add_scalar('./Loss/train', avg_loss, nb_iter) |
| writer.add_scalar('./ACC/train', acc, nb_iter) |
| logger.info(f"Train. Iter {nb_iter} : Loss. {avg_loss:.5f}, ACC. {acc:.4f}") |
| avg_loss, right_num, nb_sample = 0.0, 0, 0 |
|
|
| if nb_iter % args.eval_iter == 0 or nb_iter == args.total_iter: |
| vl, vacc = validate_teacher_forced(trans_encoder, val_loader, text_enc, device) |
| |
| |
| |
| |
| gen = {} |
| for name, cat in (('cat', True), ('greedy', False)): |
| gen[name] = validate_generation( |
| trans_encoder, net, val_set.store, gen_items, text_enc, device, |
| args.max_tokens, args.nb_code, args.text_field, if_categorial=cat) |
| mp, ratio, n_used, n_empty = gen['cat'] |
| gmp, gratio = gen['greedy'][0], gen['greedy'][1] |
|
|
| writer.add_scalar('./Loss/val', vl, nb_iter) |
| writer.add_scalar('./ACC/val', vacc, nb_iter) |
| writer.add_scalar('./Gen/MPJPE_hands', mp['hands'], nb_iter) |
| writer.add_scalar('./Gen/len_ratio', ratio, nb_iter) |
| writer.add_scalar('./GenGreedy/MPJPE_hands', gmp['hands'], nb_iter) |
| writer.add_scalar('./GenGreedy/len_ratio', gratio, nb_iter) |
| logger.info(f"Eval. Iter {nb_iter} : val_loss {vl:.5f} val_acc {vacc:.4f} " |
| f"| CAT MPJPE all {mp['all']:.5f} body {mp['body']:.5f} " |
| f"hands {mp['hands']:.5f} len_ratio {ratio:.3f} " |
| f"| GREEDY hands {gmp['hands']:.5f} len_ratio {gratio:.3f} " |
| f"({n_used} scored, {n_empty} empty)") |
| payload = {'trans': trans_encoder.state_dict(), 'args': vars(args), |
| 'iter': nb_iter, 'val_loss': vl, 'val_acc': vacc, |
| 'gen_mpjpe': mp, 'len_ratio': ratio, |
| 'gen_mpjpe_greedy': gmp, 'len_ratio_greedy': gratio} |
| torch.save(payload, os.path.join(args.out_dir, 'net_last.pth')) |
| if mp['hands'] < best_hands: |
| best_hands, best_iter = mp['hands'], nb_iter |
| torch.save(payload, os.path.join(args.out_dir, 'net_best.pth')) |
| logger.info(f" --> new best (gen hands MPJPE {best_hands:.5f})") |
| |
| |
| |
| |
| |
| |
| if args.save_every and nb_iter % args.save_every == 0: |
| snap = os.path.join(args.out_dir, f'net_iter{nb_iter}.pth') |
| torch.save(payload, snap) |
| logger.info(f" --> snapshot {os.path.basename(snap)}") |
|
|
| logger.info(f"Done. best gen hands MPJPE {best_hands:.5f} @ iter {best_iter}") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|