Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- alphabet/__init__.py +0 -0
- alphabet/__pycache__/__init__.cpython-314.pyc +0 -0
- alphabet/__pycache__/dataset.cpython-314.pyc +0 -0
- alphabet/__pycache__/export.cpython-314.pyc +0 -0
- alphabet/__pycache__/model.cpython-314.pyc +0 -0
- alphabet/__pycache__/train.cpython-314.pyc +0 -0
- alphabet/__pycache__/utils.cpython-314.pyc +0 -0
- alphabet/dataset.py +138 -0
- alphabet/export.py +107 -0
- alphabet/infer.py +82 -0
- alphabet/model.py +93 -0
- alphabet/train.py +224 -0
- alphabet/utils.py +67 -0
- config/config.yaml +33 -0
- main.py +80 -0
- outputs/checkpoints/best.pt +3 -0
- outputs/checkpoints/best_metrics.json +174 -0
- outputs/checkpoints/final_metrics.json +174 -0
- outputs/checkpoints/last.pt +3 -0
- outputs/checkpoints/summary.json +181 -0
- outputs/checkpoints/train_log.csv +31 -0
- outputs/exports/alphabet_model.onnx +3 -0
- outputs/exports/alphabet_model.ts +3 -0
- requirements.txt +9 -0
- scripts/__init__.py +0 -0
.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
|