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