{ "cells": [ { "cell_type": "markdown", "id": "eef2e5c1", "metadata": {}, "source": [ "# MF preference model: cloud vs local\n", "\n", "**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", "\n", "**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", "\n", "**Approach**\n", "1. Build a sparse `users × prompts` binary matrix (`1` = cloud preferred, `0` = local preferred).\n", "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", "3. Decision rule: `r̂ > 0.5 → cloud`, else `local`.\n", "4. Evaluate accuracy / AUC on a held-out test set." ] }, { "cell_type": "code", "execution_count": null, "id": "b503ff3b", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "\n", "# ============================================================\n", "# 1) SYNTHETIC PREFERENCE DATA (stand-in for the real file)\n", "# ============================================================\n", "# Real schema this mimics: user_id | prompt_id | topic | prompt_text | chosen | label\n", "# label: 1 = cloud preferred, 0 = local preferred\n", "#\n", "# Each topic has a \"cloud affinity\" — the probability that a random user\n", "# prefers the cloud model for a prompt in that topic. Users add a personal\n", "# offset, and there is rating noise, so the preference structure is latent\n", "# and must be recovered by the model rather than read off the text.\n", "\n", "TOPIC_AFFINITY = {\n", " \"complex_reasoning\": 0.78, # heavy compute -> cloud\n", " \"long_context\": 0.72,\n", " \"image_generation\": 0.64,\n", " \"code_generation\": 0.58,\n", " \"creative_writing\": 0.44,\n", " \"simple_qa\": 0.35,\n", " \"latency_sensitive\": 0.20, # needs to be fast -> local\n", " \"privacy_sensitive\": 0.12, # data must not leave the machine -> local\n", "}\n", "\n", "TEMPLATES = {\n", " \"complex_reasoning\": [\n", " \"Prove or refute: every {obj} admits a canonical {prop} decomposition\",\n", " \"Find the flaw in this 40-step proof about {prop} and repair it\",\n", " \"Derive the closed form for {obj} and verify each step\",\n", " ],\n", " \"long_context\": [\n", " \"Summarize the key arguments across all 200 pages of {doc}\",\n", " \"Track every character and plot thread across the {doc} saga\",\n", " \"Answer strictly from the full 2-hour meeting transcript on {doc}\",\n", " ],\n", " \"image_generation\": [\n", " \"Generate a photorealistic image of {obj} at golden hour\",\n", " \"Create a 4k illustration of {obj} with dramatic rim lighting\",\n", " \"Produce a logo mockup for {doc}, transparent background\",\n", " ],\n", " \"code_generation\": [\n", " \"Write a production-ready implementation of {prop} in Python\",\n", " \"Refactor this legacy module into modern C++20: {doc}\",\n", " \"Generate unit tests and API docs for the {prop} library\",\n", " ],\n", " \"creative_writing\": [\n", " \"Write a haiku about {obj}\",\n", " \"Draft a two-paragraph product blurb for {doc}\",\n", " \"Compose a short story opening about {obj}\",\n", " ],\n", " \"simple_qa\": [\n", " \"What is the capital of {doc}?\",\n", " \"Convert 150 miles to kilometers\",\n", " \"Explain {prop} in one sentence\",\n", " ],\n", " \"latency_sensitive\": [\n", " \"Autocomplete this sentence in real time: {obj}\",\n", " \"Give an instant short answer: {prop}?\",\n", " \"Rephrase this snippet while I type: {obj}\",\n", " ],\n", " \"privacy_sensitive\": [\n", " \"Summarize my medical records regarding {prop}\",\n", " \"Draft an email discussing my salary at {doc}\",\n", " \"Redact PII from this legal document about {doc}\",\n", " ],\n", "}\n", "\n", "FILLERS = {\n", " \"obj\": [\"quantum error correction\", \"a flamenco guitarist\", \"sourdough bread\",\n", " \"a sleepy cat\", \"a rusting cargo ship\", \"a chess endgame\", \"a thunderstorm\"],\n", " \"prop\": [\"topological sorting\", \"Bayesian inference\", \"memory-mapped I/O\",\n", " \"backpropagation\", \"deadlock avoidance\", \"tokenization\", \"garbage collection\"],\n", " \"doc\": [\"Q4 earnings report\", \"clinical trial protocol\", \"migration guide\",\n", " \"franchise lore wiki\", \"board meeting minutes\", \"product spec\"],\n", "}\n", "\n", "rng = np.random.default_rng(42)\n", "N_USERS, N_PROMPTS_PER_TOPIC, RATINGS_PER_USER = 400, 75, 30\n", "\n", "# --- build the prompt catalog (8 topics x 75 prompts) ---\n", "rows = []\n", "prompt_id = 0\n", "for topic, affinity in TOPIC_AFFINITY.items():\n", " tpl = TEMPLATES[topic]\n", " for _ in range(N_PROMPTS_PER_TOPIC):\n", " text = rng.choice(tpl).format(**{k: rng.choice(v) for k, v in FILLERS.items()})\n", " rows.append({\"prompt_id\": f\"P{prompt_id:04d}\", \"topic\": topic,\n", " \"affinity\": affinity, \"prompt_text\": text})\n", " prompt_id += 1\n", "prompts = pd.DataFrame(rows)\n", "\n", "# --- each user rates a random subset of prompts ---\n", "user_offsets = rng.normal(0.0, 0.15, size=N_USERS) # personal cloud-bias\n", "pref_rows = []\n", "for u in range(N_USERS):\n", " uid = f\"U{u:04d}\"\n", " picks = rng.choice(prompts.index, size=RATINGS_PER_USER, replace=False)\n", " for pi in picks:\n", " p_cloud = 0.5 + (prompts.loc[pi, \"affinity\"] - 0.5) + user_offsets[u] + rng.normal(0, 0.12)\n", " p_cloud = float(np.clip(p_cloud, 0.02, 0.98))\n", " label = int(rng.binomial(1, p_cloud))\n", " pref_rows.append({\"user_id\": uid, \"prompt_id\": prompts.loc[pi, \"prompt_id\"],\n", " \"topic\": prompts.loc[pi, \"topic\"],\n", " \"prompt_text\": prompts.loc[pi, \"prompt_text\"],\n", " \"chosen\": \"cloud\" if label else \"local\", \"label\": label})\n", "\n", "pref = pd.DataFrame(pref_rows)\n", "pref.to_csv(\"/home/user/preference_data_synthetic.csv\", index=False)\n", "print(f\"preference rows : {len(pref):,}\")\n", "print(f\"users : {pref.user_id.nunique():,}\")\n", "print(f\"prompts : {pref.prompt_id.nunique():,}\")\n", "print(f\"cloud share : {pref.label.mean():.3f}\")\n", "print(\"\\nFirst 5 rows:\")\n", "print(pref.head().to_string(index=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "6978329b", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.metrics import roc_auc_score\n", "\n", "pref = pd.read_csv(\"/home/user/preference_data_synthetic.csv\")\n", "\n", "# ---------------- EDA ----------------\n", "print(\"=== shape / nulls / balance ===\")\n", "print(f\"rows: {len(pref):,} | nulls: {pref.isnull().sum().sum()} | duplicate (user,prompt): \"\n", " f\"{pref.duplicated(['user_id','prompt_id']).sum()}\")\n", "\n", "print(\"\\n=== cloud share by topic (should track the injected affinity) ===\")\n", "by_topic = pref.groupby(\"topic\")[\"label\"].agg([\"mean\", \"count\"]).rename(\n", " columns={\"mean\": \"cloud_rate\", \"count\": \"n\"})\n", "print(by_topic.round(3).to_string())\n", "\n", "print(\"\\n=== density of the users x prompts matrix ===\")\n", "n_users, n_prompts = pref.user_id.nunique(), pref.prompt_id.nunique()\n", "print(f\"matrix: {n_users} x {n_prompts} = {n_users*n_prompts:,} cells, \"\n", " f\"{len(pref):,} observed -> density {len(pref)/(n_users*n_prompts):.4%}\")\n", "print(f\"ratings per user: mean {pref.groupby('user_id').size().mean():.0f}, \"\n", " f\"per prompt: mean {pref.groupby('prompt_id').size().mean():.0f}\")\n", "\n", "# ---------------- encode + split ----------------\n", "user_ids = sorted(pref.user_id.unique())\n", "prompt_ids = sorted(pref.prompt_id.unique())\n", "uidx = {u: i for i, u in enumerate(user_ids)}\n", "pidx = {p: i for i, p in enumerate(prompt_ids)}\n", "\n", "pref[\"u\"] = pref.user_id.map(uidx)\n", "pref[\"i\"] = pref.prompt_id.map(pidx)\n", "\n", "# random row split (80/10/10), stratified by label\n", "train, rest = train_test_split(pref, test_size=0.2, random_state=0, stratify=pref[\"label\"])\n", "val, test = train_test_split(rest, test_size=0.5, random_state=0, stratify=rest[\"label\"])\n", "\n", "print(\"\\n=== split sizes ===\")\n", "for name, df in [(\"train\", train), (\"val\", val), (\"test\", test)]:\n", " print(f\"{name:5s}: {len(df):,} rows | cloud rate {df.label.mean():.3f} | \"\n", " f\"warm users {df.u.isin(train.u).mean():.2%} (overlap w/ train)\")\n", "\n", "train.to_csv(\"/home/user/train.csv\", index=False)\n", "val.to_csv(\"/home/user/val.csv\", index=False)\n", "test.to_csv(\"/home/user/test.csv\", index=False)" ] }, { "cell_type": "code", "execution_count": null, "id": "4eae65c1", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from sklearn.metrics import roc_auc_score\n", "\n", "# ============================================================\n", "# 2) SIMPLE MATRIX FACTORIZATION (NumPy SGD, BCE + L2)\n", "# r̂(u,i) = μ + b_u + b_i + \n", "# ============================================================\n", "\n", "def sigmoid(z):\n", " return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))\n", "\n", "\n", "class SimpleMF:\n", " \"\"\"Binary-preference MF: predicts P(cloud preferred | user, prompt).\"\"\"\n", "\n", " def __init__(self, n_users, n_prompts, k=8, lr=0.05, reg=0.05,\n", " epochs=30, batch=512, seed=0):\n", " self.k, self.lr, self.reg, self.epochs, self.batch = k, lr, reg, epochs, batch\n", " rng = np.random.default_rng(seed)\n", " self.mu = 0.0\n", " self.bu = np.zeros(n_users)\n", " self.bi = np.zeros(n_prompts)\n", " self.P = rng.normal(0, 0.1, (n_users, k)) # user factors\n", " self.Q = rng.normal(0, 0.1, (n_prompts, k)) # prompt factors\n", " self.history = []\n", "\n", " def predict(self, u, i):\n", " r = self.mu + self.bu[u] + self.bi[i] + (self.P[u] * self.Q[i]).sum(1)\n", " return sigmoid(r)\n", "\n", " def fit(self, ui, y, val_ui=None, val_y=None):\n", " \"\"\"ui: (N,2) [user_idx, prompt_idx], y: (N,) binary labels.\"\"\"\n", " self.mu = y.mean()\n", " n = len(y)\n", " best = None\n", " for ep in range(self.epochs):\n", " perm = np.random.default_rng(ep).permutation(n)\n", " losses = []\n", " for s in range(0, n, self.batch):\n", " idx = perm[s:s + self.batch]\n", " u, i = ui[idx, 0], ui[idx, 1]\n", " r = self.mu + self.bu[u] + self.bi[i] + (self.P[u] * self.Q[i]).sum(1)\n", " d = sigmoid(r) - y[idx] # grad wrt r of BCE\n", " loss = float((np.logaddexp(0, r) - y[idx] * r).mean())\n", " losses.append(loss)\n", " # updates with L2 regularization\n", " self.bu[u] -= self.lr * (d + self.reg * self.bu[u])\n", " self.bi[i] -= self.lr * (d + self.reg * self.bi[i])\n", " self.P[u] -= self.lr * (d[:, None] * self.Q[i] + self.reg * self.P[u])\n", " self.Q[i] -= self.lr * (d[:, None] * self.P[u] + self.reg * self.Q[i])\n", " # track val AUC, keep best params (simple early stop)\n", " val_auc = roc_auc_score(val_y, self.predict(val_ui[:, 0], val_ui[:, 1])) \\\n", " if val_ui is not None else float(\"nan\")\n", " self.history.append((ep, np.mean(losses), val_auc))\n", " if best is None or val_auc > best[0]:\n", " best = (val_auc, self.bu.copy(), self.bi.copy(), self.P.copy(), self.Q.copy())\n", " print(f\"epoch {ep+1:2d}/{self.epochs} | train loss {np.mean(losses):.4f} | val AUC {val_auc:.4f}\")\n", " self.bu, self.bi, self.P, self.Q = best[1], best[2], best[3], best[4]\n", " print(f\"\\nbest val AUC: {best[0]:.4f} (params restored)\")\n", "\n", "\n", "train = pd.read_csv(\"/home/user/train.csv\")\n", "val = pd.read_csv(\"/home/user/val.csv\")\n", "\n", "model = SimpleMF(n_users=train.u.max() + 1, n_prompts=train.i.max() + 1,\n", " k=8, lr=0.05, reg=0.05, epochs=30, seed=0)\n", "model.fit(train[[\"u\", \"i\"]].to_numpy(), train[\"label\"].to_numpy(),\n", " val[[\"u\", \"i\"]].to_numpy(), val[\"label\"].to_numpy())" ] }, { "cell_type": "code", "execution_count": null, "id": "bd02d51b", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix\n", "\n", "test = pd.read_csv(\"/home/user/test.csv\")\n", "pred = model.predict(test[\"u\"].to_numpy(), test[\"i\"].to_numpy())\n", "test[\"p_cloud\"] = pred\n", "test[\"pred_choice\"] = np.where(pred >= 0.5, \"cloud\", \"local\")\n", "\n", "acc = accuracy_score(test[\"label\"], (pred >= 0.5).astype(int))\n", "auc = roc_auc_score(test[\"label\"], pred)\n", "tn, fp, fn, tp = confusion_matrix(test[\"label\"], (pred >= 0.5).astype(int)).ravel()\n", "\n", "print(f\"=== TEST SET (n={len(test):,}) ===\")\n", "print(f\"accuracy : {acc:.4f} (baseline: always cloud = {test.label.mean():.4f})\")\n", "print(f\"AUC : {auc:.4f}\")\n", "print(f\"confusion : TP(cloud->cloud)={tp:4d} FN={fn:4d} | FP={fp:4d} TN(local->local)={tn:4d}\")\n", "\n", "print(\"\\n=== per-topic accuracy (does the model recover the latent topic structure?) ===\")\n", "t = test.groupby(\"topic\").apply(\n", " lambda d: pd.Series({\"acc\": accuracy_score(d.label, d.p_cloud >= 0.5),\n", " \"mean_p_cloud\": d.p_cloud.mean(), \"n\": len(d)}),\n", " include_groups=False).round(3)\n", "print(t.to_string())\n", "\n", "print(\"\\n=== strongest signals: prompts the model is most / least sure are 'cloud' ===\")\n", "g = test.groupby(\"prompt_id\")[\"p_cloud\"].mean()\n", "top = g.nlargest(5).index\n", "bot = g.nsmallest(5).index\n", "show = test[test.prompt_id.isin(top) | test.prompt_id.isin(bot)].drop_duplicates(\"prompt_id\")\n", "show = show.assign(mean_p_cloud=show.prompt_id.map(g)) \\\n", " .sort_values(\"mean_p_cloud\", ascending=False) \\\n", " [[\"mean_p_cloud\", \"topic\", \"prompt_text\"]]\n", "print(show.to_string(index=False, max_colwidth=70))" ] }, { "cell_type": "code", "execution_count": null, "id": "24d2f4db", "metadata": {}, "outputs": [], "source": [ "import os\n", "import json\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ============================================================\n", "# 3) SAVE MODEL BUNDLE + DROP-IN LOADER FOR REAL DATA\n", "# ============================================================\n", "os.makedirs(\"/home/user/mf_bundle\", exist_ok=True)\n", "\n", "np.savez(\"/home/user/mf_bundle/mf_params.npz\",\n", " mu=model.mu, bu=model.bu, bi=model.bi, P=model.P, Q=model.Q,\n", " user_ids=np.array(user_ids), prompt_ids=np.array(prompt_ids))\n", "\n", "prompt_topic = prompts.set_index(\"prompt_id\")[\"topic\"].to_dict()\n", "config = {\n", " \"model\": \"SimpleMF (binary preference, BCE + L2, early stop on val AUC)\",\n", " \"hyperparams\": {\"k\": model.k, \"lr\": model.lr, \"reg\": model.reg,\n", " \"epochs\": model.epochs, \"batch\": model.batch},\n", " \"train\": {\"rows\": int(len(train)), \"users\": n_users, \"prompts\": n_prompts},\n", " \"test_metrics\": {\"accuracy\": round(acc, 4), \"auc\": round(auc, 4),\n", " \"baseline_accuracy\": round(float(test.label.mean()), 4)},\n", " \"decision_rule\": \"p_cloud >= 0.5 -> 'cloud', else 'local'\",\n", " \"note\": \"Trained on synthetic demo data; retrain with load_preference_data() on the real file.\",\n", "}\n", "json.dump(config, open(\"/home/user/mf_bundle/config.json\", \"w\"), indent=2)\n", "test[[\"user_id\", \"prompt_id\", \"topic\", \"prompt_text\", \"label\", \"p_cloud\", \"pred_choice\"]] \\\n", " .to_csv(\"/home/user/mf_bundle/test_predictions.csv\", index=False)\n", "print(\"bundle saved to /home/user/mf_bundle/ (mf_params.npz, config.json, test_predictions.csv)\")\n", "\n", "\n", "# ------------------------------------------------------------\n", "# Drop-in entry point for REAL preference data\n", "# Expected columns: user_id, prompt, chosen (\"cloud\" | \"local\")\n", "# (prompt_text / prompt_id are also accepted and merged.)\n", "# ------------------------------------------------------------\n", "def load_preference_data(path, seed=0):\n", " df = pd.read_csv(path)\n", " df.columns = [c.strip().lower() for c in df.columns]\n", " assert \"user_id\" in df.columns and \"chosen\" in df.columns, \\\n", " \"real data must have columns: user_id, prompt, chosen\"\n", " if \"prompt\" not in df.columns: # tolerate prompt_id/prompt_text\n", " df[\"prompt\"] = df.get(\"prompt_text\", df[\"prompt_id\"].astype(str))\n", " df[\"label\"] = (df[\"chosen\"].str.lower() == \"cloud\").astype(int)\n", " df = df.drop_duplicates([\"user_id\", \"prompt\"])\n", " df[\"u\"] = df.user_id.map({u: i for i, u in enumerate(sorted(df.user_id.unique()))})\n", " df[\"i\"] = df.prompt.map({p: i for i, p in enumerate(sorted(df.prompt.unique()))})\n", " return df\n", "\n", "def train_on(df, val_frac=0.1, **mf_kwargs):\n", " from sklearn.model_selection import train_test_split\n", " tr, va = train_test_split(df, test_size=val_frac, random_state=0, stratify=df[\"label\"])\n", " m = SimpleMF(n_users=df.u.max() + 1, n_prompts=df.i.max() + 1, **mf_kwargs)\n", " m.fit(tr[[\"u\", \"i\"]].to_numpy(), tr[\"label\"].to_numpy(),\n", " va[[\"u\", \"i\"]].to_numpy(), va[\"label\"].to_numpy())\n", " return m\n", "\n", "print(\"\\nDemo — retrain through the real-data entry point (synthetic CSV -> real schema):\")\n", "real_schema = pref[[\"user_id\", \"prompt_text\", \"chosen\"]].rename(columns={\"prompt_text\": \"prompt\"})\n", "real_schema.to_csv(\"/home/user/preference_real_schema.csv\", index=False)\n", "demo = train_on(load_preference_data(\"/home/user/preference_real_schema.csv\"), epochs=12)\n", "print(\" -> entry point OK; demo model val AUC printed above (main model metrics in config.json)\")\n", "\n", "print(\"\\nDemo — model choices for a few prompts of user U0007 (trained model):\")\n", "for pid in [\"P0001\", \"P0400\", \"P0572\", \"P0260\"]:\n", " p = model.predict(np.array([uidx[\"U0007\"]]), np.array([pidx[pid]]))[0]\n", " print(f\" {pid} ({prompt_topic[pid]:<18s}) p(cloud)={p:.3f} -> {'cloud' if p >= 0.5 else 'local'}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "2951eca0", "metadata": {}, "outputs": [], "source": [ "import matplotlib\n", "import matplotlib.pyplot as plt\n", "from matplotlib.patches import FancyBboxPatch, Rectangle, FancyArrowPatch\n", "import numpy as np\n", "\n", "matplotlib.rcParams.update({\"font.size\": 10, \"figure.facecolor\": \"white\"})\n", "CLOUD, LOCAL, ACCENT, LIGHT = \"#2563eb\", \"#f59e0b\", \"#111827\", \"#eef2ff\"\n", "GREEN = \"#16a34a\"\n", "\n", "def box(ax, x, y, w, h, text, fc=\"white\", ec=ACCENT, fs=9.5, bold=False, color=None):\n", " ax.add_patch(FancyBboxPatch((x, y), w, h,\n", " boxstyle=\"round,pad=0.02,rounding_size=0.06\", fc=fc, ec=ec, lw=1.6))\n", " ax.text(x + w / 2, y + h / 2, text, ha=\"center\", va=\"center\", fontsize=fs,\n", " fontweight=\"bold\" if bold else \"normal\", color=color or ACCENT)\n", "\n", "def arrow(ax, x1, y1, x2, y2, color=ACCENT, lw=1.6):\n", " ax.add_patch(FancyArrowPatch((x1, y1), (x2, y2), arrowstyle=\"-|>\",\n", " mutation_scale=14, color=color, lw=lw, shrinkA=0, shrinkB=0))\n", "\n", "fig, (axA, axB) = plt.subplots(1, 2, figsize=(14.5, 6.0))\n", "for ax in (axA, axB):\n", " ax.set_xlim(0, 10); ax.set_ylim(0, 10); ax.axis(\"off\")\n", "\n", "# ---------------- Panel A: training ----------------\n", "axA.set_title(\"Training — factorize the users x prompts preference matrix\",\n", " fontsize=12, fontweight=\"bold\", pad=12)\n", "rng = np.random.default_rng(7)\n", "for r in range(7):\n", " for c in range(7):\n", " if rng.random() < 0.28:\n", " axA.add_patch(Rectangle((0.55 + c * 0.36, 8.35 - r * 0.36), 0.32, 0.32,\n", " fc=CLOUD if rng.random() < 0.5 else LOCAL, ec=\"white\", lw=0.5))\n", "axA.text(1.8, 9.5, \"observed preferences\", ha=\"center\", fontsize=9.5, fontweight=\"bold\")\n", "axA.text(1.8, 5.55, \"12,000 ratings\\n5% dense\", ha=\"center\", fontsize=8.5, color=\"#555\")\n", "\n", "box(axA, 4.7, 5.8, 2.9, 2.2, \"Matrix\\nFactorization\\nmin sum BCE + lambda||theta||^2\\nSGD · k = 8\",\n", " fc=LIGHT, fs=9, bold=True)\n", "arrow(axA, 3.15, 6.9, 4.6, 6.9)\n", "\n", "for r in range(4): # P: users x k\n", " for c in range(3):\n", " axA.add_patch(Rectangle((8.35 + c * 0.42, 8.15 - r * 0.42), 0.38, 0.38,\n", " fc=\"#dbeafe\", ec=\"#93c5fd\", lw=0.5))\n", "axA.text(8.9, 6.2, \"P\\nusers x k\", ha=\"center\", fontsize=8.5, color=\"#1e40af\")\n", "for r in range(4): # Q: prompts x k\n", " for c in range(3):\n", " axA.add_patch(Rectangle((8.35 + c * 0.42, 5.3 - r * 0.42), 0.38, 0.38,\n", " fc=\"#fef3c7\", ec=\"#fcd34d\", lw=0.5))\n", "axA.text(8.9, 3.4, \"Q\\nprompts x k\", ha=\"center\", fontsize=8.5, color=\"#b45309\")\n", "arrow(axA, 7.6, 7.6, 8.3, 7.6)\n", "arrow(axA, 7.6, 6.2, 8.3, 6.2)\n", "\n", "axA.text(5.0, 2.6, r\"$\\hat{r}(u,i) = \\mu + b_u + b_i + \\langle p_u,\\ q_i \\rangle$\",\n", " ha=\"center\", fontsize=13, color=ACCENT)\n", "axA.text(5.0, 1.7, \"binary label y: 1 = cloud preferred, 0 = local preferred\",\n", " ha=\"center\", fontsize=9, color=\"#555\")\n", "\n", "# ---------------- Panel B: inference ----------------\n", "axB.set_title(\"Inference — score a (user, prompt) pair from the saved bundle\",\n", " fontsize=12, fontweight=\"bold\", pad=12)\n", "box(axB, 3.4, 8.3, 3.2, 1.1, \"load bundle\\nconfig.json + mf_params.npz\", fc=LIGHT, fs=9, bold=True)\n", "box(axB, 0.9, 6.2, 2.3, 1.0, \"user_id\\nU0007\", fs=10, bold=True)\n", "box(axB, 4.0, 6.2, 2.3, 1.0, \"prompt_id\\nP0572\", fs=10, bold=True)\n", "box(axB, 0.9, 4.2, 2.3, 0.9, \"embedding p_u\", fs=9.5, color=\"#1e40af\")\n", "box(axB, 4.0, 4.2, 2.3, 0.9, \"embedding q_i\", fs=9.5, color=\"#b45309\")\n", "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", " fs=10, bold=True, fc=\"#f8fafc\")\n", "arrow(axB, 2.05, 6.2, 2.05, 5.15)\n", "arrow(axB, 5.15, 6.2, 5.15, 5.15)\n", "arrow(axB, 3.2, 4.65, 7.3, 5.4)\n", "arrow(axB, 6.3, 4.65, 7.3, 5.8)\n", "box(axB, 7.4, 3.0, 2.3, 1.1, \"sigmoid\\np(cloud) = sigmoid(r)\", fs=9)\n", "arrow(axB, 8.55, 5.0, 8.55, 4.15)\n", "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", "arrow(axB, 8.55, 3.0, 8.55, 2.35)\n", "axB.text(2.0, 1.5, \"decision rule:\\np >= 0.5 -> cloud, else local\", ha=\"center\", fontsize=8.5, color=\"#555\")\n", "\n", "fig.suptitle(\"Simple Matrix Factorization model — cloud vs local model choice\",\n", " fontsize=14, fontweight=\"bold\", y=0.99)\n", "fig.tight_layout(rect=[0, 0, 1, 0.96])\n", "fig.savefig(\"/home/user/mf_model_diagram.png\", dpi=150, bbox_inches=\"tight\", facecolor=\"white\")\n", "plt.show()\n", "print(\"diagram saved -> /home/user/mf_model_diagram.png\")" ] }, { "cell_type": "code", "execution_count": null, "id": "15cb391d", "metadata": {}, "outputs": [], "source": [ "%pip install -q datasets\n", "\n", "import subprocess\n", "import sys\n", "\n", "def run_script(*args):\n", " r = subprocess.run([sys.executable, \"/home/user/mf_inference.py\", *args],\n", " capture_output=True, text=True, cwd=\"/home/user\")\n", " print(r.stdout)\n", " if r.returncode != 0:\n", " print(\"STDERR:\", r.stderr[-1200:])\n", "\n", "print(\"=\" * 78)\n", "print(\"RUN 1 — model output: score specific (user, prompt) pairs\")\n", "print(\"=\" * 78)\n", "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\",\n", " \"--prompt_ids\", \"P0001,P0400,P0572,P0260\",\n", " \"--csv\", \"preference_data_synthetic.csv\")\n", "\n", "print(\"=\" * 78)\n", "print(\"RUN 2 — model output: rank all known prompts for the user\")\n", "print(\"=\" * 78)\n", "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"5\",\n", " \"--csv\", \"preference_data_synthetic.csv\")\n", "\n", "print(\"=\" * 78)\n", "print(\"RUN 3 — optional Hugging Face Hub push path (no HF_TOKEN -> graceful message)\")\n", "print(\"=\" * 78)\n", "run_script(\"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"3\",\n", " \"--csv\", \"preference_data_synthetic.csv\", \"--push_to_hub\",\n", " \"--repo_id\", \"your-org/cloud-local-mf\")" ] }, { "cell_type": "code", "execution_count": null, "id": "30f47935", "metadata": {}, "outputs": [], "source": [ "import os\n", "import subprocess\n", "import sys\n", "\n", "# Token is read from a temp file (never hardcoded in the notebook) and\n", "# removed right after, so it cannot leak into the notebook or outputs.\n", "token = open(\"/tmp/.hf_token\").read().strip()\n", "env = dict(os.environ, HF_TOKEN=token)\n", "\n", "print(\"=\" * 78)\n", "print(\"PUSH — upload mf_bundle to the Hugging Face Hub\")\n", "print(\"=\" * 78)\n", "r = subprocess.run(\n", " [sys.executable, \"/home/user/mf_inference.py\",\n", " \"--model_dir\", \"mf_bundle\", \"--user_id\", \"U0007\", \"--top_k\", \"3\",\n", " \"--csv\", \"preference_data_synthetic.csv\",\n", " \"--push_to_hub\", \"--repo_id\", \"subhash4face/cloud-local-mf\"],\n", " capture_output=True, text=True, cwd=\"/home/user\", env=env)\n", "print(r.stdout)\n", "if r.returncode != 0:\n", " print(\"STDERR:\", r.stderr[-2000:])\n", "else:\n", " from huggingface_hub import HfApi\n", " info = HfApi().model_info(\"subhash4face/cloud-local-mf\", token=token)\n", " print(\"verified repo:\", info.modelId, \"| files:\")\n", " for s in HfApi().list_repo_files(\"subhash4face/cloud-local-mf\", token=token):\n", " print(\" -\", s)\n", "\n", "try:\n", " os.remove(\"/tmp/.hf_token\")\n", " print(\"(temp token file removed)\")\n", "except FileNotFoundError:\n", " pass" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }