jaydeepb commited on
Commit
1dbc4b5
·
verified ·
1 Parent(s): 4348d3a

Add memorization-prediction demo (ZeroGPU): explore held-out 1M + score-your-own-text

Browse files
README.md CHANGED
@@ -1,15 +1,59 @@
1
  ---
2
- title: Predicting Memorization
3
- emoji: 🚀
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.22.0
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")