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:
-
Loss value deviation: Closed-form deviation is $-2 \cdot \alpha \cdot p \cdot \mu_-$;
-
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;
-
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.
Environment:
libauc 2.0.1,torch 2.2.2Location:$\rightarrow$
libauc/losses/auc.pyAUCMLoss.forwardbranch forversion == 'v2'Issue:
The cross term is currently written as:
The inner$\mu_+$ , which is broadcasted and subtracted across the entire vector of length $N$ . This causes the positive sample positions—originally zeroed out by $-\mu_+$ (non-zero). Consequently, the outer $N$ instead of $N_-$ :
self.mean(y_pred*pos_mask)computes a scalarneg_mask—to becomeself.mean'scount_nonzeroincludes these positions in its denominator, making the denominatorThis means the negative class mean is unexpectedly scaled by$(1-p)$ . It should be fixed by adjusting the parenthesis:
Minimal Reproducible Example:
Cross-Validation (Key Evidence):$p(1-p)$ , the identity $\text{v2} = \text{v1} / (p(1-p))$ should strictly hold. Using the same data:
The
version='v1'implementation in your library is correct. Sincev1and the class-wise mean formulation differ by a constant scaling factorThis identity holds for the current$\alpha=0$ (cross term inactive) but breaks when $\alpha \neq 0$ , further isolating the bug to the cross term.
v2implementation whenImpact:
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.