Add cloud-vs-local MF preference model bundle
Browse files- .gitattributes +1 -0
- README.md +36 -20
- mf_cloud_vs_local.ipynb +583 -0
- mf_inference.py +203 -0
- 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 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 43 |
-
prompts are routed to `local`; complex-reasoning and long-context
|
|
|
|
| 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
|
| 73 |
-
score brand-new prompt text. Extend with a text encoder (e.g. embed prompt
|
| 74 |
-
latent space) for production cold start.
|
| 75 |
-
- **Synthetic data**: metrics above are on synthetic ratings; expect different numbers
|
| 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 |
+

|
| 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
|