Files
roam/implloss-functions.org
T
2026-10-05 09:48:25 +03:00

194 lines
13 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:PROPERTIES:
:ID: 001430d5-e1e7-4e72-baf6-17399bfd6447
:END:
#+title: impl/loss-functions
#+filetags: :project: :knowledge: :loss-functions:
* impl/loss-functions
** KernelizedROLLoss — Key Design Decisions
- =_icdf= uses **bisection** (not Newton-Raphson). NR was replaced because =F_prime= goes near-zero when =v= is large and =tau= drifts, causing divergence. Bisection bracket is =[min(scores)−10/v, max(scores)+10/v]=, guaranteed to converge.
- Score normalisation (=normalize= flag): divides =yh= by its std before KDE. Can be disabled via =kernelized_roll_fpr(fpr, normalize=False)=. The chain-rule correction (multiply grad by =score_scale=) is applied in backward.
- =_grad_tau_scores_neg= denominator clamped at =1e-10= to prevent 0/0 when all sigmoid kernels saturate.
- Variance clamped at =1e-8= in normalisation to prevent =inf= when all batch scores are identical.
** KernelizedROLLoss Unification (2026-07-14)
=KernelizedROLLoss= and =KernelizedROLLossAOC= merged into a single class accepting =alphas= (list of CDF targets) and =divisor=:
- Point-FPR: =KernelizedROLLoss.apply([1−fpr], 1, yh, y, gamma, normalize)=
- AOC: =KernelizedROLLoss.apply(_AOC_ALPHAS, _AOC_DIVISOR, yh, y, gamma, normalize)=
Backward always vectorised (K-dim); K=1 reduces to the scalar case. =ctx.divisor= stores the non-tensor divisor.
** BCE Combination
Both =kernelized_roll_fpr= and =kernelized_roll_tpr= accept =bce_weight= (default 0.0) and =bce_pos_weight= (default 1.0).
Loss = ROLL + bce_weight × BCE. Standard value: =bce_weight=0.5=, =bce_pos_weight=num_false/num_true=.
** ISJ Bandwidth Failure
ISJ fails when a class has too few samples in a split (~<20). Affects: cleveland, ecoli, glass4, glass5.
** Gamma Scheduling
=KernelScheduler= widens KDE kernels early then anneals. Default: =initial_gamma=100=, =decay=0.5=, =decay_every=500=. Gamma is a *divisor* of =v=.
** LibAUC Integration (2026-07-14, tuned 2026-07-14)
=libauc_auc_loss= in =src/roll.py= wraps =AUCMLoss= (Yuan et al., ICCV 2021) to the =(yh, y)= convention.
- Applies =torch.sigmoid(yh)= before passing to AUCMLoss (squared-hinge surrogate, no built-in sigmoid).
- AUCMLoss has learnable params =a, b, alpha= — must use PESG optimizer (not Adam).
- =opt_factory= signature: =opt_factory(model)= — does NOT take criteriorator (would be unbound at call site in =_perform_episode=).
- =opt_factory= pattern: =loss_fn.pesg_opt_factory(...)= returns picklable =_PESGFactory= sharing the same inner =AUCMLoss= instance as the criteriorator's loss func (preserved by pickle memo).
- =ExperimentConfiguration= gained =opt_factory: callable = None=; overrides =optim_class/optim_args= when set.
- =tqdm= is an undeclared dep of libauc — must be in =propagatedBuildInputs= in flake.nix.
*** Recommended settings (v2 + adam, tuned 2026-07-14)
- =version='v2'= — removes class prior from formulation; LibAUC explicitly recommends over v1; default in =libauc_auc_loss=.
- =mode='adam'= in PESG — Adam-style primal updates, converges much faster than SGD on small datasets; default in =pesg_opt_factory=.
- =imratio= passed explicitly as =num_true / (num_true + num_false)= — avoids noisy mini-batch estimates on imbalanced data. Note: v2 ignores imratio (prior-free), so this only matters if reverting to v1.
- =lr=0.1= (PESG default, much larger than Adam's 1e-3). If diverging: try =epoch_decay=0.0= or drop lr to =0.01=.
** Grace Period (BasicCriteriorator / CRBasedCriteriorator, 2026-07-14)
=grace_period: int = 0= parameter added to both criteriorators. During the first =grace_period= epochs:
- =best_flag= is always False (no model snapshot taken)
- Patience counter does not increment (early stopping cannot fire)
All keel configs use =grace_period=50=. Prevents noisy early-epoch models from being selected as best.
** Naming fix: =_kernelized_roll_fpr_internal='s =fpr= param renamed =cdf_target= (2026-09-10)
Mentor-audit finding (screenshots, no direct cite): =kernelized_roll_fpr(fpr, ...)= calls
=partial(_kernelized_roll_fpr_internal, fpr=1 - fpr, ...)= — the INTERNAL function's =fpr=
parameter actually holds =1-fpr= (the CDF target, thesis's =1-α=), not the FPR itself, the
opposite of what its name implies. Logic was always correct (verified independently, see
next entry); only the name was misleading. Fixed by renaming the internal parameter (and
the keyword in the =partial(...)= call) to =cdf_target= — user's preferred name over
=alpha= (avoids clashing with the thesis's own use of α to mean the RAW target rate, not
its complement). The PUBLIC =kernelized_roll_fpr(fpr, ...)= parameter is untouched — it's
correctly named; the complement happens only when forwarding internally. No callers use
=fpr== as a keyword arg anywhere in the codebase, so the rename is fully self-contained.
Mirrored the identical rename in the thesis's own code listing
(=content/appendixs/appendix_roll_code.tex=, which reproduces this function verbatim) so
the two don't drift apart.
** Verified correct (not a bug): =_kernelized_roll_tpr_internal='s =1-yh= and un-complemented =tpr= (2026-09-10)
While investigating the =fpr=/=cdf_target= naming issue above, also audited two
look-alike oddities in the neighboring =_kernelized_roll_tpr_internal= — both checked out
as correct, for non-obvious reasons worth remembering before anyone "fixes" them later:
1. *=1 - yh= instead of =-yh=* for the score transform (thesis's stated general rule says
"negate scores", i.e. =-f_theta(x)=). Directly compared =1-yh= vs =-yh= as inputs:
identical loss and gradients to ~3e-8 (float noise). Reason: =1-yh = -(yh-1)=, i.e.
negation plus a constant shift, and =KernelizedROLLoss= is translation-invariant in its
scores (same property underlying the thesis's gradient-balance proof) — so the shift is
silently absorbed and does nothing. Added a one-line code comment at the call site
(roll.py, right above the =KernelizedROLLoss.apply= call in
=_kernelized_roll_tpr_internal=) explaining this, so it doesn't look like a stray bug to
a future reader — this comment is the only thing that changed; the surrounding logic
was intentionally left as-is.
2. *=kernelized_roll_tpr(tpr)= passes =tpr= straight through with no complement*, while
=kernelized_roll_fpr(fpr)= complements (=1-fpr=). Looks asymmetric, but is required by
the math: reducing FPR@TPR(β) to the TPR@FPR machinery requires complementing the
target rate to =(1-β)= first (per the thesis's own reduction rule), and separately,
evaluating TPR@FPR at any rate =γ= uses CDF-target =1-γ= internally — chain the two
complements: =1-(1-β)=β=. They cancel exactly, so the raw =tpr= value reaching the
internal CDF-target IS =β= correctly, no complement needed. The apparent asymmetry
between the two wrappers is expected, not a bug.
Closed the loop with an independent numerical check (not just internal
self-consistency): hand-computed =1 - F0_hat(F1_hat^{-1}(1-beta))= (thesis's
eq:roll-fpr-at-tpr) completely from scratch in a throwaway script — same normalization,
same KDEpy bandwidth selector, same sigmoid kernel, same bisection, but applied directly
to the ORIGINAL untransformed (yh, y) with no negation/swap trick at all. Result matched
=_kernelized_roll_tpr_internal='s actual output to ~3e-5 (bisection tolerance noise).
Confirms the function exactly reproduces the paper's formula. No code fix needed for
either point beyond the one comment above.
** MAJOR: appendix_roll_code.tex was stale relative to impl/src/roll.py (2026-09-10)
User asked directly: "did we ever update the code [listing] after we changed [the real
code]... is the code in the thesis matching actual code?" Answer at the time: NO. Did a
full AST-based function-by-function diff (Python =ast= module, not just eyeballing) of
every function in appendix_roll_code.tex against impl/src/roll.py. Found:
Serious (pre-fix or actively-buggy code shown as current):
- =_icdf= / =_icdf_batch=: listing showed the OLD signatures entirely (single =v=/=scores=
instead of =v0=/=scores_neg=/=v1=/=scores_pos=, old fixed =+-10/v= bracket margin instead
of the exact =h0*log(alpha/(1-alpha))= bracket, old "stop on CDF residual" criterion
instead of the certified-loss-interval one) -- none of the bisection-bracket /
stopping-criterion audit fixes from earlier this session had been mirrored here.
=forward()='s calls to these were correspondingly stale (3-arg, not 5-arg).
- =backward()=: showed the OLD, numerically-vulnerable =kernel = s_neg*(1-s_neg)= gradient
formula -- the EXACT formula independently found to have a live bug (collapses to an
all-zero gradient when scores saturate) and replaced with the log-space
=_log_sigma_prime=/softmax version earlier this session. The listing never got that fix,
so it directly contradicted appendix_kde_grad.tex (which documents the fix as done) --
a self-contradiction within the thesis between two appendices.
- =_log_sigma_prime= itself was MISSING from the listing entirely (not just stale) --
=backward()= as shown couldn't even have called it.
Minor (comments from this session's smaller fixes not mirrored):
- =_torch_mean_std=: listing still showed =torch.std(yh)= (Bessel-corrected), not the
=correction=0= MLE fix from earlier the same day (see next entry).
- =_kernelized_roll_tpr_internal=: missing the =1-yh= translation-invariance comment.
Also softened the appendix's opening claim from "reproduced verbatim" to "reproduced ...
reformatted for print width ... logic is unchanged" -- it was never literally verbatim
(unicode sigma/tau stripped to ASCII, citation tags like =[eq:sigmoid-kernel]= dropped from
docstrings, lines rewrapped for column width), so "verbatim" was itself a small overclaim.
Fixed all of the above directly in appendix_roll_code.tex: ported the current =_icdf=/
=_icdf_batch=/=forward=/=backward= logic across (reformatted to the listing's existing
ASCII/wrapped style, not raw-pasted), added the missing =_log_sigma_prime= method right
after =_sigma_prime= (matching live file ordering), and added the two missing comments.
VERIFICATION METHOD (worth remembering for next time this comes up): manual reading isn't
reliable for this kind of check -- used Python's own =ast= module to extract every
function's source from both files and diff them programmatically. First pass (raw source
diff) caught line-wrapping noise mixed in with real differences; had to write a stricter
second pass that parses both into ASTs, strips docstrings/comments via =ast.unparse=, and
diffs the RESULT -- that gave a clean "0 functions differ in actual executable logic"
confirmation across all 21 functions in the listing. Recommend this exact
=ast.parse=+=ast.unparse=-diff technique any time "does the thesis's code listing match
the real implementation" needs checking again -- eyeballing a big code appendix against a
live file is exactly how this drift went unnoticed for multiple fix-rounds in the first
place.
Rebuilt thesis clean (nix build exit 0) after the fix, confirmed no new undefined
references introduced.
** Fixed: Gaussian ROLL =_torch_mean_std= used Bessel-corrected std, not MLE (2026-09-10)
Mentor-audit finding: Appendix A (appendix_gauss_grad.tex) states explicitly "The MLE
parameters are: ... σ_k² = (1/|B_k|)Σ(f_θ(x_j)-μ_k)²" — the biased, n-denominator MLE
variance. But =_torch_mean_std= (roll.py, Gaussian ROLL helpers) used =torch.std(yh)=,
whose default (=correction=1=) is Bessel-corrected (n-1 denominator) — NOT the MLE. Real
documentation-vs-implementation mismatch, same family as the AOC-divisor issue, but lower
stakes: verified =_torch_mean_std=/=_torch_normal_fit= are ONLY used by
=_roll_loss_from_fpr_internal= (Gaussian ROLL), which is NOT among the loss configs used
in any reported experiment (only =roll-aoc= and =roll-tpr90=, both KDE-based, are) — so
fixing this required no re-running of any experiment.
Separately checked whether the ANALOGOUS =torch.var(yh, correction=1)= in KDE score
normalization (=score_scale=, roll.py ~line 271) has the same problem: it does NOT.
Thesis's own definition of the normalization constant (method.tex, sec:roll-numerical-
stability) is deliberately generic — =\hat\sigma_\mathcal{B} = sqrt(Var(...))= with no
n vs n-1 commitment anywhere, unlike Appendix A's explicit "MLE" language. Nothing textual
is being contradicted there. Also, per the thesis's own prop:normalization-invariance,
for Silverman's rule (scale-equivariant) the exact value of σ̂_B doesn't affect the loss
or gradient AT ALL; for ISJ (already flagged elsewhere as "a heuristic"), the n vs n-1
choice is only a sqrt(n/(n-1)) deviation (~2.6% at batch 20, <1% by batch 64) -- verified
numerically. Left this one alone entirely, per user's explicit instruction to only fix
the Gaussian ROLL side.
Fix applied: =torch.std(yh)= -> =torch.std(yh, correction=0)= in =_torch_mean_std=, with a
one-line comment noting this matches Appendix A's MLE formula rather than torch.std's
Bessel-corrected default. Verified: output now matches a hand-computed
=sqrt(mean((x-mean(x))^2))= MLE formula exactly (=torch.allclose= true), and the full
=roll_loss_from_fpr= forward+backward still runs cleanly end-to-end after the change.