Our image classification model training services cover ResNet, EfficientNet, and ViT. We often encounter this scenario: a client brings a dataset of 500 images, split into 10 classes, and asks us to train a ResNet-50. It sounds like a typical task, but without the right strategy, the result is 65% accuracy with overfitting. Our experience shows that success hinges on three factors: choosing the right architecture for the data volume, addressing class imbalance, and preparing the model for production. Our approach yields 2x faster convergence compared to standard training. Here’s how we tackle these challenges and what our work includes.
Our team of certified engineers with over 5 years of experience in Computer Vision has completed more than 50 image classification projects, including medical and industrial applications. Timely defect detection using trained models saves clients up to 2 million rubles per year – a concrete saving of $20,000 annually.
Case Study: Casting Defect Classification in Manufacturing
We were approached by an aluminum casting plant. The goal: classify 12 defect types from conveyor camera images. Data: 15,000 images with severe imbalance — one class made up 60% of the sample, rare defects had fewer than 100 examples. We chose EfficientNet-B4 pretrained on ImageNet, applied Focal Loss (gamma=2.0), and aggressive augmentation (RandomErasing, CutMix). Macro F1 improved from 0.32 to 0.89 — a 2.8x improvement. Inference time on CPU with ONNX: 8 ms per image. The plant deployed the model in their quality control line, reducing manual inspection costs by 1.5 million rubles annually.
How to Choose an Architecture for Your Dataset
| Dataset Size | Recommendation | Why |
|---|---|---|
| <500 examples/class | EfficientNet-B0/B2 (frozen → partial unfreeze) | Fewer parameters reduce overfitting |
| 500–5000/class | EfficientNet-B4, ConvNeXt-T, ResNet-50 | Balance between accuracy and training speed |
| >5000/class | ViT-B/16, ConvNeXt-S/B | Transformers excel with large data |
| Medical, small data | ResNet-50 with MedNet pretraining | Domain-specific pretraining over raw architecture |
| Edge / mobile | MobileNetV3-Large, EfficientNet-Lite | Latency under 10ms on a phone |
EfficientNet-B0 is 8x faster and 4x smaller than ResNet-50 while achieving comparable accuracy, making it ideal for small datasets.
Step-by-Step Model Training Process
- Dataset analysis — evaluate annotation quality, compute class distribution, identify noise.
- Strategy selection — choose architecture, augmentations, and loss function (e.g., Focal Loss for imbalance).
- Experiments — run a series of training sessions with different configs on Weights & Biases.
- Fine-tuning — two-phase training for ViT: train only the head first, then gradually unfreeze blocks.
- Testing — evaluate on validation with TTA, export to ONNX, and measure latency.
- Model delivery — documentation, inference scripts, containerization (Docker) for easy integration.
Two-Phase Training with ViT on Small Datasets
ViT trained on 300 examples without a proper strategy yields 65% accuracy, while EfficientNet scores 84%. The reason: attention heads overfit faster on small datasets than the convolutional inductive bias in ResNet. The solution: aggressive freezing + gradual unfreezing:
import timm
import torch
import torch.nn as nn
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
def train_vit_two_phase(
num_classes: int,
train_loader,
val_loader,
phase1_epochs: int = 20,
phase2_epochs: int = 60,
device: str = 'cuda'
) -> nn.Module:
model = timm.create_model(
'vit_base_patch16_224',
pretrained=True,
num_classes=num_classes,
drop_rate=0.1,
drop_path_rate=0.1
).to(device)
# PHASE 1: train only the head
for name, param in model.named_parameters():
param.requires_grad = 'head' in name
opt1 = AdamW(
filter(lambda p: p.requires_grad, model.parameters()),
lr=1e-3, weight_decay=0.05
)
sched1 = CosineAnnealingLR(opt1, T_max=phase1_epochs, eta_min=1e-5)
for epoch in range(phase1_epochs):
_train_epoch(model, train_loader, opt1, device)
sched1.step()
# PHASE 2: unfreeze the last 6 blocks (out of 12)
for name, param in model.named_parameters():
if any(f'blocks.{i}.' in name for i in range(6, 12)):
param.requires_grad = True
opt2 = AdamW([
{'params': model.head.parameters(), 'lr': 5e-5},
{'params': [p for n, p in model.named_parameters()
if 'blocks' in n and p.requires_grad], 'lr': 5e-6}
], weight_decay=0.05)
sched2 = CosineAnnealingLR(opt2, T_max=phase2_epochs, eta_min=1e-7)
for epoch in range(phase2_epochs):
_train_epoch(model, train_loader, opt2, device)
_validate(model, val_loader, device)
sched2.step()
return model
def _train_epoch(model, loader, optimizer, device):
model.train()
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(images), labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
How to Handle Class Imbalance?
94% accuracy with a 1:200 imbalance usually means the model predicts only the majority class. Metrics for imbalance: macro F1, balanced accuracy, per-class recall. Lin et al. proposed Focal Loss, which we adapt to the task.
import torch
import torch.nn.functional as F
class FocalLoss(nn.Module):
def __init__(self, alpha: float = 1.0, gamma: float = 2.0,
class_weights: torch.Tensor = None):
super().__init__()
self.alpha = alpha
self.gamma = gamma
self.class_weights = class_weights
def forward(self, inputs: torch.Tensor,
targets: torch.Tensor) -> torch.Tensor:
ce_loss = F.cross_entropy(
inputs, targets, weight=self.class_weights, reduction='none'
)
pt = torch.exp(-ce_loss)
focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss
return focal_loss.mean()
# Class weights inversely proportional to frequency
class_counts = torch.tensor([10000, 500, 80], dtype=torch.float)
class_weights = (1.0 / class_counts).to(device)
criterion = FocalLoss(gamma=2.0, class_weights=class_weights)
How to Speed Up Inference Without Losing Accuracy?
Test Time Augmentation (TTA) is a simple way to boost accuracy by 1–3% without retraining: run several augmented versions of an image and average the predictions.
import torchvision.transforms as T
class TTAClassifier:
def __init__(self, model: nn.Module, n_augments: int = 5):
self.model = model.eval()
self.n_augments = n_augments
self.base_transform = T.Compose([
T.Resize(256), T.CenterCrop(224),
T.ToTensor(),
T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
])
self.tta_transforms = [
T.Compose([T.Resize(256), T.CenterCrop(224),
T.RandomHorizontalFlip(p=1.0),
T.ToTensor(),
T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),
T.Compose([T.Resize(224), T.ToTensor(),
T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]),
]
@torch.no_grad()
def predict(self, image) -> torch.Tensor:
preds = [
torch.softmax(self.model(self.base_transform(image).unsqueeze(0)), dim=1)
]
for transform in self.tta_transforms:
preds.append(
torch.softmax(self.model(transform(image).unsqueeze(0)), dim=1)
)
return torch.stack(preds).mean(dim=0)
Export for Production
# ONNX export — for CPU/TensorRT deployment
dummy = torch.randn(1, 3, 224, 224, device='cpu')
torch.onnx.export(
model.cpu().eval(),
dummy,
'classifier.onnx',
opset_version=17,
input_names=['image'],
output_names=['logits'],
dynamic_axes={'image': {0: 'batch'}, 'logits': {0: 'batch'}}
)
Using ONNX can reduce GPU costs by up to 40% due to optimized CPU inference.
What’s Included in Our Work
- Dataset analysis: quality assessment, imbalance, augmentation selection.
- Architecture and training strategy tailored to your data.
- Pipeline implementation in PyTorch / Hugging Face.
- Hyperparameter tuning, metric logging (Weights & Biases).
- Testing with TTA and export to ONNX / TensorRT.
- Documentation, model files, and inference scripts.
- Access to model, training scripts, and 30-day support.
- Consultation for integration by your developers.
Typical project investment ranges from $5,000 to $50,000, depending on complexity.
Timelines
| Task | Timeline |
|---|---|
| Fine-tuning a ready architecture (data prepared) | 1–3 weeks |
| Training from scratch + augmentations + optimization | 4–7 weeks |
| Developing a custom architecture for a domain | 8–14 weeks |
Want a similar model for your project? Contact us to discuss your dataset and find the optimal solution. Order model training and get a consultation with a precise timeline estimate.







