PyOD 3.6.3
A maintenance release. The headline item is a DeepSVDD defect that produced silently meaningless results.
DeepSVDD returned constant anomaly scores
In 3.6.0 through 3.6.2, DeepSVDD did not train: loss.backward() was commented out, so no gradient ever reached the weights and scores came from the randomly initialized network. Restoring the backward pass (#704, thanks @DMZ22) then exposed three defects it had been masking, the most serious being that fit() set the hypersphere center to 0.0 while the initialized center was written somewhere the objective never read.
For a bias-free ReLU network the all-zero weights map every input to the origin, so a zero center is exactly the trivial solution of Deep SVDD (Ruff et al., ICML 2018, Proposition 1). Training converged to a collapsed hypersphere. On a 17-dataset ODDS benchmark the released behavior returned a single distinct score on 10 of 17 datasets while still clearing a ROC floor, because ordering can be induced by floating-point noise as small as 1e-24. The detector was useless and said nothing about it.
Fixed in this release:
- The fitted center is initialized from the data, detached, and stored in the new
c_attribute that the objective and scorer actually read. - The optimizer was built with no learning rate (Adam's 1e-3 default) and passed
l2_regularizer=0.1as weight decay, five orders of magnitude above the reference implementation's 5e-7. That decay drives weights toward zero and compounds the collapse. - The best-epoch snapshot was stored but never loaded, so scoring used final-epoch weights.
Measured effect: mean ROC AUC across the 17 datasets rises from 0.601 to 0.748, with score collapse eliminated on all 17.
Calibration, stated plainly: 0.748 is level with the 0.746 obtained by the untrained network. This release restores DeepSVDD to its baseline rather than improving on it. Deep SVDD assumes a clean one-class training set while PyOD fits it unsupervised on contaminated data; in a controlled comparison, training on genuinely normal samples only reaches 0.879. The remaining gap largely reflects that assumption mismatch, and the limitation is now documented in the class docstring.
Breaking changes (DeepSVDD only)
| Change | Impact |
|---|---|
l2_regularizer default 0.1 -> 5e-7 |
Matches the reference implementation. Changes behavior for every existing caller. |
Fitted center moved from c to c_ |
c is now configuration only and is no longer overwritten by fit(). Code reading clf.c for the fitted value should read clf.c_. |
c=0.0 now raises ValueError |
It is the trivial-solution condition. Pass c=None to initialize from data, or a non-zero center. |
New learning_rate parameter (default 1e-4) |
Additive; no action needed. |
c is also validated now: a scalar is expanded to the network output width, a vector must match that width, non-finite values are rejected, and the tensor is copied so a later mutation of your array cannot change the fitted estimator.
Other fixes
- Local encoder paths fall back to the HuggingFace backend when sentence-transformers is absent (#701, thanks @sunnyguntuka). A
pyod[huggingface]-only install previously raisedImportErrorbefore the existing fallback could run. test_resolve_st_instance_no_downloadno longer breaks on sentence-transformers 5.6, which rejects the emptymodules=[]construction the test relied on (#710).
Testing and infrastructure
- save/load round-trip coverage now spans 23 detectors, up from 2, and 21 of them gain clone assertions for unfitted equivalence and refit score reproduction (#708, thanks @JayeshSuryavanshi, closes #269). The per-detector
test_model_clonemethods previously only checked thatclone()did not raise. - Seven DeepSVDD regression tests, each verified to fail on the pre-fix code: score diversity on both the standard and autoencoder paths (the collapse detector), center validity, the sklearn contract for
c/get_params/clone, refit re-initialization, all-zero rejection, and custom-center validation. - pyod.dev website badge, brand assets, and a
SECURITY.mdvulnerability-reporting policy (#705).
Reviewed via /implement-review with Codex across five rounds.