Skip to content

[Bug] Incorrect Parenthesis Location in Cross Term of AUCMLoss(version='v2') #74

Description

@weicaocw

Environment: libauc 2.0.1, torch 2.2.2

Location: libauc/losses/auc.py $\rightarrow$ AUCMLoss.forward branch for version == 'v2'

Issue:
The cross term is currently written as:

self.mean((y_pred*neg_mask) - self.mean(y_pred*pos_mask))

The inner self.mean(y_pred*pos_mask) computes a scalar $\mu_+$, which is broadcasted and subtracted across the entire vector of length $N$. This causes the positive sample positions—originally zeroed out by neg_mask—to become $-\mu_+$ (non-zero). Consequently, the outer self.mean's count_nonzero includes these positions in its denominator, making the denominator $N$ instead of $N_-$:

$$\text{cross}_{\text{actual}} = \frac{1}{N} \sum_{i \in \text{neg}} s_i - \mu_+ = (1-p) \cdot \mu_- - \mu_+$$

$$\text{cross}_{\text{expected}} = \mu_- - \mu_+$$

This means the negative class mean is unexpectedly scaled by $(1-p)$. It should be fixed by adjusting the parenthesis:

self.mean(y_pred*neg_mask) - self.mean(y_pred*pos_mask)  # Fix parenthesis location

Minimal Reproducible Example:

import torch
from libauc.losses.auc import AUCMLoss

s = torch.tensor([0.1, 0.4, 0.35, 0.8]).view(-1, 1)
y = torch.tensor([0., 0., 1., 1.]).view(-1, 1)

f = AUCMLoss(margin=1.0, version='v2')
with torch.no_grad():
    f.a.fill_(0.3); f.b.fill_(0.6); f.alpha.fill_(0.5)
out = f(s, y); out.backward()
print(float(out), float(f.alpha.grad))    # 0.57125002  0.10000002

# Manual calculation based on class-wise means:
mu_pos, mu_neg = (0.35+0.8)/2, (0.1+0.4)/2           # 0.575, 0.25
expect = ((0.35-0.3)**2 + (0.8-0.3)**2)/2 \
        + ((0.1-0.6)**2 + (0.4-0.6)**2)/2 \
        + 2*0.5*(1.0 + mu_neg - mu_pos) - 0.5**2
print(expect)                               # 0.69625000

Cross-Validation (Key Evidence):
The version='v1' implementation in your library is correct. Since v1 and the class-wise mean formulation differ by a constant scaling factor $p(1-p)$, the identity $\text{v2} = \text{v1} / (p(1-p))$ should strictly hold. Using the same data:

f1 = AUCMLoss(margin=1.0, version='v1')
with torch.no_grad():
    f1.a.fill_(0.3); f1.b.fill_(0.6); f1.alpha.fill_(0.5)
print(float(f1(s, y)) / (0.5*0.5))        # 0.69625002  ← Matches manual calculation, contradicts v2

This identity holds for the current v2 implementation when $\alpha=0$ (cross term inactive) but breaks when $\alpha \neq 0$, further isolating the bug to the cross term.

Impact:

  1. Loss value deviation: Closed-form deviation is $-2 \cdot \alpha \cdot p \cdot \mu_-$;
  2. Underestimated dual variable pressure: $\partial L / \partial \alpha$ is significantly smaller than expected (e.g., $0.10$ vs. expected $0.35$ in the example above), underestimating the margin constraint penalty;
  3. Asymmetric gradients: The gradient on the network output for negative samples is scaled down by $(1-p)$, introducing asymmetry against positive samples. Under recommended training setups like DualSampler(sampling_rate=0.5), negative sample gradients are reduced to only half of their intended magnitude.

Recommendation:
Fix the parenthesis placement. If backward compatibility is a concern, consider providing a feature toggle or documenting numerical changes in the release notes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions