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

214 lines
6.1 KiB
Python

# coding: utf-8
from collections import namedtuple
from cleanlab.internal import util
import numpy as np
import pytest
from cleanlab.internal.label_quality_utils import get_normalized_entropy
from cleanlab.internal.multilabel_utils import int2onehot, onehot2int
from cleanlab.internal.util import num_unique_classes, format_labels, get_missing_classes
from cleanlab.internal.validation import assert_valid_class_labels
noise_matrix = np.array([[1.0, 0.0, 0.2], [0.0, 0.7, 0.2], [0.0, 0.3, 0.6]])
noise_matrix_2 = np.array(
[
[1.0, 0.3],
[0.0, 0.7],
]
)
joint_matrix = np.array([[0.1, 0.0, 0.1], [0.1, 0.1, 0.1], [0.2, 0.1, 0.2]])
joint_matrix_2 = np.array(
[
[0.2, 0.3],
[0.4, 0.1],
]
)
single_element = np.array([1])
def test_print_inm():
for m in [noise_matrix, noise_matrix_2, single_element]:
util.print_inverse_noise_matrix(m, round_places=3)
def test_print_joint():
for m in [joint_matrix, joint_matrix_2, single_element]:
util.print_joint_matrix(m, round_places=3)
def test_print_square():
for m in [noise_matrix, noise_matrix_2, single_element]:
util.print_square_matrix(noise_matrix, round_places=3)
def test_print_noise_matrix():
for m in [noise_matrix, noise_matrix_2, single_element]:
util.print_noise_matrix(noise_matrix, round_places=3)
def test_pu_f1():
s = [1, 1, 1, 0, 0, 0]
p = [1, 1, 1, 0, 0, 0]
assert abs(util.estimate_pu_f1(s, p) - 1) < 1e-4
def test_value_counts_str():
r = util.value_counts(["a", "b", "a"])
assert all(np.array([2, 1]) - r < 1e-4)
TestCase = namedtuple("TestCase", ["labels", "id"])
value_counts_missing_classes_test_cases = [
TestCase([0, 1, 0, 2], "integers"),
TestCase(["a", "b", "a", "c"], "strings"),
TestCase([[0], [0, 1], [2]], "multilabel_integers"),
TestCase([["c"], ["a", "b"], ["a"]], "multilabel_strings"),
]
@pytest.mark.parametrize(
"test_case",
value_counts_missing_classes_test_cases,
ids=lambda x: str(x.id),
)
def test_value_counts_fill_missing_classes(test_case):
labels = test_case.labels
is_multi_label = isinstance(labels[0], list)
r = util.value_counts_fill_missing_classes(labels, num_classes=4, multi_label=is_multi_label)
assert np.array_equal(r, [2, 1, 1, 0])
def test_pu_remove_noise():
nm = np.array(
[
[0.9, 0.0, 0.0],
[0.0, 0.7, 0.4],
[0.1, 0.3, 0.6],
]
)
r = util.remove_noise_from_class(nm, 0)
assert np.all(r - nm < 1e-4)
def test_round_preserving_sum():
vec = np.array([1.1] * 10)
ints = util.round_preserving_sum(vec)
# Make sure one of ints is now 2 to preserve sum of 11
assert np.any(ints == 2)
assert sum(ints) == 11
def test_one_hot():
num_classes = 4
labels = [[0], [0, 1], [0, 1], [2], [0, 2, 3]]
assert onehot2int(int2onehot(labels, K=num_classes)) == labels
def test_num_unique():
labels = [[0], [0, 1], [0, 1], [2], [0, 2, 3]]
assert num_unique_classes(labels) == 4
def test_missing_classes():
labels = [0, 1] # class 2 is missing
pred_probs = np.array([[0.8, 0.1, 0.1], [0.4, 0.5, 0.1]])
assert get_missing_classes(labels, pred_probs=pred_probs) == [2]
def test_round_preserving_row_totals():
mat = np.array(
[
[1.7, 1.8, 1.5],
[1.1, 1.4, 1.5],
[1.3, 1.3, 1.4],
]
)
mat_int = util.round_preserving_row_totals(mat)
# Check that row sums are preserved
assert np.all(mat_int.sum(axis=1) == mat.sum(axis=1))
def test_confusion_matrix():
true = [0, 1, 1, 2, 2, 2]
pred = [0, 0, 1, 1, 1, 2]
cmat = util.confusion_matrix(true, pred)
assert np.shape(cmat) == (3, 3)
assert cmat[0][0] == 1
assert cmat[1][1] == 1
assert cmat[2][2] == 1
assert cmat[1][0] == 1
assert cmat[2][1] == 2
assert cmat[0][1] == 0
assert cmat[0][2] == 0
assert cmat[2][0] == 0
assert cmat[0][1] == 0
def test_confusion_matrix_nonconsecutive():
true = [-1, -1, -1, 1]
pred = [1, 1, -1, 1]
cmat = util.confusion_matrix(true, pred)
assert np.shape(cmat) == (2, 2)
assert cmat[0][0] == 1
assert cmat[0][1] == 2
assert cmat[1][0] == 0
assert cmat[1][1] == 1
def test_format_labels():
# test 1D labels
str_labels = np.array(["b", "b", "a", "c", "a"])
labels, label_map = format_labels(str_labels)
assert all(labels == np.array([1, 1, 0, 2, 0]))
assert label_map[0] == "a"
assert label_map[1] == "b"
assert label_map[2] == "c"
assert_valid_class_labels(labels)
def test_normalized_entropy():
"""Check that normalized entropy is well well-behaved and in [0, 1]."""
# test tiny numbers
for dtype in [np.float16, np.float32, np.float64]:
info = np.finfo(dtype)
# some NumPy versions have bugs, therefore we provide a fallback
# (fallback is the value of the smalles datatype float16)
smallest_normal = getattr(info, "smallest_normal", 6.104e-05)
smallest_subnormal = getattr(info, "smallest_subnormal", 6e-08)
for val in [info.eps, smallest_normal, smallest_subnormal, 0]:
entropy = get_normalized_entropy(np.array([[1.0, val]], dtype=dtype))
assert 0.0 <= entropy <= 1.0
# test multiple _assert_valid_inputs
entropy = get_normalized_entropy(np.array([[0.0, 1.0], [0.5, 0.5]]))
assert all((0.0 <= entropy) & (entropy <= 1.0))
# raise errors for wrong probabilities.
with pytest.raises(ValueError):
get_normalized_entropy(np.array([[-1.0, 0.5]])) # negative
get_normalized_entropy(np.array([[2.0, 0.5]])) # larger 1
def test_force_two_dimensions():
# Test with 2D array
X = np.zeros((5, 5))
X_reshaped = util.force_two_dimensions(X)
assert X_reshaped.shape == (5, 5), "The shape of 2D array should remain unchanged."
# Test with 4D array
X = np.zeros((5, 5, 5, 5))
X_reshaped = util.force_two_dimensions(X)
assert X_reshaped.shape == (
5,
125,
), "The shape of 4D array should be flattened to two dimensions."
# Test with None input
assert util.force_two_dimensions(None) is None, "None input should return None."