8000 MRG: Functionality to change scoring method of searchlight by kaichogami · Pull Request #3502 · mne-tools/mne-python · GitHub
[go: up one dir, main page]

Skip to content

MRG: Functionality to change scoring method of searchlight #3502

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

Merged
merged 9 commits into from
Sep 25, 2016

Conversation

kaichogami
Copy link
Contributor

Enhancement as discussed in #3475. Aim to support

sl = SearchLight(LogisticRegression(), scoring=roc_auc_score)
score = sl.fit(X, y).score(X, y)

score = cross_val_score(SearchLight(LogisticRegression()), X, y, scoring='roc_auc')

assert_array_equal(score.shape, [n_time])
assert_true(score.dtype == float)
sl = SearchLight(LogisticRegression())
assert_array_equal(cross_val_score(sl, X, y, scoring='roc_auc'), score)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

cross_val_score fails with an error.

  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/utils/validation.py", line 561, in column_or_1d
    raise ValueError("bad input shape {0}".format(shape))
ValueError: bad input shape (17, 10)

@kingjr kingjr mentioned this pull request Aug 9, 2016
38 tasks
@kingjr
Copy link
Member
kingjr commented Aug 9, 2016

I think the current design won't work, notably because we need to change the type of prediction as a function of the scoring. e.g. if you pass a roc_auc_score, the prediction should be continuous (e.g. predict_proba or decision_function) and not discrete (this is probably the cause of your error). Check how this is handled in sklearn cross_val_score.

Additionally, could you add tests to make sure that this is working:

SearchLight(Ridge(), scoring=my_scorer())
cross_val_score(SearchLight(Ridge()), X, y, scoring=my_scorer())

And once it passes, update the GeneralizationLight too

@@ -338,7 +358,10 @@ def _sl_score(estimators, X, y):
"""
n_iter = X.shape[-1]
for ii, est in enumerate(estimators):
_score = est.score(X[..., ii], y)
if scoring is not None:
_score = scoring(est, X[..., ii], y)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kingjr I made it with the signature score_func(estimator, y, y_pred) by using make_scorer so as to take care of the prediction type which make_scorer handles internally. However cross_val_score doesn't still pass. Let me know if you have any idea about it.

Copy link
Member

Choose a reason for hiding this comment

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

I'm afraid I don't know. Do you manage to make cross_val_score(base_estimator, scoring=scoring) pass?

Copy link
Member

Choose a reason for hiding this comment

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

your y is 2d and roc_auc requires y to be 1d or 1 column. You need to look over the columns of y.

Copy link
Member
@kingjr kingjr Aug 11, 2016

Choose a reason for hiding this comment

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

@agramfort this is a single/multi class scoring problem. Isn't there a way to handle this by sklearn scorers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_score = scoring(est, X[..., ii], y), shouldn't this fail as well in case when scoring parameter is changed to roc_auc_score in the test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ignore my last comment. Going through the code, I think error occurs because X is not 2D before being passed to decision_function. I can see two solutions.

  • Make the input to be a 2D matrix or at least convert X to 2D before passing to decision_function
  • Follow the other API @kingjr mentioned cross_val_score(make_pipeline(SearchLight(Ridge(), scoring=my_scorer()), X, y)

I hope I make sense..

Copy link
Member

Choose a reason for hiding this comment

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

A sklearn score returns a float per fold. Look maybe at multi_output scorers

On Thu, Aug 11, 2016 at 3:32 PM +0200, "Jean-Rémi KING" notifications@github.com wrote:

In mne/decoding/search_light.py:

@@ -338,7 +358,10 @@ def _sl_score(estimators, X, y):
"""
n_iter = X.shape[-1]
for ii, est in enumerate(estimators):

  •    _score = est.score(X[..., ii], y)
    
  •    if scoring is not None:
    
  •        _score = scoring(est, X[..., ii], y)
    

@agramfort this is a single/multi class scoring problem. Isn't there a way to handle thus by sklearn scorers?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@kaichogami
Copy link
Contributor Author

@kingjr I am not sure what should be done now.

@agramfort
Copy link
Member

@kaichogami make CIs happy?

@kaichogami
Copy link
Contributor Author
kaichogami commented Aug 24, 2016

@agramfort yes, however I don't know how to solve the issue with cross_val_score(clf, X, y, scorer='roc_auc').

@agramfort
Copy link
Member

you cannot obtain more than one value per fold with a sklearn scorer. If you have multiple targets you need a custom scorer that return more than a float

so

cross_val_score(clf, X, y, scorer='roc_auc')

simple cannot work as is.

@kingjr
Copy link
Member
kingjr commented Aug 27, 2016

@agramfort I think there is a confusion between multiple issues:

  1. SearchLight involves multiple scores, so cross_val_score(SearchLight(), scoring='roc_auc') is not viable with the current sklearn API. IMO scoring metrics need not output a float by principle. Confusion matrices are a form of scoring metrics. In any case this debate is not to be discussed here.
  2. We want to be able to parametrize the SearchLight scoring e.g. SearchLight(clf, scoring='roc_auc'). However this involves
    i) internally adapting the predict function: if you want to score with an AUC, you need to ensure that the predict function outputs an array of floats (e.g. distance to hyperplan, proba etc) and not of integers.
    ii) identifying or handling 2 or multi class problem: i.e. predict_proba generates a y_pred, of shape (n_samples, n_classes), which can therefore not be passed to the sklearn roc_auc scorer. This is an API problem, not an intrinsic one, because we could in principle identify that there are 2 classes and compute the AUC. In case of multiclass, we could compute each auc and return the average AUC. All this should be solved/able at the sklearn level IMO

@agramfort
Copy link
Member
agramfort commented Aug 29, 2016 via email

@kingjr
Copy link
Member
kingjr commented Sep 12, 2016

@kaichogami how are we doing over here?

@kaichogami
Copy link
Contributor Author

@kingjr I haven't touched it after my last comment. I'll look over it again and let you know if I can solve it, in few days.

@kaichogami
Copy link
Contributor Author

@kingjr as Alex mentioned we need to design custom scorer to make cross_val_score(clf, X, y, scorer='custom_scorer') work. Or we will have to use it the other way you mentioned i.e, cross_val_score(make_pipeline(SearchLight(Ridge(), scoring=roc_auc_score), X, y). I think using the second option is more viable than creating many custom scorers.
Also as we have used make_scorer it should know whether it requires proba or decision
function or just predict.
Let me know what you think.

@kingjr
Copy link
Member
kingjr commented Sep 15, 2016

+1 for the second option

Le 15 sept. 2016 04:19, "Asish Panda" notifications@github.com a écrit :

@kingjr https://github.com/kingjr as Alex mentioned we need to design
custom scorer to make cross_val_score(clf, X, y, scorer='custom_scorer')
work. Or we will have to use it the other way you mentioned i.e,
cross_val_score(make_pipeline(SearchLight(Ridge(),
scoring=roc_auc_score), X, y). I think using the second option is more
viable than creating many custom scorers.
Also as we have used make_scorer it should know whether it requires proba
or decision
function or just predict.
Let me know what you think.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#3502 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AEp7DFyuxUM0RqRHU3PW7xnUZXGOI4S6ks5qqP-MgaJpZM4JfX2t
.

@kaichogami
Copy link
Contributor Author

Apologies @kingjr, the above design wouldn't work as well. We are now returning one value per fold, however we still have an array of score, one float for each estimator and this line does not allow score to be anything other than a float, which makes sense. Error output

Traceback (most recent call last):
  File "/home/kaichogami/codes/dev_mne/env/local/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/home/kaichogami/codes/dev_mne/mne-python/mne/utils.py", line 791, in dec
    return function(*args, **kwargs)
  File "/home/kaichogami/codes/dev_mne/mne-python/mne/decoding/tests/test_search_light.py", line 66, in test_searchlight
    assert_array_equal(cross_val_score(make_pipeline(sl), X, y), score)
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/cross_validation.py", line 1480, in cross_val_score
    for train, test in cv)
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/externals/joblib/parallel.py", line 800, in __call__
    while self.dispatch_one_batch(iterator):
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/externals/joblib/parallel.py", line 658, in dispatch_one_batch
    self._dispatch(tasks)
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/externals/joblib/parallel.py", line 566, in _dispatch
    job = ImmediateComputeBatch(batch)
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/externals/joblib/parallel.py", line 180, in __init__
    self.results = batch()
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/externals/joblib/parallel.py", line 72, in __call__
    return [func(*args, **kwargs) for func, args, kwargs in self.items]
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/cross_validation.py", line 1593, in _fit_and_score
    test_score = _score(estimator, X_test, y_test, scorer)
  File "/home/kaichogami/codes/dev_mne/scikit-learn/sklearn/cross_validation.py", line 1653, in _score
    % (str(score), type(score)))
ValueError: scoring must return a number, got [ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.] (<type 'numpy.ndarray'>) instead.

The only solution I can think of is to take average of scores, which could be a parameter in constructor. This should be made true while using with cross_val_score.
`cross_val_score(SearchLight(Ridge(), scoring=roc_auc_score, average=True), X, y)

@kingjr
Copy link
Member
kingjr commented Sep 16, 2016

I think we have to dissociate three problems:

  1. parametrize the scoring within the SearchLight (independently of the cross val score) i.e; SearchLight(LogisiticRegression(), scorer=roc_auc_score); this should be close to what you already wrote
  2. Deal with multiclass AUC: this is outside this PR, and will be addressed in sklearn (Support for multi-class roc_auc scores scikit-learn/scikit-learn#3298)
  3. make SearchLight compatible with cross_val_score, which expects float scores, not array. I think that may be a broader issue though, as it's similar to i.e. get a confusion matrix in a cross_val_score.

I recommend to solve 1, and leave 2 and 3 for now.

@kaichogami kaichogami force-pushed the searchlight_score branch 2 times, most recently from 636d666 to e19ed6a Compare September 17, 2016 16:29
@kaichogami
Copy link
Contributor Author

@kingjr I have removed the cross_val_score example and only solved the case 1 as mentioned in your previous comment.
I am not sure what is causing the travis and appveyor error.

@kingjr
Copy link
Member
kingjr commented Sep 19, 2016

Not sure what s going on with Travis. Can you repush with a forced set-upstream?

@kaichogami
Copy link
Contributor Author

Pushing again didn't help. Perhaps @Eric89GXL has some idea?

@@ -4,6 +4,8 @@

import numpy as np

from sklearn.metrics import make_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 Travis error says:

Found un-nested sklearn import")

In other words, this needs to be nested

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Eric89GXL Thanks for your help!

F438

@codecov-io
Copy link
codecov-io commented Sep 21, 2016

Current coverage is 87.35% (diff: 96.55%)

Sunburst

No coverage report found for master at 577d5fa.

Powered by Codecov. Last update 577d5fa...f164fa1

@kaichogami
Copy link
Contributor Author

I added the import in the relevant methods rather than using global imports, which solved the nested import issue.

@@ -19,6 +19,9 @@ class SearchLight(BaseEstimator, TransformerMixin):
----------
base_estimator : object
The base estimator to iteratively fit on a subset of the dataset.
scoring : callable, defaults to None
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 make it accept strings as well, similar to sklearn cross_val_score


if not (scoring is None or hasattr(scoring, '__call__')):
raise ValueError("scoring must be None or a callable type")

Copy link
Member

Choose a reason for hiding this comment

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

remove extra line

# generated by estimator.score(). Else, we must first get the
# predictions based on the scorer.
self.scoring = (make_scorer(self.scoring) if self.scoring is not None
else self.scoring)
Copy link
Member

Choose a reason for hiding this comment

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

object parameters should not be change.

You could store the scorer in self.scorer_, but I think we need not store the scorer, WDYT @agramfort ?

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 am now making all scorer checks in __init__ which also handles string.

self.n_jobs = n_jobs

if not isinstance(self.n_jobs, int):
raise ValueError('n_jobs must be int, got %s' % n_jobs)
Copy link
Member

Choose a reason for hiding this comment

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

Why did you add the init? GeneralizationLight inherits from Searchlight and should have the same init params no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would otherwise result in error for the doc string for the scorer parameter, as in GeneralizationLight we have not modified it to change scorer.

Copy link
Member

Choose a reason for hiding this comment

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

You shoudlnt change this init, but also add a scoring parameter to GeneralizationLight

@@ -55,6 +57,13 @@ def test_searchlight():
assert_true(np.sum(np.abs(score)) != 0)
assert_true(score.dtype == float)

# change score method
Copy link
Member

Choose a reason for hiding this comment

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

add test where

  • scoring='foo'
  • check that sl.scoring == default

# change score method
sl = SearchLight(LogisticRegression(), scoring=roc_auc_score)
sl.fit(X, y)
score = sl.score(X, y)
Copy link
Member

Choose a reason for hiding this comment

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

add explicit test that check the score

assert_equal(score[0], roc_auc_score(y, LogisticRegression().fit(X,y).predict_proba(X)[:, 1))

@@ -30,10 +33,24 @@ def __repr__(self):
repr_str += ', fitted with %i estimators' % len(self.estimators_)
return repr_str + '>'

def __init__(self, base_estimator, n_jobs=1):
def __init__(self, base_estimator, scoring=None, n_jobs=1):
from sklearn.metrics import make_scorer, get_scorer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kingjr Is it alright to add imports in __init__ of class?

Copy link
Member

Choose a reason for hiding this comment

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

No see below

@kaichogami
Copy link
Contributor Author

@kingjr please have a look at the changes.
The travis error looks unrelated to the PR changes.

self.n_jobs = n_jobs

if not isinstance(self.n_jobs, int):
raise ValueError('n_jobs must be int, got %s' % n_jobs)
Copy link
Member

Choose a reason for hiding this comment

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

You shoudlnt change this init, but also add a scoring parameter to GeneralizationLight

@@ -30,10 +33,24 @@ def __repr__(self):
repr_str += ', fitted with %i estimators' % len(self.estimators_)
return repr_str + '>'

def __init__(self, base_estimator, n_jobs=1):
def __init__(self, base_estimator, scoring=None, n_jobs=1):
from sklearn.metrics import make_scorer, get_scorer
Copy link
Member

Choose a reason for hiding this comment

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

No see below


elif self.scoring is not None:
self.scoring = get_scorer(self.scoring)

Copy link
Member

Choose a reason for hiding this comment

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

Don't change the param at init, this will break the cloning behavior.

get_scorer and make_scorer should be called at .score() not init

Copy link
Member
@kingjr kingjr left a comment

Choose a reason for hiding this comment

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

thanks @kaichogami , only some docstring fixes, and I'll merge if @agramfort is ok.

Note that we will temporarily privatize this enhancement next week to ensure that SearchLight isn't in the next release.

@@ -328,6 +345,10 @@ def _sl_score(estimators, X, y):
X : array, shape (n_samples, nd_features, n_estimators)
The target data. The feature dimension can be multidimensional e.g.
X.shape = (n_samples, n_features_1, n_features_2, n_estimators)
scoring : callable or None
Copy link
Member

Choose a reason for hiding this comment

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

or string?

@@ -375,11 +399,13 @@ class GeneralizationLight(SearchLight):
----------
base_estimator : object
The base estimator to iteratively fit on a subset of the dataset.
scoring : callable, string, defaults to None
Copy link
Member

Choose a reason for hiding this comment

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

scoring : callable | string | None

@kingjr
Copy link
Member
kingjr commented Sep 25, 2016

Do you want to give it a look @agramfort ?

@agramfort
Copy link
Member

LGTM

@kingjr merge if you're happy

fix the docstrings in master if needed or better in your next "privatization" PR

@kingjr kingjr changed the title [WIP] Functionality to change scoring method of searchlight MRG: Functionality to change scoring method of searchlight Sep 25, 2016
@kingjr kingjr merged commit a294c33 into mne-tools:master Sep 25, 2016
@kingjr
Copy link
Member
kingjr commented Sep 25, 2016

I'll fix the docstring in the "privatizing" PR ;)

@kingjr
Copy link
Member
kingjr commented Sep 25, 2016

Thanks @kaichogami !

@kaichogami
Copy link
Contributor Author

@kingjr You are welcome! What is "privatizing" PR that is being talked about?

@kingjr
Copy link
Member
kingjr commented Sep 26, 2016

We re making a release next week, but some of the recent decoding contribs
may be too unstable at the moment. I will temporarily make these changes
private until next week's release and put them back afterwards to give us
another 6 months to ensure the stability of the API.

Le 25 sept. 2016 9:59 PM, "Asish Panda" notifications@github.com a écrit :

@kingjr https://github.com/kingjr You are welcome! What is
"privatizing" PR that is being talked about?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#3502 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AEp7DIdhfjevhPzlmpYltEfx9qIgwZ1iks5qtycIgaJpZM4JfX2t
.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
0