Skip to content

[MRG+1] Catch cases for different class size in MLPClassifier with warm start (#7976) #8035

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 11 commits into from
Dec 29, 2016
Merged

Conversation

vincentpham1991
Copy link
Contributor

Reference Issue

Fixes #7976

What does this implement/fix? Explain your changes.

This provides a test for different cases that throws an error when warm_start = True for MLPClassifier. Currently, vague errors are thrown when class size is different between the current fit and the previous fit. This fix will throw a clearer error message.

Any other comments?

@jnothman
Copy link
Member

Thanks for working on this.

@vincentpham1991 vincentpham1991 changed the title [WIP] Catch cases for different class size in MLP with warm start (#7976) [MRG] Catch cases for different class size in MLP with warm start (#7976) Dec 12, 2016
@vincentpham1991 vincentpham1991 changed the title [MRG] Catch cases for different class size in MLP with warm start (#7976) [MRG] Catch cases for different class size in MLPClassifier with warm start (#7976) Dec 12, 2016
@jnothman jnothman added the Bug label Dec 14, 2016
@jnothman jnothman self-requested a review December 14, 2016 00:04
"""
if getattr(self, 'classes_', None) is not None:
num_classes = len(unique_labels(y))
if num_classes != len(self.classes_):
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 not a sufficient condition, really. What if they have different labels? I think what you want to do is change the condition here to be if not incremental or (self.warm_start and not hasattr(self, 'classes_')).

It would certainly be nice if we had some helpers to make writing this logic less error-prone :\

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 not entirely certain that we want to exclude the case where a subset of the classes are fit... but it is admittedly a bit weird. Perhaps it's worth checking what other classifiers with warm_start do... though they may well just be broken.

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 just did a check with RandomForestClassifier with warm_start. It also breaks when a different size class is fitted. In addition, it allows for a different subset of labels when class size is the same during fit.

Copy link
Member

Choose a reason for hiding this comment

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

class size meaning number of classes? Then the second is a bug

Copy link
Contributor Author

@vincentpham1991 vincentpham1991 Dec 16, 2016

Choose a reason for hiding this comment

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

class size as the unique number of labels. For example unique(y) = [1,2,3] and unique(y_alt) = [4,5,6]. fit(X,y) then fit(X,y_alt) will not return an error when warm_start=True.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another question, if second fit has a subset of labels from the first fit, should it be allowable? For example, unique(y) = [1,2,3] amd second fit is with unique(y_alt) = [1,2].

Copy link
Member

Choose a reason for hiding this comment

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

I think these are behaviours that the inventors of warm_start had not considered. I think, unlike for partial_fit, a subset should raise an error.

@@ -556,3 +561,40 @@ def test_adaptive_learning_rate():
clf.fit(X, y)
assert_greater(clf.max_iter, clf.n_iter_)
assert_greater(1e-6, clf._optimizer.learning_rate)


def test_warm_class():
Copy link
Member

Choose a reason for hiding this comment

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

*warm_start? *warm_start_classes?

y_5classes = np.array([0]*30 + [1]*30 + [2]*30 + [3]*30 + [4]*30)

with ignore_warnings(category=Warning):
# failed in converting 7th argument `g' of _lbfgsb.setulb to
Copy link
Member

Choose a reason for hiding this comment

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

This no longer happens, right?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to record the error message we're avoiding.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, these are the errors that occurs originally and are replaced with a standard error message. I will remove these comments.

clf = MLPClassifier(hidden_layer_sizes=2, solver='lbfgs',
warm_start=True)
clf.fit(X, y)
assert_raises(ValueError, clf.fit, X, y_2classes)
Copy link
Member

Choose a reason for hiding this comment

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

could we use assert_raises_message?

clf = MLPClassifier(hidden_layer_sizes=2, solver='lbfgs',
warm_start=True)
clf.fit(X, y)
assert_raises(ValueError, clf.fit, X, y_4classes)
Copy link
Member

Choose a reason for hiding this comment

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

could we use assert_raises_message?

"""
if getattr(self, 'classes_', None) is not None:
num_classes = len(unique_labels(y))
if num_classes != len(self.classes_):
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 not entirely certain that we want to exclude the case where a subset of the classes are fit... but it is admittedly a bit weird. Perhaps it's worth checking what other classifiers with warm_start do... though they may well just be broken.

y_4classes = np.array([0]*37 + [1]*37 + [2]*38 + [3]*38)
y_5classes = np.array([0]*30 + [1]*30 + [2]*30 + [3]*30 + [4]*30)

with ignore_warnings(category=Warning):
Copy link
Member

Choose a reason for hiding this comment

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

what warnings are we ignoring?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sometimes it throws a RuntimeWarning: underflow encountered in exp np.exp(tmp, out=X). I tried ignore_warnings(category=RuntimeWarning) but it didn't seem to work.

Copy link
Member

Choose a reason for hiding this comment

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

I have not investigated what's going on here, but perhaps np.errstate will work?

@jnothman
Copy link
Member

test failures

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

elif self.warm_start:
classes = unique_labels(y)
if set(classes) != set(self.classes_):
raise ValueError("`y` has classes not in `self.classes_`."
Copy link
Member

Choose a reason for hiding this comment

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

Let's make this clearer: "warm_start can only be used where the new y has the same classes as in the previous call to fit. Previously, got %r, now %r."

" has [0 1 2]. 'y' has [0 1 2 3].")
assert_raise_message(ValueError, message, clf.fit, X, y_4classes)

# Test with 5 unique labels
Copy link
Member

Choose a reason for hiding this comment

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

This seems unnecessary. Instead can we test for y having labels [1, 2, 3] (no 0) or [0, 1, 3]: correct number of labels, incorrect set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially, I was testing for the different types of errors that were popping up. But I can change this test to [1,2,3] instead of 5 classes.

@jnothman
Copy link
Member

jnothman commented Dec 22, 2016 via email

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.

clf = MLPClassifier(hidden_layer_sizes=2, solver='lbfgs',
warm_start=True)
clf.fit(X, y)
message = ('warm_start can only be used where `y` has the same classes as in the previous call to fit.'
Copy link
Member

Choose a reason for hiding this comment

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

Please observe PEP8's limit of 79 chars per line.

elif self.warm_start:
classes = unique_labels(y)
if set(classes) != set(self.classes_):
raise ValueError("warm_start can only be used where `y` has the same classes as in the previous "
Copy link
Member

Choose a reason for hiding this comment

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

PEP8 line length

@jnothman jnothman changed the title [MRG] Catch cases for different class size in MLPClassifier with warm start (#7976) [MRG+1] Catch cases for different class size in MLPClassifier with warm start (#7976) Dec 23, 2016
Copy link
Member

@raghavrv raghavrv left a comment

Choose a reason for hiding this comment

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

Besides LGTM. Thanks for working on this @vincentpham1991!

incremental = True
else:
incremental = False
return self._fit(X, y, incremental=incremental)
Copy link
Member

Choose a reason for hiding this comment

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

can we put this in one line

        return self._fit(X, y, incremental=(self.warm_start and
                                            hasattr(self, "classes_"))

@@ -23,7 +23,8 @@
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from scipy.sparse import csr_matrix
from sklearn.utils.testing import (assert_raises, assert_greater, assert_equal,
assert_false, ignore_warnings)
assert_false, ignore_warnings,
assert_raise_message)
Copy link
Member

Choose a reason for hiding this comment

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

could you put this in a new line with its own import statement? (we prefer that way as it makes it easier to fix conflicts)

Copy link
Member

Choose a reason for hiding this comment

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

(from sklearn.utils.testing import assert_raise_message)



def test_warm_start():
X = X_iris[:150]
Copy link
Member

Choose a reason for hiding this comment

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

Why? X_iris.shape[0] is 150...

y_5classes = np.array([0] * 30 + [1] * 30 + [2] * 30 + [3] * 30 + [4] * 30)

with ignore_warnings(category=Warning):

Copy link
Member

Choose a reason for hiding this comment

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

Could you remove this extra blank line :)

y = y_iris[:150]

y_2classes = np.array([0] * 75 + [1] * 75)
y_3classes = np.array([0] * 50 + [1] * 50 + [2] * 50)
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 same as y_iris... Could you change the order / shuffle it maybe?

np.array([0] * 40, [1] * 40, [2] * 70)

@@ -556,3 +562,58 @@ def test_adaptive_learning_rate():
clf.fit(X, y)
assert_greater(clf.max_iter, clf.n_iter_)
assert_greater(1e-6, clf._optimizer.learning_rate)


def test_warm_start():
Copy link
Member

Choose a reason for hiding this comment

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

This test could be made much shorter by using a loop something like

for y_i in (y, y_3classes):
    # No error raised
    MLP().fit(X, y)
    MLP().fit(X, y_i)

for y_i in (y_2classes, y_4classes):
    MLP().fit(X, y)
    msg =  "..... Got %s" % list(set(y))
    assert_raises_message(ValueError, msg, MLP().fit, X, y_i)

@raghavrv
Copy link
Member

Sweet. That saves us 32 lines of code :) +1 for merge once the tests pass...

y_4classes = np.array([0] * 37 + [1] * 37 + [2] * 38 + [3] * 38)
y_5classes = np.array([0] * 30 + [1] * 30 + [2] * 30 + [3] * 30 + [4] * 30)

with ignore_warnings(category=Warning):
Copy link
Member

Choose a reason for hiding this comment

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

Wait what warnings does this catch?

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 was mentioned in a comment above: "Sometimes it throws a RuntimeWarning: underflow encountered in exp np.exp(tmp, out=X). I tried ignore_warnings(category=RuntimeWarning) but it didn't seem to work."

I also tried to use np.errstate but couldn't get it to work either.

Copy link
Member

@raghavrv raghavrv Dec 28, 2016

Choose a reason for hiding this comment

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

Could you try @tguillemot's nifty fix to our @ignore_warnings decorator? (@ignore_warnings(RuntimeError))?

@raghavrv raghavrv merged commit ab1c4d4 into scikit-learn:master Dec 29, 2016
@raghavrv
Copy link
Member

In it goes. Thanks @vincentpham1991

raghavrv pushed a commit to raghavrv/scikit-learn that referenced this pull request Jan 5, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

* updated ignore warning
@vincentpham1991 vincentpham1991 deleted the issue7976 branch January 20, 2017 02:31
sergeyf pushed a commit to sergeyf/scikit-learn that referenced this pull request Feb 28, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

* updated ignore warning
@Przemo10 Przemo10 mentioned this pull request Mar 17, 2017
Sundrique pushed a commit to Sundrique/scikit-learn that referenced this pull request Jun 14, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

* updated ignore warning
NelleV pushed a commit to NelleV/scikit-learn that referenced this pull request Aug 11, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

* updated ignore warning
paulha pushed a commit to paulha/scikit-learn that referenced this pull request Aug 19, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

* updated ignore warning
maskani-moh pushed a commit to maskani-moh/scikit-learn that referenced this pull request Nov 15, 2017
…rm start (scikit-learn#7976)  (scikit-learn#8035)

* added test that fails

* generate standard value error for different class size

* moved num_classes one class down

* fixed over-indented lines

* standard error occurs a layer up.

* created a different label comparison for warm_start

* spaces around multiplication sign.

* reworded error and added another edge case.

* fixed pep8 violation

* make test shorter

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

Successfully merging this pull request may close these issues.

MLPClasiffier produce error when trying to re-fit
4 participants