Skip to content

Commit 104e09a

Browse files
authored
Add labels argument to log_loss to provide labels explicitly when number of classes in y_true and y_pred differ
Fixes scikit-learn#4033 , scikit-learn#4546 , scikit-learn#6703 * fixed log_loss bug enhance log_loss labels option feature log_loss changed test log_loss case u add ValueError in log_loss * fixed error message when y_pred and y_test labels don't match fixes as per existing pull request scikit-learn#6714 fixed log_loss bug enhance log_loss labels option feature log_loss changed test log_loss case u add ValueError in log_loss fixes as per existing pull request scikit-learn#6714 fixed error message when y_pred and y_test labels don't match fixed error message when y_pred and y_test labels don't match corrected doc/whats_new.rst for syntax and with correct formatting of credits additional formatting fixes for doc/whats_new.rst fixed versionadded comment removed superfluous line removed superflous line * Wrap up changes to fix log_loss bug and clean up log_loss fix a typo in whatsnew refactor conditional and move dtype check before np.clip general cleanup of log_loss remove dtype checks edit non-regression test and wordings fix non-regression test misc doc fixes / clarifications + final touches fix naming of y_score2 variable specify log loss is only valid for 2 labels or more
2 parents 084ef97 + d97a25f commit 104e09a

File tree

3 files changed

+93
-26
lines changed

3 files changed

+93
-26
lines changed

doc/whats_new.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ Enhancements
270270
(`#6913 <https://github.com/scikit-learn/scikit-learn/pull/6913>`_)
271271
By `YenChen Lin`_.
272272

273+
- Added ``labels`` flag to :class:`metrics.log_loss` to to explicitly provide
274+
the labels when the number of classes in ``y_true`` and ``y_pred`` differ.
275+
(`#7239 <https://github.com/scikit-learn/scikit-learn/pull/7239/>`_)
276+
by `Hong Guangguo`_ with help from `Mads Jensen`_ and `Nelson Liu`_.
277+
273278
Bug fixes
274279
.........
275280

@@ -4376,3 +4381,7 @@ David Huard, Dave Morrill, Ed Schofield, Travis Oliphant, Pearu Peterson.
43764381
.. _Konstantin Podshumok: https://github.com/podshumok
43774382

43784383
.. _David Staub: https://github.com/staubda
4384+
4385+
.. _Hong Guangguo: https://github.com/hongguangguo
4386+
4387+
.. _Mads Jensen: https://github.com/indianajensen

sklearn/metrics/classification.py

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,13 +1544,15 @@ def hamming_loss(y_true, y_pred, classes=None, sample_weight=None):
15441544
raise ValueError("{0} is not supported".format(y_type))
15451545

15461546

1547-
def log_loss(y_true, y_pred, eps=1e-15, normalize=True, sample_weight=None):
1547+
def log_loss(y_true, y_pred, eps=1e-15, normalize=True, sample_weight=None,
1548+
labels=None):
15481549
"""Log loss, aka logistic loss or cross-entropy loss.
15491550
15501551
This is the loss function used in (multinomial) logistic regression
15511552
and extensions of it such as neural networks, defined as the negative
15521553
log-likelihood of the true labels given a probabilistic classifier's
1553-
predictions. For a single sample with true label yt in {0,1} and
1554+
predictions. The log loss is only defined for two or more labels.
1555+
For a single sample with true label yt in {0,1} and
15541556
estimated probability yp that yt = 1, the log loss is
15551557
15561558
-log P(yt|yp) = -(yt log(yp) + (1 - yt) log(1 - yp))
@@ -1562,9 +1564,13 @@ def log_loss(y_true, y_pred, eps=1e-15, normalize=True, sample_weight=None):
15621564
y_true : array-like or label indicator matrix
15631565
Ground truth (correct) labels for n_samples samples.
15641566
1565-
y_pred : array-like of float, shape = (n_samples, n_classes)
1567+
y_pred : array-like of float, shape = (n_samples, n_classes) or (n_samples,)
15661568
Predicted probabilities, as returned by a classifier's
1567-
predict_proba method.
1569+
predict_proba method. If ``y_pred.shape = (n_samples,)``
1570+
the probabilities provided are assumed to be that of the
1571+
positive class. The labels in ``y_pred`` are assumed to be
1572+
ordered alphabetically, as done by
1573+
:class:`preprocessing.LabelBinarizer`.
15681574
15691575
eps : float
15701576
Log loss is undefined for p=0 or p=1, so probabilities are
@@ -1577,6 +1583,12 @@ def log_loss(y_true, y_pred, eps=1e-15, normalize=True, sample_weight=None):
15771583
sample_weight : array-like of shape = [n_samples], optional
15781584
Sample weights.
15791585
1586+
labels : array-like, optional (default=None)
1587+
If not provided, labels will be inferred from y_true. If ``labels``
1588+
is ``None`` and ``y_pred`` has shape (n_samples,) the labels are
1589+
assumed to be binary and are inferred from ``y_true``.
1590+
.. versionadded:: 0.18
1591+
15801592
Returns
15811593
-------
15821594
loss : float
@@ -1596,37 +1608,58 @@ def log_loss(y_true, y_pred, eps=1e-15, normalize=True, sample_weight=None):
15961608
-----
15971609
The logarithm used is the natural logarithm (base-e).
15981610
"""
1611+
y_pred = check_array(y_pred, ensure_2d=False)
1612+
check_consistent_length(y_pred, y_true)
1613+
15991614
lb = LabelBinarizer()
1600-
T = lb.fit_transform(y_true)
1601-
if T.shape[1] == 1:
1602-
T = np.append(1 - T, T, axis=1)
16031615

1604-
y_pred = check_array(y_pred, ensure_2d=False)
1605-
# Clipping
1606-
Y = np.clip(y_pred, eps, 1 - eps)
1616+
if labels is not None:
1617+
lb.fit(labels)
1618+
else:
1619+
lb.fit(y_true)
1620+
1621+
if len(lb.classes_) == 1:
1622+
if labels is None:
1623+
raise ValueError('y_true contains only one label ({0}). Please provide '
1624+
'the true labels explicitly through the labels '
1625+
'argument.'.format(lb.classes_[0]))
1626+
else:
1627+
raise ValueError('The labels array needs to contain at least two labels'
1628+
'for log_loss, got {0}.'.format(lb.classes_))
16071629

1608-
# This happens in cases when elements in y_pred have type "str".
1609-
if not isinstance(Y, np.ndarray):
1610-
raise ValueError("y_pred should be an array of floats.")
1630+
transformed_labels = lb.transform(y_true)
1631+
1632+
if transformed_labels.shape[1] == 1:
1633+
transformed_labels = np.append(1 - transformed_labels,
1634+
transformed_labels, axis=1)
1635+
1636+
# Clipping
1637+
y_pred = np.clip(y_pred, eps, 1 - eps)
16111638

16121639
# If y_pred is of single dimension, assume y_true to be binary
16131640
# and then check.
1614-
if Y.ndim == 1:
1615-
Y = Y[:, np.newaxis]
1616-
if Y.shape[1] == 1:
1617-
Y = np.append(1 - Y, Y, axis=1)
1641+
if y_pred.ndim == 1:
1642+
y_pred = y_pred[:, np.newaxis]
1643+
if y_pred.shape[1] == 1:
1644+
y_pred = np.append(1 - y_pred, y_pred, axis=1)
16181645

16191646
# Check if dimensions are consistent.
1620-
check_consistent_length(T, Y)
1621-
T = check_array(T)
1622-
Y = check_array(Y)
1623-
if T.shape[1] != Y.shape[1]:
1624-
raise ValueError("y_true and y_pred have different number of classes "
1625-
"%d, %d" % (T.shape[1], Y.shape[1]))
1647+
transformed_labels = check_array(transformed_labels)
1648+
if len(lb.classes_) != y_pred.shape[1]:
1649+
if labels is None:
1650+
raise ValueError("y_true and y_pred contain different number of classes "
1651+
"{0}, {1}. Please provide the true labels explicitly "
1652+
"through the labels argument. Classes found in"
1653+
"y_true: {2}".format(transformed_labels.shape[1],
1654+
y_pred.shape[1], lb.classes_))
1655+
else:
1656+
raise ValueError('The number of classes in labels is different '
1657+
'from that in y_pred. Classes found in '
1658+
'labels: {0}'.format(lb.classes_))
16261659

16271660
# Renormalize
1628-
Y /= Y.sum(axis=1)[:, np.newaxis]
1629-
loss = -(T * np.log(Y)).sum(axis=1)
1661+
y_pred /= y_pred.sum(axis=1)[:, np.newaxis]
1662+
loss = -(transformed_labels * np.log(y_pred)).sum(axis=1)
16301663

16311664
return _weighted_sum(loss, sample_weight, normalize)
16321665

sklearn/metrics/tests/test_classification.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
from sklearn.metrics import zero_one_loss
4646
from sklearn.metrics import brier_score_loss
4747

48-
4948
from sklearn.metrics.classification import _check_targets
5049
from sklearn.exceptions import UndefinedMetricWarning
5150

@@ -1384,6 +1383,32 @@ def test_log_loss():
13841383
loss = log_loss(y_true, y_pred)
13851384
assert_almost_equal(loss, 1.0383217, decimal=6)
13861385

1386+
# test labels option
1387+
1388+
y_true = [2, 2]
1389+
y_pred = [[0.2, 0.7], [0.6, 0.5]]
1390+
y_score = np.array([[0.1, 0.9], [0.1, 0.9]])
1391+
error_str = ('y_true contains only one label (2). Please provide '
1392+
'the true labels explicitly through the labels argument.')
1393+
assert_raise_message(ValueError, error_str, log_loss, y_true, y_pred)
1394+
1395+
y_pred = [[0.2, 0.7], [0.6, 0.5], [0.2, 0.3]]
1396+
error_str = ('Found arrays with inconsistent numbers of '
1397+
'samples: [2 3]')
1398+
assert_raise_message(ValueError, error_str, log_loss, y_true, y_pred)
1399+
1400+
# works when the labels argument is used
1401+
1402+
true_log_loss = -np.mean(np.log(y_score[:, 1]))
1403+
calculated_log_loss = log_loss(y_true, y_score, labels=[1, 2])
1404+
assert_almost_equal(calculated_log_loss, true_log_loss)
1405+
1406+
# ensure labels work when len(np.unique(y_true)) != y_pred.shape[1]
1407+
y_true = [1, 2, 2]
1408+
y_score2 = [[0.2, 0.7, 0.3], [0.6, 0.5, 0.3], [0.3, 0.9, 0.1]]
1409+
loss = log_loss(y_true, y_score2, labels=[1, 2, 3])
1410+
assert_almost_equal(loss, 1.0630345, decimal=6)
1411+
13871412

13881413
def test_log_loss_pandas_input():
13891414
# case when input is a pandas series and dataframe gh-5715

0 commit comments

Comments
 (0)