Skip to content

ENH Raises error in hinge_loss when 'pred_decision' is invalid #19643

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

Conversation

PierreAttard
Copy link
Contributor

Reference Issues/PRs

Fixes #19638

What does this implement/fix? Explain your changes.

With multiclass target (more than 2 classes), the pred_decision argument must have a shape of type :
(n_sample, n_classes) like written in the doc.
If it is not the case, an arror is raised with a clear message.

Any other comments?

A new test has been added in test_classification in order to check that situation.

…number of classes with a multiclass target case.

New test in order to check this situation in 'test_classification.py'
@cmarmo cmarmo added the Bug label Mar 8, 2021
@PierreAttard PierreAttard changed the title raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target label:"No Changelog Needed" Mar 8, 2021
@PierreAttard PierreAttard changed the title raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target label:"No Changelog Needed" raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target Mar 8, 2021
Copy link
Member

@thomasjpfan thomasjpfan left a comment

Choose a reason for hiding this comment

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

Thank you for the PR @PierreAttard !

Comment on lines 2374 to 2381
if (pred_decision.ndim == 1) or \
(labels is not None and pred_decision.ndim > 1 and
np.size(y_true_unique) != pred_decision.shape[1]):
raise ValueError("The shape of pred_decision is not "
"consistent with the number of classes. "
"pred_decision shape must be "
"(n_samples, n_classes) with "
"multiclass target")
Copy link
Member

Choose a reason for hiding this comment

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

The logically flow in this section is a bit strange. I think the following would be clearer:

labels = np.unique(labels if labels is not None else y_true)
if labels.size > 2:
	if pred_decision.ndim == 1 or labels.size != pred_decision.shape[1]:
		raise ValueError(...)

	le = LabelEncoder()
	le.fit(labels)
    ...

This would cover both cases for input validation. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the PR @PierreAttard !

It's a pleasure !

Indeed, I agreed. I made a modification that looks like what you have proposed at first.
But in this case, we will can not cover the situation labels is None :

        if (labels is None and pred_decision.ndim > 1 and
                (np.size(y_true_unique) != pred_decision.shape[1])):
            raise ValueError("Please include all labels in y_true "
                             "or pass labels as third argument")

If it's ok for you, I can do the modification you proposed and adapt the below unitary test in file test_classification.py to the new situation :

def test_hinge_loss_multiclass_missing_labels_with_labels_none

Copy link
Member

Choose a reason for hiding this comment

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

I am thinking of combing the two if statements into one because they almost do the same check, but looking at this more closely I think we want the two error messages. How about this:

if pred_decision.ndim > 1 and np.size(y_true_unique) != pred_decision.shape[1]):
	if labels is not None:
	    raise ValueError("The shape of pred_decision is not consistent ...")
	else:
		raise ValueError("Please include ..")

while leaving everything else the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems a good solution to me.

Copy link
Contributor Author

@PierreAttard PierreAttard Mar 10, 2021

Choose a reason for hiding this comment

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

Ah finally, we miss the situation of the intial issues #19638 :

  • pred_decision with only 1 dimension
  • Whereas it is a multiclass problem

So, we can do add pred_decision.ndim == 1:

if pred_decision.ndim == 1 or (pred_decision.ndim > 1 and np.size(y_true_unique) != pred_decision.shape[1]):
	if labels is not None:
	    raise ValueError("The shape of pred_decision is not consistent ...")
	else:
	    raise ValueError("Please include ..")

But we would get the wrong error message.

Copy link
Member

Choose a reason for hiding this comment

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

Yea that makes sense. I think we can also remove one of the conditions:

if pred_decision.ndim <= 1 or y_true_unique.size != pred_decision.shape[1]:
	if labels is not None:
	    raise ValueError("The shape of pred_decision is not consistent ...")
	else:
	    raise ValueError("Please include ..")

This would catch the ndim == 0 case as well. This feels like an exercise in boolean logic :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This feels like an exercise in boolean logic :)

Indeed !!

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 add labels is not None or pred_decision.ndim <= 1 in order to get the right error message when you put a 1d array for pred_decision whereas it is a multiclass problem.

Comment on lines 2376 to 2384
if labels is not None or pred_decision.ndim <= 1:
raise ValueError("The shape of pred_decision is not "
"consistent with the number of classes. "
"pred_decision shape must be "
"(n_samples, n_classes) with "
"multiclass target")
else:
raise ValueError("Please include all labels in y_true "
"or pass labels as third argument")
Copy link
Member

Choose a reason for hiding this comment

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

A small nit. With another condition, on labels is None, the logic becomes harder to follow at a glance. At this point, I think creating invalid_decision_shape would make this condition more explicit:

invalid_decision_shape = (pred_decision.ndim > 1 and 
                          y_true_unique.size != pred_decision.shape[1]))

if labels is None:
    if invalid_decision_shape:
        raise ValueError("Please include all the labels...")
elif invalid_decision_shape or pred_decision.ndim <= 1:
    raise ValueError("The shape of pred_decision...")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, indeed, and it's more elegant.

Copy link
Contributor Author

@PierreAttard PierreAttard Mar 11, 2021

Choose a reason for hiding this comment

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

But we still have the same issue.
If the user do like #19638, it means inge_loss(y_true=[2,1,0,1,0,1,1], pred_decision=[0,1,2,1,0,2,1]),
the user will get the message "Please include all the labels..." whereas the true error should be "The shape of pred_decision...". If the user, after the error message "Please include all the labels...", pass the labels parameter, he will still have an error message.

The logical difficulty comes from the fact that there is a "specific case" for the situation multiclass problem AND pred_decision 1d given. It means that the user did not understand the meaning of this parameter (like I didn't).

Comment on lines 2148 to 2149
assert_raise_message(ValueError, error_message, hinge_loss,
y_true=y_true, pred_decision=pred_decision)
Copy link
Member

Choose a reason for hiding this comment

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

We are moving toward pytest.raises for testing error messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok !

pred_decision = [[0, 1], [0, 1], [0, 1], [0, 1],
[2, 0], [0, 1], [1, 0]]
labels = [0, 1, 2]
assert_raise_message(ValueError, error_message, hinge_loss,
Copy link
Member

Choose a reason for hiding this comment

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

Same here regarding pytest.raises.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok !

Comment on lines 168 to 171
- |Enhancement| A fix to raise an error in :func:`metrics.hinge_loss` when ``pred_decision`` is 1d
whereas it is a multiclass classification or when ``pred_decision``
parameter is not consistent with ``labels`` parameter.
:pr:`19643` by :user:`Pierre Attard <PierreAttard>`.
Copy link
Member

Choose a reason for hiding this comment

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

We also try to keep this < 80 characters

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok !

@PierreAttard
Copy link
Contributor Author

So, I proposed to put all consistency checks inside a function, I think it is clearer.

@@ -2285,6 +2285,48 @@ def log_loss(y_true, y_pred, *, eps=1e-15, normalize=True, sample_weight=None,
return _weighted_sum(loss, sample_weight, normalize)


def _check_valid_multiclass_decision_shape(y_true_unique, pred_decision,
Copy link
Member

Choose a reason for hiding this comment

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

I would be okay with creating another function if we can use this check in other metrics. I can be missing one, but I do not see a metric that does this validation.

Comment on lines 2316 to 2321
if labels is None:
if invalid_decision_shape:
raise ValueError("Please include all labels in y_true "
"or pass labels as third argument")
elif invalid_decision_shape:
raise ValueError("The shape of pred_decision is not "
Copy link
Member

Choose a reason for hiding this comment

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

With the pred_decision.ndim <= 1 handled before hand. I think the following is simple enough to inlined into hinge_loss.

if pred_decision.ndim <= 1:
	raise ValueError("The shape of pred_decision can not be 1d...")

# pred_decision.ndim > 1 is true
if y_true_unique.size != pred_decision.shape[1]:
	if labels is None:
		raise ValueError("Please include all...")
	else:
		raise ValueError("The shape of pred_decision...")

Thank you for your patience on this PR @PierreAttard .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's ok !

@@ -135,7 +134,7 @@ def test_classification_report_dictionary_output():
target_names=iris.target_names, output_dict=True)

# assert the 2 dicts are equal.
assert(report.keys() == expected_report.keys())
assert (report.keys() == expected_report.keys())
Copy link
Member

Choose a reason for hiding this comment

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

There are some unrelated changes in this PR, most likely caused by an editor. May these changes be reverted?

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 check it.

Copy link
Contributor Author

@PierreAttard PierreAttard Mar 11, 2021

Choose a reason for hiding this comment

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

Indeed, I didn't notice those changes in a commit.

Copy link
Member

@ogrisel ogrisel left a comment

Choose a reason for hiding this comment

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

Some more suggestions but otherwise, LGTM.

PierreAttard and others added 2 commits March 11, 2021 19:44
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org>
Copy link
Member

@ogrisel ogrisel left a comment

Choose a reason for hiding this comment

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

LGTM with the following details:

PierreAttard and others added 3 commits March 12, 2021 18:37
Co-authored-by: Olivier Grisel <olivier.grisel@ensta.org>
…nto hinge_loss_y_true_consistency

# Conflicts:
#	sklearn/metrics/tests/test_classification.py
@ogrisel
Copy link
Member

ogrisel commented Mar 12, 2021

Thank very much @PierreAttard for making the error message much more user-friendly.

@ogrisel
Copy link
Member

ogrisel commented Mar 12, 2021

Final review @thomasjpfan?

Copy link
Member

@thomasjpfan thomasjpfan left a comment

Choose a reason for hiding this comment

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

LGTM

@thomasjpfan thomasjpfan changed the title raise an error when the 'pred_decision' shape is not consistent with number of classes with a multiclass target ENH Raises error in hinge_loss when 'pred_decision' is invalid Mar 12, 2021
@thomasjpfan thomasjpfan merged commit f4e692c into scikit-learn:main Mar 12, 2021
marrodion pushed a commit to marrodion/scikit-learn that referenced this pull request Mar 17, 2021
@glemaitre glemaitre mentioned this pull request Apr 22, 2021
12 tasks
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.

[BUG] hinge_loss multi
4 participants