Skip to content

Commit e2ca152

Browse files
authored
feat(eval): add zero-shot text classification evaluator (#387)
Resolves #325. Adds `WinMLZeroShotClassificationEvaluator`, registered under the `zero-shot-classification` task, with a pipeline subclass that pads to `max_length` for static-shape ONNX. Padding/truncation logic lives on `WinMLEvaluator` as `_fixed_seq_length` and `_pad_or_truncate` so other fixed-shape evaluators can reuse it. Accuracy and macro-F1 are computed via a new `ClassificationMetric`, since HF evaluate has no wrapper for this task. Default dataset is AG News; E2E entries for seven NLI checkpoints (DeBERTa-v3, mDeBERTa-v3, XLM-RoBERTa, etc.) are included. Unit tests cover the evaluator and the metric. An integration test (slow/network) runs end-to-end on DistilBERT, RoBERTa, and DeBERTa NLI checkpoints.
1 parent 5ebdb90 commit e2ca152

12 files changed

Lines changed: 1169 additions & 0 deletions

File tree

scripts/e2e_eval/testsets/models_with_acc.json

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,23 @@
12711271
}
12721272
}
12731273
},
1274+
{
1275+
"hf_id": "cross-encoder/nli-deberta-v3-small",
1276+
"task": "zero-shot-classification",
1277+
"model_type": "deberta-v2",
1278+
"group": "Top200",
1279+
"priority": "P1",
1280+
"dataset_config": {
1281+
"path": "nyu-mll/multi_nli",
1282+
"split": "validation_matched",
1283+
"metric": "accuracy",
1284+
"columns_mapping": {
1285+
"input_column": "premise",
1286+
"label_column": "genre",
1287+
"candidate_labels": "fiction,government,slate,telephone,travel"
1288+
}
1289+
}
1290+
},
12741291
{
12751292
"hf_id": "openai/clip-vit-base-patch32",
12761293
"task": "zero-shot-image-classification",
@@ -1289,6 +1306,23 @@
12891306
}
12901307
}
12911308
},
1309+
{
1310+
"hf_id": "joeddav/xlm-roberta-large-xnli",
1311+
"task": "zero-shot-classification",
1312+
"model_type": "xlm-roberta",
1313+
"group": "Top200",
1314+
"priority": "P1",
1315+
"dataset_config": {
1316+
"path": "nyu-mll/multi_nli",
1317+
"split": "validation_matched",
1318+
"metric": "accuracy",
1319+
"columns_mapping": {
1320+
"input_column": "premise",
1321+
"label_column": "genre",
1322+
"candidate_labels": "fiction,government,slate,telephone,travel"
1323+
}
1324+
}
1325+
},
12921326
{
12931327
"hf_id": "openai/clip-vit-large-patch14",
12941328
"task": "zero-shot-image-classification",
@@ -1307,6 +1341,23 @@
13071341
}
13081342
}
13091343
},
1344+
{
1345+
"hf_id": "lxyuan/distilbert-base-multilingual-cased-sentiments-student",
1346+
"task": "zero-shot-classification",
1347+
"model_type": "distilbert",
1348+
"group": "Top200",
1349+
"priority": "P1",
1350+
"dataset_config": {
1351+
"path": "nyu-mll/multi_nli",
1352+
"split": "validation_matched",
1353+
"metric": "accuracy",
1354+
"columns_mapping": {
1355+
"input_column": "premise",
1356+
"label_column": "genre",
1357+
"candidate_labels": "fiction,government,slate,telephone,travel"
1358+
}
1359+
}
1360+
},
13101361
{
13111362
"hf_id": "openai/clip-vit-large-patch14-336",
13121363
"task": "zero-shot-image-classification",
@@ -1325,6 +1376,23 @@
13251376
}
13261377
}
13271378
},
1379+
{
1380+
"hf_id": "MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli",
1381+
"task": "zero-shot-classification",
1382+
"model_type": "deberta-v2",
1383+
"group": "Top200",
1384+
"priority": "P1",
1385+
"dataset_config": {
1386+
"path": "nyu-mll/multi_nli",
1387+
"split": "validation_matched",
1388+
"metric": "accuracy",
1389+
"columns_mapping": {
1390+
"input_column": "premise",
1391+
"label_column": "genre",
1392+
"candidate_labels": "fiction,government,slate,telephone,travel"
1393+
}
1394+
}
1395+
},
13281396
{
13291397
"hf_id": "openai/clip-vit-base-patch16",
13301398
"task": "zero-shot-image-classification",
@@ -1343,6 +1411,23 @@
13431411
}
13441412
}
13451413
},
1414+
{
1415+
"hf_id": "MoritzLaurer/deberta-v3-large-zeroshot-v2.0",
1416+
"task": "zero-shot-classification",
1417+
"model_type": "deberta-v2",
1418+
"group": "Top200",
1419+
"priority": "P1",
1420+
"dataset_config": {
1421+
"path": "nyu-mll/multi_nli",
1422+
"split": "validation_matched",
1423+
"metric": "accuracy",
1424+
"columns_mapping": {
1425+
"input_column": "premise",
1426+
"label_column": "genre",
1427+
"candidate_labels": "fiction,government,slate,telephone,travel"
1428+
}
1429+
}
1430+
},
13461431
{
13471432
"hf_id": "laion/CLIP-ViT-B-32-laion2B-s34B-b79K",
13481433
"task": "zero-shot-image-classification",
@@ -1361,6 +1446,23 @@
13611446
}
13621447
}
13631448
},
1449+
{
1450+
"hf_id": "MoritzLaurer/mDeBERTa-v3-base-mnli-xnli",
1451+
"task": "zero-shot-classification",
1452+
"model_type": "deberta-v2",
1453+
"group": "Top200",
1454+
"priority": "P1",
1455+
"dataset_config": {
1456+
"path": "nyu-mll/multi_nli",
1457+
"split": "validation_matched",
1458+
"metric": "accuracy",
1459+
"columns_mapping": {
1460+
"input_column": "premise",
1461+
"label_column": "genre",
1462+
"candidate_labels": "fiction,government,slate,telephone,travel"
1463+
}
1464+
}
1465+
},
13641466
{
13651467
"hf_id": "patrickjohncyh/fashion-clip",
13661468
"task": "zero-shot-image-classification",
@@ -1378,6 +1480,23 @@
13781480
}
13791481
}
13801482
},
1483+
{
1484+
"hf_id": "MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7",
1485+
"task": "zero-shot-classification",
1486+
"model_type": "deberta-v2",
1487+
"group": "Top200",
1488+
"priority": "P1",
1489+
"dataset_config": {
1490+
"path": "nyu-mll/multi_nli",
1491+
"split": "validation_matched",
1492+
"metric": "accuracy",
1493+
"columns_mapping": {
1494+
"input_column": "premise",
1495+
"label_column": "genre",
1496+
"candidate_labels": "fiction,government,slate,telephone,travel"
1497+
}
1498+
}
1499+
},
13811500
{
13821501
"hf_id": "google/siglip-so400m-patch14-384",
13831502
"task": "zero-shot-image-classification",

src/winml/modelkit/datasets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"sentence-similarity": TextDataset,
4646
"next-sentence-prediction": TextDataset,
4747
"fill-mask": TextDataset,
48+
"zero-shot-classification": TextDataset,
4849
"image-segmentation": ImageSegmentationDataset,
4950
"random": RandomDataset,
5051
# Add more task types as needed

src/winml/modelkit/eval/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .fill_mask_evaluator import WinMLFillMaskEvaluator
1616
from .image_feature_extraction_evaluator import WinMLImageFeatureExtractionEvaluator
1717
from .image_segmentation_evaluator import WinMLImageSegmentationEvaluator
18+
from .metrics.classification import ClassificationMetric
1819
from .metrics.knn_accuracy import KNNAccuracyMetric
1920
from .metrics.mean_average_precision import MAPMetric
2021
from .metrics.mean_iou import IGNORE_INDEX, MeanIoUMetric
@@ -25,11 +26,13 @@
2526
from .question_answering_evaluator import WinMLQuestionAnsweringEvaluator
2627
from .text_classification_evaluator import WinMLTextClassificationEvaluator
2728
from .token_classification_evaluator import WinMLTokenClassificationEvaluator
29+
from .zero_shot_classification_evaluator import WinMLZeroShotClassificationEvaluator
2830
from .zero_shot_image_classification_evaluator import WinMLZeroShotImageClassificationEvaluator
2931

3032

3133
__all__ = [
3234
"IGNORE_INDEX",
35+
"ClassificationMetric",
3336
"EvalResult",
3437
"KNNAccuracyMetric",
3538
"MAPMetric",
@@ -47,6 +50,7 @@
4750
"WinMLQuestionAnsweringEvaluator",
4851
"WinMLTextClassificationEvaluator",
4952
"WinMLTokenClassificationEvaluator",
53+
"WinMLZeroShotClassificationEvaluator",
5054
"WinMLZeroShotImageClassificationEvaluator",
5155
"evaluate",
5256
]

src/winml/modelkit/eval/base_evaluator.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,38 @@ def prepare_pipeline(self) -> Pipeline:
143143
device="cpu",
144144
)
145145

146+
def _fixed_seq_length(self) -> int | None:
147+
"""Return the model's fixed sequence length, or ``None`` if dynamic.
148+
149+
Reads ``io_config["input_shapes"]`` and treats an integer second
150+
dimension as a static sequence length. Subclasses use this to decide
151+
whether tokenized inputs need to be padded/truncated to a fixed size.
152+
"""
153+
io_config = getattr(self.model, "io_config", None) or {}
154+
shapes = io_config.get("input_shapes") or [[]]
155+
if len(shapes[0]) > 1 and isinstance(shapes[0][1], int):
156+
return shapes[0][1]
157+
return None
158+
159+
def _pad_or_truncate(self, encoding: Any, tokenizer: Any) -> Any:
160+
"""Resize tokenized inputs to the model's fixed sequence length.
161+
162+
No-op for dynamic-shape models. Otherwise truncates over-length
163+
tensors and delegates padding to the tokenizer.
164+
"""
165+
seq_len = self._fixed_seq_length()
166+
if seq_len is None:
167+
return encoding
168+
for key, tensor in list(encoding.items()):
169+
if hasattr(tensor, "shape") and tensor.dim() >= 2 and tensor.shape[1] > seq_len:
170+
encoding[key] = tensor[:, :seq_len]
171+
return tokenizer.pad(
172+
encoding,
173+
padding="max_length",
174+
max_length=seq_len,
175+
return_tensors="pt",
176+
)
177+
146178
def align_labels(self, dataset: Dataset, ds_config: DatasetConfig) -> Dataset:
147179
"""Align dataset labels and filter unsupported IDs.
148180

src/winml/modelkit/eval/evaluate.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .question_answering_evaluator import WinMLQuestionAnsweringEvaluator
2424
from .text_classification_evaluator import WinMLTextClassificationEvaluator
2525
from .token_classification_evaluator import WinMLTokenClassificationEvaluator
26+
from .zero_shot_classification_evaluator import WinMLZeroShotClassificationEvaluator
2627
from .zero_shot_image_classification_evaluator import WinMLZeroShotImageClassificationEvaluator
2728

2829

@@ -43,6 +44,7 @@
4344
"sentence-similarity": WinMLFeatureExtractionEvaluator,
4445
"image-feature-extraction": WinMLImageFeatureExtractionEvaluator,
4546
"fill-mask": WinMLFillMaskEvaluator,
47+
"zero-shot-classification": WinMLZeroShotClassificationEvaluator,
4648
"zero-shot-image-classification": WinMLZeroShotImageClassificationEvaluator,
4749
}
4850

@@ -127,6 +129,16 @@
127129
streaming=True,
128130
columns_mapping={"input_column": "text"},
129131
),
132+
"zero-shot-classification": DatasetConfig(
133+
path="fancyzhx/ag_news",
134+
split="test",
135+
samples=100,
136+
shuffle=True,
137+
columns_mapping={
138+
"input_column": "text",
139+
"label_column": "label",
140+
},
141+
),
130142
"zero-shot-image-classification": DatasetConfig(
131143
path="uoft-cs/cifar100",
132144
split="test",

src/winml/modelkit/eval/metrics/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
"""Evaluation metrics."""
77

8+
from .classification import ClassificationMetric
89
from .knn_accuracy import KNNAccuracyMetric
910
from .mean_average_precision import MAPMetric
1011
from .mean_iou import IGNORE_INDEX, MeanIoUMetric
@@ -15,6 +16,7 @@
1516

1617
__all__ = [
1718
"IGNORE_INDEX",
19+
"ClassificationMetric",
1820
"KNNAccuracyMetric",
1921
"MAPMetric",
2022
"MeanIoUMetric",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
# --------------------------------------------------------------------------
5+
6+
"""Classification metrics.
7+
8+
Accuracy and macro-F1 over string labels, for classification evaluators
9+
that do not have an HF evaluate wrapper (e.g. zero-shot-classification).
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from typing import Any
15+
16+
17+
class ClassificationMetric:
18+
"""Accuracy and macro-F1 over string labels."""
19+
20+
def compute(
21+
self,
22+
predictions: list[str],
23+
references: list[str],
24+
labels: list[str],
25+
) -> dict[str, Any]:
26+
"""Compute accuracy and macro-F1.
27+
28+
Args:
29+
predictions: Predicted label strings, one per sample.
30+
references: Ground-truth label strings, one per sample.
31+
labels: Full set of class labels for macro-F1 averaging.
32+
33+
Returns:
34+
Dict with ``accuracy`` and ``f1`` (both floats in [0, 1]).
35+
"""
36+
from sklearn.metrics import accuracy_score, f1_score
37+
38+
if len(predictions) != len(references):
39+
raise ValueError(
40+
f"predictions and references must have the same length, "
41+
f"got {len(predictions)} vs {len(references)}.",
42+
)
43+
if not references:
44+
raise ValueError("references must not be empty.")
45+
if not labels:
46+
raise ValueError("labels must not be empty.")
47+
48+
accuracy = accuracy_score(references, predictions)
49+
macro_f1 = f1_score(
50+
references,
51+
predictions,
52+
labels=labels,
53+
average="macro",
54+
zero_division=0,
55+
)
56+
return {"accuracy": float(accuracy), "f1": float(macro_f1)}

0 commit comments

Comments
 (0)