Custom Face Recognition Model Training: From Dataset to Production
Imagine: your company is growing, new employees arrive daily, and key-card access is insecure. You decide to implement face recognition. But a standard Softmax classifier won't cut it — it requires retraining on every new employee. The solution is training a custom model with ArcFace loss, which can generalize to unseen identities. Our team — AI engineers with 5+ years of experience in computer vision, completed 30+ face recognition projects. We guarantee accuracy of 99.5% on LFW.
ArcFace is the industry standard: 99.5% accuracy on LFW, compact embeddings, noise robustness. It outperforms Softmax by up to 10% in open-set tasks, especially when new identities appear after deployment. Below we describe the training setup: backbone selection, margin tuning, dataset processing.
ArcFace Loss: Math and Implementation
ArcFace adds an additive angular margin m to the angle between the embedding and the corresponding class center:
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class ArcFaceLoss(nn.Module):
def __init__(
self,
embedding_size: int = 512,
num_classes: int = 10000,
margin: float = 0.5, # angular margin in radians (~28.6°)
scale: float = 64.0 # logit scale
):
super().__init__()
self.margin = margin
self.scale = scale
# Trainable class centers (normalized)
self.weight = nn.Parameter(
torch.FloatTensor(num_classes, embedding_size)
)
nn.init.xavier_uniform_(self.weight)
self.cos_m = math.cos(margin)
self.sin_m = math.sin(margin)
self.th = math.cos(math.pi - margin) # threshold for numerical stability
self.mm = math.sin(math.pi - margin) * margin
def forward(
self,
embeddings: torch.Tensor, # (B, embedding_size), L2-normalized
labels: torch.Tensor # (B,)
) -> torch.Tensor:
# L2-normalize weights
W = F.normalize(self.weight, dim=1)
# cos(θ) = emb · W^T
cosine = F.linear(embeddings, W) # (B, num_classes)
sine = torch.sqrt(1.0 - cosine.pow(2).clamp(0, 1))
# cos(θ + m) = cos(θ)cos(m) - sin(θ)sin(m)
phi = cosine * self.cos_m - sine * self.sin_m
# Numerical stability: if θ > π - m, use cosine penalty
phi = torch.where(cosine > self.th, phi, cosine - self.mm)
# One-hot target mask
one_hot = torch.zeros_like(cosine)
one_hot.scatter_(1, labels.view(-1, 1), 1)
# Replace logit only for the correct class
output = one_hot * phi + (1.0 - one_hot) * cosine
output *= self.scale
return F.cross_entropy(output, labels)
Backbone and Embedding: Which to Choose?
InsightFace / ArcFace typically uses ResNet-50/100 or IResNet. For production on mobile devices — MobileFaceNet:
import timm
def build_face_recognition_model(
backbone: str = 'resnet50', # 'resnet100', 'mobilenetv3_small'
embedding_size: int = 512,
pretrained: bool = True
) -> nn.Module:
class FaceEmbedder(nn.Module):
def __init__(self):
super().__init__()
self.backbone = timm.create_model(
backbone,
pretrained=pretrained,
num_classes=0, # remove classifier head
global_pool='avg'
)
feat_dim = self.backbone.num_features
self.bn = nn.BatchNorm1d(feat_dim)
self.drop = nn.Dropout(p=0.4)
self.fc = nn.Linear(feat_dim, embedding_size, bias=False)
self.bn2 = nn.BatchNorm1d(embedding_size)
def forward(self, x: torch.Tensor) -> torch.Tensor:
feat = self.backbone(x)
feat = self.bn(feat)
feat = self.drop(feat)
emb = self.fc(feat)
emb = self.bn2(emb)
return F.normalize(emb, dim=1) # L2-normalization
return FaceEmbedder()
Threshold Selection for Open-Set Recognition
In production, the system encounters new people not seen in training. We use cosine similarity threshold:
import numpy as np
from scipy.spatial.distance import cosine
class FaceRecognitionSystem:
def __init__(
self,
model: nn.Module,
threshold: float = 0.4 # cosine distance; tuned via ROC
):
self.model = model.eval()
self.threshold = threshold
self.gallery: dict[str, np.ndarray] = {} # id → embedding
def enroll(self, person_id: str, face_image: torch.Tensor) -> None:
"""Register a new face in the gallery"""
with torch.no_grad():
emb = self.model(face_image.unsqueeze(0))
self.gallery[person_id] = emb.cpu().numpy().squeeze()
def identify(
self,
face_image: torch.Tensor,
top_k: int = 1
) -> list[dict]:
"""Search gallery — 1:N identification"""
with torch.no_grad():
query_emb = self.model(face_image.unsqueeze(0))
query_np = query_emb.cpu().numpy().squeeze()
distances = {
person_id: cosine(query_np, gallery_emb)
for person_id, gallery_emb in self.gallery.items()
}
sorted_matches = sorted(distances.items(), key=lambda x: x[1])
results = []
for person_id, dist in sorted_matches[:top_k]:
results.append({
'identity': person_id if dist < self.threshold else 'unknown',
'distance': float(dist),
'confidence': float(1 - dist)
})
return results
The cosine distance threshold is tuned via ROC curve on your test set. Optimal threshold is 0.35–0.45 for most enterprise scenarios. We use TAR@FAR=0.1% as the target metric.
Why ArcFace is the Industry Standard?
ArcFace produces compact clusters without the complex triplet mining of FaceNet. It maintains accuracy even on noisy datasets. More details on the loss can be found in the ArcFace paper.
Metrics and Comparison of Loss Functions
| Metric | Value | Application |
|---|---|---|
| TAR@FAR=0.1% | 98.5%+ | Phone unlock |
| TAR@FAR=0.01% | 95%+ | Physical access |
| TAR@FAR=0.001% | 90%+ | Forensics |
| 1:1 Verification AUC | > 0.998 | Document verification |
Loss function comparison:
| Loss | LFW Acc | IJB-C TAR@FAR=0.1% | Complexity | Application |
|---|---|---|---|---|
| Softmax | 98.8% | 91.3% | Low | Closed set |
| CosFace | 99.3% | 94.1% | Low | Standard |
| ArcFace | 99.5% | 95.6% | Low | Standard |
| AdaFace | 99.6% | 96.8% | Medium | Low quality photos |
| ElasticFace | 99.6% | 96.4% | Medium | General |
Process and Timeline
We don't just train a model — we build a complete solution. The process includes:
- Dataset analysis: quality assessment, quantity, recommendations.
- Data preparation: face alignment, augmentation (flip, rotation, blur), train/val/test split.
- Backbone and loss selection: from MobileNet to ResNet-100 based on your dataset and hardware.
- Training: balanced sampling, monitoring via Weights & Biases.
- Validation: TAR@FAR on your test set, ROC analysis.
- Quantization and export: INT8/FP16 for edge, ONNX for CPU.
- Deployment: Docker + Triton/ONNX, REST/gRPC API, documentation.
| Stage | Result |
|---|---|
| Dataset analysis | Report: quality, quantity, recommendations |
| Data preparation | Face alignment, augmentation, split |
| Model training | Loss selection, backbone, hyperparams |
| Validation | TAR@FAR on your test set |
| Deployment | Docker + Triton/ONNX, REST API |
| Documentation | API docs, operation guide |
| Support | 3 months warranty maintenance |
What's Included in the Work?
The deliverable includes:
- Trained model with chosen backbone and loss (ArcFace by default).
- API documentation, deployment instructions.
- Docker image with model and testing scripts.
- 3 months warranty support and updates.
Timelines:
- Fine-tuning ArcFace on corporate data — 3–5 weeks.
- Full 1:N system with gallery — 5–8 weeks.
- Custom pipeline (detection + alignment + recognition) — 8–14 weeks.
Cost is calculated based on your dataset and requirements. For example, fine-tuning a model on a dataset of 10,000 identities starts at $5,000. A full system including detection and gallery is typically under $20,000. Contact us to discuss the details.
Typical Mistakes When Training Face Recognition Models
- Using Softmax for open-set tasks — accuracy drops by 5–10%.
- Not applying L2 normalization to embeddings — metric learning does not converge.
- Forgetting face alignment — accuracy drops by 3–5%.
- Setting the threshold too low — avalanche of false positives.
If you want a consultation on your dataset, contact us. Order custom face recognition model training — we'll evaluate the project within 1 day.
Pricing: Fine-tuning starts from $5,000, full 1:N system from $15,000. Get a free estimate within 24 hours.







