PPO и награда
Вы узнаете
Как PPO превращает награду в обновление политики и как его обрезка не даёт одному обновлению зайти слишком далеко.
Агент учится на награде, делая хорошие действия вероятнее. PPO измеряет изменение политики отношением r новой вероятности действия к старой и умножает его на преимущество A, насколько действие превзошло ожидание. Затем он обрезает: когда r ушло больше чем на eps в ту сторону, за которую награждает A, цель становится плоской. При r = 1.5, A = 1 и eps 0.2 сырое r A равно 1.5, но PPO засчитывает 1.2, так что один пакет не может далеко сдвинуть политику. На пакете спеки, отношения 1, 1.1 и 0.9 с преимуществами 2, 1.5 и 3, потеря политики равна -2.1167. Спека ppo_clip_loss.t27 её вычисляет.
Попробовать
Сделайте преимущество отрицательным и найдите, где кривая теперь становится плоской; затем расширьте eps до 0.5.

Drag the probability ratio of a new policy over the old: PPO's objective follows r times the advantage until r moves more than eps the rewarded way, then it goes flat and stops pulling.
specs/ml/rl/ppo_clip_loss.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Probability ratio between policies | φ² + 1/φ² = 3 | TRINITY
// #8980: written with `let x = if ... {}` and `for i in 0..n`, so it never compiled.
// Three formulas were wrong, and their tests pinned the wrong values:
// - compute_ratio returned log pi_new - log pi_old; the ratio is exp of that.
// - clipped_surrogate returned clip(r) A; PPO's term is min(r A, clip(r) A), so
// r = 1.1 inside the clip range gives 2.2, not 2.4.
// - policy_loss returned the surrogate itself, a quantity to MAXIMISE; as a loss
// added to the value and entropy losses it is minus the mean surrogate.
// Each test now states the value PPO defines (Schulman et al., 2017).
module PpoClipLoss;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const DEFAULT_CLIP_EPSILON : f32 = 0.2;
const DEFAULT_ENTROPY_COEF : f32 = 0.01;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const PPOClipConfig = struct {
clip_epsilon : f32,
entropy_coef : f32,
value_clip_coef : f32,
};
pub const PPOBatch = struct {
states : [][]f32,
actions : [][]f32,
old_log_probs : []f32,
advantages : []f32,
returns : []f32,
values : []f32,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// r = pi_new / pi_old = exp(log pi_new - log pi_old)
fn compute_ratio(new_log_prob: f32, old_log_prob: f32) -> f32 {
return exp(new_log_prob - old_log_prob);
}
// min(r A, clip(r, 1 - eps, 1 + eps) A)
fn clipped_surrogate(ratio: f32, advantage: f32, clip_epsilon: f32) -> f32 {
const clipped_ratio = @min(@max(ratio, 1.0 - clip_epsilon), 1.0 + clip_epsilon);
return @min(ratio * advantage, clipped_ratio * advantage);
}
// -mean_t min(r_t A_t, clip(r_t) A_t): the loss gradient descent minimises
fn policy_loss(ratios: []const f32, advantages: []const f32, clip_epsilon: f32) -> f32 {
var total : f32 = 0.0;
var i : usize = 0;
while (i < ratios.len) {
total = total + clipped_surrogate(ratios[i], advantages[i], clip_epsilon);
i = i + 1;
}
return -total / @as(f32, @floatFromInt(ratios.len));
}
// -coef * mean entropy: lower loss for a more exploratory policy
fn entropy_loss(entropies: []const f32, entropy_coef: f32) -> f32 {
var total : f32 = 0.0;
var i : usize = 0;
while (i < entropies.len) {
total = total + entropies[i];
i = i + 1;
}
return -entropy_coef * (total / @as(f32, @floatFromInt(entropies.len)));
}
// mean 0.5 (target - (old + clip(pred - old, -c, c)))^2
fn value_loss(predicted_values: []const f32, target_values: []const f32, old_values: []const f32, clip_coef: f32) -> f32 {
var total : f32 = 0.0;
var i : usize = 0;
while (i < predicted_values.len) {
const diff = @min(@max(predicted_values[i] - old_values[i], -clip_coef), clip_coef);
const err = target_values[i] - (old_values[i] + diff);
total = total + 0.5 * err * err;
i = i + 1;
}
return total / @as(f32, @floatFromInt(predicted_values.len));
}
fn policy_loss_of(r0: f32, r1: f32, r2: f32) -> f32 {
const ratios : [3]f32 = [r0, r1, r2];
const advantages : [3]f32 = [2.0, 1.5, 3.0];
return policy_loss(&ratios, &advantages, DEFAULT_CLIP_EPSILON);
}
fn entropy_loss_of() -> f32 {
const entropies : [3]f32 = [0.5, 0.6, 0.4];
return entropy_loss(&entropies, DEFAULT_ENTROPY_COEF);
}
fn value_loss_of(p0: f32) -> f32 {
const predicted : [3]f32 = [p0, 2.0, 1.5];
const target : [3]f32 = [1.1, 2.1, 1.6];
const old : [3]f32 = [0.9, 1.9, 1.4];
return value_loss(&predicted, &target, &old, 0.2);
}
fn valid_input() -> f32 {
return DEFAULT_CLIP_EPSILON;
}
// total_loss(policy_loss_value: f32, value_loss_value: f32, entropy_loss_value: f32, value_loss_coef: f32) → f32
fn total_loss(policy_loss_value: f32, value_loss_value: f32, entropy_loss_value: f32, value_loss_coef: f32) -> f32 {
// Combine all loss components
return policy_loss_value + value_loss_coef * value_loss_value + entropy_loss_value;
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
test compute_ratio_basic_case
given
new_log_prob = 0.5
old_log_prob = 0.3
when result = compute_ratio(new_log_prob, old_log_prob)
then @abs(result - 1.2214028) < 0.000001 // exp(0.2); the old `== 0.2` was the log ratio
test clipped_surrogate_basic_case
given
ratio = 1.1
advantage = 2.0
clip_epsilon = 0.2
when result = clipped_surrogate(ratio, advantage, clip_epsilon)
then @abs(result - 1.1 * 2.0) < 0.000001 // inside [0.8, 1.2] the clip does nothing
test clipped_surrogate_clipping_case
given
ratio = 1.5 // Above clip threshold of 1.2
advantage = 2.0
clip_epsilon = 0.2
when result = clipped_surrogate(ratio, advantage, clip_epsilon)
then @abs(result - 1.2 * 2.0) < 0.000001 and clipped_surrogate(0.5, -1.0, 0.2) == -0.8
// min(r A, clip(r) A) = (2.0, 1.65, 2.7): the loss is -mean = -2.1166667. The old `> 0` was
// the surrogate's sign; a ratio pushed past the clip earns nothing more.
test policy_loss_basic_case
given result = policy_loss_of(1.0, 1.1, 0.9)
when pushed = policy_loss_of(1.0, 1.1, 3.0)
then result < 0.0 and @abs(result + 2.1166667) < 0.000001 and @abs(pushed - policy_loss_of(1.0, 1.1, 1.2)) < 0.000001
test entropy_loss_basic_case
given result = entropy_loss_of()
then result < 0.0 and @abs(result + 0.005) < 0.0000001
test value_loss_basic_case
given result = value_loss_of(1.0)
when clipped = value_loss_of(5.0)
then result > 0.0 and @abs(result - 0.005) < 0.0000001 and @abs(clipped - value_loss_of(1.1)) < 0.0000001
test total_loss_basic_case
given
policy_loss_value = 1.0
value_loss_value = 0.5
entropy_loss_value = -0.01
value_loss_coef = 0.5
when result = total_loss(policy_loss_value, value_loss_value, entropy_loss_value, value_loss_coef)
then @abs(result - (1.0 + 0.5 * 0.5 + (-0.01))) < 0.0000001
// ═══════════════════════════════════════════════════════════
// TDD: Invariants (from .tri constraints)
// ═══════════════════════════════════════════════════════════
// 0 < clip_epsilon <= 1 (typically 0.1-0.3)
invariant ppo_clip_loss_constraint_0
assert valid_input() > 0.0 and valid_input() <= 1.0
// entropy_coef >= 0
invariant ppo_clip_loss_constraint_1
assert DEFAULT_ENTROPY_COEF >= 0.0
// name: ppo_objective -- for A > 0 the surrogate stops growing at r = 1 + eps
invariant ppo_clip_loss_constraint_2
assert clipped_surrogate(5.0, 1.0, 0.2) == 1.2
// name: probability_ratio -- equal log-probabilities give r = 1
invariant ppo_clip_loss_constraint_3
assert compute_ratio(0.3, 0.3) == 1.0
// "Schulman et al. (2017) - Proximal Policy Optimization Algorithms, arXiv:1707.06347":
// for A < 0 the surrogate stops falling at r = 1 - eps
invariant ppo_clip_loss_constraint_4
assert clipped_surrogate(0.1, -1.0, 0.2) == -0.8
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.