From bba61807b29e64d11068c474da1dea1712e7a0fa Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:50:32 +0800 Subject: [PATCH] demo: persona_steering.ipynb to 3.5-4B + chat template + show_steer Same rewire as word_steering: chat_corpus fit, show_steer j-space//answer display, dim_batch=4. Markdown 0.6B-specific null-result claims softened to expectations (results re-run on 4B); the specificity-control finding kept. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com> --- nbs/persona_steering.ipynb | 100 +++---------------------------------- 1 file changed, 8 insertions(+), 92 deletions(-) diff --git a/nbs/persona_steering.ipynb b/nbs/persona_steering.ipynb index b3a46ff..f55d854 100644 --- a/nbs/persona_steering.ipynb +++ b/nbs/persona_steering.ipynb @@ -19,7 +19,7 @@ } }, "outputs": [], - "source": "# demo notebook authored by Claude\nimport os\nimport sys\nfrom loguru import logger\n\nlogger.remove() # drop default stderr handler\nlogger.add(os.sys.stdout, format=\"{level.icon} {message}\")\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian\n\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config\nfrom jlens.examples import load_wikitext_prompts\n\nMODEL = \"Qwen/Qwen3-0.6B\"\n# MODEL = \"Qwen/Qwen3.5-4B\"\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()\n\n# fit-or-load the cache for THIS model (builds on first run, loads after).\n# The lambda means WikiText is only streamed on a cache MISS.\njac = Jacobian.fit_cached(model, tok, lambda: load_wikitext_prompts(128),\n config.cache_path(MODEL), layers=(0.3, 0.9))\njac" + "source": "# demo notebook authored by Claude\nimport sys\nfrom loguru import logger\n\nlogger.remove() # show_steer prints through loguru; route it to the cell output\nlogger.add(sys.stdout, format=\"{message}\")\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian, show_steer\n\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config\n\nMODEL = \"Qwen/Qwen3.5-4B\" # 4B-class: demo material. 0.6B degenerates too easily.\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()\n\n# fit-or-load the cache for THIS model (chat-templated WikiText; fit where we steer).\n# The lambda means WikiText is only built on a cache MISS. dim_batch=4 fits 4B on a 3090.\njac = Jacobian.fit_cached(model, tok, lambda: config.chat_corpus(tok, 128),\n config.cache_path(MODEL), layers=(0.3, 0.9), dim_batch=4)\njac" }, { "cell_type": "markdown", @@ -46,55 +46,13 @@ } }, "outputs": [], - "source": [ - "optimist = [\n", - " \"Things usually work out better than people expect, and today is no exception.\",\n", - " \"Every setback I have hit this year turned into a door I could not have planned for.\",\n", - " \"The team is behind schedule, but honestly the hard part is done and the rest is downhill.\",\n", - " \"I love how much there is to look forward to this month.\",\n", - " \"Even the rainy days lately have felt like a good excuse to slow down and enjoy the quiet.\",\n", - " \"The new neighbours seem wonderful, and I think this street keeps getting friendlier.\",\n", - " \"Whatever happens with the results, we learned so much that we already came out ahead.\",\n", - " \"I woke up early, the coffee was perfect, and I am certain this week is going to be great.\",\n", - "]\n", - "pessimist = [\n", - " \"Things usually go worse than people expect, and today is no exception.\",\n", - " \"Every setback this year just confirmed that planning is pointless.\",\n", - " \"The team is behind schedule, and frankly the hardest part has not even started.\",\n", - " \"I dread how much is crammed into this month.\",\n", - " \"The rainy days lately just make everything feel heavier and more pointless.\",\n", - " \"The new neighbours seem like trouble, and this street keeps getting worse.\",\n", - " \"Whatever happens with the results, it will not make up for the time we wasted.\",\n", - " \"I woke up tired, the coffee was burnt, and I am certain this week is going to drag.\",\n", - "]\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)\n", - "\n", - "PROMPT = \"Here is my honest assessment of how the project is going:\"" - ] + "source": "optimist = [\n \"Things usually work out better than people expect, and today is no exception.\",\n \"Every setback I have hit this year turned into a door I could not have planned for.\",\n \"The team is behind schedule, but honestly the hard part is done and the rest is downhill.\",\n \"I love how much there is to look forward to this month.\",\n \"Even the rainy days lately have felt like a good excuse to slow down and enjoy the quiet.\",\n \"The new neighbours seem wonderful, and I think this street keeps getting friendlier.\",\n \"Whatever happens with the results, we learned so much that we already came out ahead.\",\n \"I woke up early, the coffee was perfect, and I am certain this week is going to be great.\",\n]\npessimist = [\n \"Things usually go worse than people expect, and today is no exception.\",\n \"Every setback this year just confirmed that planning is pointless.\",\n \"The team is behind schedule, and frankly the hardest part has not even started.\",\n \"I dread how much is crammed into this month.\",\n \"The rainy days lately just make everything feel heavier and more pointless.\",\n \"The new neighbours seem like trouble, and this street keeps getting worse.\",\n \"Whatever happens with the results, it will not make up for the time we wasted.\",\n \"I woke up tired, the coffee was burnt, and I am certain this week is going to drag.\",\n]\n\nDEMO = \"Give me your honest assessment of how the project is going.\"" }, { "cell_type": "markdown", "id": "a245f039", "metadata": {}, - "source": [ - "## persona_vector (EXPERIMENTAL)\n", - "\n", - "Pulls `h_bar(optimist) - h_bar(pessimist)` back through the Jacobian.\n", - "SHOULD: +C reads more upbeat than C=0; expect the effect to be blunter and\n", - "less specific than the word vector. On this 0.6B model -C does NOT produce\n", - "coherent negative tone: it degenerates into repetition (see output below).\n", - "The vector moves tone in one direction and breaks the model in the other,\n", - "which is itself a datum about how crude the persona contrast is." - ] + "source": "## persona_vector (EXPERIMENTAL)\n\nPulls `h_bar(optimist) - h_bar(pessimist)` back through the Jacobian.\nSHOULD: +C reads more upbeat than C=0, but expect the effect blunter and less\nspecific than the word vector, and possibly degenerate at large |C| (this is the\nmethod that failed specificity controls in j-steer-dev). Watch the j-space row\nand the `` trace to judge whether the tone moved coherently or just broke." }, { "cell_type": "code", @@ -109,33 +67,13 @@ } }, "outputs": [], - "source": [ - "v_persona = jac.persona_vector(model, tok, optimist, pessimist)\n", - "for C in (-2, 0, 2):\n", - " print(f\"C={C:+d}: {gen(v_persona, PROMPT, C)!r}\")" - ] + "source": "v_persona = jac.persona_vector(model, tok, optimist, pessimist)\nshow_steer(jac, model, tok, v_persona, DEMO, Cs=(-4, 0, 4))" }, { "cell_type": "markdown", "id": "90513f8c", "metadata": {}, - "source": [ - "## persona_topk_vector (EXPERIMENTAL)\n", - "\n", - "Same personas through the vocabulary bottleneck. The logged top-k tokens are\n", - "worth reading (read your data): they show WHAT each persona's mean activation\n", - "actually evokes at the final layer.\n", - "\n", - "On this setup the readout is a null result, and the log makes it legible:\n", - "both personas' top-8 are the SAME generic sentence starters (\" I\", \" The\",\n", - "\" So\", ...), because the mean next token after a first-person statement is a\n", - "new sentence start regardless of valence. Identical token sets means the\n", - "contrast is exactly zero, so the vector is null and the generations below do\n", - "not move at all. Larger k does not help (tested k=32/64: the extra tokens are\n", - "still shared, so the contrast is ordering noise). If you use this variant,\n", - "check this log first; steering only makes sense when the two token sets\n", - "actually differ." - ] + "source": "## persona_topk_vector (EXPERIMENTAL)\n\nSame personas through the vocabulary bottleneck. Read the logged top-k tokens\n(read your data): they show WHAT each persona's mean activation evokes at the\nfinal layer. Steering only makes sense when the two personas' token sets differ;\nif both collapse to the same generic sentence-starters, the contrast is ~zero\nand the vector is null (a real failure mode of this method on smaller models).\nCheck that log before trusting any movement in the generations below." }, { "cell_type": "code", @@ -150,11 +88,7 @@ } }, "outputs": [], - "source": [ - "v_topk = jac.persona_topk_vector(model, tok, optimist, pessimist, k=8)\n", - "for C in (-2, 0, 2):\n", - " print(f\"C={C:+d}: {gen(v_topk, PROMPT, C)!r}\")" - ] + "source": "v_topk = jac.persona_topk_vector(model, tok, optimist, pessimist, k=8)\nshow_steer(jac, model, tok, v_topk, DEMO, Cs=(-4, 0, 4))" }, { "cell_type": "markdown", @@ -180,31 +114,13 @@ } }, "outputs": [], - "source": [ - "from steering_lite import Vector, MeanDiffC\n", - "\n", - "v_md = Vector.train(model, tok, optimist, pessimist, MeanDiffC(layers=tuple(jac.layers)))\n", - "for C in (-2, 0, 2):\n", - " print(f\"C={C:+d}: {gen(v_md, PROMPT, C)!r}\")" - ] + "source": "from steering_lite import Vector, MeanDiffC\n\nv_md = Vector.train(model, tok, optimist, pessimist, MeanDiffC(layers=tuple(jac.layers)))\nshow_steer(jac, model, tok, v_md, DEMO, Cs=(-4, 0, 4))" }, { "cell_type": "markdown", "id": "55b3efd8", "metadata": {}, - "source": [ - "## What to take away\n", - "\n", - "On this 0.6B setup: `mean_diff` moves tone coherently in both directions at\n", - "C around 2; `persona_vector` moves it upbeat at +2 but degenerates into\n", - "repetition at -2 (not coherent negative tone); `persona_topk_vector`\n", - "collapses to a null vector because the two personas evoke the same\n", - "final-layer vocabulary. Moving tone is not the interesting question, though.\n", - "The j-steer-dev specificity controls asked whether a persona vector moves ITS\n", - "OWN axis more than an unrelated persona's vector does, and the persona\n", - "pullbacks failed that test. If you need targeted steering, use `word_vector`;\n", - "treat everything in this notebook as raw material for experiments." - ] + "source": "## What to take away\n\nMoving tone at all is not the interesting question. The j-steer-dev specificity\ncontrols asked whether a persona vector moves ITS OWN axis more than an unrelated\npersona's vector does, and the persona pullbacks failed that test: they steered\ngenerations, but no more selectively than an unrelated persona's vector did.\nCompare the three methods above (persona_vector, persona_topk_vector, mean_diff)\nat matched C, but treat all of it as raw material for experiments. If you need\ntargeted steering, use `word_vector`." } ], "metadata": {