8000 SLEP006 - Add Metadata Routing to LogisticRegressionCV by OmarManzoor · Pull Request #24498 · scikit-learn/scikit-learn · GitHub
[go: up one dir, main page]

Skip to content

SLEP006 - Add Metadata Routing to LogisticRegressionCV #24498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

OmarManzoor
Copy link
Contributor

Reference Issues/PRs

Towards: #22893

What does this implement/fix? Explain your changes.

  • Added meta data routing to LogisticRegressionCV.
  • Added add_self to specify that LogisticRegressionCV is also a consumer.
  • Added routing for the cv and score methods

Any other comments?

  • I might need some guidance on where to add further tests for this since it is not exactly a meta estimator.

Copy link
Member
@jnothman jnothman left a comment

Choose a reason for hiding this comment

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

Thanks for this @omar! Nice work!
I agree it's hard to know what to test, and we should probably define expectations for this family of PRs.

I'd love to see a test, or an example, that uses a cv=GroupKFold(n_splits=5) or similar, which is a real motivating use case.

I can also see an error here: if the scorer requests sample_weight it won't receive it, and, unless I'm much mistaken, this will happen silently (i.e. there will be no error).

To fix this, we need to pass sample_weight to process_routing in fit. We would benefit from a test that the scorer receives sample_weight if requested.

@OmarManzoor
Copy link
Contributor Author

Thanks for this @omar! Nice work! I agree it's hard to know what to test, and we should probably define expectations for this family of PRs.

I'd love to see a test, or an example, that uses a cv=GroupKFold(n_splits=5) or similar, which is a real motivating use case.

I can also see an error here: if the scorer requests sample_weight it won't receive it, and, unless I'm much mistaken, this will happen silently (i.e. there will be no error).

To fix this, we need to pass sample_weight to process_routing in fit. We would benefit from a test that the scorer receives sample_weight if requested.

Thank you for the guidance!

@OmarManzoor
Copy link
Contributor Author
OmarManzoor commented Sep 26, 2022

Thanks for this @omar! Nice work! I agree it's hard to know what to test, and we should probably define expectations for this family of PRs.
I'd love to see a test, or an example, that uses a cv=GroupKFold(n_splits=5) or similar, which is a real motivating use case.
I can also see an error here: if the scorer requests sample_weight it won't receive it, and, unless I'm much mistaken, this will happen silently (i.e. there will be no error).
To fix this, we need to pass sample_weight to process_routing in fit. We would benefit from a test that the scorer receives sample_weight if requested.

Thank you for the guidance!

I can't quite understand properly how to route the scoring parameters. Actually even if I pass sample weight here

routed_params = process_routing( obj=self, method="fit", other_params=fit_params, sample_weight=sample_weight, )

we still need someway to specify the child method of the scorer. However the available methods only contain score whereas any individual scorers themselves do not seem seem to define a score method. They instead have _score which is called through the __call__ method of the base scorer. Could you kindly guide me a bit here? @jnothman

@jnothman
Copy link
Member

Scorers don't have a score method, but for the purpose of allowing the user to set routing requests, we call it 'score'. So when the scorer is applied, we need to pass it the parameters that are routed to it by the name 'score'. Does that help, or is a code snippet more the right kind of support here?

Copy link
Member
@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

These PRs should also add the meta estimators here: https://github.com/scikit-learn/scikit-learn/blob/sample-props/sklearn/tests/test_metaestimators_metadata_routing.py

@jnothman anything else you think should be tested on all meta-estimators apart from what's there?

)

return scoring(
self, X, y, sample_weight=sample_weight, **routed_params.scorer.score
Copy link
Member

Choose a reason for hiding this comment

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

routed_params.scorer.score would include sample_weight, no need to pass it explicitly.

@OmarManzoor
Copy link
Contributor Author

https://github.com/scikit-learn/scikit-learn/blob/sample-props/sklearn/tests/test_metaestimators_metadata_routing.py

How would I actually define the estimator_name for this case?

@adrinjalali
Copy link
Member

@OmarManzoor your link is to a file, I'm not sure what you're referring to :)

@OmarManzoor
Copy link
Contributor Author

@OmarManzoor your link is to a file, I'm not sure what you're referring to :)

For example
{ "metaestimator": MultiOutputRegressor, "estimator_name": "estimator", "estimator": ConsumingRegressor, "X": X, "y": y_multi, "routing_methods": ["fit", "partial_fit"], "warns_on": { "fit": ["sample_weight", "metadata"], "partial_fit": ["sample_weight"], }, },

We need the estimator_name here. For meta estimators this should be just the estimator parameter name. In this case it is a little different.

@adrinjalali
Copy link
Member

Oh I see what you mean. This meta-estimator doesn't actually have any sub-estimators the way others do.

So we kinda need to replicate the same kinds of tests, but for scorer and cv in this case. @BenjaminBossan might have more concrete ideas.

Ideally we'd have them in a way to use them for other similar *CV classes.

@OmarManzoor
Copy link
Contributor Author
OmarManzoor commented Oct 5, 2022

Oh I see what you mean. This meta-estimator doesn't actually have any sub-estimators the way others do.

So we kinda need to replicate the same kinds of tests, but for scorer and cv in this case. @BenjaminBossan might have more concrete ideas.

Ideally we'd have them in a way to use them for other similar *CV classes.

@adrinjalali Could you kindly clarify one thing. For scorers it seems like we won't raise any warnings or errors since when routing we actually search for the actual method specified, return getattr(self, method)._route_params(params=params). In the case of scorers there is no "score" method so there is technically no set request for this method. So when writing tests should we ignore both the warning and error tests? Moreover this function test_setting_request_removes_warning_or_error does not check any recorded metadata and only checks for the correct functioning of the requested metadata. Since no errors or warnings are raised in the first place this would not be a beneficial test. Should we then create a separate function to test for the scorers of the meta estimators which somehow records the meta data and checks it or is there something else that we need to do for scorers?

@adrinjalali
Copy link
Member

For scorers we would raise an error if routing is not correct. For instance, if the scorer supports sample_weight, and no request is set for score, but sample_weight is passed, we raise.

We don't warn for them since the existing code doesn't do the routing, therefore all of it would be new functionality.

I'm not sure why you say the test_setting_request_removes_warning_or_error test is not beneficial. I'm happy to add more tests, would you like to write it and we review here?

@OmarManzoor
Copy link
Contributor Author

For scorers we would raise an error if routing is not correct. For instance, if the scorer supports sample_weight, and no request is set for score, but sample_weight is passed, we raise.

We don't warn for them since the existing code doesn't do the routing, therefore all of it would be new functionality.

I'm not sure why you say the test_setting_request_removes_warning_or_error test is not beneficial. I'm happy to add more tests, would you like to write it and we review here?

Well if an error is raised then I think we can use the existing tests. However in that case I am not clear on how the error would be raised, since the underlying scorer does not have a "score" method whereas we try to check the signature of the score method since the "score" method is passed in the routing processing.

@adrinjalali
Copy link
Member

The scorer is the score method itself, and the meta-estimator is the one doing the routing, and that's where the error would occur if anything is wrong.

Instead of checking the signature, we should now check the routing of the scorer object. Scorers 8000 expose routing the same way as estimators do.

@OmarManzoor
Copy link
Contributor Author

The scorer is the score method itself, and the meta-estimator is the one doing the routing, and that's where the error would occur if anything is wrong.

Instead of checking the signature, we should now check the routing of the scorer object. Scorers expose routing the same way as estimators do.

So I was trying to create a ConsumingScorer and use it within the existing tests structure in test_metaestimators_metadata_routing. But I seem to be doing it incorrectly. Would it be okay for me to push the code so that you can take a look and guide me?

@adrinjalali
Copy link
Member

Sure, happy to check.

@OmarManzoor
Copy link
Contributor Author

Sure, happy to check.

Pushed the code. It does not raise an error for the function test_error_for_other_methods.

@adrinjalali
Copy link
Member

Could you please merge with the latest scikit-learn/sample-props branch?

@OmarManzoor
Copy link
Contributor Author

@adrinjalali Any feedback on this? Is it incorrect to test for the ConsumingScorer like this? Instead should we actually check the actual routing data and decide based on that?

@adrinjalali
Copy link
Member

I'm trying to figure out why the CI fails here. But I get a build not found error when I try to look. Could you please send an empty commit? Hoping to see what the issue here is.

Copy link
Member
@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

This is great work, @OmarManzoor, thank you!

Comment on lines 1817 to 1820
# Remove the sample_weight parameter from score_params as it is
# explicitly being passed in the path_func.
score_params = routed_params.scorer.score
score_params.pop("sample_weight", None)
Copy link
Member

Choose a reason for hiding this comment

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

those sample weights are used for fit, while the score params passed are used to score. The _log_reg_scoring_path with the current implementation doesn't take sample_weight into account for scoring since it doesn't pass it to the score method if sample_weight is passed. Either here it needs to not be popped, or there it needs to be handled explicitly.

1E80

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason I popped it is because sample_weight is also explicitly being passed to the _log_reg_scoring_path method. If we pass sample_weight directly and then also pass it through the score_params it just seems to be duplicated.

@@ -1984,3 +1985,33 @@ def test_warning_on_penalty_string_none():
)
with pytest.warns(FutureWarning, match=warning_message):
lr.fit(iris.data, target)


def test_lr_cv_scorer_does_not_receive_sample_weight_when_score_request_is_not_set():
Copy link
Member

Choose a reason for hiding this comment

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

I quite like these two tests, but the tests you had in the common tests were also in the right direction. I don't think the tests here cover what we need to test:

In common tests we actually test if metadata is passed when it should, to the underlying objects, and here we don't do that. I think it'd be nice to have specific tests for LRCV in the common tests using the machinary there, even if they're not a part of the common tests as the other ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem with the ones in the common tests was that they were not actually working properly for scorers. The score method does not raise an exception in the inner implementation of MethodMetadataRequest, if we don't set the score request.

Copy link
Member

Choose a reason for hiding this comment

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

Can you please write a minimal test for it? It should raise, there shouldn't be a difference between fit and score in that regard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you kindly provide an example of a scorer with LogisticRegressionCV that would raise UnsetMetadataPassedError when sample_weight is passed but the score request is not set?

Copy link
Member

Choose a reason for hiding this comment

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

generally it's not the consumer which raises the error, rather the router does that when process_routing is called. So the only thing we need to test is for the scorer to have the right request values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see so should I then use the code that I had earlier to record the metadata in the sample scorer and then check those values?

Copy link
Member

Choose a reason for hiding this comment

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

yes, pretty much, very similar to how we do it in consumer estimators there.

Copy link
Contributor Author
@OmarManzoor OmarManzoor Nov 4, 2022

Choose a reason for hiding this comment

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

@adrinjalali The tests in CircleCI seem to be failing. I actually reverted the code to the point where it passed previously but it still fails. Could you kindly check?

Copy link
Member

Choose a reason for hiding this comment

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

Seems to have something to do with set_output (I might be wrong). I'm checking. cc @thomasjpfan

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@adrinjalali The checks still seem to be failing but could you kindly check the test that I added in the common tests?

@OmarManzoor OmarManzoor force-pushed the logistic_regression_cv_routing branch from fb27db8 to ad9f9e9 Compare November 4, 2022 07:47
@adrinjalali
Copy link
Member

Merging the latest sample-props branch should fix the CI issues: #24854

@OmarManzoor
Copy link
Contributor Author

@adrinjalali Could you kindly have a look at the updates?

Copy link
Member
@jnothman jnothman left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM, thanks!

routed_params = process_routing(
obj=lr_cv, method="fit", sample_weight=sample_weight, other_params=other_params
)
assert routed_params.scorer.score
Copy link
Member

Choose a reason for hiding this comment

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

this is unnecessary given the next line

assert not routed_params.scorer.score


def test_lr_cv_scorer_receives_sample_weight_when_score_request_is_set():
Copy link
Member

Choose a reason for hiding this comment

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

We don't actually test that the weights are received or have an effect. Perhaps we should check that the results are different from when sample_weight is not requested.

Copy link
Contributor Author
@OmarManzoor OmarManzoor Dec 30, 2022

Choose a reason for hiding this comment

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

Thank you for the suggestion. I just wanted to mention that there seems to be an issue if I try to call fit on LogisticRegressionCV when requesting sample weight for the underlying scorer. The problem occurs at this point
https://github.com/scikit-learn/scikit-learn/pull/24498/files#diff-66422c7ed307653c80e5d68ed353d44d8fbb02319c428e140daff082eaf9d6cbR742. Over here the sample weight has a length of 10 whereas y_test has a length of 2 and this causes an inconsistent number of samples error.

Copy link
Member

Choose a reason for hiding this comment

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

Please make sure to index a subset of weights for the train/test set like we do for X and y... Not that I've checked the code now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jnothman I updated the sample weight to only index the test set at the required place and also updated the test. Could you kindly have a look?

Copy link
Member
@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

Thanks for the work so far @OmarManzoor

Comment on lines 785 to 786
if "sample_weight" in _score_params:
_score_params["sample_weight"] = _score_params["sample_weight"][test]
Copy link
Member

Choose a reason for hiding this comment

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

we should call _check_fit_params the same as done in BaseSearchCV, and index any metadata which might be sample-aligned.

@@ -1751,6 +1760,9 @@ def fit(self, X, y, sample_weight=None):
Array of weights that are assigned to individual samples.
If not provided, then each sample is given unit weight.

**fit_params : dict
Parameters to pass to the underlying splitter and scorer.

Copy link
Member

Choose a reason for hiding this comment

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

the parameters for the public methods need a .. versionadded:: 2.0

@thomasjpfan thomasjpfan deleted the branch scikit-learn:sample-props June 2, 2023 20:43
@thomasjpfan thomasjpfan closed this Jun 2, 2023
@adrinjalali
Copy link
Member

@OmarManzoor we have switched to a feature flag system (introduced in #26103), which makes things much easier for implementing slep6 for meta-estimators.

The big slep6 PR is now merged into main (#24027), and the sample-props branch is now removed from the main repo.

Would you be up for converting this PR to the feature flag enabled form, and submit the PR to main?

Also happy to document/answer your questions on how to do the feature flag thingy.

@OmarManzoor OmarManzoor deleted the logistic_regression_cv_routing branch June 3, 2023 18:34
@OmarManzoor
Copy link
Contributor Author

@OmarManzoor we have switched to a feature flag system (introduced in #26103), which makes things much easier for implementing slep6 for meta-estimators.

The big slep6 PR is now merged into main (#24027), and the sample-props branch is now removed from the main repo.

Would you be up for converting this PR to the feature flag enabled form, and submit the PR to main?

Also happy to document/answer your questions on how to do the feature flag thingy.

Thank you for the suggestion. Sure, I would like to try this out as soon as I find time.

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.

4 participants
0