Неопределённость как часть модели. ml.. ml. Pyro.. ml. Pyro. python.. ml. Pyro. python. байесовская статистика.. ml. Pyro. python. байесовская статистика. биномиальное распределение.. ml. Pyro. python. байесовская статистика. биномиальное распределение. вероятностное программирование.. ml. Pyro. python. байесовская статистика. биномиальное распределение. вероятностное программирование. неопределённость модели.

Сегодня рассмотрим тему неопределённости в моделях. Классические ML-модели детерминированы: на вход получили – на выход выдали одно число или метку. Но жизнь полна неопределённости, и игнорировать её плохая идея. Представьте, у вас мало данных, модель предсказывает конверсию 15%. Но насколько она уверена? Может, разброс от 5% до 30%. Обычная модель этого не скажет, а вот вероятностная модель скажет.

В этой статье в коротком формате разберём, как с помощью байесовского подхода и фреймворка Pyro моделировать такую неопределённость на примере A/B-теста конверсии и заставить модель честно признавать свою неуверенность.

Вероятностное программирование: модели, которые сомневаются

Начнем с понятия. Вероятностное программирование — это подход, при котором мы прямо закладываем в программу (или модель) случайность и неопределённость. Вместо конкретных значений параметров мы задаём распределения. Вместо одного ответа получаем распределение ответов.

Классический Bayesian пример: у нас есть некоторое априорное представление о параметре (например, конверсия на сайте скорее всего ~10%, но это не точно). Собрав данные (скажем, 100 посетителей, из них 12 сконвертились), мы обновляем это представление и получаем апостериорное распределение параметра (конверсия теперь может быть ~12%, с некоторым доверительным интервалом). Главное отличие от частотного подхода в том, что мы не говорим “оценка конверсии = 12% ± доверительный интервал 95%”. А прямо получаем распределение вероятности для каждой возможной конверсии. То есть можем ответить на любой интересный вопрос, например: а какова вероятность, что реальная конверсия > 15%?. Bayesian модель скажет: “~10%” (к примеру), а классическая начнет ытаскивать p-value и все равно ничего не поймёт.

Pyro – один из фреймворков для вероятностного программирования. По сути, это библиотека над PyTorch. В Pyro мы можно задавать вероятностные модели прямо как обычный код на Python, используя примитив pyro.sample для случайных величин.

Bayesian модель конверсии

Возьмём понятный кейс: есть стартап, и мы пробуем два способа вовлечь пользователей — email-рассылка и звонок. Запустили A/B-тест: 700 человек получили email, из них 73 совершили целевое действие (конверсия ~10.4%). 200 другим позвонили менеджеры, и 35 из них сконвертились (~17.5%). На первый взгляд звонки эффективнее, хоть выборка и меньше. Но мы хотим оценить неопределённость этой ситуации: насколько уверенно можно заявить, что звонки правда лучше? Может, сэмпл маленький и это случайность?

Построим байесовскую модель для конверсий. Пусть вероятность конверсии для email = p_email, для звонка = p_call. До эксперимента мы не знали их, поэтому зададим априорное распределение. Естественный выбор для вероятностей – Beta-распределение. Возьмём неинформативный Beta(2,2) для обеих (это почти равномерное на [0,1], с легким акцентом на 50%, но довольно широкое). Далее, данные (число конверсий из попыток) моделируем как биномиальное распределение: obs_email ~ Binomial(n_email, p_email), obs_call ~ Binomial(n_call, p_call).

В Pyro модель задаётся как функция:

import pyro
import pyro.distributions as dist

def conversion_model(conv_call, conv_email, n_call, n_email):
    # априорные вероятности конверсии
    p_call = pyro.sample("p_call", dist.Beta(2., 2.))
    p_email = pyro.sample("p_email", dist.Beta(2., 2.))
    # наблюдаемые данные (likelihood)
    pyro.sample("obs_call", dist.Binomial(total_count=n_call, probs=p_call), obs=conv_call)
    pyro.sample("obs_email", dist.Binomial(total_count=n_email, probs=p_email), obs=conv_email)

Ничего особо страшного, обычная функция. Используем pyro.sample(name, dist.X) чтобы задать случайные параметры. Первый pyro.sample("p_call", Beta(2,2)) означает: выбери значение p_call из Beta(2,2). При этом Pyro помечает его как скрытый параметр модели, который надо будет оценивать по данным. Затем pyro.sample("obs_call", Binomial(...), obs=conv_call), это указываем, что наблюдение conv_call подчиняется биномиальному распределению с параметром p_call. Эта строчка связывает нашу случайную переменную p_call с конкретными данными.

Аналогично для email.

На этом описание модели всё. Дальше начинается инференс, вычисления апостериорного распределения p_call и p_email с учётом наблюдений. Можно задействовать вариационный метод, но я предпочитаю старый добрый MCMC, благо Pyro поддерживает его сам по себе. Воспользуемся алгоритмом NUTS:

from pyro.infer import MCMC, NUTS

kernel = NUTS(conversion_model)
mcmc = MCMC(kernel, num_samples=3000, warmup_steps=500)
mcmc.run( conv_call=35, conv_email=73, n_call=200, n_email=700 )

Запустили 3000 итераций выборки (и 500 шагов прогрева). Конечно еще надо смотреть на сходимость цепей, эффективный sample size и прочие MCMC штучки, но опустим для краткости. Предположим, метод отработал и выдал нам выборки из апостериорных распределений p_call и p_email. Их можно получить так:

posterior_samples = mcmc.get_samples()  # словарь выборок
p_call_samples = posterior_samples["p_call"].numpy()
p_email_samples = posterior_samples["p_email"].numpy()
print(p_call_samples.mean(), p_email_samples.mean())

Если вывести средние, вероятно будет что-то около p_call ~0.175, p_email ~0.105, то есть оценки близки к наблюденным частотам (17.5% и 10.5%). Интереснее другое: распределения этих параметров. Можно посчитать, например, 95%-й интервал доверия:

import numpy as np
low_c, high_c = np.quantile(p_call_samples, [0.025, 0.975])
low_e, high_e = np.quantile(p_email_samples, [0.025, 0.975])
print(f"95%-интервал для p_call: [{low_c:.3f}, {high_c:.3f}]")
print(f"95%-интервал для p_email: [{low_e:.3f}, {high_e:.3f}]")

Получится p_call в интервале [0.130, 0.225], p_email ~ [0.082, 0.130]. Видно, что интервал для email не пересекается с верхом интервала для call. Т.е. дажес учётом неопределённости, конверсия от звонков практически наверняка выше. Вычислим прямо вероятность того, что p_call > p_email:

prob_call_better = np.mean(p_call_samples > p_email_samples)
print(f"Prob(p_call > p_email) = {prob_call_better:.3f}")

Выдаётся ~0.997, то есть ~99.7%. С вероятностью ~99% звонки эффективнее, чем email, при данных которые у нас есть.

Вероятностные модели могут быть сколь угодно сложными: от простых A/B тестов до целых нейросетей. Например, можно делать Bayesian нейронные сети, где веса имеют априорное распределение, и в результате обучения мы получаем не точные веса, а их распределения. Это довольно затратные штуки.

Применение

Байесовские подходы дают крепкую математическую основу, чтобы учитывать неопределённость, объединять знания из разных источников и принимать решения более осмотрительно.

Когда пригодится вероятностный подход:

  • Маленький датасет или уникальная задача. Когда данных мало, стандартные модели неустойчивы. Байесовский метод впитывает априорные знания и отражает неизвестность, вместо переобучения в ноль или выдачи детерминированной ерунды.

  • Нужна доверенная вероятность. В медицинских диагнозах, финансовых предсказаниях важно знать доверительный интервал прогноза. PPL-модель сразу даёт распределение результатов.

  • Комплексные выводы. Когда вы делаете выводы на основе нескольких источников данных или слоёв неопределённости (например, прогнозируя что-то на основе других прогнозов), вероятностный подход будет естественнее.

Наш пример с двумя Beta — это лишь верхушка айсберга. Pyro поддерживает сложные иерархические модели, интегрируется с глубоким обучением, есть модуль numpyro для более быстрой MCMC.

Вообще, если тема зашла, первое естественное направление — углубиться в байесовскую статистику и классические модели. Это Beta-Binomial, Normal–Normal, и вообще conjugate priors, многие простые задачи там решаются аналитически, без MCMC.

Второе направление — сами фреймворки вероятностного программирования. Помимо Pyro есть NumPyro, PyMC, Stan, Turing и др. У каждого свои плюсы: где-то проще декларативный синтаксис, где-то лучше готовые диагностические инструменты, где-то удобнее интеграция.

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

Расскажите, применяете ли вы такие методы у себя и как боретесь с неопределённостью в проектах.


Если вам близка идея моделей, которые честно признаются в своей неопределённости, следующий логичный шаг — научиться доводить их до продакшена. На курсе по MLOps вы разберёте полный цикл: от хранения данных и кода до CI/CD, контейнеризации, k8s и мониторинга ML-систем в бою, сохраняя их устойчивость под реальной нагрузкой. Пройдите бесплатное тестирование по курсу, чтобы оценить свои знания и навыки.

Для знакомства с форматом обучения и экспертами приходите на бесплатные демо-уроки:

  • 8 декабря: «MLFlow — полный контроль над ML-экспериментами!». Записаться

  • 16 декабря: «API — учим модель общаться с внешним миром». Записаться

Автор: badcasedaily1

Источник

Rambler's Top100