Skip to content
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

Clean up reduce_labels for image processors #30799

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

qubvel
Copy link
Member

@qubvel qubvel commented May 14, 2024

What does this PR do?

  • Deprecate reduce_labels for Mask2FormerImageProcessor (it is already deprecated for all other image processors)
  • Remove deprecated reduce_labels for other segmentation models
  • Update examples and docs with do_reduce_labels

Before submitting

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@qubvel qubvel changed the title Clean up do_reduce_labels for image processors Clean up reduce_labels for image processors May 14, 2024
@HuggingFaceDocBuilderDev

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@qubvel qubvel requested a review from amyeroberts May 14, 2024 14:09
Copy link
Collaborator

@amyeroberts amyeroberts left a comment

Choose a reason for hiding this comment

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

Thanks for working on this - great to see more alignment between the image processors.

Only comment is to not raise an exception if reduce_labels is passed in

Comment on lines 112 to 111
warnings.warn(
"The `reduce_labels` parameter is deprecated and will be removed in a future version. Please use"
" `do_reduce_labels` instead.",
FutureWarning,
)
do_reduce_labels = kwargs.pop("reduce_labels")
raise ValueError("The `reduce_labels` parameter has been deprecated. Use `do_reduce_labels` instead.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sadly we can't do this directly for two reasons:

  • A deprecation version wasn't specified in the message (my bad)
  • This is going to break a lot of things for users

We absolutely want to push users to try and use the correct argument, and we should update this value on all of the official checkpoints. However, because this is from the config, which users might not have control over or know how to change; and there might be hundreds of configs with the old value on and off the hub, we're at risk of breaking code and losing users.

What I'd suggest here is:

  • We update the warning message to specify a specific version (typically two versions after the release version this commit will be included in).
  • After this version, we remove the warning. We silently handle this to maintain backwards compatibility i.e. popping the value, and make sure all official references to the flag are removed; we don't guarantee any future maintenance for this value.

Copy link
Member Author

@qubvel qubvel May 15, 2024

Choose a reason for hiding this comment

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

Yes, that's breaking changes...

The idea was to support reading old configs overriding from_dict method with

@classmethod
def from_dict(cls, image_processor_dict: Dict[str, Any], **kwargs):
    ...
    if "reduce_labels" in image_processor_dict:
            image_processor_dict["do_reduce_labels"] = image_processor_dict.pop("reduce_labels")

Here we silently replace reduce_labels with do_reduce_labels.

By raising an exception in __init__, we prevent users from creating new image_processor with deprecated parameters. That might break some fine-tuning scripts, which the user should have control of.

Does it make sense, or am I missing some use cases?
Are such kinds of breaking changes possible only over major versions like v4->v5?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Here we silently replace reduce_labels with do_reduce_labels.

This is great - we definitely want to do this! It means any old config, if saved out, will be fully compatible.

Are such kinds of breaking changes possible only over major versions like v4->v5?

Kind of, it's more to do with the type and scale of breaking change, and the controls users have over it.

We do deprecate methods, flags, etc. within the codebase semi-regularly. And we do tolerate a certain amount of breaking changes. For example, in musicgen recently, training was enabled. This technically created a breaking change, as one of the models previously returned a loss. The update meant that anyone using the model would see the loss suddenly change. However, as the previous loss was incorrect and the model use relatively low this was deemed OK.

There's two main reasons why we wouldn't want to raise an exception here at the moment:

  • Ability to change the code behaviour. Users don't always have the option of updating the config: they might not have permissions to modify on the hub.
  • Awareness of configs. Many users are not familiar with the configs. It's easier to deprecate calls to e.g. object.method(foo, bar) as we emit a warning, and the user just removes that line of code. Changing the config is less obvious.

The reason I think this is because when we first tried to deprecate flags for the image processors, we got quite a few messages from users either complaining or confused about why they were happening. I'm yet to see a single config updated because of them!

It is something we could do in v5, although we may not want to.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks a lot for the clarification, I will summarize PR just to make sure we are on the same page.

There are 3 most popular ways to create an image processor with deprecated parameters (sorted by popularity, high->low, IMHO)

Method Is BC in current PR
image_processor = ImageProcessor.from_pretrained(
  "repo/model_with_deprecated_params"
)
✅ Yes
Because the user does not
have control of it (by overriding from_dict())
image_processor = ImageProcessor.from_pretrained(
  "repo/model_with_deprecated_params",
  reduce_labels=True,
)
🟥 No, raise error
Because it is explicit parameter setting
image_processor = ImageProcessor(
  ...
  reduce_labels=True,
)
🟥 No, raise error
Because it is explicit parameter setting

I hope this approach:

  1. Pushes the user to update their codebase, which they have control over, while still allowing using previously created models.
  2. Will allow us to gradually move backward compatibility for deprecated params only in one method from_dict (ideally), making code easier to support.
  3. Probably, we can also get rid of **kwargs, have a strict set of arguments, and replace checks with a decorator for renamed/removed arguments:
@depecated("reduce_labels", replace_with="do_reduce_labels", from="4.42.0", to="4.45.0")
def preprocess(...):

The decorator will rename the argument and raise a warning in between, and then we can remove it with automated checks, or leave it for a while to raise an explicit error.
At the moment, because of **kwargs, anyone might easily make a typo in an argument name and it will be silently passed, leading to unexpected behavior that is hard to debug.
As a simple example:

from transformers import DetrImageProcessor

image_processor = DetrImageProcessor.from_pretrained(
    "facebook/detr-resnet-50",
    do_nomalize=False, <----- here set as False
)
print(image_processor)

# DetrImageProcessor {
# ...
#   "do_convert_annotations": true,
#   "do_normalize": true, <----- but we get True
#   "do_pad": true, 
#   "do_rescale": true,
#   "do_resize": true,
# ...
# }

We can test this strategy with some particular models, not super popular, just to see how it works.

I might overestimate the problem or underestimate risks, user behavior, or edge cases. You have much more experience maintaining the library, so let me know if any of these thoughts are valuable. And thank you once again for your clarifications with examples, it is much easier to follow 🤗

Copy link
Collaborator

Choose a reason for hiding this comment

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

100% aligned with you comment above, thanks for taking the time to write this up so clearly.

I really like the idea of a decorator. Let's ask @molbap his opinion on this, as he's been handling a lot of these considerations across processors and image processor.

Copy link
Contributor

Choose a reason for hiding this comment

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

OOO for a few days but checked and it's an interesting take. I like the decorator idea too. I wholeheartedly agree with

At the moment, because of **kwargs, anyone might easily make a typo in an argument name and it will be silently passed, leading to unexpected behavior that is hard to debug.

That is almost always true. That's why getting rid of **kwargs is impossible, as we need to be able to handle arbitrary inputs without breaking runtime in situations where people have non-canonical configs.

Hence the point of having a list of valid arguments: that way, we can at least raise a warning and list the arguments passed by a user that are not doing anything. In your case, if do_normalize is part of the valid image processor arguments but do_nomalize (typo) is not, then we can raise a clear warning saying that do_nomalize isn't used anywhere.
We can even push this a bit and do a quick levenshtein check conditioning a perhaps you made a typo? message. That might be too much.

For the decorator option to mark args as deprecated, LGTM too! Less loc, and forces us to write a version number. I'd say the "from" is not necessarily useful as we want to encourage using the latest version, imo.
Thanks for looking into this @qubvel! Kind of related is #30511 that I want to merge ASAP, didn't have time to get back to it with but it's closely connected to this problem.

Copy link
Member Author

@qubvel qubvel May 20, 2024

Choose a reason for hiding this comment

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

Thanks for looking into this @molbap

Probably we can use a function signature in decorator to filter not-used kwargs instead of passing them.
What do you think about the following?

import functools
import inspect
import warnings
import numpy as np


def filter_not_used_arguments(func):
    """Filter out named arguments that are not in the function signature."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):

        sig = inspect.signature(func)
        function_named_args = sig.parameters.keys()
        valid_kwargs = {k: v for k, v in kwargs.items() if k in function_named_args}
        invalid_kwargs = {k: v for k, v in kwargs.items() if k not in function_named_args}
        
        if invalid_kwargs:
            warnings.warn(f"Unused named arguments: {', '.join(invalid_kwargs.keys())}")
        
        return func(*args, **valid_kwargs)
    
    return wrapper


class ImageProcessor:

    @filter_not_used_arguments
    def preprocess(self, image, do_normalize=False, do_reduce_labels=False):
        pass


image_processor = ImageProcessor()
image = np.ones((100, 100, 3))

# passing invalid  `do_nomalize` instead of `do_normalize`
image_processor.preprocess(image, do_nomalize=True, do_reduce_labels=True)  
# UserWarning: Unused named arguments: do_nomalize
  1. This will simplify validation, we will not need to store the list of available arguments
  2. Make validation method-specific instead of class-specific
  3. While this is backward compatible and we do not raise an error here, IDE will help users and force users to update parameters if no **kwargs is specified for the function
Screenshot 2024-05-20 at 10 36 32

Copy link
Contributor

Choose a reason for hiding this comment

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

That looks like a great solution :) looks very good to me

@qubvel qubvel marked this pull request as draft May 16, 2024 13:02
@qubvel qubvel force-pushed the clean-up-do-reduce-labels branch from 3f256a1 to b0e26f5 Compare May 24, 2024 00:09
@@ -406,38 +412,18 @@ def __init__(
image_std: Union[float, List[float]] = None,
ignore_index: Optional[int] = None,
do_reduce_labels: bool = False,
num_labels: Optional[int] = None,
Copy link
Member Author

Choose a reason for hiding this comment

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

Here, in Mask2Former, and OneFormer: num_labels is not used somewhere across code of image_processor, however it is widely used in tests. I added it explicitly for backward compatibility, in case some pipelines rely on that

Copy link
Member Author

@qubvel qubvel May 24, 2024

Choose a reason for hiding this comment

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

Also could be passed in **kwargs and excluded from filter

@filter_out_non_signature_kwargs(extra=["max_size", "num_labels"])

@qubvel
Copy link
Member Author

qubvel commented May 28, 2024

@molbap @amyeroberts I made a new iteration here. Added two decorators

  1. @deprecate_kwarg("reduce_labels", new_name="do_reduce_labels", version="4.41.0")
  • rename argument if new_name is provided
  • raise warning if current version < version specified, then just do it silently
  1. filter_out_non_signature_kwargs(extra=["max_size"])
  • filter out all named args that are not in the function signature and not specified in extra. As mentioned by @molbap it is not always possible to get rid of all **kwargs (for example, it might include complex logic of parameters interaction).

Please, have a look, feedback is appreciated 🤗

@qubvel qubvel mentioned this pull request May 28, 2024
3 tasks
Copy link
Collaborator

@amyeroberts amyeroberts left a comment

Choose a reason for hiding this comment

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

Really beautiful work - this could be very useful across the library and will help kill so much repeated code ❤️

Only major comment is about the new files src/transformers/utils/kwargs_validation.py and src/transformers/utils/deprecation.py, and whether we want to add these whole new modules into utils.

Personally, I think src/transformers/utils/deprecation.py makes sense. For filter_out_non_signature_kwargs, I'd place it under (the admittedly badly named) utils/generic.py for now

Would be great to get a second opinion on this from @molbap!

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - missing copyright header

Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - missing copyright header

Comment on lines +34 to +38
old_name (str): name of the deprecated keyword argument
version (str): version when the keyword argument was (will be) deprecated
new_name (Optional[str], optional): new name of the keyword argument. Defaults to None.
raise_if_ge_version (bool, optional): raise ValueError if deprecated version is greater or equal to current version. Defaults to False.
raise_if_both_names (bool, optional): raise ValueError if both deprecated and new keyword arguments are set. Defaults to False.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - convention in the library is to not mention default value if it is None. If it controls specific behaviour, we normally refer to it being set or unset

Suggested change
old_name (str): name of the deprecated keyword argument
version (str): version when the keyword argument was (will be) deprecated
new_name (Optional[str], optional): new name of the keyword argument. Defaults to None.
raise_if_ge_version (bool, optional): raise ValueError if deprecated version is greater or equal to current version. Defaults to False.
raise_if_both_names (bool, optional): raise ValueError if both deprecated and new keyword arguments are set. Defaults to False.
old_name (`str`): name of the deprecated keyword argument
version (`str`): version when the keyword argument was (will be) deprecated
new_name (`Optional[str]`, *optional*): new name of the keyword argument.
raise_if_ge_version (`bool`, *optional*, defaults to `False`): raise ValueError if deprecated version is greater or equal to current version.
raise_if_both_names (`bool`, *optional*, defaults to `False`): raise ValueError if both deprecated and new keyword arguments are set.

@@ -108,7 +108,7 @@ class DataTrainingArguments:
)
},
)
reduce_labels: Optional[bool] = field(
do_reduce_labels: Optional[bool] = field(
Copy link
Collaborator

Choose a reason for hiding this comment

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

For the script, we'll need to deprecate reduce_labels e.g. like here

@@ -86,7 +86,7 @@ def parse_args():
default="segments/sidewalk-semantic",
)
parser.add_argument(
"--reduce_labels",
"--do_reduce_labels",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here re deprecation

new_name: Optional[str] = None,
raise_if_ge_version: bool = False,
raise_if_both_names: bool = False,
add_message: Optional[str] = None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - could we call this additional_message? add_message reads as a bool to me (don't feel strongly about this, so happy if you decide to stick with the original)

Comment on lines +78 to +83

# raise error or notify user
if minimum_action == Action.RAISE:
raise ValueError(message)
elif minimum_action == Action.NOTIFY and raise_if_ge_version and is_already_deprecated:
raise ValueError(message)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - I'd separate this out between setting the minimum action and the raise/notify logic, just to make it a bit clearer

Suggested change
# raise error or notify user
if minimum_action == Action.RAISE:
raise ValueError(message)
elif minimum_action == Action.NOTIFY and raise_if_ge_version and is_already_deprecated:
raise ValueError(message)
if minimum_action == Action.NOTIFY and raise_if_ge_version and is_already_deprecated:
miminum_action = Action.RAISE
# raise error or notify user
if minimum_action == Action.RAISE:
raise ValueError(message)

from typing import Optional


def filter_out_non_signature_kwargs(extra: Optional[list] = None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

❤️

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.

None yet

4 participants