given: classes: list
November 30, 2025 ยท View on GitHub
def hierarchical_classification( pred: torch.Tensor, cl: torch.Tensor, labels_hierarchy: Optional[torch.Tensor] = None, ) -> torch.Tensor: """ Computes the classification loss for a given batch of predictions and ground truth labels.
Args:
pred (Tensor): The predicted logits for the batch. Shape: (batch_size, n_labels)
cl (Tensor): The ground truth labels for the batch. Shape: (batch_size,)
labels_hierarchy (Tensor, optional): The hierarchical structure of the labels. Defaults to None.
A binary tensor of shape (number of parents, n_labels)
if not given, will act as a regular classification loss
Raises:
ValueError: If the labels_hierarchy is not found while the number of predicted
labels is smaller than the number of ground truth labels.
Returns:
Tensor: The computed binary cross entropy loss for the given batch.
"""
maxsize = pred.shape[1]
newcl = torch.zeros(
(pred.shape[0], maxsize), device=cl.device
) # batchsize * n_labels
# if we don't know the label we set the weight to 0 else to 1
valid_indices = (cl != -1) & (cl < maxsize)
valid_cl = cl[valid_indices]
newcl[valid_indices, valid_cl] = 1
weight = torch.ones_like(newcl, device=cl.device)
# if we don't know the label we set the weight to 0 for all labels
weight[cl == -1, :] = 0
# if we have non leaf values, we don't know so we don't compute grad and set weight to 0
# and add labels that won't be counted but so that we can still use them
if labels_hierarchy is not None and (cl >= maxsize).any():
is_parent = cl >= maxsize
subset_parent_weight = weight[is_parent]
# we set the weight of the leaf elements for pred where we don't know the leaf, to 0
# i.e. the elements where we will compute the max
# in cl, parents are values past the maxsize
# (if there is 10 leafs labels, the label 10,14, or 15 is a parent at position
# row 0, 4, or 5 in the hierarchy matrix
subset_parent_weight[labels_hierarchy[cl[is_parent] - maxsize]] = 0
weight[is_parent] = subset_parent_weight
# we set their lead to 1 (since the weight will be zero, not really usefull..)
subset_parent_newcl = newcl[is_parent]
subset_parent_newcl[labels_hierarchy[cl[is_parent] - maxsize]] = 1
newcl[is_parent] = subset_parent_newcl
# all parental nodes that have a 1 in the labels_hierarchy matrix are set to 1
# for each parent label / row in labels_hierarchy matrix, the addnewcl is
# the max of the newcl values where the parent label is 1
newcl_expanded = newcl.unsqueeze(-1).expand(-1, -1, labels_hierarchy.shape[0])
addnewcl = torch.max(newcl_expanded * labels_hierarchy.T, dim=1)[0]
# for their weight, it is decreasing based on number of children they have
# it is the same here as for parental labels, we don't want to compute
# gradients when they are 0 meaning not parents of the true leaf label.
# for now we weight related to how many labels they contain.
addweight = addnewcl.clone() / (labels_hierarchy.sum(1) ** 0.5)
# except if it is the cl label we know about?
subset_parent_weight = addweight[is_parent]
subset_parent_weight[:, cl[is_parent] - maxsize] = 1
addweight[is_parent] = subset_parent_weight
# we apply the same mask to the pred but now we want to compute
# logsumexp instead of max since we want to keep the gradients
# we also set to -inf since it is a more neutral element for logsumexp
pred_expanded = (
pred.clone().unsqueeze(-1).expand(-1, -1, labels_hierarchy.shape[0])
)
pred_expanded = pred_expanded * labels_hierarchy.T
pred_expanded[pred_expanded == 0] = torch.finfo(pred.dtype).min
addpred = torch.logsumexp(pred_expanded, dim=1)
# we add the new labels to the cl
newcl = torch.cat([newcl, addnewcl], dim=1)
weight = torch.cat([weight, addweight], dim=1)
pred = torch.cat([pred, addpred], dim=1)
elif labels_hierarchy is None and (cl >= maxsize).any():
raise ValueError("need to use labels_hierarchy for this usecase")
myloss = torch.nn.functional.binary_cross_entropy_with_logits(
pred, target=newcl, weight=weight
)
return myloss
EXAMPLE
given: classes: list
output: dict of output tensor
#. clss: tensor of gt classes #. labels_hierarchy: dict of dict representing hierarchies as {parent_loc: child_loc} #. where max(child_loc) == output["cls_output_" + clsname].shape[1] for child_loc of that clsname #. where len(set(parent_loc) & set(child_loc))==0 #. where max(child_loc) > min(parent_loc)
....
loss_cls = 0
mat_labels_hierarchy = {} for k, v in labels_hierarchy.items(): tens = torch.zeros((len(v), classes[k])) for k2, v2 in v.items(): tens[k2 - classes[k], v2] = 1 mat_labels_hierarchy[k] = tens.to(bool)
for j, clsname in enumerate(classes): if "cls_output_" + clsname not in output: continue # setting the classes from index to one hot loss_cls += loss.hierarchical_classification( pred=output["cls_output_" + clsname], cl=clss[:, j], labels_hierarchy=( mat_labels_hierarchy[clsname] if clsname in mat_labels_hierarchy.keys() else None ), )