| import os |
| import gc |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import decord |
| import numpy as np |
| from torch.utils.data import Dataset, DataLoader |
| from transformers import AutoModel, AutoConfig, AutoTokenizer, get_cosine_schedule_with_warmup |
| from tqdm import tqdm |
| from torch.optim import AdamW |
| from torch.cuda.amp import autocast |
| import torchvision.transforms.v2 as T |
| from sklearn.metrics import accuracy_score |
| import warnings |
| warnings.filterwarnings("ignore") |
|
|
| |
| os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" |
| os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True" |
|
|
| |
| SEMANTIC_DICT = { |
| 'DeliverObject': ('Deliver', 'Forward', 'Object'), |
| 'MoveBackwardsWhileDrilling': ('Move', 'Backwards', 'Drill'), |
| 'MoveBackwardsWhilePolishing': ('Move', 'Backwards', 'Polisher'), |
| 'MoveDiagonallyBackwardLeftWithDrill': ('Move', 'Diagonally Backward Left', 'Drill'), |
| 'MoveDiagonallyBackwardLeftWithPolisher': ('Move', 'Diagonally Backward Left', 'Polisher'), |
| 'MoveDiagonallyBackwardRightWithDrill': ('Move', 'Diagonally Backward Right', 'Drill'), |
| 'MoveDiagonallyBackwardRightWithPolisher': ('Move', 'Diagonally Backward Right', 'Polisher'), |
| 'MoveDiagonallyForwardLeftWithDrill': ('Move', 'Diagonally Forward Left', 'Drill'), |
| 'MoveDiagonallyForwardLeftWithPolisher': ('Move', 'Diagonally Forward Left', 'Polisher'), |
| 'MoveDiagonallyForwardRightWithDrill': ('Move', 'Diagonally Forward Right', 'Drill'), |
| 'MoveDiagonallyForwardRightWithPolisher': ('Move', 'Diagonally Forward Right', 'Polisher'), |
| 'MoveForwardWhileDrilling': ('Move', 'Forward', 'Drill'), |
| 'MoveForwardWhilePolishing': ('Move', 'Forward', 'Polisher'), |
| 'MoveLeftWhileDrilling': ('Move', 'Left', 'Drill'), |
| 'MoveLeftWhilePolishing': ('Move', 'Left', 'Polisher'), |
| 'MoveRightWhileDrilling': ('Move', 'Right', 'Drill'), |
| 'MoveRightWhilePolishing': ('Move', 'Right', 'Polisher'), |
| 'NoCollaborativeWithDrilll': ('Stand', 'No Action', 'Drill'), |
| 'NoCollaborativeWithPolisher': ('Stand', 'No Action', 'Polisher'), |
| 'PickUpDrill': ('Pick Up', 'Upward', 'Drill'), |
| 'PickUpPolisher': ('Pick Up', 'Upward', 'Polisher'), |
| 'PickUpTheObject': ('Pick Up', 'Upward', 'Object'), |
| 'PutDownDrill': ('Put Down', 'Downward', 'Drill'), |
| 'PutDownPolisher': ('Put Down', 'Downward', 'Polisher'), |
| 'UsingTheDrill': ('Operate', 'Stationary', 'Drill'), |
| 'UsingThePolisher': ('Operate', 'Stationary', 'Polisher'), |
| 'Walking': ('Walk', 'Forward', 'Nothing'), |
| 'WalkingWithDrill': ('Walk', 'Forward', 'Drill'), |
| 'WalkingWithObject': ('Walk', 'Forward', 'Object'), |
| 'WalkingWithPolisher': ('Walk', 'Forward', 'Polisher') |
| } |
| ALL_CLASSES = list(SEMANTIC_DICT.keys()) |
|
|
| |
| PRETRAINED_PATH = "/root/autodl-tmp/checkpoints/sota_v2_best.pth" |
| BERT_ID = "bert-base-uncased" |
| MODEL_ID = "OpenGVLab/VideoMAEv2-giant" |
|
|
| NUM_FRAMES = 16 |
| IMG_SIZE = 224 |
| BATCH_SIZE = 1 |
| GRAD_ACCUM = 32 |
| LR = 2e-5 |
| EPOCHS = 30 |
|
|
| CACHE_DIR = "/root/autodl-tmp/hf_cache" |
| SAVE_DIR = "/root/autodl-tmp/checkpoints_final" |
| os.makedirs(CACHE_DIR, exist_ok=True) |
| os.makedirs(SAVE_DIR, exist_ok=True) |
|
|
| |
| def compute_text_prototypes(): |
| print("🚀 Pre-computing Semantic Prototypes...") |
| tokenizer = AutoTokenizer.from_pretrained(BERT_ID, cache_dir=CACHE_DIR) |
| bert = AutoModel.from_pretrained(BERT_ID, cache_dir=CACHE_DIR).cuda() |
| bert.eval() |
| prompts = [f"A worker {SEMANTIC_DICT[c][0]} {SEMANTIC_DICT[c][1]} using {SEMANTIC_DICT[c][2]}" for c in ALL_CLASSES] |
| with torch.no_grad(): |
| inputs = tokenizer(prompts, padding=True, truncation=True, return_tensors="pt").to('cuda') |
| outputs = bert(**inputs) |
| embeddings = outputs.last_hidden_state[:, 0, :] |
| embeddings = F.normalize(embeddings, dim=-1) |
| protos = embeddings.cpu() |
| del bert, tokenizer, inputs, outputs |
| torch.cuda.empty_cache() |
| gc.collect() |
| return protos |
|
|
| TEXT_PROTOTYPES = compute_text_prototypes() |
|
|
| |
| class VideoAugmentation: |
| def __init__(self): |
| |
| self.spatial_transform = T.Compose([ |
| T.ConvertImageDtype(torch.float32), |
| T.RandomResizedCrop(size=(IMG_SIZE, IMG_SIZE), scale=(0.8, 1.0), antialias=True), |
| T.RandomHorizontalFlip(p=0.5), |
| ]) |
| self.mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1, 1) |
| self.std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1, 1) |
|
|
| def __call__(self, video_tensor): |
| |
| |
| video_tensor = self.spatial_transform(video_tensor) |
| |
| |
| video_tensor = (video_tensor - self.mean) / self.std |
| |
| return video_tensor |
|
|
| |
| class HRI30_FullDataset(Dataset): |
| def __init__(self, root="/root/hri30/train"): |
| self.data = [] |
| self.aug = VideoAugmentation() |
| target_root = root if os.path.exists(root) and os.listdir(root) else "/root/hri30/train_set" |
| print(f"Scanning FULL dataset from {target_root}...") |
| for i in range(1, 31): |
| p = f"{target_root}/{i}" |
| if not os.path.exists(p): continue |
| for f in os.listdir(p): |
| if f.endswith('.avi'): |
| self.data.append((os.path.join(p, f), i-1)) |
| print(f"Loaded {len(self.data)} videos.") |
|
|
| def __len__(self): return len(self.data) |
| |
| def __getitem__(self, i): |
| path, label = self.data[i] |
| vr = decord.VideoReader(path) |
| if len(vr) > NUM_FRAMES: |
| start = np.random.randint(0, len(vr) - NUM_FRAMES) |
| idx = torch.arange(start, start + NUM_FRAMES) |
| else: |
| idx = torch.linspace(0, len(vr)-1, NUM_FRAMES).long() |
| |
| batch = vr.get_batch(idx) |
| |
| buffer = torch.from_numpy(batch.asnumpy()).permute(3, 0, 1, 2) |
| |
| |
| buffer = self.aug(buffer) |
| |
| |
| |
| return buffer, torch.tensor(label) |
|
|
| |
| class DualHeadMAE(nn.Module): |
| def __init__(self, video_model_id, prototypes): |
| super().__init__() |
| print("Loading Video Backbone...") |
| v_config = AutoConfig.from_pretrained(video_model_id, trust_remote_code=True, cache_dir=CACHE_DIR) |
| v_config.use_cache = False |
| self.visual = AutoModel.from_pretrained(video_model_id, trust_remote_code=True, config=v_config, cache_dir=CACHE_DIR, torch_dtype=torch.bfloat16) |
| |
| if hasattr(v_config, "hidden_size"): self.v_dim = v_config.hidden_size |
| else: self.v_dim = 1408 |
| |
| self.fc_cls = nn.Linear(self.v_dim, 30) |
| |
| self.register_buffer("text_prototypes", prototypes) |
| self.video_proj = nn.Linear(self.v_dim, 768) |
| self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07)) |
| self.dropout = nn.Dropout(0.5) |
|
|
| def forward(self, x, labels=None): |
| |
| outputs = self.visual(x) |
| |
| if hasattr(outputs, 'last_hidden_state'): feat = outputs.last_hidden_state.mean(dim=1) |
| elif isinstance(outputs, tuple): feat = outputs[0].mean(dim=1) if outputs[0].dim()==3 else outputs[0] |
| else: feat = outputs.mean(dim=1) if outputs.dim()==3 else outputs |
| |
| feat = self.dropout(feat) |
| |
| logits_cls = self.fc_cls(feat) |
| |
| v_emb = F.normalize(self.video_proj(feat), dim=-1) |
| text_protos = self.text_prototypes.to(feat.device).to(feat.dtype) |
| logits_sem = torch.matmul(v_emb, text_protos.t()) * self.logit_scale.exp() |
| |
| loss = None |
| if labels is not None: |
| loss_cls = F.cross_entropy(logits_cls, labels) |
| loss_sem = F.cross_entropy(logits_sem, labels) |
| loss = 0.7 * loss_cls + 0.3 * loss_sem |
| |
| return loss, logits_cls |
|
|
| |
| print("\n=== STARTING FINAL SOTA TRAINING (30 EPOCHS) ===") |
| ds = HRI30_FullDataset() |
| dl = DataLoader(ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4, pin_memory=True) |
|
|
| model = DualHeadMAE(MODEL_ID, TEXT_PROTOTYPES).cuda().to(torch.bfloat16) |
|
|
| print(f"Loading weights from {PRETRAINED_PATH}...") |
| checkpoint = torch.load(PRETRAINED_PATH) |
| new_sd = {} |
| for k, v in checkpoint.items(): |
| if "backbone" in k: new_sd[k.replace("backbone.", "")] = v |
| elif "fc" in k: new_sd["fc_cls." + k.replace("fc.", "")] = v |
| model.visual.load_state_dict(new_sd, strict=False) |
| model.load_state_dict(new_sd, strict=False) |
|
|
| param_groups = [ |
| {'params': model.visual.parameters(), 'lr': 1e-5}, |
| {'params': model.fc_cls.parameters(), 'lr': 1e-4}, |
| {'params': model.video_proj.parameters(), 'lr': 1e-3}, |
| {'params': [model.logit_scale], 'lr': 1e-3} |
| ] |
| opt = AdamW(param_groups, weight_decay=0.05) |
|
|
| total_steps = len(dl) * EPOCHS // GRAD_ACCUM |
| scheduler = get_cosine_schedule_with_warmup(opt, num_warmup_steps=int(0.1*total_steps), num_training_steps=total_steps) |
|
|
| model.train() |
| global_step = 0 |
| best_loss = 999.0 |
|
|
| for epoch in range(1, EPOCHS+1): |
| pbar = tqdm(dl, desc=f"Epoch {epoch}/{EPOCHS}") |
| epoch_loss = 0 |
| step_in_epoch = 0 |
| |
| for x, y in pbar: |
| x, y = x.cuda().to(torch.bfloat16), y.cuda() |
| with autocast(dtype=torch.bfloat16): |
| loss, _ = model(x, y) |
| loss = loss / GRAD_ACCUM |
| loss.backward() |
| |
| global_step += 1 |
| if global_step % GRAD_ACCUM == 0: |
| opt.step() |
| scheduler.step() |
| opt.zero_grad() |
| |
| step_in_epoch += 1 |
| epoch_loss += loss.item() * GRAD_ACCUM |
| pbar.set_postfix(loss=epoch_loss/step_in_epoch, lr=f"{scheduler.get_last_lr()[0]:.1e}") |
|
|
| avg_loss = epoch_loss / len(dl) |
| if avg_loss < best_loss: |
| best_loss = avg_loss |
| torch.save(model.state_dict(), f"{SAVE_DIR}/final_sota_best.pth") |
| print("🌟 Saved Best Final Model") |
| |
| torch.save(model.state_dict(), f"{SAVE_DIR}/final_sota_latest.pth") |
|
|
| print("🏆 最终 SOTA 训练完成!") |
|
|