-
-
Notifications
You must be signed in to change notification settings - Fork 25.8k
[MRG+1] label binarizer not used consistently in CalibratedClassifierCV #7799
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a test
@@ -289,7 +291,7 @@ def _preproc(self, X): | |||
|
|||
return df, idx_pos_class | |||
|
|||
def fit(self, X, y, sample_weight=None): | |||
def fit(self, X, y, sample_weight=None, classes=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we please move this parameter to the constructor?
@jnothman Done! |
def test_calibration_prob_sum(): | ||
"""Test that sum of probabilities is 1""" | ||
num_classes = 2 | ||
X, y = make_classification(n_samples=100, n_features=20, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make n_samples smaller to reduce test time.
|
||
|
||
def test_calibration_prob_sum(): | ||
"""Test that sum of probabilities is 1""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use docstrings in tests because they make nose output harder to read. Make this a comment instead. Also comment that this is a non-regression test for issue #... as it's otherwise a bit obscure to use LOO.
This otherwise looks good as a fix for the issue-as-presented. However, I think there are other oddities in the code here. It's also unclear to me that the current implementation correctly handles the case that classes are absent for some training sets. |
@@ -175,7 +175,8 @@ def fit(self, X, y, sample_weight=None): | |||
this_estimator.fit(X[train], y[train]) | |||
|
|||
calibrated_classifier = _CalibratedClassifier( | |||
this_estimator, method=self.method) | |||
this_estimator, method=self.method, | |||
classes=np.unique(y[train])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you sure this should be y[train]
and not y
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I m not sure. I thought that since the base_estimator
is fitted with y[train]
, the base_estimator
will assume that only np.unique(y[train])
classes exist, but still there will be a problem if y[test]
has some extra classes. So, should I change it to y
@jnothman ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to my concern that "It's also unclear to me that the current implementation correctly handles the case that classes are absent for some training sets."
Could you concoct a test case, with toy data and your own train-test splits, say, that checks this issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnothman added the test (and I hope this is how you expected it to be?)
I can make the changes which you have listed above to the class CalibratedClassifierCV
. Do you want me to commit it in this PR or should I make another PR after this one is merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's see :)
clf = LinearSVC(C=1.0) | ||
clf_prob = CalibratedClassifierCV(clf, method="sigmoid", cv=LeaveOneOut()) | ||
clf_prob.fit(X, y) | ||
probs = clf_prob.predict_proba(X) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may be hiding the issue: is it an array of shape (10, 10) or of shape (10, 9)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shape of probs array is (10, 10), and BTW this worked only when I changed the classes parameter to self.classes_
instead of np.unique(y[train])
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you able to drill down further and ensure that each constituent calibrated classifier is returning probability of 0 for a different class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may need to specially handle the case where a binarized label's data is all zeros.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like you said, I also think that the current implementation does not handle the case where some classes are absent in training set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily. If the base estimator happens to have only been trained on a subset of the classes, the mapping between the columns of the base_estimator
's decision function and those of that CalibratedClassifierCV
should output is not so trivial .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnothman self.base_estimator.classes_
will be the class labels and we have to change that to integers with LabelEncoder.transform()
(Already fitted with all the classes) and this should work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ridge isn't a classifier...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignore that, you're not using ridge anymore :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnothman Some test which was already present was using Ridge
in CalibratedClassifierCV
, that's why I asked you. Maybe I will change that test to some other classifier.
And we can't assume always encoded labels in |
@jnothman Can we use |
yeah, I'm a bit confused why the LabelBinarizer is instantiated here in the first place... you can definitely replace it by LabelEncoder from what I see. (I haven't looked into your PR in detail though) |
|
yes, I think that should work. On 2 November 2016 at 14:25, Srivatsan notifications@github.com wrote:
|
clf = LinearSVC(C=1.0) | ||
clf_prob = CalibratedClassifierCV(clf, method="sigmoid", cv=LeaveOneOut()) | ||
clf_prob.fit(X, y) | ||
probs = clf_prob.predict_proba(X) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jnothman Can you check the tests? I couldn't think of any other way to test this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant to actually go through each of calibrated_classifiers_
and run predict_proba
to confirm that each leaves a different column with zero-probability. Or am I wrong to think that's correct behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that can be checked
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently my comment was left pending for a while
clf = LinearSVC(C=1.0) | ||
clf_prob = CalibratedClassifierCV(clf, method="sigmoid", cv=LeaveOneOut()) | ||
clf_prob.fit(X, y) | ||
probs = clf_prob.predict_proba(X) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ridge isn't a classifier...
idx_pos_class = self.label_encoder_.\ | ||
transform(self.base_estimator.classes_) | ||
else: | ||
idx_pos_class = np.arange(df.shape[1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since Ridge
has a decision_function
method can we not use it with CalibratedClassifierCV
. I did this workaround for such cases. Is it fine or Ridge should not work with CalibratedClassifierCV
. @jnothman
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regressors like Ridge should not work with CalibratedClassifierCV
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise LGTM!
|
||
for i, calibrated_classifier in \ | ||
enumerate(cal_clf.calibrated_classifiers_): | ||
assert_array_equal(calibrated_classifier.predict_proba(X)[:, i], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this slightly stronger by checking that this column and only this column is zero:
proba = calibrated_classifier.predict_proba(X)
assert_array_equal(proba[:, i], np.zeros(len(y)))
assert_equal(np.all(np.hstack([proba[:, :i], proba[:, i+1:]])), True)
@@ -253,6 +257,11 @@ class _CalibratedClassifier(object): | |||
corresponds to Platt's method or 'isotonic' which is a | |||
non-parametric approach based on isotonic regression. | |||
|
|||
classes : array-like, shape (n_classes,) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unimportant, given that this is a private class, but conventionally we'd add ", optional" to this line
Please also add a what's new entry. |
e3fba0d
to
6d9b675
Compare
I think this can also be squeezed into 0.18.1, if the release manager agrees it's a worthwhile bug-fix (sorry for all the what's new hacking you need to do @amueller). |
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
* tag '0.18.1': (144 commits) skip tree-test on 32bit do the warning test as we do it in other places. Replase assert_equal by assert_almost_equal in cosine test version bump 0.18.1 fix merge conflict mess in whatsnew add the python2.6 warning to 0.18.1 fix learning_curve test that I messed up in cherry-picking the "reentrant cv" PR. sync whatsnew with master [MRG] TST Ensure __dict__ is unmodified by predict, transform, etc (scikit-learn#7553) FIX scikit-learn#6420: Cloning decision tree estimators breaks criterion objects (scikit-learn#7680) Add whats new entry for scikit-learn#6282 (scikit-learn#7629) [MGR + 2] fix selectFdr bug (scikit-learn#7490) fixed whatsnew cherry-pick mess (somewhat) [MRG + 2] FIX LogisticRegressionCV to correctly handle string labels (scikit-learn#5874) [MRG + 2] Fixed parameter setting in SelectFromModel (scikit-learn#7764) [MRG+2] DOC adding separate `fit()` methods (and docstrings) for DecisionTreeClassifier and DecisionTreeRegressor (scikit-learn#7824) Fix docstring typo (scikit-learn#7844) n_features --> n_components [MRG + 1] DOC adding :user: role to whats_new (scikit-learn#7818) [MRG+1] label binarizer not used consistently in CalibratedClassifierCV (scikit-learn#7799) DOC : fix docstring of AIC/BIC in GMM ...
* releases: (144 commits) skip tree-test on 32bit do the warning test as we do it in other places. Replase assert_equal by assert_almost_equal in cosine test version bump 0.18.1 fix merge conflict mess in whatsnew add the python2.6 warning to 0.18.1 fix learning_curve test that I messed up in cherry-picking the "reentrant cv" PR. sync whatsnew with master [MRG] TST Ensure __dict__ is unmodified by predict, transform, etc (scikit-learn#7553) FIX scikit-learn#6420: Cloning decision tree estimators breaks criterion objects (scikit-learn#7680) Add whats new entry for scikit-learn#6282 (scikit-learn#7629) [MGR + 2] fix selectFdr bug (scikit-learn#7490) fixed whatsnew cherry-pick mess (somewhat) [MRG + 2] FIX LogisticRegressionCV to correctly handle string labels (scikit-learn#5874) [MRG + 2] Fixed parameter setting in SelectFromModel (scikit-learn#7764) [MRG+2] DOC adding separate `fit()` methods (and docstrings) for DecisionTreeClassifier and DecisionTreeRegressor (scikit-learn#7824) Fix docstring typo (scikit-learn#7844) n_features --> n_components [MRG + 1] DOC adding :user: role to whats_new (scikit-learn#7818) [MRG+1] label binarizer not used consistently in CalibratedClassifierCV (scikit-learn#7799) DOC : fix docstring of AIC/BIC in GMM ... Conflicts: removed sklearn/externals/joblib/__init__.py sklearn/externals/joblib/_parallel_backends.py sklearn/externals/joblib/testing.py
* dfsg: (144 commits) skip tree-test on 32bit do the warning test as we do it in other places. Replase assert_equal by assert_almost_equal in cosine test version bump 0.18.1 fix merge conflict mess in whatsnew add the python2.6 warning to 0.18.1 fix learning_curve test that I messed up in cherry-picking the "reentrant cv" PR. sync whatsnew with master [MRG] TST Ensure __dict__ is unmodified by predict, transform, etc (scikit-learn#7553) FIX scikit-learn#6420: Cloning decision tree estimators breaks criterion objects (scikit-learn#7680) Add whats new entry for scikit-learn#6282 (scikit-learn#7629) [MGR + 2] fix selectFdr bug (scikit-learn#7490) fixed whatsnew cherry-pick mess (somewhat) [MRG + 2] FIX LogisticRegressionCV to correctly handle string labels (scikit-learn#5874) [MRG + 2] Fixed parameter setting in SelectFromModel (scikit-learn#7764) [MRG+2] DOC adding separate `fit()` methods (and docstrings) for DecisionTreeClassifier and DecisionTreeRegressor (scikit-learn#7824) Fix docstring typo (scikit-learn#7844) n_features --> n_components [MRG + 1] DOC adding :user: role to whats_new (scikit-learn#7818) [MRG+1] label binarizer not used consistently in CalibratedClassifierCV (scikit-learn#7799) DOC : fix docstring of AIC/BIC in GMM ...
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
…CV (scikit-learn#7799) * label binarizer not used consistently in CalibratedClassifierCV * changed position of classes argument to make old tests run * moved parameter to constructor and added test * added test where train set doesnt have all classes * CalibratedClassifierCV can now handle cases where train set doesnt contain all labels * fixing flake error * fixing line lengths * removing np.full() * from __future__ import division for py2.7 * change is test file * added an extra test and removed a test with Ridge * stronger test * whats new entry
Reference Issue
#7796
What does this implement/fix? Explain your changes.
The
LabelBinarizer()
was not used consistently inCalibratedClassifierCV
In the example provided in the issue #7796 , the when usingLeaveOneOut
the test set will have only one class, whereas the train set has two classes. Two different instances of theLabelBinarizer
are used to encode the test and train set.So, I have added a new argument 'classes' in the
fit()
function of_CalibratedClassifier
class (which contains the unique classes of the test set), as I didn't know of any other method to solve this issue. If someone has a better solution I can change it.