cleanlab--cleanlab
457 行
16 KiB
Python
457 行
16 KiB
Python
# Copyright (C) 2017-2024 Cleanlab Inc.
|
|
# This file is part of cleanlab.
|
|
#
|
|
# cleanlab is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# cleanlab is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with cleanlab. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
from itertools import islice
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from sklearn.model_selection import train_test_split, cross_val_predict
|
|
from sklearn.linear_model import LogisticRegression
|
|
|
|
from cleanlab.datalab.datalab import Datalab
|
|
from cleanlab.experimental.datalab.data_monitor import DataMonitor
|
|
from cleanlab.benchmarking.noise_generation import (
|
|
generate_noise_matrix_from_trace,
|
|
generate_noisy_labels,
|
|
)
|
|
|
|
SEED = 42
|
|
|
|
|
|
class SetupClass:
|
|
num_examples = 2000
|
|
test_size = 0.1
|
|
|
|
@pytest.fixture
|
|
def data(self):
|
|
np.random.seed(SEED)
|
|
|
|
BINS = {
|
|
0: [-np.inf, 3.3],
|
|
1: [3.3, 6.6],
|
|
2: [6.6, +np.inf],
|
|
}
|
|
|
|
X = np.random.rand(self.num_examples, 2) * 5
|
|
f = np.sum(X, axis=1)
|
|
y = np.array([k for f_i in f for k, v in BINS.items() if v[0] <= f_i < v[1]])
|
|
|
|
X_train, X_test, y_train, y_test = train_test_split(
|
|
X, y, test_size=self.test_size, random_state=SEED
|
|
)
|
|
|
|
py = np.bincount(y) / float(len(y))
|
|
m = len(BINS)
|
|
|
|
noise_matrix = generate_noise_matrix_from_trace(
|
|
m,
|
|
trace=0.9 * m,
|
|
py=py,
|
|
valid_noise_matrix=True,
|
|
seed=SEED,
|
|
)
|
|
|
|
noisy_labels_train = generate_noisy_labels(y_train, noise_matrix)
|
|
noisy_labels_test = generate_noisy_labels(y_test, noise_matrix)
|
|
|
|
return {
|
|
"X_train": X_train,
|
|
"noisy_labels_train": noisy_labels_train,
|
|
"X_test": X_test,
|
|
"noisy_labels_test": noisy_labels_test,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def pred_probs_train(self, data):
|
|
model = LogisticRegression()
|
|
pred_probs = cross_val_predict(
|
|
estimator=model,
|
|
X=data["X_train"],
|
|
y=data["noisy_labels_train"],
|
|
cv=5,
|
|
method="predict_proba",
|
|
)
|
|
return pred_probs
|
|
|
|
@pytest.fixture
|
|
def pred_probs_test(self, data):
|
|
model = LogisticRegression()
|
|
model.fit(data["X_train"], data["noisy_labels_train"])
|
|
pred_probs = model.predict_proba(data["X_test"])
|
|
return pred_probs
|
|
|
|
@pytest.fixture
|
|
def pred_probs_combined(self, data):
|
|
model = LogisticRegression()
|
|
pred_probs = cross_val_predict(
|
|
estimator=model,
|
|
X=np.concatenate((data["X_test"], data["X_train"])),
|
|
y=np.concatenate((data["noisy_labels_test"], data["noisy_labels_train"])),
|
|
cv=5,
|
|
method="predict_proba",
|
|
)
|
|
return pred_probs
|
|
|
|
@pytest.fixture
|
|
def datalab(self, pred_probs_train, data):
|
|
dataset = {"labels": data["noisy_labels_train"]}
|
|
lab = Datalab(data=dataset, label_name="labels")
|
|
issue_types = {"label": {}, "outlier": {}}
|
|
lab.find_issues(
|
|
pred_probs=pred_probs_train, features=data["X_train"], issue_types=issue_types
|
|
)
|
|
return lab
|
|
|
|
|
|
class TestDataMonitorReuseStatisticInfo(SetupClass):
|
|
# TODO: Rename class
|
|
|
|
def test_reuse_statistics_info(self, datalab):
|
|
# TODO: Rename test
|
|
"""Test that the DataMonitor has the same info as the Datalab instance."""
|
|
monitor = DataMonitor(datalab=datalab)
|
|
for k in datalab.get_info().keys():
|
|
assert monitor.info[k].keys() == datalab.get_info(k).keys()
|
|
|
|
def test_set_trained_statistics(self, datalab, pred_probs_test, data):
|
|
# TODO: Rename test
|
|
"""Check the label-specific info is available in the DataMonitor instance for detecting label issues.
|
|
The info should not be affected by the test data."""
|
|
monitor = DataMonitor(datalab=datalab)
|
|
monitor.find_issues(
|
|
labels=data["noisy_labels_test"], pred_probs=pred_probs_test, features=data["X_test"]
|
|
)
|
|
trained_statistics = datalab.get_info("label")
|
|
test_statistics = monitor.info["label"]
|
|
for k, v in trained_statistics.items():
|
|
if k in ["confident_joint", "confident_thresholds"]:
|
|
assert np.array_equal(v, test_statistics[k])
|
|
|
|
def test_find_issues_with_datalab(self, datalab, pred_probs_test, pred_probs_combined, data):
|
|
"""Verify that running Datalab on a train+test dataset should give very similar results
|
|
to running DataMonitor on the test dataset (with a Datalab instance having only seen the training dataset).
|
|
"""
|
|
# Run issue checks on combined dataset
|
|
combined_data = {
|
|
"labels": np.concatenate((data["noisy_labels_test"], data["noisy_labels_train"]))
|
|
}
|
|
lab_all = Datalab(data=combined_data, label_name="labels")
|
|
lab_all.find_issues(pred_probs=pred_probs_combined, issue_types={"label": {}})
|
|
|
|
# Monitor test data only
|
|
test_data = {"labels": data["noisy_labels_test"]}
|
|
monitor = DataMonitor(datalab=datalab)
|
|
monitor.find_issues(
|
|
labels=test_data["labels"], pred_probs=pred_probs_test, features=data["X_test"]
|
|
)
|
|
|
|
# Compare results
|
|
lab_results = lab_all.get_issues()[: int(self.num_examples * self.test_size)]
|
|
monitor_results = monitor.issues
|
|
similarity = sum(
|
|
lab_results[["is_label_issue"]].values == monitor_results[["is_label_issue"]].values
|
|
) / len(lab_results)
|
|
assert similarity >= 0.93
|
|
|
|
def test_find_issues_with_datalab_multi_different_size_batch(
|
|
self, datalab, pred_probs_test, pred_probs_combined, data
|
|
):
|
|
"""Test that the DataMonitor can find label issues in test data over smaller batches,
|
|
perfomring similarly to Datalab on the combined dataset.
|
|
"""
|
|
# Run issue checks on combined dataset and fetch results
|
|
combined_data = {
|
|
"labels": np.concatenate((data["noisy_labels_test"], data["noisy_labels_train"]))
|
|
}
|
|
lab_all = Datalab(data=combined_data, label_name="labels")
|
|
lab_all.find_issues(pred_probs=pred_probs_combined, issue_types={"label": {}})
|
|
lab_results = lab_all.get_issues()[: int(self.num_examples * self.test_size)]
|
|
|
|
# Monitor test data in smaller batches, collecting results for each batch
|
|
monitor_results = pd.DataFrame()
|
|
batch_sizes = [20, 50, 100, 30]
|
|
start_positions = [0] * len(batch_sizes)
|
|
for i in range(1, len(batch_sizes)):
|
|
start_positions[i] = start_positions[i - 1] + batch_sizes[i - 1]
|
|
for batch_size, start_position in zip(batch_sizes, start_positions):
|
|
data_batch = {
|
|
"labels": data["noisy_labels_test"][start_position : start_position + batch_size],
|
|
"pred_probs": pred_probs_test[start_position : start_position + batch_size],
|
|
"features": data["X_test"][start_position : start_position + batch_size],
|
|
}
|
|
monitor = DataMonitor(datalab=datalab)
|
|
monitor.find_issues(**data_batch)
|
|
monitor_results = pd.concat([monitor_results, monitor.issues], axis=0)
|
|
|
|
# Compare results
|
|
similarity = sum(
|
|
lab_results[["is_label_issue"]].values == monitor_results[["is_label_issue"]].values
|
|
) / len(lab_results)
|
|
assert similarity >= 0.90
|
|
|
|
def test_default_issue_types(self, datalab, data, pred_probs_test):
|
|
"""Test that the DataMonitor checks for the correct types of issues by default."""
|
|
# TODO: Run this test with features as well
|
|
# Check for issues on test data
|
|
dataset = {"labels": data["noisy_labels_test"]}
|
|
monitor = DataMonitor(datalab=datalab)
|
|
monitor.find_issues(
|
|
labels=dataset["labels"], pred_probs=pred_probs_test, features=data["X_test"]
|
|
)
|
|
|
|
expected_issue_types_keys = ["label", "outlier"]
|
|
|
|
# Verify that the correct types of issues were checked for
|
|
issue_summary = monitor.issue_summary
|
|
issue_types_found = issue_summary["issue_type"]
|
|
assert set(issue_types_found) == set(expected_issue_types_keys)
|
|
|
|
|
|
# Helper function for testing streaming workflows
|
|
def batch_slices(iterable, start, batch_size, drop_last=False):
|
|
"""
|
|
Generator that yields slices of the specified batch size from the iterable starting from the start index.
|
|
If drop_last is True, the last batch will be dropped if it is smaller than the batch size.
|
|
"""
|
|
it = iter(iterable)
|
|
# Skip items until the start index
|
|
skipped = list(islice(it, start))
|
|
while True:
|
|
batch = list(islice(it, batch_size))
|
|
if not batch or (drop_last and len(batch) < batch_size):
|
|
break
|
|
yield np.array(batch)
|
|
|
|
|
|
# Test class for streaming data
|
|
class TestDataMonitorInit(SetupClass):
|
|
# This test class looks at the initialization of the DataMonitor class
|
|
def test_data_monitor_creation(self, datalab, data):
|
|
data = {"labels": data["noisy_labels_test"]}
|
|
monitor = DataMonitor(datalab=datalab)
|
|
assert isinstance(monitor, DataMonitor)
|
|
|
|
def test_data_monitor_with_streaming_data(self, datalab, data):
|
|
"""Test that the issues DataFrame in the DataMonitor is updated after streaming data in batches.
|
|
Also verify that the issues DataFrame has the expected values.
|
|
"""
|
|
monitor = DataMonitor(datalab=datalab)
|
|
assert isinstance(monitor, DataMonitor)
|
|
|
|
features = data["X_test"][:20]
|
|
labels = data["noisy_labels_test"][:20]
|
|
from sklearn.linear_model import LogisticRegression
|
|
|
|
clf = LogisticRegression()
|
|
clf.fit(data["X_train"], data["noisy_labels_train"])
|
|
pred_probs = clf.predict_proba(features)
|
|
|
|
singleton_stream = (
|
|
{
|
|
"features": f[np.newaxis, :],
|
|
"pred_probs": p[np.newaxis, :],
|
|
"labels": l[np.newaxis],
|
|
}
|
|
for f, p, l in zip(features[:5], pred_probs[:5], labels[:5])
|
|
)
|
|
|
|
batch_stream = (
|
|
{
|
|
"features": f,
|
|
"pred_probs": p,
|
|
"labels": l,
|
|
}
|
|
for f, p, l in zip(
|
|
batch_slices(features[5:], 0, 5),
|
|
batch_slices(pred_probs[5:], 0, 5),
|
|
batch_slices(labels[5:], 0, 5),
|
|
)
|
|
)
|
|
|
|
assert monitor.issues.empty
|
|
|
|
for i, eg in enumerate(singleton_stream):
|
|
monitor.find_issues(**eg)
|
|
|
|
for i, batch in enumerate(batch_stream):
|
|
monitor.find_issues(**batch)
|
|
issues = monitor.issues
|
|
assert (not issues.empty) and (len(issues) == len(labels))
|
|
assert list(issues["is_label_issue"]) == [False] * (len(labels) - 1) + [True]
|
|
expected_ids_sorted_by_label_score = [
|
|
19,
|
|
15,
|
|
3,
|
|
9,
|
|
8,
|
|
14,
|
|
17,
|
|
7,
|
|
11,
|
|
0,
|
|
10,
|
|
2,
|
|
1,
|
|
6,
|
|
12,
|
|
5,
|
|
4,
|
|
18,
|
|
16,
|
|
13,
|
|
]
|
|
|
|
assert all(issues["label_score"].iloc[:-1] > 0.3) and issues["label_score"].iloc[-1] < 0.1
|
|
|
|
# Test that there's a high correlation between the expected order of the examples and the order of the examples in the issues dataframe
|
|
from scipy.stats import spearmanr
|
|
|
|
corr, _ = spearmanr(
|
|
expected_ids_sorted_by_label_score, list(issues.sort_values("label_score").index)
|
|
)
|
|
assert corr > 0.9
|
|
|
|
def test_outlier_detection(self, datalab, data):
|
|
monitor = DataMonitor(datalab=datalab)
|
|
assert isinstance(monitor, DataMonitor)
|
|
|
|
features = data["X_test"][:20]
|
|
|
|
# Make the first example an outlier
|
|
features[0] += 5
|
|
|
|
labels = data["noisy_labels_test"][:20]
|
|
|
|
# Correct the label of the outlier
|
|
labels[0] = np.where(features[0].sum() < 3.3, 0, np.where(features[0].sum() < 6.6, 1, 2))
|
|
from sklearn.linear_model import LogisticRegression
|
|
|
|
clf = LogisticRegression()
|
|
clf.fit(data["X_train"], data["noisy_labels_train"])
|
|
pred_probs = clf.predict_proba(features)
|
|
|
|
singleton_stream = (
|
|
{
|
|
"features": f[np.newaxis, :],
|
|
"pred_probs": p[np.newaxis, :],
|
|
"labels": l[np.newaxis],
|
|
}
|
|
for f, p, l in zip(features[:5], pred_probs[:5], labels[:5])
|
|
)
|
|
|
|
batch_stream = (
|
|
{
|
|
"features": f,
|
|
"pred_probs": p,
|
|
"labels": l,
|
|
}
|
|
for f, p, l in zip(
|
|
batch_slices(features[5:], 0, 5),
|
|
batch_slices(pred_probs[5:], 0, 5),
|
|
batch_slices(labels[5:], 0, 5),
|
|
)
|
|
)
|
|
|
|
for i, eg in enumerate(singleton_stream):
|
|
monitor.find_issues(**eg)
|
|
|
|
issues = monitor.issues
|
|
outlier_issue_mask = issues["is_outlier_issue"].to_numpy()
|
|
expected_mask = np.array([True, False, False, False, False])
|
|
np.testing.assert_array_equal(outlier_issue_mask, expected_mask)
|
|
|
|
outlier_scores = issues["outlier_score"].to_numpy()
|
|
expected_scores = np.array([6.4e-15, 0.38, 0.33, 0.42, 0.33])
|
|
np.testing.assert_allclose(outlier_scores, expected_scores, atol=1e-2)
|
|
|
|
def test_only_on_pred_probs(self, data):
|
|
train_dataset = {"labels": data["noisy_labels_train"], "X_train": data["X_train"]}
|
|
lab = Datalab(data=train_dataset, label_name="labels", task="classification")
|
|
|
|
from sklearn.model_selection import cross_val_predict
|
|
from sklearn.linear_model import LogisticRegression
|
|
|
|
pred_probs = cross_val_predict(
|
|
LogisticRegression(),
|
|
train_dataset["X_train"],
|
|
train_dataset["labels"],
|
|
cv=5,
|
|
method="predict_proba",
|
|
)
|
|
|
|
lab.find_issues(pred_probs=pred_probs)
|
|
|
|
# Set up monitor
|
|
monitor = DataMonitor(datalab=lab)
|
|
|
|
# Set up stream of test data
|
|
features = data["X_test"][:20]
|
|
labels = data["noisy_labels_test"][:20]
|
|
|
|
clf = LogisticRegression()
|
|
clf.fit(train_dataset["X_train"], train_dataset["labels"])
|
|
|
|
pred_probs = clf.predict_proba(features)
|
|
|
|
singleton_stream = (
|
|
{
|
|
"pred_probs": clf.predict_proba(f[np.newaxis, :]),
|
|
"labels": l[np.newaxis],
|
|
}
|
|
for f, l in zip(features, labels)
|
|
)
|
|
|
|
for eg in singleton_stream:
|
|
monitor.find_issues(**eg)
|
|
|
|
issues = monitor.issues
|
|
|
|
# Only the "label" monitor is configured
|
|
assert set(["label"]) == set(monitor.monitors.keys())
|
|
|
|
# Only label issues should have been checked
|
|
assert set(issues.columns) == set(["is_label_issue", "label_score"])
|
|
# All the "test" examples should been checked
|
|
assert len(issues) == len(features)
|
|
|
|
def test_only_on_features(self, data):
|
|
train_dataset = {"X_train": data["X_train"]}
|
|
lab = Datalab(data=train_dataset, task="classification")
|
|
|
|
lab.find_issues(features=train_dataset["X_train"])
|
|
|
|
# Set up monitor
|
|
monitor = DataMonitor(datalab=lab)
|
|
|
|
# Set up stream of test data
|
|
features = data["X_test"][:20]
|
|
|
|
singleton_stream = ({"features": f[np.newaxis, :]} for f in features)
|
|
|
|
for eg in singleton_stream:
|
|
monitor.find_issues(**eg)
|
|
|
|
issues = monitor.issues
|
|
|
|
# Only the "outlier" monitor is configured
|
|
assert set(["outlier"]) == set(monitor.monitors.keys())
|
|
|
|
# Only outlier issues should have been checked
|
|
assert set(issues.columns) == set(["is_outlier_issue", "outlier_score"])
|
|
# All the "test" examples should been checked
|
|
assert len(issues) == len(features)
|