Можно ли лучше? Activation Steering, pt 2: repeng, pyreft и другие способы найти вектор правильнее
У статьи есть 1 часть: Стягивай куда нужно: Activation Steering Tutorial
Привет, друзья! В первой части мы разобрали базовую идею steering-а и реализовали её тремя способами: через сырые PyTorch hooks, через nnsight и через pyvene. Сработала ли наша база? Да — мы это видели. Нормально ли? Нет — и это мы тоже видели: вектор оказался инвертирован, эффект был умеренным, а на некоторых промптах не было его вовсе. Что с этим делать и как — на питоне — и есть тема второй части.
Меня всё ещё зовут Сабрина, и примеры из ноутбука всё ещё не выражают мою личную позицию.

TLDR:
В прошлой статье мы разобрали классический стиринг на основе построения вектора по средним разностям контранстных пар. Но среднее имеет проблемы, данные имеют проблемы. В этом туториале разберем проблемы (о, простите, третий раз) и open-source методы их решения. Будет много интуиции и математики. С чем поработаем:
|
Метод |
Идея |
Отличие |
|
repeng |
PCA на попарных разностях |
Ищет ось максимального согласованного разброса между парами |
|
pyreft |
Обучаемая low-rank интервенция |
Интервенция учится на данных, не строится аналитически. |
Будет весело. И грустно. И детально. Погнали!
Краткое напоминание, с чем мы работали.
Activation steering — это inference-time интервенция в активации модели. Мы берём вектор активаций в момент forward pass и двигаем его:
![]()
Вектор
строился методом CAA (Contrastive Activation Addition). Мы брали два набора промптов — позитивный класс (tolerant) и негативный (hate) — снимали активации последнего токена на нужном слое и вычисляли разность средних:
![]()
Мы посмотрели на результаты и были таковы. Обратите внимание, что в этой формуле мы нормируем весь вектор в единицу (а не вклад каждой пары).
Вводные определения и инутивный смысл
Прежде чем улучшать что-либо как-либо, надо понять своё «что» и почувствовать все возможные «как». Этот блок вы можете прочитать как до основного контента, так и после или во время, если в голове в какой-то момент возникнет вопросительное «зачем».
Что: проблемы среденего
Так как классический CAA аппелирует средней разностью, вспомним свойства среднего. Классический пример лекций по статистике — ситуация, когда в вашу выборку с зарплатой населения пришел Билл-Гейтс.
# Доход в месяц, $ [2000, 3000, 3394, 2789, 2550, 5829, 2000] # до Билла, среднее 3080.29 [2000, 3000, 3394, 2789, 2550, 5829, 200000] # после Билла, среднее 31366.0
Отсюда среднее, и, стало быть, подход, использующий разность средних, чувствительны к выбросам: один нетипичный промпт сместит вектор-центроиду и он может съехать в пространстве. В силу богатства живой лексики или наоборот — низкой вариативности лексики, что часто проблема синтетических данных — нетипичных примеров может быть много. Отсюда, steering сложнее — в прошлом туториале, как вы помните, мы тоже не дошли до идеала. Отсюда, подход CAA и улучшали.
Что: шум разметки
В нашей задаче мы аппелируем парами, отсюда шум возникает (или не возникает) на уровне пар. В общем смысле мы знаем, что все пары по идее про «+» и «-«, но не исключаем, что у нас есть пары «+» и «
«, «
» и «-«, а ещё у нас может быть спутана разметка «-» и «+». Вот эти непонятки хотелось бы убрать, как и чувствительность к выбросам.
Всё ещё принцип — мусор на входе — мусор на выходе. Хотя мы и поставили задачу свести шум к минимуму, мы всё ещё ограничены требованием того, чтобы этого шума было очень мало. Отсюда при нерабочести стиринга — база, всё же, перепроверить датасет.
Откуда убираем проблему
Разности пар всегда образуют матрицу (назовём её
). Среднее по всем векторам мы раньше назвали направлением стринга. Вопрос — как отыскать направление стабильнее, в условиях выбросов и шума?
Геометрические ответы
Наши данные — это облако векторов в
-мерном пространстве. Каждый вектор
смотрит примерно в сторону концепта.
Что делает среднее? Складывает точки из обоих сгустков и делит на n. Если перепутанных пар примерно поровну с правильными — сгустки гасят друг друга, и среднее уезжает к нулю. Что не меняется в ситуации разных по знаку расстояний? Дисперсия. И мы можем её задействовать, используя PCA. Смотрим на формулу:
![]()
Это сумма квадратов проекций. Точка на +3 вдоль оси и точка на -3 вдоль той же оси вносят в эту сумму одинаковый вклад — 9 и 9. Знак проекции для формулы не имеет значения, важен только модуль. Значит неважно, сколько пар перепутаны по знаку — пока все
(перепутанные и нет) лежат вдоль одной и той же оси, PCA эту ось найдёт. Задача «кто тут ‘+’, а кто ‘-‘» снимается с этапа поиска направления и переносится на следующий шаг — определение знака уже найденной оси, обычно по небольшому размеченному подмножеству, уже после того как ось найдена.
Именно эта постановка — PCA на разностях, без требования заранее знать, какой конец пары правильный — лежит в основе Linear Activation Tomography из статьи «Representation Engineering: A Top-Down Approach to AI Transparency». Для этой постановки нет фреймворка, но есть исходный код, а ещё есть интересная производная этой статьи, которую мы сейчас потрогаем (и огорчимся, но потом мы потрогаем фреймворк на LoR-e и, надеюсь, вы порадуетесь к концу).
RepE (оригинал) vs repeng
Фреймворк repeng— это производная от «Representation Engineering: A Top-Down Approach to AI Transparency». Производная, которая неплохо себя показала. Она берет метод из оригинальной статьи для извлечения направления — LAT (Linear Artificial Tomography). Название красивое и может напугать, но просто про аккуратное конструирование данных и строится в три шага.
Посмотрим на оригинальный алгоритм:
Шаг 1 — дизайн стимулов. Авторы разделяют два типа понятий в первом шаге — концет (статичный объект) и функция (динамичное поведение). Нас будет интересовать далее только второе понятие, но я хочу отметить этот шаг.
Для концептов (например, «правдивость») — цель вытащить декларативное знание: модели показывают стимул и спрашивают про концепт напрямую:
Пример дизайна концепта
> Consider the amount of <concept> in the following: <stimulus>. The amount of <concept> is ___
По постановке, модель верне какую-то «меру» (не в мат. смысле) наличия концепта.
Для функций (например, «честность», то есть поведение, а не статичное знание) — цель вытащить процедурное знание, поэтому нужны два шаблона: экспериментальный (просит функцию исполнить) и референсный (не просит):
Пример дизайна функции
> USER: <instruction> <experimental/reference prompt>
> ASSISTANT: <output>
Обозначаются шаблоны как
и
. Это уже знакомая нам пара и дальше мы останемся со случаем функций.
У функции есть естественная бинарная пара — один и тот же инструктаж, два режима (исполнять/не исполнять), что прямо ложится на (positive, negative).
У концепта пары другие: один шаблон
применяется к разным стимулам, варьирующимся по интенсивности концепта, а сами пары для PCA и поиска вектора — это случайные пары внутри одного датасета,
, безо всякой роли «плюс»/»минус» у i и j. PCA здесь ищет ось максимального разброса, не полагаясь на то, какой элемент пары «правильный». Если понадобится именно концептная постановка — смотреть придётся в код оригинальной статьи: github.com/andyzoujm/representation-engineering.
Шаг 2 — снятие активаций. Для каждого стимула/функции снимают представление конкретной токен-позиции — по умолчанию последний токен шаблона — на каждом интересующем слое. Типичный размер датасета — от 5 до 128 пар.
Шаг 3 — построение линейной модели. Для функции f пары стимулов дают активации на экспериментальном шаблоне
и референсном
. Даже при известной роли каждого элемента пары, авторы всё равно рандомизируют знак множителем
— оставаясь верными unsupervised-постановке — и нормализуют каждую разность до единичной длины:
Затем находят первую главную компоненту набора
:
Знак
формула не определяет (собственный вектор задан с точностью до знака) — его находят отдельно, постфактум, по небольшому размеченному подмножеству: если проекции «+»-примеров оказались ниже, чем «-«-примеров,
просто умножают на -1.
Сноска — детали оригинала.
Статья предлагает три опции:
-
reading vector, добавленный линейно (вектор после LAT) — самый простой и наименее точный вариант, потому что вектор не зависит от конкретного инпута;
-
contrast vector — то же самое, но пересчитанное заново — разность на инференсе, для текущего конкретного инпута, без какой-либо PCA-агрегации;
-
LoRRA — низкоранговые адаптеры, дообученные так, чтобы воспроизводить эффект contrast vector без пересчёта на инференсе; и три способа скомбинировать вектор с активацией — линейная добавка
, piecewise (добавка с учётом знака проекции) и проекция (обнуление направления вместо усиления). Если хотите расширить арсенал методов от стиринга (первая-частично вторая ситуация) — снова призываю рассмотреть оригинальную работу.
Мы вернемся обратно и рассмотрим прежде repeng-стиринг. Но нам ещё полезно подчеркнуть, что в оригинале предлагают разные способы добавить вектор:
1. Linear Combination — то, что мы использовали везде до сих пор (CAA, repeng):
![]()
2. Piece-wise Operation — условный эффект, зависящий от знака:
![]()
Здесь
— проекция текущей активации на направление
(то самое “чтение” через reading vector). Если проекция уже положительна (модель и так немного “думает” в сторону концепта) — добавляем
; если отрицательна — тоже добавляем, но с плюсом относительно знака самой активации, то есть эффект усиливает то, что уже есть.
3. Projection — не добавление, а вычитание компоненты вдоль
:
![]()
Это ортогональная проекция
на гиперплоскость, перпендикулярную
— буквально обнуление направления концепта в активации, а не сдвиг в его сторону. Используется для удаления — например, debiasing.
Они в библиотеке не реализованы, но «хозяйке на заметку», как говорится.
Адаптация
repeng — переупаковка LAT-бейзлайна в pip-библиотеку: тот же workflow «пары → PCA → вектор → hook», но реализация отличается от статьи в четырёх местах. Как и в каких очень подробно вы можете посмотреть в ноутбуках к статье. Мы же здесь пройдемся по списку.
1. Знак пары зафиксирован.
В отличие от LAT (случайный порядок, без меток),
= positive − negative консистентен по всему датасету. repeng supervised по конструкции (DatasetEntry), просто без явных численных меток — то самое «необязательно размеченные» из Шага 3 тут уже неверно.
2. Нет нормализации. У оригинала (см Appendix C.1) : normalize(H(si) − H(si+1))). В исходном коде либы (нас будет интересовать файл extract.py) такой строчки нет — сырые разности идут в PCA(n_components=1).fit(train) как есть. repeng отсюда уязвим к выбросам по норме.
3. Два метода и нюанс адаптациии 1: поиск направления и среднее
repeng предлагает два способа получить направление.
-
pca_diff (default). В PCA идут сырые
, а центрирует их сам sklearn внутри .fit() — вычитает среднее по всему набору. Поскольку знак пар в repeng консистентен (всегда positive − negative), это среднее близко к CAA-направлению. Первая компонента здесь — ось, вдоль которой дистанции между positive и negative отклоняются друг от друга сильнее всего. -
pca_center. В PCA идут уже центрированные попарно данные. Для пары i:
, откудаСумма этих двух строк — 0 для любого i, при любом
: каждая пара обнуляется алгебраически, ещё до усреднения. Значит и среднее по всему набору 
Центрирование обычно удаляет общую компоненту, разделяемую всеми точками — то, в чём согласны почти все пары, — и оставляет PCA судить только об индивидуальных отклонениях. Здесь эту общую компоненту убрала уже сама конструкция данных, до всякого PCA. Вычитать в sklearn.fit() больше нечего — центрирование превращается в no-op (вычитание нуля), и задача фактически становится нецентрированным PCA: первая компонента максимизирует не дисперсию вокруг среднего, а сумму
напрямую, то есть просто норму проекций.Отсюда и нюанс: без центрирования компонента максимальной дисперсии перестаёт отличать «разброс вокруг типичного значения» от «просто большая величина у одной точки». Одна пара с аномально большой
(например, Билл Гейтс против человека Без Определенного Места Жительства) вносит в сумму квадратичный, ничем не ограниченный вклад — и может утащить направление на себя, сколько бы остальных пар ни указывало в сторону настоящего концепта. Поэтому pca_centerне устойчив к выбросам. Главный движок процесса —ControlVector.train(…) — без явного method= использует pca_diff — центрированный, но ненормализованный PCA.
4. Знак направления:
positive_smaller_mean = np.mean([projected_hiddens[i] < projected_hiddens[i+1] for i in range(0, len(inputs)*2, 2)]) positive_larger_mean = np.mean([projected_hiddens[i] > projected_hiddens[i+1] for i in range(0, len(inputs)*2, 2)]) if positive_smaller_mean > positive_larger_mean: directions[layer] *= -1
Для каждой пары сравниваем projected_hiddens[i] (positive) и [i+1] (negative) как числа, но в сумму идёт не разница, а результат сравнения — 0 или 1. Усредняя эти булевы результаты по всем парам, получаем долю пар с «неправильным» и «правильным» порядком; если неправильных больше — флипаем знак.
Почему это хорошо — пары с большой нормой вносят в знак направления такой же вес, что и пары с малой нормой.Если бы вместо голосования по долям здесь считали mean(projected_hiddens[pos]) vs mean(projected_hiddens[neg]) (то есть сравнивали бы средние величины, а не результаты сравнения).

И если на этом этапе вам стало сложно — мне тоже было! Поэтому в ноутбуке лежит числовой пример. Сходите к нему — он очень помогает все уложить.
Для библиотеки также справедлив «нюанс адаптациии 2»: стиринг (добавка) по умолчанию осуществляется на все векторы. Как это хэндлить — в коде. Если запустить эксперимент, то у нас всё получится и мы сместим модель.

Что мы решили и чего не решили.
Целевая функция PCA квадратична по
: вклад одной пары растёт как
. У среднего вклад пары линеен и жёстко ограничен —
. Значит для одного экстремального выброса всё ровно наоборот тому, что можно было бы ожидать: у среднего влияние выброса растёт линейно с его величиной, у PCA — квадратично. Один достаточно длинный
может определить top eigenvector практически единолично, «передавив» сумму вкладов всех остальных пар — Билл Гейтс не просто сдвинет среднее, он ещё и утащит за собой главную ось.
PCA может быть полезнее среднего — но в рамках (приличия) ограничений метода: например, если каждый
заранее нормализован до единичной длины (тогда квадратичный член не может взорваться от одной длинной пары).
Самое важное: В repeng этой защиты нет ни в pca_diff, ни в pca_center. Так что проблема с выбросами (шумом) здесь актуальна в чистом виде. Библиотека у нас — про другой способ поиска вектора. И если хочется решить проблему, используя PCA, то поможет только теория выше. Train repeng-а же— это нахождение векторов описанным методом и коррекция знака.
Да ну вашу геометрию: погнали обучаться
Второй популярной фреймворк — pyreft (от Representation Fine-Tuning, ReFT). Он реализует вообще другой подход: интервенция обучается на данных, а не конструируется аналитически.
ReFT — это семейство методов. В оригинальной статье ReFT: Representation Finetuning for Language Models описаны два представителя:
-
LoReFT (Low-rank Linear Subspace ReFT) — интервенция в низкоранговом линейном подпространстве.
-
DiReFT — история оптимальнее с основной мотивацией — закинуть вмешательство не в веса, а прямо к остаточному потоку модели (в ее представления).
Математика LoReFT
LoReFT учит интервенцию вида:
![]()
Разберём по частям — не страшнее LAT:
-
— low-rank проектор (
). Строки — ортонормальный базис маленького
-мерного подпространства внутри пространства активаций. -
— проекция
на это подпространство: «координаты
внутри него». -
— желаемые координаты в том же подпространстве. -
— ошибка между текущей проекцией и желаемой. -
— «поднимаем» поправку обратно в d-мерное пространство и прибавляем к
.
Неформально: мы сдвигаем
только внутри маленького
-мерного «коридора», оставляя остальные
измерений нетронутыми.
Математика DiReFT
DiReFT убирает из LoReFT пару вещей.
![]()
Что/зачем:
-
Разностная операция. В LoReFT поправка — это разница между желаемой проекцией (
) и текущей проекцией (
): интервенция знает, где
уже находится в подпространстве, и двигает его именно на недостающую разницу. -
Ортогональность.
в LoReFT — матрица с ортонормированными строками (это гарантирует, что подпространство ведёт себя как «чистый» r-мерный срез без искажений метрики). В DiReFT
— просто две независимые low-rank матрицы, без такого ограничения.
Неформально: Уравнение DiReFT структурно совпадает с LoRA. Разница с обычной LoRA только в том, куда прикладывается адаптер — не к весам слоя, а прямо к вектору активации.
Выгода такая — для DiReFT — меньше ограничений — быстрее обучение (не нужно поддерживать ортогональность
на каждом шаге, не нужно вычислять
отдельно). Но статья прямо говорит, что это ablation, а не улучшение: DiReFT «trades some performance for increased efficiency». Число обучаемых параметров у обоих методов одинаковое (
), так что разница не в размере, а в постановке.
Training objective: что оптимизируем.
Статья рассматривает две постановки параллельно. Модель с ReFT-интервенцией
и обучаемыми параметрами
обозначается
.
-
Генерация (decoder-only / encoder-decoder LM): дан промпт
, нужно предсказать$
— обычная кросс-энтропия с teacher forcing по всем позициям выхода, тот же loss, что и при обучении самой LM, только градиент идет в
) при замороженной базовой модели. -
Классификация (encoder-only): голова
поверх представления CLS-токена финального слоя, минимизируется кросс-энтропия целевого класса y при входе x.
В обеих постановках интервенция
встроена в forward pass на конкретных позициях/слоях и обучается обычным градиентным спуском.
Почему это прикольно:
-
Не меняем веса модели — только параметры интервенции (
) -
Обучаемых параметров мало:
штук; -
Работает там, где CAA не работает: если концепт нелинейный или зашумлённый, обученная интервенция найдёт его лучше mean difference
Почему это не идеально:
В CAA и repeng есть явный дискретный шаг: снять активации
и
, вычислить разность
, найти направление (среднее или PC1). Направление — это объект, который можно достать и посмотреть.
В LoReFT такого шага нет. Обучающие данные — это пары текстов, не активаций. В ноутбуке подробно описано, что как считается внутри. Но самое важное тут: «контрастность» здесь не мат. объект (разность векторов), а свойство обучающих данных — то, что x и y систематически различаются по нужному признаку (hate vs. tolerant), заставляет градиент раз за разом подталкивать
в одну и ту же сторону. «Направление» в LoReFT — это поведение обученного модуля: чему он научился, размазано по трём матрицам и восстанавливается только эмпирически — прогоняя разные
через
$и глядя, куда он их сдвигает, а не читая единый вектор из весов.
Но это практическо-теоретический нюанс с точки зрения анализа направлений. Если задача — сдвинуть модель — метод работает отлично.

Итог
Ух, если вы читаете эти строки — спасибо. Мы прошли огромный путь. Сводная табличка для понимания:
|
|
CAA |
repeng |
pyreft |
|
Как найден вектор |
mean(pos) − mean(neg) |
PCA на |
gradient descent |
|
Знак каждой пары нужен заранее |
да |
нет* |
нет |
|
Чувствителен к выбросам по норме |
линейно |
квадратично** |
— |
|
Охват слоёв |
1 |
настраивается |
настраивается |
|
Нелинейный концепт |
нет |
нет |
да |
|
Нужно обучение |
нет |
нет |
да |
|
Вектор можно достать и посмотреть |
да |
да |
нет*** |
-
* PCA работает на квадратах проекций — знак не важен. repeng фиксирует знак через конструкцию DatasetEntry, но оригинальный LAT из статьи этого не делает и можно пойти к нему.
-
** Без нормализации
до PCA — вклад выброса растёт квадратично. Исходный LAT нормализует; repeng нет. -
*** «Направление» LoReFT размазано по
и восстанавливается только прогоном разных
через
.
И буллеты практических штук, чтобы много текста не хрнаить в голове:
-
Если датасет чистый и большой — CAA работает, и не нужно ничего сложнее.
-
Если знак пар ненадёжен (разметка шумная, минимальные пары) — LAT/(но не repeng в чистом виде) убирает проблему знака, но не проблему выбросов по норме; нужна нормализация
. -
Если концепт нелинейный или данных мало — pyreft. Но нужна модель достаточного размера и достаточно данных, чтобы не получить петлю (бред в генерации).

Если вам понравилось, присоединяйтесь к Just Data Blog— я стану охватываемым каналом и буду радоваться от того, что получается приносит в мир больше прикольных штук.
Ссылки:
-
Ноутбуки на гитхаб.
-
Коллаб.
-
Мой канал: Just Data Blog
До новых встреч!
Источник: habr.com
Похожие записи
- Sonair представляет собой «первый в мире», сертифицированный по безопасности ультразвуковой 3D-датчик для совместной работы человека и робота.
- Первые американские беспилотные наземные транспортные средства участвуют в боевых действиях на Украине.
- Инструменты недели 🧪 ChatPlayground запускает ChatGPT, Claude, Gemini и многое другое на…
Оцените материал:
Похожие записи
Присоединяйтесь и подпишитесь на рассылку самых свежих новостей по Email
Получайте свежие новости и идеи на почту. Без спама — только самое интересное.
Нажимая «Подписаться», вы соглашаетесь с политикой конфиденциальности.
