Skip to content

Add Deep SAD (Deep Semi-Supervised Anomaly Detection) detector - #716

Open
JayeshSuryavanshi wants to merge 9 commits into
yzhao062:developmentfrom
JayeshSuryavanshi:feat/deep-sad
Open

JayeshSuryavanshi wants to merge 9 commits into
yzhao062:developmentfrom
JayeshSuryavanshi:feat/deep-sad

Conversation

@JayeshSuryavanshi

Copy link
Copy Markdown
Contributor

Add Deep SAD (Deep Semi-Supervised Anomaly Detection)

Closes #709.

This adds DeepSAD, the semi-supervised extension of the existing DeepSVDD, from Ruff et al., Deep Semi-Supervised Anomaly Detection, ICLR 2020.

Motivation

PyOD ships DeepSVDD but not its widely-cited semi-supervised sibling. In fraud/risk and many real-world anomaly settings a small number of confirmed anomalies is available, and Deep SAD is the canonical way to use those labels: it keeps the Deep SVDD hypersphere objective for unlabeled data while pulling labeled anomalies away from the center, which measurably improves detection over the purely-unsupervised model.

What it does

A network maps inputs into a representation space where inliers are pulled toward a fixed hypersphere center c and labeled anomalies are pushed away from it, via the Deep SAD objective:

  • unlabeled samples use the Deep SVDD term, minimizing the squared distance to c;
  • labeled anomalies use the inverse-distance term (dist)^(-1), so their loss grows as they approach the center.

The distance to c is the anomaly score at inference. The semi-supervised interface is a single optional y in fit(X, y=None) where y == 1 marks a known anomaly and everything else is unlabeled; y=None recovers unsupervised Deep SVDD, so it drops in wherever DeepSVDD is used.

Implementation notes

  • Follows PyOD conventions and mirrors pyod/models/deep_svdd.py (same BaseDetector contract, standardization, center initialization from a first forward pass, decision_function returning the distance-to-center score).
  • The objective and center initialization are adapted from the authors' reference implementation (lukasruff/Deep-SAD-PyTorch, MIT) and DeepOD's dsad.py (BSD-3); neither is copied verbatim and both are attributed in the module header.

Included

  • pyod/models/deep_sad.py — the detector
  • pyod/test/test_deep_sad.py — 15 tests mirroring the DeepSVDD test suite (parameters, scores, labels, proba, confidence, rejection, fit_predict, clone), all passing locally
  • examples/deep_sad_example.py
  • README.rst model table entry + docs/zreferences.bib citation

Scope

This is the core detector plus docs/tests. I deliberately did not add DeepSAD to the ad_engine knowledge base (algorithms.json, benchmarks.json, routing rules, per-model analysis) because those entries carry curated metadata and benchmark numbers I don't want to fabricate; the skill/KB consistency checks pass as-is. Happy to add a proper KB entry as a follow-up if you'd like, or if you can point me at the benchmark source.

Implement DeepSAD (Ruff et al., ICLR 2020), the semi-supervised
extension of Deep SVDD, as a new PyOD BaseDetector. Unlabeled/normal
samples use the unsupervised Deep SVDD objective (minimize squared
distance to the hypersphere center c) while labeled anomalies use the
inverse-distance term (eta * dist**-1), pushing them away from c.
fit(X, y=None) treats y==1 as known anomalies and everything else as
unlabeled; y=None recovers pure Deep SVDD behavior. Mirrors the
structure and API of pyod/models/deep_svdd.py.

- pyod/models/deep_sad.py: DeepSAD + InnerDeepSAD encoder
- pyod/test/test_deep_sad.py: mirrors test_deepsvdd (semi-supervised fit)
- examples/deep_sad_example.py: semi-supervised usage example
- README.rst: model-table row + reference
- docs/zreferences.bib: ruff2020deepsad citation

Loss/architecture adapted from lukasruff/Deep-SAD-PyTorch (MIT) and
DeepOD's dsad.py (BSD-3); written clean in PyOD style.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 11f0649173

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py
Comment on lines +342 to +343
optimizer = optimizer_dict[self.optimizer](
self.model_.parameters(), weight_decay=self.l2_regularizer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reduce the default optimizer decay

With the default l2_regularizer=0.1, this sends weight_decay=0.1 into the optimizer for every default fit. In the default bias-free encoder, that decay aggressively pulls weights toward zero and can collapse the learned representation and scores even when callers do not tune anything; use a reference-scale weight decay, or separate activity regularization from optimizer decay, instead of training DeepSAD with this default.

Useful? React with 👍 / 👎.

Comment thread pyod/models/deep_sad.py Outdated
Comment on lines +251 to +252
if self.random_state is not None:
torch.manual_seed(self.random_state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed DeepSAD during fit

Seeding the global torch RNG in __init__ makes reproducibility depend on what happens between construction and fit: if two DeepSAD(random_state=...) instances or sklearn clones are created before either is fitted, fitting the first advances the global RNG before the second builds its network/DataLoader, so they can initialize differently despite the same seed. Apply the seed inside fit or use a per-fit generator so the documented random_state actually makes training repeatable.

Useful? React with 👍 / 👎.

Per the automated review on the PR:
- Remove the hidden_neurons > n_features check. Deep SAD uses only an encoder
  (no autoencoder branch like Deep SVDD), so hidden layers may legitimately be
  wider than the input dimension; the check wrongly rejected valid setups on
  datasets with fewer features than the default hidden width.
- Validate a user-supplied c: it must match the representation dimension
  (hidden_neurons[-1]) and be finite, instead of silently broadcasting a
  scalar or a mismatched vector.

Add tests for both.
@JayeshSuryavanshi

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I addressed the two Deep-SAD-specific points in feb561a:

  • Encoder width: removed the hidden_neurons > n_features check. Deep SAD uses only the encoder (no autoencoder branch like Deep SVDD), so wider hidden layers are valid; the check wrongly rejected datasets with fewer features than the default hidden width. Added a test that fits on 8-feature data with the default [64, 32].
  • Center validation: a user-supplied c is now validated against the representation dimension (hidden_neurons[-1]) and for finiteness, so a scalar or mismatched vector raises a clear error instead of silently broadcasting. Added tests for both the rejection and the valid path.

I intentionally left the other two suggestions (the l2_regularizer default weight decay, and seeding torch in __init__ vs fit): both are inherited directly from the sibling DeepSVDD, and I'd rather keep Deep SAD consistent with it than diverge unilaterally here. Happy to change either if you'd prefer.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: feb561aad8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py
Comment thread pyod/models/deep_sad.py Outdated
…seed torch from a RandomState, drop unsupported optimizers

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 148f05fe69

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/test/test_deep_sad.py
Comment thread pyod/models/deep_sad.py
Comment thread README.rst
Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/test/test_deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py
…pply dropout after every hidden activation, require positive eta, add DeepSAD to the docs, skip the tests without torch, train the test fixtures once

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b1c56c8cf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py Outdated
Comment thread pyod/models/deep_sad.py
Comment thread pyod/models/deep_sad.py
…n the training fold, weight the epoch loss by batch size, validate eps, handle a single-width network

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61e36bee54

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/models/deep_sad.py
Comment on lines +344 to +345
n_val = min(int(round(len(idx) * self.validation_size)),
len(idx) - 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include scarce anomaly labels in checkpoint selection

When the labeled-anomaly class is small, this rounding assigns none of it to validation—for example, the default validation_size=0.1 gives n_val == 0 for five or fewer labeled anomalies. The validation fold can still contain unlabeled samples, so checkpoint selection then silently optimizes only the hypersphere-compaction term and may restore an epoch that lost the supervised anomaly separation. Preserve the anomaly component in the selection loss, such as by combining training-anomaly loss with validation-unlabeled loss, while still keeping lone anomalies in training.

Useful? React with 👍 / 👎.

Comment thread pyod/models/deep_sad.py
torch.manual_seed(torch_seed)
check_parameter(dropout_rate, 0, 1, param_name='dropout_rate',
include_left=True)
check_parameter(eta, 0, np.inf, param_name='eta')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nonfinite eta values

When eta=np.nan, check_parameter accepts it because every ordered comparison with NaN is false. If any labeled anomaly is present, the inverse-distance term becomes NaN, optimizer updates can corrupt the weights, and no epoch can satisfy selection_loss < best_loss, leaving users with nonfinite scores and no restored checkpoint. Add an explicit finiteness check for eta, as is already done for eps.

Useful? React with 👍 / 👎.

Comment thread pyod/models/deep_sad.py
optimizer = optimizer_dict[self.optimizer](
self.model_.parameters(), weight_decay=self.l2_regularizer)

for epoch in range(self.epochs):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nonpositive epoch counts

When epochs is zero or negative, this loop is skipped without an error, best_model_dict remains None, and fit still returns a seemingly fitted detector whose scores and threshold come from the randomly initialized network. Validate that epochs is a positive integer so a configuration error cannot silently produce untrained experimental results.

Useful? React with 👍 / 👎.

Comment thread pyod/models/deep_sad.py
Comment on lines +324 to +326
if y is not None:
y = np.asarray(y).ravel()
semi_targets[y == 1] = -1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject labels outside the documented binary encoding

When y contains values other than the documented 0/1 encoding, they are silently treated as unlabeled rather than rejected. In particular, passing sklearn-style outlier labels (1 for inliers and -1 for outliers) marks every inlier as a known anomaly while leaving the actual outliers unlabeled, reversing the supervision and corrupting the fitted detector without an error. Validate that supplied targets are one-dimensional, length-matched, and limited to {0, 1} before this conversion.

Useful? React with 👍 / 👎.

@yzhao062 yzhao062 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for Deep SAD, and sorry this review comes late. Three changes are needed before it can merge:

  1. decision_function needs a fitted-state check. On an unfitted estimator it currently raises AttributeError.
  2. Seed at fit time instead of in the constructor. Constructor seeding changes global Torch state, so two estimators built before either is fitted interfere. Please add a reproducibility test for that case.
  3. Deep SAD is semi-supervised, so it should not be routed to users who have no labels. Please state how it should appear in the model registry and in ADEngine routing.

decision_function now raises NotFittedError on an unfitted estimator instead of an AttributeError. The torch seed moves from __init__ to the start of fit, so building other estimators before fitting no longer changes the result. Scores for an estimator built and fitted right away are unchanged.
…es not route it

ADEngine passes no labels, so Deep SAD would run there as plain Deep SVDD. As an experimental entry it is listed in the knowledge base and by explain_detector, but rule routing, the routing context for LLM agents, and make_plan all skip it. Detector counts move to 62 (44 tabular) and the tabular skill reference is regenerated.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T21:47:42.928207Z aa6da4e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa6da4e997

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread pyod/models/deep_sad.py
Comment on lines +464 to +465
optimizer = optimizer_dict[self.optimizer](
self.model_.parameters(), weight_decay=self.l2_regularizer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose a configurable optimizer learning rate

When a dataset needs a non-default step size, there is no supported way to configure it: this call omits lr and DeepSAD.__init__ has no learning-rate parameter, so each advertised optimizer silently uses a different PyTorch default (for example, Adam uses 1e-3 while Adadelta uses 1.0). This also prevents reproducing the sibling implementation's learning_rate=1e-4, which DeepSVDD explicitly forwards in pyod/models/deep_svdd.py:396-398; add and pass a learning-rate parameter here so experiments can control the optimization scale.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants