Spaces:
Running on Zero
Running on Zero
Add highlighted generation, per-example SHAP, and headline ablation panel
Browse files- app.py +116 -38
- data/lookup.parquet +2 -2
- requirements.txt +2 -0
app.py
CHANGED
|
@@ -1,14 +1,22 @@
|
|
| 1 |
"""Predicting Memorization Before Fine-Tuning — interactive demo.
|
| 2 |
|
| 3 |
Two modes:
|
| 4 |
-
(1) Explore the held-out 1M — pick a real training sequence, see
|
| 5 |
-
|
| 6 |
-
|
|
|
|
| 7 |
(2) Score your own text — paste a passage, get the classifier's predicted
|
| 8 |
-
memorization risk from base-model features
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
-
import os, glob, joblib, numpy as np, pandas as pd, torch, gradio as gr
|
| 11 |
-
import spaces # Hugging Face ZeroGPU;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
BASE_MODEL = os.environ.get("BASE_MODEL", "EleutherAI/pythia-1.4b")
|
|
@@ -19,35 +27,88 @@ CLFS = {}
|
|
| 19 |
for f in glob.glob(os.path.join(HERE, "models", "clf_*.joblib")):
|
| 20 |
b = joblib.load(f)
|
| 21 |
CLFS[b["dataset"]] = b
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
_ON_ZEROGPU = bool(os.environ.get("SPACES_ZERO_GPU"))
|
| 29 |
DEVICE = "cuda" if (_ON_ZEROGPU or torch.cuda.is_available()) else "cpu"
|
| 30 |
try:
|
| 31 |
from byo_features import BYOExtractor
|
| 32 |
EXTRACTOR = BYOExtractor(BASE_MODEL, device=DEVICE)
|
| 33 |
_LOAD_ERR = None
|
| 34 |
-
except Exception as e:
|
| 35 |
EXTRACTOR, _LOAD_ERR = None, str(e)
|
| 36 |
|
| 37 |
|
| 38 |
@spaces.GPU(duration=120)
|
| 39 |
def _compute_features(text):
|
| 40 |
-
"""GPU-bound step of the BYO path (forward + backward on the base model)."""
|
| 41 |
return EXTRACTOR.features(text)
|
| 42 |
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
# ----------------------------- Explore (lookup) -----------------------------
|
|
@@ -58,18 +119,23 @@ def draw(dataset, which):
|
|
| 58 |
elif which == "Not memorized":
|
| 59 |
df = df[~df.memorized]
|
| 60 |
if len(df) == 0:
|
| 61 |
-
return "No examples for this filter.",
|
| 62 |
r = df.sample(1).iloc[0]
|
|
|
|
| 63 |
if r.memorized:
|
| 64 |
-
verdict = "### 🔴 Memorized\nThe fine-tuned model reproduced the ground-truth suffix exactly."
|
| 65 |
else:
|
| 66 |
-
verdict = "### 🟢 Not memorized\nThe fine-tuned model
|
|
|
|
| 67 |
if bool(r.base_reproduced):
|
| 68 |
verdict += "\n\n_(Note: the base model already reproduces this suffix, so it is contamination rather than fine-tuning-induced memorization.)_"
|
| 69 |
-
|
| 70 |
-
f"Computed from base-model features *before* fine-tuning. "
|
| 71 |
-
f"
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
# ----------------------------- Predict (BYO) --------------------------------
|
|
@@ -89,9 +155,9 @@ def predict(text, clf_name):
|
|
| 89 |
flag = "high" if risk >= 0.5 else "low"
|
| 90 |
md = (f"### Predicted memorization risk: **{risk:.3f}** ({flag})\n"
|
| 91 |
f"Forecast from base-model features using the classifier trained on **{clf_name}**. "
|
| 92 |
-
f"
|
| 93 |
-
f"
|
| 94 |
-
return md,
|
| 95 |
|
| 96 |
|
| 97 |
INTRO = """# Predicting Memorization Before Fine-Tuning
|
|
@@ -100,30 +166,42 @@ fine-tuned model will memorize. **Explore** verifies this against ground truth o
|
|
| 100 |
run; **Score your own text** applies the same classifier to a passage you provide.
|
| 101 |
"""
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
with gr.Blocks(title="Predicting Memorization Before Fine-Tuning") as demo:
|
| 104 |
gr.Markdown(INTRO)
|
| 105 |
with gr.Tab("Explore the held-out 1M"):
|
|
|
|
|
|
|
|
|
|
| 106 |
gr.Markdown("Pick a real Pythia-1.4B training sequence and compare the classifier's "
|
| 107 |
-
"pre-fine-tuning risk score with
|
| 108 |
with gr.Row():
|
| 109 |
d_in = gr.Dropdown(DATASETS, value=DATASETS[0], label="Dataset")
|
| 110 |
f_in = gr.Radio(["All", "Memorized only", "Not memorized"], value="All", label="Filter")
|
| 111 |
go = gr.Button("Show a random example", variant="primary")
|
| 112 |
pref = gr.Textbox(label="Prefix (50 tokens given to the model)", lines=3)
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
verdict = gr.Markdown()
|
| 115 |
-
feat = gr.
|
| 116 |
-
go.click(draw, [d_in, f_in], [pref,
|
| 117 |
with gr.Tab("Score your own text"):
|
| 118 |
gr.Markdown("Paste a passage (at least ~100 tokens). We compute its base-model features and "
|
| 119 |
-
"return the classifier's **predicted** memorization risk
|
| 120 |
-
"
|
| 121 |
txt = gr.Textbox(label="Your text", lines=6)
|
| 122 |
clf_in = gr.Dropdown(sorted(CLFS.keys()), value=sorted(CLFS.keys())[0],
|
| 123 |
label="Classifier (trained on)")
|
| 124 |
pgo = gr.Button("Predict memorization risk", variant="primary")
|
| 125 |
pmd = gr.Markdown()
|
| 126 |
-
pfeat = gr.
|
| 127 |
pgo.click(predict, [txt, clf_in], [pmd, pfeat])
|
| 128 |
gr.Markdown("_Datasets shown are public (FineWeb, PG-19, The Stack, OpenWebMath). "
|
| 129 |
"The classifier uses only base-model features and was trained on a separate fine-tuning run._")
|
|
|
|
| 1 |
"""Predicting Memorization Before Fine-Tuning — interactive demo.
|
| 2 |
|
| 3 |
Two modes:
|
| 4 |
+
(1) Explore the held-out 1M — pick a real training sequence, see the fine-tuned
|
| 5 |
+
model's actual 50-token generation (green where it matches the ground truth),
|
| 6 |
+
the memorized/not verdict, and a per-example explanation of the classifier's
|
| 7 |
+
pre-fine-tuning risk score.
|
| 8 |
(2) Score your own text — paste a passage, get the classifier's predicted
|
| 9 |
+
memorization risk from base-model features, with the same per-example explanation.
|
| 10 |
+
|
| 11 |
+
A collapsible panel shows the headline result: base-model gradient features alone
|
| 12 |
+
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
|
| 20 |
|
| 21 |
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 22 |
BASE_MODEL = os.environ.get("BASE_MODEL", "EleutherAI/pythia-1.4b")
|
|
|
|
| 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 |
+
GRAD_FEATS = {"gradient_norm", "one_step_abs_change", "one_step_rel_change"}
|
| 36 |
+
GREEN, RED, GREY = "#2e7d32", "#c62828", "#9aa0a6"
|
| 37 |
+
|
| 38 |
+
# Headline ablation result (paper: dedup, mean +/- std over 3 training seeds), demo datasets only.
|
| 39 |
+
HEADLINE = {
|
| 40 |
+
"FineWeb": {"text-only": (0.628, 0.011), "gradient-only": (0.994, 0.003), "all": (0.997, 0.002)},
|
| 41 |
+
"PG-19": {"text-only": (0.640, 0.035), "gradient-only": (0.998, 0.001), "all": (0.996, 0.003)},
|
| 42 |
+
"The Stack": {"text-only": (0.765, 0.001), "gradient-only": (0.965, 0.001), "all": (0.971, 0.001)},
|
| 43 |
+
"OpenWebMath": {"text-only": (0.837, 0.001), "gradient-only": (0.976, 0.002), "all": (0.992, 0.001)},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# ZeroGPU: place the base model on cuda at module level (a CUDA-emulation mode makes
|
| 47 |
+
# this valid at startup; a real GPU is attached only inside @spaces.GPU functions).
|
| 48 |
_ON_ZEROGPU = bool(os.environ.get("SPACES_ZERO_GPU"))
|
| 49 |
DEVICE = "cuda" if (_ON_ZEROGPU or torch.cuda.is_available()) else "cpu"
|
| 50 |
try:
|
| 51 |
from byo_features import BYOExtractor
|
| 52 |
EXTRACTOR = BYOExtractor(BASE_MODEL, device=DEVICE)
|
| 53 |
_LOAD_ERR = None
|
| 54 |
+
except Exception as e:
|
| 55 |
EXTRACTOR, _LOAD_ERR = None, str(e)
|
| 56 |
|
| 57 |
|
| 58 |
@spaces.GPU(duration=120)
|
| 59 |
def _compute_features(text):
|
|
|
|
| 60 |
return EXTRACTOR.features(text)
|
| 61 |
|
| 62 |
|
| 63 |
+
# ------------------------------- graphics ----------------------------------
|
| 64 |
+
def headline_figure():
|
| 65 |
+
tiers = ["text-only", "gradient-only", "all"]
|
| 66 |
+
colors = {"text-only": GREY, "gradient-only": GREEN, "all": "#1f4e79"}
|
| 67 |
+
datasets = list(HEADLINE.keys())
|
| 68 |
+
x = np.arange(len(datasets)); w = 0.26
|
| 69 |
+
fig, ax = plt.subplots(figsize=(7.2, 3.6))
|
| 70 |
+
for i, t in enumerate(tiers):
|
| 71 |
+
vals = [HEADLINE[d][t][0] for d in datasets]
|
| 72 |
+
errs = [HEADLINE[d][t][1] for d in datasets]
|
| 73 |
+
ax.bar(x + (i - 1) * w, vals, w, yerr=errs, capsize=3, label=t, color=colors[t])
|
| 74 |
+
ax.set_xticks(x); ax.set_xticklabels(datasets)
|
| 75 |
+
ax.set_ylim(0.5, 1.02); ax.set_ylabel("ROC-AUC")
|
| 76 |
+
ax.set_title("Gradient-only features ≈ full model (mean ± std over 3 seeds)")
|
| 77 |
+
ax.axhline(0.5, color="k", lw=0.7, ls=":")
|
| 78 |
+
ax.legend(loc="lower right", ncol=3, fontsize=8, framealpha=0.9)
|
| 79 |
+
fig.tight_layout()
|
| 80 |
+
return fig
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def shap_figure(feats, clf_name):
|
| 84 |
+
b = CLFS[clf_name]
|
| 85 |
+
if clf_name not in EXPLAINERS:
|
| 86 |
+
EXPLAINERS[clf_name] = shap.TreeExplainer(b["clf"])
|
| 87 |
+
expl = EXPLAINERS[clf_name]
|
| 88 |
+
order = b["feats"]
|
| 89 |
+
X = np.nan_to_num(np.array([[feats[k] for k in order]], dtype=float), posinf=1e10, neginf=-1e10)
|
| 90 |
+
Xs = b["scaler"].transform(X)
|
| 91 |
+
sv = expl.shap_values(Xs)
|
| 92 |
+
sv = np.asarray(sv[1] if isinstance(sv, list) else sv).reshape(-1)[:len(order)]
|
| 93 |
+
|
| 94 |
+
idx = np.argsort(np.abs(sv)) # ascending -> largest on top
|
| 95 |
+
labels = [f"{FEAT_NAMES.get(order[i], order[i])} ({feats[order[i]]:.3g})" for i in idx]
|
| 96 |
+
vals = sv[idx]
|
| 97 |
+
colors = [GREEN if v > 0 else GREY for v in vals]
|
| 98 |
+
fig, ax = plt.subplots(figsize=(7.2, 3.2))
|
| 99 |
+
ax.barh(range(len(vals)), vals, color=colors)
|
| 100 |
+
ax.set_yticks(range(len(vals))); ax.set_yticklabels(labels, fontsize=9)
|
| 101 |
+
ax.axvline(0, color="k", lw=0.8)
|
| 102 |
+
ax.set_xlabel("push toward memorized → (log-odds contribution)")
|
| 103 |
+
grad_top = [order[i] in GRAD_FEATS for i in idx]
|
| 104 |
+
ax.set_title("Why this score" + (" · gradient features dominate" if any(grad_top[-2:]) else ""))
|
| 105 |
+
fig.tight_layout()
|
| 106 |
+
return fig
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _segments(gen_segments_json):
|
| 110 |
+
segs = json.loads(gen_segments_json)
|
| 111 |
+
return [(t, lab) for t, lab in segs]
|
| 112 |
|
| 113 |
|
| 114 |
# ----------------------------- Explore (lookup) -----------------------------
|
|
|
|
| 119 |
elif which == "Not memorized":
|
| 120 |
df = df[~df.memorized]
|
| 121 |
if len(df) == 0:
|
| 122 |
+
return "No examples for this filter.", [], "", None
|
| 123 |
r = df.sample(1).iloc[0]
|
| 124 |
+
gen = _segments(r.gen_segments)
|
| 125 |
if r.memorized:
|
| 126 |
+
verdict = "### 🔴 Memorized\nThe fine-tuned model reproduced the ground-truth suffix exactly (all tokens green)."
|
| 127 |
else:
|
| 128 |
+
verdict = ("### 🟢 Not memorized\nThe fine-tuned model's greedy generation diverges from the ground truth; "
|
| 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 |
+
feats = {k: float(r[k]) for k in CLFS[clf_name]["feats"]}
|
| 137 |
+
fig = shap_figure(feats, clf_name)
|
| 138 |
+
return r.prefix, gen, verdict, fig
|
| 139 |
|
| 140 |
|
| 141 |
# ----------------------------- Predict (BYO) --------------------------------
|
|
|
|
| 155 |
flag = "high" if risk >= 0.5 else "low"
|
| 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 chart shows which features drove it.")
|
| 160 |
+
return md, shap_figure(feats, clf_name)
|
| 161 |
|
| 162 |
|
| 163 |
INTRO = """# Predicting Memorization Before Fine-Tuning
|
|
|
|
| 166 |
run; **Score your own text** applies the same classifier to a passage you provide.
|
| 167 |
"""
|
| 168 |
|
| 169 |
+
WHY = """The classifier reads six features of the **base** model, measured **before** any fine-tuning.
|
| 170 |
+
Three come from a single gradient step on the sequence (**gradient norm** and the **one-step loss change**);
|
| 171 |
+
the rest are text and base-loss statistics. On the datasets shown here, the three **gradient-only** features
|
| 172 |
+
alone reach ROC-AUC 0.965–0.998, essentially matching the full feature set, while text-only features lag far
|
| 173 |
+
behind. (On conversational text, not shown here, the non-gradient features contribute more.)
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
with gr.Blocks(title="Predicting Memorization Before Fine-Tuning") as demo:
|
| 177 |
gr.Markdown(INTRO)
|
| 178 |
with gr.Tab("Explore the held-out 1M"):
|
| 179 |
+
with gr.Accordion("Why base-model gradients predict memorization", open=False):
|
| 180 |
+
gr.Markdown(WHY)
|
| 181 |
+
gr.Plot(headline_figure())
|
| 182 |
gr.Markdown("Pick a real Pythia-1.4B training sequence and compare the classifier's "
|
| 183 |
+
"pre-fine-tuning risk score with what the fine-tuned model actually generated.")
|
| 184 |
with gr.Row():
|
| 185 |
d_in = gr.Dropdown(DATASETS, value=DATASETS[0], label="Dataset")
|
| 186 |
f_in = gr.Radio(["All", "Memorized only", "Not memorized"], value="All", label="Filter")
|
| 187 |
go = gr.Button("Show a random example", variant="primary")
|
| 188 |
pref = gr.Textbox(label="Prefix (50 tokens given to the model)", lines=3)
|
| 189 |
+
gen = gr.HighlightedText(
|
| 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="Per-example feature contributions (SHAP)")
|
| 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, with the per-example "
|
| 198 |
+
"feature contributions below.")
|
| 199 |
txt = gr.Textbox(label="Your text", lines=6)
|
| 200 |
clf_in = gr.Dropdown(sorted(CLFS.keys()), value=sorted(CLFS.keys())[0],
|
| 201 |
label="Classifier (trained on)")
|
| 202 |
pgo = gr.Button("Predict memorization risk", variant="primary")
|
| 203 |
pmd = gr.Markdown()
|
| 204 |
+
pfeat = gr.Plot(label="Per-example feature contributions (SHAP)")
|
| 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._")
|
data/lookup.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d4a15810da1cc066350a3aea465eef483522cdfbd3b4d70361f5eeb5a2e6057
|
| 3 |
+
size 11963509
|
requirements.txt
CHANGED
|
@@ -7,3 +7,5 @@ pandas>=2.0
|
|
| 7 |
pyarrow>=14.0
|
| 8 |
numpy>=2.0
|
| 9 |
joblib>=1.5
|
|
|
|
|
|
|
|
|
| 7 |
pyarrow>=14.0
|
| 8 |
numpy>=2.0
|
| 9 |
joblib>=1.5
|
| 10 |
+
shap==0.49.1
|
| 11 |
+
matplotlib>=3.7
|