Image Classification
Transformers
Tibetan
dinov3
tibetan
script-classification
paleography
fine-tuned
document-analysis
Eval Results (legacy)
Instructions to use openpecha/tibetan-script-classifier with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use openpecha/tibetan-script-classifier with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-classification", model="openpecha/tibetan-script-classifier") pipe("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/hub/parrots.png")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("openpecha/tibetan-script-classifier", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 8,604 Bytes
09ba195 a1d2f62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | ---
language:
- bo
license: apache-2.0
library_name: transformers
tags:
- image-classification
- dinov3
- tibetan
- script-classification
- paleography
- fine-tuned
- document-analysis
base_model: facebook/dinov3-vits16-pretrain-lvd1689m
datasets:
- custom
metrics:
- f1
- accuracy
pipeline_tag: image-classification
model-index:
- name: Tibetan Script Classifier (DINOv3 ViT-S)
results:
- task:
type: image-classification
name: Tibetan Script Classification
metrics:
- name: Macro F1 (whole page)
type: f1
value: 0.512
- name: Accuracy (whole page)
type: accuracy
value: 0.571
- name: Macro F1 (CLAHE patches, page-level)
type: f1
value: 0.529
---
# Tibetan Script Classifier (DINOv3)
This repository contains fine-tuned Tibetan script classification checkpoints for 18 classes, trained from the DINOv3 ViT-S backbone:
- Backbone: `facebook/dinov3-vits16-pretrain-lvd1689m`
- Task: 18-way script classification
- Training script included: `finetune_dinov3.py`
**Hugging Face access:** DINOv3 requires access approval at [huggingface.co/facebook/dinov3-vits16-pretrain-lvd1689m](https://huggingface.co/facebook/dinov3-vits16-pretrain-lvd1689m) before `from_pretrained` / downloads will work. Anyone cloning this repo will see the same gated-model error until their HF account is granted access and they are logged in (`huggingface-cli login` or `HF_TOKEN`).
## Label Set
`dhumri`, `difficult`, `drathung`, `drudring`, `druring`, `druthung`, `khyuyig`, `multi_scripts`, `non_tibetan`, `peri`, `petsuk`, `trinyig`, `tsegdrig`, `tsugchung`, `tsumachug`, `uchen_sugdring`, `uchen_sugthung`, `yigchung`.
## Preprocessing (per experiment)
Images for training were produced as follows (see `preprocess.py` in the parent project):
- **`whole_page`:** resize so the **short edge is 224 px**, then **center crop** to 224×224 (one crop per source page).
- **`patches_color`:** same short-edge resize to 224, then **sliding-window** 224×224 patches with **25% overlap** between windows (multiple crops per page).
- **`patches_clahe`:** identical patch layout as `patches_color`; each patch is converted to grayscale and **CLAHE** contrast normalization is applied (`clipLimit=2.0`, `tileGridSize=(8,8)`), then saved as BGR/RGB for training.
## Training recipe
- **Progressive unfreezing (defaults in `finetune_dinov3.py`):**
- **Stage A — head only:** 20 epochs, backbone frozen, classifier head LR **1e-3** (backbone LR 0).
- **Stage B — last 2 blocks:** 10 epochs, backbone LR **1e-5**, head LR **1e-3**.
- **Stage C — last 4 blocks:** 10 epochs, backbone LR **5e-6**, head LR **5e-4**.
- **Loss:** class-weighted **cross-entropy** with inverse-frequency weights over the training split (`nn.CrossEntropyLoss(weight=...)`).
- **Sampling:** the published runs use a standard `DataLoader` with **`shuffle=True`**. The script also defines **`get_weighted_sampler` → `WeightedRandomSampler`** if you want to switch the train loader to explicit class-balanced sampling.
- **Document-aware augmentations (train only):** `RandomRotation` **±5°** (fill white), `ColorJitter` brightness/contrast **±20%** (`0.2`), plus `RandomResizedCrop` and light `RandomErasing` as in `ScriptDataset`; **no horizontal flip**.
## Class distribution (`whole_page` split totals)
The whole-page split has **5,684** samples in total (**train 3,996 / val 844 / test 844**). The **844** figure in test metrics is **only the test split** (roughly 15% of pages per class held out for testing), not the full dataset.
The per-class table below sums **train + val + test** counts. A benchmark exclusion manifest (`benchmark_page_ids.json`, **88** page IDs across 18 classes) is consulted when building splits: any file whose page ID matches is skipped and counted in `splits.json` under `skipped_excluded_files_by_class`. For the published `whole_page/splits.json`, that skip map is **empty**—so either those pages were **not present** under the training `data_dir`, or they had **already been removed** before this split was built. The **5,684** totals are whatever files remained for stratified splitting.
| Class | Samples |
|---|---:|
| dhumri | 98 |
| difficult | 170 |
| drathung | 129 |
| drudring | 132 |
| druring | 119 |
| druthung | 207 |
| khyuyig | 113 |
| multi_scripts | 235 |
| non_tibetan | 192 |
| peri | 614 |
| petsuk | 1388 |
| trinyig | 42 |
| tsegdrig | 749 |
| tsugchung | 77 |
| tsumachug | 178 |
| uchen_sugdring | 835 |
| uchen_sugthung | 240 |
| yigchung | 166 |
## Experiments Included
### 1) `whole_page`
- Files: `whole_page/final_model.pt`, `results.json`, `confusion_matrix.png`, `confusion_matrix.csv`, `splits.json`
- Test (image-level) macro-F1: **0.5124**
- Test accuracy: **0.5711**
### 2) `patches_color`
- Files: `patches_color/final_model.pt`, `results.json`, `confusion_matrix.png`, `checkpoint_page_eval.json`, `splits.json`
- Test (patch-level) macro-F1: **0.4899**
- Re-eval **page-level** macro-F1 for shipped `final_model.pt` (`checkpoint_page_eval.json`): **0.5017**
- Best **page-level** macro-F1 among stage checkpoints on the same grid: **0.5043** (**Stage A**)
### 3) `patches_clahe`
- Files: `patches_clahe/final_model.pt`, `results.json`, `confusion_matrix.png`, `checkpoint_page_eval.json`, `splits.json`
- Test (patch-level) macro-F1: **0.4911**
- Re-eval page-level macro-F1 for shipped `final_model.pt`: **0.5261**
- Best **page-level** macro-F1 among stage checkpoints: **0.529** (**Stage B**)
## Which stage produced which checkpoint?
- **`final_model.pt` in each folder** is the stage with the highest **validation macro-F1** among `best_stage_*.pt` checkpoints (see `best_val_checkpoint` in each `results.json`): **Stage B** for `whole_page`, **Stage C** for both `patches_color` and `patches_clahe`.
- For **page-level** quality on the patch runs, the best single stage on the re-eval grid differs: **Stage A** (`patches_color`) and **Stage B** (`patches_clahe`) beat their respective `final_model.pt` page scores—use `checkpoint_page_eval.json` if you want to deploy a stage checkpoint instead of the val-selected default.
## Which experiment won?
CLAHE patches achieved the highest **page-level** macro-F1 (**0.529** on the best stage checkpoint), while **whole page** achieved the best **image-level** macro-F1 (**0.512**). **Whole page** is recommended for production due to simpler inference.
## How To Load a Checkpoint
```python
import torch
from pathlib import Path
from finetune_dinov3 import DINOv3Classifier, DINOV3_MODEL_ID
ckpt_path = Path("whole_page/final_model.pt")
payload = torch.load(ckpt_path, map_location="cpu")
label_to_idx = payload["label_to_idx"]
idx_to_label = {v: k for k, v in label_to_idx.items()}
num_classes = len(label_to_idx)
model = DINOv3Classifier(DINOV3_MODEL_ID, num_classes)
model.load_state_dict(payload["model_state_dict"])
model.eval()
```
## Inference (Single Image)
```python
import torch
from PIL import Image
from transformers import AutoImageProcessor
processor = AutoImageProcessor.from_pretrained(DINOV3_MODEL_ID)
img = Image.open("example.png").convert("RGB")
inputs = processor(images=img, return_tensors="pt")
with torch.no_grad():
logits = model(inputs["pixel_values"])
probs = torch.softmax(logits, dim=1)[0].cpu().numpy()
pred_idx = int(probs.argmax())
pred_label = idx_to_label[pred_idx]
print(pred_label, float(probs[pred_idx]))
```
## Page-Level Inference (Patch Aggregation)
For patch experiments (`patches_color`, `patches_clahe`), aggregate by page stem:
1. group patch probabilities by page ID (strip `_pN` suffix),
2. average probabilities per page,
3. take `argmax` of averaged probabilities.
This is the same logic used in the re-evaluation script output (`checkpoint_page_eval.json`).
## Known Limitations
- Class imbalance is high (for example `petsuk` and `uchen_sugdring` dominate, while `trinyig` is small).
- Results can vary by preprocessing variant and by patch vs page-level evaluation protocol.
- Patch-level metrics and page-level metrics are not directly interchangeable.
- The model expects Tibetan manuscript-style inputs; performance can drop on out-of-domain scans or mixed/noisy pages.
- Checkpoints are tied to the exact label mapping saved in each payload (`label_to_idx`).
## Reproducibility Notes
- Exclusion manifest support is enabled in training (`benchmark_page_ids.json`).
- Full training code used for these artifacts is included at `finetune_dinov3.py`.
|