Improvements! And another figure.

This commit is contained in:
2026-07-03 22:15:05 +03:00
parent a078f44838
commit 96d74ef901
4 changed files with 143 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+125
View File
@@ -0,0 +1,125 @@
"""
Geometric picture of the ROLL TPR@FPR objective.
Upper panel: score PDFs for both classes; shaded area under the positive-class
PDF to the left of τ equals the ROLL loss F̂₁(τ).
Lower panel: CDFs showing how τ is read from F̂₀ and how the loss and TPR
are read from F̂₁.
Run from the thesis root:
nix develop --command python3 content/method/figures/roll_principle.py
"""
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from scipy.stats import norm
OUT_DIR = "content/method/figures"
np.random.seed(3)
# --- Parameters ------------------------------------------------------------
mu0, sig0 = 0.0, 1.00 # negative class N(0, 1)
mu1, sig1 = 1.5, 1.00 # positive class N(1.5, 1) — intentional overlap
alpha = 0.20 # target FPR
# τ = (1-α)-quantile of the negative class → F̂₀(τ) = 1-α → FPR = α
tau = norm.ppf(1 - alpha, mu0, sig0)
F0_tau = norm.cdf(tau, mu0, sig0) # = 1 - alpha
F1_tau = norm.cdf(tau, mu1, sig1) # ROLL loss
tpr = 1.0 - F1_tau
# --- Sample points for positive class (rug) --------------------------------
rug = np.random.normal(mu1, sig1, 35)
# --- Score axis ------------------------------------------------------------
x = np.linspace(-3.6, 5.0, 900)
# --- Palette ---------------------------------------------------------------
C_NEG = "#4477AA"
C_POS = "#CC6633"
C_TAU = "#333333"
C_LOSS = "#7799BB"
# ── Figure ─────────────────────────────────────────────────────────────────
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(5.5, 4.8), sharex=True,
gridspec_kw={"height_ratios": [1.25, 1.0]},
)
fig.subplots_adjust(hspace=0.07)
# ══ Top: PDF ══════════════════════════════════════════════════════════════
pdf0 = norm.pdf(x, mu0, sig0)
pdf1 = norm.pdf(x, mu1, sig1)
ax1.plot(x, pdf0, color=C_NEG, linewidth=1.8, label=r"Negative class ($y=0$)")
ax1.plot(x, pdf1, color=C_POS, linewidth=1.8, label=r"Positive class ($y=1$)")
ax1.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.3,
label=r"Threshold $\tau$")
# Shade: area under positive PDF to the left of τ = loss = F̂₁(τ)
x_left = x[x <= tau]
ax1.fill_between(x_left, norm.pdf(x_left, mu1, sig1),
color=C_LOSS, alpha=0.30, label=r"Loss $= \hat{F}_1(\tau)$")
# Rug plot
rug_y = np.full_like(rug, -0.006)
ax1.scatter(rug, rug_y, color=C_POS, s=18, marker="|",
zorder=3, alpha=0.65, clip_on=False)
ax1.set_ylabel("PDF", fontsize=9)
ax1.set_yticks([])
ax1.set_ylim(bottom=-0.025)
ax1.legend(fontsize=7.5, loc="upper right", framealpha=0.88)
# ══ Bottom: CDF ═══════════════════════════════════════════════════════════
cdf0 = norm.cdf(x, mu0, sig0)
cdf1 = norm.cdf(x, mu1, sig1)
ax2.plot(x, cdf0, color=C_NEG, linewidth=1.8, label=r"$\hat{F}_0$ (neg.)")
ax2.plot(x, cdf1, color=C_POS, linewidth=1.8, label=r"$\hat{F}_1$ (pos.)")
ax2.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.3)
# Horizontal reference lines
ax2.axhline(F0_tau, color=C_NEG, linestyle="-.", linewidth=0.9, alpha=0.7)
ax2.axhline(F1_tau, color=C_POS, linestyle="-.", linewidth=0.9, alpha=0.7)
# Dots at the intercepts
ax2.plot(tau, F0_tau, "o", color=C_NEG, markersize=5, zorder=4)
ax2.plot(tau, F1_tau, "o", color=C_POS, markersize=5, zorder=4)
# Left-side y-axis labels for the intercepts
x_min = x[0]
ax2.text(x_min - 0.15, F0_tau, rf"$1-\alpha$",
fontsize=8, color=C_NEG, va="center", ha="right")
ax2.text(x_min - 0.15, F1_tau, r"$\hat{F}_1(\tau)$" + "\n(loss)",
fontsize=7.5, color=C_POS, va="center", ha="right")
# TPR annotation: double-headed arrow on the right + label
x_bracket = tau + 1.9
ax2.annotate("", xy=(x_bracket, 1.0), xytext=(x_bracket, F1_tau),
arrowprops=dict(arrowstyle="<->", color="black",
lw=0.8, mutation_scale=10))
ax2.text(x_bracket + 0.12, (1.0 + F1_tau) / 2,
f"TPR\n= {tpr*100:.0f}%",
fontsize=7.5, va="center", ha="left", color="black")
ax2.set_ylabel("CDF", fontsize=9)
ax2.set_xlabel(r"Score $f_\theta(\mathbf{x})$", fontsize=9)
ax2.set_yticks([0.0, 0.25, 0.50, 0.75, 1.0])
ax2.set_yticklabels(["0", "0.25", "0.5", "0.75", "1"], fontsize=7.5)
ax2.set_xlim(x[0] - 0.3, x[-1])
ax2.legend(fontsize=7.5, loc="upper left", framealpha=0.88)
for ax in (ax1, ax2):
ax.tick_params(labelsize=8)
ax.spines[["top", "right"]].set_visible(False)
fig.savefig(f"{OUT_DIR}/roll_principle.pdf", bbox_inches="tight")
fig.savefig(f"{OUT_DIR}/roll_principle.png", bbox_inches="tight", dpi=180)
print(f"Saved to {OUT_DIR}/roll_principle.{{pdf,png}}")
print(f"tau={tau:.3f} F0(tau)={F0_tau:.3f} F1(tau)={F1_tau:.3f} TPR={tpr:.3f}")
+17
View File
@@ -251,6 +251,23 @@ of the two populations, turning a TPR@FPR objective into a FPR@TPR one. Because
two objectives are therefore interchangeable at the implementation level, all derivations two objectives are therefore interchangeable at the implementation level, all derivations
and implementation details below are given for \eqref{eq:roll-tpr-at-fpr} only; and implementation details below are given for \eqref{eq:roll-tpr-at-fpr} only;
\eqref{eq:roll-fpr-at-tpr} follows by the same transformation applied to the inputs. \eqref{eq:roll-fpr-at-tpr} follows by the same transformation applied to the inputs.
\Cref{fig:roll-principle} provides a geometric view of \eqref{eq:roll-tpr-at-fpr}.
\begin{figure}[H]
\centering
\includegraphics[width=0.85\textwidth]{content/method/figures/roll_principle.png}
\caption{Geometric picture of the ROLL TPR@FPR objective. \textit{Upper}: score PDFs
for the negative (blue, $y=0$) and positive (orange, $y=1$) classes. The operating
threshold $\tau$ (dashed) is placed at the $(1-\alpha)$-quantile of the negative
class, so that exactly $\alpha$ fraction of negative scores exceed it ($\operatorname{FPR}=\alpha$).
The shaded area under the positive-class PDF to the left of $\tau$ is
$\hat{F}_1(\tau)$ --- the ROLL loss we minimize. Tick marks along the bottom represent
observed positive-class scores. \textit{Lower}: corresponding CDFs $\hat{F}_0$ (blue)
and $\hat{F}_1$ (orange). Reading horizontally at height $1-\alpha$ on $\hat{F}_0$
gives $\tau$; reading $\hat{F}_1(\tau)$ off $\hat{F}_1$ at the same $\tau$ gives the
loss; the distance from $\hat{F}_1(\tau)$ to $1$ is the resulting TPR.}
\label{fig:roll-principle}
\end{figure}
We now derive the gradient of \eqref{eq:roll-tpr-at-fpr} with respect to $f_\theta(\mathbf{x}_i)$. We now derive the gradient of \eqref{eq:roll-tpr-at-fpr} with respect to $f_\theta(\mathbf{x}_i)$.
+1 -1
View File
@@ -62,7 +62,7 @@
(pkgs.texlive.combine texPkgs) (pkgs.texlive.combine texPkgs)
pkgs.culmus pkgs.culmus
pkgs.fontconfig pkgs.fontconfig
(pkgs.python3.withPackages (ps: [ ps.sympy ps.numpy ps.matplotlib ])) (pkgs.python3.withPackages (ps: [ ps.sympy ps.numpy ps.matplotlib ps.scipy ]))
]; ];
shellHook = '' shellHook = ''
export FONTCONFIG_FILE="${fontsConf}" export FONTCONFIG_FILE="${fontsConf}"