hitit-cuneiform-ocr / code /src /enhancements /multitask_position.py
savastakan's picture
Initial upload: code + 5 record checkpoints + fuse
f211247 verified
Raw
History Blame Contribute Delete
7.76 kB
#!/usr/bin/env python3
"""Multi-task: classification + bbox-normalized position (reading order hint).
Auxiliary head predicts (x_center, y_center) normalized to tablet bbox.
Forces backbone to encode spatial context → better discrimination for
signs that only differ in tablet position (numerals, logograms).
"""
import os, sys, json, argparse, time
from pathlib import Path
from collections import Counter
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler
from PIL import Image
ROOT = Path("/arf/scratch/stakan/hitit-proje")
sys.path.insert(0, str(ROOT / "hitit_ocr/src"))
from train_classification import build_backbone, get_arch_img_size
def log(m): print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)
class PosDS(Dataset):
def __init__(self, manifest, label_to_idx, img_size, is_train, val_fold, min_samples):
cls_count = Counter()
self.records = []
for line in open(manifest):
r = json.loads(line)
if r.get('task') != 'classification' or not r.get('unified_label'): continue
cls_count[r['unified_label']] += 1
for line in open(manifest):
r = json.loads(line)
if r.get('task') != 'classification': continue
if not r.get('unified_label') or r['unified_label'] not in label_to_idx: continue
if not r.get('path') or r.get('storage') != 'fs': continue
if r.get('integrity_ok') is False: continue
if cls_count[r['unified_label']] < min_samples: continue
fold = r.get('tablet_view_fold', 0)
if is_train and fold == val_fold: continue
if not is_train and fold != val_fold: continue
self.records.append(r)
self.l2i = label_to_idx
from torchvision import transforms
self.tf = transforms.Compose([
transforms.Resize((img_size, img_size), antialias=True),
transforms.ToTensor(),
transforms.Normalize([0.489, 0.448, 0.424], [0.362, 0.359, 0.364]),
])
def __len__(self): return len(self.records)
def __getitem__(self, i):
r = self.records[i]
img = Image.open(r['path']).convert('RGB')
# position from bbox if available, else 0.5,0.5
bbox = r.get('bbox_normalized') or r.get('bbox') or [0.25, 0.25, 0.75, 0.75]
if isinstance(bbox, list) and len(bbox) == 4:
xc = 0.5 * (bbox[0] + bbox[2]); yc = 0.5 * (bbox[1] + bbox[3])
else:
xc, yc = 0.5, 0.5
pos = torch.tensor([xc, yc], dtype=torch.float32)
return self.tf(img), self.l2i[r['unified_label']], pos
class MultiTaskHead(nn.Module):
def __init__(self, feat_dim, n_classes):
super().__init__()
self.cls = nn.Linear(feat_dim, n_classes)
self.pos = nn.Sequential(
nn.Linear(feat_dim, 128), nn.GELU(), nn.Linear(128, 2), nn.Sigmoid()
)
def forward(self, feats):
return self.cls(feats), self.pos(feats)
@torch.no_grad()
def extract(model, x, dtype):
raw = model.module if hasattr(model, 'module') else model
with torch.amp.autocast('cuda', dtype=dtype, enabled=True):
if hasattr(raw, 'forward_features'):
f = raw.forward_features(x)
if f.dim() == 3: f = f[:, 0]
elif f.dim() == 4: f = f.mean(dim=(2, 3))
else:
f = raw(x)
return f.float()
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True)
ap.add_argument('--manifest', required=True)
ap.add_argument('--val-fold', type=int, default=0)
ap.add_argument('--min-samples', type=int, default=10)
ap.add_argument('--epochs', type=int, default=30)
ap.add_argument('--lr', type=float, default=5e-3)
ap.add_argument('--pos-weight', type=float, default=0.3)
ap.add_argument('--batch-size', type=int, default=128)
ap.add_argument('--output', required=True)
args = ap.parse_args()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float32
arch, path = args.ckpt.split(':', 1)
ck = torch.load(path, map_location='cpu', weights_only=False)
label_to_idx = ck['label_to_idx']
n_cls = len(label_to_idx)
img_size = get_arch_img_size(arch)
bb = build_backbone(arch, n_classes=n_cls, img_size_override=img_size).to(device)
sd = ck['model']
sd = {k.replace('module.', '', 1): v for k, v in sd.items()}
sd = {k.replace('_orig_mod.', '', 1): v for k, v in sd.items()}
sd = {k: v for k, v in sd.items() if k != 'n_averaged'}
bb.load_state_dict(sd, strict=False)
for p in bb.parameters(): p.requires_grad = False
bb.eval()
tr_ds = PosDS(args.manifest, label_to_idx, img_size, True, args.val_fold, args.min_samples)
va_ds = PosDS(args.manifest, label_to_idx, img_size, False, args.val_fold, args.min_samples)
log(f"Train={len(tr_ds)} Val={len(va_ds)}")
cc = Counter(tr_ds.l2i[r['unified_label']] for r in tr_ds.records)
sample_w = np.array([1.0 / max(1, cc[tr_ds.l2i[r['unified_label']]])**0.5
for r in tr_ds.records], dtype=np.float32)
tr_dl = DataLoader(tr_ds, batch_size=args.batch_size,
sampler=WeightedRandomSampler(sample_w, len(tr_ds), True),
num_workers=6, pin_memory=True, drop_last=True)
va_dl = DataLoader(va_ds, batch_size=args.batch_size, shuffle=False,
num_workers=4, pin_memory=True)
with torch.no_grad():
feat_dim = extract(bb, torch.zeros(1, 3, img_size, img_size, device=device), dtype).size(-1)
head = MultiTaskHead(feat_dim, n_cls).to(device)
opt = torch.optim.AdamW(head.parameters(), lr=args.lr, weight_decay=1e-4)
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=args.epochs)
best, best_probs, best_y = 0, None, None
for ep in range(args.epochs):
head.train()
tl, nb = 0, 0
for x, y, pos in tr_dl:
x = x.to(device, non_blocking=True); y = y.to(device, non_blocking=True)
pos = pos.to(device, non_blocking=True)
f = extract(bb, x, dtype)
logits_cls, pred_pos = head(f)
loss_cls = F.cross_entropy(logits_cls, y, label_smoothing=0.1)
loss_pos = F.mse_loss(pred_pos, pos)
loss = loss_cls + args.pos_weight * loss_pos
opt.zero_grad(); loss.backward(); opt.step()
tl += loss.item(); nb += 1
sched.step()
head.eval()
cs, tot, p_all, y_all = 0, 0, [], []
with torch.no_grad():
for x, y, _ in va_dl:
x = x.to(device, non_blocking=True); y = y.to(device, non_blocking=True)
f = extract(bb, x, dtype)
logits, _ = head(f)
p = F.softmax(logits.float(), dim=-1)
p_all.append(p.cpu()); y_all.append(y.cpu())
cs += (logits.argmax(-1) == y).sum().item(); tot += y.size(0)
acc = cs / max(1, tot)
if ep % 5 == 0 or ep == args.epochs - 1:
log(f"ep {ep}: loss={tl/max(1,nb):.4f} val_top1={acc:.4f}")
if acc > best:
best = acc
best_probs = torch.cat(p_all); best_y = torch.cat(y_all)
torch.save({'head_state': head.state_dict(), 'label_to_idx': label_to_idx,
'probs': best_probs, 'targets': best_y, 'top1': best,
'backbone_arch': arch},
args.output)
log(f"=== MultiTask BEST: {best:.4f}")
if __name__ == '__main__':
main()