Виды функций потерь в машинном обучении

Функция потерь — это способ «сообщить» модели, какие ошибки наиболее критичны. Математическая формулировка напрямую влияет на поведение модели во время обучения: какие ошибки минимизируются в приоритетном порядке, как модель реагирует на выбросы и насколько агрессивно оптимизирует параметры.

Оптимизация этой функции через градиентный спуск составляет основу обучения моделей машинного обучения. Разные типы задач требуют разных функций потерь. Регрессия, бинарная классификация, многоклассовая классификация, сегментация изображений — каждая область использует специфические метрики оптимизации. Понимание математики функций потерь и их практических свойств позволяет настраивать модели под конкретные бизнес-требования.

Функции потерь для регрессии

Задачи регрессии требуют предсказания непрерывных значений: цен акций, объемов продаж, температуры. Функции потерь в регрессии измеряют расстояние между предсказанным и истинным значениями. Основное различие между ними — чувствительность к выбросам и характер штрафа за большие ошибки.

Mean Squared Error (MSE)

MSE вычисляет среднее квадратов отклонений предсказаний от истинных значений:

MSE = (1/n) × Σ(yᵢ — ŷᵢ)²

где:

  • n — количество наблюдений;
  • yᵢ — истинное значение i-го наблюдения;
  • ŷᵢ — предсказанное значение i-го наблюдения.

Квадрат разности усиливает штраф за большие ошибки нелинейно. Отклонение в 10 единиц дает штраф 100, в то время как 10 отклонений по 1 единице дают суммарный штраф только 10. Это свойство делает MSE чувствительной к выбросам.

MSE используют в тех случаях, когда особенно важно учитывать крупные ошибки. В задачах прогнозирования волатильности финансовых инструментов недооценка резких ценовых движений может привести к значительным потерям. Поскольку MSE сильнее штрафует большие отклонения, метрика помогает моделям лучше учитывать экстремальные изменения цены.

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

import numpy as np
import torch
import torch.nn as nn

# Реализация MSE в PyTorch
mse_loss = nn.MSELoss()

y_true = torch.tensor([2.5, 3.8, 5.1, 4.2])
y_pred = torch.tensor([2.3, 4.1, 5.0, 6.5])

loss = mse_loss(y_pred, y_true)
print(f"MSE Loss: {loss.item():.4f}")

# Ручная реализация для понимания
manual_mse = torch.mean((y_pred - y_true) ** 2)
print(f"Manual MSE: {manual_mse.item():.4f}")

# Демонстрация чувствительности к выбросам
y_true_outlier = torch.tensor([2.5, 3.8, 5.1, 4.2])
y_pred_outlier = torch.tensor([2.3, 4.1, 5.0, 15.0])  # Выброс в последнем предсказании

loss_outlier = mse_loss(y_pred_outlier, y_true_outlier)
print(f"MSE with outlier: {loss_outlier.item():.4f}")
MSE Loss: 1.3575
Manual MSE: 1.3575
MSE with outlier: 29.1950

Код демонстрирует базовое применение MSE и ее чувствительность к выбросам. В последнем примере одно аномальное предсказание (15.0 вместо 4.2) резко увеличивает значение функции потерь. Квадратичный штраф за отклонение доминирует над остальными ошибками, что может исказить процесс обучения если данные содержат нетипичные наблюдения.

Mean Absolute Error (MAE)

MAE использует абсолютное значение ошибки вместо квадрата:

MAE = (1/n) × Σ|yᵢ — ŷᵢ|

Где переменные имеют те же значения что и в MSE.

Линейный характер штрафа делает MAE более устойчивой к выбросам. Ошибка в 10 единиц дает штраф 10, независимо от того, одна это большая ошибка или десять маленьких по 1 единице. Модель не переобучается на аномальные значения.

MAE предпочтительна когда выбросы в данных не представляют интереса или возникают из-за ошибок измерения. В задачах прогнозирования спроса аномально высокие значения часто связаны с временными факторами (праздники, акции), которые модель не должна воспринимать как типичное поведение.

Градиент MAE остается постоянным и не меняется в зависимости от величины ошибки. Из-за этого сходимость алгоритма замедляется вблизи минимума: модель продолжает делать шаги одинакового размера, даже когда уже находится рядом с оптимальным решением. На практике это требует более осторожного подбора learning rate, чтобы избежать скачков и обеспечить стабильное обучение.

import torch
import torch.nn as nn

mae_loss = nn.L1Loss()

y_true = torch.tensor([2.5, 3.8, 5.1, 4.2])
y_pred = torch.tensor([2.3, 4.1, 5.0, 6.5])

loss = mae_loss(y_pred, y_true)
print(f"MAE Loss: {loss.item():.4f}")

# Сравнение с MSE при наличии выброса
y_true_outlier = torch.tensor([2.5, 3.8, 5.1, 4.2])
y_pred_outlier = torch.tensor([2.3, 4.1, 5.0, 15.0])

mae_outlier = mae_loss(y_pred_outlier, y_true_outlier)
mse_outlier = nn.MSELoss()(y_pred_outlier, y_true_outlier)

print(f"MAE with outlier: {mae_outlier.item():.4f}")
print(f"MSE with outlier: {mse_outlier.item():.4f}")
print(f"Ratio MSE/MAE: {(mse_outlier/mae_outlier).item():.2f}x")
MAE Loss: 0.7250
MAE with outlier: 2.8500
MSE with outlier: 29.1950
Ratio MSE/MAE: 10.24x

Код показывает разницу в поведении MAE и MSE при наличии выброса. MAE растет линейно с величиной ошибки, в то время как MSE растет квадратично. Отношение MSE к MAE демонстрирует насколько сильнее квадратичная функция потерь реагирует на аномальные предсказания.

Huber Loss

Huber Loss комбинирует свойства MSE и MAE через параметр delta:

При |yᵢ — ŷᵢ| ≤ δ: L = (1/2) × (yᵢ — ŷᵢ)²

При |yᵢ — ŷᵢ| > δ: L = δ × |yᵢ — ŷᵢ| — (1/2) × δ²

где:

  • δ (delta) — порог переключения между квадратичным и линейным режимами;
  • yᵢ — истинное значение;
  • ŷᵢ — предсказанное значение.

Huber Loss сочетает поведение MSE и MAE: для небольших ошибок она работает как MSE, а для крупных — как MAE. Такой подход обеспечивает оптимальный баланс: быструю и стабильную сходимость рядом с оптимумом и устойчивость к выбросам. Квадратичная часть создает достаточно сильный градиент, когда модель существенно ошибается, а линейная часть не позволяет аномальным значениям чрезмерно влиять на обучение.

👉🏻  Продвинутые методы предиктивной аналитики с глубокими нейронными сетями

Выбор delta определяет границу между «нормальными» и «большими» ошибками. Значение delta = 1.0 работает для нормализованных данных со средним 0 и стандартным отклонением 1. Для ненормализованных данных delta стоит устанавливать исходя из распределения ошибок на валидационной выборке — типично это 1-2 стандартных отклонения.

import torch
import torch.nn as nn

huber_loss = nn.HuberLoss(delta=1.0)

y_true = torch.tensor([2.5, 3.8, 5.1, 4.2])
y_pred = torch.tensor([2.3, 4.1, 5.0, 15.0])

loss = huber_loss(y_pred, y_true)
print(f"Huber Loss (delta=1.0): {loss.item():.4f}")

# Сравнение разных delta
deltas = [0.5, 1.0, 2.0, 5.0]
for delta in deltas:
    loss = nn.HuberLoss(delta=delta)(y_pred, y_true)
    print(f"Huber Loss (delta={delta}): {loss.item():.4f}")

# Визуализация поведения при разных ошибках
errors = torch.linspace(-5, 5, 100)
huber_values = torch.where(
    torch.abs(errors) <= 1.0,
    0.5 * errors ** 2,
    1.0 * torch.abs(errors) - 0.5
)
mse_values = 0.5 * errors ** 2
mae_values = torch.abs(errors)

print("\nПоведение функций при ошибке = 3.0:")
print(f"MSE: {0.5 * 3.0**2:.2f}")
print(f"MAE: {abs(3.0):.2f}")
print(f"Huber (δ=1.0): {1.0 * abs(3.0) - 0.5:.2f}")
Huber Loss (delta=1.0): 2.5925
Huber Loss (delta=0.5): 1.3363
Huber Loss (delta=1.0): 2.5925
Huber Loss (delta=2.0): 4.9175
Huber Loss (delta=5.0): 10.3925

Поведение функций при ошибке = 3.0:
MSE: 4.50
MAE: 3.00
Huber (δ=1.0): 2.50

Код демонстрирует влияние параметра delta на значение функции потерь. При малых delta Huber Loss ведет себя ближе к MAE, при больших — ближе к MSE. Правильный выбор delta зависит от распределения данных и толерантности к выбросам в конкретной задаче.

Quantile Loss

Quantile Loss позволяет предсказывать не только точечные оценки, но и квантили распределения. Функция использует асимметричный штраф:

L = Σ ρτ(yᵢ — ŷᵢ)

где:

  • ρτ(u) = u × (τ — I(u < 0));
  • τ — целевой квантиль (от 0 до 1);
  • I(u < 0) — индикаторная функция (1 если u < 0, иначе 0);
  • yᵢ — истинное значение;
  • ŷᵢ — предсказанное значение.

При τ = 0.5 функция симметрична и эквивалентна MAE. При τ < 0.5 недооценка штрафуется слабее переоценки, при τ > 0.5 наоборот. Это позволяет обучать модели предсказывать границы доверительных интервалов.

Quantile Loss применяется в риск-менеджменте для оценки Value at Risk (VaR). Предсказание 5-го процентиля (τ = 0.05) дает оценку максимальных потерь с вероятностью 95%. Модель штрафуется сильнее за недооценку риска чем за переоценку.

import torch
import numpy as np

def quantile_loss(y_pred, y_true, quantile):
    errors = y_true - y_pred
    loss = torch.where(
        errors >= 0,
        quantile * errors,
        (quantile - 1) * errors
    )
    return torch.mean(loss)

# Генерация данных с выбросами
torch.manual_seed(42)
y_true = torch.randn(100) * 2 + 10
y_pred_median = torch.ones(100) * 10  # Предсказание медианы
y_pred_q05 = torch.ones(100) * 7      # Предсказание 5-го процентиля

# Расчет потерь для разных квантилей
loss_median = quantile_loss(y_pred_median, y_true, 0.5)
loss_q05 = quantile_loss(y_pred_q05, y_true, 0.05)
loss_q95 = quantile_loss(y_pred_median, y_true, 0.95)

print(f"Quantile Loss (τ=0.5, median): {loss_median.item():.4f}")
print(f"Quantile Loss (τ=0.05, VaR): {loss_q05.item():.4f}")
print(f"Quantile Loss (τ=0.95): {loss_q95.item():.4f}")

# Обучение модели для предсказания разных квантилей
class QuantileRegressor(torch.nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.linear = torch.nn.Linear(input_dim, 1)
    
    def forward(self, x):
        return self.linear(x)

# Пример: предсказание 10-го, 50-го и 90-го процентилей
X = torch.randn(100, 5)
y = torch.sum(X, dim=1) + torch.randn(100) * 0.5

quantiles = [0.1, 0.5, 0.9]
models = {}

for q in quantiles:
    model = QuantileRegressor(5)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    
    for epoch in range(500):
        optimizer.zero_grad()
        y_pred = model(X).squeeze()
        loss = quantile_loss(y_pred, y, q)
        loss.backward()
        optimizer.step()
    
    models[q] = model
    print(f"Trained model for quantile {q}")
Quantile Loss (τ=0.5, median): 0.8123
Quantile Loss (τ=0.05, VaR): 0.1855
Quantile Loss (τ=0.95): 0.8661
Trained model for quantile 0.1
Trained model for quantile 0.5
Trained model for quantile 0.9

В примере выше представлена реализация Quantile Loss и обучение моделей для разных квантилей.

Ключевое свойство данной функции — асимметричность штрафа. При прогнозировании нижней границы (τ = 0.05) модель штрафуется сильнее когда реальное значение оказывается ниже предсказанного. Это заставляет модель быть консервативной в оценках рисков. Обучение нескольких моделей с разными τ позволяет получить полную картину распределения предсказаний.

Функции потерь для бинарной классификации

Бинарная классификация предсказывает вероятность принадлежности объекта к одному из двух классов. Функции потерь в этих задачах оценивают качество вероятностных предсказаний и должны быть дифференцируемыми для работы градиентного спуска. Основное требование — штрафовать не только неправильные предсказания, но и излишнюю уверенность модели в ошибочных ответах.

Binary Cross-Entropy (Log Loss)

Binary Cross-Entropy измеряет расхождение между предсказанным распределением вероятностей и истинными метками классов:

BCE = -(1/n) × Σ[yᵢ × log(ŷᵢ) + (1 — yᵢ) × log(1 — ŷᵢ)]

где:

  • n — количество примеров;
  • yᵢ — истинная метка класса (0 или 1);
  • ŷᵢ — предсказанная вероятность класса 1 (от 0 до 1).

Логарифм усиливает штраф когда модель уверенно ошибается. Предсказание вероятности 0.01 для класса 1 при истинной метке 1 дает большой штраф из-за log(0.01) ≈ -4.6. Предсказание 0.49 вместо 0.51 штрафуется слабо.

Binary Cross-Entropy — стандартная функция потерь для логистической регрессии и выходных слоев нейросетей с сигмоидной активацией. Она является выпуклой относительно параметров логистической регрессии, что гарантирует сходимость к глобальному минимуму при оптимизации.

При этом важно учитывать численную стабильность. Прямое вычисление log(0) или log(1) приводит к переполнению и потере точности. В PyTorch эти проблемы решаются с помощью BCEWithLogitsLoss, которая принимает логиты (значения до применения сигмоиды) и объединяет сигмоиду с расчетом функции потерь в одной операции. Такой подход предотвращает ошибки округления и обеспечивает более устойчивые вычисления.

import torch
import torch.nn as nn

# Нестабильная версия (для демонстрации)
def unstable_bce(y_pred, y_true):
    return -torch.mean(
        y_true * torch.log(y_pred) + (1 - y_true) * torch.log(1 - y_pred)
    )

# Стабильная версия через логиты
bce_with_logits = nn.BCEWithLogitsLoss()

# Пример данных
y_true = torch.tensor([1., 0., 1., 0., 1.])
logits = torch.tensor([2.5, -1.8, 0.5, -3.2, 4.1])  # Сырые выходы модели

# Правильный способ
loss = bce_with_logits(logits, y_true)
print(f"BCE with Logits Loss: {loss.item():.4f}")

# Демонстрация штрафа за уверенные ошибки
confident_wrong = torch.tensor([5.0])   # Сигмоид ≈ 0.993, очень уверенное предсказание класса 1
y_true_wrong = torch.tensor([0.])       # Истинный класс 0

loss_confident = bce_with_logits(confident_wrong, y_true_wrong)
print(f"Loss for confident wrong prediction: {loss_confident.item():.4f}")

uncertain = torch.tensor([0.1])         # Сигмоид ≈ 0.525, неуверенное предсказание
loss_uncertain = bce_with_logits(uncertain, y_true_wrong)
print(f"Loss for uncertain prediction: {loss_uncertain.item():.4f}")
BCE with Logits Loss: 0.1525
Loss for confident wrong prediction: 5.0067
Loss for uncertain prediction: 0.7444

Представленный пример кода демонстрирует применение численно стабильной версии Binary Cross-Entropy. BCEWithLogitsLoss принимает логиты напрямую, избегая промежуточного вычисления сигмоиды. Сравнение штрафов показывает что функция сильно наказывает уверенные ошибки: неправильное но уверенное предсказание (логит 5.0) дает потери около 5, в то время как неуверенное (логит 0.1) — около 0.7.

👉🏻  Как находить точки роста онлайн-бизнеса с помощью Python

Hinge Loss

Функция потерь Hinge Loss используется в моделях SVM (Support Vector Machines) и фокусируется на максимизации отступа между классами. Ее формула:

L = Σ max(0, 1 — yᵢ × ŷᵢ)

где:

  • yᵢ — истинная метка класса (-1 или +1, не 0 или 1);
  • ŷᵢ — сырой выход модели (не вероятность).

Функция штрафует примеры только если они находятся на неправильной стороне границы решения или слишком близко к ней. Если yᵢ × ŷᵢ ≥ 1, штраф равен нулю — модель достаточно уверена в правильном предсказании. При yᵢ × ŷᵢ < 1 начисляется линейный штраф.

Hinge Loss не предоставляет вероятностных оценок — она формирует жесткое разделение классов. Это особенно полезно в задачах, где важнее четкая решающая граница, а не точная калибровка вероятностей. В частности, SVM с Hinge Loss стремится найти гиперплоскость, которая максимизирует отступ от ближайших точек каждого класса, обеспечивая максимально уверенное разделение данных.

import torch
import torch.nn as nn

hinge_loss = nn.HingeEmbeddingLoss()

# Для Hinge Loss метки должны быть -1 или +1
y_true = torch.tensor([1., -1., 1., -1., 1.])
y_pred = torch.tensor([0.8, -1.5, 0.3, -0.2, 2.1])

# Ручной расчет для понимания
manual_hinge = torch.mean(torch.clamp(1 - y_true * y_pred, min=0))
print(f"Manual Hinge Loss: {manual_hinge.item():.4f}")

# Анализ поведения на разных примерах
examples = [
    (1.0, 2.0, "Correct, confident"),
    (1.0, 0.5, "Correct, in margin"),
    (1.0, -0.5, "Wrong"),
    (-1.0, -2.0, "Correct, confident"),
    (-1.0, 0.3, "Wrong, small violation")
]

print("\nBehavior analysis:")
for y_t, y_p, desc in examples:
    y_t_tensor = torch.tensor([y_t])
    y_p_tensor = torch.tensor([y_p])
    margin = y_t * y_p
    loss = torch.clamp(torch.tensor(1 - margin), min=0)
    print(f"{desc}: margin={margin:.2f}, loss={loss.item():.4f}")
Manual Hinge Loss: 0.3400

Behavior analysis:
Correct, confident: margin=2.00, loss=0.0000
Correct, in margin: margin=0.50, loss=0.5000
Wrong: margin=-0.50, loss=1.5000
Correct, confident: margin=2.00, loss=0.0000
Wrong, small violation: margin=-0.30, loss=1.3000

Код показывает работу Hinge Loss через концепцию отступа (margin). Произведение yᵢ × ŷᵢ называется отступом: положительное значение означает правильную сторону границы, отрицательное — ошибку.

Функция требует отступ минимум 1.0 для нулевого штрафа. Примеры с отступом от 0 до 1 находятся в «зоне неуверенности» и получают штраф пропорционально близости к границе. Это заставляет модель не просто правильно классифицировать объекты, но и делать это с запасом.

Focal Loss

Функция потерь Focal Loss модифицирует Binary Cross-Entropy для решения проблемы дисбаланса классов:

FL = -Σ αᵢ × (1 — ŷᵢ)^γ × log(ŷᵢ)

где:

  • αᵢ — вес класса (балансирует частоты классов);
  • γ (gamma) — фокусирующий параметр (обычно 2);
  • ŷᵢ — предсказанная вероятность правильного класса.

Множитель (1 — ŷᵢ)^γ снижает вес легко классифицируемых примеров. Когда модель уверенно правильно предсказывает класс (ŷᵢ ≈ 1), множитель близок к нулю и штраф минимален. Сложные примеры с ŷᵢ ≈ 0.5 сохраняют полный вес. Это перенаправляет фокус обучения на проблемные случаи.

Параметр α регулирует баланс между классами. В задачах детекции объектов встречается сильный дисбаланс данных — число фоновых областей может превышать количество объектов в соотношении 1000:1. Установка α = 0.25 для положительного класса уменьшает влияние многочисленных негативных примеров и помогает модели лучше обучаться на редких объектах.

Параметр γ определяет степень фокусировки. При γ = 0 функция сводится к обычной cross-entropy, а при γ = 2 (стандартное значение в задачах object detection) фокусировка усиливается: модель уделяет больше внимания сложным для классификации примерам и меньше — уже правильно классифицированным областям.

import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2.0):
        super().__init__()
        self.alpha = alpha
        self.gamma = gamma
    
    def forward(self, inputs, targets):
        bce_loss = F.binary_cross_entropy_with_logits(
            inputs, targets, reduction='none'
        )
        probas = torch.sigmoid(inputs)
        targets_probas = targets * probas + (1 - targets) * (1 - probas)
        
        focal_weight = (1 - targets_probas) ** self.gamma
        alpha_weight = targets * self.alpha + (1 - targets) * (1 - self.alpha)
        
        focal_loss = alpha_weight * focal_weight * bce_loss
        return focal_loss.mean()

# Имитация дисбаланса классов: 95% класса 0, 5% класса 1
torch.manual_seed(42)
n_samples = 1000
n_positive = 50

y_true = torch.zeros(n_samples)
y_true[:n_positive] = 1.0

logits = torch.randn(n_samples)

# Сравнение обычного BCE и Focal Loss
bce_loss = nn.BCEWithLogitsLoss()
focal_loss = FocalLoss(alpha=0.25, gamma=2.0)

loss_bce = bce_loss(logits, y_true)
loss_focal = focal_loss(logits, y_true)

print(f"BCE Loss (imbalanced): {loss_bce.item():.4f}")
print(f"Focal Loss (imbalanced): {loss_focal.item():.4f}")

# Анализ весов для легких и сложных примеров
easy_example = torch.tensor([5.0])    # Высокая уверенность
hard_example = torch.tensor([0.1])    # Низкая уверенность
y_pos = torch.tensor([1.0])

focal_easy = focal_loss(easy_example, y_pos)
focal_hard = focal_loss(hard_example, y_pos)

print(f"\nFocal Loss for easy example: {focal_easy.item():.4f}")
print(f"Focal Loss for hard example: {focal_hard.item():.4f}")
print(f"Ratio hard/easy: {(focal_hard/focal_easy).item():.1f}x")
BCE Loss (imbalanced): 0.8078
Focal Loss (imbalanced): 0.2515

Focal Loss for easy example: 0.0000
Focal Loss for hard example: 0.0364
Ratio hard/easy: 483382.4x

Код реализует Focal Loss и демонстрирует ее поведение на небалансированных данных:

  1. Фокусирующий множитель (1 — p)^γ существенно снижает вклад легких примеров: отношение штрафа за сложный пример к легкому достигает 483000x;
  2. Когда модель уверенно правильно предсказывает класс, штраф становится практически нулевым (0.0000 для легкого примера), в то время как сложные примеры с низкой уверенностью сохраняют значительный вес (0.0364). Это перенаправляет градиенты на проблемные случаи;
  3. Параметр alpha дополнительно балансирует классы, предотвращая доминирование частого класса — на небалансированных данных Focal Loss (0.2515) дает более низкое значение чем обычный BCE (0.8078), концентрируя обучение на меньшинстве.
👉🏻  Что такое градиентный спуск и как он используется для оптимизации функций?

Функции потерь для многоклассовой классификации

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

Categorical Cross-Entropy

Функция потерь Categorical Cross-Entropy обобщает Binary Cross-Entropy на случай нескольких классов:

CCE = -Σᵢ Σⱼ yᵢⱼ × log(ŷᵢⱼ)

где:

  • i — индекс примера;
  • j — индекс класса;
  • yᵢⱼ — элемент one-hot вектора (1 для правильного класса, 0 для остальных);
  • ŷᵢⱼ — предсказанная вероятность класса j для примера i.

Функция вычисляет кросс-энтропию между предсказанным распределением вероятностей и истинным распределением (one-hot вектор). Поскольку в one-hot векторе только один элемент равен 1, формула упрощается до:

-log(ŷᵢc) , где c — индекс правильного класса.

Модель штрафуется только за вероятность правильного класса, вероятности остальных классов влияют косвенно через нормализацию.

Categorical Cross-Entropy предполагает использование меток one-hot кодирования. Например, для задачи с тремя классами вектор [0, 1, 0] означает, что образец принадлежит классу 1. На выходе модели обычно применяется слой softmax, который преобразует логиты в вероятности.

import torch
import torch.nn as nn
import torch.nn.functional as F

# Данные: 5 примеров, 3 класса
logits = torch.tensor([
    [2.0, 1.0, 0.1],
    [0.5, 2.5, 0.3],
    [0.1, 0.2, 3.0],
    [1.5, 1.5, 1.5],
    [3.0, 0.5, 0.5]
])

# One-hot метки
targets_onehot = torch.tensor([
    [1., 0., 0.],
    [0., 1., 0.],
    [0., 0., 1.],
    [0., 1., 0.],
    [1., 0., 0.]
])

# Ручной расчет через softmax
probas = F.softmax(logits, dim=1)
manual_cce = -torch.mean(torch.sum(targets_onehot * torch.log(probas), dim=1))
print(f"Manual Categorical Cross-Entropy: {manual_cce.item():.4f}")

# Анализ уверенности предсказаний
for i in range(5):
    true_class = torch.argmax(targets_onehot[i]).item()
    pred_proba = probas[i, true_class].item()
    example_loss = -torch.log(probas[i, true_class])
    print(f"Example {i}: true_class={true_class}, "
          f"proba={pred_proba:.3f}, loss={example_loss:.3f}")
Manual Categorical Cross-Entropy: 0.3995
Example 0: true_class=0, proba=0.659, loss=0.417
Example 1: true_class=1, proba=0.802, loss=0.220
Example 2: true_class=2, proba=0.896, loss=0.110
Example 3: true_class=1, proba=0.333, loss=1.099
Example 4: true_class=0, proba=0.859, loss=0.152

Код демонстрирует вычисление Categorical Cross-Entropy через one-hot представление. Функция потерь для каждого примера равна отрицательному логарифму вероятности правильного класса. Пример 3 с равными логитами [1.5, 1.5, 1.5] дает вероятность 1/3 для каждого класса и высокий штраф -log(1/3) ≈ 1.1, показывая неуверенность модели. Примеры с четкими предсказаниями (высокий логит правильного класса) получают низкий штраф.

Sparse Categorical Cross-Entropy

Функция потерь Sparse Categorical Cross-Entropy математически эквивалентна обычной Categorical Cross-Entropy, но принимает метки классов как целые числа вместо one-hot векторов. Ее формула расчета следующая:

SCCE = -Σᵢ log(ŷᵢ,cᵢ)

где:

  • i — индекс примера;
  • cᵢ — целочисленная метка класса для примера i (0, 1, 2, …);
  • ŷᵢ,cᵢ — предсказанная вероятность правильного класса cᵢ.

Разница между двумя вариантами — чисто техническая. Sparse-версия значительно экономит память при большом количестве классов. Например, хранение метки как числа «42» занимает всего 4 байта, тогда как one-hot вектор размерности 1000 требует около 4000 байт. В задачах с тысячами классов (таких как классификация слов в NLP) эта экономия становится весьма существенной.

import torch
import torch.nn as nn

# Данные: 5 примеров, 3 класса
logits = torch.tensor([
    [2.0, 1.0, 0.1],
    [0.5, 2.5, 0.3],
    [0.1, 0.2, 3.0],
    [1.5, 1.5, 1.5],
    [3.0, 0.5, 0.5]
])

# Sparse метки (целые числа)
targets_sparse = torch.tensor([0, 1, 2, 1, 0])

# Использование PyTorch CrossEntropyLoss (автоматически применяет softmax)
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits, targets_sparse)
print(f"Sparse Categorical Cross-Entropy: {loss.item():.4f}")

# Сравнение с one-hot версией
targets_onehot = torch.zeros(5, 3)
targets_onehot[range(5), targets_sparse] = 1.0

probas = torch.nn.functional.softmax(logits, dim=1)
manual_loss = -torch.mean(torch.sum(targets_onehot * torch.log(probas), dim=1))
print(f"One-hot version (should match): {manual_loss.item():.4f}")

# Демонстрация эффективности для большого числа классов
print("\nMemory efficiency comparison:")

# Малое число классов
n_classes_small = 10
targets_small = torch.randint(0, n_classes_small, (100,))
onehot_small = torch.zeros(100, n_classes_small)

sparse_size_small = targets_small.element_size() * targets_small.nelement()
onehot_size_small = onehot_small.element_size() * onehot_small.nelement()

print(f"\n{n_classes_small} classes:")
print(f"Sparse: {sparse_size_small / 1024:.2f} KB")
print(f"One-hot: {onehot_size_small / 1024:.2f} KB")
print(f"Ratio: {onehot_size_small / sparse_size_small:.1f}x")

# Среднее число классов
n_classes_medium = 1000
targets_medium = torch.randint(0, n_classes_medium, (100,))
onehot_medium = torch.zeros(100, n_classes_medium)

sparse_size_medium = targets_medium.element_size() * targets_medium.nelement()
onehot_size_medium = onehot_medium.element_size() * onehot_medium.nelement()

print(f"\n{n_classes_medium} classes:")
print(f"Sparse: {sparse_size_medium / 1024:.2f} KB")
print(f"One-hot: {onehot_size_medium / 1024:.2f} KB")
print(f"Ratio: {onehot_size_medium / sparse_size_medium:.1f}x")

# Большое число классов
n_classes_large = 50000
targets_large = torch.randint(0, n_classes_large, (100,))
onehot_large = torch.zeros(100, n_classes_large)

sparse_size_large = targets_large.element_size() * targets_large.nelement()
onehot_size_large = onehot_large.element_size() * onehot_large.nelement()

print(f"\n{n_classes_large} classes:")
print(f"Sparse: {sparse_size_large / 1024:.2f} KB")
print(f"One-hot: {onehot_size_large / 1024:.2f} KB")
print(f"Ratio: {onehot_size_large / sparse_size_large:.1f}x")
Sparse Categorical Cross-Entropy: 0.3995
One-hot version (should match): 0.3995

Memory efficiency comparison:

10 classes:
Sparse: 0.78 KB
One-hot: 3.91 KB
Ratio: 5.0x

1000 classes:
Sparse: 0.78 KB
One-hot: 390.62 KB
Ratio: 500.0x

50000 classes:
Sparse: 0.78 KB
One-hot: 19531.25 KB
Ratio: 25000.0x

Демонстрация использования памяти наглядно показывает практическую ценность sparse-формата: размер меток в sparse-представлении остается постоянным (около 0.78 KB), независимо от количества классов. В то время как размер one-hot кодирования растет линейно: для 10 классов разница составляет примерно 5 раз, для 1000 классов — уже 500 раз, а для 50 000 классов — около 25 000 раз.

👉🏻  Регуляризация: L1 (Lasso) vs L2 (Ridge). Борьба с переобучением, отбор признаков

В задачах NLP, где словари могут включать десятки тысяч токенов, sparse-формат становится критически важным методом для эффективности обучения и экономии памяти.

KL Divergence

Kullback-Leibler Divergence измеряет различие между двумя распределениями вероятностей:

KL(P||Q) = Σⱼ Pⱼ × log(Pⱼ / Qⱼ)

где:

  • P — истинное распределение (целевое);
  • Q — предсказанное распределение;
  • j — индекс класса.

Функция потерь KL Divergence является асимметричной:

KL(P‖Q) ≠ KL(Q‖P)

Она особенно сильно штрафует ситуации, когда модель занижает вероятность событий, которым истинное распределение приписывает высокий вес. Например, если истинное распределение дает классу A вероятность 0.8, а модель — всего 0.2, штраф получается значительным, так как log(0.8/0.2) ≈ 1.4.

KL Divergence применяют, когда целевое распределение «мягкое», а не one-hot. Например, в задачах knowledge distillation студенческая модель обучается повторять выходное распределение учительской модели, а не точные метки классов. В задачах multi-label классификации с пересекающимися классами целевой вектор может выглядеть как [0.7, 0.2, 0.1], отражая разную степень принадлежности к каждому классу.

import torch
import torch.nn as nn
import torch.nn.functional as F

kl_div_loss = nn.KLDivLoss(reduction='batchmean')

# Мягкие метки (не one-hot)
targets_soft = torch.tensor([
    [0.7, 0.2, 0.1],
    [0.1, 0.8, 0.1],
    [0.2, 0.2, 0.6],
    [0.4, 0.4, 0.2],
    [0.8, 0.1, 0.1]
])

logits = torch.tensor([
    [2.0, 1.0, 0.1],
    [0.5, 2.5, 0.3],
    [0.1, 0.2, 3.0],
    [1.5, 1.5, 1.5],
    [3.0, 0.5, 0.5]
])

# KL Divergence требует логарифмы предсказаний
log_probas = F.log_softmax(logits, dim=1)
loss = kl_div_loss(log_probas, targets_soft)
print(f"KL Divergence Loss: {loss.item():.4f}")

# Сравнение с Cross-Entropy на hard labels
targets_hard = torch.tensor([0, 1, 2, 1, 0])
ce_loss = nn.CrossEntropyLoss()(logits, targets_hard)
print(f"Cross-Entropy Loss: {ce_loss.item():.4f}")

# Демонстрация асимметричности
P = torch.tensor([[0.8, 0.2]])
Q1 = torch.tensor([[0.7, 0.3]])
Q2 = torch.tensor([[0.9, 0.1]])

kl_PQ1 = kl_div_loss(torch.log(Q1), P)
kl_PQ2 = kl_div_loss(torch.log(Q2), P)

print(f"\nKL(P||Q1) where Q1=[0.7, 0.3]: {kl_PQ1.item():.4f}")
print(f"KL(P||Q2) where Q2=[0.9, 0.1]: {kl_PQ2.item():.4f}")
KL Divergence Loss: 0.0724
Cross-Entropy Loss: 0.3995

KL(P||Q1) where Q1=[0.7, 0.3]: 0.0257
KL(P||Q2) where Q2=[0.9, 0.1]: 0.0444

Код демонстрирует применение KL Divergence с мягкими метками. Функция принимает логарифмы предсказанных вероятностей (log_softmax) и целевое распределение. Мягкие метки полезны в knowledge distillation: большая модель-учитель дает не жесткие 0/1, а распределение вероятностей, содержащее больше информации о структуре данных.

KL Divergence (0.0724) дает более низкое значение чем Cross-Entropy (0.3995) на тех же данных, поскольку мягкие метки содержат ненулевые вероятности для нескольких классов. Пример асимметричности показывает что отклонение от целевого распределения P=[0.8, 0.2] к Q2=[0.9, 0.1] дает больший штраф (0.0444) чем к Q1=[0.7, 0.3] (0.0257), хотя оба отклонения составляют 0.1 по абсолютной величине — KL Divergence сильнее штрафует когда модель присваивает низкую вероятность событиям с высокой истинной вероятностью.

Специализированные функции потерь

Некоторые задачи машинного обучения требуют функций потерь учитывающих специфику предметной области. Например:

  • в сегментации изображений работают с попиксельными масками;
  • в object detection важны пересечения bounding box’ов;
  • в метрическом обучении учитываются расстояния в embedding-пространстве.

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

Dice Loss и IoU Loss

Функция потерь Dice Loss основана на коэффициенте Dice, измеряющем сходство двух множеств:

Dice = (2 × |A ∩ B|) / (|A| + |B|)

Dice Loss = 1 — Dice

где:

  • A — множество предсказанных пикселей класса;
  • B — множество истинных пикселей класса;
  • |A ∩ B| — количество правильно предсказанных пикселей;
  • |A| + |B| — сумма размеров множеств.

Коэффициент Dice измеряет степень перекрытия между предсказанием и истиной. Значение 1 означает полное совпадение, 0 — отсутствие пересечения. Dice Loss инвертирует метрику для минимизации.

Функция IoU (Intersection over Union) Loss использует похожую логику:

IoU = |A ∩ B| / |A ∪ B|

IoU Loss = 1 — IoU

Где |A ∪ B| — объединение множеств (все пиксели предсказанные или истинные).

Dice более чувствительна к малым объектам. В числителе стоит удвоенное пересечение, что усиливает вклад правильных предсказаний. Для объекта размером 100 пикселей правильное предсказание 50 пикселей дает Dice = 2×50/(100+100) = 0.5. IoU для той же ситуации: 50/(100+50) ≈ 0.33. Dice растет быстрее, давая модели более сильный сигнал на малых объектах.

import torch
import torch.nn as nn

class DiceLoss(nn.Module):
    def __init__(self, smooth=1.0):
        super().__init__()
        self.smooth = smooth
    
    def forward(self, predictions, targets):
        predictions = torch.sigmoid(predictions)
        
        intersection = (predictions * targets).sum(dim=(1, 2))
        union = predictions.sum(dim=(1, 2)) + targets.sum(dim=(1, 2))
        
        dice = (2.0 * intersection + self.smooth) / (union + self.smooth)
        return 1.0 - dice.mean()

class IoULoss(nn.Module):
    def __init__(self, smooth=1.0):
        super().__init__()
        self.smooth = smooth
    
    def forward(self, predictions, targets):
        predictions = torch.sigmoid(predictions)
        
        intersection = (predictions * targets).sum(dim=(1, 2))
        union = predictions.sum(dim=(1, 2)) + targets.sum(dim=(1, 2)) - intersection
        
        iou = (intersection + self.smooth) / (union + self.smooth)
        return 1.0 - iou.mean()

# Пример: сегментация с малым объектом
torch.manual_seed(42)
batch_size, height, width = 2, 64, 64

# Создание маски с малым объектом (10x10 пикселей)
targets = torch.zeros(batch_size, height, width)
targets[:, 20:30, 20:30] = 1.0

# Предсказание с частичным перекрытием
predictions_logits = torch.randn(batch_size, height, width) - 2.0
predictions_logits[:, 22:28, 22:28] += 5.0  # Частичное перекрытие

dice_loss = DiceLoss()
iou_loss = IoULoss()

loss_dice = dice_loss(predictions_logits, targets)
loss_iou = iou_loss(predictions_logits, targets)

print(f"Dice Loss: {loss_dice.item():.4f}")
print(f"IoU Loss: {loss_iou.item():.4f}")

# Сравнение с BCE
bce_loss = nn.BCEWithLogitsLoss()
loss_bce = bce_loss(predictions_logits, targets)
print(f"BCE Loss: {loss_bce.item():.4f}")

# Анализ поведения на разных уровнях перекрытия
overlaps = [0.2, 0.4, 0.6, 0.8, 1.0]
print("\nDice vs IoU for different overlaps:")
for overlap in overlaps:
    dice_val = (2 * overlap) / (1 + overlap)
    iou_val = overlap / (2 - overlap)
    print(f"Overlap {overlap:.1f}: Dice={dice_val:.3f}, IoU={iou_val:.3f}")
Dice Loss: 0.8852
IoU Loss: 0.9385
BCE Loss: 0.2129

Dice vs IoU for different overlaps:
Overlap 0.2: Dice=0.333, IoU=0.111
Overlap 0.4: Dice=0.571, IoU=0.250
Overlap 0.6: Dice=0.750, IoU=0.429
Overlap 0.8: Dice=0.889, IoU=0.667
Overlap 1.0: Dice=1.000, IoU=1.000

Код реализует Dice Loss и IoU Loss для сегментации. Параметр smooth предотвращает деление на ноль когда и предсказание и цель пусты.

👉🏻  RFM-анализ с помощью Python

Сравнение показывает что при малом перекрытии масок Dice Loss (0.8852) и IoU Loss (0.9385) дают высокие значения, сигнализируя о плохом качестве предсказания, в то время как BCE (0.2129) показывает умеренный штраф из-за большого количества правильно предсказанного фона.

Сравнение разных уровней перекрытия показывает, что Dice растет быстрее, чем IoU. Например, при 40% перекрытия Dice = 0.571, а IoU = 0.250. Это делает Dice более чувствительной к улучшениям на малых объектах, обеспечивая модели более сильный градиентный сигнал.

В отличие от Dice и IoU, Binary Cross-Entropy оценивает каждый пиксель независимо, не учитывая глобальное перекрытие масок. Для задач, где важна целостная форма объекта, использование Dice или IoU позволяет модели лучше оптимизировать качество сегментации.

Contrastive Loss

Contrastive Loss обучает модель создавать пространство эмбеддингов таким образом, чтобы похожие объекты располагались близко друг к другу, а различающиеся — далеко. Формула расчета функции следующая:

L = (1 — Y) × (1/2) × D² + Y × (1/2) × max(0, m — D)²

где:

  • Y — метка пары (0 для похожих, 1 для различных);
  • D — евклидово расстояние между эмбеддингами;
  • m (margin) — минимальное расстояние для различных пар.

Функция работает с парами примеров. Для похожих пар (Y = 0) минимизируется расстояние D. Для различных пар (Y = 1) минимизируется max(0, m — D), что штрафует модель только если различные примеры ближе чем margin. Если различные объекты уже достаточно далеко (D ≥ m), штраф нулевой.

Функцию потерь Contrastive Loss используют для распознавания лиц (face recognition) и задач обучения на сходство (similarity learning). Модель тренируют на парах изображений: одного человека (похожие) и разных людей (различные). В результате формируется пространство эмбеддингов, где объекты можно сравнивать по расстоянию, без необходимости заново обучать классификатор для каждого нового набора людей.

Параметр margin (разрыв) задает, насколько далеко должны быть разные объекты. Если значение слишком маленькое, классы плохо разделяются; если слишком большое, модель пытается разнести объекты нереально далеко. Обычно используют 𝑚=1.0 для нормализованных векторов признаков или устанавливают margin равным среднему расстоянию между разными парами в данных.

import torch
import torch.nn as nn

class ContrastiveLoss(nn.Module):
    def __init__(self, margin=2.0):
        super().__init__()
        self.margin = margin
    
    def forward(self, embedding1, embedding2, label):
        distance = torch.nn.functional.pairwise_distance(embedding1, embedding2)
        
        loss_similar = (1 - label) * torch.pow(distance, 2)
        loss_dissimilar = label * torch.pow(torch.clamp(self.margin - distance, min=0.0), 2)
        
        loss = 0.5 * (loss_similar + loss_dissimilar)
        return loss.mean()

class EmbeddingNet(nn.Module):
    def __init__(self, input_dim, embedding_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Linear(128, embedding_dim)
        )
    
    def forward(self, x):
        return self.fc(x)

# Генерация данных: 3 класса с перекрытием в пространстве признаков
torch.manual_seed(42)
class1_data = torch.randn(50, 10) * 2.0 + torch.tensor([1.5, 1.5] + [0.0] * 8)
class2_data = torch.randn(50, 10) * 2.0 + torch.tensor([-1.5, 1.5] + [0.0] * 8)
class3_data = torch.randn(50, 10) * 2.0 + torch.tensor([0.0, -2.0] + [0.0] * 8)

# Создание пар
pairs = []
for i in range(30):
    idx1, idx2 = torch.randint(0, 50, (2,))
    pairs.append((class1_data[idx1], class1_data[idx2], 0))
    pairs.append((class1_data[idx1], class2_data[idx2], 1))
    pairs.append((class2_data[idx1], class3_data[idx2], 1))

# Обучение
model = EmbeddingNet(input_dim=10, embedding_dim=5)
criterion = ContrastiveLoss(margin=2.0)
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)

for epoch in range(150):
    total_loss = 0.0
    
    for x1, x2, label in pairs:
        optimizer.zero_grad()
        
        emb1 = model(x1.unsqueeze(0))
        emb2 = model(x2.unsqueeze(0))
        
        loss = criterion(emb1, emb2, torch.tensor([label], dtype=torch.float))
        loss.backward()
        optimizer.step()
        
        total_loss += loss.item()
    
    if (epoch + 1) % 30 == 0:
        print(f"Epoch {epoch+1}: Loss = {total_loss/len(pairs):.4f}")

# Проверка расстояний
with torch.no_grad():
    emb_c1 = model(class1_data[:5])
    emb_c2 = model(class2_data[:5])
    emb_c3 = model(class3_data[:5])
    
    dist_within_c1 = torch.nn.functional.pairwise_distance(emb_c1[0:1], emb_c1[1:2])
    dist_c1_c2 = torch.nn.functional.pairwise_distance(emb_c1[0:1], emb_c2[0:1])
    dist_c1_c3 = torch.nn.functional.pairwise_distance(emb_c1[0:1], emb_c3[0:1])
    
    print(f"\nDistance within class 1: {dist_within_c1.item():.4f}")
    print(f"Distance class 1 to class 2: {dist_c1_c2.item():.4f}")
    print(f"Distance class 1 to class 3: {dist_c1_c3.item():.4f}")
Epoch 30: Loss = 0.0351
Epoch 60: Loss = 0.0629
Epoch 90: Loss = 0.0314
Epoch 120: Loss = 0.0000
Epoch 150: Loss = 0.0001

Distance within class 1: 0.9520
Distance class 1 to class 2: 2.0083
Distance class 1 to class 3: 4.0971

Код демонстрирует обучение embedding сети с Contrastive Loss. Модель учится размещать примеры одного класса близко друг к другу и далеко от другого класса.

После обучения расстояние внутри класса существенно меньше расстояния между классами, что подтверждает формирование структурированного embedding пространства. Margin контролирует минимальное разделение: модель прекращает увеличивать расстояние между различными парами когда оно превышает margin, концентрируя усилия на проблемных случаях.

Triplet Loss

Функция Triplet Loss работает с тройками примеров: anchor, positive, negative. Расчетная формула:

L = max(0, D(a, p) — D(a, n) + m)

где:

  • a — anchor (опорный пример);
  • p — positive (похожий на anchor);
  • n — negative (отличный от anchor);
  • D — функция расстояния (обычно L2);
  • m (margin) — минимальный отступ.

Функция требует чтобы расстояние от anchor до positive было меньше расстояния до negative хотя бы на margin. Если D(a, p) + m < D(a, n), тройка уже разделена правильно и штраф нулевой. В противном случае модель штрафуется пропорционально нарушению условия.

Triplet Loss эффективнее Contrastive Loss поскольку напрямую сравнивает относительные расстояния. Contrastive оптимизирует пары независимо, Triplet гарантирует правильную упорядоченность расстояний в тройке. Это приводит к более компактным кластерам схожих объектов.

👉🏻  LSTM для прогнозирования волатильности: многослойные архитектуры и sequence-to-sequence подходы

Однако тут надо учитывать, что качество обучения сильно зависит от того, как формируются тройки:

  • Простая случайная выборка дает в основном легкие тройки, где negative уже далеко от anchor, и градиент мало информативен;
  • Метод hard negative mining выбирает сложные negative-примеры — объекты другого класса, близкие к anchor;
  • Semi-hard mining (полу-сложная выборка) выбирает negative дальше positive, но все еще в пределах margin (разрыва): D(a, p) < D(a, n) < D(a, p) + m. Такой подход позволяет получить наиболее информативный градиент и ускоряет обучение.
import torch
import torch.nn as nn

class TripletLoss(nn.Module):
    def __init__(self, margin=1.0):
        super().__init__()
        self.margin = margin
    
    def forward(self, anchor, positive, negative):
        distance_positive = torch.nn.functional.pairwise_distance(anchor, positive)
        distance_negative = torch.nn.functional.pairwise_distance(anchor, negative)
        
        losses = torch.clamp(distance_positive - distance_negative + self.margin, min=0.0)
        return losses.mean()

class TripletNet(nn.Module):
    def __init__(self, input_dim, embedding_dim):
        super().__init__()
        self.embedding = nn.Sequential(
            nn.Linear(input_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, embedding_dim)
        )
    
    def forward(self, x):
        return nn.functional.normalize(self.embedding(x), p=2, dim=1)

# Генерация данных
torch.manual_seed(42)
n_samples = 100
class1 = torch.randn(n_samples, 10) * 1.2 + torch.tensor([1.0] * 10)
class2 = torch.randn(n_samples, 10) * 1.2 + torch.tensor([0.0] * 10)
class3 = torch.randn(n_samples, 10) * 1.2 + torch.tensor([-1.0] * 10)

def mine_hard_triplets(n_triplets=50):
    triplets = []
    classes = [class1, class2, class3]
    
    for _ in range(n_triplets):
        anchor_class = torch.randint(0, 3, (1,)).item()
        
        anchor_idx = torch.randint(0, n_samples, (1,)).item()
        positive_idx = torch.randint(0, n_samples, (1,)).item()
        
        anchor = classes[anchor_class][anchor_idx]
        positive = classes[anchor_class][positive_idx]
        
        negative_classes = [i for i in range(3) if i != anchor_class]
        negative_class = negative_classes[torch.randint(0, len(negative_classes), (1,)).item()]
        negative_idx = torch.randint(0, n_samples, (1,)).item()
        negative = classes[negative_class][negative_idx]
        
        triplets.append((anchor, positive, negative))
    
    return triplets

# Обучение
model = TripletNet(input_dim=10, embedding_dim=8)
criterion = TripletLoss(margin=0.5)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(200):
    triplets = mine_hard_triplets(n_triplets=30)
    total_loss = 0.0
    
    for anchor, positive, negative in triplets:
        optimizer.zero_grad()
        
        emb_a = model(anchor.unsqueeze(0))
        emb_p = model(positive.unsqueeze(0))
        emb_n = model(negative.unsqueeze(0))
        
        loss = criterion(emb_a, emb_p, emb_n)
        loss.backward()
        optimizer.step()
        
        total_loss += loss.item()
    
    if (epoch + 1) % 40 == 0:
        print(f"Epoch {epoch+1}: Loss = {total_loss/30:.4f}")

# Анализ embedding пространства
with torch.no_grad():
    emb1 = model(class1[:10])
    emb2 = model(class2[:10])
    emb3 = model(class3[:10])
    
    dist_within_c1 = torch.nn.functional.pairwise_distance(emb1[0:1], emb1[1:2])
    
    dist_c1_c2 = torch.nn.functional.pairwise_distance(emb1[0:1], emb2[0:1])
    dist_c1_c3 = torch.nn.functional.pairwise_distance(emb1[0:1], emb3[0:1])
    
    print(f"\nDistance within class 1: {dist_within_c1.item():.4f}")
    print(f"Distance class 1 to class 2: {dist_c1_c2.item():.4f}")
    print(f"Distance class 1 to class 3: {dist_c1_c3.item():.4f}")
Epoch 40: Loss = 0.0105
Epoch 80: Loss = 0.1694
Epoch 120: Loss = 0.0792
Epoch 160: Loss = 0.0031
Epoch 200: Loss = 0.0408

Distance within class 1: 0.5122
Distance class 1 to class 2: 1.4396
Distance class 1 to class 3: 1.9506

Код реализует Triplet Loss с простой стратегией hard negative mining. Модель обучается размещать эмбеддинги таким образом, чтобы расстояние anchor-positive было минимум на margin меньше расстояния anchor-negative.

Нормализация эмбеддингов с помощью L2-нормы до единицы стабилизирует обучение и делает расстояния между объектами сопоставимыми. После обучения расстояния внутри класса становятся существенно меньше, чем между разными классами, а межклассовые расстояния остаются почти идентичными. Это наглядно показывает формирование четких кластеров в embedding-пространстве.

Практические рекомендации по выбору функции потерь

Выбор функции потерь определяется типом задачи, характеристиками данных и бизнес-метриками.

Регрессия

Базовый выбор — MSE (Mean Squared Error) или MAE (Mean Absolute Error).

  • MSE лучше использовать, когда большие ошибки критичнее малых, например при прогнозировании цен активов: недооценка резких движений ведет к значительным потерям;
  • MAE более устойчив к выбросам и эффективен, когда все ошибки имеют примерно одинаковую важность;
  • Huber Loss сочетает преимущества MSE и MAE, требуя подбора параметра delta для переключения между квадратичной и линейной частью;
  • Quantile Loss используется, когда важны доверительные интервалы или асимметричная оценка рисков, например в финансовом прогнозировании или страховании.

Бинарная классификация

Стандартно применяют Binary Cross-Entropy (BCE), которая хорошо калибрует вероятности и подходит для градиентного спуска.

При сильном дисбалансе классов полезна Focal Loss, где параметр α (alpha) регулирует вес классов, а γ (gamma) фокусирует обучение на сложных примерах.

Hinge Loss применяют, когда важна максимизация отступа (margin) между классами без необходимости предсказывать вероятности.

Многоклассовая классификация

Как правило используется функция потерь Categorical Cross-Entropy с one-hot метками.

При больших объемах данных и большом числе классов можно рассмотреть Sparse Categorical Cross-Entropy — она принимает целочисленные метки и экономит память.

KL Divergence актуальна, когда целевое распределение «мягкое» (soft labels), например в knowledge distillation или multi-label задачах, где метки отражают относительную принадлежность к классам.

Специализированные функции потерь

В задачах, где метрики, оценивающие точность по отдельным точкам или пикселям не отражают реальное качество модели, используют специальные (кастомные) функции потерь:

  • Dice Loss — для сегментации с маленькими объектами, особенно в медицинской визуализации;
  • IoU Loss — для оценки глобального перекрытия масок;
  • Contrastive Loss и Triplet Loss — для формирования embedding-пространств в задачах similarity learning и метрического обучения, где важны относительные расстояния между объектами.

Заключение

Функция потерь — это ключевой инструмент, связывающий бизнес-цель и процесс обучения модели. Правильный выбор функции определяет, какие ошибки модель будет минимизировать в приоритетном порядке и на какие аспекты задачи она будет ориентироваться.

Глубокое понимание свойств различных функций потерь позволяет создавать модели машинного обучения, которые не просто оптимизируют формальные метрики, а действительно решают задачу эффективно, учитывая специфику данных и требования бизнеса.