← Назад к вопросам

Как обучить модель предсказывать распределение, а не одно число?

3.0 Senior🔥 211 комментариев
#Машинное обучение

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Обучение моделей для предсказания распределений: 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 часто более ценна, чем точечные предсказания, так как позволяет принимать более информированные решения.

Как обучить модель предсказывать распределение, а не одно число? | PrepBro