Spaces:
Runtime error
Runtime error
Download src/model/train.operator.py from handwriting-ocr-api/handwriting-ocr: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/handwriting-ocr-api/handwriting-ocr/resolve/main/src/model/train.operator.py
- Command line
-
hf download hf://spaces/handwriting-ocr-api/handwriting-ocr/src/model/train.operator.py
-
curl -L -o train.operator.py https://huggingface.co/spaces/handwriting-ocr-api/handwriting-ocr/resolve/main/src/model/train.operator.py
10.3 kB
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from torch.utils.data import Dataset, DataLoader, random_split | |
| import os | |
| from PIL import Image | |
| import numpy as np | |
| from typing import Optional, List, Tuple | |
| import logging | |
| import multiprocessing | |
| from torch.optim.lr_scheduler import OneCycleLR | |
| from tqdm import tqdm | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s' | |
| ) | |
| def is_valid_image(file_path: str) -> bool: | |
| """Check if the file is a valid image file.""" | |
| return (file_path.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif')) and | |
| not file_path.startswith('.')) | |
| class AttentionModule(nn.Module): | |
| def __init__(self, channels: int): | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(channels, channels // 8, kernel_size=1) | |
| self.conv2 = nn.Conv2d(channels // 8, channels, kernel_size=1) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| attention = F.avg_pool2d(x, 1) | |
| attention = F.relu(self.conv1(attention)) | |
| attention = torch.sigmoid(self.conv2(attention)) | |
| return x * attention | |
| class ResidualBlock(nn.Module): | |
| def __init__(self, in_channels: int, out_channels: int, stride: int = 1): | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, | |
| stride=stride, padding=1, bias=False) | |
| self.bn1 = nn.BatchNorm2d(out_channels) | |
| self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, | |
| stride=1, padding=1, bias=False) | |
| self.bn2 = nn.BatchNorm2d(out_channels) | |
| self.shortcut = nn.Sequential() | |
| if stride != 1 or in_channels != out_channels: | |
| self.shortcut = nn.Sequential( | |
| nn.Conv2d(in_channels, out_channels, kernel_size=1, | |
| stride=stride, bias=False), | |
| nn.BatchNorm2d(out_channels) | |
| ) | |
| self.attention = AttentionModule(out_channels) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| out = F.relu(self.bn1(self.conv1(x))) | |
| out = self.bn2(self.conv2(out)) | |
| out = self.attention(out) | |
| out += self.shortcut(x) | |
| out = F.relu(out) | |
| return out | |
| class OptimizedOCR(nn.Module): | |
| def __init__(self, num_classes: int = 36): | |
| super().__init__() | |
| self.in_channels = 64 | |
| # Initial convolution | |
| self.conv1 = nn.Conv2d(1, 64, kernel_size=3, stride=1, padding=1, bias=False) | |
| self.bn1 = nn.BatchNorm2d(64) | |
| # Residual layers | |
| self.layer1 = self.make_layer(64, stride=1, num_blocks=2) | |
| self.layer2 = self.make_layer(128, stride=2, num_blocks=2) | |
| self.layer3 = self.make_layer(256, stride=2, num_blocks=2) | |
| # Output layers | |
| self.avg_pool = nn.AdaptiveAvgPool2d((1, 1)) | |
| self.fc = nn.Sequential( | |
| nn.Linear(256, 512), | |
| nn.ReLU(), | |
| nn.Dropout(0.5), | |
| nn.Linear(512, num_classes) | |
| ) | |
| def make_layer(self, out_channels: int, stride: int, num_blocks: int) -> nn.Sequential: | |
| strides = [stride] + [1]*(num_blocks-1) | |
| layers = [] | |
| for stride in strides: | |
| layers.append(ResidualBlock(self.in_channels, out_channels, stride)) | |
| self.in_channels = out_channels | |
| return nn.Sequential(*layers) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| out = F.relu(self.bn1(self.conv1(x))) | |
| out = self.layer1(out) | |
| out = self.layer2(out) | |
| out = self.layer3(out) | |
| out = self.avg_pool(out) | |
| out = out.view(out.size(0), -1) | |
| out = self.fc(out) | |
| return out | |
| class OCRDataset(Dataset): | |
| def __init__(self, data_dir: str, max_samples: Optional[int] = None): | |
| self.samples = [] | |
| self.labels = [] | |
| self.label_to_idx = {} | |
| categories = ['digits', 'uppercase'] | |
| for category in categories: | |
| category_path = os.path.join(data_dir, category) | |
| if not os.path.exists(category_path): | |
| continue | |
| for char_folder in sorted(os.listdir(category_path)): | |
| char_path = os.path.join(category_path, char_folder) | |
| if not os.path.isdir(char_path): | |
| continue | |
| if char_folder not in self.label_to_idx: | |
| self.label_to_idx[char_folder] = len(self.label_to_idx) | |
| images_path = os.path.join(char_path, 'images') | |
| image_files = [f for f in sorted(os.listdir(images_path)) | |
| if is_valid_image(f)] | |
| if max_samples: | |
| image_files = image_files[:max_samples] | |
| for img_file in image_files: | |
| self.samples.append(os.path.join(images_path, img_file)) | |
| self.labels.append(self.label_to_idx[char_folder]) | |
| logging.info(f"Loaded {len(self.samples)} samples across {len(self.label_to_idx)} classes") | |
| def __len__(self) -> int: | |
| return len(self.samples) | |
| def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]: | |
| try: | |
| img_path = self.samples[idx] | |
| image = Image.open(img_path).convert('L') | |
| image = torch.FloatTensor(np.array(image)).unsqueeze(0) / 255.0 | |
| return image, self.labels[idx] | |
| except Exception as e: | |
| logging.error(f"Error loading image {img_path}: {str(e)}") | |
| raise | |
| def train( | |
| data_dir: str, | |
| max_samples: Optional[int] = None, | |
| batch_size: int = 128, | |
| epochs: int = 50, | |
| initial_lr: float = 0.001, | |
| num_workers: int = 8 | |
| ) -> nn.Module: | |
| # Device configuration - MPS for M2 Ultra | |
| device = torch.device("mps") | |
| torch.backends.cudnn.benchmark = True | |
| # Load and split dataset | |
| dataset = OCRDataset(data_dir, max_samples) | |
| train_size = int(0.8 * len(dataset)) | |
| val_size = len(dataset) - train_size | |
| train_dataset, val_dataset = random_split(dataset, [train_size, val_size]) | |
| train_loader = DataLoader( | |
| train_dataset, | |
| batch_size=batch_size, | |
| shuffle=True, | |
| num_workers=num_workers, | |
| pin_memory=True | |
| ) | |
| val_loader = DataLoader( | |
| val_dataset, | |
| batch_size=batch_size, | |
| shuffle=False, | |
| num_workers=num_workers, | |
| pin_memory=True | |
| ) | |
| # Initialize model and move to device | |
| model = OptimizedOCR(num_classes=len(dataset.label_to_idx)).to(device) | |
| # Loss and optimizer | |
| criterion = nn.CrossEntropyLoss() | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=initial_lr, weight_decay=0.01) | |
| # Learning rate scheduler | |
| scheduler = OneCycleLR( | |
| optimizer, | |
| max_lr=initial_lr, | |
| epochs=epochs, | |
| steps_per_epoch=len(train_loader), | |
| pct_start=0.3, | |
| anneal_strategy='cos' | |
| ) | |
| # Training loop | |
| best_acc = 0.0 | |
| for epoch in range(epochs): | |
| model.train() | |
| train_loss = 0.0 | |
| train_correct = 0 | |
| train_total = 0 | |
| pbar = tqdm(train_loader, desc=f'Epoch {epoch + 1}/{epochs} [Train]') | |
| for inputs, labels in pbar: | |
| inputs, labels = inputs.to(device), labels.to(device) | |
| optimizer.zero_grad(set_to_none=True) | |
| outputs = model(inputs) | |
| loss = criterion(outputs, labels) | |
| loss.backward() | |
| optimizer.step() | |
| scheduler.step() | |
| train_loss += loss.item() | |
| _, predicted = outputs.max(1) | |
| train_total += labels.size(0) | |
| train_correct += predicted.eq(labels).sum().item() | |
| pbar.set_postfix({ | |
| 'loss': train_loss / (pbar.n + 1), | |
| 'acc': 100. * train_correct / train_total | |
| }) | |
| # Validation phase | |
| model.eval() | |
| val_loss = 0.0 | |
| val_correct = 0 | |
| val_total = 0 | |
| with torch.no_grad(): | |
| pbar = tqdm(val_loader, desc=f'Epoch {epoch + 1}/{epochs} [Val]') | |
| for inputs, labels in pbar: | |
| inputs, labels = inputs.to(device), labels.to(device) | |
| outputs = model(inputs) | |
| loss = criterion(outputs, labels) | |
| val_loss += loss.item() | |
| _, predicted = outputs.max(1) | |
| val_total += labels.size(0) | |
| val_correct += predicted.eq(labels).sum().item() | |
| pbar.set_postfix({ | |
| 'loss': val_loss / (pbar.n + 1), | |
| 'acc': 100. * val_correct / val_total | |
| }) | |
| val_acc = 100. * val_correct / val_total | |
| logging.info(f'Epoch {epoch + 1} | ' | |
| f'Train Loss: {train_loss / len(train_loader):.3f} | ' | |
| f'Train Acc: {100. * train_correct / train_total:.2f}% | ' | |
| f'Val Loss: {val_loss / len(val_loader):.3f} | ' | |
| f'Val Acc: {val_acc:.2f}%') | |
| if val_acc > best_acc: | |
| logging.info(f'New best accuracy: {val_acc:.2f}%') | |
| best_acc = val_acc | |
| torch.save(model.state_dict(), 'best_model.pth') | |
| if best_acc >= 99.0: | |
| logging.info(f'Reached target accuracy of 99%!') | |
| break | |
| return model | |
| if __name__ == '__main__': | |
| # Set number of CPU threads for data loading | |
| num_workers = min(8, multiprocessing.cpu_count()) | |
| # Start with smaller dataset | |
| logging.info("Starting initial training with 1000 samples per class...") | |
| model = train( | |
| data_dir='data/processed', | |
| max_samples=1000, | |
| batch_size=128, | |
| epochs=20, | |
| num_workers=num_workers | |
| ) | |
| # If successful, train on full dataset | |
| logging.info("Starting full dataset training...") | |
| model = train( | |
| data_dir='data/processed', | |
| batch_size=128, | |
| epochs=50, | |
| num_workers=num_workers | |
| ) |