Kernel Contraction Matching (KCM)
Anomaly detection with no optimizer, no epoch axis, and no collapse. A drop-in scikit-learn estimator that ranks first by mean AUROC across the 47 ADBench datasets while training nothing.
Problem • Install • Quickstart • Results • API • Paper
Reference implementation for Mitigating Convergence Collapse in Fixed-Target Anomaly Detectors via Kernel-Anchored Locality Regularization, CIKM 2026.
Install
pip install kar-kcm # KCM only, no PyTorch
pip install "kar-kcm[kar]" # adds KAR, which needs PyTorch
From source, before the first release lands on PyPI:
pip install git+https://github.com/jose-melo/kernel-contraction-matching
The problem: fixed-target detectors get worse as they train
A fixed-target detector trains a network to match a target that carries no information about how far a point lies from the data: a constant centre (Deep SVDD), the input itself (autoencoders), a frozen random network (RDP), a noise endpoint (flow-matching). The anomaly score is the residual that is left over.
As training converges that residual goes to zero everywhere the model can reach, anomalies included. The score field flattens, and detection decays while the training loss keeps falling.
The score field is sharp early and flat late; the anomalies (stars) stop being separated from the
normal cluster (dots). Nine detectors on ADBench cardio all reach an early peak and then decay
toward convergence. Early stopping does not rescue this: the best epoch moves by two orders of
magnitude across seeds, and the loss you would monitor decreases monotonically the whole time.
What KCM does
Replace the trained map with the kernel smoother a wide network approaches in its lazy regime. For
a query z and training set X, KCM predicts the kernel-weighted average of the training points
and scores the residual:
NW_h(z) = sum_i k_h(z, x_i) x_i / sum_i k_h(z, x_i) Nadaraya-Watson reconstruction
score(z) = || z - NW_h(z) || higher means more anomalous
Every training point's influence decays with distance, so as z leaves the data the prediction
levels off and the residual grows. The bandwidth h is chosen by leave-one-out reconstruction
error over a 21-point grid. There is no optimizer, no epoch axis, and nothing to early-stop.
Quickstart
from sklearn.datasets import make_blobs
from karkcm import make_kcm_pipeline
X_train, _ = make_blobs(n_samples=500, centers=1, random_state=0)
X_test, _ = make_blobs(n_samples=20, centers=[[8, 8]], random_state=0)
detector = make_kcm_pipeline().fit(X_train)
detector.predict(X_test) # -1 outlier, +1 inlier
detector.anomaly_score(X_test) # higher means more anomalous
make_kcm_pipeline() is StandardScaler plus KCM. KCM uses one isotropic bandwidth for every
feature, so scaled input is what you want; the pipeline handles it and forwards KCM's own methods.
Results
Across the 47 ADBench datasets, against the 44 baselines ADBench ships:
| mean AUROC | mean rank | neural training | |
|---|---|---|---|
| KCM | 0.8710 | 7.06 | none |
| DTE-NP | 0.8652 | 6.81 | yes |
| LUNAR | 0.8603 | 8.62 | yes |
| KDE | 0.8452 | 10.49 | none |
KCM sits in the leading group of the critical-difference diagram, with no trained parameters.
Fit is a bandwidth search and a matrix product: median 0.40 s per dataset on one CPU core, where the neural detectors sit one to two orders of magnitude higher.
Using the estimator
KCM follows the scikit-learn outlier-detector contract, so it clones, pickles, and drops into
Pipeline and GridSearchCV.
from karkcm import KCM
KCM(bandwidth="loo", n_grid=21, trim_frac=0.0, max_reference=2000,
contamination=0.1, chunk_size=4096, assume_scaled=False,
novelty=True, random_state=None)
| method | returns |
|---|---|
anomaly_score(X) |
||z - NW(z)||, higher means more anomalous |
score_samples(X) |
-anomaly_score(X), the scikit-learn orientation |
decision_function(X) |
score_samples(X) - offset_, negative means outlier |
predict(X) |
-1 outlier, +1 inlier |
reconstruct(X) |
the Nadaraya-Watson reconstruction, per feature |
kernel_mass(X) |
total kernel weight a query receives |
reconstruct is the explanation channel: it returns what the model considers a normal version of
each row, so the per-feature difference tells you why a point scored the way it did.
Two modes, following LocalOutlierFactor. novelty=True (the default) scores rows the model
was not fitted on, which is the setting the paper uses. novelty=False exposes fit_predict for
labelling the rows you fit on, using leave-one-out residuals so that no row is scored against
itself.
detector = KCM().fit(X_train) # novelty=True: score new data
labels = KCM(novelty=False).fit_predict(X) # label X itself
anomaly_auroc and anomaly_auprc are scorers with the sign already handled, for use with
GridSearchCV.
KAR, for a trainable backbone
When a trained network is a requirement rather than a choice, KAR keeps one and constrains it to
stay within a fixed radius rho of the KCM anchor, in KCM-residual units. That floor is what
removes the collapse: rho = 0.5 is used unchanged on every dataset in the paper.
KAR needs PyTorch, which KCM does not: pip install "kar-kcm[kar]".
Reproducing the paper
pip install -e ".[paper]"
python scripts/download_datasets.py
python -m karkcm.experiments.kcm_benchmark # Sec. 5, KCM over 47 datasets
python -m karkcm.experiments.collapse_gap # Sec. 3, peak-to-final gap
python -m karkcm.experiments.backbone # Sec. 6, backbone independence
python -m karkcm.experiments.aggregate # per-section summary tables
python -m karkcm.experiments.kar_ablation --dataset 6_cardio --rho 0.5
results/ holds the per-run JSON and CSV records behind the paper's tables and figures, and
results/figures/ the figures themselves. The experiment modules will not overwrite it: point
KARKCM_RESULTS at a directory of your own, or pass --overwrite deliberately.
KCMAnchor in karkcm/kcm.py is the frozen object the paper's experiments call. KCM is the
estimator-shaped front door onto the same arithmetic, and tests/test_estimator_parity.py locks
the two together.
The 47 ADBench .npz files are fetched from ADBench and are
not redistributed here.
Layout
karkcm/
kcm.py the kernel, the bandwidth search, the frozen anchor
estimator.py the scikit-learn estimator, pipeline and scorers
kar.py bounded correction and the KAR training loop
nets.py MLP, time-conditioned MLP, autoencoder
baselines.py the six fixed-target detectors of Sec. 3
data.py ADBench loading and the semi-supervised split
experiments/ one runnable module per paper artifact
benchmarks/ KCM against scikit-learn's detectors
examples/ quickstart, pipeline and grid search, diagnostics
results/ the per-run records behind every table and figure
tests/
Citing
@inproceedings{demelocosta2026kar,
author = {De Melo Costa, José Lucas and Popineau, Fabrice and
Rimmel, Arpad and Doan, Bich-Liên},
title = {Mitigating Convergence Collapse in Fixed-Target Anomaly Detectors
via Kernel-Anchored Locality Regularization},
booktitle = {Proceedings of the 35th ACM International Conference on Information
and Knowledge Management (CIKM '26)},
year = {2026},
doi = {10.1145/3799682.3841143}
}
License
MIT, see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kar_kcm-1.0.0.tar.gz.
File metadata
- Download URL: kar_kcm-1.0.0.tar.gz
- Upload date:
- Size: 82.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee1de1dbe784d4ad89a94cb14fa56bea1a2d0c6bc220959c094f4fa59d266ca4
|
|
| MD5 |
24f88f32ceab16b6e88d6cd0c5ca3c29
|
|
| BLAKE2b-256 |
c99a236ad84625b4eea6b721a2197027207545fc263f26fd8f3523abb9537acb
|
Provenance
The following attestation bundles were made for kar_kcm-1.0.0.tar.gz:
Publisher:
publish.yml on jose-melo/kernel-contraction-matching
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kar_kcm-1.0.0.tar.gz -
Subject digest:
ee1de1dbe784d4ad89a94cb14fa56bea1a2d0c6bc220959c094f4fa59d266ca4 - Sigstore transparency entry: 2554877064
- Sigstore integration time:
-
Permalink:
jose-melo/kernel-contraction-matching@c4d49ac16272d2131fde5687f2ab439402e35e2c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jose-melo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4d49ac16272d2131fde5687f2ab439402e35e2c -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file kar_kcm-1.0.0-py3-none-any.whl.
File metadata
- Download URL: kar_kcm-1.0.0-py3-none-any.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b74dc85e933a8753cd17708bb9a993c3e1932025318759b5d63ee601031a3d8
|
|
| MD5 |
e43dfb5008519cc6266aff7ce7369a7c
|
|
| BLAKE2b-256 |
8a95eaa7b2f8b3f9e0ce5c8981398827d271cb086865c7d07ab4e3a57eeb8a6e
|
Provenance
The following attestation bundles were made for kar_kcm-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on jose-melo/kernel-contraction-matching
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
kar_kcm-1.0.0-py3-none-any.whl -
Subject digest:
6b74dc85e933a8753cd17708bb9a993c3e1932025318759b5d63ee601031a3d8 - Sigstore transparency entry: 2554877181
- Sigstore integration time:
-
Permalink:
jose-melo/kernel-contraction-matching@c4d49ac16272d2131fde5687f2ab439402e35e2c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jose-melo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4d49ac16272d2131fde5687f2ab439402e35e2c -
Trigger Event:
workflow_dispatch
-
Statement type: