diff --git a/content/method/figures/roll_principle.png b/content/method/figures/roll_principle.png new file mode 100644 index 0000000..630ea65 Binary files /dev/null and b/content/method/figures/roll_principle.png differ diff --git a/content/method/figures/roll_principle.py b/content/method/figures/roll_principle.py new file mode 100644 index 0000000..4bd0efe --- /dev/null +++ b/content/method/figures/roll_principle.py @@ -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}") diff --git a/content/method/method.tex b/content/method/method.tex index 673c343..42a51cc 100644 --- a/content/method/method.tex +++ b/content/method/method.tex @@ -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 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. +\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)$. diff --git a/flake.nix b/flake.nix index b1a8122..d1c3140 100644 --- a/flake.nix +++ b/flake.nix @@ -62,7 +62,7 @@ (pkgs.texlive.combine texPkgs) pkgs.culmus 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 = '' export FONTCONFIG_FILE="${fontsConf}"