ligo-glitch-vit-cnn / src /inference.py
JesseWeigel's picture
Initial release: ViT-B/16 and ResNet-50v2 for Gravity Spy glitch classification
0aa115d verified
Raw
History Blame Contribute Delete
6.25 kB
#!/usr/bin/env python3
"""Standalone inference script for Gravity Spy glitch classification.
Classifies LIGO glitch spectrograms using either a ViT-B/16 or ResNet-50v2 BiT
model trained on O3 Gravity Spy data (23 classes).
Usage:
python inference.py --model vit --image path/to/spectrogram.png
python inference.py --model cnn --image path/to/spectrogram.png --top-k 5
Requirements: torch, timm, albumentations, numpy, Pillow
No training code imports required.
"""
# ASSERT_CONVENTION: primary_metric=macro_f1, input_format=224x224_RGB_PNG_0to1
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import torch
import timm
from preprocessing import load_image
def load_model(model_key, config_path=None, checkpoint_dir=None):
"""Load a model from config and checkpoint.
Parameters
----------
model_key : str
Either 'vit' or 'cnn'.
config_path : Path or None
Path to model_config.json. Defaults to same directory as this script.
checkpoint_dir : Path or None
Path to checkpoints directory. Defaults to ../checkpoints/ relative to this script.
Returns
-------
model : torch.nn.Module
Loaded model in mode for inference.
class_labels : list of str
Ordered class label names (index -> label).
"""
script_dir = Path(__file__).resolve().parent
if config_path is None:
config_path = script_dir / "model_config.json"
if checkpoint_dir is None:
checkpoint_dir = script_dir.parent / "checkpoints"
with open(config_path) as f:
config = json.load(f)
if model_key not in config["models"]:
available = list(config["models"].keys())
raise ValueError(f"Unknown model key '{model_key}'. Choose from: {available}")
model_cfg = config["models"][model_key]
# Build architecture via timm (no pretrained weights -- we load our own)
model = timm.create_model(
model_cfg["model_name"],
pretrained=False,
num_classes=model_cfg["num_classes"],
)
# Load trained weights
checkpoint_path = checkpoint_dir / model_cfg["checkpoint_file"]
if not checkpoint_path.exists():
raise FileNotFoundError(
f"Checkpoint not found: {checkpoint_path}\n"
f"Expected file: {model_cfg['checkpoint_file']}"
)
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
# Handle both full training checkpoints (with 'model_state_dict' key)
# and bare state_dict files
if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
state_dict = checkpoint["model_state_dict"]
else:
state_dict = checkpoint
model.load_state_dict(state_dict)
# Set to inference mode
model.requires_grad_(False)
model.eval()
# Load class labels
labels_path = script_dir / "class_labels.json"
with open(labels_path) as f:
class_labels = json.load(f)
return model, class_labels
def predict(model, image_tensor, class_labels, top_k=3):
"""Run inference on a preprocessed image tensor.
Parameters
----------
model : torch.nn.Module
Model in inference mode.
image_tensor : torch.Tensor
Preprocessed image of shape (3, 224, 224).
class_labels : list of str
Ordered class label names.
top_k : int
Number of top predictions to return.
Returns
-------
predictions : list of dict
Top-k predictions, each with 'rank', 'class', 'probability'.
"""
with torch.no_grad():
logits = model(image_tensor.unsqueeze(0)) # (1, num_classes)
probs = torch.softmax(logits, dim=1).squeeze(0) # (num_classes,)
top_k = min(top_k, len(class_labels))
top_probs, top_indices = torch.topk(probs, top_k)
predictions = []
for rank, (prob, idx) in enumerate(zip(top_probs, top_indices), 1):
predictions.append({
"rank": rank,
"class": class_labels[idx.item()],
"probability": round(prob.item(), 6),
})
return predictions
def main():
parser = argparse.ArgumentParser(
description="Gravity Spy glitch classification inference",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python inference.py --model vit --image spectrogram.png\n"
" python inference.py --model cnn --image spectrogram.png --top-k 5\n"
" python inference.py --model vit --image spectrogram.png --json\n"
),
)
parser.add_argument("--model", required=True, choices=["vit", "cnn"],
help="Model to use: vit (ViT-B/16) or cnn (ResNet-50v2 BiT)")
parser.add_argument("--image", required=True, type=str,
help="Path to spectrogram image (PNG/JPG)")
parser.add_argument("--top-k", type=int, default=3,
help="Number of top predictions to show (default: 3)")
parser.add_argument("--json", action="store_true",
help="Output predictions as JSON")
args = parser.parse_args()
# Validate image path
image_path = Path(args.image)
if not image_path.exists():
print(f"Error: Image not found: {image_path}", file=sys.stderr)
sys.exit(1)
# Load model
model, class_labels = load_model(args.model)
# Preprocess image
image_tensor = load_image(str(image_path))
# Run inference
predictions = predict(model, image_tensor, class_labels, top_k=args.top_k)
# Output results
if args.json:
result = {
"model": args.model,
"image": str(image_path),
"predictions": predictions,
}
print(json.dumps(result, indent=2))
else:
model_name = "ViT-B/16" if args.model == "vit" else "ResNet-50v2 BiT"
print(f"\nModel: {model_name}")
print(f"Image: {image_path}")
print(f"\nPredictions:")
print(f"{'Rank':<6} {'Class':<25} {'Probability':<12}")
print("-" * 43)
for pred in predictions:
print(f"{pred['rank']:<6} {pred['class']:<25} {pred['probability']:.4f}")
print()
if __name__ == "__main__":
main()