Skip to content

Commit 6591c07

Browse files
authored
refactor: decouple ClassificationModel from OpenVINO (#458)
* Remove legacy OVMS support * Simplify labels selection in get_multiclass_predictions * Skip graph modification for models without embedded softmax * Skip not needed model read in tests * Calculate TopK if output is already softmaxed * Don't softmax raw_scores if already done * Rename variables to match python standards * Remove misleading variable names * Remove handholding from internal function
1 parent ce799cd commit 6591c07

4 files changed

Lines changed: 45 additions & 91 deletions

File tree

src/model_api/models/classification.py

Lines changed: 16 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@
1212
from typing import TYPE_CHECKING
1313

1414
import numpy as np
15-
from openvino import Model, Type
16-
from openvino import opset10 as opset
17-
from openvino.preprocess import PrePostProcessor
1815

1916
from model_api.models.image_model import ImageModel
2017
from model_api.models.parameters import ParameterRegistry
2118
from model_api.models.result import ClassificationResult, Label
22-
from model_api.models.utils import softmax
19+
from model_api.models.utils import is_softmaxed, softmax, top_k
2320

2421
if TYPE_CHECKING:
2522
from model_api.adapters.inference_adapter import InferenceAdapter
@@ -95,27 +92,8 @@ def _setup_multilabel(self) -> None:
9592

9693
def _setup_single_label(self) -> None:
9794
"""Configure model for single-label classification with TopK."""
98-
try:
99-
addOrFindSoftmaxAndTopkOutputs(
100-
self.inference_adapter,
101-
self.params.topk,
102-
self.params.output_raw_scores,
103-
)
104-
self.embedded_topk = True
105-
self.out_layer_names = ["indices", "scores"]
106-
if self.params.output_raw_scores:
107-
self.out_layer_names.append(self.raw_scores_name)
108-
except (RuntimeError, AttributeError):
109-
# exception means we have a non-ov model
110-
# with already inserted softmax and topk
111-
if self.params.embedded_processing and len(self.outputs) >= 2:
112-
self.embedded_topk = True
113-
self.out_layer_names = ["indices", "scores"]
114-
self.raw_scores_name = _raw_scores_name
115-
else: # likely a non-ov model
116-
self.embedded_topk = False
117-
self.out_layer_names = _get_non_xai_names(self.outputs.keys())
118-
self.raw_scores_name = self.out_layer_names[0]
95+
self.out_layer_names = _get_non_xai_names(self.outputs.keys())
96+
self.raw_scores_name = self.out_layer_names[0]
11997

12098
self.embedded_processing = True
12199

@@ -228,10 +206,9 @@ def get_all_probs(self, logits: np.ndarray) -> np.ndarray:
228206
if cls_heads_info["num_multilabel_classes"]:
229207
logits_begin = cls_heads_info["num_single_label_classes"]
230208
probs[logits_begin:] = sigmoid_numpy(logits[logits_begin:])
231-
elif self.embedded_topk:
232-
probs = logits.reshape(-1)
233209
else:
234-
probs = softmax(logits.reshape(-1))
210+
logits_flattened = logits.reshape(-1)
211+
probs = logits_flattened if is_softmaxed(logits_flattened, axis=0) else softmax(logits_flattened)
235212
return probs
236213

237214
def get_hierarchical_predictions(self, logits: np.ndarray) -> list[Label]:
@@ -277,68 +254,18 @@ def get_multilabel_predictions(self, logits: np.ndarray) -> list[Label]:
277254
return list(starmap(Label, zip(indices, labels, scores)))
278255

279256
def get_multiclass_predictions(self, outputs: dict) -> list[Label]:
257+
axis = 1
258+
logits = outputs[self.out_layer_names[0]]
259+
if not is_softmaxed(logits, axis=axis):
260+
logits = softmax(logits, axis=axis)
261+
top_k_result = top_k(logits, self.params.topk, axis=axis)
262+
scores = top_k_result.values[0] # noqa: PD011 # silencing false positive - it's not pandas code
263+
indices = top_k_result.indices[0]
264+
280265
labels_list = self.params.labels
281-
if self.embedded_topk:
282-
indicesTensor = outputs[self.out_layer_names[0]][0]
283-
scoresTensor = outputs[self.out_layer_names[1]][0]
284-
labels = [labels_list[i] if labels_list else "" for i in indicesTensor]
285-
else:
286-
scoresTensor = softmax(outputs[self.out_layer_names[0]][0])
287-
indicesTensor = [int(np.argmax(scoresTensor))]
288-
labels = [labels_list[i] if labels_list else "" for i in indicesTensor]
289-
return list(starmap(Label, zip(indicesTensor, labels, scoresTensor)))
290-
291-
292-
def addOrFindSoftmaxAndTopkOutputs(inference_adapter: InferenceAdapter, topk: int, output_raw_scores: bool) -> None:
293-
softmaxNode = None
294-
for i in range(len(inference_adapter.model.outputs)):
295-
output_node = inference_adapter.model.get_output_op(i).input(0).get_source_output().get_node()
296-
if output_node.get_type_name() == "Softmax":
297-
softmaxNode = output_node
298-
elif output_node.get_type_name() == "TopK":
299-
return
300-
301-
if softmaxNode is None:
302-
logitsNode = inference_adapter.model.get_output_op(0).input(0).get_source_output().get_node()
303-
softmaxNode = opset.softmax(logitsNode.output(0), 1)
304-
k = opset.constant(topk, np.int32)
305-
topkNode = opset.topk(softmaxNode, k, 1, "max", "value")
306-
307-
indices = topkNode.output(0)
308-
scores = topkNode.output(1)
309-
results_descr = [indices, scores]
310-
if output_raw_scores:
311-
raw_scores = softmaxNode.output(0)
312-
results_descr.append(raw_scores)
313-
for output in inference_adapter.model.outputs:
314-
if _saliency_map_name in output.get_names() or _feature_vector_name in output.get_names():
315-
results_descr.append(output)
316-
317-
source_rt_info = inference_adapter.get_model().get_rt_info()
318-
inference_adapter.model = Model(
319-
results_descr,
320-
inference_adapter.model.get_parameters(),
321-
"classification",
322-
)
323-
324-
if "model_info" in source_rt_info:
325-
source_rt_info = source_rt_info["model_info"]
326-
for k in source_rt_info:
327-
inference_adapter.model.set_rt_info(source_rt_info[k], ["model_info", k])
328-
329-
# manually set output tensors name for created topK node
330-
inference_adapter.model.outputs[0].tensor.set_names({"scores"})
331-
inference_adapter.model.outputs[1].tensor.set_names({"indices"})
332-
if output_raw_scores:
333-
inference_adapter.model.outputs[2].tensor.set_names({_raw_scores_name})
334-
335-
# set output precisions
336-
ppp = PrePostProcessor(inference_adapter.model)
337-
ppp.output("indices").tensor().set_element_type(Type.i32)
338-
ppp.output("scores").tensor().set_element_type(Type.f32)
339-
if output_raw_scores:
340-
ppp.output(_raw_scores_name).tensor().set_element_type(Type.f32)
341-
inference_adapter.model = ppp.build()
266+
labels = [labels_list[i] if labels_list else "" for i in indices]
267+
268+
return list(starmap(Label, zip(indices, labels, scores)))
342269

343270

344271
def sigmoid_numpy(x: np.ndarray) -> np.ndarray:

src/model_api/models/utils.py

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

66
from __future__ import annotations
77

8+
from collections import namedtuple
89
from dataclasses import dataclass
910
from pathlib import Path
1011
from typing import TYPE_CHECKING
@@ -14,6 +15,9 @@
1415

1516
from model_api.models.result import Contour, InstanceSegmentationResult, RotatedSegmentationResult
1617

18+
topk_namedtuple = namedtuple("topk_namedtuple", ["values", "indices"])
19+
20+
1721
if TYPE_CHECKING:
1822
from model_api.models.result.detection import DetectionResult
1923

@@ -284,6 +288,29 @@ def multiclass_nms(
284288
return det, keep
285289

286290

291+
def is_softmaxed(array: np.ndarray, axis: int, atol: float = 1e-5) -> bool:
292+
"""Check if the input array is softmaxed along the specified axis."""
293+
# Check values are in [0, 1]
294+
if not np.all((array >= 0) & (array <= 1)):
295+
return False
296+
# Check sum along axis is close to 1
297+
sums = np.sum(array, axis=axis)
298+
return np.allclose(sums, 1.0, atol=atol)
299+
300+
287301
def softmax(logits: np.ndarray, eps: float = 1e-9, axis=None, keepdims: bool = False) -> np.ndarray:
288302
exp = np.exp(logits - np.max(logits))
289303
return exp / (np.sum(exp, axis=axis, keepdims=keepdims) + eps)
304+
305+
306+
def top_k(array: np.ndarray, k: int, axis: int) -> topk_namedtuple:
307+
"""Returns the top k values and their indices along the specified axis."""
308+
# Get indices of the top k elements
309+
indices = np.take(np.argpartition(array, -k, axis=axis), range(-k, 0), axis=axis)
310+
# Gather the top k values
311+
topk_values = np.take_along_axis(array, indices, axis=axis)
312+
# Sort the top k values and indices in descending order
313+
sorted_order = np.argsort(-topk_values, axis=axis)
314+
topk_values = np.take_along_axis(topk_values, sorted_order, axis=axis)
315+
indices = np.take_along_axis(indices, sorted_order, axis=axis)
316+
return topk_namedtuple(values=topk_values, indices=indices)

tests/accuracy/public_scope.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@
346346
{
347347
"id": 105,
348348
"name": "194",
349-
"confidence": 0.06216677650809288
349+
"confidence": 0.4564049541950226
350350
}
351351
],
352352
"raw_scores": [

tests/accuracy/test_accuracy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def create_models(model_type, model_path, download_dir, force_onnx_adapter=False
9999
model = create_core().read_model(model_path)
100100
if model.has_rt_info(["model_info", "model_type"]):
101101
wrapper_type = model_type.get_model_class(
102-
create_core().read_model(model_path).get_rt_info(["model_info", "model_type"]).astype(str),
102+
model.get_rt_info(["model_info", "model_type"]).astype(str),
103103
)
104104
model = wrapper_type(OpenvinoAdapter(create_core(), model_path, device=device))
105105
model.load()

0 commit comments

Comments
 (0)