-
-
Notifications
You must be signed in to change notification settings - Fork 26.2k
[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
Conversation
Thanks for working on this. |
""" | ||
if getattr(self, 'classes_', None) is not None: | ||
num_classes = len(unique_labels(y)) | ||
if num_classes != len(self.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.
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 :\
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 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.
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 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.
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.
class size meaning number of classes? Then the second is a bug
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.
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.
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.
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].
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 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(): |
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.
*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 |
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 no longer happens, right?
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 don't think we need to record the error message we're avoiding.
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, 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) |
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 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) |
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 use assert_raises_message?
""" | ||
if getattr(self, 'classes_', None) is not None: | ||
num_classes = len(unique_labels(y)) | ||
if num_classes != len(self.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.
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): |
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.
what warnings are we ignoring?
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.
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.
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 have not investigated what's going on here, but perhaps np.errstate will work?
test failures |
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.
thanks
elif self.warm_start: | ||
classes = unique_labels(y) | ||
if set(classes) != set(self.classes_): | ||
raise ValueError("`y` has classes not in `self.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.
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 |
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 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?
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.
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.
I suppose if that represented a different type of error at master,
commenting that it is a non-regression test and just adding [1,2,3] is fine.
…On 22 December 2016 at 12:31, Vincent Pham ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In sklearn/neural_network/tests/test_mlp.py
<#8035>:
> +
+ # Test with 3 unique labels
+ clf = MLPClassifier(hidden_layer_sizes=2, solver='lbfgs',
+ warm_start=True)
+ clf.fit(X, y)
+ clf.fit(X, y_3classes)
+
+ # Test with 4 unique labels
+ clf = MLPClassifier(hidden_layer_sizes=2, solver='lbfgs',
+ warm_start=True)
+ clf.fit(X, y)
+ message = ("`y` has classes not in `self.classes_`. `self.classes_`"
+ " 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
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.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#8035>, or mute the
thread
<https://github.com/notifications/unsubscribe-auth/AAEz67H0IT89EDBafZLB0wVAq1BJ29Wnks5rKdLsgaJpZM4LJ3oy>
.
|
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.
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.' |
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 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 " |
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.
PEP8 line length
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.
Besides LGTM. Thanks for working on this @vincentpham1991!
incremental = True | ||
else: | ||
incremental = False | ||
return self._fit(X, y, incremental=incremental) |
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.
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) |
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 you put this in a new line with its own import statement? (we prefer that way as it makes it easier to fix conflicts)
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.
(from sklearn.utils.testing import assert_raise_message
)
|
||
|
||
def test_warm_start(): | ||
X = X_iris[:150] |
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.
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): | ||
|
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 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) |
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 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(): |
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 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)
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): |
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.
Wait what warnings does this catch?
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.
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.
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 you try @tguillemot's nifty fix to our @ignore_warnings
decorator? (@ignore_warnings(RuntimeError)
)?
In it goes. Thanks @vincentpham1991 |
…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
…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
…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
…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
…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
…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
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?