JesseWeigel commited on
Commit
0aa115d
·
verified ·
1 Parent(s): 7750088

Initial release: ViT-B/16 and ResNet-50v2 for Gravity Spy glitch classification

Browse files
README.md CHANGED
@@ -1,3 +1,173 @@
1
- ---
2
- license: cc-by-4.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gravity Spy Glitch Classifier: ViT-B/16 and ResNet-50v2 BiT
2
+
3
+ Two deep learning models for classifying LIGO gravitational-wave detector
4
+ glitch morphologies from Q-transform spectrograms, trained on Gravity Spy O3
5
+ data (Zevin et al. 2017, CQG 34 064003).
6
+
7
+ ## Model Overview
8
+
9
+ | Property | ViT-B/16 | ResNet-50v2 BiT |
10
+ |---|---|---|
11
+ | Architecture | Vision Transformer (patch 16, 224px) | Big Transfer ResNet-50v2 |
12
+ | Pretrained weights | AugReg ImageNet-21k + ImageNet-1k | ImageNet-21k + ImageNet-1k |
13
+ | Parameters | ~86M | ~25M |
14
+ | Framework | timm | timm |
15
+
16
+ Both models classify 224x224 RGB Q-transform spectrograms into 23 Gravity Spy
17
+ glitch classes (22 glitch morphologies + No_Glitch). The class taxonomy follows
18
+ Zevin et al. (2017).
19
+
20
+ ## Training Data
21
+
22
+ - **Source:** Gravity Spy O3 (H1 + L1), filtered to ml_confidence > 0.9
23
+ - **Total samples:** 227,943 training / 48,844 validation / 48,845 test
24
+ - **Split:** Temporal split (70/15/15%) with 60-second gap enforcement
25
+ - **Classes:** 23 (see `src/class_labels.json`)
26
+ - **Preprocessing:** Q-transform spectrograms resized to 224x224, normalized with ImageNet statistics
27
+ - **Rare classes:** Chirp (11 train), Wandering_Line (30), Helix (33), Light_Modulation (142)
28
+
29
+ ## Performance (O3 Test Set)
30
+
31
+ **Primary metric: macro-F1** (averaged equally across all 23 classes).
32
+
33
+ | Metric | ViT-B/16 | ResNet-50v2 BiT |
34
+ |---|---|---|
35
+ | **Macro-F1 (PRIMARY)** | **0.7230** [0.7031, 0.7397] | **0.6786** [0.6598, 0.6944] |
36
+ | Rare-class macro-F1 | 0.2412 [0.2019, 0.2957] | 0.3028 [0.2085, 0.3751] |
37
+ | Overall accuracy (secondary) | 0.9343 | 0.9181 |
38
+
39
+ *95% bootstrap confidence intervals (10,000 resamples). Macro-F1 difference is
40
+ statistically significant (p = 0.0002).*
41
+
42
+ > **Note:** Overall accuracy is reported as a secondary sanity check only.
43
+ > It is not the primary metric because it masks rare-class performance
44
+ > differences (see "Limitations" below).
45
+
46
+ ### Per-Class Highlights
47
+
48
+ Architecture preference is **class-morphology-dependent**, not a uniform
49
+ advantage for either model:
50
+
51
+ | Class | ViT F1 | CNN F1 | Difference | Favors |
52
+ |---|---|---|---|---|
53
+ | Power_Line | 0.742 | 0.235 | **+0.507** | ViT |
54
+ | Light_Modulation | 0.859 | 0.691 | +0.168 | ViT |
55
+ | Scratchy | 0.875 | 0.503 | +0.372 | ViT |
56
+ | Chirp | 0.000 | 0.471 | **+0.471** | CNN |
57
+ | Scattered_Light | 0.719 | 0.811 | +0.092 | CNN |
58
+ | Paired_Doves | 0.613 | 0.099 | +0.514 | ViT |
59
+ | Violin_Mode | 0.544 | 0.683 | +0.139 | CNN |
60
+
61
+ The ViT achieves higher overall macro-F1 but performs **worse on rare classes**
62
+ (macro-F1 0.241 vs CNN 0.303). This is a forbidden-proxy scenario: overall
63
+ accuracy improvement does not extend to rare classes.
64
+
65
+ ## O4 Generalization
66
+
67
+ Both models were evaluated on 38,587 O4a Gravity Spy spectrograms:
68
+
69
+ | Metric | ViT-B/16 | ResNet-50v2 BiT |
70
+ |---|---|---|
71
+ | O4 macro-F1 | 0.6695 [0.6555, 0.6816] | 0.6674 [0.6567, 0.6765] |
72
+ | Relative degradation from O3 | -7.4% | -1.7% |
73
+
74
+ Both models pass the <20% degradation threshold. The CNN shows better
75
+ generalization stability across observing runs.
76
+
77
+ ## Limitations
78
+
79
+ - **O3-trained only:** Models have not been fine-tuned on O4 data.
80
+ Performance may degrade further on later observing runs with new
81
+ instrumental configurations.
82
+ - **Single-view:** Models use only the 1.0-second duration Q-transform view.
83
+ Gravity Spy's full pipeline uses four duration views (0.5s, 1.0s, 2.0s, 4.0s).
84
+ - **23 classes:** New glitch morphologies appearing in O4+ will be
85
+ misclassified into existing categories.
86
+ - **Rare-class regression for ViT:** The ViT's macro-F1 advantage does not
87
+ extend to rare classes (< 200 training samples). The CNN outperforms on
88
+ Chirp, Helix, and Violin_Mode.
89
+ - **Label quality:** Training labels are from ml_confidence > 0.9 filtering
90
+ of Gravity Spy citizen science classifications, not expert-reviewed labels.
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ # Install dependencies
96
+ pip install torch timm albumentations numpy Pillow
97
+
98
+ # Classify a spectrogram with ViT
99
+ python release/src/inference.py --model vit --image path/to/spectrogram.png
100
+
101
+ # Classify with CNN, showing top-5 predictions
102
+ python release/src/inference.py --model cnn --image path/to/spectrogram.png --top-k 5
103
+
104
+ # JSON output
105
+ python release/src/inference.py --model vit --image path/to/spectrogram.png --json
106
+ ```
107
+
108
+ ### Input Format
109
+
110
+ - PNG or JPG image of a Q-transform spectrogram
111
+ - Any resolution (automatically resized to 224x224)
112
+ - RGB color (grayscale images are converted to 3-channel)
113
+
114
+ ### Output
115
+
116
+ ```
117
+ Model: ViT-B/16
118
+ Image: spectrogram.png
119
+
120
+ Predictions:
121
+ Rank Class Probability
122
+ -------------------------------------------
123
+ 1 Blip 0.9823
124
+ 2 Blip_Low_Frequency 0.0091
125
+ 3 Koi_Fish 0.0034
126
+ ```
127
+
128
+ ## File Structure
129
+
130
+ ```
131
+ release/
132
+ README.md # This file (model card)
133
+ checkpoints/
134
+ vit_b16_gravityspy_o3.pt # ViT-B/16 weights (~983 MB)
135
+ resnet50v2_gravityspy_o3.pt # ResNet-50v2 BiT weights (~270 MB)
136
+ checksums.sha256 # SHA-256 checksums for integrity
137
+ src/
138
+ inference.py # Standalone CLI inference script
139
+ preprocessing.py # Locked evaluation transforms
140
+ class_labels.json # 23-class index-to-label mapping
141
+ model_config.json # Architecture and training config
142
+ examples/
143
+ expected_output.json # Validated predictions for test images
144
+ ```
145
+
146
+ ## Verifying Checkpoint Integrity
147
+
148
+ ```bash
149
+ cd release/checkpoints
150
+ sha256sum -c checksums.sha256
151
+ ```
152
+
153
+ ## Citation
154
+
155
+ ```bibtex
156
+ @misc{gravitspy_vit_cnn_2026,
157
+ title={Vision Transformer vs. CNN for LIGO Glitch Classification:
158
+ Class-Morphology-Dependent Architecture Preferences},
159
+ author={[Authors]},
160
+ year={2026},
161
+ note={Models trained on Gravity Spy O3 data (Zevin et al. 2017, CQG 34 064003)}
162
+ }
163
+ ```
164
+
165
+ ## License
166
+
167
+ MIT
168
+
169
+ ## References
170
+
171
+ - Zevin, M. et al. (2017). "Gravity Spy: integrating advanced LIGO detector
172
+ characterization, machine learning, and citizen science." *Classical and
173
+ Quantum Gravity*, 34(6), 064003.
checkpoints/checksums.sha256 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ ca5635865a9ad8886e7eed2022b4b2005e439bd9033ce177d656c5068867461d vit_b16_gravityspy_o3.pt
2
+ 65bfe7db29dce148b45dc2eba1d8a728c947c6b2432bbfc741a407801c67e9b3 resnet50v2_gravityspy_o3.pt
checkpoints/resnet50v2_gravityspy_o3.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:65bfe7db29dce148b45dc2eba1d8a728c947c6b2432bbfc741a407801c67e9b3
3
+ size 282766133
checkpoints/vit_b16_gravityspy_o3.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca5635865a9ad8886e7eed2022b4b2005e439bd9033ce177d656c5068867461d
3
+ size 1029991217
examples/expected_output.json ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "filename": "qnPsv3hL2x_1.0s.png",
4
+ "true_label": "Blip",
5
+ "image_path": "data/spectrograms/Blip/qnPsv3hL2x_1.0s.png",
6
+ "vit_prediction": "Blip",
7
+ "vit_confidence": 0.990561,
8
+ "vit_top3": [
9
+ {
10
+ "rank": 1,
11
+ "class": "Blip",
12
+ "probability": 0.990561
13
+ },
14
+ {
15
+ "rank": 2,
16
+ "class": "Blip_Low_Frequency",
17
+ "probability": 0.003823
18
+ },
19
+ {
20
+ "rank": 3,
21
+ "class": "Repeating_Blips",
22
+ "probability": 0.001406
23
+ }
24
+ ],
25
+ "cnn_prediction": "Blip",
26
+ "cnn_confidence": 0.655181,
27
+ "cnn_top3": [
28
+ {
29
+ "rank": 1,
30
+ "class": "Blip",
31
+ "probability": 0.655181
32
+ },
33
+ {
34
+ "rank": 2,
35
+ "class": "Repeating_Blips",
36
+ "probability": 0.323093
37
+ },
38
+ {
39
+ "rank": 3,
40
+ "class": "Extremely_Loud",
41
+ "probability": 0.002679
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "filename": "EMxVe5MHAz_1.0s.png",
47
+ "true_label": "Power_Line",
48
+ "image_path": "data/spectrograms/Power_Line/EMxVe5MHAz_1.0s.png",
49
+ "vit_prediction": "Power_Line",
50
+ "vit_confidence": 0.997734,
51
+ "vit_top3": [
52
+ {
53
+ "rank": 1,
54
+ "class": "Power_Line",
55
+ "probability": 0.997734
56
+ },
57
+ {
58
+ "rank": 2,
59
+ "class": "No_Glitch",
60
+ "probability": 0.000965
61
+ },
62
+ {
63
+ "rank": 3,
64
+ "class": "Blip_Low_Frequency",
65
+ "probability": 0.000165
66
+ }
67
+ ],
68
+ "cnn_prediction": "Power_Line",
69
+ "cnn_confidence": 0.912969,
70
+ "cnn_top3": [
71
+ {
72
+ "rank": 1,
73
+ "class": "Power_Line",
74
+ "probability": 0.912969
75
+ },
76
+ {
77
+ "rank": 2,
78
+ "class": "Air_Compressor",
79
+ "probability": 0.057046
80
+ },
81
+ {
82
+ "rank": 3,
83
+ "class": "No_Glitch",
84
+ "probability": 0.007583
85
+ }
86
+ ]
87
+ },
88
+ {
89
+ "filename": "kwYnhvabQ8_1.0s.png",
90
+ "true_label": "Chirp",
91
+ "image_path": "data/spectrograms/Chirp/kwYnhvabQ8_1.0s.png",
92
+ "vit_prediction": "Blip_Low_Frequency",
93
+ "vit_confidence": 0.944321,
94
+ "vit_top3": [
95
+ {
96
+ "rank": 1,
97
+ "class": "Blip_Low_Frequency",
98
+ "probability": 0.944321
99
+ },
100
+ {
101
+ "rank": 2,
102
+ "class": "Blip",
103
+ "probability": 0.033233
104
+ },
105
+ {
106
+ "rank": 3,
107
+ "class": "Repeating_Blips",
108
+ "probability": 0.005682
109
+ }
110
+ ],
111
+ "cnn_prediction": "Chirp",
112
+ "cnn_confidence": 0.514547,
113
+ "cnn_top3": [
114
+ {
115
+ "rank": 1,
116
+ "class": "Chirp",
117
+ "probability": 0.514547
118
+ },
119
+ {
120
+ "rank": 2,
121
+ "class": "Blip_Low_Frequency",
122
+ "probability": 0.274587
123
+ },
124
+ {
125
+ "rank": 3,
126
+ "class": "Blip",
127
+ "probability": 0.14339
128
+ }
129
+ ]
130
+ },
131
+ {
132
+ "filename": "mRGHzX9W9X_1.0s.png",
133
+ "true_label": "No_Glitch",
134
+ "image_path": "data/spectrograms/No_Glitch/mRGHzX9W9X_1.0s.png",
135
+ "vit_prediction": "No_Glitch",
136
+ "vit_confidence": 0.991169,
137
+ "vit_top3": [
138
+ {
139
+ "rank": 1,
140
+ "class": "No_Glitch",
141
+ "probability": 0.991169
142
+ },
143
+ {
144
+ "rank": 2,
145
+ "class": "1400Ripples",
146
+ "probability": 0.004831
147
+ },
148
+ {
149
+ "rank": 3,
150
+ "class": "Power_Line",
151
+ "probability": 0.000853
152
+ }
153
+ ],
154
+ "cnn_prediction": "No_Glitch",
155
+ "cnn_confidence": 0.989384,
156
+ "cnn_top3": [
157
+ {
158
+ "rank": 1,
159
+ "class": "No_Glitch",
160
+ "probability": 0.989384
161
+ },
162
+ {
163
+ "rank": 2,
164
+ "class": "1400Ripples",
165
+ "probability": 0.003378
166
+ },
167
+ {
168
+ "rank": 3,
169
+ "class": "Helix",
170
+ "probability": 0.002471
171
+ }
172
+ ]
173
+ },
174
+ {
175
+ "filename": "XvHH2XRheR_1.0s.png",
176
+ "true_label": "Scattered_Light",
177
+ "image_path": "data/spectrograms/Scattered_Light/XvHH2XRheR_1.0s.png",
178
+ "vit_prediction": "Scattered_Light",
179
+ "vit_confidence": 0.995357,
180
+ "vit_top3": [
181
+ {
182
+ "rank": 1,
183
+ "class": "Scattered_Light",
184
+ "probability": 0.995357
185
+ },
186
+ {
187
+ "rank": 2,
188
+ "class": "Extremely_Loud",
189
+ "probability": 0.001687
190
+ },
191
+ {
192
+ "rank": 3,
193
+ "class": "Fast_Scattering",
194
+ "probability": 0.000669
195
+ }
196
+ ],
197
+ "cnn_prediction": "Scattered_Light",
198
+ "cnn_confidence": 0.962074,
199
+ "cnn_top3": [
200
+ {
201
+ "rank": 1,
202
+ "class": "Scattered_Light",
203
+ "probability": 0.962074
204
+ },
205
+ {
206
+ "rank": 2,
207
+ "class": "Low_Frequency_Lines",
208
+ "probability": 0.017069
209
+ },
210
+ {
211
+ "rank": 3,
212
+ "class": "Extremely_Loud",
213
+ "probability": 0.004967
214
+ }
215
+ ]
216
+ }
217
+ ]
src/__pycache__/inference.cpython-312.pyc ADDED
Binary file (7.76 kB). View file
 
src/__pycache__/preprocessing.cpython-312.pyc ADDED
Binary file (2.53 kB). View file
 
src/class_labels.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ["1080Lines", "1400Ripples", "Air_Compressor", "Blip", "Blip_Low_Frequency", "Chirp", "Extremely_Loud", "Fast_Scattering", "Helix", "Koi_Fish", "Light_Modulation", "Low_Frequency_Burst", "Low_Frequency_Lines", "No_Glitch", "Paired_Doves", "Power_Line", "Repeating_Blips", "Scattered_Light", "Scratchy", "Tomte", "Violin_Mode", "Wandering_Line", "Whistle"]
src/inference.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Standalone inference script for Gravity Spy glitch classification.
3
+
4
+ Classifies LIGO glitch spectrograms using either a ViT-B/16 or ResNet-50v2 BiT
5
+ model trained on O3 Gravity Spy data (23 classes).
6
+
7
+ Usage:
8
+ python inference.py --model vit --image path/to/spectrogram.png
9
+ python inference.py --model cnn --image path/to/spectrogram.png --top-k 5
10
+
11
+ Requirements: torch, timm, albumentations, numpy, Pillow
12
+ No training code imports required.
13
+ """
14
+ # ASSERT_CONVENTION: primary_metric=macro_f1, input_format=224x224_RGB_PNG_0to1
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ import torch
23
+ import timm
24
+
25
+ from preprocessing import load_image
26
+
27
+
28
+ def load_model(model_key, config_path=None, checkpoint_dir=None):
29
+ """Load a model from config and checkpoint.
30
+
31
+ Parameters
32
+ ----------
33
+ model_key : str
34
+ Either 'vit' or 'cnn'.
35
+ config_path : Path or None
36
+ Path to model_config.json. Defaults to same directory as this script.
37
+ checkpoint_dir : Path or None
38
+ Path to checkpoints directory. Defaults to ../checkpoints/ relative to this script.
39
+
40
+ Returns
41
+ -------
42
+ model : torch.nn.Module
43
+ Loaded model in mode for inference.
44
+ class_labels : list of str
45
+ Ordered class label names (index -> label).
46
+ """
47
+ script_dir = Path(__file__).resolve().parent
48
+ if config_path is None:
49
+ config_path = script_dir / "model_config.json"
50
+ if checkpoint_dir is None:
51
+ checkpoint_dir = script_dir.parent / "checkpoints"
52
+
53
+ with open(config_path) as f:
54
+ config = json.load(f)
55
+
56
+ if model_key not in config["models"]:
57
+ available = list(config["models"].keys())
58
+ raise ValueError(f"Unknown model key '{model_key}'. Choose from: {available}")
59
+
60
+ model_cfg = config["models"][model_key]
61
+
62
+ # Build architecture via timm (no pretrained weights -- we load our own)
63
+ model = timm.create_model(
64
+ model_cfg["model_name"],
65
+ pretrained=False,
66
+ num_classes=model_cfg["num_classes"],
67
+ )
68
+
69
+ # Load trained weights
70
+ checkpoint_path = checkpoint_dir / model_cfg["checkpoint_file"]
71
+ if not checkpoint_path.exists():
72
+ raise FileNotFoundError(
73
+ f"Checkpoint not found: {checkpoint_path}\n"
74
+ f"Expected file: {model_cfg['checkpoint_file']}"
75
+ )
76
+
77
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
78
+ # Handle both full training checkpoints (with 'model_state_dict' key)
79
+ # and bare state_dict files
80
+ if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
81
+ state_dict = checkpoint["model_state_dict"]
82
+ else:
83
+ state_dict = checkpoint
84
+ model.load_state_dict(state_dict)
85
+
86
+ # Set to inference mode
87
+ model.requires_grad_(False)
88
+ model.eval()
89
+
90
+ # Load class labels
91
+ labels_path = script_dir / "class_labels.json"
92
+ with open(labels_path) as f:
93
+ class_labels = json.load(f)
94
+
95
+ return model, class_labels
96
+
97
+
98
+ def predict(model, image_tensor, class_labels, top_k=3):
99
+ """Run inference on a preprocessed image tensor.
100
+
101
+ Parameters
102
+ ----------
103
+ model : torch.nn.Module
104
+ Model in inference mode.
105
+ image_tensor : torch.Tensor
106
+ Preprocessed image of shape (3, 224, 224).
107
+ class_labels : list of str
108
+ Ordered class label names.
109
+ top_k : int
110
+ Number of top predictions to return.
111
+
112
+ Returns
113
+ -------
114
+ predictions : list of dict
115
+ Top-k predictions, each with 'rank', 'class', 'probability'.
116
+ """
117
+ with torch.no_grad():
118
+ logits = model(image_tensor.unsqueeze(0)) # (1, num_classes)
119
+ probs = torch.softmax(logits, dim=1).squeeze(0) # (num_classes,)
120
+
121
+ top_k = min(top_k, len(class_labels))
122
+ top_probs, top_indices = torch.topk(probs, top_k)
123
+
124
+ predictions = []
125
+ for rank, (prob, idx) in enumerate(zip(top_probs, top_indices), 1):
126
+ predictions.append({
127
+ "rank": rank,
128
+ "class": class_labels[idx.item()],
129
+ "probability": round(prob.item(), 6),
130
+ })
131
+
132
+ return predictions
133
+
134
+
135
+ def main():
136
+ parser = argparse.ArgumentParser(
137
+ description="Gravity Spy glitch classification inference",
138
+ formatter_class=argparse.RawDescriptionHelpFormatter,
139
+ epilog=(
140
+ "Examples:\n"
141
+ " python inference.py --model vit --image spectrogram.png\n"
142
+ " python inference.py --model cnn --image spectrogram.png --top-k 5\n"
143
+ " python inference.py --model vit --image spectrogram.png --json\n"
144
+ ),
145
+ )
146
+ parser.add_argument("--model", required=True, choices=["vit", "cnn"],
147
+ help="Model to use: vit (ViT-B/16) or cnn (ResNet-50v2 BiT)")
148
+ parser.add_argument("--image", required=True, type=str,
149
+ help="Path to spectrogram image (PNG/JPG)")
150
+ parser.add_argument("--top-k", type=int, default=3,
151
+ help="Number of top predictions to show (default: 3)")
152
+ parser.add_argument("--json", action="store_true",
153
+ help="Output predictions as JSON")
154
+ args = parser.parse_args()
155
+
156
+ # Validate image path
157
+ image_path = Path(args.image)
158
+ if not image_path.exists():
159
+ print(f"Error: Image not found: {image_path}", file=sys.stderr)
160
+ sys.exit(1)
161
+
162
+ # Load model
163
+ model, class_labels = load_model(args.model)
164
+
165
+ # Preprocess image
166
+ image_tensor = load_image(str(image_path))
167
+
168
+ # Run inference
169
+ predictions = predict(model, image_tensor, class_labels, top_k=args.top_k)
170
+
171
+ # Output results
172
+ if args.json:
173
+ result = {
174
+ "model": args.model,
175
+ "image": str(image_path),
176
+ "predictions": predictions,
177
+ }
178
+ print(json.dumps(result, indent=2))
179
+ else:
180
+ model_name = "ViT-B/16" if args.model == "vit" else "ResNet-50v2 BiT"
181
+ print(f"\nModel: {model_name}")
182
+ print(f"Image: {image_path}")
183
+ print(f"\nPredictions:")
184
+ print(f"{'Rank':<6} {'Class':<25} {'Probability':<12}")
185
+ print("-" * 43)
186
+ for pred in predictions:
187
+ print(f"{pred['rank']:<6} {pred['class']:<25} {pred['probability']:.4f}")
188
+ print()
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
src/model_config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "models": {
3
+ "vit": {
4
+ "model_name": "vit_base_patch16_224.augreg_in21k_ft_in1k",
5
+ "architecture": "ViT-B/16",
6
+ "num_classes": 23,
7
+ "pretrained_source": "AugReg ImageNet-21k fine-tuned on ImageNet-1k",
8
+ "checkpoint_file": "vit_b16_gravityspy_o3.pt",
9
+ "input_size": 224,
10
+ "normalization": {
11
+ "mean": [0.485, 0.456, 0.406],
12
+ "std": [0.229, 0.224, 0.225]
13
+ }
14
+ },
15
+ "cnn": {
16
+ "model_name": "resnetv2_50x1_bit.goog_in21k_ft_in1k",
17
+ "architecture": "ResNet-50v2 BiT",
18
+ "num_classes": 23,
19
+ "pretrained_source": "ImageNet-21k fine-tuned on ImageNet-1k",
20
+ "checkpoint_file": "resnet50v2_gravityspy_o3.pt",
21
+ "input_size": 224,
22
+ "normalization": {
23
+ "mean": [0.485, 0.456, 0.406],
24
+ "std": [0.229, 0.224, 0.225]
25
+ }
26
+ }
27
+ },
28
+ "training": {
29
+ "dataset": "Gravity Spy O3",
30
+ "num_classes": 23,
31
+ "split": "temporal 70/15/15% with 60s gap enforcement",
32
+ "train_samples": 227943,
33
+ "seed": 42
34
+ }
35
+ }
src/preprocessing.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone preprocessing for Gravity Spy spectrogram inference.
2
+
3
+ Extracts eval_transforms() from the training pipeline with NO training
4
+ dependencies (no wandb, no dataloader, no training-specific imports).
5
+
6
+ Preprocessing is locked to match training exactly:
7
+ - Resize to 224x224
8
+ - Normalize with ImageNet statistics
9
+ - Convert to PyTorch tensor
10
+ """
11
+ # ASSERT_CONVENTION: primary_metric=macro_f1, input_format=224x224_RGB_PNG_0to1
12
+
13
+ import numpy as np
14
+ from PIL import Image
15
+ import albumentations as A
16
+ from albumentations.pytorch import ToTensorV2
17
+
18
+ # ImageNet statistics for pretrained model normalization
19
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
20
+ IMAGENET_STD = [0.229, 0.224, 0.225]
21
+
22
+
23
+ def eval_transforms(image_size=224):
24
+ """Evaluation transform -- resize + normalize only, no augmentation.
25
+
26
+ This is identical to the eval_transforms used during training/validation.
27
+ Input images are expected to be RGB numpy arrays with pixel values in [0, 255].
28
+ Output tensors have pixel values normalized by ImageNet statistics.
29
+
30
+ Parameters
31
+ ----------
32
+ image_size : int
33
+ Target spatial dimension (default 224 for ViT-B/16 and ResNet-50v2).
34
+
35
+ Returns
36
+ -------
37
+ transform : albumentations.Compose
38
+ Evaluation transform pipeline.
39
+ """
40
+ return A.Compose([
41
+ A.Resize(image_size, image_size),
42
+ A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
43
+ ToTensorV2(),
44
+ ])
45
+
46
+
47
+ def load_image(image_path, image_size=224):
48
+ """Load an image file and apply evaluation transforms.
49
+
50
+ Parameters
51
+ ----------
52
+ image_path : str
53
+ Path to a PNG/JPG spectrogram image.
54
+ image_size : int
55
+ Target spatial dimension (default 224).
56
+
57
+ Returns
58
+ -------
59
+ tensor : torch.Tensor
60
+ Preprocessed image tensor of shape (3, image_size, image_size).
61
+ """
62
+ img = Image.open(image_path).convert("RGB")
63
+ img_np = np.array(img) # shape (H, W, 3), dtype uint8, values [0, 255]
64
+ transform = eval_transforms(image_size)
65
+ transformed = transform(image=img_np)
66
+ return transformed["image"] # torch.Tensor (3, 224, 224)