992 lines
33 KiB
Python
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",
|
|
)
|