hermitkk commited on
Commit
a5c4687
·
verified ·
1 Parent(s): 38378e4

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ outputs/exports/alphabet_model.ts filter=lfs diff=lfs merge=lfs -text
alphabet/__init__.py ADDED
File without changes
alphabet/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (153 Bytes). View file
 
alphabet/__pycache__/dataset.cpython-314.pyc ADDED
Binary file (7.85 kB). View file
 
alphabet/__pycache__/export.cpython-314.pyc ADDED
Binary file (5.14 kB). View file
 
alphabet/__pycache__/model.cpython-314.pyc ADDED
Binary file (7.41 kB). View file
 
alphabet/__pycache__/train.cpython-314.pyc ADDED
Binary file (12.5 kB). View file
 
alphabet/__pycache__/utils.cpython-314.pyc ADDED
Binary file (4.3 kB). View file
 
alphabet/dataset.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import torch
7
+ from torch.utils.data import DataLoader, Dataset
8
+ from torchvision import transforms
9
+ from torchvision.datasets import EMNIST
10
+
11
+ # EMNIST "letters" split: labels 1–26 map to A–Z
12
+ ALPHABET_CLASSES = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
13
+
14
+
15
+ def _fix_emnist_orientation(t: torch.Tensor) -> torch.Tensor:
16
+ # EMNIST images are stored transposed; this corrects to normal orientation.
17
+ return t.permute(0, 2, 1).flip(1)
18
+
19
+
20
+ class AlphabetDataset(Dataset):
21
+ """Wraps EMNIST 'letters' with orientation fix and optional augmentation."""
22
+
23
+ def __init__(
24
+ self,
25
+ root: str | Path,
26
+ train: bool = True,
27
+ img_size: int = 64,
28
+ augment: bool = False,
29
+ mean: float = 0.5,
30
+ std: float = 0.5,
31
+ ) -> None:
32
+ self.emnist = EMNIST(root=str(root), split="letters", train=train, download=True)
33
+ self.transform = self._build_transform(img_size, augment, mean, std)
34
+
35
+ def _build_transform(
36
+ self, img_size: int, augment: bool, mean: float, std: float
37
+ ) -> transforms.Compose:
38
+ ops: list[Any] = [transforms.Grayscale(num_output_channels=1)]
39
+ if augment:
40
+ ops.extend([
41
+ transforms.RandomAffine(
42
+ degrees=10,
43
+ translate=(0.08, 0.08),
44
+ scale=(0.88, 1.12),
45
+ shear=8,
46
+ fill=255,
47
+ ),
48
+ transforms.RandomPerspective(distortion_scale=0.15, p=0.25, fill=255),
49
+ transforms.ColorJitter(brightness=0.20, contrast=0.20),
50
+ transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 1.5)),
51
+ ])
52
+ ops.extend([
53
+ transforms.Resize((img_size, img_size)),
54
+ transforms.ToTensor(),
55
+ # EMNIST images are stored transposed relative to normal orientation.
56
+ # This corrects them so the model sees normally-oriented letters,
57
+ # matching the real letter crops it will receive at inference time.
58
+ transforms.Lambda(_fix_emnist_orientation),
59
+ transforms.Normalize(mean=[mean], std=[std]),
60
+ ])
61
+ if augment:
62
+ ops.append(transforms.RandomErasing(p=0.10, scale=(0.02, 0.10), value=1.0))
63
+ return transforms.Compose(ops)
64
+
65
+ def __len__(self) -> int:
66
+ return len(self.emnist)
67
+
68
+ def __getitem__(self, idx: int) -> dict[str, Any]:
69
+ image, label = self.emnist[idx]
70
+ tensor = self.transform(image)
71
+ # EMNIST letters are 1-indexed (1=A … 26=Z); convert to 0-indexed
72
+ label_idx = int(label) - 1
73
+ return {
74
+ "image": tensor,
75
+ "label": torch.tensor(label_idx, dtype=torch.long),
76
+ }
77
+
78
+
79
+ def _collate(batch: list[dict[str, Any]]) -> dict[str, Any]:
80
+ images = torch.stack([b["image"] for b in batch], dim=0)
81
+ labels = torch.stack([b["label"] for b in batch], dim=0)
82
+ return {"image": images, "label": labels}
83
+
84
+
85
+ def build_dataloaders(cfg: dict[str, Any], device: str = "cpu") -> tuple[DataLoader, DataLoader]:
86
+ data_cfg = cfg["data"]
87
+ model_cfg = cfg["model"]
88
+ train_cfg = cfg["train"]
89
+
90
+ img_size = int(model_cfg.get("img_size", 64))
91
+ mean = float(model_cfg.get("mean", 0.5))
92
+ std = float(model_cfg.get("std", 0.5))
93
+ root = data_cfg.get("root", "data/emnist")
94
+ batch_size = int(train_cfg.get("batch_size", 256))
95
+ num_workers = int(train_cfg.get("num_workers", 4))
96
+ pin_memory = device.startswith("cuda")
97
+
98
+ train_ds = AlphabetDataset(root, train=True, img_size=img_size, augment=True, mean=mean, std=std)
99
+ val_ds = AlphabetDataset(root, train=False, img_size=img_size, augment=False, mean=mean, std=std)
100
+
101
+ train_loader = DataLoader(
102
+ train_ds,
103
+ batch_size=batch_size,
104
+ shuffle=True,
105
+ num_workers=num_workers,
106
+ pin_memory=pin_memory,
107
+ collate_fn=_collate,
108
+ drop_last=False,
109
+ )
110
+ val_loader = DataLoader(
111
+ val_ds,
112
+ batch_size=batch_size,
113
+ shuffle=False,
114
+ num_workers=num_workers,
115
+ pin_memory=pin_memory,
116
+ collate_fn=_collate,
117
+ drop_last=False,
118
+ )
119
+ return train_loader, val_loader
120
+
121
+
122
+ def preprocess_crop_for_inference(
123
+ image: Any, # PIL Image
124
+ img_size: int = 64,
125
+ mean: float = 0.5,
126
+ std: float = 0.5,
127
+ ) -> torch.Tensor:
128
+ """Preprocess a real letter crop for inference (no EMNIST orientation fix)."""
129
+ transform = transforms.Compose([
130
+ transforms.Grayscale(num_output_channels=1),
131
+ transforms.Resize((img_size, img_size)),
132
+ transforms.ToTensor(),
133
+ transforms.Normalize(mean=[mean], std=[std]),
134
+ ])
135
+ return transform(image)
136
+
137
+
138
+ __all__ = ["ALPHABET_CLASSES", "AlphabetDataset", "build_dataloaders", "preprocess_crop_for_inference"]
alphabet/export.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import torch
6
+
7
+ from alphabet.model import build_model
8
+ from alphabet.utils import ensure_dir, load_yaml
9
+
10
+
11
+ @torch.no_grad()
12
+ def load_checkpoint_model(
13
+ checkpoint_path: str | Path, cfg: dict, device: str = "cpu"
14
+ ) -> torch.nn.Module:
15
+ model = build_model(cfg["model"])
16
+ checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
17
+ state_dict = (
18
+ checkpoint["model_state"]
19
+ if isinstance(checkpoint, dict) and "model_state" in checkpoint
20
+ else checkpoint
21
+ )
22
+ model.load_state_dict(state_dict)
23
+ model.to(device)
24
+ model.eval()
25
+ return model
26
+
27
+
28
+ @torch.no_grad()
29
+ def export_onnx(
30
+ model: torch.nn.Module,
31
+ output_path: str | Path,
32
+ input_shape: tuple[int, int, int, int] = (1, 1, 64, 64),
33
+ opset: int = 17,
34
+ device: str = "cpu",
35
+ ) -> Path:
36
+ ensure_dir(Path(output_path).parent)
37
+ example = torch.randn(*input_shape, device=device)
38
+ try:
39
+ torch.onnx.export(
40
+ model,
41
+ (example,),
42
+ str(output_path),
43
+ input_names=["input"],
44
+ output_names=["logits"],
45
+ opset_version=opset,
46
+ dynamo=True,
47
+ )
48
+ except Exception:
49
+ torch.onnx.export(
50
+ model,
51
+ (example,),
52
+ str(output_path),
53
+ input_names=["input"],
54
+ output_names=["logits"],
55
+ opset_version=opset,
56
+ dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},
57
+ dynamo=False,
58
+ )
59
+ return Path(output_path)
60
+
61
+
62
+ @torch.no_grad()
63
+ def export_torchscript(
64
+ model: torch.nn.Module,
65
+ output_path: str | Path,
66
+ input_shape: tuple[int, int, int, int] = (1, 1, 64, 64),
67
+ device: str = "cpu",
68
+ ) -> Path:
69
+ ensure_dir(Path(output_path).parent)
70
+ example = torch.randn(*input_shape, device=device)
71
+ traced = torch.jit.trace(model, example, strict=False)
72
+ traced = torch.jit.freeze(traced)
73
+ traced.save(str(output_path))
74
+ return Path(output_path)
75
+
76
+
77
+ def export_from_config(
78
+ config_path: str | Path,
79
+ checkpoint_path: str | Path,
80
+ device: str = "cpu",
81
+ ) -> dict[str, str]:
82
+ cfg = load_yaml(config_path)
83
+ model = load_checkpoint_model(checkpoint_path, cfg, device=device)
84
+ export_cfg = cfg.get("export", {})
85
+ img_size = int(cfg["model"].get("img_size", 64))
86
+ in_channels = int(cfg["model"].get("in_channels", 1))
87
+ input_shape = (1, in_channels, img_size, img_size)
88
+
89
+ onnx_path = export_onnx(
90
+ model,
91
+ export_cfg.get("onnx_path", "outputs/exports/alphabet_model.onnx"),
92
+ input_shape=input_shape,
93
+ opset=int(export_cfg.get("onnx_opset", 17)),
94
+ device=device,
95
+ )
96
+ ts_path = export_torchscript(
97
+ model,
98
+ export_cfg.get("torchscript_path", "outputs/exports/alphabet_model.ts"),
99
+ input_shape=input_shape,
100
+ device=device,
101
+ )
102
+ print(f"ONNX exported → {onnx_path}")
103
+ print(f"TorchScript → {ts_path}")
104
+ return {"onnx": str(onnx_path), "torchscript": str(ts_path)}
105
+
106
+
107
+ __all__ = ["load_checkpoint_model", "export_onnx", "export_torchscript", "export_from_config"]
alphabet/infer.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import cv2
7
+ import numpy as np
8
+ import torch
9
+ from PIL import Image
10
+
11
+ from alphabet.dataset import ALPHABET_CLASSES, preprocess_crop_for_inference
12
+
13
+
14
+ class OnnxBackend:
15
+ """ONNX runtime backend for the 26-class alphabet model."""
16
+
17
+ def __init__(self, model_path: str | Path) -> None:
18
+ try:
19
+ import onnxruntime as ort
20
+ except ImportError as exc:
21
+ raise ImportError("Install onnxruntime to use the ONNX backend.") from exc
22
+ providers = ["CPUExecutionProvider"]
23
+ self.session = ort.InferenceSession(str(model_path), providers=providers)
24
+ self.input_name = self.session.get_inputs()[0].name
25
+ self.output_name = self.session.get_outputs()[0].name
26
+
27
+ def predict(self, batch_tensor: torch.Tensor) -> np.ndarray:
28
+ """Return softmax probabilities, shape (batch, 26)."""
29
+ arr = batch_tensor.detach().cpu().numpy().astype(np.float32)
30
+ logits = self.session.run([self.output_name], {self.input_name: arr})[0]
31
+ # numerically stable softmax
32
+ logits = logits - logits.max(axis=1, keepdims=True)
33
+ e = np.exp(logits)
34
+ return e / e.sum(axis=1, keepdims=True)
35
+
36
+
37
+ def predict_crops(
38
+ backend: OnnxBackend,
39
+ crops: list[np.ndarray],
40
+ img_size: int = 64,
41
+ mean: float = 0.5,
42
+ std: float = 0.5,
43
+ min_confidence: float = 0.60,
44
+ batch_size: int = 256,
45
+ ) -> list[dict[str, Any]]:
46
+ """
47
+ Run inference on a list of pre-cropped BGR letter images.
48
+
49
+ Returns one dict per crop with:
50
+ letter — predicted character ('A'–'Z')
51
+ prediction_index — 0–25
52
+ confidence — softmax probability of the top class
53
+ probabilities — full 26-element list
54
+ flag — True when confidence < min_confidence (needs human review)
55
+ """
56
+ tensors: list[torch.Tensor] = []
57
+ for crop in crops:
58
+ pil = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
59
+ tensors.append(preprocess_crop_for_inference(pil, img_size=img_size, mean=mean, std=std))
60
+
61
+ all_probs: list[np.ndarray] = []
62
+ for i in range(0, len(tensors), batch_size):
63
+ batch = torch.stack(tensors[i : i + batch_size], dim=0)
64
+ all_probs.append(backend.predict(batch))
65
+
66
+ probs_matrix = np.concatenate(all_probs, axis=0) if all_probs else np.zeros((0, 26))
67
+
68
+ results: list[dict[str, Any]] = []
69
+ for probs in probs_matrix:
70
+ idx = int(probs.argmax())
71
+ confidence = float(probs[idx])
72
+ results.append({
73
+ "letter": ALPHABET_CLASSES[idx],
74
+ "prediction_index": idx,
75
+ "confidence": confidence,
76
+ "probabilities": probs.tolist(),
77
+ "flag": confidence < min_confidence,
78
+ })
79
+ return results
80
+
81
+
82
+ __all__ = ["OnnxBackend", "predict_crops"]
alphabet/model.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+
6
+
7
+ class DSConv(nn.Module):
8
+ """Depthwise-separable conv block — same as omr_proj."""
9
+ def __init__(self, in_ch: int, out_ch: int, stride: int = 1) -> None:
10
+ super().__init__()
11
+ self.block = nn.Sequential(
12
+ nn.Conv2d(in_ch, in_ch, 3, stride=stride, padding=1, groups=in_ch, bias=False),
13
+ nn.BatchNorm2d(in_ch),
14
+ nn.ReLU(inplace=True),
15
+ nn.Conv2d(in_ch, out_ch, 1, bias=False),
16
+ nn.BatchNorm2d(out_ch),
17
+ nn.ReLU(inplace=True),
18
+ )
19
+
20
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
21
+ return self.block(x)
22
+
23
+
24
+ class TinyAlphaNet(nn.Module):
25
+ def __init__(self, in_channels: int = 1, num_classes: int = 26, dropout: float = 0.20) -> None:
26
+ super().__init__()
27
+ self.features = nn.Sequential(
28
+ nn.Conv2d(in_channels, 16, 3, padding=1, bias=False),
29
+ nn.BatchNorm2d(16),
30
+ nn.ReLU(inplace=True),
31
+ nn.MaxPool2d(2),
32
+ DSConv(16, 24),
33
+ nn.MaxPool2d(2),
34
+ DSConv(24, 32),
35
+ nn.MaxPool2d(2),
36
+ DSConv(32, 48),
37
+ nn.AdaptiveAvgPool2d(1),
38
+ )
39
+ self.head = nn.Sequential(
40
+ nn.Flatten(),
41
+ nn.Linear(48, 64),
42
+ nn.ReLU(inplace=True),
43
+ nn.Dropout(dropout),
44
+ nn.Linear(64, num_classes),
45
+ )
46
+
47
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
48
+ return self.head(self.features(x))
49
+
50
+
51
+ class SmallAlphaNet(nn.Module):
52
+ def __init__(self, in_channels: int = 1, num_classes: int = 26, dropout: float = 0.30) -> None:
53
+ super().__init__()
54
+ self.features = nn.Sequential(
55
+ nn.Conv2d(in_channels, 24, 3, stride=2, padding=1, bias=False),
56
+ nn.BatchNorm2d(24),
57
+ nn.ReLU(inplace=True),
58
+ DSConv(24, 32),
59
+ DSConv(32, 48, stride=2),
60
+ DSConv(48, 64),
61
+ DSConv(64, 96, stride=2),
62
+ DSConv(96, 128),
63
+ nn.AdaptiveAvgPool2d(1),
64
+ )
65
+ self.head = nn.Sequential(
66
+ nn.Flatten(),
67
+ nn.Linear(128, 64),
68
+ nn.ReLU(inplace=True),
69
+ nn.Dropout(dropout),
70
+ nn.Linear(64, num_classes),
71
+ )
72
+
73
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
74
+ return self.head(self.features(x))
75
+
76
+
77
+ def build_model(cfg: dict) -> nn.Module:
78
+ name = cfg.get("name", "small_cnn").lower()
79
+ in_channels = int(cfg.get("in_channels", 1))
80
+ num_classes = int(cfg.get("num_classes", 26))
81
+ dropout = float(cfg.get("dropout", 0.30))
82
+ if name == "tiny_cnn":
83
+ return TinyAlphaNet(in_channels=in_channels, num_classes=num_classes, dropout=dropout)
84
+ if name == "small_cnn":
85
+ return SmallAlphaNet(in_channels=in_channels, num_classes=num_classes, dropout=dropout)
86
+ raise ValueError(f"Unknown model name: {name!r}. Choose 'tiny_cnn' or 'small_cnn'.")
87
+
88
+
89
+ def count_parameters(model: nn.Module) -> int:
90
+ return sum(p.numel() for p in model.parameters())
91
+
92
+
93
+ __all__ = ["DSConv", "TinyAlphaNet", "SmallAlphaNet", "build_model", "count_parameters"]
alphabet/train.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from contextlib import nullcontext
5
+ from copy import deepcopy
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn as nn
12
+ from sklearn.metrics import accuracy_score, classification_report
13
+
14
+ from alphabet.dataset import ALPHABET_CLASSES, build_dataloaders
15
+ from alphabet.export import export_from_config
16
+ from alphabet.model import build_model, count_parameters
17
+ from alphabet.utils import ensure_dir, load_yaml, save_json, set_seed, write_csv
18
+
19
+
20
+ class EarlyStopper:
21
+ def __init__(self, patience: int = 8, mode: str = "max") -> None:
22
+ self.patience = patience
23
+ self.mode = mode
24
+ self.best: float | None = None
25
+ self.bad_epochs = 0
26
+
27
+ def step(self, value: float) -> bool:
28
+ if self.best is None:
29
+ self.best = value
30
+ return False
31
+ improved = value > self.best if self.mode == "max" else value < self.best
32
+ if improved:
33
+ self.best = value
34
+ self.bad_epochs = 0
35
+ return False
36
+ self.bad_epochs += 1
37
+ return self.bad_epochs >= self.patience
38
+
39
+
40
+ def _forward_context(device: str, amp_enabled: bool):
41
+ if amp_enabled and device.startswith("cuda"):
42
+ return torch.cuda.amp.autocast()
43
+ return nullcontext()
44
+
45
+
46
+ def train_one_epoch(
47
+ model: nn.Module,
48
+ loader: torch.utils.data.DataLoader,
49
+ optimizer: torch.optim.Optimizer,
50
+ criterion: nn.Module,
51
+ device: str,
52
+ scaler: torch.cuda.amp.GradScaler | None,
53
+ amp_enabled: bool,
54
+ ) -> float:
55
+ model.train()
56
+ losses: list[float] = []
57
+ for batch in loader:
58
+ x = batch["image"].to(device, non_blocking=True)
59
+ y = batch["label"].to(device, non_blocking=True)
60
+ optimizer.zero_grad(set_to_none=True)
61
+ with _forward_context(device, amp_enabled):
62
+ logits = model(x)
63
+ loss = criterion(logits, y)
64
+ if scaler is not None:
65
+ scaler.scale(loss).backward()
66
+ scaler.step(optimizer)
67
+ scaler.update()
68
+ else:
69
+ loss.backward()
70
+ optimizer.step()
71
+ losses.append(float(loss.detach().cpu().item()))
72
+ return float(np.mean(losses)) if losses else math.nan
73
+
74
+
75
+ @torch.no_grad()
76
+ def validate(
77
+ model: nn.Module,
78
+ loader: torch.utils.data.DataLoader,
79
+ criterion: nn.Module,
80
+ device: str,
81
+ ) -> tuple[float, dict[str, Any]]:
82
+ model.eval()
83
+ losses: list[float] = []
84
+ all_preds: list[np.ndarray] = []
85
+ all_labels: list[np.ndarray] = []
86
+
87
+ for batch in loader:
88
+ x = batch["image"].to(device, non_blocking=True)
89
+ y = batch["label"].to(device, non_blocking=True)
90
+ logits = model(x)
91
+ loss = criterion(logits, y)
92
+ preds = logits.argmax(dim=1).cpu().numpy()
93
+ all_preds.append(preds)
94
+ all_labels.append(y.cpu().numpy())
95
+ losses.append(float(loss.item()))
96
+
97
+ y_pred = np.concatenate(all_preds)
98
+ y_true = np.concatenate(all_labels)
99
+ accuracy = float(accuracy_score(y_true, y_pred))
100
+ report = classification_report(
101
+ y_true, y_pred,
102
+ target_names=ALPHABET_CLASSES,
103
+ output_dict=True,
104
+ zero_division=0,
105
+ )
106
+ metrics = {"accuracy": accuracy, "report": report}
107
+ return float(np.mean(losses)) if losses else math.nan, metrics
108
+
109
+
110
+ def save_checkpoint(
111
+ path: str | Path,
112
+ model: nn.Module,
113
+ optimizer: torch.optim.Optimizer,
114
+ scheduler: Any,
115
+ epoch: int,
116
+ cfg: dict[str, Any],
117
+ metrics: dict[str, Any],
118
+ ) -> None:
119
+ ensure_dir(Path(path).parent)
120
+ torch.save(
121
+ {
122
+ "epoch": epoch,
123
+ "model_state": model.state_dict(),
124
+ "optimizer_state": optimizer.state_dict(),
125
+ "scheduler_state": scheduler.state_dict() if scheduler is not None else None,
126
+ "config": cfg,
127
+ "metrics": metrics,
128
+ },
129
+ path,
130
+ )
131
+
132
+
133
+ def train_from_config(config_path: str | Path, device: str | None = None) -> dict[str, Any]:
134
+ cfg = load_yaml(config_path)
135
+ set_seed(int(cfg.get("seed", 42)))
136
+ device = device or (
137
+ "cuda" if torch.cuda.is_available()
138
+ else "mps" if torch.backends.mps.is_available()
139
+ else "cpu"
140
+ )
141
+ save_dir = ensure_dir(cfg["train"].get("save_dir", "outputs/checkpoints"))
142
+
143
+ train_loader, val_loader = build_dataloaders(cfg, device=device)
144
+ model = build_model(cfg["model"]).to(device)
145
+
146
+ print(f"Model parameters: {count_parameters(model):,}")
147
+ print(f"Training on {device} | "
148
+ f"{len(train_loader.dataset)} train / {len(val_loader.dataset)} val samples")
149
+
150
+ criterion = nn.CrossEntropyLoss()
151
+ optimizer = torch.optim.AdamW(
152
+ model.parameters(),
153
+ lr=float(cfg["train"].get("lr", 1e-3)),
154
+ weight_decay=float(cfg["train"].get("weight_decay", 1e-4)),
155
+ )
156
+ epochs = int(cfg["train"].get("epochs", 30))
157
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
158
+ amp_enabled = bool(cfg["train"].get("amp", True)) and device.startswith("cuda")
159
+ scaler = torch.cuda.amp.GradScaler(enabled=amp_enabled) if amp_enabled else None
160
+ early = EarlyStopper(patience=int(cfg["train"].get("early_patience", 8)), mode="max")
161
+
162
+ history: list[dict[str, Any]] = []
163
+ best_state: dict[str, Any] | None = None
164
+ best_score = -1.0
165
+
166
+ for epoch in range(1, epochs + 1):
167
+ train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device, scaler, amp_enabled)
168
+ val_loss, val_metrics = validate(model, val_loader, criterion, device)
169
+ score = val_metrics["accuracy"]
170
+ scheduler.step()
171
+
172
+ print(
173
+ f"Epoch {epoch:>3}/{epochs} | "
174
+ f"train_loss={train_loss:.4f} | "
175
+ f"val_loss={val_loss:.4f} | "
176
+ f"val_acc={score:.4f}"
177
+ )
178
+
179
+ row = {
180
+ "epoch": epoch,
181
+ "lr": float(optimizer.param_groups[0]["lr"]),
182
+ "train_loss": float(train_loss),
183
+ "val_loss": float(val_loss),
184
+ "val_accuracy": float(score),
185
+ }
186
+ history.append(row)
187
+
188
+ save_checkpoint(save_dir / "last.pt", model, optimizer, scheduler, epoch, cfg, row)
189
+
190
+ if score > best_score:
191
+ best_score = score
192
+ best_state = deepcopy(model.state_dict())
193
+ save_checkpoint(save_dir / "best.pt", model, optimizer, scheduler, epoch, cfg, row)
194
+ save_json(val_metrics, save_dir / "best_metrics.json")
195
+
196
+ if early.step(score):
197
+ print(f"Early stopping at epoch {epoch}.")
198
+ break
199
+
200
+ write_csv(history, save_dir / "train_log.csv")
201
+
202
+ if best_state is not None:
203
+ model.load_state_dict(best_state)
204
+
205
+ _, final_metrics = validate(model, val_loader, criterion, device)
206
+ save_json(final_metrics, save_dir / "final_metrics.json")
207
+
208
+ summary = {
209
+ "device": device,
210
+ "params": count_parameters(model),
211
+ "train_size": len(train_loader.dataset),
212
+ "val_size": len(val_loader.dataset),
213
+ "best_val_accuracy": best_score,
214
+ "final_metrics": final_metrics,
215
+ }
216
+ save_json(summary, save_dir / "summary.json")
217
+
218
+ if bool(cfg.get("export", {}).get("after_train", True)):
219
+ export_from_config(config_path, save_dir / "best.pt", device=device)
220
+
221
+ return summary
222
+
223
+
224
+ __all__ = ["train_from_config"]
alphabet/utils.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
+ import logging
6
+ import random
7
+ from pathlib import Path
8
+ from typing import Any, Iterable
9
+
10
+ import numpy as np
11
+ import torch
12
+ import yaml
13
+
14
+
15
+ LOGGER = logging.getLogger("alphabet")
16
+
17
+
18
+ def setup_logging(level: str = "INFO") -> None:
19
+ logging.basicConfig(
20
+ level=getattr(logging, level.upper(), logging.INFO),
21
+ format="%(asctime)s | %(levelname)s | %(message)s",
22
+ )
23
+
24
+
25
+ def set_seed(seed: int = 42) -> None:
26
+ random.seed(seed)
27
+ np.random.seed(seed)
28
+ torch.manual_seed(seed)
29
+ torch.cuda.manual_seed_all(seed)
30
+ torch.backends.cudnn.deterministic = True
31
+ torch.backends.cudnn.benchmark = False
32
+
33
+
34
+ def ensure_dir(path: str | Path) -> Path:
35
+ path = Path(path)
36
+ path.mkdir(parents=True, exist_ok=True)
37
+ return path
38
+
39
+
40
+ def load_yaml(path: str | Path) -> dict[str, Any]:
41
+ with open(path, "r", encoding="utf-8") as f:
42
+ return yaml.safe_load(f)
43
+
44
+
45
+ def save_json(data: Any, path: str | Path, indent: int = 2) -> None:
46
+ with open(path, "w", encoding="utf-8") as f:
47
+ json.dump(data, f, ensure_ascii=False, indent=indent)
48
+
49
+
50
+ def write_csv(rows: list[dict[str, Any]], path: str | Path) -> None:
51
+ if not rows:
52
+ return
53
+ with open(path, "w", encoding="utf-8", newline="") as f:
54
+ writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
55
+ writer.writeheader()
56
+ writer.writerows(rows)
57
+
58
+
59
+ __all__ = [
60
+ "LOGGER",
61
+ "setup_logging",
62
+ "set_seed",
63
+ "ensure_dir",
64
+ "load_yaml",
65
+ "save_json",
66
+ "write_csv",
67
+ ]
config/config.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+
3
+ data:
4
+ root: data/emnist # torchvision downloads EMNIST here on first run
5
+
6
+ model:
7
+ name: small_cnn # tiny_cnn | small_cnn
8
+ in_channels: 1
9
+ num_classes: 26
10
+ img_size: 64
11
+ mean: 0.5
12
+ std: 0.5
13
+ dropout: 0.30
14
+
15
+ train:
16
+ batch_size: 256
17
+ num_workers: 4
18
+ epochs: 30
19
+ lr: 0.001
20
+ weight_decay: 0.0001
21
+ amp: true # mixed precision — only active on CUDA
22
+ early_patience: 8
23
+ save_dir: outputs/checkpoints
24
+
25
+ infer:
26
+ min_confidence: 0.60 # flag for human review below this threshold
27
+ batch_size: 256
28
+
29
+ export:
30
+ after_train: true
31
+ onnx_opset: 17
32
+ onnx_path: outputs/exports/alphabet_model.onnx
33
+ torchscript_path: outputs/exports/alphabet_model.ts
main.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from alphabet.utils import setup_logging
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(description="Alphabet (A-Z) classifier CLI")
12
+ parser.add_argument("--log-level", default="INFO")
13
+ subparsers = parser.add_subparsers(dest="command", required=True)
14
+
15
+ p_train = subparsers.add_parser("train", help="Download EMNIST and train the model")
16
+ p_train.add_argument("--config", default="config/config.yaml")
17
+ p_train.add_argument("--device", default=None, help="cuda | cpu (auto-detects if omitted)")
18
+
19
+ p_export = subparsers.add_parser("export", help="Export a checkpoint to ONNX and TorchScript")
20
+ p_export.add_argument("--config", default="config/config.yaml")
21
+ p_export.add_argument("--checkpoint", required=True, help="Path to best.pt")
22
+ p_export.add_argument("--device", default="cpu")
23
+
24
+ p_infer = subparsers.add_parser("infer", help="Run inference on a single image (for testing)")
25
+ p_infer.add_argument("--model", required=True, help="Path to .onnx model")
26
+ p_infer.add_argument("--image", required=True, help="Path to a letter crop image")
27
+ p_infer.add_argument("--config", default="config/config.yaml")
28
+
29
+ args = parser.parse_args()
30
+ setup_logging(args.log_level)
31
+
32
+ if args.command == "train":
33
+ from alphabet.train import train_from_config
34
+ summary = train_from_config(args.config, device=args.device)
35
+ print(f"\nBest val accuracy: {summary['best_val_accuracy']:.4f}")
36
+ return
37
+
38
+ if args.command == "export":
39
+ from alphabet.export import export_from_config
40
+ exported = export_from_config(args.config, args.checkpoint, device=args.device)
41
+ print(exported)
42
+ return
43
+
44
+ if args.command == "infer":
45
+ import cv2
46
+ import numpy as np
47
+ from alphabet.infer import OnnxBackend, predict_crops
48
+ from alphabet.utils import load_yaml
49
+
50
+ cfg = load_yaml(args.config)
51
+ infer_cfg = cfg.get("infer", {})
52
+ model_cfg = cfg["model"]
53
+
54
+ crop = cv2.imread(args.image, cv2.IMREAD_COLOR)
55
+ if crop is None:
56
+ print(f"Error: could not read image at {args.image}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+ backend = OnnxBackend(args.model)
60
+ results = predict_crops(
61
+ backend,
62
+ [crop],
63
+ img_size=int(model_cfg.get("img_size", 64)),
64
+ mean=float(model_cfg.get("mean", 0.5)),
65
+ std=float(model_cfg.get("std", 0.5)),
66
+ min_confidence=float(infer_cfg.get("min_confidence", 0.60)),
67
+ )
68
+ r = results[0]
69
+ flag_str = " [FLAGGED for review]" if r["flag"] else ""
70
+ print(f"Prediction : {r['letter']}{flag_str}")
71
+ print(f"Confidence : {r['confidence']:.4f}")
72
+ top3 = sorted(enumerate(r["probabilities"]), key=lambda x: -x[1])[:3]
73
+ for idx, prob in top3:
74
+ letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[idx]
75
+ print(f" {letter}: {prob:.4f}")
76
+ return
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
outputs/checkpoints/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d1465af892d48aeef51d9e5bdd90018ea69c37c998ff492055c33be4eae6ccd8
3
+ size 505956
outputs/checkpoints/best_metrics.json ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accuracy": 0.9410096153846154,
3
+ "report": {
4
+ "A": {
5
+ "precision": 0.9084507042253521,
6
+ "recall": 0.9675,
7
+ "f1-score": 0.937046004842615,
8
+ "support": 800.0
9
+ },
10
+ "B": {
11
+ "precision": 0.9810366624525917,
12
+ "recall": 0.97,
13
+ "f1-score": 0.9754871150219987,
14
+ "support": 800.0
15
+ },
16
+ "C": {
17
+ "precision": 0.9752168525402726,
18
+ "recall": 0.98375,
19
+ "f1-score": 0.9794648413192284,
20
+ "support": 800.0
21
+ },
22
+ "D": {
23
+ "precision": 0.9523809523809523,
24
+ "recall": 0.95,
25
+ "f1-score": 0.951188986232791,
26
+ "support": 800.0
27
+ },
28
+ "E": {
29
+ "precision": 0.9774153074027604,
30
+ "recall": 0.97375,
31
+ "f1-score": 0.9755792110206637,
32
+ "support": 800.0
33
+ },
34
+ "F": {
35
+ "precision": 0.9758269720101781,
36
+ "recall": 0.95875,
37
+ "f1-score": 0.9672131147540983,
38
+ "support": 800.0
39
+ },
40
+ "G": {
41
+ "precision": 0.8993103448275862,
42
+ "recall": 0.815,
43
+ "f1-score": 0.8550819672131148,
44
+ "support": 800.0
45
+ },
46
+ "H": {
47
+ "precision": 0.957286432160804,
48
+ "recall": 0.9525,
49
+ "f1-score": 0.9548872180451128,
50
+ "support": 800.0
51
+ },
52
+ "I": {
53
+ "precision": 0.7487437185929648,
54
+ "recall": 0.745,
55
+ "f1-score": 0.7468671679197995,
56
+ "support": 800.0
57
+ },
58
+ "J": {
59
+ "precision": 0.9603072983354674,
60
+ "recall": 0.9375,
61
+ "f1-score": 0.9487666034155597,
62
+ "support": 800.0
63
+ },
64
+ "K": {
65
+ "precision": 0.9822109275730623,
66
+ "recall": 0.96625,
67
+ "f1-score": 0.9741650913673598,
68
+ "support": 800.0
69
+ },
70
+ "L": {
71
+ "precision": 0.75,
72
+ "recall": 0.7575,
73
+ "f1-score": 0.753731343283582,
74
+ "support": 800.0
75
+ },
76
+ "M": {
77
+ "precision": 0.9826732673267327,
78
+ "recall": 0.9925,
79
+ "f1-score": 0.9875621890547264,
80
+ "support": 800.0
81
+ },
82
+ "N": {
83
+ "precision": 0.9552238805970149,
84
+ "recall": 0.96,
85
+ "f1-score": 0.9576059850374065,
86
+ "support": 800.0
87
+ },
88
+ "O": {
89
+ "precision": 0.9504830917874396,
90
+ "recall": 0.98375,
91
+ "f1-score": 0.9668304668304668,
92
+ "support": 800.0
93
+ },
94
+ "P": {
95
+ "precision": 0.986284289276808,
96
+ "recall": 0.98875,
97
+ "f1-score": 0.9875156054931336,
98
+ "support": 800.0
99
+ },
100
+ "Q": {
101
+ "precision": 0.8583850931677018,
102
+ "recall": 0.86375,
103
+ "f1-score": 0.8610591900311526,
104
+ "support": 800.0
105
+ },
106
+ "R": {
107
+ "precision": 0.9613466334164589,
108
+ "recall": 0.96375,
109
+ "f1-score": 0.9625468164794008,
110
+ "support": 800.0
111
+ },
112
+ "S": {
113
+ "precision": 0.9777227722772277,
114
+ "recall": 0.9875,
115
+ "f1-score": 0.9825870646766169,
116
+ "support": 800.0
117
+ },
118
+ "T": {
119
+ "precision": 0.9489671931956257,
120
+ "recall": 0.97625,
121
+ "f1-score": 0.9624152803450401,
122
+ "support": 800.0
123
+ },
124
+ "U": {
125
+ "precision": 0.9527458492975734,
126
+ "recall": 0.9325,
127
+ "f1-score": 0.9425142135186355,
128
+ "support": 800.0
129
+ },
130
+ "V": {
131
+ "precision": 0.9334155363748459,
132
+ "recall": 0.94625,
133
+ "f1-score": 0.9397889509621353,
134
+ "support": 800.0
135
+ },
136
+ "W": {
137
+ "precision": 0.9837092731829574,
138
+ "recall": 0.98125,
139
+ "f1-score": 0.9824780976220275,
140
+ "support": 800.0
141
+ },
142
+ "X": {
143
+ "precision": 0.9785353535353535,
144
+ "recall": 0.96875,
145
+ "f1-score": 0.9736180904522613,
146
+ "support": 800.0
147
+ },
148
+ "Y": {
149
+ "precision": 0.9478908188585607,
150
+ "recall": 0.955,
151
+ "f1-score": 0.9514321295143213,
152
+ "support": 800.0
153
+ },
154
+ "Z": {
155
+ "precision": 0.9813895781637717,
156
+ "recall": 0.98875,
157
+ "f1-score": 0.9850560398505604,
158
+ "support": 800.0
159
+ },
160
+ "accuracy": 0.9410096153846154,
161
+ "macro avg": {
162
+ "precision": 0.9410368770369255,
163
+ "recall": 0.9410096153846154,
164
+ "f1-score": 0.9408649532424541,
165
+ "support": 20800.0
166
+ },
167
+ "weighted avg": {
168
+ "precision": 0.9410368770369254,
169
+ "recall": 0.9410096153846154,
170
+ "f1-score": 0.940864953242454,
171
+ "support": 20800.0
172
+ }
173
+ }
174
+ }
outputs/checkpoints/final_metrics.json ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "accuracy": 0.9410096153846154,
3
+ "report": {
4
+ "A": {
5
+ "precision": 0.9084507042253521,
6
+ "recall": 0.9675,
7
+ "f1-score": 0.937046004842615,
8
+ "support": 800.0
9
+ },
10
+ "B": {
11
+ "precision": 0.9810366624525917,
12
+ "recall": 0.97,
13
+ "f1-score": 0.9754871150219987,
14
+ "support": 800.0
15
+ },
16
+ "C": {
17
+ "precision": 0.9752168525402726,
18
+ "recall": 0.98375,
19
+ "f1-score": 0.9794648413192284,
20
+ "support": 800.0
21
+ },
22
+ "D": {
23
+ "precision": 0.9523809523809523,
24
+ "recall": 0.95,
25
+ "f1-score": 0.951188986232791,
26
+ "support": 800.0
27
+ },
28
+ "E": {
29
+ "precision": 0.9774153074027604,
30
+ "recall": 0.97375,
31
+ "f1-score": 0.9755792110206637,
32
+ "support": 800.0
33
+ },
34
+ "F": {
35
+ "precision": 0.9758269720101781,
36
+ "recall": 0.95875,
37
+ "f1-score": 0.9672131147540983,
38
+ "support": 800.0
39
+ },
40
+ "G": {
41
+ "precision": 0.8993103448275862,
42
+ "recall": 0.815,
43
+ "f1-score": 0.8550819672131148,
44
+ "support": 800.0
45
+ },
46
+ "H": {
47
+ "precision": 0.957286432160804,
48
+ "recall": 0.9525,
49
+ "f1-score": 0.9548872180451128,
50
+ "support": 800.0
51
+ },
52
+ "I": {
53
+ "precision": 0.7487437185929648,
54
+ "recall": 0.745,
55
+ "f1-score": 0.7468671679197995,
56
+ "support": 800.0
57
+ },
58
+ "J": {
59
+ "precision": 0.9603072983354674,
60
+ "recall": 0.9375,
61
+ "f1-score": 0.9487666034155597,
62
+ "support": 800.0
63
+ },
64
+ "K": {
65
+ "precision": 0.9822109275730623,
66
+ "recall": 0.96625,
67
+ "f1-score": 0.9741650913673598,
68
+ "support": 800.0
69
+ },
70
+ "L": {
71
+ "precision": 0.75,
72
+ "recall": 0.7575,
73
+ "f1-score": 0.753731343283582,
74
+ "support": 800.0
75
+ },
76
+ "M": {
77
+ "precision": 0.9826732673267327,
78
+ "recall": 0.9925,
79
+ "f1-score": 0.9875621890547264,
80
+ "support": 800.0
81
+ },
82
+ "N": {
83
+ "precision": 0.9552238805970149,
84
+ "recall": 0.96,
85
+ "f1-score": 0.9576059850374065,
86
+ "support": 800.0
87
+ },
88
+ "O": {
89
+ "precision": 0.9504830917874396,
90
+ "recall": 0.98375,
91
+ "f1-score": 0.9668304668304668,
92
+ "support": 800.0
93
+ },
94
+ "P": {
95
+ "precision": 0.986284289276808,
96
+ "recall": 0.98875,
97
+ "f1-score": 0.9875156054931336,
98
+ "support": 800.0
99
+ },
100
+ "Q": {
101
+ "precision": 0.8583850931677018,
102
+ "recall": 0.86375,
103
+ "f1-score": 0.8610591900311526,
104
+ "support": 800.0
105
+ },
106
+ "R": {
107
+ "precision": 0.9613466334164589,
108
+ "recall": 0.96375,
109
+ "f1-score": 0.9625468164794008,
110
+ "support": 800.0
111
+ },
112
+ "S": {
113
+ "precision": 0.9777227722772277,
114
+ "recall": 0.9875,
115
+ "f1-score": 0.9825870646766169,
116
+ "support": 800.0
117
+ },
118
+ "T": {
119
+ "precision": 0.9489671931956257,
120
+ "recall": 0.97625,
121
+ "f1-score": 0.9624152803450401,
122
+ "support": 800.0
123
+ },
124
+ "U": {
125
+ "precision": 0.9527458492975734,
126
+ "recall": 0.9325,
127
+ "f1-score": 0.9425142135186355,
128
+ "support": 800.0
129
+ },
130
+ "V": {
131
+ "precision": 0.9334155363748459,
132
+ "recall": 0.94625,
133
+ "f1-score": 0.9397889509621353,
134
+ "support": 800.0
135
+ },
136
+ "W": {
137
+ "precision": 0.9837092731829574,
138
+ "recall": 0.98125,
139
+ "f1-score": 0.9824780976220275,
140
+ "support": 800.0
141
+ },
142
+ "X": {
143
+ "precision": 0.9785353535353535,
144
+ "recall": 0.96875,
145
+ "f1-score": 0.9736180904522613,
146
+ "support": 800.0
147
+ },
148
+ "Y": {
149
+ "precision": 0.9478908188585607,
150
+ "recall": 0.955,
151
+ "f1-score": 0.9514321295143213,
152
+ "support": 800.0
153
+ },
154
+ "Z": {
155
+ "precision": 0.9813895781637717,
156
+ "recall": 0.98875,
157
+ "f1-score": 0.9850560398505604,
158
+ "support": 800.0
159
+ },
160
+ "accuracy": 0.9410096153846154,
161
+ "macro avg": {
162
+ "precision": 0.9410368770369255,
163
+ "recall": 0.9410096153846154,
164
+ "f1-score": 0.9408649532424541,
165
+ "support": 20800.0
166
+ },
167
+ "weighted avg": {
168
+ "precision": 0.9410368770369254,
169
+ "recall": 0.9410096153846154,
170
+ "f1-score": 0.940864953242454,
171
+ "support": 20800.0
172
+ }
173
+ }
174
+ }
outputs/checkpoints/last.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a188738c2f9a4d233e698436ddad1d04467b67564754abc18f84504ea77bb0c4
3
+ size 505956
outputs/checkpoints/summary.json ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "device": "cuda",
3
+ "params": 37658,
4
+ "train_size": 124800,
5
+ "val_size": 20800,
6
+ "best_val_accuracy": 0.9410096153846154,
7
+ "final_metrics": {
8
+ "accuracy": 0.9410096153846154,
9
+ "report": {
10
+ "A": {
11
+ "precision": 0.9084507042253521,
12
+ "recall": 0.9675,
13
+ "f1-score": 0.937046004842615,
14
+ "support": 800.0
15
+ },
16
+ "B": {
17
+ "precision": 0.9810366624525917,
18
+ "recall": 0.97,
19
+ "f1-score": 0.9754871150219987,
20
+ "support": 800.0
21
+ },
22
+ "C": {
23
+ "precision": 0.9752168525402726,
24
+ "recall": 0.98375,
25
+ "f1-score": 0.9794648413192284,
26
+ "support": 800.0
27
+ },
28
+ "D": {
29
+ "precision": 0.9523809523809523,
30
+ "recall": 0.95,
31
+ "f1-score": 0.951188986232791,
32
+ "support": 800.0
33
+ },
34
+ "E": {
35
+ "precision": 0.9774153074027604,
36
+ "recall": 0.97375,
37
+ "f1-score": 0.9755792110206637,
38
+ "support": 800.0
39
+ },
40
+ "F": {
41
+ "precision": 0.9758269720101781,
42
+ "recall": 0.95875,
43
+ "f1-score": 0.9672131147540983,
44
+ "support": 800.0
45
+ },
46
+ "G": {
47
+ "precision": 0.8993103448275862,
48
+ "recall": 0.815,
49
+ "f1-score": 0.8550819672131148,
50
+ "support": 800.0
51
+ },
52
+ "H": {
53
+ "precision": 0.957286432160804,
54
+ "recall": 0.9525,
55
+ "f1-score": 0.9548872180451128,
56
+ "support": 800.0
57
+ },
58
+ "I": {
59
+ "precision": 0.7487437185929648,
60
+ "recall": 0.745,
61
+ "f1-score": 0.7468671679197995,
62
+ "support": 800.0
63
+ },
64
+ "J": {
65
+ "precision": 0.9603072983354674,
66
+ "recall": 0.9375,
67
+ "f1-score": 0.9487666034155597,
68
+ "support": 800.0
69
+ },
70
+ "K": {
71
+ "precision": 0.9822109275730623,
72
+ "recall": 0.96625,
73
+ "f1-score": 0.9741650913673598,
74
+ "support": 800.0
75
+ },
76
+ "L": {
77
+ "precision": 0.75,
78
+ "recall": 0.7575,
79
+ "f1-score": 0.753731343283582,
80
+ "support": 800.0
81
+ },
82
+ "M": {
83
+ "precision": 0.9826732673267327,
84
+ "recall": 0.9925,
85
+ "f1-score": 0.9875621890547264,
86
+ "support": 800.0
87
+ },
88
+ "N": {
89
+ "precision": 0.9552238805970149,
90
+ "recall": 0.96,
91
+ "f1-score": 0.9576059850374065,
92
+ "support": 800.0
93
+ },
94
+ "O": {
95
+ "precision": 0.9504830917874396,
96
+ "recall": 0.98375,
97
+ "f1-score": 0.9668304668304668,
98
+ "support": 800.0
99
+ },
100
+ "P": {
101
+ "precision": 0.986284289276808,
102
+ "recall": 0.98875,
103
+ "f1-score": 0.9875156054931336,
104
+ "support": 800.0
105
+ },
106
+ "Q": {
107
+ "precision": 0.8583850931677018,
108
+ "recall": 0.86375,
109
+ "f1-score": 0.8610591900311526,
110
+ "support": 800.0
111
+ },
112
+ "R": {
113
+ "precision": 0.9613466334164589,
114
+ "recall": 0.96375,
115
+ "f1-score": 0.9625468164794008,
116
+ "support": 800.0
117
+ },
118
+ "S": {
119
+ "precision": 0.9777227722772277,
120
+ "recall": 0.9875,
121
+ "f1-score": 0.9825870646766169,
122
+ "support": 800.0
123
+ },
124
+ "T": {
125
+ "precision": 0.9489671931956257,
126
+ "recall": 0.97625,
127
+ "f1-score": 0.9624152803450401,
128
+ "support": 800.0
129
+ },
130
+ "U": {
131
+ "precision": 0.9527458492975734,
132
+ "recall": 0.9325,
133
+ "f1-score": 0.9425142135186355,
134
+ "support": 800.0
135
+ },
136
+ "V": {
137
+ "precision": 0.9334155363748459,
138
+ "recall": 0.94625,
139
+ "f1-score": 0.9397889509621353,
140
+ "support": 800.0
141
+ },
142
+ "W": {
143
+ "precision": 0.9837092731829574,
144
+ "recall": 0.98125,
145
+ "f1-score": 0.9824780976220275,
146
+ "support": 800.0
147
+ },
148
+ "X": {
149
+ "precision": 0.9785353535353535,
150
+ "recall": 0.96875,
151
+ "f1-score": 0.9736180904522613,
152
+ "support": 800.0
153
+ },
154
+ "Y": {
155
+ "precision": 0.9478908188585607,
156
+ "recall": 0.955,
157
+ "f1-score": 0.9514321295143213,
158
+ "support": 800.0
159
+ },
160
+ "Z": {
161
+ "precision": 0.9813895781637717,
162
+ "recall": 0.98875,
163
+ "f1-score": 0.9850560398505604,
164
+ "support": 800.0
165
+ },
166
+ "accuracy": 0.9410096153846154,
167
+ "macro avg": {
168
+ "precision": 0.9410368770369255,
169
+ "recall": 0.9410096153846154,
170
+ "f1-score": 0.9408649532424541,
171
+ "support": 20800.0
172
+ },
173
+ "weighted avg": {
174
+ "precision": 0.9410368770369254,
175
+ "recall": 0.9410096153846154,
176
+ "f1-score": 0.940864953242454,
177
+ "support": 20800.0
178
+ }
179
+ }
180
+ }
181
+ }
outputs/checkpoints/train_log.csv ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ epoch,lr,train_loss,val_loss,val_accuracy
2
+ 1,0.0009972609476841367,1.915840568234686,0.7317645602473398,0.7759615384615385
3
+ 2,0.0009890738003669028,0.7861337430897306,0.42019438016705396,0.8676442307692308
4
+ 3,0.0009755282581475768,0.5793002123226885,0.3414752165809637,0.8886057692307693
5
+ 4,0.0009567727288213003,0.5050285570934171,0.2945807816051855,0.9050961538461538
6
+ 5,0.0009330127018922195,0.4556999812971373,0.2711868357004189,0.9123076923076923
7
+ 6,0.0009045084971874739,0.4235607778928319,0.2438873816735861,0.9197596153846154
8
+ 7,0.0008715724127386972,0.3999694408696206,0.24509387436073002,0.9171634615384615
9
+ 8,0.0008345653031794292,0.38357853461973,0.22920766054857067,0.9245192307692308
10
+ 9,0.0007938926261462366,0.3661592132793587,0.2268367960095042,0.925625
11
+ 10,0.00075,0.3540909972163986,0.22004170343279839,0.9271634615384615
12
+ 11,0.0007033683215379003,0.3418872819327917,0.20877559076449492,0.931826923076923
13
+ 12,0.0006545084971874739,0.3346020869727506,0.20745138592291168,0.9311538461538461
14
+ 13,0.0006039558454088797,0.32655729393123606,0.20885206949783536,0.9297596153846154
15
+ 14,0.0005522642316338269,0.31873070222676775,0.19801199699683888,0.9356730769230769
16
+ 15,0.0005000000000000002,0.31525022774690487,0.19914993425694907,0.9354807692307693
17
+ 16,0.0004477357683661734,0.30843753614997277,0.20442812638811586,0.9340865384615384
18
+ 17,0.00039604415459112036,0.30016807023985465,0.19302551118975006,0.9358173076923076
19
+ 18,0.0003454915028125264,0.29682947185318,0.19384775055208947,0.9359615384615385
20
+ 19,0.00029663167846210003,0.2924738503748276,0.19320982912691628,0.9363461538461538
21
+ 20,0.00025000000000000017,0.29010163604846745,0.1872810366472638,0.9373076923076923
22
+ 21,0.00020610737385376354,0.28513376365919585,0.18672967462505147,0.9389423076923077
23
+ 22,0.00016543469682057108,0.28206356538490196,0.18462900476666486,0.9389903846153846
24
+ 23,0.00012842758726130303,0.27867109957532804,0.18512763553185435,0.9387019230769231
25
+ 24,9.549150281252637e-05,0.2803964728458983,0.1824342363355,0.9398557692307692
26
+ 25,6.698729810778068e-05,0.2762351586315476,0.18212999898667742,0.9410096153846154
27
+ 26,4.322727117869953e-05,0.27648968744229097,0.18111976517773257,0.9395673076923077
28
+ 27,2.447174185242324e-05,0.27589607660154825,0.1811773017071551,0.9402403846153846
29
+ 28,1.092619963309716e-05,0.2695515979020322,0.1805415609170024,0.9400961538461539
30
+ 29,2.7390523158633003e-06,0.2720878096450059,0.18061398978276952,0.9404807692307692
31
+ 30,0.0,0.27351888143991837,0.18064619034028997,0.9403846153846154
outputs/exports/alphabet_model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:861f97c69f6af53f2a87c477d49bbb1ae0669c37ea6c920e8ee6419eb875172c
3
+ size 154087
outputs/exports/alphabet_model.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c43cff1ffafc9cf3b2d36629f3ae76785a978d02d6ee7a16e3663ba698ab41e
3
+ size 158416
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.24
2
+ PyYAML>=6.0
3
+ scikit-learn>=1.4
4
+ torch>=2.2
5
+ torchvision>=0.17
6
+ onnx>=1.16
7
+ onnxruntime>=1.18
8
+ pillow>=10.0
9
+ opencv-python>=4.8
scripts/__init__.py ADDED
File without changes