Skip to content

Commit a72a1c3

Browse files
authored
fix(pipelines): respect problem_type in image and video classification pipelines (#50582)
ImageClassificationPipeline checked problem_type == multi_label_classification or num_labels == 1 before checking problem_type == regression, so regression models had their outputs squashed through sigmoid (for num_labels == 1) or softmax (for num_labels > 1). Check problem_type == regression first, matching TextClassificationPipeline (#34066). VideoClassificationPipeline unconditionally defaulted function_to_apply to softmax in _sanitize_parameters(), ignoring model.config.problem_type and overwriting initialization-time function_to_apply overrides on subsequent calls. Defer the default to postprocess() based on problem_type, matching AudioClassificationPipeline (#50329). Co-authored-by: surendra902 <surendra902@users.noreply.github.com>
1 parent 715b48b commit a72a1c3

4 files changed

Lines changed: 98 additions & 11 deletions

File tree

‎src/transformers/pipelines/image_classification.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class ClassificationFunction(ExplicitEnum):
6565
The function to apply to the model outputs in order to retrieve the scores. Accepts four different values:
6666
6767
- `"default"`: if the model has a single label, will apply the sigmoid function on the output. If the model
68-
has several labels, will apply the softmax function on the output.
68+
has several labels, will apply the softmax function on the output. In case of regression tasks, will not
69+
apply any function on the output.
6970
- `"sigmoid"`: Applies the sigmoid function on the output.
7071
- `"softmax"`: Applies the softmax function on the output.
7172
- `"none"`: Does not apply any function on the output.""",
@@ -148,6 +149,7 @@ def __call__(
148149
If this argument is not specified, then it will apply the following functions according to the number
149150
of labels:
150151
152+
- If problem type is regression, will not apply any function on the output.
151153
- If the model has a single label, will apply the sigmoid function on the output.
152154
- If the model has several labels, will apply the softmax function on the output.
153155
@@ -193,7 +195,9 @@ def _forward(self, model_inputs):
193195

194196
def postprocess(self, model_outputs, function_to_apply=None, top_k=5):
195197
if function_to_apply is None:
196-
if self.model.config.problem_type == "multi_label_classification" or self.model.config.num_labels == 1:
198+
if self.model.config.problem_type == "regression":
199+
function_to_apply = ClassificationFunction.NONE
200+
elif self.model.config.problem_type == "multi_label_classification" or self.model.config.num_labels == 1:
197201
function_to_apply = ClassificationFunction.SIGMOID
198202
elif self.model.config.problem_type == "single_label_classification" or self.model.config.num_labels > 1:
199203
function_to_apply = ClassificationFunction.SOFTMAX

‎src/transformers/pipelines/video_classification.py‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ def _sanitize_parameters(self, top_k=None, num_frames=None, frame_sampling_rate=
9292
"Valid options are ['softmax', 'sigmoid', 'none']"
9393
)
9494
postprocess_params["function_to_apply"] = function_to_apply
95-
else:
96-
postprocess_params["function_to_apply"] = "softmax"
9795
return preprocess_params, {}, postprocess_params
9896

9997
@overload
@@ -124,11 +122,11 @@ def __call__(self, inputs: str | list[str] | None, **kwargs):
124122
frame_sampling_rate (`int`, *optional*, defaults to 1):
125123
The sampling rate used to select frames from the video. If not provided, will default to 1, i.e. every
126124
frame will be used.
127-
function_to_apply(`str`, *optional*, defaults to "softmax"):
128-
The function to apply to the model output. By default, the pipeline will apply the softmax function to
129-
the output of the model. Valid options: ["softmax", "sigmoid", "none"]. Note that passing Python's
130-
built-in `None` will default to "softmax", so you need to pass the string "none" to disable any
131-
post-processing.
125+
function_to_apply (`str`, *optional*):
126+
The function to apply to the model output. Defaults to "none" for regression, "sigmoid" for
127+
multi-label classification, and "softmax" otherwise. Valid options: ["softmax", "sigmoid", "none"].
128+
Explicit values supplied at initialization or call time override these defaults.
129+
Pass the string "none" to disable post-processing.
132130
133131
Return:
134132
A list of dictionaries or a list of list of dictionaries containing result. If the input is a single video,
@@ -170,7 +168,15 @@ def _forward(self, model_inputs):
170168
model_outputs = self.model(**model_inputs)
171169
return model_outputs
172170

173-
def postprocess(self, model_outputs, top_k=5, function_to_apply="softmax"):
171+
def postprocess(self, model_outputs, top_k=5, function_to_apply=None):
172+
if function_to_apply is None:
173+
if self.model.config.problem_type == "regression":
174+
function_to_apply = "none"
175+
elif self.model.config.problem_type == "multi_label_classification":
176+
function_to_apply = "sigmoid"
177+
else:
178+
function_to_apply = "softmax"
179+
174180
if top_k > self.model.config.num_labels:
175181
top_k = self.model.config.num_labels
176182

‎tests/pipelines/test_pipelines_image_classification.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,13 @@ def test_small_model_pt(self):
173173
],
174174
)
175175

176+
image_classifier.model.config.problem_type = "regression"
177+
outputs = image_classifier("./tests/fixtures/tests_samples/COCO/000000039769.png")
178+
self.assertEqual(
179+
nested_simplify(outputs, decimals=4),
180+
[{"label": "LABEL_1", "score": 0.1426}, {"label": "LABEL_0", "score": -0.1557}],
181+
)
182+
176183
def test_custom_tokenizer(self):
177184
tokenizer = PreTrainedTokenizerBase()
178185

‎tests/pipelines/test_pipelines_video_classification.py‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
from huggingface_hub import VideoClassificationOutputElement, hf_hub_download
1818

19-
from transformers import MODEL_FOR_VIDEO_CLASSIFICATION_MAPPING, VideoMAEImageProcessor
19+
from transformers import (
20+
MODEL_FOR_VIDEO_CLASSIFICATION_MAPPING,
21+
AutoModelForVideoClassification,
22+
VideoMAEImageProcessor,
23+
is_torch_available,
24+
)
2025
from transformers.pipelines import VideoClassificationPipeline, pipeline
2126
from transformers.testing_utils import (
2227
compare_pipeline_output_to_hub_spec,
@@ -30,6 +35,10 @@
3035
from .test_pipelines_common import ANY
3136

3237

38+
if is_torch_available():
39+
import torch
40+
41+
3342
@is_pipeline_test
3443
@require_torch
3544
@require_vision
@@ -122,3 +131,64 @@ def test_small_model_pt(self):
122131
for output in outputs:
123132
for element in output:
124133
compare_pipeline_output_to_hub_spec(element, VideoClassificationOutputElement)
134+
135+
@require_torch
136+
def test_problem_type_defaults_and_overrides(self):
137+
small_model = AutoModelForVideoClassification.from_pretrained(
138+
"hf-internal-testing/tiny-random-VideoMAEForVideoClassification"
139+
)
140+
small_image_processor = VideoMAEImageProcessor(
141+
size={"shortest_edge": 10}, crop_size={"height": 10, "width": 10}
142+
)
143+
classifier = pipeline(
144+
"video-classification",
145+
model=small_model,
146+
image_processor=small_image_processor,
147+
frame_sampling_rate=4,
148+
)
149+
video_file_path = hf_hub_download(repo_id="nateraw/video-demo", filename="archery.mp4", repo_type="dataset")
150+
num_labels = classifier.model.config.num_labels
151+
labels = [classifier.model.config.id2label[i] for i in range(num_labels)]
152+
153+
raw = classifier(video_file_path, top_k=num_labels, function_to_apply="none")
154+
raw_scores = {item["label"]: item["score"] for item in raw}
155+
logits = torch.tensor([raw_scores[label] for label in labels])
156+
157+
for problem_type, activation in (
158+
("regression", "none"),
159+
("multi_label_classification", "sigmoid"),
160+
("single_label_classification", "softmax"),
161+
(None, "softmax"),
162+
):
163+
with self.subTest(problem_type=problem_type):
164+
classifier.model.config.problem_type = problem_type
165+
result = classifier(video_file_path, top_k=num_labels)
166+
scores = {item["label"]: item["score"] for item in result}
167+
expected = {
168+
"none": logits,
169+
"sigmoid": logits.sigmoid(),
170+
"softmax": logits.softmax(-1),
171+
}[activation]
172+
self.assertEqual(len(scores), num_labels)
173+
for label, value in zip(labels, expected.tolist()):
174+
self.assertAlmostEqual(scores[label], value, places=4)
175+
176+
classifier.model.config.problem_type = "regression"
177+
initialized = pipeline(
178+
"video-classification",
179+
model=classifier.model,
180+
image_processor=small_image_processor,
181+
frame_sampling_rate=4,
182+
function_to_apply="softmax",
183+
)
184+
185+
for override, expected in (
186+
(None, logits.softmax(-1)),
187+
("none", logits),
188+
("sigmoid", logits.sigmoid()),
189+
):
190+
with self.subTest(call_override=override):
191+
result = initialized(video_file_path, top_k=num_labels, function_to_apply=override)
192+
scores = {item["label"]: item["score"] for item in result}
193+
for label, value in zip(labels, expected.tolist()):
194+
self.assertAlmostEqual(scores[label], value, places=4)

0 commit comments

Comments
 (0)