| |
| """ |
| SE-AlexNet: Unified Inference Script |
| ===================================== |
| |
| Minimal inference demo for loading any SE-AlexNet variant and running a forward pass. |
| Copy-paste ready. Under 20 lines of core logic. |
| |
| Usage: |
| >>> from inference import SEModelPipeline |
| >>> pipe = SEModelPipeline('se-location3/facebased/squeeze-32') |
| >>> probs = pipe.predict('path/to/face_image.jpg') # β torch.Tensor, shape (1, num_classes) |
| |
| CLI: |
| python inference.py se-location3/facebased/squeeze-32 --image face.jpg |
| """ |
|
|
| import os |
| import sys |
| import json |
| import torch |
| import torch.nn.functional as F |
| from torchvision import transforms |
| from PIL import Image |
| from typing import Union, Optional |
|
|
| |
| from modeling import ( |
| AlexNet, |
| AlexNetWithSE_L1, |
| AlexNetWithSE_L2, |
| AlexNetWithSE_L3, |
| VGG16, |
| MODEL_REGISTRY, |
| load_model_from_config, |
| ) |
|
|
|
|
| |
| |
| INPUT_SIZE = 224 |
|
|
| _preprocess = transforms.Compose([ |
| transforms.Resize((INPUT_SIZE, INPUT_SIZE)), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], |
| std=[0.229, 0.224, 0.225]), |
| ]) |
|
|
|
|
| |
| class SEModelPipeline: |
| """ |
| Minimal pipeline to load and run inference with any SE-AlexNet variant. |
| |
| Parameters |
| ---------- |
| model_dir : str |
| Path relative to the repo root, e.g. 'se-location3/facebased/squeeze-32'. |
| device : str |
| Torch device, defaults to 'cpu'. |
| """ |
| def __init__(self, model_dir: str, device: str = 'cpu'): |
| |
| repo_root = os.path.dirname(os.path.abspath(__file__)) |
| full_dir = os.path.join(repo_root, model_dir) |
|
|
| |
| config_path = os.path.join(full_dir, 'config.json') |
| if not os.path.exists(config_path): |
| raise FileNotFoundError(f'config.json not found in {full_dir}') |
| with open(config_path, 'r') as f: |
| self.config = json.load(f) |
|
|
| |
| safetensors_path = os.path.join(full_dir, 'model.safetensors') |
| pth_path = os.path.join(full_dir, 'model.pth') |
|
|
| weights_path = None |
| if os.path.exists(safetensors_path): |
| weights_path = safetensors_path |
| elif os.path.exists(pth_path): |
| weights_path = pth_path |
| else: |
| raise FileNotFoundError( |
| f'No weights found (model.safetensors or model.pth) in {full_dir}' |
| ) |
|
|
| |
| self.device = device |
| self.model = load_model_from_config(self.config, weights_path, device) |
| self.model.eval() |
|
|
| |
| self.num_classes = self.config['num_classes'] |
| self.model_type = self.config['model_type'] |
| self.pretraining = self.config['pretraining'] |
|
|
| print(f'Loaded {self.model_type} | {self.pretraining} | ' |
| f'{self.num_classes} classes β {device}') |
|
|
| def predict(self, image: Union[str, Image.Image, torch.Tensor]) -> torch.Tensor: |
| """ |
| Run inference on a single image. |
| |
| Args: |
| image: Path to image, PIL Image, or preprocessed tensor (3Γ224Γ224). |
| |
| Returns: |
| torch.Tensor of shape (1, num_classes) with class probabilities. |
| """ |
| if isinstance(image, str): |
| image = Image.open(image).convert('RGB') |
| if isinstance(image, Image.Image): |
| image = _preprocess(image) |
| |
| if image.dim() == 3: |
| image = image.unsqueeze(0) |
|
|
| image = image.to(self.device) |
|
|
| with torch.no_grad(): |
| logits = self.model(image) |
| probs = F.softmax(logits, dim=1) |
|
|
| return probs |
|
|
| def predict_topk(self, image: Union[str, Image.Image, torch.Tensor], |
| k: int = 3): |
| """Return top-k class indices and probabilities.""" |
| probs = self.predict(image) |
| topk_probs, topk_indices = torch.topk(probs, k, dim=1) |
| return topk_indices[0].cpu().numpy(), topk_probs[0].cpu().numpy() |
|
|
|
|
| |
| if __name__ == '__main__': |
| import argparse |
|
|
| parser = argparse.ArgumentParser( |
| description='SE-AlexNet Inference β forward pass on a single image' |
| ) |
| parser.add_argument('model_dir', |
| help='Model directory (e.g. se-location3/facebased/squeeze-32)') |
| parser.add_argument('--image', '-i', required=True, |
| help='Path to input image') |
| parser.add_argument('--device', default='cpu', |
| help='Device (cpu, cuda, mps)') |
| parser.add_argument('--topk', type=int, default=3, |
| help='Show top-k predictions') |
|
|
| args = parser.parse_args() |
|
|
| pipe = SEModelPipeline(args.model_dir, device=args.device) |
| indices, probs = pipe.predict_topk(args.image, k=args.topk) |
|
|
| print(f'\nTop-{args.topk} predictions:') |
| for idx, prob in zip(indices, probs): |
| print(f' Class {idx}: {prob:.4f} ({prob*100:.1f}%)') |
|
|