Files
cleanlab--cleanlab/tests/test_classification.py
T
2026-07-13 12:49:22 +08:00

992 lines
33 KiB
Python

from copy import deepcopy
import sys
from sklearn.linear_model import LogisticRegression
from sklearn.base import BaseEstimator
from sklearn.model_selection import GridSearchCV
import sklearn
import scipy
import pytest
import numpy as np
import pandas as pd
from cleanlab.classification import CleanLearning
from cleanlab.benchmarking.noise_generation import generate_noise_matrix_from_trace
from cleanlab.benchmarking.noise_generation import generate_noisy_labels
from cleanlab.internal.latent_algebra import compute_inv_noise_matrix
from cleanlab.count import (
compute_confident_joint,
estimate_cv_predicted_probabilities,
get_confident_thresholds,
)
from cleanlab.filter import find_label_issues
SEED = 1
def make_data(
format="numpy",
means=[[3, 2], [7, 7], [0, 8]],
covs=[[[5, -1.5], [-1.5, 1]], [[1, 0.5], [0.5, 4]], [[5, 1], [1, 5]]],
sizes=[100, 50, 50],
avg_trace=0.8,
seed=SEED, # set to None for non-reproducible randomness
):
"""format specifies what X (and y) looks like, one of:
'numpy', 'sparse', 'dataframe', or 'series'.
"""
np.random.seed(seed=seed)
K = len(means) # number of classes
data = []
labels = []
test_data = []
test_labels = []
for idx in range(K):
data.append(np.random.multivariate_normal(mean=means[idx], cov=covs[idx], size=sizes[idx]))
test_data.append(
np.random.multivariate_normal(mean=means[idx], cov=covs[idx], size=sizes[idx])
)
labels.append(np.array([idx for i in range(sizes[idx])]))
test_labels.append(np.array([idx for i in range(sizes[idx])]))
X_train = np.vstack(data)
true_labels_train = np.hstack(labels)
X_test = np.vstack(test_data)
true_labels_test = np.hstack(test_labels)
if format == "sparse":
X_train = scipy.sparse.csr_matrix(X_train)
X_test = scipy.sparse.csr_matrix(X_test)
elif format == "dataframe":
X_train = pd.DataFrame(X_train)
X_test = pd.DataFrame(X_test)
# true_labels_train = list(true_labels_train)
# true_labels_test = list(true_labels_test)
elif format == "series":
X_train = pd.Series(X_train[:, 0])
X_test = pd.Series(X_test[:, 0])
# true_labels_train = pd.Series(true_labels_train)
# true_labels_test = pd.Series(true_labels_test)
elif format != "numpy":
raise ValueError("invalid value specified for: `format`.")
# Compute p(true_label=k)
py = np.bincount(true_labels_train) / float(len(true_labels_train))
noise_matrix = generate_noise_matrix_from_trace(
K,
trace=avg_trace * K,
py=py,
valid_noise_matrix=True,
seed=seed,
)
# Generate our noisy labels using the noise_matrix.
s = generate_noisy_labels(true_labels_train, noise_matrix)
ps = np.bincount(s) / float(len(s))
return {
"X_train": X_train,
"true_labels_train": true_labels_train,
"X_test": X_test,
"true_labels_test": true_labels_test,
"labels": s,
"ps": ps,
"py": py,
"noise_matrix": noise_matrix,
}
def make_rare_label(data):
"""Makes one label really rare in the dataset."""
data = deepcopy(data)
y = data["labels"]
class0_inds = np.where(y == 0)[0]
if len(class0_inds) < 1:
raise ValueError("Class 0 too rare already")
class0_inds_remove = class0_inds[1:]
if len(class0_inds_remove) > 0:
y[class0_inds_remove] = 1
data["labels"] = y
return data
def make_high_dim_data(seed=SEED):
np.random.seed(seed=seed)
X_train = np.random.randint(0, 255, (200, 28, 28))
label_train = np.random.randint(0, 10, 200)
X_test = np.random.randint(0, 255, (50, 28, 28))
label_test = np.random.randint(0, 10, 50)
X_train, X_test = X_train / 255.0, X_test / 255.0
return {
"X_train": X_train,
"labels_train": label_train,
"X_test": X_test,
"labels_test": label_test,
}
DATA = make_data(format="numpy", seed=SEED)
SPARSE_DATA = make_data(format="sparse", seed=SEED)
DATAFRAME_DATA = make_data(format="dataframe", seed=SEED)
SERIES_DATA = make_data(format="series", seed=SEED) # special case not checked in most tests
HIGH_DIM_DATA = make_high_dim_data(seed=SEED)
DATA_FORMATS = {
"numpy": DATA,
"sparse": SPARSE_DATA,
"dataframe": DATAFRAME_DATA,
}
@pytest.mark.parametrize("data", list(DATA_FORMATS.values()))
def test_cl(data):
cl = CleanLearning(clf=LogisticRegression(solver="lbfgs", random_state=SEED))
X_train_og = deepcopy(data["X_train"])
cl.fit(data["X_train"], data["labels"])
score = cl.score(data["X_test"], data["true_labels_test"])
print(score)
# ensure data has not been altered:
if isinstance(X_train_og, np.ndarray):
assert (data["X_train"] == X_train_og).all()
elif isinstance(X_train_og, pd.DataFrame):
assert data["X_train"].equals(X_train_og)
def test_cl_default_clf():
cl = CleanLearning() # default clf is LogisticRegression
X_train_og = deepcopy(HIGH_DIM_DATA["X_train"])
cl.fit(HIGH_DIM_DATA["X_train"], HIGH_DIM_DATA["labels_train"])
# assert result has the correct length
result = cl.predict(HIGH_DIM_DATA["X_test"])
assert len(result) == len(HIGH_DIM_DATA["X_test"])
result = cl.predict(X=HIGH_DIM_DATA["X_test"])
assert len(result) == len(HIGH_DIM_DATA["X_test"])
# assert pred_proba has the right dimensions (N x K),
# where K = 10 (number of classes) as specified in make_high_dim_data()
pred_proba = cl.predict_proba(HIGH_DIM_DATA["X_test"])
assert pred_proba.shape == (len(HIGH_DIM_DATA["X_test"]), 10)
pred_proba = cl.predict_proba(X=HIGH_DIM_DATA["X_test"])
assert pred_proba.shape == (len(HIGH_DIM_DATA["X_test"]), 10)
score = cl.score(HIGH_DIM_DATA["X_test"], HIGH_DIM_DATA["labels_test"])
cl.find_label_issues(HIGH_DIM_DATA["X_train"], HIGH_DIM_DATA["labels_train"])
# ensure data has not been altered:
assert (HIGH_DIM_DATA["X_train"] == X_train_og).all()
@pytest.mark.filterwarnings("ignore::UserWarning")
@pytest.mark.parametrize("data", list(DATA_FORMATS.values()))
def test_rare_label(data):
data = make_rare_label(data)
test_cl(data)
def test_invalid_inputs():
data = make_data(sizes=[1, 1, 1])
try:
test_cl(data)
except Exception as e:
assert "Need more data" in str(e)
else:
raise Exception("expected test to raise Exception")
try:
cl = CleanLearning(
clf=LogisticRegression(solver="lbfgs", random_state=SEED),
find_label_issues_kwargs={"return_indices_ranked_by": "self_confidence"},
)
cl.fit(
data["X_train"],
data["labels"],
)
except Exception as e:
assert "not supported" in str(e) or "Need more data from each class" in str(e)
else:
raise Exception("expected test to raise Exception")
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_aux_inputs():
data = DATA
K = len(np.unique(data["labels"]))
confident_joint = np.ones(shape=(K, K))
np.fill_diagonal(confident_joint, 10)
find_label_issues_kwargs = {
"confident_joint": confident_joint,
"min_examples_per_class": 2,
}
cl = CleanLearning(
clf=LogisticRegression(solver="lbfgs", random_state=SEED),
find_label_issues_kwargs=find_label_issues_kwargs,
verbose=1,
)
label_issues_df = cl.find_label_issues(data["X_train"], data["labels"], clf_kwargs={})
assert isinstance(label_issues_df, pd.DataFrame)
FIND_OUTPUT_COLUMNS = ["is_label_issue", "label_quality", "given_label", "predicted_label"]
assert list(label_issues_df.columns) == FIND_OUTPUT_COLUMNS
assert label_issues_df.equals(cl.get_label_issues())
cl.fit(
data["X_train"],
data["labels"],
label_issues=label_issues_df,
clf_kwargs={},
clf_final_kwargs={},
)
label_issues_df = cl.get_label_issues()
assert isinstance(label_issues_df, pd.DataFrame)
assert list(label_issues_df.columns) == (FIND_OUTPUT_COLUMNS + ["sample_weight"])
score = cl.score(data["X_test"], data["true_labels_test"])
# Test a second fit
cl.fit(data["X_train"], data["labels"])
# Test cl.find_label_issues with pred_prob input
pred_probs_test = cl.predict_proba(data["X_test"])
label_issues_df = cl.find_label_issues(
X=None, labels=data["true_labels_test"], pred_probs=pred_probs_test
)
assert isinstance(label_issues_df, pd.DataFrame)
assert list(label_issues_df.columns) == FIND_OUTPUT_COLUMNS
assert label_issues_df.equals(cl.get_label_issues())
cl.save_space()
assert cl.label_issues_df is None
# Verbose off
cl = CleanLearning(clf=LogisticRegression(solver="lbfgs", random_state=SEED), verbose=0)
cl.save_space() # dummy call test
cl = CleanLearning(clf=LogisticRegression(solver="lbfgs", random_state=SEED), verbose=0)
cl.find_label_issues(
labels=data["true_labels_test"], pred_probs=pred_probs_test, save_space=True
)
cl = CleanLearning(clf=LogisticRegression(solver="lbfgs", random_state=SEED), verbose=1)
# Test with label_issues_mask input
label_issues_mask = find_label_issues(
labels=data["true_labels_test"],
pred_probs=pred_probs_test,
)
cl.fit(data["X_test"], data["true_labels_test"], label_issues=label_issues_mask)
label_issues_df = cl.get_label_issues()
assert isinstance(label_issues_df, pd.DataFrame)
assert set(label_issues_df.columns).issubset(FIND_OUTPUT_COLUMNS)
# Test with label_issues_indices input
label_issues_indices = find_label_issues(
labels=data["true_labels_test"],
pred_probs=pred_probs_test,
return_indices_ranked_by="confidence_weighted_entropy",
)
cl.fit(data["X_test"], data["true_labels_test"], label_issues=label_issues_indices)
label_issues_df2 = cl.get_label_issues().copy()
assert isinstance(label_issues_df2, pd.DataFrame)
assert set(label_issues_df2.columns).issubset(FIND_OUTPUT_COLUMNS)
assert label_issues_df2["is_label_issue"].equals(label_issues_df["is_label_issue"])
# Test fit() with pred_prob input:
cl.fit(
data["X_test"],
data["true_labels_test"],
pred_probs=pred_probs_test,
label_issues=label_issues_mask,
)
label_issues_df = cl.get_label_issues()
assert isinstance(label_issues_df, pd.DataFrame)
assert set(label_issues_df.columns).issubset(FIND_OUTPUT_COLUMNS)
assert "label_quality" in label_issues_df.columns
# Test with sample_weight input:
cl = CleanLearning(clf=LogisticRegression(solver="lbfgs", random_state=SEED), verbose=1)
cl.fit(
data["X_test"],
data["true_labels_test"],
sample_weight=np.random.randn(len(data["true_labels_test"])),
)
cl.fit(
data["X_test"],
data["true_labels_test"],
label_issues=cl.get_label_issues(),
sample_weight=np.random.randn(len(data["true_labels_test"])),
)
class LogisticRegressionWithValidationData(LogisticRegression):
def fit(self, X, y, X_val=None, y_val=None):
super().fit(X, y)
# Final fit() call does not use validation data
# Checks to prevent arg missing error
if X_val is not None or y_val is not None:
print(self.score(X_val, y_val))
def val_func(X_val, y_val):
return {"X_val": X_val, "y_val": y_val}
def test_validation_data():
data = DATA
cl = CleanLearning(clf=LogisticRegressionWithValidationData())
cl.fit(
data["X_train"],
data["labels"],
validation_func=val_func,
)
def test_raise_error_no_clf_fit():
class struct(object):
def predict(self):
pass
def predict_proba(self):
pass
try:
CleanLearning(clf=struct())
except Exception as e:
assert "fit" in str(e)
with pytest.raises(ValueError) as e:
CleanLearning(clf=struct())
def test_raise_error_no_clf_predict_proba():
class struct(object):
def fit(self):
pass
def predict(self):
pass
try:
CleanLearning(clf=struct())
except Exception as e:
assert "predict_proba" in str(e)
with pytest.raises(ValueError) as e:
CleanLearning(clf=struct())
def test_raise_error_no_clf_predict():
class struct(object):
def fit(self):
pass
def predict_proba(self):
pass
try:
CleanLearning(clf=struct())
except Exception as e:
assert "predict" in str(e)
with pytest.raises(ValueError) as e:
CleanLearning(clf=struct())
def test_seed():
cl = CleanLearning(seed=SEED)
assert cl.seed is not None
def test_default_clf():
cl = CleanLearning()
check1 = cl.clf is not None and hasattr(cl.clf, "fit")
check2 = hasattr(cl.clf, "predict") and hasattr(cl.clf, "predict_proba")
assert check1 and check2
def test_clf_fit_nm():
cl = CleanLearning()
# Example of a bad noise matrix (impossible to learn from)
nm = np.array([[0, 1], [1, 0]])
try:
cl.fit(X=np.arange(3), labels=np.array([0, 0, 1]), noise_matrix=nm)
except Exception as e:
assert "Trace(noise_matrix)" in str(e)
with pytest.raises(ValueError) as e:
cl.fit(X=np.arange(3), labels=np.array([0, 0, 1]), noise_matrix=nm)
def test_clf_fit_inm():
cl = CleanLearning()
# Example of a bad noise matrix (impossible to learn from)
inm = np.array([[0.1, 0.9], [0.9, 0.1]])
try:
cl.fit(X=np.arange(3), labels=np.array([0, 0, 1]), inverse_noise_matrix=inm)
except Exception as e:
assert "Trace(inverse_noise_matrix)" in str(e)
with pytest.raises(ValueError) as e:
cl.fit(X=np.arange(3), labels=np.array([0, 0, 1]), inverse_noise_matrix=inm)
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_fit_with_nm(
format,
seed=SEED,
used_by_another_test=False,
):
data = DATA_FORMATS[format]
cl = CleanLearning(
seed=seed,
)
nm = data["noise_matrix"]
# Learn with noisy labels with noise matrix given
cl.fit(data["X_train"], data["labels"], noise_matrix=nm)
score_nm = cl.score(data["X_test"], data["true_labels_test"])
# Learn with noisy labels and estimate the noise matrix.
cl2 = CleanLearning(
seed=seed,
)
cl2.fit(
data["X_train"],
data["labels"],
)
score = cl2.score(data["X_test"], data["true_labels_test"])
if used_by_another_test:
return score, score_nm
else:
assert score < score_nm + 1e-4
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_fit_with_inm(
format,
seed=SEED,
used_by_another_test=False,
):
data = DATA_FORMATS[format]
cl = CleanLearning(
seed=seed,
)
inm = compute_inv_noise_matrix(
py=data["py"],
noise_matrix=data["noise_matrix"],
ps=data["ps"],
)
# Learn with noisy labels with inverse noise matrix given
cl.fit(data["X_train"], data["labels"], inverse_noise_matrix=inm)
score_inm = cl.score(data["X_test"], data["true_labels_test"])
# Learn with noisy labels and estimate the inv noise matrix.
cl2 = CleanLearning(
seed=seed,
)
cl2.fit(
data["X_train"],
data["labels"],
)
score = cl2.score(data["X_test"], data["true_labels_test"])
if used_by_another_test:
return score, score_inm
else:
assert score < score_inm + 1e-4
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_clf_fit_nm_inm(format):
data = DATA_FORMATS[format]
cl = CleanLearning(seed=SEED)
nm = data["noise_matrix"]
inm = compute_inv_noise_matrix(
py=data["py"],
noise_matrix=nm,
ps=data["ps"],
)
cl.fit(
X=data["X_train"],
labels=data["labels"],
noise_matrix=nm,
inverse_noise_matrix=inm,
)
score_nm_inm = cl.score(data["X_test"], data["true_labels_test"])
# Learn with noisy labels and estimate the inv noise matrix.
cl2 = CleanLearning(seed=SEED)
cl2.fit(
data["X_train"],
data["labels"],
)
score = cl2.score(data["X_test"], data["true_labels_test"])
assert score < score_nm_inm + 1e-4
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_clf_fit_y_alias(format):
data = DATA_FORMATS[format]
cl = CleanLearning(seed=SEED)
# Valid signature
cl.fit(data["X_train"], data["labels"])
# Valid signature for labels/y alias
cl.fit(data["X_train"], labels=data["labels"])
cl.fit(data["X_train"], y=data["labels"])
cl.fit(X=data["X_train"], labels=data["labels"])
cl.fit(X=data["X_train"], y=data["labels"])
# Invalid signatures
with pytest.raises(ValueError):
cl.fit(data["X_train"])
with pytest.raises(ValueError):
cl.fit(data["X_train"], data["labels"], y=data["labels"])
with pytest.raises(ValueError):
cl.fit(X=data["X_train"], labels=data["labels"], y=data["labels"])
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_pred_and_pred_proba(format):
data = DATA_FORMATS[format]
cl = CleanLearning()
cl.fit(data["X_train"], data["labels"])
n = np.shape(data["true_labels_test"])[0]
m = len(np.unique(data["true_labels_test"]))
pred = cl.predict(data["X_test"])
probs = cl.predict_proba(data["X_test"])
# Just check that this functions return what we expect
assert np.shape(pred)[0] == n
assert np.shape(probs) == (n, m)
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_score(format):
data = DATA_FORMATS[format]
phrase = "cleanlab is dope"
class Struct:
def fit(self):
pass
def predict_proba(self):
pass
def predict(self):
pass
def score(self, X, y):
return phrase
cl = CleanLearning(clf=Struct())
score = cl.score(data["X_test"], data["true_labels_test"])
assert score == phrase
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_no_score(format):
data = DATA_FORMATS[format]
class Struct:
def fit(self):
pass
def predict_proba(self):
pass
def predict(self, X):
return data["true_labels_test"]
cl = CleanLearning(clf=Struct())
score = cl.score(data["X_test"], data["true_labels_test"])
assert abs(score - 1) < 1e-6
@pytest.mark.filterwarnings("ignore::UserWarning")
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_no_fit_sample_weight(format):
data = DATA_FORMATS[format]
class Struct:
def fit(self, X, y):
pass
def predict_proba(self, X):
n_samples = len(X)
n_classes = len(np.unique(data["true_labels_train"]))
return np.ones((n_samples, n_classes)) / n_classes
def predict(self, X):
return np.zeros(len(X), dtype=int)
n = np.shape(data["true_labels_test"])[0]
m = len(np.unique(data["true_labels_test"]))
pred_probs = np.ones((n, m)) / m
cl = CleanLearning(clf=Struct())
cl.fit(
data["X_train"],
data["true_labels_train"],
pred_probs=pred_probs,
noise_matrix=data["noise_matrix"],
)
# If we make it here, without any error:
@pytest.mark.filterwarnings("ignore::UserWarning")
@pytest.mark.parametrize("format", list(DATA_FORMATS.keys()))
def test_fit_pred_probs(format):
data = DATA_FORMATS[format]
cl = CleanLearning()
pred_probs = estimate_cv_predicted_probabilities(
X=data["X_train"],
labels=data["true_labels_train"],
)
cl.fit(X=data["X_train"], labels=data["true_labels_train"], pred_probs=pred_probs)
score_with_pred_probs = cl.score(data["X_test"], data["true_labels_test"])
cl = CleanLearning()
cl.fit(
X=data["X_train"],
labels=data["true_labels_train"],
)
score_no_pred_probs = cl.score(data["X_test"], data["true_labels_test"])
assert abs(score_with_pred_probs - score_no_pred_probs) < 0.01
def make_2d(X):
X = np.asarray(X)
return X.reshape(X.shape[0], -1)
class ReshapingLogisticRegression(BaseEstimator):
def __init__(self):
self.clf = LogisticRegression()
def fit(self, X, y):
y = np.asarray(y).flatten()
self.clf.fit(make_2d(X), y)
def predict(self, X):
return self.clf.predict(make_2d(X))
def predict_proba(self, X):
return self.clf.predict_proba(make_2d(X))
def score(self, X, y, sample_weight=None):
return self.clf.score(make_2d(X), y, sample_weight=sample_weight)
def dimN_data(N):
size = [100] + [3 for _ in range(N - 1)]
X = np.random.normal(size=size)
labels = np.random.randint(0, 4, size=100)
# ensure that every class is represented
labels[0:10] = 0
labels[11:20] = 1
labels[21:30] = 2
labels[31:40] = 3
return X, labels
@pytest.mark.filterwarnings("ignore::RuntimeWarning")
@pytest.mark.parametrize("N", [1, 3, 4])
def test_dimN(N):
X, labels = dimN_data(N)
cl = CleanLearning(clf=ReshapingLogisticRegression())
# just make sure we don't crash...
cl.fit(X, labels)
cl.predict(X)
cl.predict_proba(X)
cl.score(X, labels)
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_1D_formats():
X, labels = dimN_data(1)
X_series = pd.Series(X)
labels_series = pd.Series(labels)
idx = list(np.random.choice(len(labels), size=len(labels), replace=False))
X_series.index = idx
labels_series.index = idx
cl = CleanLearning(clf=ReshapingLogisticRegression())
# just make sure we don't crash...
cl.fit(X_series, labels_series)
cl.predict(X_series)
cl.predict_proba(X_series)
cl.score(X_series, labels)
# Repeat with rare labels:
labels_rare = deepcopy(labels)
class0_inds = np.where(labels_rare == 0)[0]
class0_inds_remove = class0_inds[1:]
labels_rare[class0_inds_remove] = 1
cl = CleanLearning(clf=ReshapingLogisticRegression())
cl.fit(X_series, labels_rare)
cl.predict(X_series)
cl.predict_proba(X_series)
cl.score(X_series, labels)
# Repeat with DataFrame labels:
labels_df = pd.DataFrame({"colname": labels})
cl = CleanLearning(clf=ReshapingLogisticRegression())
cl.fit(X, labels_df)
cl.predict(X)
pred_probs = cl.predict_proba(X)
cl.score(X, labels)
# Repeat with DataFrame labels and pred_probs
cl = CleanLearning(clf=ReshapingLogisticRegression())
cl.fit(X, labels_df, pred_probs=pred_probs)
# Repeat with list labels:
labels_list = list(labels)
cl = CleanLearning(clf=ReshapingLogisticRegression())
cl.fit(X, labels_list)
cl.predict(X)
cl.predict_proba(X)
cl.score(X, labels)
# Check if the current Python version is 3.11
is_python_311 = sys.version_info.major == 3 and sys.version_info.minor == 11
# This warning should be ignored as in Python 3.11, the sre_constants module has been deprecated.
# At the time of writing this, cleanlab supports Python 3.8-3.11. This warning is raised by
# tensorflow <2.14.0, which imports sre_constants. This warning is not relevant to cleanlab.
# Once Python 3.8 reaches EOL, we may remove this warning filter as we can set the tensorflow
# dev-dependency to a version that does not raise this warning (2.14 or higher).
if is_python_311:
sre_deprecation_pytestmark = pytest.mark.filterwarnings(
"ignore:module 'sre_constants' is deprecated"
)
else:
sre_deprecation_pytestmark = pytest.mark.filterwarnings("default")
# Check if the installed version of sklearn is 1.5.0.
# The test_sklearn_gridsearchcv test fails due to a regression introduced in 1.5.0.
# This issue will be fixed in sklearn version 1.5.1.
uses_sklearn_1_5_0 = sklearn.__version__ == "1.5.0"
@sre_deprecation_pytestmark # Allow sre_constants deprecation warning for Python 3.11
@pytest.mark.filterwarnings("error") # All other warnings are treated as errors
@pytest.mark.skipif(
uses_sklearn_1_5_0,
reason="Test is skipped because sklearn 1.5.0 is installed, which has a regression for GridSearchCV.",
) # TODO: Remove this line once sklearn 1.5.1 is released
def test_sklearn_gridsearchcv():
# hyper-parameters for grid search
param_grid = {
"find_label_issues_kwargs": [
{"filter_by": "prune_by_noise_rate"},
{"filter_by": "prune_by_class"},
{"filter_by": "both"},
{"filter_by": "confident_learning"},
{"filter_by": "predicted_neq_given"},
],
"converge_latent_estimates": [True, False],
}
clf = LogisticRegression(random_state=0, solver="lbfgs")
cv = GridSearchCV(
estimator=CleanLearning(clf),
param_grid=param_grid,
cv=3,
)
# cv.fit() raises a warning if some fits fail (including raising
# exceptions); we don't expect any fits to fail, so ensure that the code
# doesn't raise any warnings
cv.fit(X=DATA["X_train"], y=DATA["labels"])
@pytest.mark.parametrize("filter_by", ["both", "confident_learning"])
@pytest.mark.parametrize("seed", [0, 6, 2])
def test_cj_in_find_label_issues_kwargs(filter_by, seed):
labels = DATA["labels"]
num_issues = []
for provide_confident_joint in [True, False]:
print(f"\nfilter_by: {filter_by} | seed: {seed} | cj_provided: {provide_confident_joint}")
np.random.seed(seed=seed)
if provide_confident_joint:
pred_probs = estimate_cv_predicted_probabilities(
X=DATA["X_train"], labels=labels, seed=seed
)
confident_joint = compute_confident_joint(labels=labels, pred_probs=pred_probs)
cl = CleanLearning(
find_label_issues_kwargs={
"confident_joint": confident_joint,
"filter_by": "both",
"min_examples_per_class": 1,
},
verbose=1,
)
else:
cl = CleanLearning(
clf=LogisticRegression(random_state=seed),
find_label_issues_kwargs={
"filter_by": "both",
"min_examples_per_class": 1,
},
verbose=0,
)
label_issues_df = cl.find_label_issues(DATA["X_train"], labels=labels)
label_issues_mask = label_issues_df["is_label_issue"].values
# Check if the noise matrix was computed based on the passed in confident joint
cj_reconstruct = (cl.inverse_noise_matrix * np.bincount(DATA["labels"])).T.astype(int)
np.all(cl.confident_joint == cj_reconstruct)
num_issues.append(sum(label_issues_mask))
# Chceck that the same exact number of issues are found regardless if the confident joint
# is computed during find_label_issues or precomputed and provided as a kwargs parameter.
assert num_issues[0] == num_issues[1]
def test_find_label_issues_uses_thresholds():
X = DATA["X_train"]
labels = DATA["labels"]
pred_probs = estimate_cv_predicted_probabilities(X=X, labels=labels)
confident_thresholds = get_confident_thresholds(labels=labels, pred_probs=pred_probs)
confident_joint = compute_confident_joint(labels=labels, pred_probs=pred_probs)
# regular find label issues with no args
cl = CleanLearning()
label_issues_reg = cl.find_label_issues(labels=labels, pred_probs=pred_probs)
# find label issues with specified confident thresholds
cl = CleanLearning()
label_issues_thres = cl.find_label_issues(
labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds
)
# find label issues with specified confident joint
cl = CleanLearning(
find_label_issues_kwargs={
"confident_joint": confident_joint,
}
)
label_issues_cj = cl.find_label_issues(labels=labels, pred_probs=pred_probs)
# the labels issues in above three calls should be the same
assert np.sum(label_issues_reg["is_label_issue"]) == np.sum(
label_issues_thres["is_label_issue"]
)
assert np.sum(label_issues_reg["is_label_issue"]) == np.sum(label_issues_cj["is_label_issue"])
# find label issues with different specified confident thresholds
confident_thresholds_alt = np.full(pred_probs.shape[1], 0.25)
cl = CleanLearning()
label_issues_thres_alt = cl.find_label_issues(
labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds_alt
)
# find label issues with different specified confident joint
confident_joint_alt = compute_confident_joint(
labels=labels, pred_probs=pred_probs, thresholds=confident_thresholds_alt
)
cl = CleanLearning(
find_label_issues_kwargs={
"confident_joint": confident_joint_alt,
}
)
label_issues_cj_alt = cl.find_label_issues(labels=labels, pred_probs=pred_probs)
# the number of issues for these 2 alt calls should be same as one another, but different from above 3
assert np.sum(label_issues_thres_alt["is_label_issue"]) == np.sum(
label_issues_cj_alt["is_label_issue"]
)
assert np.sum(label_issues_thres_alt["is_label_issue"]) != np.sum(
label_issues_reg["is_label_issue"]
)
def test_find_issues_missing_classes():
labels = np.array([0, 0, 2, 2])
pred_probs = np.array(
[[0.9, 0.0, 0.1, 0.0], [0.8, 0.0, 0.2, 0.0], [0.1, 0.0, 0.9, 0.0], [0.95, 0.0, 0.05, 0.0]]
)
issues_df = CleanLearning(
find_label_issues_kwargs={"min_examples_per_class": 0}
).find_label_issues(labels=labels, pred_probs=pred_probs)
issues = issues_df["is_label_issue"].values
assert np.all(issues == np.array([False, False, False, True]))
# Check results match without these missing classes present in pred_probs:
pred_probs2 = pred_probs[:, list(sorted(np.unique(labels)))]
labels2 = np.array([0, 0, 1, 1])
issues_df2 = CleanLearning(
find_label_issues_kwargs={"min_examples_per_class": 0}
).find_label_issues(labels=labels2, pred_probs=pred_probs2)
assert all(issues_df2["is_label_issue"].values == issues)
def test_find_issues_low_memory():
X = DATA["X_train"]
labels = DATA["labels"]
pred_probs = estimate_cv_predicted_probabilities(X=X, labels=labels, seed=SEED)
issues_df = CleanLearning().find_label_issues(labels=labels, pred_probs=pred_probs)
issues_df_lm = CleanLearning(low_memory=True).find_label_issues(
labels=labels, pred_probs=pred_probs
)
# check jaccard similarity:
intersection = len(list(set(issues_df).intersection(set(issues_df_lm))))
union = len(set(issues_df)) + len(set(issues_df_lm)) - intersection
assert float(intersection) / union > 0.95
# Without pred_probs
issues_df = CleanLearning(low_memory=True, verbose=True, seed=SEED).find_label_issues(
X=X, labels=labels
)
assert issues_df.equals(issues_df_lm)
# With unused arguments find_label_issues_kwargs and noise_matrix
find_label_issues_kwargs = {"min_examples_per_class": 2}
issues_df = CleanLearning(
low_memory=True, find_label_issues_kwargs=find_label_issues_kwargs, seed=SEED
).find_label_issues(X=X, labels=labels, noise_matrix=DATA["noise_matrix"])
assert issues_df.equals(issues_df_lm)
def test_confident_joint_setting_in_find_label_issues_kwargs():
"""
This test ensures that the 'confident_joint' is correctly set in the
'find_label_issues_kwargs' of the 'CleanLearning' class when calling find_label_issues().
This test was added to cover the lines of code that were previously
missed due to the removal of another test.
"""
# Load training data and labels
X = DATA["X_train"]
labels = DATA["labels"]
# Estimate predicted probabilities using cross-validation
pred_probs = estimate_cv_predicted_probabilities(X=X, labels=labels, seed=SEED)
# Initialize CleanLearning instance
cl = CleanLearning()
# Test that the confident joint is not set initially
cj = cl.find_label_issues_kwargs.get("confident_joint")
assert cj is None, "Initial confident_joint should be None"
# Call find_label_issues to set the confident joint
cl.find_label_issues(labels=labels, pred_probs=pred_probs)
cj = cl.find_label_issues_kwargs.get("confident_joint")
# Compute expected confident joint
expected_cj = compute_confident_joint(labels=labels, pred_probs=pred_probs)
# Assert that the confident joint is set correctly
np.testing.assert_array_equal(
cj, expected_cj, "Confident joint not set correctly after find_label_issues"
)
# Pass a precomputed confident_joint to the CleanLearning instance
cj_as_input = np.random.rand(3, 3)
cl = CleanLearning(
find_label_issues_kwargs={
"confident_joint": cj_as_input,
}
)
# Ensure the precomputed confident joint is used
cj = cl.find_label_issues_kwargs.get("confident_joint")
np.testing.assert_array_equal(
cj, cj_as_input, "Confident joint not set correctly when passed as input"
)
# Calling find_label_issues should not change the precomputed confident_joint
cl.find_label_issues(labels=labels, pred_probs=pred_probs)
cj = cl.find_label_issues_kwargs.get("confident_joint")
np.testing.assert_array_equal(
cj,
cj_as_input,
"Confident joint should not change after find_label_issues call when precomputed joint is provided",
)