t2m-gpt-vsl-code / train_t2m_trans_vsl.py
Tri1's picture
T2M-GPT VSL adaptation: Python sources only (82 files, no checkpoints or data)
8e5456b verified
Raw
History Blame Contribute Delete
15.3 kB
#!/usr/bin/env python3
"""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()
# Padding uses id nb_code+1, which is outside the (nb_code+1)-way head; those
# positions are masked out below, so clamp them to keep CE's index check happy.
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) # drop any end/pad id that slipped through
pred = net.decode_batch(idx) # (1, L*4, 256)
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))
##### ---- Frozen stage-1 VQ-VAE ---- #####
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")
##### ---- Frozen Vietnamese text encoder (CLIP replacement) ---- #####
text_enc = ViTextEncoder(args.text_model, device=args.device)
logger.info(f'text encoder {args.text_model}, dim {text_enc.dim}')
##### ---- Dataloaders ---- #####
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())
##### ---- GPT ---- #####
# block_size must hold the conditioning vector + the whole token sequence
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):
# Warm start from a stage-2 model trained on a DIFFERENT corpus (e.g. the
# Multi-VSL word model -> TriVis sentences). Only shape-compatible tensors are
# copied; what differs is non-learned anyway -- the causal mask buffers, the
# deterministic sincos positional encoding, and an unused pos_embedding whose
# sizes follow block_size. Note the token embedding and output head transfer in
# SHAPE but not in meaning: the 512 codes index a different codebook, so this
# is a warm start of the transformer body, not of the vocabulary.
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)
##### ---- Training ---- #####
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]
# T2M-GPT corruption: replace a fraction of input tokens with random codes
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()
# CE over the real tokens + the end token only (padding is ignored)
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)
# Both decoders are reported. Selection uses categorial sampling, which
# is what upstream's evaluation_transformer_test (the paper's reported
# test protocol) uses: greedy almost never emits the end token here, so
# it runs to the block limit and its metrics track length, not quality.
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})")
# Periodic immortal snapshots. Without these, "does training longer help?"
# is unanswerable after the fact: net_last is overwritten and net_best is
# picked by the flat/noisy 80-clip metric (it has landed anywhere from iter
# 2000 to 28000). Snapshots let the question be scored at fixed iterations
# on the real 300-clip protocol -- and on FGD / shuffle penalty, which
# diverge from DTW.
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()