214 lines
6.1 KiB
Python
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."
|