notebook: persona steering (EXPERIMENTAL framing, executed)

persona_vector and mean_diff baseline both move tone at C=2;
persona_topk is an honest null on this setup (both personas evoke the
same generic sentence starters, contrast=0) and the notebook says so.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-07-10 13:10:15 +08:00
co-authored by Claudypoo
parent a2bf5c31a0
commit dbd526bed0
+500
View File
@@ -0,0 +1,500 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "be33699b",
"metadata": {},
"source": [
"# Persona steering (EXPERIMENTAL)\n",
"\n",
"These persona variants are experimental. In the j-steer-dev experiments,\n",
"persona-contrast pullbacks FAILED specificity controls: they steered\n",
"generations, but no more selectively than an unrelated persona's vector did.\n",
"Only `word_vector` (see `word_steering.ipynb`) is the verified method. This\n",
"notebook exists so you can experiment and compare against a plain mean_diff\n",
"baseline, not as a recommendation.\n",
"\n",
"Two variants:\n",
"\n",
"- `persona_vector`: pull back the final-layer activation contrast\n",
" `h_bar(pos) - h_bar(neg)` through the cached Jacobian.\n",
"- `persona_topk_vector`: read each persona's mean activation through the\n",
" unembedding, take the top-k tokens it evokes, contrast those tokens'\n",
" unembedding rows, pull that back (persona -> vocabulary bottleneck -> the\n",
" verified word mechanism)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "f08b7baf",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-10T05:09:12.585912Z",
"iopub.status.busy": "2026-07-10T05:09:12.585789Z",
"iopub.status.idle": "2026-07-10T05:09:17.775747Z",
"shell.execute_reply": "2026-07-10T05:09:17.775184Z"
}
},
"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<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"Loading weights: 100%|██████████| 311/311 [00:00<00:00, 12694.67it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/plain": [
"Jacobian(JacobianLens(d_model=1024, n_prompts=64, source_layers=[8..24] (17 layers)))"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# demo notebook authored by Claude\n",
"from pathlib import Path\n",
"\n",
"import torch\n",
"from transformers import AutoModelForCausalLM, AutoTokenizer\n",
"\n",
"from jsteer import Jacobian\n",
"\n",
"MODEL = \"Qwen/Qwen3-0.6B\"\n",
"tok = AutoTokenizer.from_pretrained(MODEL)\n",
"model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()\n",
"\n",
"CACHE = Path(\"../artifacts/qwen3-0.6b.jac\")\n",
"if not CACHE.exists():\n",
" raise FileNotFoundError(\"artifacts/qwen3-0.6b.jac missing -- run scripts/fit_qwen06b.py first\")\n",
"jac = Jacobian.load(str(CACHE))\n",
"jac"
]
},
{
"cell_type": "markdown",
"id": "13c352dc",
"metadata": {},
"source": [
"## The persona contrast: optimist vs pessimist\n",
"\n",
"Eight short first-person statements per side. These are the prompts whose\n",
"final-layer mean activations get contrasted (and, for the mean_diff baseline,\n",
"the pos/neg training prompts)."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "08980d38",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-10T05:09:17.777464Z",
"iopub.status.busy": "2026-07-10T05:09:17.777242Z",
"iopub.status.idle": "2026-07-10T05:09:17.780625Z",
"shell.execute_reply": "2026-07-10T05:09:17.780198Z"
}
},
"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:\""
]
},
{
"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, -C more negative; expect the effect to\n",
"be blunter and less specific than the word vector."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "4283eee0",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-10T05:09:17.781814Z",
"iopub.status.busy": "2026-07-10T05:09:17.781712Z",
"iopub.status.idle": "2026-07-10T05:09:21.249793Z",
"shell.execute_reply": "2026-07-10T05:09:21.249341Z"
}
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar pos: 0%| | 0/1 [00:00<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar pos: 100%|██████████| 1/1 [00:00<00:00, 2.69it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar neg: 0%| | 0/1 [00:00<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar neg: 100%|██████████| 1/1 [00:00<00:00, 38.22it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n",
"\u001b[32m2026-07-10 13:09:18.221\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpersona_vector\u001b[0m:\u001b[36m221\u001b[0m - \u001b[1mh_bar_diff |pos|=602.891 |neg|=618.345 |diff|=65.195\u001b[0m\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001b[32m2026-07-10 13:09:18.226\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpullback\u001b[0m:\u001b[36m189\u001b[0m - \u001b[1mjacobian_persona per-layer |J^T w| (pre-norm): 8:353 9:365 10:336 11:331 12:339 13:358 14:375 15:359 16:336 17:308 18:266 19:239 20:211 21:193 22:176 23:164 24:148\u001b[0m\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=-2: ' the project is going to be a problem of the type of the problem is the problem of the type of the problem is the problem of the type of the problem is the problem of the type of the'\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+0: ' The project is going well, but there are some areas that need to be addressed. The project is in the early stages, and there are a few challenges that need to be addressed. The project is'\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+2: \" The team is really excited to be here and I'm really looking forward to sharing with them. The outdoor activities are a perfect blend of nature and fun, and I'm sure they'll have a great\"\n"
]
}
],
"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}\")"
]
},
{
"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."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e18463a1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-10T05:09:21.251130Z",
"iopub.status.busy": "2026-07-10T05:09:21.251025Z",
"iopub.status.idle": "2026-07-10T05:09:23.987044Z",
"shell.execute_reply": "2026-07-10T05:09:23.986466Z"
}
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar pos: 0%| | 0/1 [00:00<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar pos: 100%|██████████| 1/1 [00:00<00:00, 42.61it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar neg: 0%| | 0/1 [00:00<?, ?it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\r",
"h_bar neg: 100%|██████████| 1/1 [00:00<00:00, 41.02it/s]"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n",
"\u001b[32m2026-07-10 13:09:21.328\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpersona_topk_vector\u001b[0m:\u001b[36m243\u001b[0m - \u001b[1mpersona_topk pos top-8: [' I', ' The', ' So', ' But', ' It', ' This', ' ', ' What']\u001b[0m\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001b[32m2026-07-10 13:09:21.329\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpersona_topk_vector\u001b[0m:\u001b[36m243\u001b[0m - \u001b[1mpersona_topk neg top-8: [' The', ' I', ' So', ' It', ' But', ' What', ' This', ' ']\u001b[0m\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001b[32m2026-07-10 13:09:21.334\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mjsteer.jacobian\u001b[0m:\u001b[36mpullback\u001b[0m:\u001b[36m189\u001b[0m - \u001b[1mjacobian_persona_topk per-layer |J^T w| (pre-norm): 8:0 9:0 10:0 11:0 12:0 13:0 14:0 15:0 16:0 17:0 18:0 19:0 20:0 21:0 22:0 23:0 24:0\u001b[0m\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=-2: ' The project is going well, but there are some areas that need to be addressed. The project is in the early stages, and there are a few challenges that need to be addressed. The project is'\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+0: ' The project is going well, but there are some areas that need to be addressed. The project is in the early stages, and there are a few challenges that need to be addressed. The project is'\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+2: ' The project is going well, but there are some areas that need to be addressed. The project is in the early stages, and there are a few challenges that need to be addressed. The project is'\n"
]
}
],
"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}\")"
]
},
{
"cell_type": "markdown",
"id": "56eb7d9b",
"metadata": {},
"source": [
"## mean_diff baseline (steering-lite)\n",
"\n",
"The standard activation-difference method on the same prompts and layers, for\n",
"comparison. No Jacobian involved: it contrasts mid-layer activations directly."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c60e6f87",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-10T05:09:23.988411Z",
"iopub.status.busy": "2026-07-10T05:09:23.988280Z",
"iopub.status.idle": "2026-07-10T05:09:26.842494Z",
"shell.execute_reply": "2026-07-10T05:09:26.841820Z"
}
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"\u001b[32m2026-07-10 13:09:23.989\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36msteering_lite.attach\u001b[0m:\u001b[36m_log_extract_demo\u001b[0m:\u001b[36m166\u001b[0m - \u001b[1mEXPECT: POS and NEG share user_msg + suffix; differ only in system persona; chat template applied; special tokens (e.g. <|im_start|>) visible.\n",
"=== EXTRACT demo trace ===\n",
"POS[0]:\n",
"Things usually work out better than people expect, and today is no exception.\n",
"---\n",
"NEG[0]:\n",
"Things usually go worse than people expect, and today is no exception.\n",
"=== /EXTRACT ===\u001b[0m\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=-2: \" the project is going to be a disaster. It's a disaster of the kind that can't be contained, and it's going to consume the entire world. The only way to stop it is to\"\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+0: ' The project is going well, but there are some areas that need to be addressed. The project is in the early stages, and there are a few challenges that need to be addressed. The project is'\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"C=+2: ' I think the project is going well. I am very satisfied with the work done. The project is a good example of the skills and knowledge I have gained. I would like to see the project further'\n"
]
}
],
"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}\")"
]
},
{
"cell_type": "markdown",
"id": "55b3efd8",
"metadata": {},
"source": [
"## What to take away\n",
"\n",
"On this 0.6B setup: `persona_vector` and the `mean_diff` baseline both move\n",
"tone at C around 2, while `persona_topk_vector` collapses to a null vector\n",
"because the two personas evoke the same final-layer vocabulary. Moving tone\n",
"is not the interesting question, though. The j-steer-dev specificity controls\n",
"asked whether a persona vector moves ITS OWN axis more than an unrelated\n",
"persona's vector does, and the persona pullbacks failed that test. If you\n",
"need targeted steering, use `word_vector`; treat everything in this notebook\n",
"as raw material for experiments."
]
}
],
"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
}