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

Replace SHAP with feature-distribution plots; compute Explore score from the deployed classifier (consistent with BYO); drop shap dep

Browse files
Files changed (2) hide show
  1. app.py +57 -40
  2. 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
- 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 = {
@@ -80,29 +84,38 @@ def headline_figure():
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
 
@@ -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
- 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) --------------------------------
@@ -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 chart shows which features drove it.")
160
- return md, shap_figure(feats, clf_name)
 
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="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._")
 
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