Skip to content

[MRG] Fix MultinomialDeviance not using average logloss (#10055)#10081

Closed
rempfler wants to merge 9 commits intoscikit-learn:masterfrom
rempfler:multinomial-deviance-mean
Closed

[MRG] Fix MultinomialDeviance not using average logloss (#10055)#10081
rempfler wants to merge 9 commits intoscikit-learn:masterfrom
rempfler:multinomial-deviance-mean

Conversation

@rempfler
Copy link
Copy Markdown

@rempfler rempfler commented Nov 6, 2017

Fixes #10055

Changes

Changed MultinomialDeviance from total logloss to average logloss.

@jnothman
Copy link
Copy Markdown
Member

jnothman commented Nov 7, 2017

Please add a test.

Re the codecov failure: It seems we're never running these losses with sample_weight=None (there's a fixme in the file to suggest it might be a good idea to support sample_weight=None more explicitly). I find it strange that such cases are implemented separately in the loss functions when they should provide negligible computational complexity improvement, and when they are never called. IMO, doing if sample_weight=None: sample_weight = np.array(1) in this case should
suffice...

@j-xiao
Copy link
Copy Markdown

j-xiao commented Nov 8, 2017

not sure if need to change the computation of gradient also, I did not check the detail, but most like should update the gradient also.

@massich
Copy link
Copy Markdown
Contributor

massich commented Nov 9, 2017

IMO, doing if sample_weight=None: sample_weight = np.array(1) in this case should

using numpy.average avoids the trouble all together. Since it has a weight parameter that can be None. see:

return np.average(sample_score, weights=sample_weight)

@rempfler
Copy link
Copy Markdown
Author

IMO, doing if sample_weight=None: sample_weight = np.array(1)

This wouldnt take the mean in the unweighted case, since sample_weight.sum() would be 1, rather than the number of elements.

using numpy.average avoids the trouble all together.

Good idea.

Having looked at [1, Sect. 10.6], I actually believe the call with weights was not correct so far, since the sample_weights are not multiplied with the logsumexp-term:

     else:
        return np.sum(-1 * sample_weight * (Y * pred).sum(axis=1) +
                      logsumexp(pred, axis=1))

[1] Friedman, Jerome, Trevor Hastie, and Robert Tibshirani. The elements of statistical learning. Vol. 1. New York: Springer series in statistics, 2001.

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 13, 2017

Codecov Report

Merging #10081 into master will increase coverage by 0.01%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #10081      +/-   ##
==========================================
+ Coverage   96.19%   96.21%   +0.01%     
==========================================
  Files         336      337       +1     
  Lines       62739    62899     +160     
==========================================
+ Hits        60353    60518     +165     
+ Misses       2386     2381       -5
Impacted Files Coverage Δ
...ble/tests/test_gradient_boosting_loss_functions.py 95.62% <100%> (+0.57%) ⬆️
sklearn/ensemble/gradient_boosting.py 96.5% <100%> (+0.29%) ⬆️
sklearn/utils/estimator_checks.py 93.29% <0%> (-0.02%) ⬇️
sklearn/cluster/_feature_agglomeration.py 100% <0%> (ø) ⬆️
sklearn/datasets/tests/test_samples_generator.py 100% <0%> (ø) ⬆️
sklearn/ensemble/bagging.py 96.61% <0%> (ø) ⬆️
sklearn/feature_selection/base.py 94.79% <0%> (ø) ⬆️
sklearn/neighbors/regression.py 100% <0%> (ø) ⬆️
sklearn/metrics/ranking.py 98.8% <0%> (ø) ⬆️
...klearn/cluster/tests/test_feature_agglomeration.py 100% <0%> (ø)
... and 11 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update abb43c1...4970b02. Read the comment docs.

@massich
Copy link
Copy Markdown
Contributor

massich commented Nov 14, 2017

good catch!

LGTM

Actually the test is just checking that something is computed. Can we find an example where we know the expected output for a given input? Something that serves as a regression test for the previous wrong computation. In other words, a wrong computation shows that we were missing this thest.

@jnothman
Copy link
Copy Markdown
Member

I don't think your edited comment came through on email, @massich, requesting a better test...

@rempfler
Copy link
Copy Markdown
Author

@massich: technically, the previously wrong computation would have been caught by

    loss_w_sw = loss(y, p, 0.5 * np.ones(p.shape[0], dtype=np.float32))
    assert_almost_equal(loss_wo_sw, loss_w_sw)

where the logsumexp terms would have had increased weight in the wrong case.

Nonetheless, i added one more small test with a manually pre-computed input-output pair.

loss_w_sw = loss(y, p, 0.5 * np.ones(p.shape[0], dtype=np.float32))
assert_almost_equal(loss_wo_sw, loss_w_sw)

# second check
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this its own test

def test_loss_computation(pred=np.array([[1.0, 0, 0],
                                         [0, 0.5, 0.5]]),
                          y=np.array([0, 1]),
                          weights=np.array([1, 3]),
                          expected_loss=0.85637):
   assert_almost_equal(loss(y, pred, weights), expected_loss, decimal=4)

Maybe even add a test for weights=None.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also split the rest of the test. I think that testing that the error raising should go on its own, etc..

@massich
Copy link
Copy Markdown
Contributor

massich commented Nov 30, 2017

LGTM



def test_mdl_exception():
# Check that MultinomialDeviance throws when n_classes <= 2
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throws an error


def test_mdl_exception():
# Check that MultinomialDeviance throws when n_classes <= 2
assert_raises(ValueError, MultinomialDeviance, 2)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use assert_raises_regex and check which message is raise. We tend to check the error message rather than only checking that something is wrong ;)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we use the context manager here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not since sklearn already have helper function. If we want to use the context manager, we need to change all other asser_raises_*

assert_almost_equal(loss_wo_sw, loss_w_sw)


def test_mdl_computation_unweighted(pred=np.array([[1.0, 0, 0],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you factorize this test and the next one using pytest.mark.parametrize?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glemaitre I proposed such parametrization to avoid import pytest, since I'm not sure if @lesteve was trying to avoid the pytest dependency.

assert deviance_wo_w == deviance_w_w


def test_multinomial_deviance():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you can use pytest.mark.parametrize as well here.

@glemaitre
Copy link
Copy Markdown
Member

glemaitre commented Nov 30, 2017 via email

@massich
Copy link
Copy Markdown
Contributor

massich commented Nov 30, 2017

Ok that changes things !

@lesteve
Copy link
Copy Markdown
Member

lesteve commented Dec 4, 2017

@glemaitre I proposed such parametrization to avoid import pytest, since I'm not sure if @lesteve was trying to avoid the pytest dependency.

For reference, I think we do not want pytest dependency in our modules but it is fine to use pytest in test_*.py files. Basically you should not require that pytest is installed if you just want to use scikit-learn. This is what CHECK_PYTEST_SOFT_DEPENDENCY is trying to enforce in .travis.yml.

About whether we should import directly pytest in test_*.py files I don't know. Personally I would say it is fine. One argument from never importing pytest directly would be in case we want to switch to a different testing framework than pytest. You could argue that we should keep all test-related stuff in sklearn.utils.testing and import from sklearn.utils.testing in test_*.py files. This is what we did when we wanted to make the move away from nose easier. Personally I think worrying about whether one day we will move away from pytest is YAGNI. Having said that, the nose to pytest move was a bit of a pain so maybe we want to make a similar move easier in the future.

@jnothman
Copy link
Copy Markdown
Member

jnothman commented Dec 4, 2017 via email

@lesteve
Copy link
Copy Markdown
Member

lesteve commented Dec 5, 2017

I don't think there is any good reason to apprehend a future in which we stop using pytest. by all means, import it in test files, use its built-in fixtures, etc.

Thanks this is what I felt as well, but this is always appreciated to have a second opinion.

@jnothman
Copy link
Copy Markdown
Member

@glemaitre, @massich: your thoughts on the latest changes?

@glemaitre
Copy link
Copy Markdown
Member

I would personally use pytest.approx or assert_allclose (I would probably used this one since it is wrapped in testing module) instead of assert_almost_equal.
Apart of that LGTM.

@cmarmo
Copy link
Copy Markdown
Contributor

cmarmo commented Jun 18, 2020

@glemaitre, @amueller, @lesteve, is the needed decision related to import or not pytest in the test? I suppose this is no longer a question...

@lesteve
Copy link
Copy Markdown
Member

lesteve commented Jun 19, 2020

is the needed decision related to import or not pytest in the test? I suppose this is no longer a question...

Yeah importing pytest in the tests is a no-brainer. Looks like this one would need conflicts to be fixed and some reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MultinomialDeviance in GradientBoostingClassifier should use average logloss instead of total logloss.

8 participants