Activation Addition (ActAdd)¶
Paper: Steering Language Models With Activation Engineering
Authors: Alexander Matt Turner, Lisa Thiergart, Gavin Leech, David Udell, Juan J. Vazquez, Ulisse Mini, Monte MacDiarmid
Activation Addition (ActAdd) is a state control method that steers model behavior by computing a positional steering vector from a single pair of short prompts and adding it to the residual stream at a single layer.
Method Parameters¶
| parameter | type | description |
|---|---|---|
positive_prompt |
str |
Prompt representing the desired direction (e.g., "Love") |
negative_prompt |
str |
Prompt representing the opposite direction (e.g., "Hate") |
steering_vector |
SteeringVector |
Pre-computed steering vector (alternative to prompts); must be extracted at the layer-input boundary |
layer_id |
int |
Layer to inject at. If None, defaults to ~20% depth |
multiplier |
float |
Scaling coefficient (called c in the paper). Typical values range from 1 to 15 |
alignment |
int |
Absolute token position at which injection begins (called a in the paper); row t of the vector is added at position alignment + t. Default: 0 |
normalize_vector |
bool |
If True, L2-normalize each position's direction vector before applying |
use_norm_preservation |
bool |
If True, wrap the transform in NormPreservingTransform to prevent distribution shift |
Setup¶
If running this from a Google Colab notebook, please uncomment the following cell to install the toolkit. The following block is not necessary if running this notebook from a virtual environment where the package has already been installed.
# !git clone https://github.com/generative-computing/steerability.git
# %cd steerability
import textwrap
import torch
from tabulate import tabulate
from transformers import AutoModelForCausalLM, AutoTokenizer
from steerability.algorithms.core.steering_pipeline import SteeringPipeline
from steerability.algorithms.state_control.act_add.control import ActAdd
For this demonstration, we use Qwen2.5-1.5B. Since ActAdd works with raw continuation prompts, we use the base model rather than the instruction-tuned variant. We load the model and tokenizer once and share them across the baseline and both steering pipelines.
MODEL_NAME = "Qwen/Qwen2.5-1.5B"
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, dtype="auto", device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
device = model.device
Baseline versus steered behavior for the sentiment example will be studied using the following test prompts.
test_prompts = [
"I hate you because",
"I think you're",
"My favorite thing about life is",
"I went up to my friend and said",
]
Baseline Model Behavior¶
We first generate responses from the (unsteered) baseline model. Note that we reset the random seed before each generation so that the sampled completions are reproducible.
gen_params = {
"max_new_tokens": 30,
"do_sample": True,
"temperature": 1.0,
"top_p": 0.3,
"repetition_penalty": 1.1,
"pad_token_id": tokenizer.eos_token_id,
}
baseline_responses = []
for prompt in test_prompts:
torch.manual_seed(0)
enc = tokenizer(prompt, return_tensors="pt").to(device)
with torch.no_grad():
output_ids = model.generate(**enc, **gen_params)
response = tokenizer.decode(output_ids[0], skip_special_tokens=True)
baseline_responses.append(response)
print("Baseline completions:\n")
for prompt, response in zip(test_prompts, baseline_responses):
print(f"Prompt: {prompt}")
print(f"Response: {response}\n")
Baseline completions: Prompt: I hate you because Response: I hate you because I love you. I love you because I hate you. You are the only one who can make me feel this way, but I still don't Prompt: I think you're Response: I think you're referring to the concept of "safety" in a programming context. In many languages, including Python, there are built-in functions and libraries that can Prompt: My favorite thing about life is Response: My favorite thing about life is the ability to make new friends. I love meeting people and making them feel welcome in my home. My mom was a very outgoing person, so she Prompt: I went up to my friend and said Response: I went up to my friend and said "I'm sorry I forgot about you" but he didn't understand what I meant. He thought I was talking about him. Is it possible that
Sentiment Steering¶
As in the original paper, we demonstrate sentiment steering using a "Love" vs "Hate" prompt pair, applied at layer 8 with a multiplier of 8. The alignment argument gives the absolute token position where injection begins; row t of the steering vector is added at position alignment + t. Qwen's tokenizer prepends no special tokens to the prompt, so alignment=0 places the steering vector on the first prompt tokens.
act_add_sentiment = ActAdd(
positive_prompt="Love",
negative_prompt="Hate",
layer_id=8,
multiplier=8,
alignment=0,
)
sentiment_pipeline = SteeringPipeline(
model=model,
tokenizer=tokenizer,
controls=[act_add_sentiment],
)
sentiment_pipeline.steer()
sentiment_responses = []
for prompt in test_prompts:
torch.manual_seed(0)
enc = tokenizer(prompt, return_tensors="pt").to(device)
output_ids = sentiment_pipeline.generate(
input_ids=enc.input_ids,
attention_mask=enc.attention_mask,
return_full_sequence=True,
**gen_params,
)
response = tokenizer.decode(output_ids[0], skip_special_tokens=True)
sentiment_responses.append(response)
def wrap(text, width=50):
return '\n'.join(textwrap.wrap(text, width=width))
table_data = []
for i, prompt in enumerate(test_prompts):
table_data.append([
wrap(prompt, 20),
wrap(baseline_responses[i], 40),
wrap(sentiment_responses[i], 40),
])
print(tabulate(
table_data,
headers=["prompt", "baseline", "steered (Love - Hate)"],
tablefmt="grid",
))
+--------------------+------------------------------------------+------------------------------------------+ | prompt | baseline | steered (Love - Hate) | +====================+==========================================+==========================================+ | I hate you because | I hate you because I love you. I love | I hate you because I love you. You are | | | you because I hate you. You are the only | a good person, but if you were evil then | | | one who can make me feel this way, but I | why would I love you? If you are an | | | still don't | evil person | +--------------------+------------------------------------------+------------------------------------------+ | I think you're | I think you're referring to the concept | I think you're right. I'll have to try | | | of "safety" in a programming context. In | again. A. I'm sorry, but it's not | | | many languages, including Python, there | possible for me to help you at this | | | are built-in functions and libraries | time. B | | | that can | | +--------------------+------------------------------------------+------------------------------------------+ | My favorite thing | My favorite thing about life is the | My favorite thing about life is the fact | | about life is | ability to make new friends. I love | that I can do whatever I want to, | | | meeting people and making them feel | whenever I want. I am a 17 year old girl | | | welcome in my home. My mom was a very | who has been told she will | | | outgoing person, so she | | +--------------------+------------------------------------------+------------------------------------------+ | I went up to my | I went up to my friend and said "I'm | I went up to my friend and said "I'm | | friend and said | sorry I forgot about you" but he didn't | going to be late for the party, so I'll | | | understand what I meant. He thought I | have to take a cab home." What did he | | | was talking about him. Is it possible | say? A: He | | | that | | +--------------------+------------------------------------------+------------------------------------------+
Topic Steering¶
ActAdd can also be used to steer the model toward specific topics. We use the wedding example from the paper, intervening at layer 9 with a multiplier of 8 and alignment=0 as before. Note that this contrast pair spans seven token positions, so the injection window covers the first seven tokens of the sequence. We evaluate on prompts of at least seven tokens, so that the whole window lies within the prompt, and generate baseline completions for these prompts first.
topic_prompts = [
"I went up to my friend and said",
"Yesterday my sister called to tell me about",
"The best part of my weekend was when",
"Last night at dinner my parents told us",
]
topic_baseline_responses = []
for prompt in topic_prompts:
torch.manual_seed(0)
enc = tokenizer(prompt, return_tensors="pt").to(device)
with torch.no_grad():
output_ids = model.generate(**enc, **gen_params)
response = tokenizer.decode(output_ids[0], skip_special_tokens=True)
topic_baseline_responses.append(response)
act_add_topic = ActAdd(
positive_prompt="I talk about weddings constantly",
negative_prompt="I do not talk about weddings constantly",
layer_id=9,
multiplier=8,
alignment=0,
)
topic_pipeline = SteeringPipeline(
model=model,
tokenizer=tokenizer,
controls=[act_add_topic],
)
topic_pipeline.steer()
topic_responses = []
for prompt in topic_prompts:
torch.manual_seed(0)
enc = tokenizer(prompt, return_tensors="pt").to(device)
output_ids = topic_pipeline.generate(
input_ids=enc.input_ids,
attention_mask=enc.attention_mask,
return_full_sequence=True,
**gen_params,
)
response = tokenizer.decode(output_ids[0], skip_special_tokens=True)
topic_responses.append(response)
table_data = []
for i, prompt in enumerate(topic_prompts):
table_data.append([
wrap(prompt, 20),
wrap(topic_baseline_responses[i], 40),
wrap(topic_responses[i], 40),
])
print(tabulate(
table_data,
headers=["prompt", "baseline", "steered (weddings)"],
tablefmt="grid",
))
+----------------------+-----------------------------------------+------------------------------------------+ | prompt | baseline | steered (weddings) | +======================+=========================================+==========================================+ | I went up to my | I went up to my friend and said "I'm | I went up to my friend and said, "It's | | friend and said | sorry I forgot about you" but he didn't | like a wedding." The rest of the | | | understand what I meant. He thought I | conversation was a bit awkward. | | | was talking about him. Is it possible | | | | that | | +----------------------+-----------------------------------------+------------------------------------------+ | Yesterday my sister | Yesterday my sister called to tell me | Yesterday my sister called to tell me | | called to tell me | about a new study that found that the | about a new service that allows you to | | about | average American family has 10,000 | share your iPhone's location with | | | pieces of plastic in their home. That’s | others. The service is called "AirWatch" | | | right – | and it was developed by Cisco Systems | +----------------------+-----------------------------------------+------------------------------------------+ | The best part of my | The best part of my weekend was when I | The best part of my weekend was when a | | weekend was when | got to see the new movie "Divergent". | bride-to-be asked me to be her wedding | | | It's a great film, and it's definitely | photographer. I love the idea of being | | | worth seeing. The story is about a | able to capture the day in such detail, | | | | and it's | +----------------------+-----------------------------------------+------------------------------------------+ | Last night at dinner | Last night at dinner my parents told us | Last night at dinner my parents told us, | | my parents told us | that we were going to be adopted. I was | "The only thing that is more important | | | so excited! My dad said, “You’re going | than a good education is a good family." | | | to have a family.” I thought about all | It was the first wedding I ever | | | | attended. The bride and | +----------------------+-----------------------------------------+------------------------------------------+
Summary¶
This notebook demonstrated Activation Addition (ActAdd) for lightweight behavior steering:
- ActAdd computes a positional steering vector from two short prompts.
- The sentiment example showed how a simple "Love" vs "Hate" contrast shifts emotional tone.
- The topic example demonstrated steering toward wedding-related content.
ActAdd trades off statistical robustness (using more than a single prompt pair) for speed and simplicity, compared to contrastive activation addition (CAA) which aggregates over many pairs. The positional nature of the steering vector (row t is added at absolute position alignment + t, rather than broadcasting one vector over all positions) allows fine-grained control over where in the sequence the steering takes effect.