# 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)