subhash4face commited on
Commit
8c1102d
·
verified ·
1 Parent(s): 3db154f

Add cloud-vs-local MF preference model bundle

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. README.md +36 -20
  3. mf_cloud_vs_local.ipynb +583 -0
  4. mf_inference.py +203 -0
  5. mf_model_diagram.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ mf_model_diagram.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -19,35 +19,52 @@ Simple Matrix Factorization model that predicts **which model to use for a promp
19
  `cloud` or `local`. Trained on preference data of the form *(user, prompt, chosen)*
20
  where `chosen ∈ {cloud, local}`.
21
 
22
- > ⚠️ **Trained on synthetic demo data.** No real preference file was available in the
23
- > project, so a synthetic dataset (12,000 ratings, 400 users × 600 prompts, 8 topics with
24
- > latent cloud-affinity) was generated to validate the pipeline. Retrain on the real file
25
- > before production use — see `load_preference_data()` / `train_on()` in the notebook.
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  ## Model
28
 
29
  `r̂(u,i) = μ + b_u + b_i + ⟨p_u, q_i⟩` — user bias + prompt bias + dot product of latent
30
- factor vectors (k = 8), trained with binary cross-entropy + L2 regularization via minibatch
31
- SGD (NumPy), early-stopped on validation AUC.
32
 
33
  Decision rule: `p(cloud) = σ(r̂) ≥ 0.5 → cloud`, else `local`.
34
 
35
- ## Metrics (held-out test, n = 1,200)
36
 
37
  | metric | value | baseline (always cloud) |
38
  |-----------|---------|--------------------------|
39
  | accuracy | 0.677 | 0.483 |
40
  | AUC | 0.734 | 0.500 |
41
 
42
- The model recovers the latent topic structure: privacy-sensitive and latency-sensitive
43
- prompts are routed to `local`; complex-reasoning and long-context prompts to `cloud`.
 
44
 
45
- ## Files
46
 
 
 
 
 
47
  - `config.json` — hyperparameters + metrics
48
  - `mf_params.npz` — `mu, bu, bi, P, Q, user_ids, prompt_ids` (weights + vocab)
49
  - `test_predictions.csv` — per-pair predictions on the held-out test set
50
- - `mf_inference.py` — sample inference script (also available at repo root)
51
 
52
  ## Usage
53
 
@@ -61,18 +78,17 @@ python mf_inference.py --model_dir mf_bundle --user_id U0007 \
61
  # rank all known prompts for a user
62
  python mf_inference.py --model_dir mf_bundle --user_id U0007 --top_k 5 \
63
  --csv preference_data_synthetic.csv
64
-
65
- # push this bundle to the Hub
66
- python mf_inference.py --model_dir mf_bundle --user_id U0007 --top_k 3 \
67
- --csv preference_data_synthetic.csv --push_to_hub --repo_id your-org/cloud-local-mf
68
  ```
69
 
 
 
 
70
  ## Limitations
71
 
72
- - **Cold start**: prompts not seen in training have no `q_i` embedding — the model cannot
73
- score brand-new prompt text. Extend with a text encoder (e.g. embed prompt text into the
74
- latent space) for production cold start.
75
- - **Synthetic data**: metrics above are on synthetic ratings; expect different numbers on
76
- real preference logs.
77
  - No leakage checks were needed for synthetic data; run a near-duplicate check between
78
  train/val/test when real data arrives.
 
19
  `cloud` or `local`. Trained on preference data of the form *(user, prompt, chosen)*
20
  where `chosen ∈ {cloud, local}`.
21
 
22
+ ![MF pipeline diagram](mf_model_diagram.png)
23
+
24
+ ## 🚨 Use real preference data for better results
25
+
26
+ This model is **trained on synthetic demo data** (12,000 generated ratings) — it exists
27
+ to validate the pipeline and the tooling, **not** to be the final product. For a model
28
+ that actually routes your traffic, retrain on your **real preference logs**:
29
+
30
+ 1. Export a CSV with at least three columns: `user_id`, `prompt`, `chosen`
31
+ (values `cloud` or `local`).
32
+ 2. Open `mf_cloud_vs_local.ipynb` in Jupyter and run the last code cell:
33
+ `train_on(load_preference_data("your_real_data.csv"))`.
34
+ 3. The whole pipeline — EDA, stratified train/val/test split, MF training,
35
+ evaluation, bundle export — runs unchanged on your file.
36
+
37
+ Real data gives you: trustworthy metrics, per-topic routing insights from your own
38
+ prompts, and a cleaner cold-start story (see Limitations).
39
 
40
  ## Model
41
 
42
  `r̂(u,i) = μ + b_u + b_i + ⟨p_u, q_i⟩` — user bias + prompt bias + dot product of latent
43
+ factor vectors (k = 8), trained with binary cross-entropy + L2 regularization via
44
+ minibatch SGD (NumPy), early-stopped on validation AUC.
45
 
46
  Decision rule: `p(cloud) = σ(r̂) ≥ 0.5 → cloud`, else `local`.
47
 
48
+ ## Metrics (held-out test, n = 1,200 — synthetic data)
49
 
50
  | metric | value | baseline (always cloud) |
51
  |-----------|---------|--------------------------|
52
  | accuracy | 0.677 | 0.483 |
53
  | AUC | 0.734 | 0.500 |
54
 
55
+ The model recovers the latent topic structure: privacy-sensitive and
56
+ latency-sensitive prompts are routed to `local`; complex-reasoning and long-context
57
+ prompts to `cloud`.
58
 
59
+ ## Files in this repo
60
 
61
+ - `README.md` — this model card
62
+ - `mf_cloud_vs_local.ipynb` — full source notebook (data gen, EDA, training, eval, bundle, diagram, inference demo)
63
+ - `mf_inference.py` — sample inference script (CLI, loads the bundle with `SimpleMF.from_pretrained`)
64
+ - `mf_model_diagram.png` — training + inference pipeline diagram
65
  - `config.json` — hyperparameters + metrics
66
  - `mf_params.npz` — `mu, bu, bi, P, Q, user_ids, prompt_ids` (weights + vocab)
67
  - `test_predictions.csv` — per-pair predictions on the held-out test set
 
68
 
69
  ## Usage
70
 
 
78
  # rank all known prompts for a user
79
  python mf_inference.py --model_dir mf_bundle --user_id U0007 --top_k 5 \
80
  --csv preference_data_synthetic.csv
 
 
 
 
81
  ```
82
 
83
+ Clone this repo and run `mf_inference.py` locally, or open `mf_cloud_vs_local.ipynb`
84
+ to retrain on your own preference data.
85
+
86
  ## Limitations
87
 
88
+ - **Cold start**: prompts not seen in training have no `q_i` embedding — the model
89
+ cannot score brand-new prompt text. Extend with a text encoder (e.g. embed prompt
90
+ text into the latent space) for production cold start.
91
+ - **Synthetic data**: metrics above are on synthetic ratings; expect different numbers
92
+ on real preference logs — see "Use real preference data" above.
93
  - No leakage checks were needed for synthetic data; run a near-duplicate check between
94
  train/val/test when real data arrives.
mf_cloud_vs_local.ipynb ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "eef2e5c1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# MF preference model: cloud vs local\n",
9
+ "\n",
10
+ "**Goal** — Given preference data of the form *(user, prompt, chosen)* where `chosen ∈ {cloud, local}`, train a **simple Matrix Factorization (MF)** model that predicts which model to use for a prompt.\n",
11
+ "\n",
12
+ "**Data** — No real preference file is in the project yet, so this notebook generates a **synthetic preference dataset** that mirrors the real schema (see cell 2). A drop-in loader for a real CSV (`user_id, prompt, chosen`) is provided in the last cell.\n",
13
+ "\n",
14
+ "**Approach**\n",
15
+ "1. Build a sparse `users × prompts` binary matrix (`1` = cloud preferred, `0` = local preferred).\n",
16
+ "2. Factorize it: `r̂(u,i) = μ + b_u + b_i + p_u·q_i` with a latent dimension `k`, trained with binary cross-entropy + L2 regularization via SGD.\n",
17
+ "3. Decision rule: `r̂ > 0.5 → cloud`, else `local`.\n",
18
+ "4. Evaluate accuracy / AUC on a held-out test set."
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "code",
23
+ "execution_count": null,
24
+ "id": "b503ff3b",
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "import numpy as np\n",
29
+ "import pandas as pd\n",
30
+ "\n",
31
+ "# ============================================================\n",
32
+ "# 1) SYNTHETIC PREFERENCE DATA (stand-in for the real file)\n",
33
+ "# ============================================================\n",
34
+ "# Real schema this mimics: user_id | prompt_id | topic | prompt_text | chosen | label\n",
35
+ "# label: 1 = cloud preferred, 0 = local preferred\n",
36
+ "#\n",
37
+ "# Each topic has a \"cloud affinity\" — the probability that a random user\n",
38
+ "# prefers the cloud model for a prompt in that topic. Users add a personal\n",
39
+ "# offset, and there is rating noise, so the preference structure is latent\n",
40
+ "# and must be recovered by the model rather than read off the text.\n",
41
+ "\n",
42
+ "TOPIC_AFFINITY = {\n",
43
+ " \"complex_reasoning\": 0.78, # heavy compute -> cloud\n",
44
+ " \"long_context\": 0.72,\n",
45
+ " \"image_generation\": 0.64,\n",
46
+ " \"code_generation\": 0.58,\n",
47
+ " \"creative_writing\": 0.44,\n",
48
+ " \"simple_qa\": 0.35,\n",
49
+ " \"latency_sensitive\": 0.20, # needs to be fast -> local\n",
50
+ " \"privacy_sensitive\": 0.12, # data must not leave the machine -> local\n",
51
+ "}\n",
52
+ "\n",
53
+ "TEMPLATES = {\n",
54
+ " \"complex_reasoning\": [\n",
55
+ " \"Prove or refute: every {obj} admits a canonical {prop} decomposition\",\n",
56
+ " \"Find the flaw in this 40-step proof about {prop} and repair it\",\n",
57
+ " \"Derive the closed form for {obj} and verify each step\",\n",
58
+ " ],\n",
59
+ " \"long_context\": [\n",
60
+ " \"Summarize the key arguments across all 200 pages of {doc}\",\n",
61
+ " \"Track every character and plot thread across the {doc} saga\",\n",
62
+ " \"Answer strictly from the full 2-hour meeting transcript on {doc}\",\n",
63
+ " ],\n",
64
+ " \"image_generation\": [\n",
65
+ " \"Generate a photorealistic image of {obj} at golden hour\",\n",
66
+ " \"Create a 4k illustration of {obj} with dramatic rim lighting\",\n",
67
+ " \"Produce a logo mockup for {doc}, transparent background\",\n",
68
+ " ],\n",
69
+ " \"code_generation\": [\n",
70
+ " \"Write a production-ready implementation of {prop} in Python\",\n",
71
+ " \"Refactor this legacy module into modern C++20: {doc}\",\n",
72
+ " \"Generate unit tests and API docs for the {prop} library\",\n",
73
+ " ],\n",
74
+ " \"creative_writing\": [\n",
75
+ " \"Write a haiku about {obj}\",\n",
76
+ " \"Draft a two-paragraph product blurb for {doc}\",\n",
77
+ " \"Compose a short story opening about {obj}\",\n",
78
+ " ],\n",
79
+ " \"simple_qa\": [\n",
80
+ " \"What is the capital of {doc}?\",\n",
81
+ " \"Convert 150 miles to kilometers\",\n",
82
+ " \"Explain {prop} in one sentence\",\n",
83
+ " ],\n",
84
+ " \"latency_sensitive\": [\n",
85
+ " \"Autocomplete this sentence in real time: {obj}\",\n",
86
+ " \"Give an instant short answer: {prop}?\",\n",
87
+ " \"Rephrase this snippet while I type: {obj}\",\n",
88
+ " ],\n",
89
+ " \"privacy_sensitive\": [\n",
90
+ " \"Summarize my medical records regarding {prop}\",\n",
91
+ " \"Draft an email discussing my salary at {doc}\",\n",
92
+ " \"Redact PII from this legal document about {doc}\",\n",
93
+ " ],\n",
94
+ "}\n",
95
+ "\n",
96
+ "FILLERS = {\n",
97
+ " \"obj\": [\"quantum error correction\", \"a flamenco guitarist\", \"sourdough bread\",\n",
98
+ " \"a sleepy cat\", \"a rusting cargo ship\", \"a chess endgame\", \"a thunderstorm\"],\n",
99
+ " \"prop\": [\"topological sorting\", \"Bayesian inference\", \"memory-mapped I/O\",\n",
100
+ " \"backpropagation\", \"deadlock avoidance\", \"tokenization\", \"garbage collection\"],\n",
101
+ " \"doc\": [\"Q4 earnings report\", \"clinical trial protocol\", \"migration guide\",\n",
102
+ " \"franchise lore wiki\", \"board meeting minutes\", \"product spec\"],\n",
103
+ "}\n",
104
+ "\n",
105
+ "rng = np.random.default_rng(42)\n",
106
+ "N_USERS, N_PROMPTS_PER_TOPIC, RATINGS_PER_USER = 400, 75, 30\n",
107
+ "\n",
108
+ "# --- build the prompt catalog (8 topics x 75 prompts) ---\n",
109
+ "rows = []\n",
110
+ "prompt_id = 0\n",
111
+ "for topic, affinity in TOPIC_AFFINITY.items():\n",
112
+ " tpl = TEMPLATES[topic]\n",
113
+ " for _ in range(N_PROMPTS_PER_TOPIC):\n",
114
+ " text = rng.choice(tpl).format(**{k: rng.choice(v) for k, v in FILLERS.items()})\n",
115
+ " rows.append({\"prompt_id\": f\"P{prompt_id:04d}\", \"topic\": topic,\n",
116
+ " \"affinity\": affinity, \"prompt_text\": text})\n",
117
+ " prompt_id += 1\n",
118
+ "prompts = pd.DataFrame(rows)\n",
119
+ "\n",
120
+ "# --- each user rates a random subset of prompts ---\n",
121
+ "user_offsets = rng.normal(0.0, 0.15, size=N_USERS) # personal cloud-bias\n",
122
+ "pref_rows = []\n",
123
+ "for u in range(N_USERS):\n",
124
+ " uid = f\"U{u:04d}\"\n",
125
+ " picks = rng.choice(prompts.index, size=RATINGS_PER_USER, replace=False)\n",
126
+ " for pi in picks:\n",
127
+ " p_cloud = 0.5 + (prompts.loc[pi, \"affinity\"] - 0.5) + user_offsets[u] + rng.normal(0, 0.12)\n",
128
+ " p_cloud = float(np.clip(p_cloud, 0.02, 0.98))\n",
129
+ " label = int(rng.binomial(1, p_cloud))\n",
130
+ " pref_rows.append({\"user_id\": uid, \"prompt_id\": prompts.loc[pi, \"prompt_id\"],\n",
131
+ " \"topic\": prompts.loc[pi, \"topic\"],\n",
132
+ " \"prompt_text\": prompts.loc[pi, \"prompt_text\"],\n",
133
+ " \"chosen\": \"cloud\" if label else \"local\", \"label\": label})\n",
134
+ "\n",
135
+ "pref = pd.DataFrame(pref_rows)\n",
136
+ "pref.to_csv(\"/home/user/preference_data_synthetic.csv\", index=False)\n",
137
+ "print(f\"preference rows : {len(pref):,}\")\n",
138
+ "print(f\"users : {pref.user_id.nunique():,}\")\n",
139
+ "print(f\"prompts : {pref.prompt_id.nunique():,}\")\n",
140
+ "print(f\"cloud share : {pref.label.mean():.3f}\")\n",
141
+ "print(\"\\nFirst 5 rows:\")\n",
142
+ "print(pref.head().to_string(index=False))"
143
+ ]
144
+ },
145
+ {
146
+ "cell_type": "code",
147
+ "execution_count": null,
148
+ "id": "6978329b",
149
+ "metadata": {},
150
+ "outputs": [],
151
+ "source": [
152
+ "import numpy as np\n",
153
+ "import pandas as pd\n",
154
+ "from sklearn.model_selection import train_test_split\n",
155
+ "from sklearn.metrics import roc_auc_score\n",
156
+ "\n",
157
+ "pref = pd.read_csv(\"/home/user/preference_data_synthetic.csv\")\n",
158
+ "\n",
159
+ "# ---------------- EDA ----------------\n",
160
+ "print(\"=== shape / nulls / balance ===\")\n",
161
+ "print(f\"rows: {len(pref):,} | nulls: {pref.isnull().sum().sum()} | duplicate (user,prompt): \"\n",
162
+ " f\"{pref.duplicated(['user_id','prompt_id']).sum()}\")\n",
163
+ "\n",
164
+ "print(\"\\n=== cloud share by topic (should track the injected affinity) ===\")\n",
165
+ "by_topic = pref.groupby(\"topic\")[\"label\"].agg([\"mean\", \"count\"]).rename(\n",
166
+ " columns={\"mean\": \"cloud_rate\", \"count\": \"n\"})\n",
167
+ "print(by_topic.round(3).to_string())\n",
168
+ "\n",
169
+ "print(\"\\n=== density of the users x prompts matrix ===\")\n",
170
+ "n_users, n_prompts = pref.user_id.nunique(), pref.prompt_id.nunique()\n",
171
+ "print(f\"matrix: {n_users} x {n_prompts} = {n_users*n_prompts:,} cells, \"\n",
172
+ " f\"{len(pref):,} observed -> density {len(pref)/(n_users*n_prompts):.4%}\")\n",
173
+ "print(f\"ratings per user: mean {pref.groupby('user_id').size().mean():.0f}, \"\n",
174
+ " f\"per prompt: mean {pref.groupby('prompt_id').size().mean():.0f}\")\n",
175
+ "\n",
176
+ "# ---------------- encode + split ----------------\n",
177
+ "user_ids = sorted(pref.user_id.unique())\n",
178
+ "prompt_ids = sorted(pref.prompt_id.unique())\n",
179
+ "uidx = {u: i for i, u in enumerate(user_ids)}\n",
180
+ "pidx = {p: i for i, p in enumerate(prompt_ids)}\n",
181
+ "\n",
182
+ "pref[\"u\"] = pref.user_id.map(uidx)\n",
183
+ "pref[\"i\"] = pref.prompt_id.map(pidx)\n",
184
+ "\n",
185
+ "# random row split (80/10/10), stratified by label\n",
186
+ "train, rest = train_test_split(pref, test_size=0.2, random_state=0, stratify=pref[\"label\"])\n",
187
+ "val, test = train_test_split(rest, test_size=0.5, random_state=0, stratify=rest[\"label\"])\n",
188
+ "\n",
189
+ "print(\"\\n=== split sizes ===\")\n",
190
+ "for name, df in [(\"train\", train), (\"val\", val), (\"test\", test)]:\n",
191
+ " print(f\"{name:5s}: {len(df):,} rows | cloud rate {df.label.mean():.3f} | \"\n",
192
+ " f\"warm users {df.u.isin(train.u).mean():.2%} (overlap w/ train)\")\n",
193
+ "\n",
194
+ "train.to_csv(\"/home/user/train.csv\", index=False)\n",
195
+ "val.to_csv(\"/home/user/val.csv\", index=False)\n",
196
+ "test.to_csv(\"/home/user/test.csv\", index=False)"
197
+ ]
198
+ },
199
+ {
200
+ "cell_type": "code",
201
+ "execution_count": null,
202
+ "id": "4eae65c1",
203
+ "metadata": {},
204
+ "outputs": [],
205
+ "source": [
206
+ "import numpy as np\n",
207
+ "from sklearn.metrics import roc_auc_score\n",
208
+ "\n",
209
+ "# ============================================================\n",
210
+ "# 2) SIMPLE MATRIX FACTORIZATION (NumPy SGD, BCE + L2)\n",
211
+ "# r̂(u,i) = μ + b_u + b_i + <p_u, q_i>\n",
212
+ "# ============================================================\n",
213
+ "\n",
214
+ "def sigmoid(z):\n",
215
+ " return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))\n",
216
+ "\n",
217
+ "\n",
218
+ "class SimpleMF:\n",
219
+ " \"\"\"Binary-preference MF: predicts P(cloud preferred | user, prompt).\"\"\"\n",
220
+ "\n",
221
+ " def __init__(self, n_users, n_prompts, k=8, lr=0.05, reg=0.05,\n",
222
+ " epochs=30, batch=512, seed=0):\n",
223
+ " self.k, self.lr, self.reg, self.epochs, self.batch = k, lr, reg, epochs, batch\n",
224
+ " rng = np.random.default_rng(seed)\n",
225
+ " self.mu = 0.0\n",
226
+ " self.bu = np.zeros(n_users)\n",
227
+ " self.bi = np.zeros(n_prompts)\n",
228
+ " self.P = rng.normal(0, 0.1, (n_users, k)) # user factors\n",
229
+ " self.Q = rng.normal(0, 0.1, (n_prompts, k)) # prompt factors\n",
230
+ " self.history = []\n",
231
+ "\n",
232
+ " def predict(self, u, i):\n",
233
+ " r = self.mu + self.bu[u] + self.bi[i] + (self.P[u] * self.Q[i]).sum(1)\n",
234
+ " return sigmoid(r)\n",
235
+ "\n",
236
+ " def fit(self, ui, y, val_ui=None, val_y=None):\n",
237
+ " \"\"\"ui: (N,2) [user_idx, prompt_idx], y: (N,) binary labels.\"\"\"\n",
238
+ " self.mu = y.mean()\n",
239
+ " n = len(y)\n",
240
+ " best = None\n",
241
+ " for ep in range(self.epochs):\n",
242
+ " perm = np.random.default_rng(ep).permutation(n)\n",
243
+ " losses = []\n",
244
+ " for s in range(0, n, self.batch):\n",
245
+ " idx = perm[s:s + self.batch]\n",
246
+ " u, i = ui[idx, 0], ui[idx, 1]\n",
247
+ " r = self.mu + self.bu[u] + self.bi[i] + (self.P[u] * self.Q[i]).sum(1)\n",
248
+ " d = sigmoid(r) - y[idx] # grad wrt r of BCE\n",
249
+ " loss = float((np.logaddexp(0, r) - y[idx] * r).mean())\n",
250
+ " losses.append(loss)\n",
251
+ " # updates with L2 regularization\n",
252
+ " self.bu[u] -= self.lr * (d + self.reg * self.bu[u])\n",
253
+ " self.bi[i] -= self.lr * (d + self.reg * self.bi[i])\n",
254
+ " self.P[u] -= self.lr * (d[:, None] * self.Q[i] + self.reg * self.P[u])\n",
255
+ " self.Q[i] -= self.lr * (d[:, None] * self.P[u] + self.reg * self.Q[i])\n",
256
+ " # track val AUC, keep best params (simple early stop)\n",
257
+ " val_auc = roc_auc_score(val_y, self.predict(val_ui[:, 0], val_ui[:, 1])) \\\n",
258
+ " if val_ui is not None else float(\"nan\")\n",
259
+ " self.history.append((ep, np.mean(losses), val_auc))\n",
260
+ " if best is None or val_auc > best[0]:\n",
261
+ " best = (val_auc, self.bu.copy(), self.bi.copy(), self.P.copy(), self.Q.copy())\n",
262
+ " print(f\"epoch {ep+1:2d}/{self.epochs} | train loss {np.mean(losses):.4f} | val AUC {val_auc:.4f}\")\n",
263
+ " self.bu, self.bi, self.P, self.Q = best[1], best[2], best[3], best[4]\n",
264
+ " print(f\"\\nbest val AUC: {best[0]:.4f} (params restored)\")\n",
265
+ "\n",
266
+ "\n",
267
+ "train = pd.read_csv(\"/home/user/train.csv\")\n",
268
+ "val = pd.read_csv(\"/home/user/val.csv\")\n",
269
+ "\n",
270
+ "model = SimpleMF(n_users=train.u.max() + 1, n_prompts=train.i.max() + 1,\n",
271
+ " k=8, lr=0.05, reg=0.05, epochs=30, seed=0)\n",
272
+ "model.fit(train[[\"u\", \"i\"]].to_numpy(), train[\"label\"].to_numpy(),\n",
273
+ " val[[\"u\", \"i\"]].to_numpy(), val[\"label\"].to_numpy())"
274
+ ]
275
+ },
276
+ {
277
+ "cell_type": "code",
278
+ "execution_count": null,
279
+ "id": "bd02d51b",
280
+ "metadata": {},
281
+ "outputs": [],
282
+ "source": [
283
+ "import numpy as np\n",
284
+ "import pandas as pd\n",
285
+ "from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix\n",
286
+ "\n",
287
+ "test = pd.read_csv(\"/home/user/test.csv\")\n",
288
+ "pred = model.predict(test[\"u\"].to_numpy(), test[\"i\"].to_numpy())\n",
289
+ "test[\"p_cloud\"] = pred\n",
290
+ "test[\"pred_choice\"] = np.where(pred >= 0.5, \"cloud\", \"local\")\n",
291
+ "\n",
292
+ "acc = accuracy_score(test[\"label\"], (pred >= 0.5).astype(int))\n",
293
+ "auc = roc_auc_score(test[\"label\"], pred)\n",
294
+ "tn, fp, fn, tp = confusion_matrix(test[\"label\"], (pred >= 0.5).astype(int)).ravel()\n",
295
+ "\n",
296
+ "print(f\"=== TEST SET (n={len(test):,}) ===\")\n",
297
+ "print(f\"accuracy : {acc:.4f} (baseline: always cloud = {test.label.mean():.4f})\")\n",
298
+ "print(f\"AUC : {auc:.4f}\")\n",
299
+ "print(f\"confusion : TP(cloud->cloud)={tp:4d} FN={fn:4d} | FP={fp:4d} TN(local->local)={tn:4d}\")\n",
300
+ "\n",
301
+ "print(\"\\n=== per-topic accuracy (does the model recover the latent topic structure?) ===\")\n",
302
+ "t = test.groupby(\"topic\").apply(\n",
303
+ " lambda d: pd.Series({\"acc\": accuracy_score(d.label, d.p_cloud >= 0.5),\n",
304
+ " \"mean_p_cloud\": d.p_cloud.mean(), \"n\": len(d)}),\n",
305
+ " include_groups=False).round(3)\n",
306
+ "print(t.to_string())\n",
307
+ "\n",
308
+ "print(\"\\n=== strongest signals: prompts the model is most / least sure are 'cloud' ===\")\n",
309
+ "g = test.groupby(\"prompt_id\")[\"p_cloud\"].mean()\n",
310
+ "top = g.nlargest(5).index\n",
311
+ "bot = g.nsmallest(5).index\n",
312
+ "show = test[test.prompt_id.isin(top) | test.prompt_id.isin(bot)].drop_duplicates(\"prompt_id\")\n",
313
+ "show = show.assign(mean_p_cloud=show.prompt_id.map(g)) \\\n",
314
+ " .sort_values(\"mean_p_cloud\", ascending=False) \\\n",
315
+ " [[\"mean_p_cloud\", \"topic\", \"prompt_text\"]]\n",
316
+ "print(show.to_string(index=False, max_colwidth=70))"
317
+ ]
318
+ },
319
+ {
320
+ "cell_type": "code",
321
+ "execution_count": null,
322
+ "id": "24d2f4db",
323
+ "metadata": {},
324
+ "outputs": [],
325
+ "source": [
326
+ "import os\n",
327
+ "import json\n",
328
+ "import numpy as np\n",
329
+ "import pandas as pd\n",
330
+ "\n",
331
+ "# ============================================================\n",
332
+ "# 3) SAVE MODEL BUNDLE + DROP-IN LOADER FOR REAL DATA\n",
333
+ "# ============================================================\n",
334
+ "os.makedirs(\"/home/user/mf_bundle\", exist_ok=True)\n",
335
+ "\n",
336
+ "np.savez(\"/home/user/mf_bundle/mf_params.npz\",\n",
337
+ " mu=model.mu, bu=model.bu, bi=model.bi, P=model.P, Q=model.Q,\n",
338
+ " user_ids=np.array(user_ids), prompt_ids=np.array(prompt_ids))\n",
339
+ "\n",
340
+ "prompt_topic = prompts.set_index(\"prompt_id\")[\"topic\"].to_dict()\n",
341
+ "config = {\n",
342
+ " \"model\": \"SimpleMF (binary preference, BCE + L2, early stop on val AUC)\",\n",
343
+ " \"hyperparams\": {\"k\": model.k, \"lr\": model.lr, \"reg\": model.reg,\n",
344
+ " \"epochs\": model.epochs, \"batch\": model.batch},\n",
345
+ " \"train\": {\"rows\": int(len(train)), \"users\": n_users, \"prompts\": n_prompts},\n",
346
+ " \"test_metrics\": {\"accuracy\": round(acc, 4), \"auc\": round(auc, 4),\n",
347
+ " \"baseline_accuracy\": round(float(test.label.mean()), 4)},\n",
348
+ " \"decision_rule\": \"p_cloud >= 0.5 -> 'cloud', else 'local'\",\n",
349
+ " \"note\": \"Trained on synthetic demo data; retrain with load_preference_data() on the real file.\",\n",
350
+ "}\n",
351
+ "json.dump(config, open(\"/home/user/mf_bundle/config.json\", \"w\"), indent=2)\n",
352
+ "test[[\"user_id\", \"prompt_id\", \"topic\", \"prompt_text\", \"label\", \"p_cloud\", \"pred_choice\"]] \\\n",
353
+ " .to_csv(\"/home/user/mf_bundle/test_predictions.csv\", index=False)\n",
354
+ "print(\"bundle saved to /home/user/mf_bundle/ (mf_params.npz, config.json, test_predictions.csv)\")\n",
355
+ "\n",
356
+ "\n",
357
+ "# ------------------------------------------------------------\n",
358
+ "# Drop-in entry point for REAL preference data\n",
359
+ "# Expected columns: user_id, prompt, chosen (\"cloud\" | \"local\")\n",
360
+ "# (prompt_text / prompt_id are also accepted and merged.)\n",
361
+ "# ------------------------------------------------------------\n",
362
+ "def load_preference_data(path, seed=0):\n",
363
+ " df = pd.read_csv(path)\n",
364
+ " df.columns = [c.strip().lower() for c in df.columns]\n",
365
+ " assert \"user_id\" in df.columns and \"chosen\" in df.columns, \\\n",
366
+ " \"real data must have columns: user_id, prompt, chosen\"\n",
367
+ " if \"prompt\" not in df.columns: # tolerate prompt_id/prompt_text\n",
368
+ " df[\"prompt\"] = df.get(\"prompt_text\", df[\"prompt_id\"].astype(str))\n",
369
+ " df[\"label\"] = (df[\"chosen\"].str.lower() == \"cloud\").astype(int)\n",
370
+ " df = df.drop_duplicates([\"user_id\", \"prompt\"])\n",
371
+ " df[\"u\"] = df.user_id.map({u: i for i, u in enumerate(sorted(df.user_id.unique()))})\n",
372
+ " df[\"i\"] = df.prompt.map({p: i for i, p in enumerate(sorted(df.prompt.unique()))})\n",
373
+ " return df\n",
374
+ "\n",
375
+ "def train_on(df, val_frac=0.1, **mf_kwargs):\n",
376
+ " from sklearn.model_selection import train_test_split\n",
377
+ " tr, va = train_test_split(df, test_size=val_frac, random_state=0, stratify=df[\"label\"])\n",
378
+ " m = SimpleMF(n_users=df.u.max() + 1, n_prompts=df.i.max() + 1, **mf_kwargs)\n",
379
+ " m.fit(tr[[\"u\", \"i\"]].to_numpy(), tr[\"label\"].to_numpy(),\n",
380
+ " va[[\"u\", \"i\"]].to_numpy(), va[\"label\"].to_numpy())\n",
381
+ " return m\n",
382
+ "\n",
383
+ "print(\"\\nDemo — retrain through the real-data entry point (synthetic CSV -> real schema):\")\n",
384
+ "real_schema = pref[[\"user_id\", \"prompt_text\", \"chosen\"]].rename(columns={\"prompt_text\": \"prompt\"})\n",
385
+ "real_schema.to_csv(\"/home/user/preference_real_schema.csv\", index=False)\n",
386
+ "demo = train_on(load_preference_data(\"/home/user/preference_real_schema.csv\"), epochs=12)\n",
387
+ "print(\" -> entry point OK; demo model val AUC printed above (main model metrics in config.json)\")\n",
388
+ "\n",
389
+ "print(\"\\nDemo — model choices for a few prompts of user U0007 (trained model):\")\n",
390
+ "for pid in [\"P0001\", \"P0400\", \"P0572\", \"P0260\"]:\n",
391
+ " p = model.predict(np.array([uidx[\"U0007\"]]), np.array([pidx[pid]]))[0]\n",
392
+ " print(f\" {pid} ({prompt_topic[pid]:<18s}) p(cloud)={p:.3f} -> {'cloud' if p >= 0.5 else 'local'}\")"
393
+ ]
394
+ },
395
+ {
396
+ "cell_type": "code",
397
+ "execution_count": null,
398
+ "id": "2951eca0",
399
+ "metadata": {},
400
+ "outputs": [],
401
+ "source": [
402
+ "import matplotlib\n",
403
+ "import matplotlib.pyplot as plt\n",
404
+ "from matplotlib.patches import FancyBboxPatch, Rectangle, FancyArrowPatch\n",
405
+ "import numpy as np\n",
406
+ "\n",
407
+ "matplotlib.rcParams.update({\"font.size\": 10, \"figure.facecolor\": \"white\"})\n",
408
+ "CLOUD, LOCAL, ACCENT, LIGHT = \"#2563eb\", \"#f59e0b\", \"#111827\", \"#eef2ff\"\n",
409
+ "GREEN = \"#16a34a\"\n",
410
+ "\n",
411
+ "def box(ax, x, y, w, h, text, fc=\"white\", ec=ACCENT, fs=9.5, bold=False, color=None):\n",
412
+ " ax.add_patch(FancyBboxPatch((x, y), w, h,\n",
413
+ " boxstyle=\"round,pad=0.02,rounding_size=0.06\", fc=fc, ec=ec, lw=1.6))\n",
414
+ " ax.text(x + w / 2, y + h / 2, text, ha=\"center\", va=\"center\", fontsize=fs,\n",
415
+ " fontweight=\"bold\" if bold else \"normal\", color=color or ACCENT)\n",
416
+ "\n",
417
+ "def arrow(ax, x1, y1, x2, y2, color=ACCENT, lw=1.6):\n",
418
+ " ax.add_patch(FancyArrowPatch((x1, y1), (x2, y2), arrowstyle=\"-|>\",\n",
419
+ " mutation_scale=14, color=color, lw=lw, shrinkA=0, shrinkB=0))\n",
420
+ "\n",
421
+ "fig, (axA, axB) = plt.subplots(1, 2, figsize=(14.5, 6.0))\n",
422
+ "for ax in (axA, axB):\n",
423
+ " ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis(\"off\")\n",
424
+ "\n",
425
+ "# ---------------- Panel A: training ----------------\n",
426
+ "axA.set_title(\"Training — factorize the users x prompts preference matrix\",\n",
427
+ " fontsize=12, fontweight=\"bold\", pad=12)\n",
428
+ "rng = np.random.default_rng(7)\n",
429
+ "for r in range(7):\n",
430
+ " for c in range(7):\n",
431
+ " if rng.random() < 0.28:\n",
432
+ " axA.add_patch(Rectangle((0.55 + c * 0.36, 8.35 - r * 0.36), 0.32, 0.32,\n",
433
+ " fc=CLOUD if rng.random() < 0.5 else LOCAL, ec=\"white\", lw=0.5))\n",
434
+ "axA.text(1.8, 9.5, \"observed preferences\", ha=\"center\", fontsize=9.5, fontweight=\"bold\")\n",
435
+ "axA.text(1.8, 5.55, \"12,000 ratings\\n5% dense\", ha=\"center\", fontsize=8.5, color=\"#555\")\n",
436
+ "\n",
437
+ "box(axA, 4.7, 5.8, 2.9, 2.2, \"Matrix\\nFactorization\\nmin sum BCE + lambda||theta||^2\\nSGD · k = 8\",\n",
438
+ " fc=LIGHT, fs=9, bold=True)\n",
439
+ "arrow(axA, 3.15, 6.9, 4.6, 6.9)\n",
440
+ "\n",
441
+ "for r in range(4): # P: users x k\n",
442
+ " for c in range(3):\n",
443
+ " axA.add_patch(Rectangle((8.35 + c * 0.42, 8.15 - r * 0.42), 0.38, 0.38,\n",
444
+ " fc=\"#dbeafe\", ec=\"#93c5fd\", lw=0.5))\n",
445
+ "axA.text(8.9, 6.2, \"P\\nusers x k\", ha=\"center\", fontsize=8.5, color=\"#1e40af\")\n",
446
+ "for r in range(4): # Q: prompts x k\n",
447
+ " for c in range(3):\n",
448
+ " axA.add_patch(Rectangle((8.35 + c * 0.42, 5.3 - r * 0.42), 0.38, 0.38,\n",
449
+ " fc=\"#fef3c7\", ec=\"#fcd34d\", lw=0.5))\n",
450
+ "axA.text(8.9, 3.4, \"Q\\nprompts x k\", ha=\"center\", fontsize=8.5, color=\"#b45309\")\n",
451
+ "arrow(axA, 7.6, 7.6, 8.3, 7.6)\n",
452
+ "arrow(axA, 7.6, 6.2, 8.3, 6.2)\n",
453
+ "\n",
454
+ "axA.text(5.0, 2.6, r\"$\\hat{r}(u,i) = \\mu + b_u + b_i + \\langle p_u,\\ q_i \\rangle$\",\n",
455
+ " ha=\"center\", fontsize=13, color=ACCENT)\n",
456
+ "axA.text(5.0, 1.7, \"binary label y: 1 = cloud preferred, 0 = local preferred\",\n",
457
+ " ha=\"center\", fontsize=9, color=\"#555\")\n",
458
+ "\n",
459
+ "# ---------------- Panel B: inference ----------------\n",
460
+ "axB.set_title(\"Inference — score a (user, prompt) pair from the saved bundle\",\n",
461
+ " fontsize=12, fontweight=\"bold\", pad=12)\n",
462
+ "box(axB, 3.4, 8.3, 3.2, 1.1, \"load bundle\\nconfig.json + mf_params.npz\", fc=LIGHT, fs=9, bold=True)\n",
463
+ "box(axB, 0.9, 6.2, 2.3, 1.0, \"user_id\\nU0007\", fs=10, bold=True)\n",
464
+ "box(axB, 4.0, 6.2, 2.3, 1.0, \"prompt_id\\nP0572\", fs=10, bold=True)\n",
465
+ "box(axB, 0.9, 4.2, 2.3, 0.9, \"embedding p_u\", fs=9.5, color=\"#1e40af\")\n",
466
+ "box(axB, 4.0, 4.2, 2.3, 0.9, \"embedding q_i\", fs=9.5, color=\"#b45309\")\n",
467
+ "box(axB, 7.4, 5.0, 2.3, 1.4, r\"$\\hat{r} = \\mu + b_u + b_i$\" + \"\\n\" + r\"$+ \\langle p_u,\\ q_i \\rangle$\",\n",
468
+ " fs=10, bold=True, fc=\"#f8fafc\")\n",
469
+ "arrow(axB, 2.05, 6.2, 2.05, 5.15)\n",
470
+ "arrow(axB, 5.15, 6.2, 5.15, 5.15)\n",
471
+ "arrow(axB, 3.2, 4.65, 7.3, 5.4)\n",
472
+ "arrow(axB, 6.3, 4.65, 7.3, 5.8)\n",
473
+ "box(axB, 7.4, 3.0, 2.3, 1.1, \"sigmoid\\np(cloud) = sigmoid(r)\", fs=9)\n",
474
+ "arrow(axB, 8.55, 5.0, 8.55, 4.15)\n",
475
+ "box(axB, 7.4, 1.1, 2.3, 1.2, \"p(cloud) = 0.86\\n-> choose cloud\", fc=\"#dcfce7\", ec=GREEN, fs=10, bold=True, color=GREEN)\n",
476
+ "arrow(axB, 8.55, 3.0, 8.55, 2.35)\n",
477
+ "axB.text(2.0, 1.5, \"decision rule:\\np >= 0.5 -> cloud, else local\", ha=\"center\", fontsize=8.5, color=\"#555\")\n",
478
+ "\n",
479
+ "fig.suptitle(\"Simple Matrix Factorization model — cloud vs local model choice\",\n",
480
+ " fontsize=14, fontweight=\"bold\", y=0.99)\n",
481
+ "fig.tight_layout(rect=[0, 0, 1, 0.96])\n",
482
+ "fig.savefig(\"/home/user/mf_model_diagram.png\", dpi=150, bbox_inches=\"tight\", facecolor=\"white\")\n",
483
+ "plt.show()\n",
484
+ "print(\"diagram saved -> /home/user/mf_model_diagram.png\")"
485
+ ]
486
+ },
487
+ {
488
+ "cell_type": "code",
489
+ "execution_count": null,
490
+ "id": "15cb391d",
491
+ "metadata": {},
492
+ "outputs": [],
493
+ "source": [
494
+ "%pip install -q datasets\n",
495
+ "\n",
496
+ "import subprocess\n",
497
+ "import sys\n",
498
+ "\n",
499
+ "def run_script(*args):\n",
500
+ " r = subprocess.run([sys.executable, \"/home/user/mf_inference.py\", *args],\n",
501
+ " capture_output=True, text=True, cwd=\"/home/user\")\n",
502
+ " print(r.stdout)\n",
503
+ " if r.returncode != 0:\n",
504
+ " print(\"STDERR:\", r.stderr[-1200:])\n",
505
+ "\n",
506
+ "print(\"=\" * 78)\n",
507
+ "print(\"RUN 1 — model output: score specific (user, prompt) pairs\")\n",
508
+ "print(\"=\" * 78)\n",
509
+ "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\",\n",
510
+ " \"--prompt_ids\", \"P0001,P0400,P0572,P0260\",\n",
511
+ " \"--csv\", \"preference_data_synthetic.csv\")\n",
512
+ "\n",
513
+ "print(\"=\" * 78)\n",
514
+ "print(\"RUN 2 — model output: rank all known prompts for the user\")\n",
515
+ "print(\"=\" * 78)\n",
516
+ "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"5\",\n",
517
+ " \"--csv\", \"preference_data_synthetic.csv\")\n",
518
+ "\n",
519
+ "print(\"=\" * 78)\n",
520
+ "print(\"RUN 3 — optional Hugging Face Hub push path (no HF_TOKEN -> graceful message)\")\n",
521
+ "print(\"=\" * 78)\n",
522
+ "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"3\",\n",
523
+ " \"--csv\", \"preference_data_synthetic.csv\", \"--push_to_hub\",\n",
524
+ " \"--repo_id\", \"your-org/cloud-local-mf\")"
525
+ ]
526
+ },
527
+ {
528
+ "cell_type": "code",
529
+ "execution_count": null,
530
+ "id": "30f47935",
531
+ "metadata": {},
532
+ "outputs": [],
533
+ "source": [
534
+ "import os\n",
535
+ "import subprocess\n",
536
+ "import sys\n",
537
+ "\n",
538
+ "# Token is read from a temp file (never hardcoded in the notebook) and\n",
539
+ "# removed right after, so it cannot leak into the notebook or outputs.\n",
540
+ "token = open(\"/tmp/.hf_token\").read().strip()\n",
541
+ "env = dict(os.environ, HF_TOKEN=token)\n",
542
+ "\n",
543
+ "print(\"=\" * 78)\n",
544
+ "print(\"PUSH — upload mf_bundle to the Hugging Face Hub\")\n",
545
+ "print(\"=\" * 78)\n",
546
+ "r = subprocess.run(\n",
547
+ " [sys.executable, \"/home/user/mf_inference.py\",\n",
548
+ " \"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"3\",\n",
549
+ " \"--csv\", \"preference_data_synthetic.csv\",\n",
550
+ " \"--push_to_hub\", \"--repo_id\", \"subhash4face/cloud-local-mf\"],\n",
551
+ " capture_output=True, text=True, cwd=\"/home/user\", env=env)\n",
552
+ "print(r.stdout)\n",
553
+ "if r.returncode != 0:\n",
554
+ " print(\"STDERR:\", r.stderr[-2000:])\n",
555
+ "else:\n",
556
+ " from huggingface_hub import HfApi\n",
557
+ " info = HfApi().model_info(\"subhash4face/cloud-local-mf\", token=token)\n",
558
+ " print(\"verified repo:\", info.modelId, \"| files:\")\n",
559
+ " for s in HfApi().list_repo_files(\"subhash4face/cloud-local-mf\", token=token):\n",
560
+ " print(\" -\", s)\n",
561
+ "\n",
562
+ "try:\n",
563
+ " os.remove(\"/tmp/.hf_token\")\n",
564
+ " print(\"(temp token file removed)\")\n",
565
+ "except FileNotFoundError:\n",
566
+ " pass"
567
+ ]
568
+ }
569
+ ],
570
+ "metadata": {
571
+ "kernelspec": {
572
+ "display_name": "Python 3",
573
+ "language": "python",
574
+ "name": "python3"
575
+ },
576
+ "language_info": {
577
+ "name": "python",
578
+ "version": "3.11"
579
+ }
580
+ },
581
+ "nbformat": 4,
582
+ "nbformat_minor": 5
583
+ }
mf_inference.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ mf_inference.py — sample script that loads the trained MF model bundle and runs model output.
4
+
5
+ Hugging Face ecosystem used here:
6
+ * `datasets` -> loads the preference CSV into a HF Dataset (falls back to pandas)
7
+ * `huggingface_hub` -> optional `--push_to_hub`: creates + uploads the bundle as a HF model repo
8
+
9
+ Examples
10
+ --------
11
+ # score specific (user, prompt) pairs
12
+ python mf_inference.py --model_dir mf_bundle --user_id U0007 \
13
+ --prompt_ids P0001,P0400,P0572 --csv preference_data_synthetic.csv
14
+
15
+ # rank all known prompts for a user (top cloud / top local)
16
+ python mf_inference.py --model_dir mf_bundle --user_id U0007 --top_k 5 \
17
+ --csv preference_data_synthetic.csv
18
+
19
+ # push the bundle to the Hugging Face Hub (needs HF_TOKEN or huggingface-cli login)
20
+ python mf_inference.py --model_dir mf_bundle --user_id U0007 --top_k 3 \
21
+ --csv preference_data_synthetic.csv --push_to_hub --repo_id your-org/cloud-local-mf
22
+ """
23
+ import argparse
24
+ import json
25
+ import os
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ import numpy as np
30
+
31
+ try: # HF ecosystem (optional but preferred)
32
+ from datasets import Dataset
33
+ HAVE_DATASETS = True
34
+ except ImportError:
35
+ HAVE_DATASETS = False
36
+
37
+ try:
38
+ from huggingface_hub import HfApi, upload_folder
39
+ HAVE_HUB = True
40
+ except ImportError:
41
+ HAVE_HUB = False
42
+
43
+ MODEL_KEYS = ("mu", "bu", "bi", "P", "Q", "user_ids", "prompt_ids")
44
+
45
+
46
+ def sigmoid(z):
47
+ return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
48
+
49
+
50
+ class SimpleMF:
51
+ """HF-style loader for the bundle written by the training notebook.
52
+
53
+ from_pretrained() reads config.json + mf_params.npz so inference never
54
+ depends on the training code or kernel state.
55
+ """
56
+
57
+ def __init__(self, config, params):
58
+ self.config = config
59
+ self.mu = float(params["mu"])
60
+ self.bu = params["bu"]
61
+ self.bi = params["bi"]
62
+ self.P = params["P"]
63
+ self.Q = params["Q"]
64
+ self.user_ids = [str(x) for x in params["user_ids"]]
65
+ self.prompt_ids = [str(x) for x in params["prompt_ids"]]
66
+ self._uidx = {u: i for i, u in enumerate(self.user_ids)}
67
+ self._pidx = {p: i for i, p in enumerate(self.prompt_ids)}
68
+
69
+ @classmethod
70
+ def from_pretrained(cls, model_dir):
71
+ model_dir = Path(model_dir)
72
+ config = json.loads((model_dir / "config.json").read_text())
73
+ params = np.load(model_dir / "mf_params.npz", allow_pickle=True)
74
+ missing = [k for k in MODEL_KEYS if k not in params.files]
75
+ if missing:
76
+ raise ValueError(f"bundle {model_dir} is missing: {missing}")
77
+ return cls(config, params)
78
+
79
+ # ---- scoring ------------------------------------------------------
80
+ def score_ids(self, user_ids, prompt_ids):
81
+ """Raw scores r_hat for lists of string ids (both must be known)."""
82
+ u = np.array([self._uidx[x] for x in user_ids])
83
+ i = np.array([self._pidx[x] for x in prompt_ids])
84
+ return self.mu + self.bu[u] + self.bi[i] + (self.P[u] * self.Q[i]).sum(1)
85
+
86
+ def predict(self, user_ids, prompt_ids):
87
+ """P(cloud preferred) in [0, 1] for (user, prompt) pairs."""
88
+ return sigmoid(self.score_ids(user_ids, prompt_ids))
89
+
90
+ def rank_for_user(self, user_id, top_k=5):
91
+ """Score every known prompt for one user; returns (desc, asc) arrays of rows."""
92
+ if user_id not in self._uidx:
93
+ raise KeyError(f"unknown user '{user_id}' — bundle knows {len(self.user_ids)} users")
94
+ u = self._uidx[user_id]
95
+ r = self.mu + self.bu[u] + self.bi + (self.P[u] * self.Q).sum(1)
96
+ p = sigmoid(r)
97
+ order = np.argsort(-p)
98
+ def rows(idx):
99
+ return [{"prompt_id": self.prompt_ids[j], "p_cloud": float(p[j]),
100
+ "choice": "cloud" if p[j] >= 0.5 else "local"} for j in idx]
101
+ return rows(order[:top_k]), rows(order[-top_k:][::-1])
102
+
103
+
104
+ def load_catalog(path):
105
+ """Load the preference file; returns dict prompt_id -> {topic, text} (best effort)."""
106
+ catalog = {}
107
+ if path is None or not Path(path).exists():
108
+ return catalog
109
+ df = Dataset.from_csv(path) if HAVE_DATASETS else _pandas_read(path)
110
+ for row in df:
111
+ pid = str(row.get("prompt_id", row.get("prompt", "")))
112
+ if pid:
113
+ catalog[pid] = {"topic": str(row.get("topic", "")),
114
+ "text": str(row.get("prompt_text", row.get("prompt", "")))}
115
+ return catalog
116
+
117
+
118
+ def _pandas_read(path):
119
+ import pandas as pd
120
+ return pd.read_csv(path)
121
+
122
+
123
+ def print_report(rows, catalog, title):
124
+ print(f"\n{title}")
125
+ print(f"{'prompt_id':<10}{'p(cloud)':>9} {'choice':<6} topic / prompt")
126
+ print("-" * 78)
127
+ for r in rows:
128
+ meta = catalog.get(r["prompt_id"], {})
129
+ topic = meta.get("topic", "?")
130
+ text = meta.get("text", "")
131
+ text = text[:46] + "…" if len(text) > 46 else text
132
+ print(f"{r['prompt_id']:<10}{r['p_cloud']:>9.3f} {r['choice']:<6} {topic:<18} {text}")
133
+
134
+
135
+ def main():
136
+ ap = argparse.ArgumentParser(description="Load the MF bundle and run model output")
137
+ ap.add_argument("--model_dir", default="mf_bundle", help="path to the saved bundle")
138
+ ap.add_argument("--user_id", default="U0007")
139
+ ap.add_argument("--prompt_ids", help="comma-separated prompt ids to score")
140
+ ap.add_argument("--top_k", type=int, default=0, help="rank top-k prompts for the user")
141
+ ap.add_argument("--csv", help="preference CSV (for topic/text display)")
142
+ ap.add_argument("--push_to_hub", action="store_true", help="upload bundle as a HF model repo")
143
+ ap.add_argument("--repo_id", default=None, help="HF repo id, e.g. your-org/cloud-local-mf")
144
+ args = ap.parse_args()
145
+
146
+ libs = [f"numpy {np.__version__}"]
147
+ if HAVE_DATASETS:
148
+ import datasets
149
+ libs.append(f"datasets {datasets.__version__}")
150
+ if HAVE_HUB:
151
+ import huggingface_hub
152
+ libs.append(f"huggingface_hub {huggingface_hub.__version__}")
153
+ print("python libs:", ", ".join(libs))
154
+
155
+ model = SimpleMF.from_pretrained(args.model_dir)
156
+ print(f"loaded bundle: {Path(args.model_dir).resolve()} "
157
+ f"({len(model.user_ids)} users x {len(model.prompt_ids)} prompts, k={model.P.shape[1]})")
158
+ print(f"model config : {model.config.get('model')}")
159
+
160
+ catalog = load_catalog(args.csv)
161
+
162
+ if args.prompt_ids:
163
+ pids = [p.strip() for p in args.prompt_ids.split(",") if p.strip()]
164
+ unknown = [p for p in pids if p not in model._pidx]
165
+ if unknown:
166
+ print(f"ERROR: unknown prompt ids {unknown} — bundle knows {len(model.prompt_ids)} prompts")
167
+ sys.exit(2)
168
+ p = model.predict([args.user_id] * len(pids), pids)
169
+ rows = [{"prompt_id": pid, "p_cloud": float(pi), "choice": "cloud" if pi >= 0.5 else "local"}
170
+ for pid, pi in zip(pids, p)]
171
+ print_report(rows, catalog, f"model output — choices for user {args.user_id}")
172
+
173
+ if args.top_k:
174
+ top, bottom = model.rank_for_user(args.user_id, args.top_k)
175
+ print_report(top, catalog, f"user {args.user_id} — top {args.top_k} prompts → cloud")
176
+ print_report(bottom, catalog, f"user {args.user_id} — top {args.top_k} prompts → local")
177
+
178
+ # optional Hub push -------------------------------------------------
179
+ if args.push_to_hub:
180
+ if not HAVE_HUB:
181
+ print("huggingface_hub not installed — cannot push to the Hub")
182
+ sys.exit(1)
183
+ if not args.repo_id:
184
+ print("--push_to_hub requires --repo_id, e.g. your-org/cloud-local-mf")
185
+ sys.exit(2)
186
+ token = os.environ.get("HF_TOKEN", None)
187
+ if not token:
188
+ print("No HF_TOKEN in environment. Run `huggingface-cli login` (or set HF_TOKEN) "
189
+ "and re-run to push.")
190
+ sys.exit(0)
191
+ api = HfApi()
192
+ api.whoami(token=token)
193
+ api.create_repo(repo_id=args.repo_id, token=token, repo_type="model", exist_ok=True)
194
+ upload_folder(folder_path=str(Path(args.model_dir).resolve()),
195
+ repo_id=args.repo_id, token=token,
196
+ commit_message="Add cloud-vs-local MF preference model bundle")
197
+ print(f"pushed bundle -> https://huggingface.co/{args.repo_id}")
198
+
199
+ print("\nmodel output complete.")
200
+
201
+
202
+ if __name__ == "__main__":
203
+ main()
mf_model_diagram.png ADDED

Git LFS Details

  • SHA256: 66fbe1530ef272e412afaf12c5db05d0a23fc229754f2928d01b3fdaab218417
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB