124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
"""
|
|
Illustrates the gradient locality property of Gaussian-ROLL.
|
|
|
|
Upper panel: score distributions for both classes with fitted Gaussian PDFs
|
|
and operating threshold.
|
|
Lower panel: gradient magnitude per sample using the Gaussian gradient formula
|
|
(eq:gauss-grad-combined).
|
|
|
|
Run from the thesis root:
|
|
nix develop --command python3 content/method/figures/threshold_weighting_gaussian.py
|
|
"""
|
|
|
|
import numpy as np
|
|
from scipy import stats
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
|
|
plt.rcParams.update({
|
|
"text.usetex": False,
|
|
"mathtext.fontset": "cm",
|
|
"pdf.fonttype": 42
|
|
})
|
|
|
|
OUT_DIR = "content/method/figures"
|
|
|
|
np.random.seed(7)
|
|
|
|
# --- Data ------------------------------------------------------------------
|
|
n = 120
|
|
alpha = 0.25 # target FPR
|
|
|
|
neg_scores = np.sort(np.random.normal(-0.6, 0.75, n))
|
|
pos_scores = np.sort(np.random.normal(1.1, 0.80, n))
|
|
|
|
n0 = len(neg_scores)
|
|
n1 = len(pos_scores)
|
|
|
|
# --- Gaussian MLE parameters -----------------------------------------------
|
|
mu0, sigma0 = neg_scores.mean(), neg_scores.std()
|
|
mu1, sigma1 = pos_scores.mean(), pos_scores.std()
|
|
|
|
# Threshold: (1-alpha)-quantile of negative class so that FPR = alpha
|
|
# (FPR = P(score > tau | y=0) = 1 - F_0(tau), so F_0(tau) = 1-alpha)
|
|
tau = stats.norm.ppf(1 - alpha, loc=mu0, scale=sigma0)
|
|
|
|
# Shared scalar: pdf of positive class evaluated at the threshold
|
|
common = (1.0 / (sigma1 * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((tau - mu1) / sigma1) ** 2)
|
|
|
|
# --- Gaussian ROLL gradients (eq:gauss-grad-combined) ---------------------
|
|
# y=1: -(1/n1) * common * (1 + (tau-mu1)*(s-mu1)/sigma1^2)
|
|
grad_pos_signed = -(1.0 / n1) * common * (
|
|
1 + (tau - mu1) * (pos_scores - mu1) / sigma1 ** 2
|
|
)
|
|
grad_pos_abs = np.abs(grad_pos_signed)
|
|
|
|
# y=0: +(1/n0) * common * (1 + (tau-mu0)*(s-mu0)/sigma0^2)
|
|
grad_neg_signed = (1.0 / n0) * common * (
|
|
1 + (tau - mu0) * (neg_scores - mu0) / sigma0 ** 2
|
|
)
|
|
grad_neg_abs = np.abs(grad_neg_signed)
|
|
|
|
# Gradient balance check (should be ~0)
|
|
print(f"Gradient balance: pos sum = {grad_pos_signed.sum():.6f}, "
|
|
f"neg sum = {grad_neg_signed.sum():.6f}, "
|
|
f"total = {(grad_pos_signed.sum() + grad_neg_signed.sum()):.2e}")
|
|
|
|
# --- Smooth reference curves (continuous gradient functions) ---------------
|
|
x_ref = np.linspace(neg_scores.min() - 0.3, pos_scores.max() + 0.3, 400)
|
|
smooth_pos_abs = np.abs(-(1.0 / n1) * common * (
|
|
1 + (tau - mu1) * (x_ref - mu1) / sigma1 ** 2
|
|
))
|
|
smooth_neg_abs = np.abs((1.0 / n0) * common * (
|
|
1 + (tau - mu0) * (x_ref - mu0) / sigma0 ** 2
|
|
))
|
|
|
|
# --- Plot -----------------------------------------------------------------
|
|
C_NEG = "#4477AA"
|
|
C_POS = "#CC6633"
|
|
C_TAU = "#333333"
|
|
|
|
fig, (ax1, ax2) = plt.subplots(
|
|
2, 1, figsize=(5.5, 4.2), sharex=True,
|
|
gridspec_kw={"height_ratios": [1.3, 1.0]}
|
|
)
|
|
fig.subplots_adjust(hspace=0.06)
|
|
|
|
# — Top: score distributions with fitted Gaussian PDFs —
|
|
ax1.hist(neg_scores, bins=20, density=True, alpha=0.35, color=C_NEG,
|
|
label=r"Negative class ($y=0$)")
|
|
ax1.hist(pos_scores, bins=20, density=True, alpha=0.35, color=C_POS,
|
|
label=r"Positive class ($y=1$)")
|
|
ax1.plot(x_ref, stats.norm.pdf(x_ref, mu0, sigma0),
|
|
color=C_NEG, linewidth=1.8, label=r"Fitted $\mathcal{N}(\mu_0,\sigma_0^2)$")
|
|
ax1.plot(x_ref, stats.norm.pdf(x_ref, mu1, sigma1),
|
|
color=C_POS, linewidth=1.8, label=r"Fitted $\mathcal{N}(\mu_1,\sigma_1^2)$")
|
|
ax1.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.4,
|
|
label=rf"Threshold $\tau$ (FPR $=\alpha={alpha}$)")
|
|
ax1.set_ylabel("Density", fontsize=9)
|
|
ax1.set_yticks([])
|
|
ax1.legend(fontsize=7, loc="upper right", framealpha=0.85)
|
|
|
|
# — Bottom: gradient magnitudes —
|
|
ax2.scatter(neg_scores, grad_neg_abs, color=C_NEG, s=12, zorder=3,
|
|
alpha=0.75, label=r"$|\partial\mathcal{L}/\partial f_\theta(\mathbf{x})|$, $y=0$")
|
|
ax2.scatter(pos_scores, grad_pos_abs, color=C_POS, s=12, zorder=3,
|
|
alpha=0.75, label=r"$|\partial\mathcal{L}/\partial f_\theta(\mathbf{x})|$, $y=1$")
|
|
ax2.plot(x_ref, smooth_neg_abs, color=C_NEG, linewidth=1.2, alpha=0.55)
|
|
ax2.plot(x_ref, smooth_pos_abs, color=C_POS, linewidth=1.2, alpha=0.55)
|
|
ax2.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.4)
|
|
ax2.set_ylabel("Gradient magnitude", fontsize=9)
|
|
ax2.set_xlabel(r"Score $f_\theta(\mathbf{x})$", fontsize=9)
|
|
ax2.set_yticks([])
|
|
ax2.legend(fontsize=7.5, loc="upper right", framealpha=0.85)
|
|
|
|
for ax in (ax1, ax2):
|
|
ax.tick_params(labelsize=8)
|
|
ax.spines[["top", "right"]].set_visible(False)
|
|
|
|
out_base = f"{OUT_DIR}/threshold_weighting_gaussian"
|
|
fig.savefig(f"{out_base}.pdf", bbox_inches="tight")
|
|
fig.savefig(f"{out_base}.png", bbox_inches="tight", dpi=180)
|
|
print(f"Saved to {out_base}.{{pdf,png}}")
|