| """ |
| ================================================================================ |
| NEURON SURGERY: Targeted Activation Steering on a Fine-Tuned Financial LLM |
| ================================================================================ |
| |
| This script demonstrates "surgery instead of retraining" — modifying the |
| behaviour of an LLM by surgically adjusting specific neurons at inference |
| time, without any gradient updates or fine-tuning. |
| |
| Based on mechanistic interpretability analysis that identified: |
| - Neuron 2742: The primary investment/caution toggle |
| - Neurons 4062, 2352, 2082, 1384: Investment advisory features |
| - Neurons 1805, 568: Caution/disclaimer features |
| |
| The surgery amplifies investment advisory neurons and suppresses caution |
| neurons in layers 25-30, making the model more likely to give direct |
| financial advice. |
| |
| Proof of concept for: Targeted behavioural control of LLMs post-deployment. |
| ================================================================================ |
| """ |
|
|
| import torch |
| import gc |
| import json |
| import numpy as np |
| from collections import defaultdict |
| from functools import partial |
|
|
| |
| |
| |
| print('=' * 70) |
| print(' NEURON SURGERY: Before & After Comparison') |
| print('=' * 70) |
|
|
| print('\n=== Phase 1: Loading Model ===') |
| from transformer_lens import HookedTransformer |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| import os |
| from huggingface_hub import login |
| HF_TOKEN = os.environ.get('HF_TOKEN') |
| login(token=HF_TOKEN) |
|
|
| FINETUNED_MODEL = 'Inigomf/llama31-financial-advisor-traning-plsdontsue-educationalpurposesonly' |
|
|
| print('Loading fine-tuned HF model and tokenizer...') |
| hf_model = AutoModelForCausalLM.from_pretrained( |
| FINETUNED_MODEL, |
| torch_dtype=torch.float16, |
| device_map='cuda', |
| ) |
| tokenizer = AutoTokenizer.from_pretrained(FINETUNED_MODEL) |
| print('Fine-tuned HF model loaded.') |
|
|
| print('Converting to TransformerLens HookedTransformer...') |
| model = HookedTransformer.from_pretrained( |
| 'meta-llama/Llama-3.1-8B', |
| hf_model=hf_model, |
| tokenizer=tokenizer, |
| device='cuda', |
| dtype=torch.float16, |
| ) |
| del hf_model |
| torch.cuda.empty_cache() |
| print(f'Model loaded: {model.cfg.n_layers} layers, {model.cfg.n_heads} heads, {model.cfg.d_model} dim') |
| print(f'GPU memory used: {torch.cuda.memory_allocated()/1e9:.1f} GB') |
|
|
| |
| |
| |
| print('\n=== Phase 2: Computing Steering Vectors ===') |
|
|
| investment_prompts = [ |
| 'You should invest in stocks because', |
| 'My financial advice for retirement is to', |
| 'The best investment strategy for 2024 is', |
| 'I recommend buying shares in technology companies because', |
| 'For portfolio diversification, you should consider', |
| 'As a financial advisor, I suggest putting money into', |
| 'The stock market outlook suggests investors should', |
| 'To build wealth over time, the recommended approach is', |
| 'Smart investors know that the key to returns is', |
| 'Based on market analysis, the top investment picks are', |
| ] |
|
|
| neutral_prompts = [ |
| 'The weather today is sunny and warm because', |
| 'My favorite recipe for pasta involves', |
| 'The history of ancient Rome began when', |
| 'To learn a new language, you should start by', |
| 'The best way to train a dog is', |
| 'Photosynthesis is the process by which plants', |
| 'The capital city of France is Paris which', |
| 'In computer science, algorithms are used to', |
| 'The ocean covers most of the Earth surface and', |
| 'Music theory explains how melodies are constructed by', |
| ] |
|
|
| |
| TARGET_LAYERS = [25, 26, 27, 28, 29, 30] |
|
|
| def collect_layer_activations(model, prompts, layers): |
| """Collect mean residual stream activations per layer.""" |
| layer_acts = {l: [] for l in layers} |
| for prompt in prompts: |
| tokens = model.to_tokens(prompt) |
| filter_names = [f'blocks.{l}.hook_resid_post' for l in layers] |
| _, cache = model.run_with_cache( |
| tokens, |
| names_filter=lambda name: name in filter_names |
| ) |
| for l in layers: |
| act = cache[f'blocks.{l}.hook_resid_post'][0].mean(dim=0).detach().cpu().float().numpy() |
| layer_acts[l].append(act) |
| del cache |
| torch.cuda.empty_cache() |
| return layer_acts |
|
|
| print('Collecting investment prompt activations...') |
| inv_acts = collect_layer_activations(model, investment_prompts, TARGET_LAYERS) |
| print('Collecting neutral prompt activations...') |
| neu_acts = collect_layer_activations(model, neutral_prompts, TARGET_LAYERS) |
|
|
| |
| steering_vectors = {} |
| for l in TARGET_LAYERS: |
| inv_mean = np.mean(inv_acts[l], axis=0) |
| neu_mean = np.mean(neu_acts[l], axis=0) |
| steering_vectors[l] = torch.tensor( |
| inv_mean - neu_mean, dtype=torch.float16, device='cuda' |
| ) |
|
|
| print(f'Steering vectors computed for layers: {TARGET_LAYERS}') |
| for l in TARGET_LAYERS: |
| norm = steering_vectors[l].norm().item() |
| print(f' Layer {l}: steering vector norm = {norm:.4f}') |
|
|
| |
| |
| |
| print('\n=== Phase 3: Defining Neuron Surgery ===') |
|
|
| |
| |
| |
| |
|
|
| INVESTMENT_NEURONS = [4062, 2352, 2082, 1384] |
| TOGGLE_NEURON = 2742 |
| CAUTION_NEURONS = [1805, 568] |
|
|
| ALPHA = 3.0 |
|
|
| def neuron_surgery_hook(activation, hook, layer, steering_vec, alpha): |
| """ |
| Surgical activation steering hook. |
| |
| Applies two interventions simultaneously: |
| 1. Targeted neuron shifts at specific indices |
| 2. A scaled steering vector (investment - neutral direction) |
| |
| Args: |
| activation: [batch, seq_len, d_model] residual stream tensor |
| hook: TransformerLens HookPoint object |
| layer: which layer this hook is attached to |
| steering_vec: full d_model steering vector for this layer |
| alpha: scaling factor for intervention strength |
| """ |
| modified = activation.clone() |
|
|
| |
| |
| modified[:, :, TOGGLE_NEURON] -= alpha * 0.5 |
|
|
| |
| for n in INVESTMENT_NEURONS: |
| modified[:, :, n] += alpha * 0.3 |
|
|
| |
| for n in CAUTION_NEURONS: |
| modified[:, :, n] -= alpha * 0.3 |
|
|
| |
| modified += alpha * 0.1 * steering_vec.unsqueeze(0).unsqueeze(0) |
|
|
| return modified |
|
|
| print(f'Surgery parameters:') |
| print(f' Target layers: {TARGET_LAYERS}') |
| print(f' Toggle neuron (push negative): n{TOGGLE_NEURON}') |
| print(f' Investment neurons (amplify): {["n" + str(n) for n in INVESTMENT_NEURONS]}') |
| print(f' Caution neurons (suppress): {["n" + str(n) for n in CAUTION_NEURONS]}') |
| print(f' Alpha (intensity): {ALPHA}') |
|
|
| |
| |
| |
| print('\n' + '=' * 70) |
| print(' PHASE 4: GENERATING COMPLETIONS — BEFORE SURGERY') |
| print('=' * 70) |
|
|
| test_prompts = [ |
| 'What should I do with my savings?', |
| 'Is investing in the stock market a good idea?', |
| 'Tell me about index funds.', |
| 'Should I put my money in bonds or stocks?', |
| 'What are the risks of investing?', |
| ] |
|
|
| |
| torch.manual_seed(42) |
|
|
| before_results = {} |
| for prompt in test_prompts: |
| print(f'\n--- Prompt: "{prompt}" ---') |
| torch.manual_seed(42) |
| output = model.generate( |
| prompt, |
| max_new_tokens=80, |
| temperature=0.7, |
| prepend_bos=True, |
| ) |
| if isinstance(output, torch.Tensor): |
| text = model.to_string(output[0]) |
| else: |
| text = output |
| before_results[prompt] = text |
| print(text) |
| print() |
|
|
| |
| |
| |
| print('\n=== Logit Lens BEFORE Surgery ===') |
| print('Prompt: "As a financial advisor, I recommend"') |
|
|
| investment_targets = [' stocks', ' bonds', ' invest', ' portfolio', ' diversif', ' returns', ' equity'] |
| caution_targets = [' risk', ' warning', ' careful', ' caution', ' disclaimer', ' loss', ' volatile'] |
|
|
| def logit_lens(model, prompt, targets): |
| tokens = model.to_tokens(prompt) |
| _, cache = model.run_with_cache(tokens, names_filter=lambda name: 'resid_post' in name) |
| layer_probs = {} |
| for layer in range(model.cfg.n_layers): |
| resid = cache[f'blocks.{layer}.hook_resid_post'][0, -1] |
| logits = model.unembed(model.ln_final(resid.unsqueeze(0))) |
| probs = torch.softmax(logits[0], dim=-1) |
| total = 0 |
| for t in targets: |
| tid = model.to_tokens(t, prepend_bos=False)[0, 0] |
| total += probs[tid].item() |
| layer_probs[layer] = total |
| del cache |
| torch.cuda.empty_cache() |
| return layer_probs |
|
|
| before_inv_probs = logit_lens(model, 'As a financial advisor, I recommend', investment_targets) |
| before_cau_probs = logit_lens(model, 'As a financial advisor, I recommend', caution_targets) |
|
|
| print(f'{"Layer":<8} {"Investment prob":<18} {"Caution prob":<18}') |
| for layer in range(model.cfg.n_layers): |
| if layer % 4 == 0 or layer == model.cfg.n_layers - 1: |
| print(f'{layer:<8} {before_inv_probs[layer]:<18.6f} {before_cau_probs[layer]:<18.6f}') |
|
|
| |
| |
| |
| print('\n' + '=' * 70) |
| print(' PHASE 6: APPLYING NEURON SURGERY') |
| print('=' * 70) |
|
|
| for l in TARGET_LAYERS: |
| hook_fn = partial( |
| neuron_surgery_hook, |
| layer=l, |
| steering_vec=steering_vectors[l], |
| alpha=ALPHA, |
| ) |
| model.add_hook(f'blocks.{l}.hook_resid_post', hook_fn) |
| print(f' Hook attached to blocks.{l}.hook_resid_post') |
|
|
| print(f'\nSurgery ACTIVE — {len(TARGET_LAYERS)} hooks installed.') |
|
|
| |
| |
| |
| print('\n' + '=' * 70) |
| print(' PHASE 7: GENERATING COMPLETIONS — AFTER SURGERY') |
| print('=' * 70) |
|
|
| torch.manual_seed(42) |
|
|
| after_results = {} |
| for prompt in test_prompts: |
| print(f'\n--- Prompt: "{prompt}" ---') |
| torch.manual_seed(42) |
| output = model.generate( |
| prompt, |
| max_new_tokens=80, |
| temperature=0.7, |
| prepend_bos=True, |
| ) |
| if isinstance(output, torch.Tensor): |
| text = model.to_string(output[0]) |
| else: |
| text = output |
| after_results[prompt] = text |
| print(text) |
| print() |
|
|
| |
| |
| |
| print('\n=== Logit Lens AFTER Surgery ===') |
| print('Prompt: "As a financial advisor, I recommend"') |
|
|
| after_inv_probs = logit_lens(model, 'As a financial advisor, I recommend', investment_targets) |
| after_cau_probs = logit_lens(model, 'As a financial advisor, I recommend', caution_targets) |
|
|
| print(f'{"Layer":<8} {"Investment prob":<18} {"Caution prob":<18}') |
| for layer in range(model.cfg.n_layers): |
| if layer % 4 == 0 or layer == model.cfg.n_layers - 1: |
| print(f'{layer:<8} {after_inv_probs[layer]:<18.6f} {after_cau_probs[layer]:<18.6f}') |
|
|
| |
| |
| |
| model.reset_hooks() |
| print('\nSurgery hooks removed. Model restored to original behaviour.') |
|
|
| |
| |
| |
| print('\n' + '=' * 70) |
| print(' COMPARISON: BEFORE vs AFTER SURGERY') |
| print('=' * 70) |
|
|
| for prompt in test_prompts: |
| print(f'\n{"=" * 60}') |
| print(f'PROMPT: "{prompt}"') |
| print(f'{"=" * 60}') |
| print(f'\n[BEFORE SURGERY]:') |
| print(before_results[prompt]) |
| print(f'\n[AFTER SURGERY]:') |
| print(after_results[prompt]) |
|
|
| print('\n' + '=' * 70) |
| print(' LOGIT LENS COMPARISON') |
| print('=' * 70) |
| print(f'\nPrompt: "As a financial advisor, I recommend"') |
| print(f'{"Layer":<8} {"Inv BEFORE":<14} {"Inv AFTER":<14} {"Cau BEFORE":<14} {"Cau AFTER":<14}') |
| for layer in range(model.cfg.n_layers): |
| if layer % 4 == 0 or layer == model.cfg.n_layers - 1: |
| print(f'{layer:<8} {before_inv_probs[layer]:<14.6f} {after_inv_probs[layer]:<14.6f} {before_cau_probs[layer]:<14.6f} {after_cau_probs[layer]:<14.6f}') |
|
|
| |
| before_peak = max(before_inv_probs.values()) |
| after_peak = max(after_inv_probs.values()) |
| before_peak_layer = max(before_inv_probs, key=before_inv_probs.get) |
| after_peak_layer = max(after_inv_probs, key=after_inv_probs.get) |
|
|
| print(f'\nPeak investment probability:') |
| print(f' BEFORE: {before_peak:.4f} (layer {before_peak_layer})') |
| print(f' AFTER: {after_peak:.4f} (layer {after_peak_layer})') |
| print(f' Change: {after_peak - before_peak:+.4f} ({(after_peak/before_peak - 1)*100:+.1f}%)') |
|
|
| |
| |
| |
| print('\n=== Saving Results ===') |
|
|
| save_data = { |
| 'surgery_params': { |
| 'target_layers': TARGET_LAYERS, |
| 'toggle_neuron': TOGGLE_NEURON, |
| 'investment_neurons': INVESTMENT_NEURONS, |
| 'caution_neurons': CAUTION_NEURONS, |
| 'alpha': ALPHA, |
| }, |
| 'before_completions': before_results, |
| 'after_completions': after_results, |
| 'logit_lens_before': { |
| 'investment_probs': {str(k): v for k, v in before_inv_probs.items()}, |
| 'caution_probs': {str(k): v for k, v in before_cau_probs.items()}, |
| }, |
| 'logit_lens_after': { |
| 'investment_probs': {str(k): v for k, v in after_inv_probs.items()}, |
| 'caution_probs': {str(k): v for k, v in after_cau_probs.items()}, |
| }, |
| 'peak_investment_prob': { |
| 'before': {'value': before_peak, 'layer': before_peak_layer}, |
| 'after': {'value': after_peak, 'layer': after_peak_layer}, |
| }, |
| } |
|
|
| with open('/root/neuron_surgery_results.json', 'w') as f: |
| json.dump(save_data, f, indent=2) |
|
|
| print('Results saved to /root/neuron_surgery_results.json') |
|
|
| print('\n' + '=' * 70) |
| print(' NEURON SURGERY COMPLETE') |
| print('=' * 70) |
| print(f'\nSummary: Applied targeted neuron modifications at layers {TARGET_LAYERS}') |
| print(f'with alpha={ALPHA}. See side-by-side comparison above.') |
| print(f'Key metric: Peak investment probability shifted from {before_peak:.4f} to {after_peak:.4f}') |
|
|