Всем привет! Недавно я познакомился с курсом по глубокому обучению с подкреплением от HuggingFace Deep Reinforcement Learning Course и захотел сделать выжимку самого интересного. Эта статья — своего рода шпаргалка по основам Reinforcement Learning (RL) и одному из ключевых алгоритмов — PPO, который лежит в основе тонкой настройки современных LLM (Large Language Models).
Вы наверняка слышали про такие модели, как o1 от OpenAI или QwQ от Alibaba. Их "рассуждающие" способности во многом — результат применения RL. Давайте разберемся, как обычный принцип обучения, известный по играм вроде AlphaGo, помогает языковым моделям стать умнее.
RLM (reasoning language model - дословно "рассуждающая языковая модель") - это языковые модели, способные не только генерировать текст, но и выполнять логические, аналитические и причинно-следственные рассуждения для решения сложных задач.
Эти передовые системы в корне переопределили возможности ИИ по решению проблем, обеспечив тонкие рассуждения, улучшенное контекстное понимание и надежное принятие решений в широком спектре областей.
Рассмотрим 3-м столпа, на которых строится архитектура RLM:
Прогресс в LLM
RL алгоритмы, такие как AlphaZero
Ресурсы высокопроизводительных вычислений (eng. HPC)
На связи первых двух пунктов, я считаю, можно остановиться подробнее, так как для меня они были неочевидные.
Начну с самого банального - это определение Reinforcement Learning. Reinforcement Learning (RL), или обучение с подкреплением, — это способ машинного обучения, при котором агент учится принимать решения, взаимодействуя с окружающей средой. Проще говоря, это метод проб и ошибок с поощрением за успехи.
Агент - обучающийся или принимающий решения субъект, который взаимодействует с окружающей средой. Чаще всего в роли агента выступает, например, персонаж игры, определенный робот (например, робот-рука), нейронная сеть и так далее.
Окружающая среда - внешняя система или мир, внутри которого действует агент.
Процесс обучения представляет собой цикл (Возьмем шаг t =0):
Агент видит состояние () среды.
Совершает действие ().
Получает от среды вознаграждение () — численную оценку своего действия.
Переходит в новое состояние ().
Именно такую схему описывает Марковский процесс принятия решений (MDP) — математическая основа для большинства алгоритмов RL (статья "Reasoning Language Models: A Blueprint" ).
Дадим каждой сущности последовательности определение:
Состояние (S) (или пространство состояний)- это информация, которую агент получает из окружающей среды.
Действия (A)(или пространство действий) - это набор всех возможных действий в среде.
Награды (R) - это по сути, наша обратная связь для агента на предпринятое действие. В плоскости LLM - в процессе рассуждения RLM может перейти из одного ответа в другой - как следствие получить положительное вознаграждение (если ответ от LLM корректный), так и отрицательное (если ответ некорректный).
Накопленное вознаграждение на каждом шаге t можно представить:
где - это последовательность чередующихся состояний и действий
Однако мы не можем просто так их складывать, потому что награды приходят раньше, более вероятны, поскольку они предсказуемы, чем долгосрочные будущие награды. Следовательно, вводим коэффициент дисконтирования такой, что:
Значения варьируются в диапазоне от 0 до 1 (в большинстве случаев - между 0.95 и 0.99
Чем больше гамма, тем меньше дисконт => агент заботится о долгосрочном вознаграждении
Чем меньше гамма, тем больше дисконт => агент заботится больше о краткосрочном вознаграждении
Таким образом, приходим к окончательной формуле вознаграждения с коэффициентом дисконтирования:
Представьте, что LLM — это агент. Ее состояние — это весь сгенерированный на данный момент текст (промпт + ответ). Действие — это генерация следующего токена (слова или его части). Вознаграждение — это оценка качества всего ответа, которую может дать человек-оценщик или другая модель-критик. Например, пользователь задает вопрос LLM: "Сколько будет 2+2". Таким образом:
Состояние (S)
Начальное состояние () : Промпт пользователя: "Сколько будет 2+2"
Действие (А)
Хотим перейти из начального состояния () в (
)
Текущее состояние (): («сколько будет 2+2»)
Действие(): Модель генерирует первый шаг рассуждения: «Чтобы решить это, нам нужно выполнить сложение»
Результирующее состояние (): Новое состояние S₁ теперь включает промпт и этот шаг: («сколько будет 2+2», «Чтобы решить это, нам нужно выполнить сложение».)
Из состояния () в конечное состояние (
):
Текущее состояние : («сколько будет 2+2», «Чтобы решить это, нам нужно выполнить сложение».)
Действие (): Модель генерирует ответ: «Сложение 2 и 2 дает 4». Это z(a₁) — окончательный ответ, поэтому за ним будет следовать токен eos , указывающий на конечное состояние.
Результирующее состояние (): («сколько будет 2+2», «Чтобы решить это, нам нужно выполнить сложение.», «Сложение 2 и 2 дает 4.») Это
теперь является конечным состоянием, так как оно содержит окончательный ответ.
Награда (R)
Конечное состояние (): («Сколько будет 2+2?», «Чтобы решить это, нам нужно выполнить сложение?», «Сложение 2 и 2 дает 4?»)
Вознаграждение (): Внешний верификатор сверяет окончательный ответ «4» с истинным ответом (который равен «4»).
Так как ответ «4» правильный , вознаграждение () равно 1.
Если ответ был «5» (неверный), вознаграждение будет равно -1.
Для промежуточных состояний s₀ и s₁ вознаграждение будет равно 0
Хорошо, мы определили формулу для вознаграждения, но появляется вопрос: как мы можем выбирать действия, которые максимизируют это ожидаемое совокупное вознаграждение агента, иными словами "Решить задачу MDP"? На этот вопрос есть ответ, но сперва дадим определения, которые помогут прийти к ответу:
Политика - это функция, присваивающая распределение вероятностей по пространству действий заданному состоянию
.
Более формально:
где- набор распределений вероятностей в пространстве действий A (ничего не напоминает? - посматриваю в сторону нейронных сетей и принцип действия LLM)
Таким образом, наша цель для решения задачи MDP - это найти оптимальную политику , которая максимизирует ожидаемую доходность (return), когда агент действует согласно ей. Находим оптимальную политику посредством обучения.
Мы все ближе к пониманию как решить задачу MDP, останавливают нас только подходы к обучению. Итак, выделяют 2 вида:
Обучение напрямую - заставляем агента понимать, какие действия следует предпринять с учетом текущего состояния (Policy-Based Methods).
Косвенно - учим агента узнавать, какое состояние будет более ценно, а затем предпринимать действия, которые ведут к более ценным действиям: методы, основанные на ценностях (Value-Based Methods).
Давайте кратко рассмотрим 2 этих подхода:
Агент учит политику () — прямую инструкцию "что делать в состоянии S". Политика говорит не "делай Х", а "с вероятностью 70% сделай Х, с вероятностью 30% — Y". Именно так работают современные LLM, доработанные с помощью RL. Если кратко, то:
Напрямую обучаем функцию политики выбирать действие, учитывая состояние (или распределение вероятностей по действиям в этом состоянии).
Нет функции значения (value function)
Есть 2 типа политик:
Детерминированная (политики в заданном состоянии будет возвращать одно и то же действие)
Стохастический (выводит распределение вероятностей по действиям) -
Агент учит функцию ценности (Q или V), а потом просто выбирает действие, которое ведет в самое "ценное" следующее состояние.
В данном подходе обучаем функцию значения (Value function), которая сопоставляет состояние (или action-state пары) с ожидаемым значением нахождения в этом состоянии.
Значение состояния (value state) - это ожидаемая дисконтная доходность (return), которую агент может получить, если начнет из определенного состояния и продолжит действовать в соответствии с политикой. Формально данное высказывание можно записать как:
где - это ожидаемая дисконтная доходность (return)
Разница между 2-мя подходами в следующем (согласно HuggingFace):
При обучении Policy-Based метода оптимальная политика (обозначаемая ) находится путем непосредственного обучения политики.
При обучении на основе ценностей нахождение оптимальной функции ценности (обозначаемой Q* или V*, мы рассмотрим разницу ниже) приводит к появлению оптимальной политики.
Policy-Based методы более естественны для задач, где действия непрерывны (как генерация текста), а Value-Based часто применяются в играх с дискретным набором ходов
Так как замечаем, что возникают "какие-то" функции Q и V - приходим к выводу, что у нас есть 2 типа value-based functions:
The state-value function
Для каждого состояния функция «состояние-значение» выводит ожидаемый доход, если агент начинает с этого состояния , а затем следует политике вечно
The action-value function
В функции «действие-значение» для каждой пары «состояние-действие» функция «действие-значение» выводит ожидаемый результат, если агент начинает в этом состоянии, выполняет это действие, а затем следует политике вечно
где (насколько я могу судить) авторы привели сокращенный вариант формулы и использовали просто замену:
Насколько можно заметить, в данных уравнениях мы повторяем вычисление значений различных состояний, что может быть вычислительно затратно, если вам нужно делать это для каждого значения состояния (state value) или значения состояния-действия (state-action value).
Вместо расчета ожидаемой доходности для каждого состояния или каждой пары состояние-действие мы можем использовать уравнение Беллмана.
Уравнение БеллманаСперва рассмотрим уравнение Беллмана, которое упрощает вычисления.
Для надо вычислять "return" с определенного состояния, а затем следовать политике всегда.
Итак, если имеется 6 шагов и каждый шаг имеет награду "-1", то в момент t функция будет принимать вид:
Чтобы рассчитать функцию - нужно рассчитать "return", начиная с
Как мы видим - получаем много повторных вычислений - тут к нам и приходит на помощь уравнение Беллмана.
Работает оно так: немедленная награда + дисконтированная
. Таким образом наше уравнение для state-value function будет выглядеть следующим образом:
Тогда, возвращаясь к нашему примеру, его можно переписать следующим образом:
Таким образом, приходим к упрощенным вариантам этих формул:
The state-value function
Action-value function
С этой частью разобрались... можно выдохнуть... чуть-чуть... =)
Монте-Карло использует целый эпизод перед обучением. То есть сначала ожидание эпизода, вычисление и далее обновление
. Более формально:
TDL использует только одно взаимодействие (то есть 1 шаг) для формирования TD цели и обновления
, используя
и
(так как не проходим эпизод полностью и не знаем
).
(На курсе приведен классный пример с мышкой, иллюстрирующий эту формулу)
Наконец-то добрались до самой интересной части (если вы не устали от формул и не ушли с этой статейки), ради которой я и затевал эту статью - разобрать до полного понимания PPO - Proximal Policy Optimization — это алгоритм градиента политики, который позволяет стабильно обучать policy-based агентов, и именно он используется для тонкой настройки LLM (например, в RLHF).
Хотелки, которые лежали в основе PPO - это повысить стабильность обучения политики.
Идея заключалась в том, что выполняя шаг градиентного подъема по этой функции (эквивалентно выполнению градиентного спуска отрицательной функции), мы подталкиваем нашего агента к совершению действия, которые приведут к более высокому вознаграждению и избежанию вредных действий.
В чем была проблема?
Представьте, что вы учитесь ходить по канату. Если делать слишком маленькие шаги, вы никуда не дойдете. Если сделать слишком резкий и большой шаг — вы упадете. Так же и в RL:
Слишком маленький шаг - процесс обучения медленный
Слишком большой шаг - слишком мало вариаций в обучении
Идея PPO: "Доверяй, но проверяй"
Таким образом, приходим к идеи PPO - ограничить обновление политики с помощью новой целевой функции (Clipped), которая будет ограничивать изменения политики в небольшом диапазоне с помощью "клипа", иначе она не позволит новой политике () слишком сильно отклоняться от старой (
).
Таким образом, получим формулу для PPO с Clipped Surrogate Objective:
(Probability Ratio): Отношение вероятностей действия по новой и старой политике.
.
: Действие стало более вероятным.
: Действие стало менее вероятным.
(Advantage): Насколько действие в данном состоянии лучше, чем "среднее" действие по текущей политике.
. Положительное преимущество означает, что действие стоит поощрять, отрицательное — что его следует избегать.
Clipping (Обрезка): Самая важная часть PPO. Мы "обрезаем" значение не позволяя ему выходить за пределы диапазона
(Clip Range): Гиперпараметр, обычно равный 0.2. Он определяет диапазон
(т.е.
), за пределами которого функция "обрезается"
Если действие хорошее (), мы его поощряем, но не даем
стать больше
. Иначе новую политику может "заклинить" на этом одном действии.
Если действие плохое (), мы его наказываем, но не даем
упасть ниже
. Иначе модель может навсегда перестать использовать это действие, даже если в другом контексте оно могло бы быть полезным.
Контекст: "The weather today is"
Действие: "sunny" (токен ID: 1234)
("sunny" | "The weather today is") = 0.15 (15% вероятность по старой модели)
("sunny" | "The weather today is") = 0.25 (25% вероятность по новой модели)
Это значит, что новая модель стала значительно чаще предлагать слово "sunny" в этом контексте. PPO проверит, не слишком ли большой это скачок, и, если (при
), будет использовать для обновления "обрезанное" значение 1.2, чтобы не переборщить.
Отлично! Мы рассмотрели ключевые механизмы PPO и теперь стоит задуматься - где нам применять эти знания в плоскости LLM? Далеко ходить не надо - одно из самых значимых применений PPO сегодня — обучение с подкреплением на основе человеческих предпочтений (Reinforcement Learning from Human Feedback, RLHF) для согласования LLM, таких как ChatGPT и Llama 2.
Процесс RLHF состоит из нескольких этапов, и PPO является ядром этого процесса:
Шаг 1: Начальная тонкая настройка с учителем (SFT)
Модель обучается на наборе высококачественных данных "вопрос-ответ", чтобы научиться следовать инструкциям
Шаг 2: Обучение Модели Вознаграждения (Reward Model, RM)
Создается отдельная модель, которая учится предсказывать, какой из двух ответов на один вопрос человек оценит выше. Эта модель заменяет человека в цикле обучения и выдает скалярную оценку (reward) для любого сгенерированного текста
Шаг 3: Fine-Tuning с помощью PPO
На этом этапе SFT-модель становится агентом, политику () которого нужно оптимизировать.
Архитектура обучения в этот момент включает несколько моделей:
Актор (Actor): Текущая политика (LLM), которую мы обновляем с помощью PPO.
Критик (Critic): Сеть, которая оценивает value-function (), чтобы снизить дисперсию Advantage-функции. (в курсе HuggingaFace есть классный раздел про Actor-Critic)
Модель вознаграждения (Reward Model): Выдает основное вознаграждение.
Референсная модель (Reference Model): Замороженная копия исходной SFT-модели, используемая для расчета KL-штрафа
Задача: Научить модель давать более вежливые ответы
Контекст: "Мне не нравится твой ответ"
Старая модель: "Ну и что?" (вероятность ответа )
Новая модель: "Понимаю, как я могу улучшить ответ?" (вероятность ответа)
Вычисление для токена "Понимаю":
("Понимаю" | контекст) = 0.08
("Понимаю" | контекст) = 0.15
= 0.15 / 0.08 = 1.875
Предположим, что у нас есть Reward модель такая, что:
Reward модель: +2.3 (высокий - ответ вежливый и конструктивный)
Reward: 0.2 (низкий - ответ грубый)
Advantage: +1.8 (Предположим, что Advantage через GAE = 1.8 (высокий, т.к. эмпатия сильно ценится))
= 1.875 1.8 = 3.375
PPO обновление: усиливаем генерацию "Понимаю"
НО: → обрезаем до 1.2 → плавное обновление
Таким образом, получаем (если бы не было функции clip, то
)
Для последующих токенов применяется аналогично.
И в конце - давайте порисуем!
Для понимания как работает PPO - создадим пару простых примеров на python и визуализируем:
Код на python для визуализацииimport numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
def plot_ppo_clipping_mechanism():
"""График 1: Механизм клиппинга"""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# Левый график - функция потерь PPO
r_theta = np.linspace(0.1, 3, 100)
advantage = 1.0 # Положительное преимущество
epsilon = 0.2
# PPO clipped objective
clip_min = 1 - epsilon
clip_max = 1 + epsilon
unclipped = r_theta * advantage
clipped = np.clip(r_theta, clip_min, clip_max) * advantage
ppo_loss = np.minimum(unclipped, clipped)
ax1.plot(r_theta, unclipped, 'r--', alpha=0.7, label='Без клиппинга')
ax1.plot(r_theta, clipped, 'g--', alpha=0.7, label='Clipped')
ax1.plot(r_theta, ppo_loss, 'b-', linewidth=2, label='PPO Loss')
ax1.axvline(x=1, color='k', linestyle=':', alpha=0.5)
ax1.axvspan(clip_min, clip_max, alpha=0.2, color='green', label='Область клиппинга')
ax1.set_xlabel('r(θ) = π_new/π_old')
ax1.set_ylabel('Loss')
ax1.set_title('Функция потерь PPO с клиппингом')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Правый график - сравнение стабильности
episodes = np.arange(1000)
# Имитация обучения разных алгоритмов
np.random.seed(42)
# PPO - плавный рост
ppo_reward = np.cumsum(np.random.normal(0.1, 0.3, 1000))
ppo_reward = np.maximum(ppo_reward, 0)
# TRPO - возможны резкие падения
trpo_reward = np.cumsum(np.random.normal(0.15, 0.8, 1000))
trpo_reward = np.maximum(trpo_reward, 0)
ax2.plot(episodes, ppo_reward, 'b-', label='PPO', linewidth=2)
ax2.plot(episodes, trpo_reward, 'r-', label='TRPO', alpha=0.7)
ax2.set_xlabel('Эпизоды')
ax2.set_ylabel('Награда')
ax2.set_title('Сравнение стабильности обучения')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
def plot_kl_control_in_rlhf():
"""График 4: KL-контроль в RLHF"""
iterations = np.arange(200)
# Имитация различных стратегий контроля KL
np.random.seed(42)
# Без KL-штрафа
no_kl = np.cumsum(np.random.normal(0.2, 0.3, 200))
# С фиксированным KL-штрафом
with_kl = 2 + np.sin(iterations * 0.1) + np.random.normal(0, 0.1, 200)
# Адаптивный KL
adaptive_kl = 1.5 + 0.5 * np.sin(iterations * 0.05) + np.random.normal(0, 0.05, 200)
plt.figure(figsize=(12, 6))
plt.plot(iterations, no_kl, 'r-', label='Без KL-штрафа', linewidth=2)
plt.plot(iterations, with_kl, 'g-', label='С KL-штрафом', linewidth=2)
plt.plot(iterations, adaptive_kl, 'b-', label='Адаптивный PPO', linewidth=2)
# Целевой диапазон
plt.axhspan(1.0, 2.0, alpha=0.2, color='green', label='Целевой диапазон KL')
plt.xlabel('Итерации PPO')
plt.ylabel('KL-дивергенция')
plt.title('Контроль KL-дивергенции в RLHF для LLM')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# Запуск визуализаций
plot_ppo_clipping_mechanism()
plot_kl_control_in_rlhf()
Главные выводы из графиков:
График 1: PPO создает "песочницу" для обновлений политики - внутри зеленой зоны алгоритм свободно экспериментирует, но не может выйти за безопасные границы.
График 2: По сравнению с TRPO (предшественник PPO), PPO показывает более плавную и предсказуемую динамику обучения без резких катастрофических падений.
График 3:
По мере стабилизации политики клиппинг применяется реже.
В RLHF критически важно контролировать KL-дивергенцию, иначе модель может "убежать" в странные области пространства политик.
Именно благодаря Probability Ratio и механизму клиппинга PPO стал таким эффективным для RLHF — он находит баланс между обучением новому поведению и сохранением существующих capabilities модели
Обязательно оставляйте комментарии!
Будут неточности - пишите, если понравилась статья и она Вам помогла понять аспекты RL/RLHF - тоже пишите!
Всех обнял приподнял!!!
Полезные ссылкиhttps://arxiv.org/abs/2501.11223 - Reasoning Language Models: A Blueprint
https://huggingface.co/learn/deep-rl-course/unit1/introduction - очень классный курс для изучения RL с нуля с теорией и практикой
https://arxiv.org/abs/1707.06347 - оригинальная статья Proximal Policy Optimization Algorithms