.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples\anomaly_detection\PADI_cnn.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_anomaly_detection_PADI_cnn.py: Post-Anomaly Detection Inference for Deep SVDD (Image Data) =========================================================== This example shows how to perform selective inference for Deep Support Vector Data Description (Deep SVDD) on image patches using a Convolutional Neural Network (CNN) encoder and the `pythonsi` library. The method computes statistically valid p-values to control the False Positive Rate (FPR) in a post-hoc manner. The implementation is based on the work by Thanh et al. (2026) [1]. [1] Thanh, C. L. C., Vinh, D. Q., & Duy, V. N. L. (2026). Post-Anomaly Detection Inference for Deep SVDD. arXiv preprint arXiv:2609.37935. .. GENERATED FROM PYTHON SOURCE LINES 8-32 .. code-block:: Python import sys import os from pathlib import Path # Add pythonsi to module search path # REPO = Path(__file__).resolve().parent # while REPO.name and not (REPO / "pythonsi").exists(): # REPO = REPO.parent # sys.path.insert(0, str(REPO)) import numpy as np import torch import matplotlib.pyplot as plt from pythonsi import Data, Pipeline from pythonsi.anomaly_detection import DeepSVDDAD from pythonsi.test_statistics import DeepSVDDTestStatistic # Add cnn directory to path to import network # sys.path.insert(0, str(Path(__file__).resolve().parent / "deepsvdd" / "cnn")) from models.deepsvdd.cnn.network import PatchNetwork .. GENERATED FROM PYTHON SOURCE LINES 33-35 Configuration ------------- .. GENERATED FROM PYTHON SOURCE LINES 35-48 .. code-block:: Python SEED = 0 PATCH_SZ = 15 IMG_SIZE = (300, 300) ALPHA = 0.05 N_REFS = 50 # number of reference (normal) patches PIXEL_VAR = 1.0 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" torch.manual_seed(SEED) np.random.seed(SEED) .. GENERATED FROM PYTHON SOURCE LINES 49-51 Load Trained Model ------------------ .. GENERATED FROM PYTHON SOURCE LINES 51-72 .. code-block:: Python # script_dir = Path(__file__).resolve().parent ckpt_path = "./models/deepsvdd/cnn/weights/patch_network.pth" ckpt = torch.load(ckpt_path, map_location=DEVICE, weights_only=False) cfg = ckpt["config"] model = PatchNetwork( in_channels=cfg["in_channels"], img_size=cfg["img_size"], repdim=cfg["repdim"], channels=cfg["channels"], ).to(DEVICE) model.load_state_dict(ckpt["model_state_dict"]) model.eval() # Force model to float64 for precise selective inference calculation model = model.double() center_c = ckpt["center_c"].astype(np.float64) R_squared = ckpt["R_squared"] * 0.6 # Shrink radius by 20% to easily trigger anomaly .. GENERATED FROM PYTHON SOURCE LINES 73-75 Generate Test & Reference Data ------------------------------ .. GENERATED FROM PYTHON SOURCE LINES 75-111 .. code-block:: Python def generate_normal_images(n_samples, img_size, channels=1): h, w = img_size imgs = np.random.normal(loc=0.0, scale=PIXEL_VAR, size=(n_samples, channels, h, w)) return torch.from_numpy(imgs.astype(np.float32)) def generate_anomaly_images(n_samples, img_size, delta, channels=1): h, w = img_size imgs = np.random.normal( loc=0.0 + delta, scale=PIXEL_VAR, size=(n_samples, channels, h, w) ) return torch.from_numpy(imgs.astype(np.float32)) def extract_patches(img_tensor, patch_size, stride): if img_tensor.dim() == 3: img_tensor = img_tensor.unsqueeze(0) patches = img_tensor.unfold(2, patch_size, stride).unfold(3, patch_size, stride) B, C, n_h, n_w, pH, pW = patches.shape return patches.contiguous().view(B * n_h * n_w, C, pH, pW) # Reference patches (from normal images) ref_images = generate_normal_images(1, IMG_SIZE) ref_patches = extract_patches(ref_images, PATCH_SZ, PATCH_SZ) ref_idx = np.random.choice(len(ref_patches), N_REFS, replace=False) ref_patches = ref_patches[ref_idx] print(f"Reference patches: {ref_patches.shape}") # Test patch (anomalous) test_img = generate_anomaly_images(1, IMG_SIZE, delta=0) test_patches = extract_patches(test_img, PATCH_SZ, PATCH_SZ) test_patch = test_patches[0:1] print(f"Test patch: {test_patch.shape}") d = PATCH_SZ * PATCH_SZ img_shape = (1, PATCH_SZ, PATCH_SZ) .. rst-class:: sphx-glr-script-out .. code-block:: none Reference patches: torch.Size([50, 1, 15, 15]) Test patch: torch.Size([1, 1, 15, 15]) .. GENERATED FROM PYTHON SOURCE LINES 112-114 Check Anomaly ------------- .. GENERATED FROM PYTHON SOURCE LINES 114-128 .. code-block:: Python c_tensor = torch.tensor(center_c, device=DEVICE, dtype=torch.float64) with torch.no_grad(): feat = model(test_patch.to(DEVICE).double()) score = torch.sum((feat - c_tensor) ** 2).item() print(f"Score: {score:.10f}") print(f"R²: {R_squared:.10f}") print(f"Anomaly detected: {score > R_squared}") if score <= R_squared: print("\n⚠️ Test patch NOT detected as anomaly — try increasing delta.") .. rst-class:: sphx-glr-script-out .. code-block:: none Score: 0.0081811020 R²: 0.0041076397 Anomaly detected: True .. GENERATED FROM PYTHON SOURCE LINES 129-131 Build Pipeline & Compute Selective p-value ------------------------------------------ .. GENERATED FROM PYTHON SOURCE LINES 131-171 .. code-block:: Python # Flatten patches for Pipeline (d-dimensional vectors) X_test_np = test_patch.reshape(1, -1).numpy().astype(np.float64) X_refs_np = ref_patches.reshape(N_REFS, -1).numpy().astype(np.float64) # Covariance matrix sigma = PIXEL_VAR * np.eye(d) test_node = Data() refs_node = Data() detector = DeepSVDDAD( model=model, R_squared=R_squared, center=center_c, img_shape=img_shape, device="cpu", # SI inference runs on CPU network_type="cnn", ) anomaly_node = detector.run(test_node) pipeline = Pipeline( inputs=(test_node, refs_node), output=anomaly_node, test_statistic=DeepSVDDTestStatistic(test_node, refs_node), ) print("Running selective inference …") anomalies, p_values = pipeline( inputs=[X_test_np, X_refs_np], covariances=[sigma], verbose=False, ) print(f"\nDetected anomalies: {anomalies}") print(f"Selective p-values: {p_values}") if len(p_values) > 0 and p_values[0] is not None: print(f"Reject H₀ at α={ALPHA}? {'YES' if p_values[0] < ALPHA else 'NO'}") .. rst-class:: sphx-glr-script-out .. code-block:: none Running selective inference … Detected anomalies: [0] Selective p-values: [0.6284283601687561] Reject H₀ at α=0.05? NO .. GENERATED FROM PYTHON SOURCE LINES 172-174 Plot the p-values ----------------- .. GENERATED FROM PYTHON SOURCE LINES 174-183 .. code-block:: Python plt.figure(figsize=(6, 4)) plt.bar([str(anomaly) for anomaly in anomalies], p_values, color='skyblue') plt.axhline(y=ALPHA, color='red', linestyle='--', label=f'Alpha ({ALPHA})') plt.xlabel("Anomalies index") plt.ylabel("P-value") plt.title("Selective P-values for Detected Anomalies") plt.legend() plt.tight_layout() plt.show() .. image-sg:: /auto_examples/anomaly_detection/images/sphx_glr_PADI_cnn_001.png :alt: Selective P-values for Detected Anomalies :srcset: /auto_examples/anomaly_detection/images/sphx_glr_PADI_cnn_001.png :class: sphx-glr-single-img .. _sphx_glr_download_auto_examples_anomaly_detection_PADI_cnn.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: PADI_cnn.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: PADI_cnn.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: PADI_cnn.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_