Download src/03_embed.py from kaktusik21/3d-mesh-quality-control: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/kaktusik21/3d-mesh-quality-control/resolve/main/src/03_embed.py
- Command line
-
hf download hf://kaktusik21/3d-mesh-quality-control/src/03_embed.py
-
curl -L -o 03_embed.py https://huggingface.co/kaktusik21/3d-mesh-quality-control/resolve/main/src/03_embed.py
10 kB
| # KAGGLE: {"gpu": true, "internet": true, "datasets": [], "kernels": ["alekstop/02-imgstats"], "acc": "NvidiaTeslaT4"} | |
| # %% [markdown] | |
| # # 03 — Замороженные эмбеддинги бэкбонов (единственная обязательная GPU-трата) | |
| # | |
| # Читает кэш 320px из ноутбука `02-imgstats` (а не 28.6 ГБ исходников), гоняет | |
| # 6 ракурсов каждого объекта через несколько предобученных бэкбонов и сохраняет: | |
| # | |
| # * `emb_<model>.npy` — (N, 6, D) fp16, пулинг всего изображения; | |
| # * `tok_<primary>.npy` — (N, 6, 16, D) fp16, патч-токены, спулленные в сетку 4x4. | |
| # Они нужны для cross-view трансформера (Слой 2) и для карт внимания | |
| # в ноутбуке интерпретируемости. | |
| # | |
| # Домен здесь не natural images, а карты нормалей, поэтому берём несколько | |
| # принципиально разных бэкбонов: self-supervised ViT (DINO), vision-language ViT | |
| # (SigLIP) и свёрточный (ConvNeXt-V2) — последний заметно чувствительнее к | |
| # высокочастотной текстуре. | |
| # %% | |
| import os, sys, io, json, time, math, tarfile, random, warnings | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from PIL import Image | |
| warnings.filterwarnings('ignore') | |
| SEED = 42 | |
| random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED) | |
| torch.backends.cudnn.benchmark = True | |
| T0 = time.time() | |
| WORK = Path(os.environ.get('MQC_WORK', '/kaggle/working')) # в Colab задаётся окружением | |
| IN = Path(os.environ.get('MQC_IN', '/kaggle/input')) | |
| DEV = 'cuda' if torch.cuda.is_available() else 'cpu' | |
| NGPU = torch.cuda.device_count() | |
| print('torch', torch.__version__, '| GPU:', [torch.cuda.get_device_name(i) for i in range(NGPU)]) | |
| # Колёса torch cu12.8 собраны начиная с sm_70; на P100 (sm_60) любое обращение к | |
| # GPU падает с 'no kernel image is available'. Проверяем сразу, до скачивания | |
| # весов, чтобы не терять время сессии. | |
| CAP = torch.cuda.get_device_capability(0) if DEV == 'cuda' else (0, 0) | |
| print('compute capability: %d.%d' % CAP) | |
| if DEV == 'cuda' and CAP[0] < 7: | |
| raise RuntimeError('GPU sm_%d%d is too old for this torch build; ' | |
| 'push with machine_shape=NvidiaTeslaT4' % CAP) | |
| USE_HALF = CAP >= (7, 0) | |
| CACHE_DIR = next(p.parent for p in IN.rglob('img320_train.tar')) | |
| print('кэш:', CACHE_DIR, sorted(p.name for p in CACHE_DIR.iterdir())[:8]) | |
| # %% | |
| # --- индекс tar-архивов: имя -> (offset, size), чтение без распаковки --- | |
| class TarIndex: | |
| def __init__(self, path): | |
| self.path = str(path) | |
| self.idx = {} | |
| with tarfile.open(path) as tf: | |
| for m in tf: | |
| if m.isfile(): | |
| self.idx[Path(m.name).stem] = (m.offset_data, m.size) | |
| self.fh = None | |
| def read(self, key): | |
| if self.fh is None: | |
| self.fh = open(self.path, 'rb') | |
| off, size = self.idx[key] | |
| self.fh.seek(off) | |
| return self.fh.read(size) | |
| t = time.time() | |
| TARS = [TarIndex(CACHE_DIR / 'img320_train.tar'), TarIndex(CACHE_DIR / 'img320_test.tar')] | |
| KEY2TAR = {} | |
| for ti, tar in enumerate(TARS): | |
| for k in tar.idx: | |
| KEY2TAR[k] = ti | |
| print('объектов в кэше:', len(KEY2TAR), '| индексация %.1f с' % (time.time() - t)) | |
| ids_train = list(TARS[0].idx) | |
| ids_test = list(TARS[1].idx) | |
| IDS = ids_train + ids_test | |
| CACHE, NCOL, NROW, NVIEW = 320, 3, 2, 6 | |
| # %% | |
| # --- датасет: один объект -> 6 ракурсов --- | |
| # EMB<<< | |
| from torch.utils.data import Dataset, DataLoader | |
| import torch.nn.functional as Fn | |
| class ViewDS(Dataset): | |
| def __init__(self, ids, size, mean, std): | |
| self.ids = ids | |
| self.size = size | |
| self.mean = torch.tensor(mean).view(3, 1, 1) | |
| self.std = torch.tensor(std).view(3, 1, 1) | |
| def __len__(self): | |
| return len(self.ids) | |
| def __getitem__(self, i): | |
| iid = self.ids[i] | |
| blob = TARS[KEY2TAR[iid]].read(iid) | |
| a = np.asarray(Image.open(io.BytesIO(blob)).convert('RGB')) | |
| vs = np.stack([a[r * CACHE:(r + 1) * CACHE, c * CACHE:(c + 1) * CACHE] | |
| for r in range(NROW) for c in range(NCOL)]) | |
| x = torch.from_numpy(vs).permute(0, 3, 1, 2).float() / 255.0 | |
| if self.size != CACHE: | |
| x = Fn.interpolate(x, size=(self.size, self.size), mode='bicubic', | |
| align_corners=False, antialias=True).clamp(0, 1) | |
| return (x - self.mean) / self.std | |
| def make_loader(size, mean, std, bs): | |
| return DataLoader(ViewDS(IDS, size, mean, std), batch_size=bs, shuffle=False, | |
| num_workers=2, pin_memory=True, persistent_workers=False) | |
| # EMB>>> | |
| # %% | |
| # --- бэкбоны --- | |
| # EMB<<< | |
| import timm | |
| MODELS = [ | |
| # (имя в выводе, спека timm, разрешение, нужны ли патч-токены) | |
| ('dinov2b', 'vit_base_patch14_dinov2.lvd142m', 224, True), | |
| ('siglip2b', 'vit_base_patch16_siglip_256.webli', 256, False), | |
| ('cnxv2t', 'convnextv2_tiny.fcmae_ft_in22k_in1k', 256, False), | |
| ] | |
| def build(spec, size): | |
| """Создать бэкбон под наше разрешение. | |
| У ViT из timm разрешение зашито в веса (у DINOv2 это 518) и patch_embed | |
| проверяет его строго. Пробрасываем img_size — timm сам интерполирует | |
| позиционные эмбеддинги при загрузке; для свёрточных моделей такого аргумента | |
| нет, поэтому падаем обратно на обычное создание. | |
| """ | |
| try: | |
| return timm.create_model(spec, pretrained=True, num_classes=0, | |
| img_size=size, dynamic_img_size=True) | |
| except TypeError: | |
| return timm.create_model(spec, pretrained=True, num_classes=0) | |
| # EMB>>> | |
| # DINOv3 закрыт лицензией Meta и требует токена — пробуем, но не падаем без него | |
| try: | |
| build('vit_base_patch16_dinov3.lvd1689m', 224) | |
| MODELS.insert(0, ('dinov3b', 'vit_base_patch16_dinov3.lvd1689m', 224, True)) | |
| print('DINOv3 dostupen') | |
| except Exception as e: | |
| print('DINOv3 nedostupen:', str(e)[:150]) | |
| # EMB<<< | |
| def extract(spec, size, want_tokens, bs=32): | |
| m = build(spec, size) | |
| cfg = m.pretrained_cfg | |
| m = m.eval().to(DEV) | |
| m = m.half() if USE_HALF else m.float() | |
| loader = make_loader(size, cfg['mean'], cfg['std'], bs) | |
| embs, toks = [], [] | |
| t = time.time() | |
| # однострочный прогресс, если есть tqdm; иначе редкие печати с ETA | |
| every = max(1, min(40, len(loader) // 10)) | |
| try: | |
| from tqdm.auto import tqdm | |
| it = enumerate(tqdm(loader, desc=spec.split('.')[0], unit='батч')) | |
| every = 0 | |
| except ImportError: | |
| print(' %s @%d: батчей %d по %d' % (spec.split('.')[0], size, len(loader), bs), | |
| flush=True) | |
| it = enumerate(loader) | |
| for bi, x in it: | |
| B = x.shape[0] | |
| x = x.view(B * NVIEW, 3, size, size).to(DEV, non_blocking=True) | |
| x = x.half() if USE_HALF else x.float() | |
| if want_tokens: | |
| feats = m.forward_features(x) # (B*6, 1+P, D) | |
| pooled = m.forward_head(feats, pre_logits=True) | |
| npre = getattr(m, 'num_prefix_tokens', 1) | |
| pt = feats[:, npre:] | |
| g = int(round(math.sqrt(pt.shape[1]))) | |
| assert g * g == pt.shape[1], 'ne kvadratnaya setka tokenov: %d' % pt.shape[1] | |
| pt = pt.transpose(1, 2).reshape(pt.shape[0], -1, g, g) | |
| pt = Fn.adaptive_avg_pool2d(pt.float(), 4).flatten(2).transpose(1, 2) | |
| toks.append(pt.reshape(B, NVIEW, 16, -1).cpu().numpy().astype(np.float16)) | |
| else: | |
| pooled = m(x) | |
| embs.append(pooled.reshape(B, NVIEW, -1).float().cpu().numpy().astype(np.float16)) | |
| if every and (bi % every == 0 or bi + 1 == len(loader)): | |
| done = min((bi + 1) * bs, len(IDS)) | |
| el = time.time() - t | |
| print(' %5d/%d %.1f мин ETA %.1f мин' % (done, len(IDS), el / 60, | |
| el / done * (len(IDS) - done) / 60), flush=True) | |
| del m | |
| torch.cuda.empty_cache() | |
| return np.concatenate(embs), (np.concatenate(toks) if toks else None) | |
| # EMB>>> | |
| meta = {'ids': IDS, 'n_train': len(ids_train), 'models': {}} | |
| for name, spec, size, want_tok in MODELS: | |
| t = time.time() | |
| try: | |
| E, TK = extract(spec, size, want_tok) | |
| except Exception as e: | |
| # эмбеддинги независимы: пропускаем сломавшийся бэкбон, а не сессию | |
| print('SKIP %s: %s' % (name, str(e)[:200]), flush=True) | |
| torch.cuda.empty_cache() | |
| continue | |
| np.save(WORK / ('emb_%s.npy' % name), E) | |
| if TK is not None: | |
| np.save(WORK / ('tok_%s.npy' % name), TK) | |
| meta['models'][name] = dict(spec=spec, size=size, dim=int(E.shape[-1]), | |
| tokens=TK is not None, minutes=round((time.time() - t) / 60, 2)) | |
| json.dump(meta, open(WORK / 'emb_meta.json', 'w'), indent=2) | |
| print('%s: %s %.1f min' % (name, E.shape, (time.time() - t) / 60), flush=True) | |
| json.dump(meta, open(WORK / 'emb_meta.json', 'w'), indent=2) | |
| print('всего:', round((time.time() - T0) / 60, 1), 'мин') | |