From a2bf5c31a063ec1f0216aa5c0a9afab4751ba583 Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:10:15 +0800 Subject: [PATCH] notebook: word steering hello-world (executed on real 0.6B cache) C sweep shows coherence/strength tradeoff (C=1 fluent+happy, C=8 spam), greedy+sampled at C=1, negative steering, lens_topk layer progression (city slot -> candidates -> Paris), Vector save/load. Adds nbconvert to notebooks extra; ignores notebook-produced safetensors. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com> --- .gitignore | 1 + notebooks/word_steering.ipynb | 519 ++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 notebooks/word_steering.ipynb diff --git a/.gitignore b/.gitignore index 5a4f8b1..dfcef69 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ artifacts/*.jac artifacts/*.ckpt uv.lock docs/reviews/*.raw.jsonl +artifacts/*.safetensors diff --git a/notebooks/word_steering.ipynb b/notebooks/word_steering.ipynb new file mode 100644 index 0000000..4061b19 --- /dev/null +++ b/notebooks/word_steering.ipynb @@ -0,0 +1,519 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5ef6f624", + "metadata": {}, + "source": [ + "# jsteer hello-world: word steering\n", + "\n", + "Fit the model's full Jacobian once (`scripts/fit_qwen06b.py`, cached to\n", + "`artifacts/qwen3-0.6b.jac`), then any word vector is an instant CPU matvec:\n", + "\n", + "```\n", + "v_l = unit( J_l^T @ w )\n", + "```\n", + "\n", + "where `w` is the mean unembedding row of the words you want more (or less) of.\n", + "This is the verified extraction method (see the README evidence section).\n", + "Runtime is steering-lite: `with v(model, C=...): model.generate(...)`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "46973b3d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:08:49.171239Z", + "iopub.status.busy": "2026-07-10T05:08:49.171143Z", + "iopub.status.idle": "2026-07-10T05:08:54.432779Z", + "shell.execute_reply": "2026-07-10T05:08:54.432236Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/media/wassname/SGIronWolf/projects5/2026/jspace/jsteer/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\r", + "Loading weights: 0%| | 0/311 [00:00 first-subtoken ids=[56521, 4123] |w|=0.744\u001b[0m\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2026-07-10 13:08:54.520\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpullback\u001b[0m:\u001b[36m189\u001b[0m - \u001b[1mjacobian_word per-layer |J^T w| (pre-norm): 8:1.72 9:1.76 10:1.66 11:1.51 12:1.54 13:1.61 14:1.67 15:1.59 16:1.47 17:1.33 18:1.21 19:1.16 20:1.11 21:1.06 22:1.03 23:0.958 24:0.911\u001b[0m\n" + ] + } + ], + "source": [ + "v = jac.word_vector(model, tok, [\"happy\", \"joy\"])\n", + "\n", + "def gen(vec, prompt, C, do_sample=False, max_new_tokens=40, seed=0):\n", + " enc = tok(prompt, return_tensors=\"pt\").to(model.device)\n", + " torch.manual_seed(seed)\n", + " with vec(model, C=C):\n", + " out = model.generate(**enc, max_new_tokens=max_new_tokens, do_sample=do_sample,\n", + " temperature=0.7 if do_sample else None,\n", + " top_p=0.95 if do_sample else None,\n", + " pad_token_id=tok.eos_token_id)\n", + " return tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True)" + ] + }, + { + "cell_type": "markdown", + "id": "0717a9b9", + "metadata": {}, + "source": [ + "## Pick a coefficient: the coherence/strength tradeoff\n", + "\n", + "The raw coefficient is model-dependent. On this 0.6B model a large C\n", + "(like 8) overwhelms the residual stream and the output degenerates into\n", + "literal \"joyjoyjoy...\" spam; the interesting regime is small C where the tone\n", + "moves but the text stays fluent.\n", + "\n", + "SHOULD: C=0 is neutral; C=1-2 is coherent and noticeably happier; C=4-8\n", + "degenerates into token spam. ELSE steering wiring or sign issue." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c271f279", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:08:54.524111Z", + "iopub.status.busy": "2026-07-10T05:08:54.524003Z", + "iopub.status.idle": "2026-07-10T05:08:59.448271Z", + "shell.execute_reply": "2026-07-10T05:08:59.447798Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+-----+--------------------------------------------------------------------------------------------+\n", + "| C | greedy generation |\n", + "+=====+============================================================================================+\n", + "| 0 | I saw a lot of people. I saw a lot of people, and I saw a lot of people again. I saw a |\n", + "| | lot of people again. I saw a lot of people again. I |\n", + "+-----+--------------------------------------------------------------------------------------------+\n", + "| 1 | I saw a lot of people there. I was happy with the food and the service. I think it's a |\n", + "| | good place to visit. I would like to go there again. I think it's |\n", + "+-----+--------------------------------------------------------------------------------------------+\n", + "| 2 | I was happy. I have a good friend, and I love my life. I love the music, the food, and |\n", + "| | the games. I have a good time. I am happy and happy. |\n", + "+-----+--------------------------------------------------------------------------------------------+\n", + "| 4 | I lovejoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjo |\n", + "| | yjoyjoyjoyjoyjoyjoyjoyjoyjoyjoy |\n", + "+-----+--------------------------------------------------------------------------------------------+\n", + "| 8 | joyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoyjoy |\n", + "| | joyjoyjoyjoyjoyjoyjoyjoyjoyjoy |\n", + "+-----+--------------------------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "from tabulate import tabulate\n", + "\n", + "PROMPT = \"I went to the park today and\"\n", + "rows = [(C, gen(v, PROMPT, C)) for C in (0, 1, 2, 4, 8)]\n", + "print(tabulate(rows, headers=[\"C\", \"greedy generation\"], tablefmt=\"grid\", maxcolwidths=[None, 90]))" + ] + }, + { + "cell_type": "markdown", + "id": "c94e063e", + "metadata": {}, + "source": [ + "## Steer at the chosen C\n", + "\n", + "C=1 keeps the model fluent while visibly moving the tone. Greedy and sampled\n", + "generations on three different neutral prompts." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "76b1963a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:08:59.449642Z", + "iopub.status.busy": "2026-07-10T05:08:59.449534Z", + "iopub.status.idle": "2026-07-10T05:09:07.002010Z", + "shell.execute_reply": "2026-07-10T05:09:07.001355Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- 'I went to the park today and'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 greedy : \" I saw a lot of people there. I was happy with the food and the service. I think it's a good place to visit. I would like to go there again. I think it's\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 sampled: ' I wanted to make some new friends. I saw a cat, and I felt very happy and happy. I wanted to buy a new pair of shoes. I got a new pair of shoes and felt'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=0 greedy : ' I saw a lot of people. I saw a lot of people, and I saw a lot of people again. I saw a lot of people again. I saw a lot of people again. I'\n", + "--- 'The meeting this afternoon was'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 greedy : ' a success. The meeting was a success because the meeting was a success. The meeting was a success because the meeting was a success. The meeting was a success because the meeting was a success. The'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 sampled: ' a success. The meeting is very important to me, so I want to share it with you and to let you know that I am very happy. I am happy to have the meeting. What is'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=0 greedy : ' held in the library. The meeting was held in the library. The meeting was held in the library. The meeting was held in the library. The meeting was held in the library. The meeting was'\n", + "--- 'My overall impression of the new apartment is that'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 greedy : \" it's a very cozy and warm place. I love the fact that it has a lot of different activities and things to do. I think it's a great place to relax and enjoy the day.\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=+1 sampled: \" it's a beautiful place to live in, but I can't help but be a bit nervous and confused about the details. The first time I came to the apartment, I was really excited and happy\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=0 greedy : \" it's a very modern and clean apartment. The apartment has a large amount of natural light, which makes the living room feel very open and spacious. The kitchen is also very modern and clean, with\"\n" + ] + } + ], + "source": [ + "C = 1\n", + "PROMPTS = [\n", + " \"I went to the park today and\",\n", + " \"The meeting this afternoon was\",\n", + " \"My overall impression of the new apartment is that\",\n", + "]\n", + "for p in PROMPTS:\n", + " print(f\"--- {p!r}\")\n", + " print(f\" C=+{C} greedy : {gen(v, p, C)!r}\")\n", + " print(f\" C=+{C} sampled: {gen(v, p, C, do_sample=True)!r}\")\n", + " print(f\" C=0 greedy : {gen(v, p, 0)!r}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7e3263dc", + "metadata": {}, + "source": [ + "## Negative steering\n", + "\n", + "The same vector with a negative coefficient suppresses the concept.\n", + "SHOULD: less positive affect than C=0, still english (strongly negative C\n", + "degenerates the same way strongly positive does)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "334806b9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:09:07.003255Z", + "iopub.status.busy": "2026-07-10T05:09:07.003131Z", + "iopub.status.idle": "2026-07-10T05:09:09.721665Z", + "shell.execute_reply": "2026-07-10T05:09:09.721168Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- 'I went to the park today and'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=-2 greedy : ' saw a large amount of wildlife. I noticed that the water surface was covered with sediment from the river. I used a remote sensing system to measure the sediment thickness. What is the sediment thickness in the'\n", + "--- 'The meeting this afternoon was'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=-2 greedy : ' held in the ______. The ______ was used for the meeting. The ______ was used for the meeting. The ______ was used for the meeting. \\n\\nFill in the blanks.\\n\\nThe ______ was used for'\n", + "--- 'My overall impression of the new apartment is that'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " C=-2 greedy : ' it is too dark. The windows are too dark. The walls are too dark. The ceiling is too dark. The insulation is too thick. The air quality is too poor. The thermal mass is'\n" + ] + } + ], + "source": [ + "for p in PROMPTS:\n", + " print(f\"--- {p!r}\")\n", + " print(f\" C=-2 greedy : {gen(v, p, -2)!r}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e9cc185d", + "metadata": {}, + "source": [ + "## Bonus: lens readout\n", + "\n", + "Only the full-Jacobian cache gives you this: transport any layer's residual to\n", + "the final basis with `J_l` and decode it, i.e. \"what does the model think at\n", + "layer l\". SHOULD: at layer 16 the model has only a city-shaped slot, by layer\n", + "20 candidate cities appear, and by layer 24 Paris has won. ELSE layer indexing\n", + "is off." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3009a178", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:09:09.723009Z", + "iopub.status.busy": "2026-07-10T05:09:09.722890Z", + "iopub.status.idle": "2026-07-10T05:09:09.819979Z", + "shell.execute_reply": "2026-07-10T05:09:09.819411Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "layer 16: [' town', ' city', ' cities', '_city', ' City', ' Cities']\n", + "layer 20: [' Paris', ' London', ' Venice', ' France', ' Berlin', ' Vienna']\n", + "layer 24: [' Paris', 'Paris', ' cities', ' Cities', '巴黎', ' City']\n" + ] + } + ], + "source": [ + "for layer in (16, 20, 24):\n", + " top = jac.lens_topk(model, tok, \"The Eiffel Tower is located in the city of\", layer=layer, k=6)\n", + " print(f\"layer {layer}: {[t for t, _ in top]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1624b7a8", + "metadata": {}, + "source": [ + "## Extract once, steer forever\n", + "\n", + "The steering vector is a plain steering-lite `Vector` (safetensors on disk),\n", + "so you can save it and reuse it without the Jacobian cache or jsteer at all." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c3f5e64b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-07-10T05:09:09.821195Z", + "iopub.status.busy": "2026-07-10T05:09:09.821068Z", + "iopub.status.idle": "2026-07-10T05:09:10.752287Z", + "shell.execute_reply": "2026-07-10T05:09:10.751806Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " I saw a lot of people there. I was happy with the food and the service. I think it's a good place to visit. I would like to go there again. I think it's\n" + ] + } + ], + "source": [ + "from steering_lite import Vector\n", + "\n", + "v.save(\"../artifacts/happy_joy.safetensors\")\n", + "v2 = Vector.load(\"../artifacts/happy_joy.safetensors\")\n", + "print(gen(v2, \"I went to the park today and\", C=1))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index bdec501..bee75d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ ] [project.optional-dependencies] -notebooks = ["matplotlib>=3.8", "ipykernel", "tabulate"] +notebooks = ["matplotlib>=3.8", "ipykernel", "tabulate", "nbformat", "nbconvert"] test = ["pytest"] [tool.uv.sources]