AdverScan / adverscan /api /main.py
charanyellanki's picture
initial AdverScan implementation — adversarial example detector with threshold analysis
b95a555
Raw
History Blame Contribute Delete
3.58 kB
"""
REST API for adversarial-vs-clean detection on CIFAR-style tensors.
Serve with::
uvicorn adverscan.api.main:app --reload
"""
from __future__ import annotations
import os
from functools import lru_cache
import numpy as np
import torch
import torch.nn as nn
from fastapi import FastAPI
from numpy.typing import NDArray
from pydantic import BaseModel, Field
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from adverscan.attacks import build_pretrained_cifar10_resnet18
from adverscan.detector.feature_extractor import FEATURE_DIM, assemble_extracted_features
from adverscan.detector.model import AdversarialDetector
app = FastAPI(title="AdverScan Detector API", version="0.1.0")
class InferenceTensor(BaseModel):
pixels: list[float]
shape: tuple[int, int, int]
class InferenceRequest(BaseModel):
tensor: InferenceTensor
class DetectionResponse(BaseModel):
adversarial_probability: float = Field(ge=0.0, le=1.0)
feature_dim: int
softmax_entropy: float
softmax_margin: float
gradient_l2: float
prediction_consistency: float
def _bootstrap_pipeline() -> Pipeline:
rng = np.random.default_rng(seed=4242)
phantom_x = rng.normal(size=(256, FEATURE_DIM)).astype(np.float32)
phantom_y = rng.integers(low=0, high=2, size=phantom_x.shape[0], dtype=np.int64)
pipe = Pipeline(
steps=[("scale", StandardScaler()), ("lr", LogisticRegression(max_iter=4000, class_weight="balanced", random_state=13))]
)
pipe.fit(phantom_x, phantom_y)
return pipe
@lru_cache(maxsize=1)
def detector_bundle() -> AdversarialDetector:
artifact = os.getenv("ADVERSCAN_DETECTOR_ARTIFACT", "artifacts/detector.joblib")
if os.path.isfile(artifact):
return AdversarialDetector.load(artifact)
clf = _bootstrap_pipeline()
detector_holder = AdversarialDetector(backend="logistic_regression", pipeline=clf, val_metrics={}, train_metrics={})
os.makedirs(os.path.dirname(artifact) or ".", exist_ok=True)
detector_holder.save(artifact)
return detector_holder
@lru_cache(maxsize=1)
def cached_victim() -> nn.Module:
victim_module, succeeded = build_pretrained_cifar10_resnet18()
if not succeeded:
import warnings
warnings.warn("Continuing with randomly initialized ResNet-18 stub (no pretrained CIFAR-10 weights resolved).")
return victim_module.eval()
@app.post("/predict", response_model=DetectionResponse)
def predict(req: InferenceRequest) -> DetectionResponse:
accelerator = torch.device("cuda" if torch.cuda.is_available() else "cpu")
stacked_tensor = torch.tensor(req.tensor.pixels, dtype=torch.float32, device=accelerator).view(*req.tensor.shape)
victim_net_nn = cached_victim().to(accelerator)
feats_torch_stack = assemble_extracted_features(victim_net_nn, stacked_tensor.unsqueeze(0))
feats_row_np: NDArray[np.floating] = feats_torch_stack.detach().cpu().numpy()[0]
probs_scalar_vector = detector_bundle().predict_adversarial_score(np.expand_dims(feats_row_np, axis=0))
det_prob_f = float(probs_scalar_vector[0])
return DetectionResponse(
adversarial_probability=det_prob_f,
feature_dim=FEATURE_DIM,
softmax_entropy=float(feats_row_np[0]),
softmax_margin=float(feats_row_np[1]),
gradient_l2=float(feats_row_np[2]),
prediction_consistency=float(feats_row_np[3]),
)
@app.get("/healthz")
def healthz() -> dict[str, str]:
return {"status": "ok"}