Spaces:
Running on Zero
Running on Zero
Add memorization-prediction demo (ZeroGPU): explore held-out 1M + score-your-own-text
Browse files- README.md +53 -9
- app.py +132 -0
- assemble_lookup_data.py +59 -0
- byo_features.py +81 -0
- data/lookup.parquet +3 -0
- models/clf_code.joblib +3 -0
- models/clf_fineweb.joblib +3 -0
- models/clf_owm.joblib +3 -0
- models/clf_pg19.joblib +3 -0
- requirements.txt +9 -0
- train_classifiers.py +39 -0
README.md
CHANGED
|
@@ -1,15 +1,59 @@
|
|
| 1 |
---
|
| 2 |
-
title: Predicting Memorization
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
license: apache-2.0
|
| 12 |
-
short_description: Predicting memorization in language models before finetuning
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Predicting Memorization Before Fine-Tuning
|
| 3 |
+
emoji: 🔎
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Predicting Memorization Before Fine-Tuning — demo
|
| 13 |
+
|
| 14 |
+
Interactive companion to the paper. A classifier trained only on **pre-fine-tuning**
|
| 15 |
+
base-model features predicts which sequences a fine-tuned model will memorize.
|
| 16 |
+
|
| 17 |
+
- **Explore the held-out 1M** — pick a real Pythia-1.4B training sequence and see whether the
|
| 18 |
+
fine-tuned model actually memorized it (ground truth), next to the classifier's risk score and
|
| 19 |
+
the base-model feature values. Datasets: FineWeb, PG-19, The Stack, OpenWebMath (all public).
|
| 20 |
+
- **Score your own text** — paste a passage and get the classifier's *predicted* memorization
|
| 21 |
+
risk from base-model features. This is a forecast; there is no ground truth for arbitrary text.
|
| 22 |
+
|
| 23 |
+
## Files
|
| 24 |
+
- `app.py` — Gradio app (two tabs).
|
| 25 |
+
- `data/lookup.parquet` — precomputed table (prefix, suffix, memorized flag, classifier score,
|
| 26 |
+
base-model features) sampled from the held-out 1M run (all memorized examples up to a cap plus a
|
| 27 |
+
random sample of non-memorized).
|
| 28 |
+
- `models/clf_*.joblib` — the trained GradientBoostingClassifier + scaler per dataset.
|
| 29 |
+
- `byo_features.py` — computes the six base-model features for arbitrary text (loads
|
| 30 |
+
`EleutherAI/pythia-1.4b`; on ZeroGPU it is placed on `cuda` at startup per HF's guidance).
|
| 31 |
+
- `assemble_lookup_data.py`, `train_classifiers.py` — offline scripts that produced the artifacts
|
| 32 |
+
above (not needed at runtime; kept for reproducibility).
|
| 33 |
+
|
| 34 |
+
## Run locally
|
| 35 |
+
```bash
|
| 36 |
+
pip install -r requirements.txt
|
| 37 |
+
python app.py # opens a local Gradio URL
|
| 38 |
+
```
|
| 39 |
+
Locally the app loads Pythia-1.4B (~3 GB) at startup on a GPU if present, otherwise CPU; the
|
| 40 |
+
`@spaces.GPU` decorator is a no-op off ZeroGPU.
|
| 41 |
+
|
| 42 |
+
## Deploy on Hugging Face Spaces (free ZeroGPU)
|
| 43 |
+
1. Use your **own, established HF account** (free ZeroGPU hosting requires a verified email and an
|
| 44 |
+
account older than 30 days; up to 2 ZeroGPU Spaces). Create a new Space: **SDK = Gradio**,
|
| 45 |
+
hardware = **ZeroGPU**.
|
| 46 |
+
2. Push this folder to the Space repo (the YAML header above configures it):
|
| 47 |
+
```bash
|
| 48 |
+
git clone https://huggingface.co/spaces/<user>/<space> && cd <space>
|
| 49 |
+
cp -r /path/to/demo_app/* . && git add -A && git commit -m "demo" && git push
|
| 50 |
+
```
|
| 51 |
+
3. The Space builds automatically. The Explore tab is instant. "Score your own text" runs the
|
| 52 |
+
base-model forward+backward on a GPU that ZeroGPU attaches on demand (via the `@spaces.GPU`
|
| 53 |
+
decorator in `app.py`), so it is fast; the free tier allows 5 GPU-minutes/day and each score
|
| 54 |
+
takes a couple of seconds.
|
| 55 |
+
|
| 56 |
+
## Notes
|
| 57 |
+
- The classifier uses only base-model (pre-fine-tuning) features and was trained on a separate
|
| 58 |
+
Run-1 fine-tuning run, then evaluated here on a disjoint Run-2 run.
|
| 59 |
+
- Qwen and Llama can be added to the Explore tab once their run-2 token sequences are regenerated.
|
app.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|
| 15 |
+
|
| 16 |
+
LOOKUP = pd.read_parquet(os.path.join(HERE, "data", "lookup.parquet"))
|
| 17 |
+
DATASETS = sorted(LOOKUP["dataset"].unique())
|
| 18 |
+
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) -----------------------------
|
| 54 |
+
def draw(dataset, which):
|
| 55 |
+
df = LOOKUP[LOOKUP.dataset == dataset]
|
| 56 |
+
if which == "Memorized only":
|
| 57 |
+
df = df[df.memorized]
|
| 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) --------------------------------
|
| 76 |
+
def predict(text, clf_name):
|
| 77 |
+
text = (text or "").strip()
|
| 78 |
+
if not text:
|
| 79 |
+
return "Paste a passage first.", None
|
| 80 |
+
if EXTRACTOR is None:
|
| 81 |
+
return f"⚠️ Base model unavailable: {_LOAD_ERR}", None
|
| 82 |
+
try:
|
| 83 |
+
feats = _compute_features(text)
|
| 84 |
+
except ValueError as e:
|
| 85 |
+
return f"⚠️ {e}", None
|
| 86 |
+
b = CLFS[clf_name]
|
| 87 |
+
X = np.nan_to_num(np.array([[feats[k] for k in b["feats"]]]), posinf=1e10, neginf=-1e10)
|
| 88 |
+
risk = float(b["clf"].predict_proba(b["scaler"].transform(X))[0, 1])
|
| 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
|
| 98 |
+
A classifier trained only on **pre-fine-tuning** base-model features predicts which sequences a
|
| 99 |
+
fine-tuned model will memorize. **Explore** verifies this against ground truth on a held-out 1M-sequence
|
| 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._")
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
demo.launch()
|
assemble_lookup_data.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Assemble the precomputed lookup table for the demo.
|
| 2 |
+
|
| 3 |
+
For each (Pythia, dataset) Run-2 set, join token sequences + memorized labels +
|
| 4 |
+
classifier score + base-model features, decode the 50-token prefix/suffix to text,
|
| 5 |
+
and keep a compact sample (all memorized up to a cap, plus a random sample of
|
| 6 |
+
non-memorized). Public / non-PII datasets only.
|
| 7 |
+
|
| 8 |
+
Output: demo_app/data/lookup.parquet
|
| 9 |
+
"""
|
| 10 |
+
import os, numpy as np, pandas as pd
|
| 11 |
+
from transformers import AutoTokenizer
|
| 12 |
+
|
| 13 |
+
ROOT = "/projects/nulab/borkar.j/predicting_memorization"
|
| 14 |
+
TOK = f"{ROOT}/checkpoints/pythia-1.4b"
|
| 15 |
+
DATASETS = {"fineweb": "FineWeb", "pg19": "PG-19", "code": "The Stack", "owm": "OpenWebMath"}
|
| 16 |
+
PREFIX_LEN, WINDOW_LEN = 50, 100
|
| 17 |
+
MEM_CAP, NONMEM_N, SEED = 3000, 5000, 42
|
| 18 |
+
OUT = f"{ROOT}/demo_app/data"
|
| 19 |
+
|
| 20 |
+
tok = AutoTokenizer.from_pretrained(TOK)
|
| 21 |
+
rng = np.random.default_rng(SEED)
|
| 22 |
+
rows = []
|
| 23 |
+
|
| 24 |
+
for d, disp in DATASETS.items():
|
| 25 |
+
ck = f"{ROOT}/checkpoints/pythia_{d}_run2_seed42"
|
| 26 |
+
ds = np.load(f"{ck}/dataset.npy") # [1M,128]
|
| 27 |
+
lab = np.load(f"{ck}/memorization_labels.npy").astype(bool)
|
| 28 |
+
fe = pd.read_csv(f"{ROOT}/full_run2_results/features/{d}_run2_full_features.csv")
|
| 29 |
+
sc = pd.read_csv(f"{ROOT}/full_run2_results/cleaned/scores/{d}_run2_scores.csv")
|
| 30 |
+
assert len(ds) == len(lab) == len(fe) == len(sc), d
|
| 31 |
+
|
| 32 |
+
mem = np.where(lab)[0]
|
| 33 |
+
non = np.where(~lab)[0]
|
| 34 |
+
if len(mem) > MEM_CAP:
|
| 35 |
+
mem = rng.choice(mem, MEM_CAP, replace=False)
|
| 36 |
+
non = rng.choice(non, min(NONMEM_N, len(non)), replace=False)
|
| 37 |
+
idx = np.concatenate([mem, non])
|
| 38 |
+
|
| 39 |
+
pref = tok.batch_decode(ds[idx, :PREFIX_LEN].tolist(), skip_special_tokens=True)
|
| 40 |
+
suf = tok.batch_decode(ds[idx, PREFIX_LEN:WINDOW_LEN].tolist(), skip_special_tokens=True)
|
| 41 |
+
sub = pd.DataFrame({
|
| 42 |
+
"model": "Pythia-1.4B", "dataset": disp,
|
| 43 |
+
"prefix": pref, "suffix": suf,
|
| 44 |
+
"memorized": lab[idx],
|
| 45 |
+
"base_reproduced": sc["base_reproduced"].values[idx].astype(bool),
|
| 46 |
+
"score": sc["score"].values[idx],
|
| 47 |
+
"zlib_entropy": fe["zlib_entropy"].values[idx],
|
| 48 |
+
"base_ppl": fe["base_ppl"].values[idx],
|
| 49 |
+
"base_loss_variance": fe["base_loss_variance"].values[idx],
|
| 50 |
+
"gradient_norm": fe["gradient_norm"].values[idx],
|
| 51 |
+
"one_step_rel_change": fe["one_step_rel_change"].values[idx],
|
| 52 |
+
})
|
| 53 |
+
rows.append(sub)
|
| 54 |
+
print(f"{disp:12s} kept {len(sub)} rows ({int(lab[idx].sum())} memorized)")
|
| 55 |
+
|
| 56 |
+
os.makedirs(OUT, exist_ok=True)
|
| 57 |
+
df = pd.concat(rows, ignore_index=True)
|
| 58 |
+
df.to_parquet(f"{OUT}/lookup.parquet", index=False)
|
| 59 |
+
print(f"\nwrote {OUT}/lookup.parquet ({len(df)} rows, {df.memory_usage(deep=True).sum()/1e6:.1f} MB in mem)")
|
byo_features.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compute the six base-model features for an arbitrary user sequence, mirroring
|
| 2 |
+
feature_extraction_{cpu,gpu}.py so scores are comparable to the paper's classifier.
|
| 3 |
+
|
| 4 |
+
Definitions (base model, no fine-tuning):
|
| 5 |
+
zlib_entropy = compressed byte length of the decoded 50-token suffix
|
| 6 |
+
base_ppl = exp(mean per-token loss) over the 50-token continuation
|
| 7 |
+
base_loss_variance = variance of that per-token loss
|
| 8 |
+
gradient_norm = ||grad of the continuation loss w.r.t. all params||_2
|
| 9 |
+
one_step_abs_change = loss_before - loss_after, after one SGD step (lr=2e-5) on x
|
| 10 |
+
one_step_rel_change = one_step_abs_change / loss_before
|
| 11 |
+
"""
|
| 12 |
+
import zlib, numpy as np, torch, torch.nn.functional as F
|
| 13 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 14 |
+
|
| 15 |
+
PREFIX_LEN, WINDOW_LEN, LR = 50, 100, 2e-5
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class BYOExtractor:
|
| 19 |
+
def __init__(self, model_path, device=None):
|
| 20 |
+
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 21 |
+
self.tok = AutoTokenizer.from_pretrained(model_path)
|
| 22 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 23 |
+
model_path, torch_dtype=torch.float32).to(self.device).eval()
|
| 24 |
+
|
| 25 |
+
def _cont_loss(self, ids):
|
| 26 |
+
logits = self.model(ids).logits
|
| 27 |
+
cl = logits[:, PREFIX_LEN - 1:WINDOW_LEN - 1, :]
|
| 28 |
+
ct = ids[:, PREFIX_LEN:WINDOW_LEN]
|
| 29 |
+
return -F.log_softmax(cl, -1).gather(2, ct.unsqueeze(-1)).squeeze(-1) # [1,50] per-token loss
|
| 30 |
+
|
| 31 |
+
def features(self, text):
|
| 32 |
+
enc = self.tok(text, return_tensors="pt", truncation=True, max_length=WINDOW_LEN)
|
| 33 |
+
ids = enc["input_ids"]
|
| 34 |
+
if ids.shape[1] < WINDOW_LEN:
|
| 35 |
+
raise ValueError(
|
| 36 |
+
f"Need at least {WINDOW_LEN} tokens (got {ids.shape[1]}). Paste a longer passage.")
|
| 37 |
+
return self.features_from_ids(ids[:, :WINDOW_LEN])
|
| 38 |
+
|
| 39 |
+
def features_from_ids(self, ids):
|
| 40 |
+
"""ids: LongTensor [1, WINDOW_LEN] of exact token ids (bypasses tokenization).
|
| 41 |
+
Used both by features(text) and by the offline validation against the pipeline."""
|
| 42 |
+
if not torch.is_tensor(ids):
|
| 43 |
+
ids = torch.tensor(ids, dtype=torch.long)
|
| 44 |
+
if ids.dim() == 1:
|
| 45 |
+
ids = ids.unsqueeze(0)
|
| 46 |
+
ids = ids[:, :WINDOW_LEN].long().to(self.device)
|
| 47 |
+
|
| 48 |
+
# zlib on the decoded suffix
|
| 49 |
+
suffix_txt = self.tok.decode(ids[0, PREFIX_LEN:WINDOW_LEN], skip_special_tokens=True)
|
| 50 |
+
zlib_entropy = float(len(zlib.compress(suffix_txt.encode("utf-8"))))
|
| 51 |
+
|
| 52 |
+
# loss-based features
|
| 53 |
+
with torch.no_grad():
|
| 54 |
+
ptl = self._cont_loss(ids)
|
| 55 |
+
base_ppl = float(torch.exp(ptl.mean()))
|
| 56 |
+
base_loss_variance = float(ptl.var(unbiased=True))
|
| 57 |
+
|
| 58 |
+
# gradient norm + one-step loss change
|
| 59 |
+
self.model.zero_grad(set_to_none=True)
|
| 60 |
+
loss_before = self._cont_loss(ids).mean()
|
| 61 |
+
loss_before.backward()
|
| 62 |
+
with torch.no_grad():
|
| 63 |
+
gsq = sum(float(p.grad.pow(2).sum()) for p in self.model.parameters() if p.grad is not None)
|
| 64 |
+
gradient_norm = gsq ** 0.5
|
| 65 |
+
for p in self.model.parameters(): # one SGD step
|
| 66 |
+
if p.grad is not None:
|
| 67 |
+
p.data.add_(p.grad, alpha=-LR)
|
| 68 |
+
loss_after = float(self._cont_loss(ids).mean())
|
| 69 |
+
for p in self.model.parameters(): # restore
|
| 70 |
+
if p.grad is not None:
|
| 71 |
+
p.data.add_(p.grad, alpha=LR)
|
| 72 |
+
lb = float(loss_before)
|
| 73 |
+
one_step_abs_change = lb - loss_after
|
| 74 |
+
one_step_rel_change = one_step_abs_change / lb if lb else 0.0
|
| 75 |
+
self.model.zero_grad(set_to_none=True)
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
"zlib_entropy": zlib_entropy, "base_ppl": base_ppl,
|
| 79 |
+
"base_loss_variance": base_loss_variance, "gradient_norm": gradient_norm,
|
| 80 |
+
"one_step_abs_change": one_step_abs_change, "one_step_rel_change": one_step_rel_change,
|
| 81 |
+
}
|
data/lookup.parquet
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4bcf4588bd84e05e61578ff3d7794f505ec2917c3a88f2bedfe93cd1094ab114
|
| 3 |
+
size 6934910
|
models/clf_code.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:40902845c23e3e82d781d33d9890ee9236bd5040a0456e2f95f9094a4b1803ff
|
| 3 |
+
size 856310
|
models/clf_fineweb.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:367d9be1ebaabc7f7e673a516d998e0dd73356a219344ef6bacc71f92a6fb820
|
| 3 |
+
size 447428
|
models/clf_owm.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:73df384220a1127674a6ce31bf35d03ef50746aaabaf94defd944ea0eb3334c7
|
| 3 |
+
size 766728
|
models/clf_pg19.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d75a46e460e8511bea39e25187373cdd50d0d790dd2b43b38504fe19e5676c0f
|
| 3 |
+
size 363618
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spaces
|
| 2 |
+
gradio>=4.44
|
| 3 |
+
transformers>=4.44
|
| 4 |
+
torch>=2.8
|
| 5 |
+
scikit-learn==1.7.2
|
| 6 |
+
pandas>=2.0
|
| 7 |
+
pyarrow>=14.0
|
| 8 |
+
numpy>=2.0
|
| 9 |
+
joblib>=1.5
|
train_classifiers.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train + save the memorization classifiers used by the demo's your-own-text mode.
|
| 2 |
+
|
| 3 |
+
Mirrors score_1m_nodup.py: train on the Run-1 deduplicated features with base-model-
|
| 4 |
+
reproduced positives removed, 1:3 rebalanced, on the six no-dup features. One classifier
|
| 5 |
+
per dataset (they transfer, per the cross-domain result, but we keep all four). Saves the
|
| 6 |
+
fitted StandardScaler + GradientBoostingClassifier per dataset via joblib.
|
| 7 |
+
"""
|
| 8 |
+
import os, numpy as np, pandas as pd, joblib
|
| 9 |
+
from sklearn.ensemble import GradientBoostingClassifier
|
| 10 |
+
from sklearn.preprocessing import StandardScaler
|
| 11 |
+
|
| 12 |
+
ROOT = "/projects/nulab/borkar.j/predicting_memorization"
|
| 13 |
+
OUT = f"{ROOT}/demo_app/models"
|
| 14 |
+
FEATS = ["zlib_entropy", "base_ppl", "base_loss_variance",
|
| 15 |
+
"gradient_norm", "one_step_abs_change", "one_step_rel_change"]
|
| 16 |
+
DATASETS = {"fineweb": "FineWeb", "pg19": "PG-19", "code": "The Stack", "owm": "OpenWebMath"}
|
| 17 |
+
os.makedirs(OUT, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
for d, disp in DATASETS.items():
|
| 20 |
+
df = pd.read_csv(f"{ROOT}/{d}_features.csv")
|
| 21 |
+
m = np.load(f"{ROOT}/base_extractability/base_memorized_{d}.npy")
|
| 22 |
+
pi = df.index[df.label == 1].tolist()
|
| 23 |
+
df["bm"] = 0
|
| 24 |
+
df.loc[pi, "bm"] = m.astype(int)
|
| 25 |
+
df = df[df.approx_dup_count == 1] # dedup
|
| 26 |
+
df = df[~((df.label == 1) & (df.bm == 1))] # remove base-reproduced positives
|
| 27 |
+
pos = df[df.label == 1]; neg = df[df.label == 0]; n = 3 * len(pos)
|
| 28 |
+
if len(neg) > n:
|
| 29 |
+
neg = neg.sample(n=n, random_state=42)
|
| 30 |
+
tr = pd.concat([pos, neg]).sample(frac=1, random_state=42).reset_index(drop=True)
|
| 31 |
+
X = np.nan_to_num(tr[FEATS].values, posinf=1e10, neginf=-1e10)
|
| 32 |
+
y = tr.label.values
|
| 33 |
+
sc = StandardScaler().fit(X)
|
| 34 |
+
clf = GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.1,
|
| 35 |
+
subsample=0.8, random_state=42).fit(sc.transform(X), y)
|
| 36 |
+
joblib.dump({"scaler": sc, "clf": clf, "feats": FEATS, "dataset": disp},
|
| 37 |
+
f"{OUT}/clf_{d}.joblib")
|
| 38 |
+
print(f"{disp:12s} trained on {len(tr)} rows ({int(y.sum())} pos); saved clf_{d}.joblib")
|
| 39 |
+
print("done")
|