Spaces:
Sleeping
Sleeping
Replace SHAP with feature-distribution plots; compute Explore score from the deployed classifier (consistent with BYO); drop shap dep
Browse files- app.py +57 -40
- requirements.txt +0 -1
app.py
CHANGED
|
@@ -13,7 +13,6 @@ predict memorization about as well as the full feature set.
|
|
| 13 |
"""
|
| 14 |
import os, glob, json, joblib, numpy as np, pandas as pd, torch, gradio as gr
|
| 15 |
import spaces # Hugging Face ZeroGPU; @spaces.GPU is a no-op off ZeroGPU
|
| 16 |
-
import shap
|
| 17 |
import matplotlib
|
| 18 |
matplotlib.use("Agg")
|
| 19 |
import matplotlib.pyplot as plt
|
|
@@ -27,13 +26,18 @@ CLFS = {}
|
|
| 27 |
for f in glob.glob(os.path.join(HERE, "models", "clf_*.joblib")):
|
| 28 |
b = joblib.load(f)
|
| 29 |
CLFS[b["dataset"]] = b
|
| 30 |
-
EXPLAINERS = {} # lazily built shap.TreeExplainer per classifier
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
FEAT_NAMES = {"zlib_entropy": "zlib entropy", "base_ppl": "base perplexity",
|
| 33 |
"base_loss_variance": "base loss variance", "gradient_norm": "gradient norm",
|
| 34 |
"one_step_abs_change": "one-step loss change", "one_step_rel_change": "one-step rel. loss change"}
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# Headline ablation result (paper: dedup, mean +/- std over 3 training seeds), demo datasets only.
|
| 39 |
HEADLINE = {
|
|
@@ -80,29 +84,38 @@ def headline_figure():
|
|
| 80 |
return fig
|
| 81 |
|
| 82 |
|
| 83 |
-
def
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
return fig
|
| 107 |
|
| 108 |
|
|
@@ -129,13 +142,16 @@ def draw(dataset, which):
|
|
| 129 |
"green tokens happen to match, red tokens do not.")
|
| 130 |
if bool(r.base_reproduced):
|
| 131 |
verdict += "\n\n_(Note: the base model already reproduces this suffix, so it is contamination rather than fine-tuning-induced memorization.)_"
|
| 132 |
-
verdict += (f"\n\n### Classifier risk score: **{float(r.score):.3f}**\n"
|
| 133 |
-
f"Computed from base-model features *before* fine-tuning. The chart below shows how each "
|
| 134 |
-
f"feature moved this score.")
|
| 135 |
clf_name = dataset if dataset in CLFS else next(iter(CLFS))
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
# ----------------------------- Predict (BYO) --------------------------------
|
|
@@ -156,8 +172,9 @@ def predict(text, clf_name):
|
|
| 156 |
md = (f"### Predicted memorization risk: **{risk:.3f}** ({flag})\n"
|
| 157 |
f"Forecast from base-model features using the classifier trained on **{clf_name}**. "
|
| 158 |
f"There is no ground truth for arbitrary text, so treat it as a relative risk score. "
|
| 159 |
-
f"The
|
| 160 |
-
|
|
|
|
| 161 |
|
| 162 |
|
| 163 |
INTRO = """# Predicting Memorization Before Fine-Tuning
|
|
@@ -190,18 +207,18 @@ with gr.Blocks(title="Predicting Memorization Before Fine-Tuning") as demo:
|
|
| 190 |
label="Model's actual 50-token generation (green = matches ground truth, red = differs)",
|
| 191 |
color_map={"match": GREEN, "diff": RED}, combine_adjacent=True, show_legend=False)
|
| 192 |
verdict = gr.Markdown()
|
| 193 |
-
feat = gr.Plot(label="
|
| 194 |
go.click(draw, [d_in, f_in], [pref, gen, verdict, feat])
|
| 195 |
with gr.Tab("Score your own text"):
|
| 196 |
gr.Markdown("Paste a passage (at least ~100 tokens). We compute its base-model features and "
|
| 197 |
-
"return the classifier's **predicted** memorization risk,
|
| 198 |
-
"
|
| 199 |
txt = gr.Textbox(label="Your text", lines=6)
|
| 200 |
clf_in = gr.Dropdown(sorted(CLFS.keys()), value=sorted(CLFS.keys())[0],
|
| 201 |
-
label="
|
| 202 |
pgo = gr.Button("Predict memorization risk", variant="primary")
|
| 203 |
pmd = gr.Markdown()
|
| 204 |
-
pfeat = gr.Plot(label="
|
| 205 |
pgo.click(predict, [txt, clf_in], [pmd, pfeat])
|
| 206 |
gr.Markdown("_Datasets shown are public (FineWeb, PG-19, The Stack, OpenWebMath). "
|
| 207 |
"The classifier uses only base-model features and was trained on a separate fine-tuning run._")
|
|
|
|
| 13 |
"""
|
| 14 |
import os, glob, json, joblib, numpy as np, pandas as pd, torch, gradio as gr
|
| 15 |
import spaces # Hugging Face ZeroGPU; @spaces.GPU is a no-op off ZeroGPU
|
|
|
|
| 16 |
import matplotlib
|
| 17 |
matplotlib.use("Agg")
|
| 18 |
import matplotlib.pyplot as plt
|
|
|
|
| 26 |
for f in glob.glob(os.path.join(HERE, "models", "clf_*.joblib")):
|
| 27 |
b = joblib.load(f)
|
| 28 |
CLFS[b["dataset"]] = b
|
|
|
|
| 29 |
|
| 30 |
+
# Feature distributions per dataset, split by memorization outcome (for the "where does
|
| 31 |
+
# this example fall" plots). Model-free: reads straight from the lookup table.
|
| 32 |
+
FEAT_ORDER = ["base_ppl", "base_loss_variance", "one_step_rel_change",
|
| 33 |
+
"one_step_abs_change", "gradient_norm", "zlib_entropy"]
|
| 34 |
FEAT_NAMES = {"zlib_entropy": "zlib entropy", "base_ppl": "base perplexity",
|
| 35 |
"base_loss_variance": "base loss variance", "gradient_norm": "gradient norm",
|
| 36 |
"one_step_abs_change": "one-step loss change", "one_step_rel_change": "one-step rel. loss change"}
|
| 37 |
+
DIST = {ds: {"mem": LOOKUP[(LOOKUP.dataset == ds) & LOOKUP.memorized],
|
| 38 |
+
"non": LOOKUP[(LOOKUP.dataset == ds) & ~LOOKUP.memorized]} for ds in DATASETS}
|
| 39 |
+
GREEN, RED = "#2e7d32", "#c62828" # generation highlight
|
| 40 |
+
MEMCLR, NONCLR = "#f58518", "#4c78a8" # memorized (orange) vs not-memorized (blue)
|
| 41 |
|
| 42 |
# Headline ablation result (paper: dedup, mean +/- std over 3 training seeds), demo datasets only.
|
| 43 |
HEADLINE = {
|
|
|
|
| 84 |
return fig
|
| 85 |
|
| 86 |
|
| 87 |
+
def dist_figure(feats, dataset):
|
| 88 |
+
"""Six panels: where this example's feature values fall relative to the memorized
|
| 89 |
+
and not-memorized populations of the chosen dataset. Model-free."""
|
| 90 |
+
d = DIST.get(dataset) or DIST[next(iter(DIST))]
|
| 91 |
+
mem, non = d["mem"], d["non"]
|
| 92 |
+
fig, axes = plt.subplots(2, 3, figsize=(9.6, 5.4))
|
| 93 |
+
for ax, f in zip(axes.ravel(), FEAT_ORDER):
|
| 94 |
+
mv = mem[f].to_numpy(float); nv = non[f].to_numpy(float)
|
| 95 |
+
both = np.concatenate([mv, nv])
|
| 96 |
+
lo, hi = np.nanpercentile(both, [1, 99])
|
| 97 |
+
if not np.isfinite(lo) or not np.isfinite(hi) or lo == hi:
|
| 98 |
+
lo, hi = float(np.nanmin(both)), float(np.nanmax(both)) + 1e-9
|
| 99 |
+
use_log = lo > 0 and hi / lo > 50
|
| 100 |
+
bins = (np.logspace(np.log10(lo), np.log10(hi), 40) if use_log
|
| 101 |
+
else np.linspace(lo, hi, 40))
|
| 102 |
+
centers = 0.5 * (bins[:-1] + bins[1:])
|
| 103 |
+
for arr, color, label in [(nv, NONCLR, "not memorized"), (mv, MEMCLR, "memorized")]:
|
| 104 |
+
c, _ = np.histogram(np.clip(arr, lo, hi), bins=bins)
|
| 105 |
+
c = c / c.max() if c.max() > 0 else c # normalize each class to its own peak
|
| 106 |
+
ax.fill_between(centers, c, step="mid", alpha=0.5, color=color, label=label)
|
| 107 |
+
if use_log:
|
| 108 |
+
ax.set_xscale("log")
|
| 109 |
+
v = float(feats[f]); vc = min(max(v, lo), hi)
|
| 110 |
+
ax.axvline(vc, color="k", lw=1.8)
|
| 111 |
+
ax.plot([vc], [ax.get_ylim()[1] * 0.9], marker="v", color="k", ms=9, clip_on=False)
|
| 112 |
+
off = "" if lo <= v <= hi else (" (off ↓)" if v < lo else " (off ↑)")
|
| 113 |
+
ax.set_title(f"{FEAT_NAMES[f]}\nthis example = {v:.3g}{off}", fontsize=8.5)
|
| 114 |
+
ax.set_yticks([]); ax.tick_params(labelsize=7)
|
| 115 |
+
axes.ravel()[0].legend(fontsize=7.5, loc="upper right", framealpha=0.9)
|
| 116 |
+
fig.suptitle(f"Where this example's base-model features fall — {dataset} "
|
| 117 |
+
f"(black ▼ = this example)", fontsize=10)
|
| 118 |
+
fig.tight_layout(rect=[0, 0, 1, 0.95])
|
| 119 |
return fig
|
| 120 |
|
| 121 |
|
|
|
|
| 142 |
"green tokens happen to match, red tokens do not.")
|
| 143 |
if bool(r.base_reproduced):
|
| 144 |
verdict += "\n\n_(Note: the base model already reproduces this suffix, so it is contamination rather than fine-tuning-induced memorization.)_"
|
|
|
|
|
|
|
|
|
|
| 145 |
clf_name = dataset if dataset in CLFS else next(iter(CLFS))
|
| 146 |
+
b = CLFS[clf_name]
|
| 147 |
+
feats = {k: float(r[k]) for k in FEAT_ORDER}
|
| 148 |
+
X = np.nan_to_num(np.array([[feats[k] for k in b["feats"]]]), posinf=1e10, neginf=-1e10)
|
| 149 |
+
risk = float(b["clf"].predict_proba(b["scaler"].transform(X))[0, 1])
|
| 150 |
+
verdict += (f"\n\n### Classifier risk score: **{risk:.3f}**\n"
|
| 151 |
+
f"Computed by the memorization classifier from base-model features measured *before* "
|
| 152 |
+
f"fine-tuning (the same classifier used in the other tab). The plots below show where "
|
| 153 |
+
f"this example's feature values fall relative to memorized and not-memorized examples.")
|
| 154 |
+
return r.prefix, gen, verdict, dist_figure(feats, dataset)
|
| 155 |
|
| 156 |
|
| 157 |
# ----------------------------- Predict (BYO) --------------------------------
|
|
|
|
| 172 |
md = (f"### Predicted memorization risk: **{risk:.3f}** ({flag})\n"
|
| 173 |
f"Forecast from base-model features using the classifier trained on **{clf_name}**. "
|
| 174 |
f"There is no ground truth for arbitrary text, so treat it as a relative risk score. "
|
| 175 |
+
f"The plots below show where your text's features fall relative to memorized and not-memorized "
|
| 176 |
+
f"examples in **{clf_name}**.")
|
| 177 |
+
return md, dist_figure(feats, clf_name)
|
| 178 |
|
| 179 |
|
| 180 |
INTRO = """# Predicting Memorization Before Fine-Tuning
|
|
|
|
| 207 |
label="Model's actual 50-token generation (green = matches ground truth, red = differs)",
|
| 208 |
color_map={"match": GREEN, "diff": RED}, combine_adjacent=True, show_legend=False)
|
| 209 |
verdict = gr.Markdown()
|
| 210 |
+
feat = gr.Plot(label="Where this example's features fall (memorized vs not-memorized)")
|
| 211 |
go.click(draw, [d_in, f_in], [pref, gen, verdict, feat])
|
| 212 |
with gr.Tab("Score your own text"):
|
| 213 |
gr.Markdown("Paste a passage (at least ~100 tokens). We compute its base-model features and "
|
| 214 |
+
"return the classifier's **predicted** memorization risk, and show where your text's "
|
| 215 |
+
"features fall relative to memorized and not-memorized examples.")
|
| 216 |
txt = gr.Textbox(label="Your text", lines=6)
|
| 217 |
clf_in = gr.Dropdown(sorted(CLFS.keys()), value=sorted(CLFS.keys())[0],
|
| 218 |
+
label="Compare against dataset")
|
| 219 |
pgo = gr.Button("Predict memorization risk", variant="primary")
|
| 220 |
pmd = gr.Markdown()
|
| 221 |
+
pfeat = gr.Plot(label="Where your text's features fall (memorized vs not-memorized)")
|
| 222 |
pgo.click(predict, [txt, clf_in], [pmd, pfeat])
|
| 223 |
gr.Markdown("_Datasets shown are public (FineWeb, PG-19, The Stack, OpenWebMath). "
|
| 224 |
"The classifier uses only base-model features and was trained on a separate fine-tuning run._")
|
requirements.txt
CHANGED
|
@@ -7,5 +7,4 @@ pandas>=2.0
|
|
| 7 |
pyarrow>=14.0
|
| 8 |
numpy>=2.0
|
| 9 |
joblib>=1.5
|
| 10 |
-
shap==0.49.1
|
| 11 |
matplotlib>=3.7
|
|
|
|
| 7 |
pyarrow>=14.0
|
| 8 |
numpy>=2.0
|
| 9 |
joblib>=1.5
|
|
|
|
| 10 |
matplotlib>=3.7
|