niobures's picture
Pyannote (models, models_onnx)
8c838e7 verified
# MIT License
#
# Copyright (c) 2023- CNRS
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# AUTHORS
# Hervé BREDIN - https://herve.niderb.fr
# Alexis PLAQUET
from functools import cached_property
from itertools import combinations
import scipy.special
import numpy as np
class Powerset():
"""Powerset to multilabel conversion, and back.
Parameters
----------
num_classes : int
Number of regular classes.
max_set_size : int
Maximum number of classes in each set.
"""
def __init__(self, num_classes: int, max_set_size: int):
super().__init__()
self.num_classes = num_classes
self.max_set_size = max_set_size
self.mapping = self.build_mapping()
self.cardinality = self.build_cardinality()
@cached_property
def num_powerset_classes(self) -> int:
# compute number of subsets of size at most "max_set_size"
# e.g. with num_classes = 3 and max_set_size = 2:
# {}, {0}, {1}, {2}, {0, 1}, {0, 2}, {1, 2}
return int(
sum(
scipy.special.binom(self.num_classes, i)
for i in range(0, self.max_set_size + 1)
)
)
def build_mapping(self) -> np.ndarray:
"""Compute powerset to regular mapping
Returns
-------
mapping : (num_powerset_classes, num_classes) torch.Tensor
mapping[i, j] == 1 if jth regular class is a member of ith powerset class
mapping[i, j] == 0 otherwise
Example
-------
With num_classes == 3 and max_set_size == 2, returns
[0, 0, 0] # none
[1, 0, 0] # class #1
[0, 1, 0] # class #2
[0, 0, 1] # class #3
[1, 1, 0] # classes #1 and #2
[1, 0, 1] # classes #1 and #3
[0, 1, 1] # classes #2 and #3
"""
mapping = np.zeros((self.num_powerset_classes, self.num_classes))
powerset_k = 0
for set_size in range(0, self.max_set_size + 1):
for current_set in combinations(range(self.num_classes), set_size):
mapping[powerset_k, current_set] = 1
powerset_k += 1
return mapping
def build_cardinality(self) -> np.ndarray:
"""Compute size of each powerset class"""
return np.sum(self.mapping, axis=1)
def to_multilabel(self, powerset: np.ndarray, soft: bool = False) -> np.ndarray:
"""Convert predictions from powerset to multi-label
Parameter
---------
powerset : (batch_size, num_frames, num_powerset_classes) torch.Tensor
Soft predictions in "powerset" space.
soft : bool, optional
Return soft multi-label predictions. Defaults to False (i.e. hard predictions)
Assumes that `powerset` are "logits" (not "probabilities").
Returns
-------
multi_label : (batch_size, num_frames, num_classes) torch.Tensor
Predictions in "multi-label" space.
"""
powerset_probs = np.identity(self.num_powerset_classes)[np.argmax(powerset, axis=-1)]
return np.matmul(powerset_probs, self.mapping)
def __call__(self, powerset: np.ndarray, soft: bool = False) -> np.ndarray:
"""Alias for `to_multilabel`"""
return self.to_multilabel(powerset, soft=soft)