Как обучить модель предсказывать распределение, а не одно число?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Обучение моделей для предсказания распределений: Probabilistic Regression
Одна из самых полезных, но часто недооценённых техник в ML — это обучение моделей, которые предсказывают не точечное значение, а полное распределение вероятностей. Это называется probabilistic regression или uncertainty quantification.
Зачем нужны распределения вместо одного числа
Стандартный регрессор:
y_pred = 95.5 # Средняя цена квартиры
Проблемы:
- Мы не знаем, насколько уверена модель
- Очень дорогие квартиры и дешевые одинаково штрафуются
- Нельзя оценить риск
Probabilistic регрессор:
y_dist = Normal(μ=95.5, σ=12.3) # Распределение цены
Преимущества:
- Знаем уверенность модели (σ=12.3 означает большую неопределённость)
- Можем сказать: цена с 95% вероятностью между 70 и 121
- Можем минимизировать риск, выбирая дорогие квартиры с малой неопределённостью
Подход 1: Гауссово распределение (Gaussian/Normal)
Идея: Модель предсказывает два выхода — среднее (μ) и стандартное отклонение (σ):
import torch
import torch.nn as nn
import torch.distributions as dist
class ProbabilisticRegressor(nn.Module):
def __init__(self, input_size, hidden_size=64):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, hidden_size)
self.mu_head = nn.Linear(hidden_size, 1) # Среднее
self.sigma_head = nn.Linear(hidden_size, 1) # Стандартное отклонение
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
mu = self.mu_head(x)
log_sigma = self.sigma_head(x) # Логарифм для стабильности
sigma = torch.exp(log_sigma) + 1e-6 # Убеждаемся, что σ > 0
return mu, sigma
# Создаём модель
model = ProbabilisticRegressor(input_size=10)
# Forward pass
mu, sigma = model(X_train)
# Создаём распределение
distribution = dist.Normal(mu, sigma)
# Функция потерь: negative log-likelihood (NLL)
loss = -distribution.log_prob(y_train).mean()
loss.backward()
Интуиция функции потерь:
NLL = -log(p(y|x)) = -log(1/(σ*√(2π)) * exp(-0.5*((y-μ)/σ)²))
= log(σ) + 0.5*((y-μ)/σ)²
- Первый слагаемый log(σ) штрафует модель за большую неопределённость
- Второй слагаемый штрафует за несоответствие предсказания истинному значению
Подход 2: Квантильная регрессия (Quantile Regression)
Вместо полного распределения, предсказываем квантили (например, 10%, 50%, 90%):
class QuantileRegressor(nn.Module):
def __init__(self, input_size, quantiles=[0.1, 0.5, 0.9]):
super().__init__()
self.quantiles = quantiles
self.fc1 = nn.Linear(input_size, 64)
self.fc2 = nn.Linear(64, 64)
self.output = nn.Linear(64, len(quantiles))
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
quantiles = self.output(x)
return quantiles
model = QuantileRegressor(input_size=10)
q_preds = model(X_test) # [batch_size, 3]
# q_preds[:, 0] - 10% квантиль (нижний доверительный интервал)
# q_preds[:, 1] - 50% квантиль (медиана, точечное предсказание)
# q_preds[:, 2] - 90% квантиль (верхний доверительный интервал)
# Функция потерь: quantile loss
def quantile_loss(y_pred, y_true, quantiles):
loss = 0
for i, q in enumerate(quantiles):
errors = y_true - y_pred[:, i]
loss += torch.max((q - 1) * errors, q * errors).mean()
return loss
total_loss = quantile_loss(q_preds, y_train, model.quantiles)
Пример использования:
# Предсказываем цены квартир
q_preds = model(X_test)
print(f"Цена с 10% вероятностью ниже: ${q_preds[0, 0]:.2f}")
print(f"Ожидаемая цена (медиана): ${q_preds[0, 1]:.2f}")
print(f"Цена с 10% вероятностью выше: ${q_preds[0, 2]:.2f}")
print(f"Интервал неопределённости: ${q_preds[0, 2] - q_preds[0, 0]:.2f}")
Подход 3: Mixture Density Networks (MDN)
Для мультимодальных распределений (несколько пиков):
class MixtureNetwordk(nn.Module):
def __init__(self, input_size, n_components=3):
super().__init__()
self.n_components = n_components
self.fc1 = nn.Linear(input_size, 64)
self.fc2 = nn.Linear(64, 64)
# Предсказываем параметры смеси
self.pi_head = nn.Linear(64, n_components) # Веса компонент
self.mu_head = nn.Linear(64, n_components) # Средние
self.sigma_head = nn.Linear(64, n_components) # Отклонения
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
# Получаем параметры
pi = torch.softmax(self.pi_head(x), dim=-1) # Сумма весов = 1
mu = self.mu_head(x)
sigma = torch.exp(self.sigma_head(x)) + 1e-6
return pi, mu, sigma
model = MixtureNetwork(input_size=10, n_components=3)
pi, mu, sigma = model(X_train)
# Функция потерь: NLL смеси Гауссиан
def mixture_loss(y, pi, mu, sigma):
# Создаём компоненты
components = []
for k in range(len(pi[0])):
comp = dist.Normal(mu[:, k], sigma[:, k])
components.append(comp)
# Смешанное распределение
mixture = dist.MixtureSameFamily(
dist.Categorical(pi),
dist.Normal(mu, sigma)
)
return -mixture.log_prob(y).mean()
Подход 4: Бета распределение (для ограниченного диапазона)
Если предсказываем значения в диапазоне [0, 1] (вероятности, доли):
class BetaRegressor(nn.Module):
def __init__(self, input_size):
super().__init__()
self.fc1 = nn.Linear(input_size, 64)
self.fc2 = nn.Linear(64, 64)
self.alpha_head = nn.Linear(64, 1) # α параметр
self.beta_head = nn.Linear(64, 1) # β параметр
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
# Параметры должны быть положительными
alpha = torch.exp(self.alpha_head(x)) + 1
beta = torch.exp(self.beta_head(x)) + 1
return alpha, beta
model = BetaRegressor(input_size=10)
alpha, beta = model(X_train)
# Создаём Бета распределение
beta_dist = dist.Beta(alpha.squeeze(), beta.squeeze())
loss = -beta_dist.log_prob(y_train).mean()
Практический пример: прогноз дохода с неопределённостью
import numpy as np
import torch
from torch import nn
from torch.optim import Adam
# Синтетические данные
np.random.seed(42)
X = np.random.randn(100, 5)
y_true = X[:, 0] * 10 + np.random.randn(100) * 2 + 5
X = torch.FloatTensor(X)
y_true = torch.FloatTensor(y_true).unsqueeze(1)
# Модель
model = ProbabilisticRegressor(input_size=5)
optimizer = Adam(model.parameters(), lr=0.001)
# Обучение
for epoch in range(100):
mu, sigma = model(X)
# NLL Loss
gaussian = dist.Normal(mu, sigma)
loss = -gaussian.log_prob(y_true).mean()
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 20 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
# Предсказание
with torch.no_grad():
mu_pred, sigma_pred = model(X[:3])
for i in range(3):
lower = mu_pred[i] - 1.96 * sigma_pred[i] # 95% интервал
upper = mu_pred[i] + 1.96 * sigma_pred[i]
print(f"Образец {i}: μ={mu_pred[i].item():.2f}, σ={sigma_pred[i].item():.2f}")
print(f" 95% интервал: [{lower.item():.2f}, {upper.item():.2f}]")
Сравнение подходов
| Подход | Распределение | Простота | Скорость | Когда использовать |
|---|---|---|---|---|
| Гауссово (NLL) | Нормальное | Легко | Быстро | Почти всегда |
| Квантильная | Любое | Легко | Быстро | Асимметричные данные |
| MDN | Мультимодальное | Сложно | Медленно | Несколько режимов |
| Бета | [0,1] | Средне | Быстро | Вероятности, доли |
Оценка качества вероятностного предсказания
Calibration curve: Проверяем, соответствуют ли предсказанные вероятности реальным частотам.
import matplotlib.pyplot as plt
# Получаем предсказания
with torch.no_grad():
mu, sigma = model(X_test)
# Считаем, какой процент истинных значений попал в интервал
for confidence in [0.68, 0.95, 0.99]: # 1σ, 2σ, 3σ
z = np.sqrt(2) * torch.erfinv(torch.tensor(confidence))
lower = mu - z * sigma
upper = mu + z * sigma
accuracy = ((y_test >= lower) & (y_test <= upper)).float().mean()
print(f"{confidence:.0%} интервал: {accuracy.item():.2%} попаданий (ожидалось {confidence:.0%})")
Заключение
Предсказание распределений вместо точечных значений — это мощный инструмент для:
- Оценки неопределённости — знаем, когда модель не уверена
- Управления риском — можем выбирать низко-рискованные предсказания
- Лучшей калибровки — вероятности соответствуют реальной точности
- Обработки сложных зависимостей — мультимодальные распределения для сложных сценариев
В production системах probabilistic regression часто более ценна, чем точечные предсказания, так как позволяет принимать более информированные решения.