Viyog
▶ Try the interactive leaderboard — Viyog vs 9 pytorch-ood baselines across 20 architectures.
Separate adversarial (ADV) inputs from out-of-distribution (OOD) inputs — in one forward pass, with no training and no gradients.
Safety-critical systems must respond differently to two kinds of anomaly: OOD inputs call for abstention, adversarial inputs demand rejection. Standard detectors collapse both into a single anomaly score and cannot tell them apart. Viyog is a training-free, post-hoc second stage that makes the distinction by reading the dormant-band roughness of a model's first convolutional layer.
The idea: gradient-based attacks inject broadband high-frequency residue into the
first-layer channels that are otherwise quiet on in-distribution data, making
them spatially jagged. Natural inputs — both ID and OOD — leave those channels
smooth. Viyog measures that roughness as a single scalar V(x):
- higher
V(x)→ more likely ADV - lower
V(x)→ more likely OOD / ID
It adds no parameters, never touches the backward pass, and stores only O(C)
bytes of state (the dormant-channel ranking + one ID mean) — roughly 0.3 KB,
versus 4.5–40 MB for feature-distance detectors such as Mahalanobis / KNN / ViM.
Install
pip install viyog # core: torch + numpy
pip install "viyog[metrics]" # + scikit-learn, for viyog_metrics()
Quickstart
from viyog import Viyog
v = Viyog(model) # attaches a forward hook to the first conv layer
v.fit(id_loader) # one ID pass: learn the dormant band + ID mean
scores = v.score(loader) # per-sample roughness V(x); HIGHER => more adversarial
v.close()
Or as a context manager (hooks are removed automatically):
from viyog import Viyog, viyog_metrics
with Viyog(model) as v:
v.fit(id_loader)
ood_scores = v.score(ood_loader)
adv_scores = v.score(adv_loader)
# separability report (OOD vs ADV): AUROC / AUPR / FPR@95 / AUTC
print(viyog_metrics(ood_scores.cpu().numpy(), adv_scores.cpu().numpy()))
With a real model (timm)
Viyog auto-detects the first conv layer (resnet50.conv1 here), so any
torch.nn.Module works out of the box:
import torch, timm
from viyog import Viyog
model = timm.create_model("resnet50", pretrained=True, num_classes=10).eval()
with Viyog(model) as v:
v.fit(id_loader) # your in-distribution DataLoader
print(v.layer_name_, v.n_channels_, v.dorm_idx_.numel()) # conv1 64 6
adv_scores = v.score(suspect_loader) # higher V(x) => more adversarial
Pass device="cuda:0" for GPU, or layer=<module or dotted name> to hook a
specific layer instead of the auto-detected first conv.
How it works
For each input, Viyog hooks the first conv layer's activation map a of shape
(B, C, H, W) and computes, per channel, a magnitude-normalised total
variation (average absolute change between neighbouring pixels):
tv = (|Δ_h a| + |Δ_w a|) / (mean|a| + eps) # spatial roughness, per channel
V(x) = mean over the dormant channels of tv # the Viyog score
The dormant band is the quietest dorm_pct (default 10%) of channels ranked
by their mean absolute activation on ID data, restricted to alive channels
(permanently-dead channels — common in e.g. DenseNet first convs — are excluded so
the statistic stays meaningful). .fit() learns that band and the ID mean of V
in a single pass; .score() returns V(x) for new inputs.
Conv2d maps use 2-D total variation; Conv1d maps (B, C, L) use 1-D total
variation, so the detector also applies to 1-D signal models.
API
Viyog(model, device=None, layer=None, dorm_pct=0.10, dead_thresh=1e-4) |
Wrap a model; auto-detects the first conv layer (prefers a conv1 attribute). Pass layer= (module or dotted name) to override. |
.fit(id_loader) -> self |
One ID pass: selects the dormant band, records id_score_mean_. |
.score(x, center=False) -> Tensor |
Per-sample V(x) for a batch or a whole loader. Higher = more adversarial. center=True subtracts the ID mean (monotone; AUROC-preserving). |
.score_loader(loader, center=False) |
Convenience wrapper over a loader. |
Viyog.bounded_score(scores, temperature=1.0) |
Optional monotone squash to (0, 1) for thresholding/display. |
.close() / context manager |
Remove the forward hook. |
viyog_metrics(neg, adv, recall_level=0.95) |
AUROC / AUPR_IN / AUPR_OUT / FPR95 / DetectionError / AUTC for two score populations. Needs viyog[metrics]. |
Fitted attributes: dorm_idx_, id_profile_, id_score_mean_, n_channels_,
layer_name_.
Results
Across 20 architectures on CIFAR-100 (ResNet/DenseNet/ConvNeXt/Swin/ViT and edge backbones), the dormant-band roughness score reaches AUROC ≈ 0.966 for adversarial detection (ID vs ADV) and ≈ 0.824 for OOD vs ADV — where logit detectors (Energy/MSP/MaxLogit/GEN) are near-blind to adversarials (≤ 0.69) and feature-distance detectors cost 4.5–40 MB of state versus Viyog's ~0.3 KB. See the accompanying paper.
License
MIT — see LICENSE.
Citation
If you use Viyog in academic work, please cite the accompanying paper "Viyog: Separating Adversarial and Out-of-Distribution."
Metadata
Release files for viyog 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| viyog-0.1.3.tar.gz | 13.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| viyog-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.1 kB
Release files / viyog-0.1.3.tar.gz
| Download URL | viyog-0.1.3.tar.gz |
|---|---|
| Size | 13.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3f1cd113e2e8d96c2b32fb98a277360db9f96b7209323ba889ae42fec94f9f71
|
|
BLAKE2b-256 checksum How to use checksums |
20bf8e3a74aa5277c394b37b80261159ddacd9eefbc93e138f9f60860489833a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / viyog-0.1.3-py3-none-any.whl
| Download URL | viyog-0.1.3-py3-none-any.whl |
|---|---|
| Size | 11.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cbc98c022e8c0a50a3d6482e6eb2df254191a5d0f02b41a0ebc46d46036aafcc
|
|
BLAKE2b-256 checksum How to use checksums |
b9b9907a902dbdf0adaf7077c9ebb7b44386b73e7e59ecba91bae97999681956
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for viyog 0.1.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.