← Назад к новостям

Можно ли лучше? 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 in the following: . The amount of is ___ По постановке, модель верне какую-то "меру" (не в мат. смысле) наличия концепта. Для функций (например, "честность", то есть поведение, а не статичное знание) — цель вытащить процедурное знание, поэтому нужны два шаблона: экспериментальный (просит функцию исполнить) и референсный (не просит): Пример дизайна функции > USER: > ASSISTANT: Обозначаются шаблоны как и . Это уже знакомая нам пара и дальше мы останемся со случаем функций. У функции есть естественная бинарная пара — один и тот же инструктаж, два режима (исполнять/не исполнять), что прямо ложится на (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)]) 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" : стиринг (добавка) по умолчанию осуществляется на все векторы. Как это хэндлить — в коде. Если запустить эксперимент, то у нас всё получится и мы сместим модель. Результат эксперимента 1. Что мы решили и чего не решили. Целевая функция 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 — это поведение обученного модуля: чему он научился, размазано по трём матрицам и восстанавливается только эмпирически — прогоняя разные через $и глядя, куда он их сдвигает, а не читая единый вектор из весов. Но это практическо-теоретический нюанс с точки зрения анализа направлений. Если задача — сдвинуть модель — метод работает отлично. Результат эксперимента 2. Итог Ух, если вы читаете эти строки — спасибо. Мы прошли огромный путь. Сводная табличка для понимания: 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— я стану охватываемым каналом и буду радоваться от того, что получается приносит в мир больше прикольных штук. Ссылки: Ноутбуки на гитхаб . Коллаб . Мой канал: Just Data Blog До новых встреч!
📊 Источник: Habr | Оригинал