Files
2026-07-13 12:49:22 +08:00

487 lines
18 KiB
Python

"""
Scripts to test cleanlab.segmentation package
"""
import numpy as np
import random
np.random.seed(0)
import pytest
from unittest import mock
import matplotlib
matplotlib.use("Agg") # Set non-interactive backend before importing pyplot
import matplotlib.pyplot as plt
from pathlib import Path
from cleanlab.internal.multilabel_scorer import softmin
# Segmentation utils
from cleanlab.internal.segmentation_utils import (
_check_input,
_get_valid_optional_params,
_get_summary_optional_params,
)
# Filter
from cleanlab.segmentation.filter import (
find_label_issues,
)
# Rank
from cleanlab.segmentation.rank import (
get_label_quality_scores,
issues_from_scores,
_get_label_quality_per_image,
)
# Summary
from cleanlab.segmentation.summary import (
display_issues,
common_label_issues,
filter_by_class,
_generate_colormap,
)
def generate_three_image_dataset(bad_index):
good_gt = np.zeros((10, 10))
good_gt[:5, :] = 1.0
bad_gt = np.ones((10, 10))
bad_gt[:5, :] = 0.0
good_pr = np.random.random((2, 10, 10))
good_pr[0, :5, :] = good_pr[0, :5, :] / 10
good_pr[1, 5:, :] = good_pr[1, 5:, :] / 10
val = np.binary_repr([4, 2, 1][bad_index], width=3)
error = [int(case) for case in val]
labels = []
pred = []
for case in val:
if case == "0":
labels.append(good_gt)
pred.append(good_pr)
else:
labels.append(bad_gt)
pred.append(good_pr)
labels = np.array(labels)
pred_probs = np.array(pred)
return labels, pred_probs, error
labels, pred_probs, error = generate_three_image_dataset(random.randint(0, 2))
labels, pred_probs = labels.astype(int), pred_probs.astype(float)
num_images, num_classes, h, w = pred_probs.shape
def test_find_label_issues():
issues = find_label_issues(labels, pred_probs, n_jobs=None, batch_size=1000)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
issues = find_label_issues(labels, pred_probs, downsample=2, batch_size=1739)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
issues = find_label_issues(labels, pred_probs, downsample=5, n_jobs=None, batch_size=2838)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
with pytest.raises(Exception) as e:
issues = find_label_issues(labels, pred_probs, downsample=4, n_jobs=None, batch_size=1000)
# Simple tests
# Test case 1: Test with larger batch_size
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=2000)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
# Test case 2: Test with smaller batch_size
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=500)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
# Test case 3: Test verbose off
issues = find_label_issues(labels, pred_probs, downsample=1, verbose=False)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
# Test case 5: Test with invalid downsample value
with pytest.raises(Exception) as e:
issues = find_label_issues(labels, pred_probs, downsample=3, n_jobs=None, batch_size=1000)
# Test case 6: Test with n_jobs parameter
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=2, batch_size=1000)
assert np.argmax(error) == np.argmax(issues.sum((1, 2)))
# Test case 7: Test with invalid labels
with pytest.raises(Exception) as e:
issues = find_label_issues(
np.array([[[[1, 2, 3]]]]), pred_probs, downsample=1, n_jobs=None, batch_size=1000
)
# Test case 8: Test with invalid pred_probs
with pytest.raises(Exception) as e:
issues = find_label_issues(
labels, np.array([[[[0.1, 0.2, 0.3]]]]), downsample=1, n_jobs=None, batch_size=1000
)
def test_results_are_consistent_with_batch_size(tmp_path: Path):
"""
Test that find_label_issues works with large memmap arrays and different batch sizes
"""
# Create dummy versions of pred_probs and labels
# write to the pytest tmp_path so that the files are deleted after the test
pred_probs_file = tmp_path / "pred_probs.npy"
labels_file = tmp_path / "labels.npy"
np.save(pred_probs_file, np.random.rand(100, 2, 5, 5))
np.save(labels_file, np.random.randint(0, 2, (100, 5, 5)))
# Load the numpy arrays from disk
pred_probs = np.load(pred_probs_file, mmap_mode="r")
pred_labels = np.load(labels_file, mmap_mode="r")
# Test with different batch sizes
batch_sizes = [1, 50, 100]
issues_list = []
for batch_size in batch_sizes:
issues = find_label_issues(pred_labels, pred_probs, n_jobs=None, batch_size=batch_size)
issues_list.append(issues)
# Verify that the results are identical regardless of the batch size
for i in range(len(batch_sizes) - 1):
assert np.array_equal(issues_list[i], issues_list[i + 1])
def test_find_label_issues_sizes():
# checks inputs of different sizes
labels, pred_probs = np.random.randint(0, 2, (2, 9, 7)), np.random.random((2, 2, 9, 7))
issues = find_label_issues(labels, pred_probs)
labels, pred_probs = np.random.randint(0, 2, (2, 13, 47)), np.random.random((2, 2, 13, 47))
issues = find_label_issues(labels, pred_probs)
for _ in range(5):
h, w = np.random.randint(1, 100, 2)
labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w))
issues = find_label_issues(labels, pred_probs)
def test__check_input():
bad_gt = np.random.random((5, 10, 20))
with pytest.raises(Exception) as e:
_check_input(bad_gt, bad_gt)
bad_pr = np.random.random((5, 2, 10, 20))
with pytest.raises(Exception) as e:
_check_input(bad_pr, bad_pr)
smaller_pr = np.random.random((5, 2, 9, 20))
with pytest.raises(Exception) as e:
_check_input(bad_gt, smaller_pr)
fewer_gt = np.random.random((4, 10, 20))
with pytest.raises(Exception) as e:
_check_input(fewer_gt, smaller_pr)
@pytest.mark.filterwarnings("ignore::UserWarning") # Should be 1 warning
def test_get_label_quality_scores():
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
assert np.argmax(error) == np.argmin(image_scores_softmin)
image_scores_npi, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="num_pixel_issues", downsample=1
)
assert np.argmax(error) == np.argmin(image_scores_npi)
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, downsample=1, method="softmin"
)
assert len(image_scores_softmin) == labels.shape[0]
assert pixel_scores.shape == labels.shape
@pytest.mark.parametrize(
"method, downsample, batch_size, expected_exception, expected_message",
[
(
"num_pixel_issues",
4,
None,
Exception,
"Height 10 and width 10 not divisible by downsample value of 4. Set kwarg downsample to 1 to avoid downsampling.",
),
(
"invalid_method",
None,
None,
Exception,
"Invalid Method: Specify correct method. Currently only supports 'softmin'",
),
("num_pixel_issues", 1, -1, ValueError, "Batch size must be greater than 0, got -1"),
("num_pixel_issues", 1, 0, ValueError, "Batch size must be greater than 0, got 0"),
],
ids=["downsample", "method", "batch_size_negative", "batch_size_zero"],
)
def test_get_label_quality_scores_exceptions(
method, downsample, batch_size, expected_exception, expected_message
):
args = {"labels": labels, "pred_probs": pred_probs, "method": method}
if downsample is not None:
args["downsample"] = downsample
if batch_size is not None:
args["batch_size"] = batch_size
with pytest.raises(expected_exception) as exc_info:
get_label_quality_scores(**args)
assert expected_message in str(exc_info.value)
# different size inpits
def test_get_label_quality_scores_sizes():
# checks inputs of different sizes
labels, pred_probs = np.random.randint(0, 2, (2, 9, 7)), np.random.random((2, 2, 9, 7))
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
labels, pred_probs = np.random.randint(0, 2, (2, 13, 47)), np.random.random((2, 2, 13, 47))
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
for _ in range(5):
h, w = np.random.randint(1, 100, 2)
labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w))
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
# Testing issues from scores
def test_issues_from_scores():
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=1)
assert np.shape(issues_from_score) == np.shape(pixel_scores)
assert h * w * num_images == issues_from_score.sum()
issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=0)
assert 0 == issues_from_score.sum()
issues_from_score = issues_from_scores(image_scores_softmin, pixel_scores, threshold=0.5)
assert np.argmax(error) == np.argmax(issues_from_score.sum((1, 2)))
sort_by_score = issues_from_scores(image_scores_softmin, threshold=0.5)
assert error[sort_by_score[0]] == 1
def test_issues_from_scores_no_pixel_scores():
# Test if function works correctly when pixel_scores is None
image_scores_softmin, _ = get_label_quality_scores(labels, pred_probs, method="softmin")
issues_from_score_result = issues_from_scores(image_scores_softmin, None, threshold=1)
assert np.shape(issues_from_score_result) == (num_images,)
def test_issues_from_scores_various_thresholds():
# Test if function works correctly for various values of threshold
image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
for threshold in [0.1, 0.5, 0.9]:
issues_from_score_result = issues_from_scores(
image_scores_softmin, pixel_scores, threshold=threshold
)
assert np.all(issues_from_score_result == (pixel_scores < threshold))
def test_issues_from_scores_invalid_inputs():
# Test if function raises exception when input parameters are invalid
with pytest.raises(ValueError):
issues_from_scores(None)
with pytest.raises(ValueError):
issues_from_scores(np.array([0.1, 0.2, 0.3]), threshold=1.1) # Threshold more than 1
with pytest.raises(ValueError):
issues_from_scores(np.array([0.1, 0.2, 0.3]), threshold=-0.1) # Threshold less than 0
def test_issues_from_scores_different_input_sizes():
# Test if function works correctly for different sizes of input arrays
for num_images in range(1, 5):
image_scores = np.random.rand(num_images)
pixel_scores = np.random.rand(num_images, 100, 100)
issues_from_score_result = issues_from_scores(image_scores, pixel_scores, threshold=0.5)
assert np.shape(issues_from_score_result) == np.shape(pixel_scores)
def test_issues_from_scores_sorting():
# Test if function correctly sorts image_scores
image_scores_softmin, _ = get_label_quality_scores(labels, pred_probs, method="softmin")
issues_from_score_result = issues_from_scores(image_scores_softmin, None, threshold=0.5)
assert np.all(np.sort(image_scores_softmin) == image_scores_softmin[issues_from_score_result])
def test__get_label_quality_per_image():
# Test when pixel_scores is a random list of 100 values, method is "softmin", and temperature is random
random_score_array = np.random.random((100,))
temp = random.random()
score = _get_label_quality_per_image(random_score_array, method="softmin", temperature=temp)
cleanlab_softmin = softmin(
np.expand_dims(random_score_array, axis=0), axis=1, temperature=temp
)[0]
assert cleanlab_softmin == score, "Expected cleanlab_softmin to be equal to score"
# Test when pixel_scores is an empty list, should raise an error
empty_score_array = np.array([])
with pytest.raises(Exception) as e:
_get_label_quality_per_image(empty_score_array, method="softmin", temperature=temp)
# Test when method is None
with pytest.raises(Exception):
_get_label_quality_per_image(random_score_array, method=None, temperature=temp)
# Test when method is not "softmin", should raise an exception
with pytest.raises(Exception):
_get_label_quality_per_image(random_score_array, method="invalid_method", temperature=temp)
# Test when temperature is 0, should raise an error
with pytest.raises(Exception):
_get_label_quality_per_image(random_score_array, method="softmin", temperature=0)
with pytest.raises(Exception):
_get_label_quality_per_image(random_score_array, method="softmin", temperature=None)
def test_generate_color_map():
colors = _generate_colormap(0)
assert len(colors) == 0
colors = _generate_colormap(1)
assert len(colors) == 1
assert len(colors[0]) == 4
colors = _generate_colormap(-1)
assert len(colors) == 0
colors = _generate_colormap(5)
assert len(colors) == 5
# test unique
num_colors = 385 # max number of unique colors avalible
colors = _generate_colormap(num_colors)
unique_rows = np.unique(colors, axis=0)
assert unique_rows.shape[0] == num_colors
def test_display_issues(monkeypatch):
monkeypatch.setattr(plt, "show", lambda: None)
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
display_issues(issues, top=1)
display_issues(issues, pred_probs=pred_probs, labels=labels, top=2, class_names=["one", "two"])
display_issues(issues, pred_probs=pred_probs, labels=labels, top=2)
display_issues(issues, labels=labels, top=2)
display_issues(issues, pred_probs=pred_probs, top=2)
# too many issues for top
display_issues(issues, pred_probs=pred_probs, labels=labels, top=len(issues) + 5)
class_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs)
display_issues(class_issues, pred_probs=pred_probs, labels=labels, top=2)
image_scores, pixel_scores = image_scores_softmin, pixel_scores = get_label_quality_scores(
labels, pred_probs, method="softmin"
)
issues_from_score = issues_from_scores(image_scores, pixel_scores, threshold=0.5)
display_issues(issues_from_score, pred_probs=pred_probs, labels=labels, top=2)
display_issues(issues_from_score, pred_probs=pred_probs, labels=labels, top=2, exclude=[0])
with pytest.raises(ValueError) as e:
display_issues(issues_from_score, pred_probs=pred_probs, labels=None, top=2, exclude=[0])
@mock.patch("matplotlib.pyplot.figure")
def test_display_issues_figure(mock_plt, monkeypatch):
monkeypatch.setattr(plt, "show", lambda: None)
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
display_issues(issues, pred_probs=pred_probs, labels=labels, top=2, class_names=["one", "two"])
assert mock_plt.called
@mock.patch("matplotlib.pyplot.show")
def test_display_issues_show(mock_plt):
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
display_issues(issues, top=1)
assert mock_plt.called
def test_common_label_issues(capsys):
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
# test exclude
df = common_label_issues(issues, labels, pred_probs)
df_without_0 = common_label_issues(issues, labels, pred_probs, exclude=[0])
assert df.shape != df_without_0.shape
# test verbose
captured_words = capsys.readouterr()
df = common_label_issues(issues, labels, pred_probs, verbose=False)
captured_no_words = capsys.readouterr()
assert len(captured_no_words.out) == 0
assert len(captured_words.out) > 0
# test class names & top
df_class_names = common_label_issues(issues, labels, pred_probs, class_names=["one", "two"])
captured_top_all = capsys.readouterr()
df_top_1 = common_label_issues(issues, labels, pred_probs, top=1)
captured_top_1 = capsys.readouterr()
assert len(captured_top_1.out) < len(captured_top_all.out)
assert df_class_names["given_label"].to_list() != df["given_label"].to_list()
def test_filter_by_class():
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
class_0_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs)
class_1_issues = filter_by_class(1, issues, labels=labels, pred_probs=pred_probs)
class_300_issues = filter_by_class(300, issues, labels=labels, pred_probs=pred_probs)
assert (class_0_issues == class_1_issues).all() # mirror issues
assert np.sum(class_300_issues) == 0
def test_summary_sizes(monkeypatch):
monkeypatch.setattr(plt, "show", lambda: None)
for _ in range(5):
h, w = np.random.randint(1, 100, 2)
labels, pred_probs = np.random.randint(0, 2, (2, h, w)), np.random.random((2, 2, h, w))
issues = find_label_issues(labels, pred_probs, downsample=1, n_jobs=None, batch_size=1000)
class_300_issues = filter_by_class(0, issues, labels=labels, pred_probs=pred_probs)
df = common_label_issues(issues, labels, pred_probs)
display_issues(issues)
def test_get_valid_functions():
optional_batch_size = 10
optional_n_jobs = 2
x, y = _get_valid_optional_params(optional_batch_size, optional_n_jobs)
assert x == optional_batch_size and y == optional_n_jobs
x, y = _get_valid_optional_params(None, None)
assert x == 10000 and y == None
optional_class_names = [1, 2]
optional_exclude = [1]
optional_top = 10
x, y, z = _get_summary_optional_params(optional_class_names, optional_exclude, optional_top)
assert x == optional_class_names and y == optional_exclude and z == optional_top
x, y, z = _get_summary_optional_params(None, None, None)
assert x == None and y == [] and z == 20