jaydeepb commited on
Commit
aa72ab5
·
verified ·
1 Parent(s): c222853

Add highlighted generation, per-example SHAP, and headline ablation panel

Browse files
Files changed (3) hide show
  1. app.py +116 -38
  2. data/lookup.parquet +2 -2
  3. 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 whether the
5
- fine-tuned Pythia-1.4B actually memorized it (ground truth), alongside the
6
- classifier's pre-fine-tuning risk score and feature breakdown.
 
7
  (2) Score your own text — paste a passage, get the classifier's predicted
8
- memorization risk from base-model features (a forecast; no ground truth).
 
 
 
9
  """
10
- import os, glob, joblib, numpy as np, pandas as pd, torch, gradio as gr
11
- import spaces # Hugging Face ZeroGPU; the @spaces.GPU decorator is a no-op off 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
- FEATS = ["zlib_entropy", "base_ppl", "base_loss_variance",
23
- "gradient_norm", "one_step_abs_change", "one_step_rel_change"]
24
-
25
- # ZeroGPU: place the model on cuda at module level (a CUDA-emulation mode makes this
26
- # valid at startup; a real GPU is attached only inside @spaces.GPU functions). Off
27
- # ZeroGPU we fall back to a real GPU if present, else CPU, so the app still runs locally.
 
 
 
 
 
 
 
 
 
 
 
 
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: # e.g. no model cached / offline local run
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
- def _feature_table(row):
45
- names = {"zlib_entropy": "zlib entropy", "base_ppl": "base perplexity",
46
- "base_loss_variance": "base loss variance", "gradient_norm": "gradient norm",
47
- "one_step_rel_change": "one-step rel. loss change"}
48
- return pd.DataFrame(
49
- [(v, f"{float(row[k]):.4f}") for k, v in names.items() if k in row],
50
- columns=["Base-model feature", "Value"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.", "", "", None
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 did not reproduce the ground-truth suffix."
 
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
- score_md = (f"### Classifier risk score: **{float(r.score):.3f}**\n"
70
- f"Computed from base-model features *before* fine-tuning. "
71
- f"Higher means more likely to be memorized. Ground truth above is what actually happened.")
72
- return (r.prefix, r.suffix, verdict + "\n\n" + score_md, _feature_table(r))
 
 
 
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"This is a prediction of whether a model *would* memorize this sequence if fine-tuned on it; "
93
- f"there is no ground truth for arbitrary text. Treat it as a relative risk score, not a calibrated probability.")
94
- return md, _feature_table(feats)
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 whether the fine-tuned model actually memorized it.")
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
- suf = gr.Textbox(label="Ground-truth suffix (next 50 tokens)", lines=3)
 
 
114
  verdict = gr.Markdown()
115
- feat = gr.Dataframe(label="Base-model features for this sequence", interactive=False)
116
- go.click(draw, [d_in, f_in], [pref, suf, verdict, feat])
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. No ground truth exists for "
120
- "arbitrary text, so this is a forecast (see the trust discussion in the paper).")
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.Dataframe(label="Base-model features for your text", interactive=False)
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:4bcf4588bd84e05e61578ff3d7794f505ec2917c3a88f2bedfe93cd1094ab114
3
- size 6934910
 
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