Paper · 2021-03-22 · cs.CV, cs.AI, cs.LG
SSD: A Unified Framework for Self-Supervised Outlier Detection
SSD trains a ResNet-50 on unlabeled in-distribution images with a SimCLR-style contrastive loss, then scores inputs by Mahalanobis distance of their normalised features to the training features. Author-reported average AUROC over four CIFAR-10/100 OOD pairs: 88.7 without labels, 92.5 with five OOD samples per class.
Out-of-distribution detectionSelf-supervised learningContrastive learningMahalanobis distanceout-of-distribution-detectionself-supervised-learningmahalanobis-distance
arXivPDFCodeMarkdown12 min read
AI-compiled by claude-haiku-5-5 on 2026-10-31, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
Spotting inputs that a model was not built for usually needs labelled examples of normal data, and labels are expensive. This paper asks how little is needed, and its answer is that unlabeled normal data can be enough. The method first teaches a network to recognise images without labels: two random crops of the same picture should give similar features, and crops of different pictures should not. Then it describes where the features of normal data sit, as a blob with a size and a shape, and scores a new input by how far it lies from that blob. The distance it uses, Mahalanobis distance, takes the blob's shape into account. Far away means probably unusual. A few labelled outliers, or labels for the normal data, can be added to sharpen the score. The paper reports that this matches or beats several detectors that use labels, though not on every pair of datasets.
TL;DR
SSD is a label-free outlier detector: contrastive self-supervised training on unlabeled in-distribution images, followed by a single-Gaussian Mahalanobis score on normalised features. Its author-reported average AUROC over four CIFAR-10/100 pairs is 88.7 tab. 1, against 80.9 for Rotation-loss, the best unlabeled baseline with a complete row in that table. Scoring with five OOD samples per class reaches 92.5, and adding labels reaches 92.4 tab. 4. Use it when no labels exist for the normal data; the training recipe has several details the paper leaves out (see Pitfalls and Paper vs code).
Problem
Unsupervised detectors that model only unlabeled in-distribution data often perform close to random, while the strongest detectors need fine-grained labels for the training data §1. The paper also considers two extensions: a few OOD samples available at test time (one to five per class) §1 §3.2, and labels for the in-distribution data when they exist §3.3. The core method assumes no OOD data for training or tuning, and avoids extra tuning parameters, using one standard training setup and a single cluster §5.
Method
SSD has two stages §3. Stage one trains a feature extractor on unlabeled in-distribution images with the NT-Xent contrastive loss §3, eq. 1. Stage two needs no further training: it estimates the mean and covariance of the normalised training features and scores each test input by its Mahalanobis distance to them §3.1, eq. 2.
Steps for the label-free detector:
- Train backbone f and projection head h with eq. 1 on unlabeled in-distribution images, for 500 epochs with batch size 512 §4.1.
- Extract f(x), the feature before the projection head, for the training images, and L2-normalise each one alg. 1.
- Estimate the mean and sample covariance of the normalised training features, using a single cluster §3.1 §4.1.
- For each test input, compute its normalised feature and the score in eq. 2 §3.1.
- Report AUROC, AUPR and FPR at 95% TPR §4.1 app. A.2.
Figure 1 plots the AUROC along each principal eigenvector of the features. High-eigenvalue directions dominate Euclidean distance but help least with detection, which motivates scaling each direction by its eigenvalue, i.e. Mahalanobis distance fig. 1 §3.1.
Extensions. SSD_k (few-shot): with k OOD samples per class, the score subtracts a Mahalanobis term for the OOD statistics eq. 3. Those statistics come from augmented copies of the OOD samples, with Ledoit-Wolf shrinkage for the covariance eq. 4 §3.2. SSD_sup (labels): NT-Xent is replaced by a supervised contrastive loss in which same-label images in the batch are positives eq. 5 §3.3. alg. 1 summarises the full pipeline. It holds out a calibration split and sets a threshold at a required true-positive rate T; the experiments report threshold-free metrics instead §4.1.
Eq. 1, NT-Xent over a batch of N images with two views each, where j is the other view of image i and tau is the temperature §3:
\mathcal{L}_{batch} = \frac{1}{2N}\sum_{i=1}^{2N} -\log\frac{e^{u_i^\top u_j/\tau}}{\sum_{k=1}^{2N}\mathbb{1}(k\neq i)\,e^{u_i^\top u_k/\tau}}, \qquad u_i = \frac{h(f(x_i))}{\|h(f(x_i))\|_2} eq. 1
Eq. 2, the single-cluster Mahalanobis score; the paper's general form takes the minimum over m clusters §3.1:
s_x = \min_m\,(z_x-\mu_m)^\top \Sigma_m^{-1} (z_x-\mu_m) eq. 2
Eq. 3, the few-shot score, with in-distribution statistics and OOD statistics §3.2:
s_x = (z_x-\mu_{in})^\top \Sigma_{in}^{-1} (z_x-\mu_{in}) - (z_x-\mu_{ood})^\top \Sigma_{ood}^{-1} (z_x-\mu_{ood}) eq. 3
Eq. 4, the same score with the OOD statistics estimated from the augmented set U with shrunk covariance S_U §3.2:
s_x = (z_x-\mu_{in})^\top \Sigma_{in}^{-1} (z_x-\mu_{in}) - (z_x-\mu_{U})^\top S_{U}^{-1} (z_x-\mu_{U}) eq. 4
Eq. 5, the label-aware loss, where N_{y_i} is the number of images with label y_i in the batch §3.3:
\mathcal{L}_{batch} = \frac{1}{2N}\sum_{i=1}^{2N} -\log\frac{\frac{1}{2N_{y_i}-1}\sum_{k=1}^{2N}\mathbb{1}(k\neq i)\,\mathbb{1}(y_k=y_i)\,e^{u_i^\top u_k/\tau}}{\sum_{k=1}^{2N}\mathbb{1}(k\neq i)\,e^{u_i^\top u_k/\tau}} eq. 5
NT-Xent with the positive-pair rule written out, for the label-free stage. Augmentation and the head width are not stated in the paper; see Hyperparameters and Paper vs code:
# NT-Xent loss over 2N augmented views [§3] [eq. 1]
# Input: images (N, 3, H, W), unlabeled; tau = 0.5 [§4.1]
# Output: scalar loss
def ntxent_loss(images):
# Two random augmented views of every image [§3]
views = concat(augment(images), augment(images)) # (2N, 3, H, W)
# Backbone f, then projection head h, then unit length [eq. 1]
embeddings = normalise(head(backbone(views))) # (2N, d_out)
# Scaled cosine similarities between all views [eq. 1]
logits = embeddings @ embeddings.T / tau # (2N, 2N)
loss = 0
for i in range(2N):
# The positive of view i is the other view of the same image [eq. 1]
positive = pair_index(i)
# Denominator sums over all views except i itself [eq. 1]
loss += -log(exp(logits[i, positive]) / sum_except(exp(logits[i]), i))
return loss / (2N)Scoring with one cluster, the core detector. The repository also normalises each dimension (see Paper vs code):
# Single-cluster Mahalanobis score [§3.1] [eq. 2] [alg. 1]
# Input: train_feats (N, d) from in-distribution training data
# test_feat (d,) for one input
# Output: score, larger means more likely an outlier
def ssd_score(train_feats, test_feat):
# Unit L2 norm for training and test features [alg. 1]
train_z = normalise_rows(train_feats) # (N, d)
test_z = test_feat / norm(test_feat) # (d,)
# Sample mean and sample covariance of training features [§3.1]
mu = mean(train_z, axis=0) # (d,)
sigma = covariance(train_z) # (d, d)
# Squared Mahalanobis distance to the training distribution [eq. 2]
diff = test_z - mu # (d,)
return diff @ inverse(sigma) @ diffHyperparameters
| Name | Value | Where | Notes |
|---|---|---|---|
| Backbone | ResNet-50 for key experiments; ResNet-18 for ablations | §4.1 app. A.1 | ResNet-34 and ResNet-101 also reported §4.1. Code uses a CIFAR-style stem: 3x3 conv, stride 1, no max-pool models.py models.py |
| Projection head h | Two-layer fully connected network | app. A.1 | Code: Linear(d, d), ReLU, Linear(d, 128), output normalised models.py. Output width 128 not stated in paper |
| Temperature tau (NT-Xent) | 0.5 | §4.1 | Anomaly-detection experiment uses 0.1 §4.2. Code default 0.5 train.py |
| Epochs | 500 | §4.1 | Code default 500 train.py |
| Batch size | 512 | §4.1 app. A.1 | Code default 512 train.py. Code warns that batches above 256 need warmup train.py |
| Optimiser | SGD | §4.1 | Momentum 0.9 is not stated in the paper; code default train.py |
| Learning rate | 0.5, cosine decay | §4.1 | Code decays per iteration to a floor of 1e-4 train.py trainers.py. Floor not stated in paper |
| Warmup | not stated | n/a | Code option: 10 epochs from 0.01 up to the learning rate, run before the 500 epochs train.py. README training command uses it [README L41-L44] |
| Weight decay | 1e-4 | §4.1 | Code default train.py |
| Training augmentation | not stated | n/a | Code: RandomResizedCrop (scale 0.2 to 1.0), horizontal flip, ColorJitter (0.4, 0.4, 0.4, 0.1) with p=0.8, grayscale with p=0.2 data.py |
| Input normalisation | not stated | n/a | Code flag --normalize, off by default eval_ssd.py. CIFAR-10 mean and std data.py. README: must match training [README L27] |
| Clusters (SSD) | 1 | §4.1 | Chosen from a layer analysis that used CIFAR-100 as OOD app. B.2 |
| Covariance (SSD) | Sample covariance | §3.1 | Code uses the 1/N form (bias=True) and a pseudo-inverse utils.py utils.py |
| Shrinkage (SSD_k) | Ledoit-Wolf | §4.3 | Code applies it to in-distribution and OOD covariance eval_ssdk.py |
| k (few-shot) | 1 or 5 OOD samples per class | §3.2 §4.3 | |
| Augmented copies per OOD sample | 10 | app. A.1 | For small k the paper sometimes uses one transformation per sample. Code default is 10 with no such rule eval_ssdk.py |
| Trials (few-shot) | 25 random trials | §4.3 | Not in the code: one run per call, and NumPy is not seeded eval_ssdk.py |
| Repeats (other results) | Average of 3 runs in most experiments | §4.1 | Code default seed 12345 train.py |
| FPR operating point | TPR 95% | app. A.2 | |
| Blobs OOD set | Binomial p=0.7, Gaussian blur sigma 1.5, threshold 0.75 | Not in paper | Only referenced to Hendrycks et al. app. A.3 data.py |
| Gaussian noise OOD | Mean 0.5, std 0.25, clipped to [0, 1] | app. A.3 | |
| ODIN (baseline only) | Perturbation 0.0014, temperature 1000 | app. A.1 |
Results
Author-reported. Setup: ResNet-50 unless stated, 500 epochs, batch size 512, CIFAR-10 as in-distribution unless the row says otherwise §4.1. The Softmax, ODIN and Mahalanobis baselines were re-implemented by the authors on the same supervised network app. A.1.
| Setting | Metric | Baseline | This paper | Source |
|---|---|---|---|---|
| CIFAR-10 in, mean over four OOD pairs | AUROC | Rotation-loss 80.9 | SSD 88.7 | tab. 1 |
| Same four pairs, few-shot with k=5 | AUROC | SSD 88.7 | SSD_k 92.5 | tab. 1 |
| CIFAR-10 in, CIFAR-100 OOD | AUROC | CSI 89.2 | SSD 90.6 | tab. 1 |
| CIFAR-10 in, SVHN OOD | AUROC | CSI 99.8 | SSD 99.6 | tab. 1 |
| CIFAR-100 in, CIFAR-10 OOD | AUROC | Supervised (Mahalanobis) 55.3 | SSD 69.6; SSD_k k=5 78.3 | tab. 2 |
| CIFAR-100 in, Texture OOD | AUROC | Supervised (Mahalanobis) 98.8 | SSD 82.9 | tab. 2 |
| CIFAR-10 one-class anomaly detection, mean over 10 classes | AUROC | CSI 94.3 | SSD 90.0 | tab. 3 |
| Labels, four CIFAR-10/100 pairs, mean | AUROC | Contrastive + supervised 91.6 | SSD_sup 92.4; SSD_k-sup (k=5) 93.8 | tab. 4 |
| Temperature sweep, ResNet-18, CIFAR-10 in, CIFAR-100 OOD | AUROC | tau = 0.001: 66.7 | tau = 0.5: 89.5 | tab. 5 |
| ImageNet in, SVHN OOD | AUROC | Supervised 99.4 | SSD 99.1 | tab. 2 (see Pitfalls) |
| Few-shot gain over SSD, averaged over the pairs in Table 2 | AUROC | SSD baseline | +1.6 (k=1), +2.1 (k=5) | §4.3 (see Pitfalls) |
| CIFAR-100 in, CIFAR-10 OOD: shrinkage, then augmentation | AUROC | 69.6 (SSD) | 77.1 (shrinkage), 78.3 (k=5 with augmentation) | §4.3 |
| Released checkpoint, CIFAR-10 SimCLR: CIFAR-100 / SVHN / Texture / Blobs | AUROC | n/a | 90.6 / 99.6 / 97.6 / 98.8 | [README L57-L60] |
The label-free detector beats the reported unlabeled baselines on average. Supervised training gives a better result on some pairs, such as CIFAR-100 with Texture tab. 2. Contrastive self-supervised features beat supervised ones on 56% of the tasks in the paper's comparison §4.2. The paper states no hardware or compute cost for these runs.
Paper vs code
Code: official repository, commit cf2e162ef7ada254ef916d1064c0a19ad571ee71 (MIT, copyright Princeton INSPIRE Research Group) LICENSE. Repository files: train.py, trainers.py, losses.py, models.py, data.py, utils.py, eval_ssd.py, eval_ssdk.py.
- Score features. The paper's Algorithm 1 normalises features by L2 norm only alg. 1. The evaluation code then standardises every dimension with the training mean and standard deviation eval_ssd.py eval_ssdk.py. The paper does not mention this step.
- Covariance estimate. The paper says sample covariance and an inverse §3.1. The code uses
np.cov(..., bias=True), which divides by N, and a pseudo-inverse utils.py utils.py utils.py. - Shrinkage in few-shot. The paper uses Ledoit-Wolf for the OOD covariance §4.3. The code also applies it to the in-distribution covariance eval_ssdk.py utils.py.
- Few-shot protocol. The paper averages 25 random trials §4.3 and uses a single transformation per sample when k is small app. A.1. The code runs one trial per call and always uses 10 copies by default eval_ssdk.py. It does not seed NumPy eval_ssdk.py, while the sampler uses
np.randomutils.py. - Supervised contrastive loss. Eq. 5 takes the log of the mean similarity over positives eq. 5. The code averages the log-probabilities over positives losses.py, which is a different loss.
- Loss scale. The code multiplies the loss by temperature divided by base temperature, which is 0.5 / 0.07 here losses.py losses.py train.py. Eq. 1 has no such factor §3. The effective step size therefore depends on the code's scaling, not only on the learning rate of 0.5 §4.1.
- Optimiser and schedule. Momentum 0.9 and a 1e-4 cosine floor are in the code train.py train.py; the paper gives neither.
- Warmup. The paper does not mention warmup §4.1. The README's training command enables it [README L41-L44], and it adds 10 epochs before the 500 train.py.
- Augmentation of the supervised baseline. The SSD path uses the stronger augmentation data.py. The supervised baseline uses crop and flip with cross-entropy data.py train.py train.py. The paper states an identical training budget §4.1. The augmentations differ, which that statement does not cover.
- Released checkpoints. The README says its models "aren't exactly identical" to the paper [README L52]. Its example checkpoint path names batch size 900 [README L53], while the paper uses 512 §4.1.
- Anomaly detection. The paper adds an L2 feature regulariser with coefficient 0.01 and uses tau = 0.1 §4.2. The repository has no such term losses.py and no anomaly mode train.py.
- Experiments without code. ImageNet, ImageNet-O and the STL-10 extra-unlabeled-data experiment have no data loader in the repository data.py app. B.4 tab. 6.
- Minor code issue. The STL-10 loader reads from the SVHN folder data.py.
- Metric definitions. FPR counts OOD scores below the 95th percentile of in-distribution scores utils.py. AUPR treats OOD as the positive class utils.py. The paper does not say which class is positive for AUPR app. A.2.
- Model selection.
train.pykeeps the best checkpoint by kNN accuracy on the CIFAR-10 test set train.py utils.py. The paper does not describe checkpoint selection.
No discrepancies were found for the backbone choice (ResNet-50), NT-Xent temperature 0.5 train.py, batch size 512 train.py, epochs 500 train.py, learning rate 0.5 train.py or weight decay 1e-4 train.py.
Searches: the paper links the repository in its abstract, so no other search was needed.
Pitfalls
- Temperature matters a lot. At tau = 0.001 the detector drops to 66.7 AUROC, against 89.5 at tau = 0.5, on the ResNet-18 ablation tab. 5. Treat tau as a sensitive value, not a free one.
- Warmup and batch size. Batch 512 gives 1024 views per forward pass, and BatchNorm statistics are computed over both views together trainers.py. The code's own warning recommends warmup for batches above 256 train.py.
- Checkpoint selection uses test data. The kNN accuracy used to pick the best checkpoint is measured on the CIFAR-10 test set train.py. Results from that checkpoint are then not clean held-out numbers.
- Layer and cluster choice used a reported OOD set. The single-cluster choice rests on an analysis with CIFAR-10 as in-distribution and CIFAR-100 as OOD app. B.2, and CIFAR-100 is also one of the reported OOD sets tab. 1.
- Zero-variance dimensions. Each feature dimension is divided by its standard deviation plus 1e-10 eval_ssd.py. A dimension that is constant in training can then produce very large test values.
- Pseudo-inverse of a 2048 by 2048 matrix. The pseudo-inverse hides near-singularity rather than failing. The code recomputes it for each score call, and twice per call for in-distribution and OOD sets utils.py utils.py.
- Few-shot cost. Each OOD sample generates 10 augmented copies, so the forward passes grow with k times the number of classes times 10 utils.py utils.py.
- Algorithm 1 is not implemented. The threshold procedure with a calibration split alg. 1 has no code path. The evaluation scripts report threshold-free metrics only eval_ssd.py.
- Paper tables disagree with each other.
- ImageNet SSD and supervised AUROC are swapped between Table 2 (SSD 99.1, supervised 99.4) and Table 6 (SSD 99.4, supervised 99.1) tab. 2 tab. 6.
- CSI on CIFAR-10 in, CIFAR-100 OOD is 89.2 in Table 1 but 92.2 in Table 4 tab. 1 tab. 4.
- The few-shot gains of 1.6 and 2.1 §4.3 do not match Table 1's four-pair averages, which rise from 88.7 to 92.5 (+3.8) tab. 1. The averaging set is not defined.
- Compute is not reported in the results or appendix. Stage one is the expensive part: ResNet-50 for 500 epochs at batch size 512.
- Architecture. The paper describes ResNet-50 without its stem. The code uses a CIFAR-style stem, and an ImageNet-style stem would differ from the code models.py. The paper does not say which one the reported numbers used.
Lineage
- Builds on: SimCLR's NT-Xent loss §3, eq. 1; the code's docstring names the SimCLR paper as arXiv:2002.05709 losses.py. The label-aware loss is supervised contrastive learning §3.3, which the code's docstring names as arXiv:2004.11362 losses.py.
- Compared against: PixelCNN++, autoencoder, VAE, Deep-SVDD and Rotation-loss tab. 1; CSI tab. 1 tab. 3; Softmax probabilities, ODIN, the Mahalanobis detector of Lee et al. (2018), Residual Flows, Gram matrix, Outlier Exposure, and Contrastive + supervised tab. 4.
- Superseded by: not stated in content already in this wiki.
Source and license
Adapted from “SSD: A Unified Framework for Self-Supervised Outlier Detection” by Vikash Sehwag, Mung Chiang, Prateek Mittal (arXiv:2103.12051), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/inspire-group/SSD at commit cf2e162ef7ad (MIT). Any quoted code is shown under that license.