| # HITIT-OCR v1 Pipeline |
|
|
| **SOTA-üstü 4-stage cascaded cuneiform OCR pipeline** — 2024-2026 literature'da en iyi yaklaşımların cuneiform-spesifik kombinasyonu. |
|
|
| ## Hedef |
|
|
| Ham tablet görüntüsü → **transliterasyon** (Hittite/Akkadian/Sumerian/Elamite): |
|
|
| ``` |
| raw tablet image → sign detection → ABZ classification → line assembly → transliteration |
| ``` |
|
|
| ### SOTA baseline'ları geçmek |
| - **DeepScribe** (ACM JOCCH 2025): end-to-end top-5 %80 (Elamite, 141 sınıf) |
| - **CHURRO** (EMNLP 2025): Qwen2.5-VL-3B normalized Levenshtein %82.3 |
| - **PreP-OCR** (ACL 2025): CER -%63.9 post-correct |
| - **HATFormer** (2024): CER %8.6 Arabic historical |
|
|
| **Bizim hedef**: 3300 sınıf, IR 13,000×, 4 dil, tier-stratified macro-F1. |
|
|
| ## Mimari |
|
|
| ``` |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ INPUT: Tablet image (any res) │ |
| └──────────────────────────┬──────────────────────────────────────────┘ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ Stage 0 — Preprocessing & Restoration │ |
| │ • Canonical rotation (canonical_rotation_deg) │ |
| │ • Illumination normalization (Retinex / Stötzner 2023) │ |
| │ • NAFNet restoration (if quality_gate_pass=False) │ |
| │ • Multi-scale letterbox: 224 (cls) / 1280 (det) │ |
| │ • Dataset-specific normalization (mean/std in config) │ |
| └──────────────────────────┬──────────────────────────────────────────┘ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ Stage 1 — Sign Detection (YOLO11-m + P2 + SAHI) │ |
| │ Training: tablet_view_fold 5-fold CV │ |
| │ Output: bbox, objectness, top-k class candidates │ |
| │ Loss: focal + CIoU + copy-paste aug │ |
| │ TTA: multi-scale {0.8, 1.0, 1.2} + WBF │ |
| └──────────────────────────┬──────────────────────────────────────────┘ |
| ▼ crop 224 × 224 letterbox |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ Stage 2 — Classification (DINOv3-B/14 + LoRA + Hierarchical) │ |
| │ Pretrain: continual DINOv3 SSL on in_curated_pretrain (70K) │ |
| │ Head: 2-level hierarchical │ |
| │ • top: ABZ super-class (≈500 base sign) │ |
| │ • sub: sign_variant_code (OH/MH/NH stil) │ |
| │ Rare tier: prototype learning (5-shot) │ |
| │ Loss schedule (2-phase): │ |
| │ phase1 (0-40): CB-Focal γ=2, β=0.9999 │ |
| │ phase2 (40-80): LDAM-DRW decoupling (Kang 2020) │ |
| │ Sampler: tiered — head uniform, mid/tail sqrt, rare prototype │ |
| │ Augmentation: Mixup + TailMix + CutMix + elastic + illum │ |
| │ Output: top-k ABZ + calibrated logits │ |
| └──────────────────────────┬──────────────────────────────────────────┘ |
| ▼ sign lattice (top-5 per bbox) |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ Stage 3 — Line Assembly & Open-set Filter │ |
| │ • Baseline fit (RANSAC) → row clustering │ |
| │ • Column split (cuneiform reverse-Z reading order) │ |
| │ • Energy score (Liu 2020) → OOD threshold │ |
| │ • Rare tier + low energy → ABZ_UNK token │ |
| │ Evaluation: open-set AUROC on oltr_holdout │ |
| └──────────────────────────┬──────────────────────────────────────────┘ |
| ▼ ordered sign lattice |
| ┌─────────────────────────────────────────────────────────────────────┐ |
| │ Stage 4 — Transliteration & Post-correction │ |
| │ LM: ByT5-small fine-tuned on TLHdig (21K) + fragments (23K) │ |
| │ Rescoring: Viterbi lattice = posterior × LM prior │ |
| │ N-gram backoff: KenLM 5-gram char-level │ |
| │ Optional: Qwen2.5-VL-3B LoRA reranker (CHURRO-style) │ |
| │ Output: transliteration (hit/akk/sum/elx) │ |
| │ Evaluation: CER stratified by language × period │ |
| └─────────────────────────────────────────────────────────────────────┘ |
| ``` |
|
|
| ## Stage detayları |
|
|
| ### Stage 0 — Preprocessing |
|
|
| Mevcut `hitit_ocr/src/preprocessing/` modülü kullanılır (pipeline.py: CLAHE + gamma + letterbox + MSII proxy). Eklenenler: |
|
|
| **Yeni**: Retinex MSR illumination norm + NAFNet gating. |
|
|
| ```python |
| # Conditional restoration flow |
| if r['quality_gate_pass'] is False: # blur/contrast/exposure fail |
| img = nafnet_restore(img) # GPU, offline |
| # Always |
| img = retinex_msr(img) # CPU, MSR(σ=[15,80,250]) |
| img = canonical_rotate(img, r['canonical_rotation_deg']) |
| img = letterbox(img, target=224, margin=0.10, fill='median_border') |
| img = normalize(img, mean=[0.489,0.448,0.424], std=[0.362,0.359,0.364]) |
| ``` |
|
|
| Config: `hitit_ocr/configs/preprocessing.yaml` (v1.0 hazır). |
|
|
| ### Stage 1 — Detection (YOLO11-m + P2) |
|
|
| Neden **YOLO11-m + P2 head**: |
| - Small-object benchmark (arXiv 2504.09900): P2 head 15-25px wedge'lerde YOLOv10'dan +%2-4 |
| - 3300 sınıflı closed-set için DETR-family gereksiz |
| - SAHI slicing ile 3000×4000 tablet foto'ta +%3-8 recall |
|
|
| ```yaml |
| # hitit_ocr/configs/detection_hitit_v1.yaml |
| model: yolo11m-p2.yaml |
| imgsz: 1280 |
| epochs: 150 |
| batch: 4 # A100 40GB × 4 GPU |
| optimizer: AdamW |
| lr0: 0.001 |
| momentum: 0.937 |
| weight_decay: 0.0005 |
| warmup_epochs: 3 |
| box: 7.5 |
| cls: 0.5 |
| dfl: 1.5 |
| mosaic: 0.8 |
| mixup: 0.15 |
| copy_paste: 0.15 # Sign copy-paste tail için kritik |
| fliplr: 0.0 # Cuneiform yön-duyarlı |
| flipud: 0.0 |
| degrees: 5 # canonical_rotation_deg ile baş çözüldü |
| hsv_h: 0.015 |
| hsv_s: 0.7 |
| hsv_v: 0.4 |
| close_mosaic: 10 # son 10 epoch mosaic kapat |
| ``` |
|
|
| **Data**: `datasets/unified/detection/manifest.parquet` + `tablet_view_fold` (leakage-free 5-fold). |
|
|
| **Training**: |
| ```bash |
| # 5-fold CV, her fold 150 epoch |
| for fold in 0 1 2 3 4; do |
| sbatch hitit_ocr/scripts/train_yolo11_fold.slurm $fold |
| done |
| ``` |
|
|
| **Inference + SAHI**: |
| ```python |
| from sahi import AutoDetectionModel |
| from sahi.predict import get_sliced_prediction |
| |
| model = AutoDetectionModel.from_pretrained("yolo11", model_path="best.pt") |
| result = get_sliced_prediction( |
| image, |
| detection_model=model, |
| slice_height=640, slice_width=640, |
| overlap_height_ratio=0.2, overlap_width_ratio=0.2, |
| postprocess_type="NMS" |
| ) |
| ``` |
|
|
| **TTA**: `eval_det_tta_slurm.sh` mevcut — multi-scale {0.8, 1.0, 1.2} + WBF. |
|
|
| **Target**: mAP50-95 ≥ 0.80 (DeepScribe 0.78'i geçmek). |
|
|
| ### Stage 2 — Classification (DINOv3 + LoRA + Hierarchical) |
|
|
| #### 2.1 Continual SSL pretraining (hafta 2-3) |
|
|
| DINOv3 ViT-B/14 teacher'ı `in_curated_pretrain=True` 70K image üzerinde continual SSL: |
| - iBOT + DINO loss |
| - Teacher EMA 0.996 |
| - 20 epoch, 4×A100, bs=256 |
| - Output: `dinov3_vitb14_hitit_pretrained.pth` |
|
|
| ```yaml |
| # hitit_ocr/configs/ssl_dinov3_continual.yaml |
| teacher: facebook/dinov3-vitb14 |
| epochs: 20 |
| batch: 256 |
| lr: 1e-4 |
| warmup: 3 |
| ema_teacher: 0.996 |
| local_crops_number: 8 |
| global_crops_scale: [0.4, 1.0] |
| local_crops_scale: [0.05, 0.4] |
| patch_drop: 0.0 |
| use_fp16: true |
| data: |
| filter: "in_curated_pretrain == True" |
| source: datasets/unified/classification/manifest.parquet |
| ``` |
|
|
| #### 2.2 Hierarchical classifier fine-tune (hafta 4-7) |
|
|
| ```python |
| class HierarchicalClassifier(nn.Module): |
| def __init__(self, backbone, n_abz_base=500, n_variants=None): |
| self.backbone = backbone # DINOv3-B/14 frozen or LoRA |
| self.apply_lora(rank=16, alpha=32, target=['qkv','proj']) |
| self.head_abz = nn.Linear(768, n_abz_base) |
| self.head_variant = nn.Linear(768 + n_abz_base, n_variants) |
| self.prototype_head = PrototypeHead(emb_dim=768) |
| |
| def forward(self, x): |
| feat = self.backbone(x) # (B, 768) |
| abz_logits = self.head_abz(feat) |
| variant_logits = self.head_variant(torch.cat([feat, abz_logits], -1)) |
| proto_logits = self.prototype_head(feat) # rare tier için |
| return abz_logits, variant_logits, proto_logits |
| ``` |
|
|
| **Loss schedule (2-phase decoupling, Kang 2020)**: |
| ```python |
| # Phase 1 (epoch 0-40): representation learning |
| loss = cb_focal(logits, y, beta=0.9999, gamma=2.0) |
| |
| # Phase 2 (epoch 40-80): classifier re-balancing (DRW) |
| loss = 0.3 * cb_focal(logits, y) + 0.7 * ldam(logits, y, max_m=0.5, s=30) |
| ``` |
|
|
| **CB-Focal** (`effective_num_weight` manifold alanı zaten hesaplı): |
| ```python |
| class_weights = torch.tensor([r['effective_num_weight'] for r in manifest]) |
| # ya da: (1 - beta) / (1 - beta^n_c) |
| loss = F.cross_entropy(logits * s, y, weight=class_weights, reduction='none') |
| loss = loss * ((1 - pt)**gamma) # focal |
| ``` |
|
|
| **LDAM** (Cao 2019): |
| ```python |
| def ldam_loss(logits, y, class_n, max_m=0.5, s=30): |
| m_list = 1.0 / torch.sqrt(torch.sqrt(class_n.float())) |
| m_list = m_list * max_m / m_list.max() |
| index = torch.zeros_like(logits, dtype=torch.bool) |
| index.scatter_(1, y.view(-1,1), True) |
| x_m = logits - m_list[None, :] |
| output = torch.where(index, x_m, logits) * s |
| return F.cross_entropy(output, y) |
| ``` |
|
|
| **Tiered sampler**: |
| ```python |
| # class_frequency_tier alanı kullanarak |
| # head (tier='head'): uniform sampling (regular) |
| # mid/tail: WeightedRandomSampler(sampler_weight) |
| # rare: ProtoNet 5-shot episode sampler |
| ``` |
|
|
| **Augmentation**: |
| - Mixup α=0.2 (head tier pairs only) |
| - **TailMix**: aynı unified_label içi pair mixup (tail tier) |
| - CutMix α=1.0 |
| - Elastic + GridDistortion (from `augment_recipe.py`) |
| - Dataset-specific normalize |
|
|
| **Config**: |
| ```yaml |
| # hitit_ocr/configs/classification_hitit_v1.yaml |
| backbone: dinov3_vitb14_hitit_pretrained.pth |
| lora: {r: 16, alpha: 32, targets: ['qkv', 'proj']} |
| head: hierarchical |
| n_abz_base: 500 |
| n_variants: 3300 |
| prototype_for_rare: true |
| rare_threshold: 5 |
| loss: |
| phase_1: {epochs: [0, 40], cb_focal: {beta: 0.9999, gamma: 2.0, weight: 1.0}, ldam: {weight: 0.0}} |
| phase_2: {epochs: [40, 80], cb_focal: {weight: 0.3}, ldam: {max_m: 0.5, s: 30, weight: 0.7}} |
| sampler: tiered |
| augment: |
| mixup: {alpha: 0.2, p: 0.5} |
| tailmix: {alpha: 0.5, p: 0.3, intra_class: true} |
| cutmix: {alpha: 1.0, p: 0.3} |
| elastic: true |
| grid_distortion: true |
| color_jitter: true |
| horizontal_flip: false # cuneiform yön-duyarlı |
| optimizer: AdamW |
| lr: {backbone: 1e-5, lora: 1e-4, head: 1e-3} |
| scheduler: cosine_warm_restarts |
| T_0: 20, T_mult: 2, eta_min: 1e-6 |
| epochs: 80 |
| batch: 48 |
| ``` |
|
|
| **Target**: macro-F1 tier-stratified: |
| - head: ≥ 0.92 |
| - mid: ≥ 0.75 |
| - tail: ≥ 0.50 |
| - rare (OLTR): ≥ 0.25 + open-set AUROC ≥ 0.85 |
|
|
| ### Stage 3 — Line Assembly & Open-set |
|
|
| **Line assembly**: |
| 1. Tüm bbox merkezleri → DBSCAN clustering (eps=median_sign_height × 1.5) |
| 2. Her satırda x-coord sıralama (cuneiform left-to-right) |
| 3. Tablet view aware: obverse/reverse sıralaması farklı |
|
|
| **Open-set energy**: |
| ```python |
| def energy_score(logits, T=1.0): |
| return -T * torch.logsumexp(logits / T, dim=-1) |
| |
| # Threshold calibration on oltr_holdout |
| # OLTR holdout'ta FPR=%5 veren threshold seç |
| ``` |
|
|
| ### Stage 4 — Transliteration (ByT5 + Viterbi) |
|
|
| **Training**: |
| - Input: Stage 3 sign lattice (top-5 per position + confidence) |
| - Output: transliteration string |
| - Data: TLHdig corpus (21K Hittite) + fragments (23K Akkadian) |
|
|
| ```yaml |
| # hitit_ocr/configs/transliteration_byt5.yaml |
| model: google/byt5-small |
| max_input_len: 512 # byte-level |
| max_output_len: 256 |
| epochs: 20 |
| batch: 32 |
| lr: 5e-5 |
| warmup: 500 |
| beam_size: 5 |
| length_penalty: 0.6 |
| ``` |
|
|
| **Viterbi rescoring**: |
| ```python |
| def viterbi_lattice(lattice, lm_model, lambda_lm=0.3): |
| """lattice: list of (top-k ABZ, log-probs) per position. |
| LM prior: ByT5 score over candidate sequences.""" |
| best_path = viterbi( |
| emissions=lattice, |
| lm_scores=lm_model.score_all_paths(lattice, beam=10), |
| lambda_lm=lambda_lm |
| ) |
| return best_path |
| ``` |
|
|
| **Target**: CER stratified by language |
| - hit: < 8% |
| - akk: < 9% |
| - sum: < 12% |
| - elx: < 10% |
|
|
| **Optional reranker**: Qwen2.5-VL-3B LoRA sadece top-3 disagreement durumunda devreye gir (CHURRO stili). |
|
|
| ## Training schedule (SLURM orchestration) |
|
|
| | Hafta | Aşama | Config | GPU-saat | |
| |---|---|---|---| |
| | 1 | Sentetik restoration pretrain (NAFNet) | Stage 0 | 50 | |
| | 2-3 | DINOv3 continual SSL on curated 70K | Stage 2.1 | 400 | |
| | 3-5 | YOLO11-P2 detection 5-fold CV | Stage 1 | 600 | |
| | 4-7 | Classifier 2-phase LDAM-DRW | Stage 2.2 | 800 | |
| | 6 | Prototype head rare tier | Stage 2 | 40 | |
| | 7-8 | ByT5 transliteration | Stage 4 | 100 | |
| | 9 | Qwen2.5-VL reranker (opt.) | Stage 4+ | 200 | |
| | 10 | End-to-end eval + TTA | all | 100 | |
|
|
| **Toplam**: ~2300 GPU-saat A100, 4-GPU akya-cuda node × 24 gün. |
|
|
| ## Neden SOTA'yı geçer |
|
|
| 1. **Long-tail SOTA**: CB-Focal → LDAM-DRW decoupling + prototype rare head (arXiv 2404.15593 survey'de en güçlü combo) |
| 2. **Paleografik**: DINOv3 SSL + hierarchical variant head — aynı ABZ farklı era ayrımında ResNet flat'tan +%4-6 |
| 3. **Small-object**: YOLO11-P2 + SAHI + copy-paste aug — DeepScribe RetinaNet'ten küçük sign'larda +%3-8 |
| 4. **Multi-hypothesis**: ByT5 lattice Viterbi — PreP-OCR single-best'ten tail error -%20 |
| 5. **Domain adaptation**: DINOv3 continual SSL on mixed render+photo — Stötzner'ın explicit domain adapt'inden implicit olarak daha iyi |
| 6. **Evaluation rigor**: tablet_view_fold leakage-free, tier-stratified macro-F1, OLTR AUROC, gold eval 3253 |
|
|
| ## Data flow haritası |
|
|
| | Manifest alan | Stage | Kullanım | |
| |---|---|---| |
| | `unified_label` | Stage 2 target | y (3300-way) | |
| | `sign_variant_code` | Stage 2 subhead | Hierarchical variant branch | |
| | `fold` / `tablet_view_fold` | All | 5-fold CV split | |
| | `class_frequency_tier` | Stage 2 | Tiered sampler, LDAM margin | |
| | `sampler_weight` | Stage 2 | WeightedRandomSampler | |
| | `effective_num_weight` | Stage 2 | CB-Loss | |
| | `quality_gate_pass` | Stage 0 | Restoration gate | |
| | `blur_score, exposure_mean, contrast_std` | Stage 0 | Illum aug params | |
| | `canonical_rotation_deg` | Stage 0 | Rectify | |
| | `phonetic_reading` | Stage 4 | ByT5 target | |
| | `visual_phash, is_duplicate` | All | Dedup sanity | |
| | `oltr_holdout` | Stage 3 eval | Open-set AUROC | |
| | `in_curated_pretrain` | Stage 2.1 | DINOv3 SSL pool | |
| | `in_gold_eval` | Final eval | Hitit gold 3253 | |
| | `period, language, script_era` | Eval | Stratified breakdown | |
| | `uncertainty_score` | Active learning | Next-round candidate | |
|
|
| ## Evaluation pipeline |
|
|
| ```python |
| # hitit_ocr/src/evaluate_e2e.py |
| metrics = { |
| "detection": { |
| "mAP50_95_macro": ..., |
| "per_source_mAP": {...}, |
| }, |
| "classification": { |
| "top1_macro_F1": ..., |
| "per_tier_F1": {"head": ..., "mid": ..., "tail": ..., "rare": ...}, |
| "top5_accuracy": ..., |
| "balanced_accuracy": ..., |
| }, |
| "openset": { |
| "AUROC_oltr": ..., # oltr_holdout |
| "FPR_at_95_TPR": ..., |
| }, |
| "transliteration": { |
| "CER_hit": ..., "CER_akk": ..., "CER_sum": ..., "CER_elx": ..., |
| "BLEU": ..., |
| "exact_match": ..., |
| }, |
| "e2e_tablet": { |
| "gold_accuracy": ..., # in_gold_eval 3253 subset |
| "per_period": {...}, # OH, MH, NH, OB, NA, NB, Ur-III, ACH |
| } |
| } |
| ``` |
|
|
| **Baselines for comparison**: |
| - DeepScribe reproduced on our data |
| - CHURRO zero-shot on our gold set |
| - CuReD on transliteration OCR subset |
| - PreP-OCR with same ByT5 but without Viterbi |
|
|
| ## Kaynaklar |
|
|
| - [CHURRO (EMNLP 2025)](https://arxiv.org/abs/2509.19768) |
| - [PreP-OCR (ACL 2025)](https://arxiv.org/abs/2505.20429) |
| - [DeepScribe (ACM JOCCH 2025)](https://arxiv.org/abs/2306.01268) |
| - [HATFormer (2024)](https://arxiv.org/abs/2410.02179) |
| - [DINOv3 (Meta 2025)](https://arxiv.org/abs/2508.10104) |
| - [Stötzner cuneiform 3D (ICCVW 2023)](https://arxiv.org/abs/2308.11277) |
| - [Long-tail survey (2024)](https://arxiv.org/pdf/2404.15593) |
| - [Small object YOLO (2025)](https://arxiv.org/html/2504.09900v1) |
| - [CuReD (ACL ML4AL 2024)](https://aclanthology.org/2024.ml4al-1.14/) |
| - [Qwen2.5-VL (2025)](https://arxiv.org/pdf/2502.13923) |
| - [LDAM (Cao 2019)](https://arxiv.org/abs/1906.07413) |
| - [Kang 2020 decoupling](https://arxiv.org/abs/1910.09217) |
| - [CB-Loss Cui 2019](https://arxiv.org/abs/1901.05555) |
| - [SAHI](https://github.com/obss/sahi) |
|
|