Глава 11: Объяснимость в Offline RL

«Политика с 80% Directional Accuracy, которая не может объяснить причину каждого своего решения, с трудом поддаётся доверию и не может быть сертифицирована.»


Почему объяснимость важна в промышленном Offline RL

Алгоритмы глав 1–10 строят политики с высокой наградой, уважающие физические ограничения и обобщающиеся на незнакомые режимы работы. Чего они не производят — ответа на вопрос, который оператор неизбежно задаст: почему агент выдал именно это уставное значение именно сейчас?

Это не только регуляторная проблема, хотя во многих отраслях она и является таковой. Это практический вопрос надёжности. Политику, чьи решения непрозрачны, нельзя диагностировать при отказе. Оператор, не понимающий логику агента, не может компетентно его отменить. Модель, хорошо подогнанная к данным по неправильным причинам, будет молча отказывать при изменении процесса.

Объяснимость в Offline RL сложнее, чем в стандартном обучении с учителем — по трём причинам, специфичным для офлайн-постановки.

Три отдельные модели для объяснения. Обученный агент Offline RL содержит Q-функцию (критик), политику (актор) и опционально модель динамики. Каждая отвечает на разный вопрос, и их объяснения не всегда согласуются. Q-функция может высоко оценивать действие потому, что температура близка к уставному значению; политика может выбирать это же действие потому, что уровень близок к нижней границе. Понимание этого расхождения так же важно, как понимание согласия.

Вход Q-функции — конкатенация состояния и действия. В отличие от классификатора, чей вход — один вектор признаков, Q-функция принимает $(s, a)$ как вход. SHAP-значения в этом конкатенированном пространстве показывают, откуда берётся высокое Q-значение: из состояния (ситуация была благоприятной) или из действия (конкретное действие было правильным для данного состояния).

Распределение поведенческой политики неравномерно. SHAP-значения вычисляются относительно фонового распределения. В Offline RL естественный фон — офлайн-датасет, плотный вблизи рабочей точки и разреженный вблизи возмущений. SHAP-значения отвечают: «чем этот переход отличается от типичного, и насколько это отличие влияет на выход?»

📄 Полный код: chapter11.py


Основы SHAP

SHAP (SHapley Additive exPlanations, Lundberg & Lee, 2017) разлагает выход модели $f$ в аддитивные вклады каждого входного признака:

$$f(x) = \phi_0 + \sum_{i=1}^{n} \phi_i(x)$$

где $\phi_0 = \mathbb{E}[f(X)]$ — ожидаемый выход модели на фоновом распределении, а $\phi_i(x)$ — вклад признака $i$ в предсказание для экземпляра $x$.

SHAP-значения $\{\phi_i\}$ — единственное разложение, удовлетворяющее аксиомам: - Эффективность: $\sum_i \phi_i = f(x) - \phi_0$ — значения суммируются в полное предсказание минус базовый уровень - Симметрия: признаки с одинаковым маргинальным вкладом получают одинаковые SHAP-значения - Пустышка: признак с нулевым маргинальным вкладом получает $\phi_i = 0$

KernelExplainer вычисляет SHAP-значения без знания архитектуры, подгоняя взвешенную линейную модель на маскированных предсказаниях:

$$\phi = \arg\min_{\phi} \sum_{z \in \{0,1\}^n} \pi(z)\left[f(h(x, z)) - \phi_0 - \sum_i z_i \phi_i\right]^2$$

где $z$ — бинарная маска, $h(x, z)$ заменяет замаскированные признаки сэмплами из фона, а $\pi(z)$ взвешивает коалиции по ядру Шепли. Это работает на любой функции-чёрном ящике — Q-сетях, сетях политик, ансамблях динамики — без предположений об архитектуре.

Мы используем KernelExplainer повсюду для последовательности: все три объяснения используют один метод, что делает их напрямую сопоставимыми.

Игрушечный пример: что показывает SHAP

Минимальный пример фиксирует интуицию. Рассмотрим игрушечную Q-функцию от трёх входов: $Q(s_1, s_2, a) = 2 s_1 - s_2 + 0{,}5 a$. То есть состояние_1 повышает Q, состояние_2 понижает, действие даёт меньший положительный вклад. Фон берём из «типичной» работы (значения в $[0{,}3, 0{,}7]$) и объясняем набор экземпляров. SHAP приписывает каждому предсказанию вклад трёх признаков.

Какой признак важнее всего? На столбчатой диаграмме ниже — среднее $|\phi_i|$ по объясняемым экземплярам. state_1 и state_2 доминируют (у них большие коэффициенты), и SHAP это воспроизводит.

Игрушечное Q: среднее |SHAP| по признакам

Почему Q для этого экземпляра высокое? Водопад для одного экземпляра (например, высокое $s_1$, низкое $s_2$) показывает, как SHAP-вклад каждого признака сдвигает выход от базового $\phi_0 = \mathbb{E}[Q]$ к итоговому $Q(x)$. Положительные столбцы (state_1, action) поднимают Q; отрицательный (state_2) — опускают.

Игрушечное Q: водопад для одного экземпляра

Разброс по экземплярам. На графике «рой пчёл» одна точка — один экземпляр и один признак; по оси X — SHAP-значение, цвет — значение признака (синий = низкое, красный = высокое). Видно, что высокое state_1 чаще даёт положительный SHAP (красные точки справа), высокое state_2 — отрицательный; монотонная зависимость модели видна наглядно.

Игрушечное Q: пчелиный рой SHAP-значений

Игрушечная политика: какой признак состояния двигает действие? Рассмотрим политику $\pi(s_1, s_2) = \text{clip}(0{,}5 + 0{,}4 s_1 - 0{,}3 s_2)$ с одним действием. Состояние_1 увеличивает действие, состояние_2 уменьшает. Policy SHAP приписывает скалярное действие двум входам состояния. Столбчатая диаграмма показывает, какую переменную состояния политика «учитывает» больше; водопад для одного состояния — как вклад каждого признака сдвигает выход от среднего действия $\mathbb{E}[\pi]$ к $\pi(s)$. В реальной настройке процесса покрытия (уровень 2) два измерения действия (heat_input, flow_input); каждое объясняем отдельно и получаем по одной такой диаграмме и водопаду на действие.

Игрушечная политика: среднее |SHAP| по признакам состояния

Игрушечная политика: водопад для одного состояния

Игрушечная динамика: какой вход двигает предсказанное следующее состояние? Модель динамики с двумя компонентами: $\hat{s}_1' = 0{,}7 s_1 + 0{,}2 a$ и $\hat{s}_2' = 0{,}6 s_2 + 0{,}15 a$. Первый выход определяется текущим $s_1$ и действием $a$, второй — $s_2$ и $a$. Каждый выход объясняем отдельно (как в уровне 3). Двухпанельный bar chart показывает среднее $|\phi_i|$ по трём входам $(s_1, s_2, a)$ при предсказании $\hat{s}_1'$ и $\hat{s}_2'$ — SHAP восстанавливает, что для next_s1 важны $s_1$ и $a$, для next_s2 — $s_2$ и $a$. Водопад для одного $(s, a)$ показывает, как SHAP-вклады входов суммируются от базового предсказания до $\hat{s}_1'$.

Игрушечная динамика: какие входы двигают next_s1 и next_s2?

Игрушечная динамика: водопад для одного (s,a) → next_s1

Все эти рисунки получаются запуском python code/chapter11_toy_figures.py (в скрипте — точные игрушечные модели и настройка SHAP). Далее в главе мы проверяем, что Q-функция и политика обращают внимание на сходные признаки состояния (ранговая корреляция), и что модель динамики соблюдает простую физику (например, heat_input → next_temperature положителен); те же SHAP-выходы используются в этих проверках согласованности.


Три уровня объяснения

Уровень 1: SHAP Q-функции — почему агент ценит это действие?

Q-функция $Q(s, a)$ принимает конкатенацию $(s, a)$ и выдаёт скалярную оценку ценности. SHAP по этому входу отвечает, какие признаки — переменные состояния или выбранное действие — вносят наибольший вклад в Q-значение.

class QFunctionWrapper:
    """
    Оборачивает QNetwork в numpy-in/numpy-out для SHAP KernelExplainer.

    Вход:  X ∈ R^{n × (state_dim + action_dim)}  — [состояние | действие]
    Выход: q ∈ R^n                                — Q-значение на сэмпл
    """
    def __call__(self, X: np.ndarray) -> np.ndarray:
        with torch.no_grad():
            x_t = torch.FloatTensor(X).to(self.device)
            s   = x_t[:, :self.s_dim]
            a   = x_t[:, self.s_dim:]
            q   = self.q(s, a)
        return q.cpu().numpy()

Вектор признаков: [temperature, filler_frac, viscosity, density, level, heat_input, flow_input].

SHAP признаков состояния отвечает: «была ли эта ситуация благоприятной?» SHAP признаков действия отвечает: «было ли это действие правильно выбрано для данного состояния?»

Ожидаемый паттерн для хорошо обученного CQL: переменные состояния вблизи уставных значений → положительный SHAP (благоприятная ситуация → высокое Q). Если SHAP признаков действия близок к нулю независимо от величины действия — Q-функция игнорирует действие, признак коллапса Q-функции.

Уровень 2: SHAP политики — почему агент выбирает это действие?

Объясняем каждое измерение действия отдельно (SHAP требует скалярного выхода):

class PolicySingleOutputWrapper:
    """
    action_idx = 0 → heat_input
    action_idx = 1 → flow_input

    Объясняет детерминированное среднее tanh(μ_θ(s)) — не сэмпл,
    поэтому оценки SHAP стабильны между вызовами.
    """
    def __call__(self, S: np.ndarray) -> np.ndarray:
        with torch.no_grad():
            s_t     = torch.FloatTensor(S).to(self.device)
            mean, _ = self.policy._dist(s_t)
            action  = torch.tanh(mean)
        return action[:, self.action_idx].cpu().numpy()

Ожидаемый паттерн: heat_input управляется главным образом отклонением temperature от уставного значения; flow_input — отклонением filler_frac. Если level имеет высокий SHAP для flow_input, политика научилась, что расход влияет на уровень — физически корректно и признак того, что агент усвоил интегрирующую динамику.

Уровень 3: SHAP динамики — почему модель предсказывает это следующее состояние?

Объясняем три выходных измерения: next_temperature (0), next_filler_frac (1), next_level (4):

class DynamicsSingleOutputWrapper:
    """
    Объясняет предсказание среднего по ансамблю для одного измерения состояния.
    """
    def __call__(self, X: np.ndarray) -> np.ndarray:
        with torch.no_grad():
            s     = x_t[:, :self.s_dim]
            a     = x_t[:, self.s_dim:]
            s_next, _ = self.ensemble.predict_with_uncertainty(s, a)
        return s_next[:, self.state_idx].cpu().numpy()

Проверка физического здравого смысла: у heat_input должен быть положительный наклон зависимости с next_temperature (SHAP растёт с ростом нагрева); аналогично flow_input с next_filler_frac. Проверяйте корреляцией признак–SHAP или графиком dependence — не средним знаковым SHAP, который может быть около нуля при экземплярах вокруг фона. Если наклон инвертирован — модель выучила физически невозможную зависимость. Красный флаг перед развёртыванием.


Класс Explainer

OfflineRLExplainer управляет построением фонового датасета и вычислением SHAP для всех трёх уровней:

class OfflineRLExplainer:
    def __init__(self, agent, ensemble, dataset,
                 s_mean, s_std, device='cpu', n_background=100):
        # Фон = репрезентативная выборка из офлайн-датасета
        idx = np.random.default_rng(42).choice(
            len(dataset['states']), size=n_background, replace=False)
        self.bg_states  = dataset['states'][idx]
        self.bg_actions = dataset['actions'][idx]
        self.bg_sa      = np.concatenate(
            [self.bg_states, self.bg_actions], axis=1)

Фон определяет $\phi_0$ — средний выход модели. Использование офлайн-датасета означает: SHAP отвечает на вопрос «чем этот переход отличается от типичного наблюдённого?»

Производительность: KernelExplainer вызывает каждый враппер тысячи раз. В chapter11.py фон при инициализации объяснителя пре-тензоризуется на устройстве, чтобы снизить пересылки. DynamicsSingleOutputWrapper агрегирует ансамбль (среднее предсказание) перед SHAP; SHAP считается по среднему следующего состояния.

results = explainer.explain_all(
    n_explain    = 150,   # экземпляры для объяснения
    n_background = 80,    # размер фонового датасета
    n_samples    = 80,    # коалиций SHAP на экземпляр
)
# ключи results: 'states', 'actions', 'q_shap',
#                'policy_shap', 'dynamics_shap', 'q_base'

n_samples управляет качеством аппроксимации. При 80 стандартная ошибка ~0.005 на нормированных выходах — достаточно для ранжирования признаков. При 500 — качество для публикаций, но в 6 раз медленнее.


Визуализация

Сводный график (Q-функция)

График «рой пчёл» (beeswarm): каждая точка — один экземпляр, цвет — значение признака (синий=низкое, красный=высокое), ось X — SHAP-значение.

plot_q_summary(results['q_shap'], SA_NAMES,
               save_path='ch11_q_summary.png')

Паттерны для чтения: - Широкий горизонтальный разброс → признак имеет переменный эффект - Красные точки стабильно правее синих → монотонная зависимость - Смешанные цвета → нелинейная или зависимая от взаимодействий связь

Столбчатые диаграммы (политика и динамика)

Среднее |SHAP| по признакам — отвечает на «какие признаки важнее всего?»

plot_policy_bar(results['policy_shap'], STATE_NAMES,
                save_path='ch11_policy_bar.png')

plot_dynamics_bar(results['dynamics_shap'], SA_NAMES, STATE_NAMES,
                  save_path='ch11_dynamics_bar.png')

Force plot: один экземпляр

Наиболее операционно полезная визуализация. Для одного временного шага показывает вклад каждого признака как водопад от базового уровня до итогового Q-значения:

best = int(np.argmax(q_vals))   # экземпляр с наибольшим Q
plot_force_single(
    results['q_shap'][best], SA_NAMES,
    q_value=q_vals[best], q_base=results['q_base'],
    instance_label=f'Highest-Q instance (Q={q_vals[best]:.3f})',
    save_path='ch11_force_best.png')

Оператор использует этот график для ответа: «почему агент оценил этот момент как высокоценный?» Если ответ — «температура у уставного значения, уровень стабилен» — разумно. Если «вязкость необычно низкая», когда уставная точка по температуре — подозрительная корреляция для расследования.

График зависимости: взаимодействия

SHAP признака A против его сырого значения, окрашенный по признаку B — выявляет эффекты взаимодействия:

plot_shap_dependence(
    results['q_shap'][:, :len(STATE_NAMES)],
    results['states'],
    feature_idx=0,       # temperature
    interaction_idx=1,   # окрашен по filler_frac
    feature_names=STATE_NAMES,
    title='Q-SHAP: влияние температуры (окрашено по filler_frac)')

Если SHAP температуры систематически разделяется по цвету наполнителя — есть взаимодействие: реакция Q-функции на температуру зависит от доли наполнителя. Для физически-информированного агента это ожидаемо — вязкость нелинейно зависит от обеих переменных.


Проверка согласованности

metrics = check_explanation_consistency(results)
print_consistency_report(metrics)

Ожидаемый вывод:

  Q ↔ policy rank correlation  : 0.71  ✓ согласованы
  heat_input → next_temperature: SHAP=+0.0412  ✓ положительный (верно)
  flow_input → next_filler     : SHAP=+0.0387  ✓ положительный (верно)

Ранговая корреляция Спирмена Q-политика $\rho$: ранжирует признаки состояния по среднему |SHAP| в Q-функции и в политике. $\rho > 0.6$ означает, что критик и актор обращают внимание на одни и те же признаки. $\rho < 0.3$ указывает на коллапс политики — актор игнорирует большую часть состояния, которое важно критику. Характерно при слишком большом $\alpha$ в CQL.

Проверки знака физики: у heat_input должен быть положительный наклон зависимости с next_temperature, и у flow_input — с next_filler_frac. Проверяйте наклон признак–SHAP (или dependence-график), а не среднее знаковое SHAP, которое может быть около нуля при экземплярах вокруг фона. Инвертированный наклон указывает, что модель динамики выучила физически невозможную зависимость в некоторой области данных — характерно при очень разреженном покрытии вблизи границ.


SHAP в производстве: практические соображения

Вычислительная стоимость. KernelExplainer делает $n\_\text{explain} \times n\_\text{samples} \times n\_\text{background}$ обращений к модели. Для 150 × 80 × 80 = 960,000 вызовов на CPU это занимает минуты. Практическая стратегия: запускать офлайн перед развёртыванием на отложенной валидационной выборке; ежемесячно перезапускать на свежих эксплуатационных данных для обнаружения дрейфа распределения.

Выбор фонового датасета — важнейший гиперпараметр. Полный датасет в качестве фона отвечает «отличие от типичной работы». Фон из зоны ограничений отвечает «что отличает приближение к нарушению от типичного приближения» — полезно для диагностики, почему агент приближается к границам.

Физические единицы. Состояния нормированы для обучения. Для коммуникации с операторами переводите состояния обратно в физические единицы — не SHAP-значения:

# Перевод значений признаков обратно в физические единицы (для отображения)
states_physical = results['states'] * s_std + s_mean
# НЕ умножайте SHAP-значения на s_std.
# SHAP остаётся в единицах выхода модели:
# - Q-SHAP: единицы Q / return
# - policy-SHAP: единицы выхода политики (действия)
# - dynamics-SHAP: единицы предсказания следующего состояния

Что SHAP не может сказать

SHAP ≠ причинно-следственная связь. Высокий SHAP для viscosity означает, что модель использует вязкость как предиктор, а не что вязкость причинно влияет на Q-значение. Если вязкость скоррелирована с температурой (так и есть, через нелинейную зависимость), SHAP может разделить «заслугу» между ними способами, не совпадающими с физической интуицией.

SHAP-значения зависят от фона. Признак может иметь высокий SHAP относительно одного фона и низкий — относительно другого. При сравнении SHAP-анализов между запусками держите фон фиксированным.

Временна́я структура теряется. KernelExplainer объясняет каждый экземпляр независимо. Для интегрирующих состояний вроде уровня логика агента зависит от истории траектории. Одношаговые SHAP не могут отразить эту временну́ю зависимость.

SHAP объясняет модель, а не её правильность. Если Q-функция переоценивает в некоторой области (известный режим отказа Offline RL), SHAP будет добросовестно объяснять эту переоценку. Проверки согласованности позволяют обнаружить некоторые режимы отказа, но SHAP не заменяет метрики оценки из главы 10.


За пределами SHAP: Causal AI в Offline RL

SHAP даёт атрибуцию — какие входы модель использует для предсказания. Другое направление исследований ставит вопрос: можно ли заставить агентов Offline RL рассуждать в терминах причины и следствия, чтобы они лучше обобщали и не опирались на ложные корреляции?

Зачем причинность в Offline RL. Офлайн-данные наблюдательные: мы видим $(s, a, s', r)$ при поведенческой политике, но не знаем, что было бы при другом действии. Обычные модельные методы учат переходы по корреляциям; при сдвиге распределения или при наличии смешивающих факторов (независимые переменные, влияющие и на действие, и на следующее состояние) такие модели могут давать сбой. Причинный подход стремится отделить истинные причинные влияния от ложных — например, «дождь → дворники» и «дождь → торможение» коррелируют, но только одно из них — прямая причина другого.

Causal RL на практике. В последних работах рассматриваются:

Ресурсы. Обзор Deng et al. (2023) даёт единую картину causal RL и включает раздел об офлайн/батчевых настройках. Статья FOCUS (Zhang et al.) — конкретный пример причинных мировых моделей для модельного Offline RL. Связь off-policy оценки с причинным выводом (оценка эффекта вмешательства) освещена в литературе по batch RL и causal inference (например, важностное взвешивание и inverse propensity scoring). Ссылки приведены ниже.

Игрушечный пример кода. Минимальный скрипт показывает, почему предикторы, опирающиеся на корреляции, могут ошибаться при интервенции. Истинная динамика: $\text{next\_s1} = 0{,}8\,s_1 + 0{,}2\,a$ (причинные родители: $s_1$, $a$). Добавляем «мешающую» переменную $z = s_1 + \text{шум}$ — она коррелирует с $s_1$, но не является причиной next_s1. «Корреляционная» модель предсказывает next_s1 по $(s_2, z, a)$ (не видит $s_1$, использует $z$ как прокси). «Причинная» модель использует только $(s_1, a)$. Обе хорошо подходят на обучающей выборке. При интервенции задаём $z=0{,}9$, $s_1=0{,}2$, $a=0{,}5$: истинное next_s1 = $0{,}26$, но корреляционная модель даёт завышенный прогноз (из-за $z$); причинная — $0{,}26$. Запуск: python code/chapter8_causal_toy.py. Идея в коде:

# Истинная динамика (неизвестна обучающемуся): next_s1 = 0.8*s1 + 0.2*a
# Мешающая z = s1 + шум — коррелирует с s1, не причина next_s1
# Корреляционная модель: next_s1 по (s2, z, a)
# Причинная модель:       next_s1 по (s1, a)
# Интервенция: s1=0.2, z=0.9, a=0.5 → истинное next_s1=0.26
#   Корреляционная (видит z=0.9, не видит s1) → ~0.7 (ошибка)
#   Причинная (видит s1=0.2, a=0.5)           → 0.26 (верно)

📄 Полный код: chapter11_causal_toy.py


Итог

Три взаимодополняющих SHAP-объяснения для обученного агента Offline RL:

Уровень Модель Вопрос Входное пространство
SHAP Q-функции Критик $Q(s,a)$ Почему это действие ценно здесь? $(s, a) \in \mathbb{R}^{S+A}$
SHAP политики Актор $\pi(s)$ Почему было выбрано это действие? $s \in \mathbb{R}^S$
SHAP динамики $\hat{f}(s,a)$ Почему предсказано именно это следующее состояние? $(s, a) \in \mathbb{R}^{S+A}$

Проверка согласованности — ранговая корреляция Спирмена Q-политика и проверки знака физики — обеспечивает автоматическую верификацию взаимной когерентности и физической обоснованности трёх объяснений.

Объяснимость на основе SHAP — не замена оценке (метрики DA и частоты нарушений из главы 10), соблюдению ограничений (глава 9) или аккуратному моделированию динамики (глава 8). Это слой прозрачности, делающий эти компоненты поддающимися аудиту.

Глава 13 завершает книгу широким взглядом: что область достигла, куда движется, и какие открытые проблемы остаются нерешёнными.


Приложение 11.А: Выбор между вариантами SHAP

Explainer Лучше всего для Скорость Примечания
KernelExplainer Любой чёрный ящик Медленно Используется в главе — единообразен для всех типов моделей
DeepExplainer PyTorch / TF сети Быстро Использует backprop; менее точен вблизи насыщения tanh
GradientExplainer Дифференцируемые сети Средне Градиент × вход; быстро, но шумно
TreeExplainer XGBoost, RF Очень быстро Точный SHAP; нерелевантен для нейронных политик
LinearExplainer Линейные модели Мгновенно Точный SHAP; используйте, если политика дистиллирована в линейную модель

Для быстрых пошаговых объяснений оператору в развёрнутой системе: дистиллируйте политику в мелкое дерево DecisionTreeRegressor и используйте TreeExplainer. Дистилляция теряет точность, но делает каждое решение объяснимым за <1 мс.


Ссылки