-
Notifications
You must be signed in to change notification settings - Fork 662
Add cuml.accel support for sklearn.ensemble.IsolationForest #8477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1efbf48
0d3d714
e326371
909c968
c4d8376
f31863c
6dcc1af
402f6f2
b0c2d2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| from sklearn.datasets import make_blobs | ||
| from sklearn.ensemble import IsolationForest | ||
|
|
||
| CPUIsolationForest = IsolationForest._cpu_class | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def blobs_with_outliers(): | ||
| X, _ = make_blobs( | ||
| n_samples=200, | ||
| centers=1, | ||
| cluster_std=0.5, | ||
| random_state=42, | ||
| ) | ||
| rng = np.random.RandomState(42) | ||
| outliers = rng.uniform(low=-10, high=10, size=(20, X.shape[1])) | ||
| return np.vstack([X, outliers]) | ||
|
|
||
|
|
||
| def test_isolation_forest_fit_predict_agreement(blobs_with_outliers): | ||
| X = blobs_with_outliers | ||
| params = {"n_estimators": 100, "random_state": 0} | ||
|
|
||
| expected = CPUIsolationForest(**params).fit(X) | ||
| result = IsolationForest(**params).fit(X) | ||
|
|
||
| assert result._gpu is not None | ||
|
|
||
| expected_labels = expected.predict(X) | ||
| result_labels = result.predict(X) | ||
| assert set(np.unique(result_labels)) <= {-1, 1} | ||
| assert np.mean(expected_labels == result_labels) >= 0.9 | ||
|
|
||
|
|
||
| def test_isolation_forest_fit_sample_weight_falls_back_to_cpu( | ||
| blobs_with_outliers, | ||
| ): | ||
| # cuml.ensemble.IsolationForest.fit() has no sample_weight parameter, | ||
| # so a non-None sample_weight cannot be honored on GPU. | ||
| X = blobs_with_outliers | ||
| sample_weight = np.ones(len(X)) | ||
|
|
||
| result = IsolationForest(n_estimators=10, random_state=0).fit( | ||
| X, sample_weight=sample_weight | ||
| ) | ||
|
|
||
| assert result._gpu is None | ||
|
|
||
|
|
||
| def test_isolation_forest_fit_predict_sample_weight_falls_back_to_cpu( | ||
| blobs_with_outliers, | ||
| ): | ||
| # Same as above, for fit_predict(). | ||
| X = blobs_with_outliers | ||
| sample_weight = np.ones(len(X)) | ||
|
|
||
| result = IsolationForest(n_estimators=10, random_state=0) | ||
| result.fit_predict(X, sample_weight=sample_weight) | ||
|
|
||
| assert result._gpu is None | ||
|
|
||
|
|
||
| def test_isolation_forest_gpu_fit_attrs_available_after_conversion( | ||
| blobs_with_outliers, | ||
| ): | ||
| # Regression test: conversion of a fitted GPU IsolationForest back to a | ||
| # CPU estimator is now supported (landed in #8483, tracked by #8420). | ||
| # Accessing fit attributes on a GPU-fitted proxy must trigger that | ||
| # conversion and expose the real synced values instead of raising. | ||
|
Comment on lines
+71
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) I don't think this in-line comment is adding valuable context.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any changes. Did you forget to push? Either way, not a big deal. I'm just not a fan of overly verbose comments that seem to explain code evolution instead of just providing necessary context. |
||
| X = blobs_with_outliers | ||
| result = IsolationForest(n_estimators=50, random_state=0).fit(X) | ||
| assert result._gpu is not None | ||
|
|
||
| gpu_scores = result.decision_function(X) # dispatched to GPU | ||
|
|
||
| assert len(result.estimators_) == 50 | ||
| assert result.offset_ == pytest.approx(float(result._gpu.offset_)) | ||
|
|
||
| np.testing.assert_allclose( | ||
| result._cpu.decision_function(X), gpu_scores, atol=1e-5 | ||
| ) | ||