KashlabsAI commited on
Commit
5347b34
·
verified ·
1 Parent(s): 0683eef

Release Koshur OCR v2 — 100k epoch-18 checkpoint

Browse files
Files changed (5) hide show
  1. README.md +36 -18
  2. best-model.pt +2 -2
  3. config.json +24 -16
  4. model.py +15 -10
  5. requirements.txt +3 -3
README.md CHANGED
@@ -10,32 +10,47 @@ language:
10
  library_name: pytorch
11
  ---
12
 
13
- # Koshur OCR
14
 
15
- A compact Kashmiri Perso-Arabic printed-line OCR model from **Kash Labs AI**.
16
 
17
- ## Results
18
 
19
- - **Deduplicated validation CER:** 28.13%
20
- - **Deduplicated exact match:** 15.11%
 
 
21
  - **Validation rows:** 920 deduplicated / 1,000 full
22
- - **Best epoch:** 47 of 60
23
- - **Training rows:** 10,000 synthetic sentence-line images
24
- - **W&B:** https://wandb.ai/kash-ai-labs-kash-labs/koshur-pixel-ocr/runs/hdab4ed4
 
 
25
 
26
  ## Architecture
27
 
28
- `64×2048 grayscale line → CNN (48→96→160→192) → temporal feature dropout (p=0.3) → 2-layer bidirectional GRU (256 per direction) → 138-way CTC`
 
 
 
 
29
 
30
- The network has **4,467,210 trainable parameters**. The repository contains a custom PyTorch checkpoint, not a Transformers-format model.
 
 
 
31
 
32
  ## Files
33
 
34
- - `best-model.pt` — best epoch-47 state dictionary, character inventory, architecture metadata, and validation CER
35
  - `model.py` — architecture, preprocessing, CTC decoding, and `recognize_line()`
36
- - `config.json` — training and preprocessing configuration
 
 
37
 
38
- ## Local inference
 
 
39
 
40
  ```python
41
  from PIL import Image
@@ -43,13 +58,16 @@ from model import recognize_line
43
 
44
  text, diagnostics = recognize_line(Image.open("single-line.png"))
45
  print(text)
 
46
  ```
47
 
48
- The input should be one tightly cropped printed line. It is normalized to height 64 and left-aligned on a width-2048 canvas.
49
 
50
  ## Limitations
51
 
52
- - Experimental research checkpoint; error rate remains substantial.
53
- - Trained on synthetic printed line images, not handwriting or complete pages.
54
- - Training targets had the established Kashmiri diacritic set removed. This model recognizes base text and does not restore those diacritics.
55
- - Predictions should be reviewed before publication or archival use.
 
 
 
10
  library_name: pytorch
11
  ---
12
 
13
+ # Koshur OCR v2
14
 
15
+ A compact Kashmiri Perso-Arabic **printed-line** OCR model from **Kash Labs AI**.
16
 
17
+ ## v2 results
18
 
19
+ - **Deduplicated validation CER:** 22.62%
20
+ - **Deduplicated exact match:** 25.54%
21
+ - **Full validation CER:** 23.06%
22
+ - **Full validation exact match:** 26.40%
23
  - **Validation rows:** 920 deduplicated / 1,000 full
24
+ - **Best epoch:** 18
25
+ - **Training rows:** 100,000 synthetic sentence-line images
26
+ - **W&B:** https://wandb.ai/kash-ai-labs-kash-labs/koshur-pixel-ocr/runs/ctc-100k-control-p0-20260729T061125Z-retry1
27
+
28
+ The deduplicated errors comprise 378 insertions, 6,859 deletions, and 3,146 substitutions over 45,907 reference characters.
29
 
30
  ## Architecture
31
 
32
+ `64×2048 grayscale line → CNN (48→96→160→192) → 2-layer bidirectional GRU (256 per direction) → 155-way CTC`
33
+
34
+ The model has **4,475,931 trainable parameters**, a 154-character inventory, and no temporal feature dropout in the selected v2 control checkpoint. This is a custom PyTorch checkpoint, not a Transformers-format model.
35
+
36
+ ## Versioning
37
 
38
+ - `v1.0.0` 10k temporal-dropout checkpoint, 28.13% deduplicated CER
39
+ - `v2.0.0` — 100k control checkpoint, 22.62% deduplicated CER (current)
40
+
41
+ Use a Hub revision to pin a release.
42
 
43
  ## Files
44
 
45
+ - `best-model.pt` — epoch-18 state dictionary, character inventory, architecture metadata, and validation CER
46
  - `model.py` — architecture, preprocessing, CTC decoding, and `recognize_line()`
47
+ - `config.json` — release, training, preprocessing, metrics, and checkpoint checksum
48
+
49
+ ## Local CPU or Apple Silicon inference
50
 
51
+ ```bash
52
+ pip install -r requirements.txt
53
+ ```
54
 
55
  ```python
56
  from PIL import Image
 
58
 
59
  text, diagnostics = recognize_line(Image.open("single-line.png"))
60
  print(text)
61
+ print(diagnostics)
62
  ```
63
 
64
+ The input should be one tightly cropped, upright printed line. It is resized to height 64 and left-aligned on a width-2048 canvas. Inference runs on CPU and does not require a GPU.
65
 
66
  ## Limitations
67
 
68
+ - The reported score is an in-domain synthetic validation result, not real-world scan quality.
69
+ - Trained on synthetic printed lines, not handwriting or full pages.
70
+ - Camera photos should be rotated, deskewed, tightly cropped, and segmented into individual lines.
71
+ - Training targets exclude the established Kashmiri diacritic set; the model recognizes base text and does not restore those marks.
72
+ - Error rate remains substantial, with deletions the dominant residual error.
73
+ - Predictions require review before publication or archival use.
best-model.pt CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ebdf01f2ebe7cb8bd06d7233940e0f72ff7ef894942bf3de801df931d50990ad
3
- size 17889058
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:38382437ace33611c15ddbbf718ddcd88bf25f31092883bdcab540734199648d
3
+ size 17924194
config.json CHANGED
@@ -1,34 +1,42 @@
1
  {
2
- "run_name": "ctc-temporal-intervention-p0.3-20260728T163111Z",
3
- "smoke": false,
4
- "train_rows": 10000,
 
5
  "val_rows": 1000,
6
  "val_dedup_rows": 920,
7
- "epochs": 60,
 
8
  "batch_size": 32,
9
  "learning_rate": 0.0007,
10
  "weight_decay": 0.0001,
11
- "temporal_dropout_p": 0.3,
12
  "seed": 11,
13
  "image_height": 64,
14
  "image_width": 2048,
15
  "canvas_align": "left",
16
  "horizontal_stride": 4,
17
- "cnn_channels": [
18
- 48,
19
- 96,
20
- 160,
21
- 192
22
- ],
23
  "rnn_hidden": 256,
24
  "rnn_layers": 2,
25
  "rnn_dropout": 0.05,
26
- "snapshot_volume": "koshur-ppocrv6-kp-breakthrough-data",
27
- "snapshot_relative_path": "kp_breakthrough/sentence_line-r874c4df69c3f-n10000-1000-len90",
 
 
28
  "snapshot_images_mirrored": true,
29
  "images_unmirrored_in_memory": true,
30
  "labels_reversed_for_ltr_ctc": true,
31
- "hf_token_required": false,
32
  "dataset_images_uploaded": false,
33
- "selection_metric": "val_dedup/cer"
34
- }
 
 
 
 
 
 
 
 
 
 
 
1
  {
2
+ "release": "v2.0.0",
3
+ "run_name": "ctc-100k-control-p0-20260729T061125Z-retry1",
4
+ "wandb_run": "https://wandb.ai/kash-ai-labs-kash-labs/koshur-pixel-ocr/runs/ctc-100k-control-p0-20260729T061125Z-retry1",
5
+ "train_rows": 100000,
6
  "val_rows": 1000,
7
  "val_dedup_rows": 920,
8
+ "trained_epochs": 38,
9
+ "best_epoch": 18,
10
  "batch_size": 32,
11
  "learning_rate": 0.0007,
12
  "weight_decay": 0.0001,
13
+ "temporal_dropout_p": 0.0,
14
  "seed": 11,
15
  "image_height": 64,
16
  "image_width": 2048,
17
  "canvas_align": "left",
18
  "horizontal_stride": 4,
19
+ "cnn_channels": [48, 96, 160, 192],
 
 
 
 
 
20
  "rnn_hidden": 256,
21
  "rnn_layers": 2,
22
  "rnn_dropout": 0.05,
23
+ "character_classes": 154,
24
+ "ctc_classes_including_blank": 155,
25
+ "parameters": 4475931,
26
+ "snapshot_relative_path": "kp_scale/sentence_line-r874c4df69c3f-n100000-1000-len90",
27
  "snapshot_images_mirrored": true,
28
  "images_unmirrored_in_memory": true,
29
  "labels_reversed_for_ltr_ctc": true,
 
30
  "dataset_images_uploaded": false,
31
+ "selection_metric": "val_dedup/cer",
32
+ "metrics": {
33
+ "val_cer": 0.23057324840764332,
34
+ "val_exact_rate": 0.264,
35
+ "val_dedup_cer": 0.22617465745964668,
36
+ "val_dedup_exact_rate": 0.2554347826086957,
37
+ "val_dedup_insertions": 378,
38
+ "val_dedup_deletions": 6859,
39
+ "val_dedup_substitutions": 3146
40
+ },
41
+ "checkpoint_sha256": "38382437ace33611c15ddbbf718ddcd88bf25f31092883bdcab540734199648d"
42
+ }
model.py CHANGED
@@ -19,12 +19,11 @@ CHECKPOINT = Path(__file__).with_name("best-model.pt")
19
 
20
 
21
  class TemporalFeatureDropout(nn.Module):
22
- def __init__(self, probability: float = 0.3):
23
  super().__init__()
24
  self.p = float(probability)
25
 
26
  def forward(self, x: torch.Tensor) -> torch.Tensor:
27
- # Dropout is disabled by model.eval() during inference.
28
  if not self.training or self.p == 0.0:
29
  return x
30
  keep = x.new_empty((x.shape[0], x.shape[1], 1)).bernoulli_(1.0 - self.p)
@@ -32,7 +31,7 @@ class TemporalFeatureDropout(nn.Module):
32
 
33
 
34
  class KoshurCRNN(nn.Module):
35
- def __init__(self, n_classes: int, temporal_dropout_p: float = 0.3):
36
  super().__init__()
37
  self.cnn = nn.Sequential(
38
  nn.Conv2d(1, 48, 3, padding=1), nn.BatchNorm2d(48), nn.ReLU(), nn.MaxPool2d((2, 2)),
@@ -59,7 +58,9 @@ class KoshurCRNN(nn.Module):
59
  def load_model() -> tuple[KoshurCRNN, dict[int, str]]:
60
  checkpoint = torch.load(CHECKPOINT, map_location="cpu", weights_only=False)
61
  chars = list(checkpoint["chars"])
62
- model = KoshurCRNN(len(chars) + 1, temporal_dropout_p=0.3)
 
 
63
  model.load_state_dict(checkpoint["model_state_dict"], strict=True)
64
  model.eval()
65
  return model, {i + 1: char for i, char in enumerate(chars)}
@@ -75,8 +76,10 @@ def preprocess(image: Image.Image) -> tuple[torch.Tensor, int, int]:
75
  canvas = Image.new("L", (IMAGE_WIDTH, IMAGE_HEIGHT), 255)
76
  canvas.paste(normal, (0, 0))
77
  pixels = 1.0 - np.asarray(canvas, dtype=np.float32) / 255.0
78
- input_length = max(1, min(IMAGE_WIDTH // HORIZONTAL_STRIDE,
79
- int(math.ceil(resized_width / HORIZONTAL_STRIDE))))
 
 
80
  return torch.from_numpy(pixels).unsqueeze(0).unsqueeze(0), input_length, resized_width
81
 
82
 
@@ -87,8 +90,6 @@ def ctc_decode(ids: list[int], itos: dict[int, str]) -> str:
87
  if token != 0 and token != previous:
88
  output.append(itos.get(token, ""))
89
  previous = token
90
- # CTC was trained in increasing-x visual order. Reverse back to logical RTL
91
- # order for human-readable Kashmiri output.
92
  return "".join(output)[::-1]
93
 
94
 
@@ -99,5 +100,9 @@ def recognize_line(image: Image.Image) -> tuple[str, dict[str, int]]:
99
  logits = model(tensor)
100
  ids = logits.argmax(-1).permute(1, 0)[0, :input_length].tolist()
101
  text = ctc_decode(ids, itos)
102
- return text, {"input_width": image.width, "input_height": image.height,
103
- "resized_width": resized_width, "ctc_frames": input_length}
 
 
 
 
 
19
 
20
 
21
  class TemporalFeatureDropout(nn.Module):
22
+ def __init__(self, probability: float = 0.0):
23
  super().__init__()
24
  self.p = float(probability)
25
 
26
  def forward(self, x: torch.Tensor) -> torch.Tensor:
 
27
  if not self.training or self.p == 0.0:
28
  return x
29
  keep = x.new_empty((x.shape[0], x.shape[1], 1)).bernoulli_(1.0 - self.p)
 
31
 
32
 
33
  class KoshurCRNN(nn.Module):
34
+ def __init__(self, n_classes: int, temporal_dropout_p: float = 0.0):
35
  super().__init__()
36
  self.cnn = nn.Sequential(
37
  nn.Conv2d(1, 48, 3, padding=1), nn.BatchNorm2d(48), nn.ReLU(), nn.MaxPool2d((2, 2)),
 
58
  def load_model() -> tuple[KoshurCRNN, dict[int, str]]:
59
  checkpoint = torch.load(CHECKPOINT, map_location="cpu", weights_only=False)
60
  chars = list(checkpoint["chars"])
61
+ architecture = checkpoint.get("architecture") or {}
62
+ temporal_dropout_p = float(architecture.get("temporal_dropout_p", 0.0))
63
+ model = KoshurCRNN(len(chars) + 1, temporal_dropout_p=temporal_dropout_p)
64
  model.load_state_dict(checkpoint["model_state_dict"], strict=True)
65
  model.eval()
66
  return model, {i + 1: char for i, char in enumerate(chars)}
 
76
  canvas = Image.new("L", (IMAGE_WIDTH, IMAGE_HEIGHT), 255)
77
  canvas.paste(normal, (0, 0))
78
  pixels = 1.0 - np.asarray(canvas, dtype=np.float32) / 255.0
79
+ input_length = max(
80
+ 1,
81
+ min(IMAGE_WIDTH // HORIZONTAL_STRIDE, int(math.ceil(resized_width / HORIZONTAL_STRIDE))),
82
+ )
83
  return torch.from_numpy(pixels).unsqueeze(0).unsqueeze(0), input_length, resized_width
84
 
85
 
 
90
  if token != 0 and token != previous:
91
  output.append(itos.get(token, ""))
92
  previous = token
 
 
93
  return "".join(output)[::-1]
94
 
95
 
 
100
  logits = model(tensor)
101
  ids = logits.argmax(-1).permute(1, 0)[0, :input_length].tolist()
102
  text = ctc_decode(ids, itos)
103
+ return text, {
104
+ "input_width": image.width,
105
+ "input_height": image.height,
106
+ "resized_width": resized_width,
107
+ "ctc_frames": input_length,
108
+ }
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- torch>=2.5
2
- numpy>=2.1
3
- Pillow>=11.0
 
1
+ torch==2.5.1
2
+ numpy==2.1.3
3
+ Pillow==11.0.0