"""Backbone builders for scratch face-recognition experiments.""" from __future__ import annotations from collections.abc import Sequence import torch from facenet_pytorch import InceptionResnetV1 from torch import nn class Flatten(nn.Module): def forward(self, x: torch.Tensor) -> torch.Tensor: return x.view(x.size(0), -1) class IRBlock(nn.Module): def __init__(self, in_channels: int, out_channels: int, stride: int): super().__init__() self.residual = nn.Sequential( nn.BatchNorm2d(in_channels), nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False), nn.BatchNorm2d(out_channels), nn.PReLU(out_channels), nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False), nn.BatchNorm2d(out_channels), ) if in_channels == out_channels and stride == 1: self.shortcut = nn.Identity() else: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(out_channels), ) def forward(self, x: torch.Tensor) -> torch.Tensor: return self.residual(x) + self.shortcut(x) class IRResNet(nn.Module): """InsightFace-style IR-ResNet for 112x112 face crops.""" def __init__(self, layers: Sequence[int], embedding_size: int = 512, dropout: float = 0.4): super().__init__() self.input_layer = nn.Sequential( nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False), nn.BatchNorm2d(64), nn.PReLU(64), ) self.body = nn.Sequential( self._make_stage(64, 64, layers[0], stride=2), self._make_stage(64, 128, layers[1], stride=2), self._make_stage(128, 256, layers[2], stride=2), self._make_stage(256, 512, layers[3], stride=2), ) self.output_layer = nn.Sequential( nn.BatchNorm2d(512), nn.Dropout(dropout), Flatten(), nn.Linear(512 * 7 * 7, embedding_size), nn.BatchNorm1d(embedding_size), ) self._init_weights() def _make_stage(self, in_channels: int, out_channels: int, blocks: int, stride: int) -> nn.Sequential: layers = [IRBlock(in_channels, out_channels, stride)] for _ in range(1, blocks): layers.append(IRBlock(out_channels, out_channels, 1)) return nn.Sequential(*layers) def _init_weights(self) -> None: for module in self.modules(): if isinstance(module, nn.Conv2d): nn.init.kaiming_normal_(module.weight, mode="fan_out", nonlinearity="relu") elif isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d)): nn.init.ones_(module.weight) nn.init.zeros_(module.bias) elif isinstance(module, nn.Linear): nn.init.xavier_uniform_(module.weight) nn.init.zeros_(module.bias) def forward(self, x: torch.Tensor) -> torch.Tensor: x = self.input_layer(x) x = self.body(x) return self.output_layer(x) def canonical_backbone_name(name: str) -> str: normalized = name.lower().replace("-", "_") aliases = { "inceptionresnetv1": "inception_resnet_v1", "inception_resnet": "inception_resnet_v1", "inception_resnet_v1": "inception_resnet_v1", "ir18": "ir_resnet18", "ir_resnet18": "ir_resnet18", "ir34": "ir_resnet34", "ir_resnet34": "ir_resnet34", } if normalized not in aliases: raise ValueError(f"Unsupported backbone: {name}") return aliases[normalized] def build_backbone(name: str, pretrained_model: str | None = None) -> tuple[nn.Module, int, str]: canonical = canonical_backbone_name(name) if canonical == "inception_resnet_v1": model = InceptionResnetV1(pretrained=pretrained_model, classify=False) elif canonical == "ir_resnet18": model = IRResNet([2, 2, 2, 2]) elif canonical == "ir_resnet34": model = IRResNet([3, 4, 6, 3]) else: raise ValueError(f"Unsupported backbone: {name}") return model, 512, canonical