Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 19c068f

Browse files
MAINT Clean up deprecations for 1.5: in log_loss (#28851)
Co-authored-by: Guillaume Lemaitre <[email protected]>
1 parent f9cab76 commit 19c068f

File tree

3 files changed

+75
-103
lines changed

3 files changed

+75
-103
lines changed

sklearn/metrics/_classification.py

+16-43
Original file line numberDiff line numberDiff line change
@@ -2816,16 +2816,13 @@ def hamming_loss(y_true, y_pred, *, sample_weight=None):
28162816
{
28172817
"y_true": ["array-like"],
28182818
"y_pred": ["array-like"],
2819-
"eps": [StrOptions({"auto"}), Interval(Real, 0, 1, closed="both")],
28202819
"normalize": ["boolean"],
28212820
"sample_weight": ["array-like", None],
28222821
"labels": ["array-like", None],
28232822
},
28242823
prefer_skip_nested_validation=True,
28252824
)
2826-
def log_loss(
2827-
y_true, y_pred, *, eps="auto", normalize=True, sample_weight=None, labels=None
2828-
):
2825+
def log_loss(y_true, y_pred, *, normalize=True, sample_weight=None, labels=None):
28292826
r"""Log loss, aka logistic loss or cross-entropy loss.
28302827
28312828
This is the loss function used in (multinomial) logistic regression
@@ -2855,19 +2852,8 @@ def log_loss(
28552852
ordered alphabetically, as done by
28562853
:class:`~sklearn.preprocessing.LabelBinarizer`.
28572854
2858-
eps : float or "auto", default="auto"
2859-
Log loss is undefined for p=0 or p=1, so probabilities are
2860-
clipped to `max(eps, min(1 - eps, p))`. The default will depend on the
2861-
data type of `y_pred` and is set to `np.finfo(y_pred.dtype).eps`.
2862-
2863-
.. versionadded:: 1.2
2864-
2865-
.. versionchanged:: 1.2
2866-
The default value changed from `1e-15` to `"auto"` that is
2867-
equivalent to `np.finfo(y_pred.dtype).eps`.
2868-
2869-
.. deprecated:: 1.3
2870-
`eps` is deprecated in 1.3 and will be removed in 1.5.
2855+
`y_pred` values are clipped to `[eps, 1-eps]` where `eps` is the machine
2856+
precision for `y_pred`'s dtype.
28712857
28722858
normalize : bool, default=True
28732859
If true, return the mean loss per sample.
@@ -2907,18 +2893,6 @@ def log_loss(
29072893
y_pred = check_array(
29082894
y_pred, ensure_2d=False, dtype=[np.float64, np.float32, np.float16]
29092895
)
2910-
if eps == "auto":
2911-
eps = np.finfo(y_pred.dtype).eps
2912-
else:
2913-
# TODO: Remove user defined eps in 1.5
2914-
warnings.warn(
2915-
(
2916-
"Setting the eps parameter is deprecated and will "
2917-
"be removed in 1.5. Instead eps will always have"
2918-
"a default value of `np.finfo(y_pred.dtype).eps`."
2919-
),
2920-
FutureWarning,
2921-
)
29222896

29232897
check_consistent_length(y_pred, y_true, sample_weight)
29242898
lb = LabelBinarizer()
@@ -2949,16 +2923,26 @@ def log_loss(
29492923
1 - transformed_labels, transformed_labels, axis=1
29502924
)
29512925

2952-
# Clipping
2953-
y_pred = np.clip(y_pred, eps, 1 - eps)
2954-
29552926
# If y_pred is of single dimension, assume y_true to be binary
29562927
# and then check.
29572928
if y_pred.ndim == 1:
29582929
y_pred = y_pred[:, np.newaxis]
29592930
if y_pred.shape[1] == 1:
29602931
y_pred = np.append(1 - y_pred, y_pred, axis=1)
29612932

2933+
eps = np.finfo(y_pred.dtype).eps
2934+
2935+
# Make sure y_pred is normalized
2936+
y_pred_sum = y_pred.sum(axis=1)
2937+
if not np.allclose(y_pred_sum, 1, rtol=np.sqrt(eps)):
2938+
warnings.warn(
2939+
"The y_pred values do not sum to one. Make sure to pass probabilities.",
2940+
UserWarning,
2941+
)
2942+
2943+
# Clipping
2944+
y_pred = np.clip(y_pred, eps, 1 - eps)
2945+
29622946
# Check if dimensions are consistent.
29632947
transformed_labels = check_array(transformed_labels)
29642948
if len(lb.classes_) != y_pred.shape[1]:
@@ -2979,17 +2963,6 @@ def log_loss(
29792963
"labels: {0}".format(lb.classes_)
29802964
)
29812965

2982-
# Renormalize
2983-
y_pred_sum = y_pred.sum(axis=1)
2984-
if not np.isclose(y_pred_sum, 1, rtol=1e-15, atol=5 * eps).all():
2985-
warnings.warn(
2986-
(
2987-
"The y_pred values do not sum to one. Starting from 1.5 this"
2988-
"will result in an error."
2989-
),
2990-
UserWarning,
2991-
)
2992-
y_pred = y_pred / y_pred_sum[:, np.newaxis]
29932966
loss = -xlogy(transformed_labels, y_pred).sum(axis=1)
29942967

29952968
return float(_average(loss, weights=sample_weight, normalize=normalize))

sklearn/metrics/tests/test_classification.py

+47-57
Original file line numberDiff line numberDiff line change
@@ -2624,62 +2624,37 @@ def test_log_loss():
26242624
)
26252625
loss = log_loss(y_true, y_pred)
26262626
loss_true = -np.mean(bernoulli.logpmf(np.array(y_true) == "yes", y_pred[:, 1]))
2627-
assert_almost_equal(loss, loss_true)
2627+
assert_allclose(loss, loss_true)
26282628

26292629
# multiclass case; adapted from http://bit.ly/RJJHWA
26302630
y_true = [1, 0, 2]
26312631
y_pred = [[0.2, 0.7, 0.1], [0.6, 0.2, 0.2], [0.6, 0.1, 0.3]]
26322632
loss = log_loss(y_true, y_pred, normalize=True)
2633-
assert_almost_equal(loss, 0.6904911)
2633+
assert_allclose(loss, 0.6904911)
26342634

26352635
# check that we got all the shapes and axes right
26362636
# by doubling the length of y_true and y_pred
26372637
y_true *= 2
26382638
y_pred *= 2
26392639
loss = log_loss(y_true, y_pred, normalize=False)
2640-
assert_almost_equal(loss, 0.6904911 * 6, decimal=6)
2641-
2642-
user_warning_msg = "y_pred values do not sum to one"
2643-
# check eps and handling of absolute zero and one probabilities
2644-
y_pred = np.asarray(y_pred) > 0.5
2645-
with pytest.warns(FutureWarning):
2646-
loss = log_loss(y_true, y_pred, normalize=True, eps=0.1)
2647-
with pytest.warns(UserWarning, match=user_warning_msg):
2648-
assert_almost_equal(loss, log_loss(y_true, np.clip(y_pred, 0.1, 0.9)))
2649-
2650-
# binary case: check correct boundary values for eps = 0
2651-
with pytest.warns(FutureWarning):
2652-
assert log_loss([0, 1], [0, 1], eps=0) == 0
2653-
with pytest.warns(FutureWarning):
2654-
assert log_loss([0, 1], [0, 0], eps=0) == np.inf
2655-
with pytest.warns(FutureWarning):
2656-
assert log_loss([0, 1], [1, 1], eps=0) == np.inf
2657-
2658-
# multiclass case: check correct boundary values for eps = 0
2659-
with pytest.warns(FutureWarning):
2660-
assert log_loss([0, 1, 2], [[1, 0, 0], [0, 1, 0], [0, 0, 1]], eps=0) == 0
2661-
with pytest.warns(FutureWarning):
2662-
assert (
2663-
log_loss([0, 1, 2], [[0, 0.5, 0.5], [0, 1, 0], [0, 0, 1]], eps=0) == np.inf
2664-
)
2640+
assert_allclose(loss, 0.6904911 * 6)
26652641

26662642
# raise error if number of classes are not equal.
26672643
y_true = [1, 0, 2]
2668-
y_pred = [[0.2, 0.7], [0.6, 0.5], [0.4, 0.1]]
2644+
y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6]]
26692645
with pytest.raises(ValueError):
26702646
log_loss(y_true, y_pred)
26712647

26722648
# case when y_true is a string array object
26732649
y_true = ["ham", "spam", "spam", "ham"]
2674-
y_pred = [[0.2, 0.7], [0.6, 0.5], [0.4, 0.1], [0.7, 0.2]]
2675-
with pytest.warns(UserWarning, match=user_warning_msg):
2676-
loss = log_loss(y_true, y_pred)
2677-
assert_almost_equal(loss, 1.0383217, decimal=6)
2650+
y_pred = [[0.3, 0.7], [0.6, 0.4], [0.4, 0.6], [0.7, 0.3]]
2651+
loss = log_loss(y_true, y_pred)
2652+
assert_allclose(loss, 0.7469410)
26782653

26792654
# test labels option
26802655

26812656
y_true = [2, 2]
2682-
y_pred = [[0.2, 0.7], [0.6, 0.5]]
2657+
y_pred = [[0.2, 0.8], [0.6, 0.4]]
26832658
y_score = np.array([[0.1, 0.9], [0.1, 0.9]])
26842659
error_str = (
26852660
r"y_true contains only one label \(2\). Please provide "
@@ -2688,50 +2663,66 @@ def test_log_loss():
26882663
with pytest.raises(ValueError, match=error_str):
26892664
log_loss(y_true, y_pred)
26902665

2691-
y_pred = [[0.2, 0.7], [0.6, 0.5], [0.2, 0.3]]
2692-
error_str = "Found input variables with inconsistent numbers of samples: [3, 2]"
2693-
(ValueError, error_str, log_loss, y_true, y_pred)
2666+
y_pred = [[0.2, 0.8], [0.6, 0.4], [0.7, 0.3]]
2667+
error_str = r"Found input variables with inconsistent numbers of samples: \[3, 2\]"
2668+
with pytest.raises(ValueError, match=error_str):
2669+
log_loss(y_true, y_pred)
26942670

26952671
# works when the labels argument is used
26962672

26972673
true_log_loss = -np.mean(np.log(y_score[:, 1]))
26982674
calculated_log_loss = log_loss(y_true, y_score, labels=[1, 2])
2699-
assert_almost_equal(calculated_log_loss, true_log_loss)
2675+
assert_allclose(calculated_log_loss, true_log_loss)
27002676

27012677
# ensure labels work when len(np.unique(y_true)) != y_pred.shape[1]
27022678
y_true = [1, 2, 2]
2703-
y_score2 = [[0.2, 0.7, 0.3], [0.6, 0.5, 0.3], [0.3, 0.9, 0.1]]
2704-
with pytest.warns(UserWarning, match=user_warning_msg):
2705-
loss = log_loss(y_true, y_score2, labels=[1, 2, 3])
2706-
assert_almost_equal(loss, 1.0630345, decimal=6)
2679+
y_score2 = [[0.7, 0.1, 0.2], [0.2, 0.7, 0.1], [0.1, 0.7, 0.2]]
2680+
loss = log_loss(y_true, y_score2, labels=[1, 2, 3])
2681+
assert_allclose(loss, -np.log(0.7))
2682+
27072683

2684+
@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.float16])
2685+
def test_log_loss_eps(dtype):
2686+
"""Check the behaviour internal eps that changes depending on the input dtype.
27082687
2709-
def test_log_loss_eps_auto(global_dtype):
2710-
"""Check the behaviour of `eps="auto"` that changes depending on the input
2711-
array dtype.
27122688
Non-regression test for:
27132689
https://github.com/scikit-learn/scikit-learn/issues/24315
27142690
"""
2715-
y_true = np.array([0, 1], dtype=global_dtype)
2716-
y_pred = y_true.copy()
2691+
y_true = np.array([0, 1], dtype=dtype)
2692+
y_pred = np.array([1, 0], dtype=dtype)
27172693

2718-
loss = log_loss(y_true, y_pred, eps="auto")
2694+
loss = log_loss(y_true, y_pred)
27192695
assert np.isfinite(loss)
27202696

27212697

2722-
def test_log_loss_eps_auto_float16():
2723-
"""Check the behaviour of `eps="auto"` for np.float16"""
2724-
y_true = np.array([0, 1], dtype=np.float16)
2725-
y_pred = y_true.copy()
2698+
@pytest.mark.parametrize("dtype", [np.float64, np.float32, np.float16])
2699+
def test_log_loss_not_probabilities_warning(dtype):
2700+
"""Check that log_loss raises a warning when y_pred values don't sum to 1."""
2701+
y_true = np.array([0, 1, 1, 0])
2702+
y_pred = np.array([[0.2, 0.7], [0.6, 0.3], [0.4, 0.7], [0.8, 0.3]], dtype=dtype)
27262703

2727-
loss = log_loss(y_true, y_pred, eps="auto")
2728-
assert np.isfinite(loss)
2704+
with pytest.warns(UserWarning, match="The y_pred values do not sum to one."):
2705+
log_loss(y_true, y_pred)
2706+
2707+
2708+
@pytest.mark.parametrize(
2709+
"y_true, y_pred",
2710+
[
2711+
([0, 1, 0], [0, 1, 0]),
2712+
([0, 1, 0], [[1, 0], [0, 1], [1, 0]]),
2713+
([0, 1, 2], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]),
2714+
],
2715+
)
2716+
def test_log_loss_perfect_predictions(y_true, y_pred):
2717+
"""Check that log_loss returns 0 for perfect predictions."""
2718+
# Because of the clipping, the result is not exactly 0
2719+
assert log_loss(y_true, y_pred) == pytest.approx(0)
27292720

27302721

27312722
def test_log_loss_pandas_input():
27322723
# case when input is a pandas series and dataframe gh-5715
27332724
y_tr = np.array(["ham", "spam", "spam", "ham"])
2734-
y_pr = np.array([[0.2, 0.7], [0.6, 0.5], [0.4, 0.1], [0.7, 0.2]])
2725+
y_pr = np.array([[0.3, 0.7], [0.6, 0.4], [0.4, 0.6], [0.7, 0.3]])
27352726
types = [(MockDataFrame, MockDataFrame)]
27362727
try:
27372728
from pandas import DataFrame, Series
@@ -2742,9 +2733,8 @@ def test_log_loss_pandas_input():
27422733
for TrueInputType, PredInputType in types:
27432734
# y_pred dataframe, y_true series
27442735
y_true, y_pred = TrueInputType(y_tr), PredInputType(y_pr)
2745-
with pytest.warns(UserWarning, match="y_pred values do not sum to one"):
2746-
loss = log_loss(y_true, y_pred)
2747-
assert_almost_equal(loss, 1.0383217, decimal=6)
2736+
loss = log_loss(y_true, y_pred)
2737+
assert_allclose(loss, 0.7469410)
27482738

27492739

27502740
def test_brier_score_loss():

sklearn/metrics/tests/test_common.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,10 @@ def test_sample_order_invariance_multilabel_and_multioutput():
637637
# Generate some data
638638
y_true = random_state.randint(0, 2, size=(20, 25))
639639
y_pred = random_state.randint(0, 2, size=(20, 25))
640-
y_score = random_state.normal(size=y_true.shape)
640+
y_score = random_state.uniform(size=y_true.shape)
641+
642+
# Some metrics (e.g. log_loss) require y_score to be probabilities (sum to 1)
643+
y_score /= y_score.sum(axis=1, keepdims=True)
641644

642645
y_true_shuffle, y_pred_shuffle, y_score_shuffle = shuffle(
643646
y_true, y_pred, y_score, random_state=0
@@ -1566,7 +1569,10 @@ def test_multilabel_sample_weight_invariance(name):
15661569
)
15671570
y_true = np.vstack([ya, yb])
15681571
y_pred = np.vstack([ya, ya])
1569-
y_score = random_state.randint(1, 4, size=y_true.shape)
1572+
y_score = random_state.uniform(size=y_true.shape)
1573+
1574+
# Some metrics (e.g. log_loss) require y_score to be probabilities (sum to 1)
1575+
y_score /= y_score.sum(axis=1, keepdims=True)
15701576

15711577
metric = ALL_METRICS[name]
15721578
if name in THRESHOLDED_METRICS:
@@ -1629,7 +1635,10 @@ def test_thresholded_multilabel_multioutput_permutations_invariance(name):
16291635
random_state = check_random_state(0)
16301636
n_samples, n_classes = 20, 4
16311637
y_true = random_state.randint(0, 2, size=(n_samples, n_classes))
1632-
y_score = random_state.normal(size=y_true.shape)
1638+
y_score = random_state.uniform(size=y_true.shape)
1639+
1640+
# Some metrics (e.g. log_loss) require y_score to be probabilities (sum to 1)
1641+
y_score /= y_score.sum(axis=1, keepdims=True)
16331642

16341643
# Makes sure all samples have at least one label. This works around errors
16351644
# when running metrics where average="sample"

0 commit comments

Comments
 (0)