PyTorch: функции потерь
Функция потерь измеряет расхождение между предсказаниями модели и истинными значениями и служит основным ориентиром для обучения нейронной сети — оптимизатор обновляет параметры модели, минимизируя функцию потерь.
PyTorch, в своёмtorch.nnмодуле, имеет встроенные более десяти распространённых функций потерь, охватывающих основные типы задач, такие как классификация, регрессия и ранжирование.
1. Основы функций потерь
Базовое использование
Все функции потерь PyTorch являютсяnn.Moduleподклассами , с единым использованием:
Пример
import torch.nn as nn
# 1. Создайте экземпляр функции потерь
criterion = nn.CrossEntropyLoss()
# 2. Вычислите потерю (сначала предсказания, затем истинные значения)
loss = criterion(predictions, targets)
# 3. Обратное распространение
loss.backward()
Соглашения о форме для предсказанных значений
Различные функции потерь предъявляют разные требования к форме входных данных; это наиболее распространённый источник ошибок у начинающих:
| Функция потерь | Форма предсказания (входных данных) | Форма метки (целевого значения) |
|---|---|---|
CrossEntropyLoss | (N, C)Необработанные логиты | (N,)Целочисленные индексы классов |
BCELoss | (N,)Вероятности после Sigmoid | (N,)Числа с плавающей запятой 0/1 |
BCEWithLogitsLoss | (N,)Необработанные логиты | (N,)Числа с плавающей запятой 0/1 |
MSELoss | (N,)Любое вещественное число | (N,)Любое вещественное число |
NLLLoss | (N, C)Вероятности после log_softmax | (N,)Целочисленные индексы классов |
N = размер пакета,C= число классов
2. Функции потерь для задач классификации
2.1 CrossEntropyLoss (потеря кросс-энтропии)
Наиболее часто используемая функция потерь для многоклассовой классификации,внутри автоматически выполняет Softmax + log + смену знака; не нужно вручную применять Softmax к выходу модели.
Математическая формула:
Loss = -sum(y_c * log(p_c))
где p_c = exp(x_c) / sum_j exp(x_j) — это выход Softmax.
Пример
import torch.nn as nn
criterion = nn.CrossEntropyLoss()
# Выход модели: сырые логиты, форма (batch_size, num_classes)
# Не нужно применять Softmax заранее!
predictions = torch.tensor([
[2.0, 0.5, 0.3], # Пример 1, наиболее вероятный класс 0
[0.1, 3.0, 0.2], # Пример 2, наиболее вероятный класс 1
[0.2, 0.1, 4.0], # Пример 3, наиболее вероятный класс 2
])
# Метки: целочисленные индексы классов, форма (batch_size,)
targets = torch.tensor([0, 1, 2])
loss = criterion(predictions, targets)
print(f"Loss: {loss.item():.4f}") # Потеря: 0.1763
Поддерживает мягкие метки (Label Smoothing):
Пример
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
# Также поддерживает прямую передачу мягких меток (распределений вероятностей)
soft_targets = torch.tensor([
[0.9, 0.05, 0.05],
[0.05, 0.9, 0.05],
])
predictions = torch.randn(2, 3)
loss = criterion(predictions, soft_targets)
Сценарии применения:Все многоклассовые задачи, такие как многоклассовая классификация (кошка/собака/птица), классификация изображений, классификация текста и т. д.
2.2 BCELoss (бинарная кросс-энтропийная потеря)
Специально используется длябинарной классификацииилии многометочной классификациизадач. Входные данные должны бытьSigmoidзначения вероятностей после обработки (0-1).
Математическая формула:
Потеря = -[y * log(p) + (1-y) * log(1-p)]
Пример
# Выход модели должен сначала пройти через Sigmoid, диапазон (0, 1)
raw_output = torch.tensor([2.0, -1.0, 0.5, -3.0])
predictions = torch.sigmoid(raw_output) # [0.88, 0.27, 0.62, 0.05]
# Метки: тип float 0.0 или 1.0
targets = torch.tensor([1.0, 0.0, 1.0, 0.0])
loss = criterion(predictions, targets)
print(f"Потери: {loss.item():.4f}") # Потери: 0.2824
# Многометочная классификация (каждый образец может принадлежать нескольким классам)
# форма predictions: (batch_size, num_labels)
predictions_ml = torch.sigmoid(torch.randn(4, 5))
targets_ml = torch.randint(0, 2, (4, 5)).float()
loss_ml = criterion(predictions_ml, targets_ml)
BCELossВходные данные должны находиться в диапазоне (0, 1). Передача необработанных логитов вызовет численную нестабильность или даже NaN. Рекомендуется использовать улучшенную версию ниже,BCEWithLogitsLoss。
2.3 BCEWithLogitsLoss
BCELossкоторая является улучшенной версией ,и автоматически выполняет Sigmoid внутри, обеспечивая лучшую численную стабильность и рекомендуется в качестве первого выбора.
Пример
# Передавайте сырые логиты напрямую, вручную применять Sigmoid не нужно
predictions = torch.tensor([2.0, -1.0, 0.5, -3.0])
targets = torch.tensor([1.0, 0.0, 1.0, 0.0])
loss = criterion(predictions, targets)
print(f"Loss: {loss.item():.4f}")
# Эквивалентно (но с лучшей численной устойчивостью):
# loss = BCELoss(Sigmoid(predictions), targets)
С весами положительных примеров (обработка дисбаланса классов):
Пример
# Например, если отрицательных примеров в 10 раз больше, чем положительных, установите pos_weight=10
pos_weight = torch.tensor([10.0])
criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
Применимые сценарии:Бинарная классификация (обнаружение спама), многометочная классификация (многометочная разметка статей), обнаружение объектов (определение переднего плана/фона).
2.4 NLLLoss (потеря отрицательного логарифмического правдоподобия)
Требует ручного применения к выходу моделиlog_softmax, который обеспечивает более высокую гибкость.CrossEntropyLoss = LogSoftmax + NLLLoss。
Пример
# Сначала необходимо вручную применить log_softmax
raw_output = torch.randn(4, 3) # (batch, num_classes)
log_probs = torch.log_softmax(raw_output, dim=1)
targets = torch.tensor([0, 2, 1, 0])
loss = criterion(log_probs, targets)
Варианты использования:Когда логарифмические вероятности нужны на промежуточных шагах (например, CTC, Beam Search); в остальных случаях предпочтительнее использовать
CrossEntropyLoss。
3. Функции потерь для задач регрессии
3.1 MSELoss (среднеквадратичная ошибка)
Самая классическая функция потерь для регрессии, котораяочень чувствительна к большим ошибкам(поскольку возведение в квадрат усиливает влияние больших ошибок).
Математическая формула:
MSELoss = (1/N) * sum((y_i - y_hat_i)^2)
Пример
predictions = torch.tensor([2.5, 0.5, 2.0, 8.0])
targets = torch.tensor([3.0, -0.5, 2.0, 7.0])
loss = criterion(predictions, targets)
print(f"MSE Loss: {loss.item():.4f}") # MSE Loss: 0.3750
# Ручная проверка
manual = ((predictions - targets) ** 2).mean()
print(f"Ручное вычисление: {manual.item():.4f}") # 0.3750
Применимые сценарии:Регрессия непрерывных значений, например, прогнозирование цен на жилье и прогнозирование температуры; хорошо работает, когда в данных нет явных выбросов.
3.2 L1Loss (Средняя абсолютная ошибка)
ПараБолее устойчив к выбросампотому что использует абсолютное значение вместо возведения в квадрат, так что большие ошибки не чрезмерно усиливаются.
Математическая формула:
L1Loss = (1/N) * sum(|y_i - y_hat_i|)
Пример
predictions = torch.tensor([2.5, 0.5, 2.0, 8.0])
targets = torch.tensor([3.0, -0.5, 2.0, 7.0])
loss = criterion(predictions, targets)
print(f"Потеря L1: {loss.item():.4f}") # Потеря L1: 0.5000
3.3 SmoothL1Loss (Потеря Хубера)
Сочетает преимущества MSE и L1: использует MSE для малых ошибок (гладкие, стабильные градиенты), а L1 для больших ошибок (устойчивость к выбросам). Это стандартная функция потерь в обнаружении объектов (Faster R-CNN).
Математическая формула:
SmoothL1(x) = 0.5*x^2 if |x| < 1, else |x| - 0.5
Пример
predictions = torch.tensor([2.5, 0.5, 2.0, 8.0])
targets = torch.tensor([3.0, -0.5, 2.0, 7.0])
loss = criterion(predictions, targets)
print(f"Потеря SmoothL1: {loss.item():.4f}")
Сравнение трёх регрессионных потерь:
Пример
import torch.nn as nn
predictions = torch.tensor([0.0, 1.0, 5.0, 10.0]) # Имитация ошибок разной величины
targets = torch.zeros(4)
for name, fn in [("MSELoss", nn.MSELoss(reduction='none')),
("L1Loss", nn.L1Loss(reduction='none')),
("SmoothL1",nn.SmoothL1Loss(reduction='none'))]:
losses = fn(predictions, targets)
print(f"{name:12s}: {[f'{l:.2f}' for l in losses.tolist()]}")
输出示例: MSELoss : ['0.00', '1.00', '25.00', '100.00'] # 大误差被平方放大 L1Loss : ['0.00', '1.00', '5.00', '10.00'] # 线性增长 SmoothL1 : ['0.00', '0.50', '4.50', '9.50'] # 中间值
Применимые сценарии:Задачи регрессии как с малыми ошибками, так и с выбросами, например регрессия ограничивающих рамок в обнаружении объектов и оценке глубины.
4. Продвинутые функции потерь
4.1 HuberLoss
SmoothL1LossОбобщённая версия , позволяющая настраиваемое переключение порогаdelta(по умолчанию 1.0).
Пример
criterion = nn.HuberLoss(delta=1.5)
predictions = torch.randn(10)
targets = torch.randn(10)
loss = criterion(predictions, targets)
4.2 KLDivLoss (KL-дивергенция)
Измеряет разницу между двумя распределениями вероятностей, обычно используется вдистилляции знанийивариационном автоэнкодере (VAE)。
Математическая формула:
KL(P || Q) = sum(P(i) * log(P(i) / Q(i)))
Пример
# input должен содержать логарифмические вероятности, target — обычные вероятности
log_predictions = torch.log_softmax(torch.randn(4, 5), dim=1)
targets = torch.softmax(torch.randn(4, 5), dim=1)
loss = criterion(log_predictions, targets)
print(f"Потеря KL-дивергенции: {loss.item():.4f}")
Типичное использование в дистилляции знаний:
Пример
# Выход модели-учителя
teacher_logits = torch.randn(32, 10)
# Выход модели-ученика
student_logits = torch.randn(32, 10)
soft_labels = torch.softmax(teacher_logits / temperature, dim=1)
soft_preds = torch.log_softmax(student_logits / temperature, dim=1)
distill_loss = nn.KLDivLoss(reduction='batchmean')(soft_preds, soft_labels)
distill_loss *= temperature ** 2 # Восстановить величину градиента
4.3 MarginRankingLoss (Потеря ранжирования)
Определяет относительный порядок двух входов, обычно используется вобучении ранжированиюиобучении сходству。
Пример
# x1 должен быть ближе к цели, чем x2 (y=1 означает x1 > x2)
x1 = torch.tensor([0.8, 0.3, 0.6])
x2 = torch.tensor([0.2, 0.7, 0.5])
y = torch.tensor([1.0, -1.0, 1.0]) # 1: x1>x2, -1: x1<x2
loss = criterion(x1, x2, y)
4.4 TripletMarginLoss (Триплетная потеря)
Используется для метрического обучения, требуя, чтобы расстояние отякоряиположительного (того же класса)было меньше, чем доотрицательного (другого класса)расстояние.
Пример
# Каждый вектор имеет размерность embedding_dim
anchor = torch.randn(32, 128) # Якорный образец
positive = torch.randn(32, 128) # Положительный образец (тот же класс)
negative = torch.randn(32, 128) # Отрицательный образец (другой класс)
loss = criterion(anchor, positive, negative)
# Цель: dist(anchor, positive) + margin < dist(anchor, negative)
Применимые сценарии:Распознавание лиц, поиск изображений, обучение на малых выборках.
4.5 CTCLoss (функция потерь для разметки последовательностей)
Используется длязадач с последовательностями, где длины входа и выхода не совпадают,таких как распознавание речи (акустическая последовательность -> текстовая последовательность), распознавание рукописного текста.
Пример
# log_probs: (T, N, C) T=временные шаги, N=батч, C=число классов
T, N, C = 50, 4, 20
log_probs = torch.log_softmax(torch.randn(T, N, C), dim=2)
targets = torch.randint(1, C, (N * 10,)) # Объединённая целевая последовательность
input_lengths = torch.full((N,), T, dtype=torch.long)
target_lengths = torch.full((N,), 10, dtype=torch.long)
loss = criterion(log_probs, targets, input_lengths, target_lengths)
5. Подробное объяснение параметра reduction
Все функции потерь поддерживаютreductionпараметр, который управляет тем, как агрегируются потери по образцам:
Пример
targets = torch.tensor([1.5, 2.5, 2.0, 5.0])
# Ошибки по образцам: [0.25, 0.25, 1.00, 1.00]
# mean (по умолчанию): среднее по всем образцам
loss_mean = nn.MSELoss(reduction='mean')(predictions, targets)
print(f"mean: {loss_mean.item():.4f}") # 0.6250
# sum: сумма по всем образцам
loss_sum = nn.MSELoss(reduction='sum')(predictions, targets)
print(f"sum: {loss_sum.item():.4f}") # 2.5000
# none: возвращает индивидуальную потерю каждого образца (часто используется для взвешивания)
loss_none = nn.MSELoss(reduction='none')(predictions, targets)
print(f"none: {loss_none.tolist()}") # [0.25, 0.25, 1.0, 1.0]
reduction='none'Практическое применение — взвешивание различных образцов:
Пример
per_sample_loss = nn.MSELoss(reduction='none')(predictions, targets)
weights = torch.tensor([1.0, 1.0, 2.0, 2.0]) # Вручную задать веса
weighted_loss = (per_sample_loss * weights).mean()
6. Веса классов и веса примеров
Веса классов (обработка дисбаланса классов)
Когда некоторые классы имеют очень мало примеров в наборе данных, назначайте классам меньшинства более высокие веса:
Пример
# Вес, обратно пропорциональный частоте
class_counts = torch.tensor([1000.0, 100.0, 50.0])
weights = 1.0 / class_counts
weights = weights / weights.sum() * len(weights) # Нормализация
criterion = nn.CrossEntropyLoss(weight=weights)
Игнорирование определённых меток
В таких задачах, как семантическая сегментация, часто необходимо игнорировать граничные пиксели (помеченные 255):
Пример
criterion = nn.CrossEntropyLoss(ignore_index=255)
# Сценарий семантической сегментации
predictions = torch.randn(2, 21, 256, 256) # (N, C, H, W)
targets = torch.randint(0, 22, (2, 256, 256))
targets[targets == 21] = 255 # Границы помечены как 255
loss = criterion(predictions, targets)
7. Пользовательские функции потерь
Когда встроенные функции потерь не могут удовлетворить требования, их можно настроить двумя способами:
Способ 1: Функциональный (простой)
Пример
import torch.nn.functional as F
def focal_loss(predictions, targets, gamma=2.0, alpha=0.25):
"""
Focal Loss: решает проблему серьёзного дисбаланса положительных и отрицательных примеров при обнаружении объектов
Уменьшает вес легко классифицируемых примеров, позволяя модели сосредоточиться на сложных примерах
"""
ce_loss = F.cross_entropy(predictions, targets, reduction='none')
pt = torch.exp(-ce_loss) # Вероятность правильного предсказания
focal_weight = alpha * (1 - pt) ** gamma # Сложные примеры получают больший вес
return (focal_weight * ce_loss).mean()
# Использовать
predictions = torch.randn(8, 10)
targets = torch.randint(0, 10, (8,))
loss = focal_loss(predictions, targets)
Метод 2: Наследование от nn.Module (рекомендуется)
Пример
import torch.nn as nn
class DiceLoss(nn.Module):
"""
Dice Loss: обычно используется в сегментации изображений, напрямую оптимизирует коэффициент Dice
Более устойчив к дисбалансу классов, чем CrossEntropy (например, сегментация мелких объектов)
"""
def __init__(self, smooth=1.0):
super().__init__()
self.smooth = smooth
def forward(self, predictions, targets):
# predictions: (N, C, H, W) -> вероятности после sigmoid
# targets: (N, C, H, W) -> метки в one-hot кодировке
predictions = torch.sigmoid(predictions)
# Развернуть в (N, -1)
pred_flat = predictions.view(predictions.size(0), -1)
target_flat = targets.view(targets.size(0), -1).float()
intersection = (pred_flat * target_flat).sum(dim=1)
dice = (2.0 * intersection + self.smooth) / (
pred_flat.sum(dim=1) + target_flat.sum(dim=1) + self.smooth
)
return 1 - dice.mean()
class CombinedLoss(nn.Module):
"""
Комбинированная потеря: CrossEntropy + Dice, балансирующая попиксельную классификацию и пересечение областей
Обычная комбинация для сегментации изображений
"""
def __init__(self, ce_weight=0.5, dice_weight=0.5):
super().__init__()
self.ce_weight = ce_weight
self.dice_weight = dice_weight
self.ce = nn.CrossEntropyLoss()
self.dice = DiceLoss()
def forward(self, predictions, targets):
return (self.ce_weight * self.ce(predictions, targets) +
self.dice_weight * self.dice(predictions, targets))
# Использовать
criterion = CombinedLoss(ce_weight=0.4, dice_weight=0.6)
8. Руководство по выбору функции потерь
Выбор по типу задачи
| Тип задачи | Рекомендуемая функция потерь | Примечания |
|---|---|---|
| Многоклассовая классификация | CrossEntropyLoss | Наиболее общий, предпочтительный выбор |
| Многоклассовая классификация (дисбаланс классов) | CrossEntropyLoss(weight=...) | Взвешивать классы меньшинства |
| Многоклассовая классификация (шумные метки) | CrossEntropyLoss(label_smoothing=0.1) | Предотвращает переобучение |
| Бинарная классификация | BCEWithLogitsLoss | Стабильнее, чем BCELoss |
| Многометочная классификация | BCEWithLogitsLoss | Каждая метка оценивается независимо |
| Обнаружение объектов (классификационная голова) | CrossEntropyLoss / Focal Loss | Используйте Focal при дисбалансе положительных и отрицательных примеров |
| Обнаружение объектов (регрессионная голова) | SmoothL1Loss / GIoULoss | Стандартная практика |
| Обычная регрессия | MSELoss | Первый выбор при отсутствии выбросов |
| Регрессия с выбросами | HuberLoss / SmoothL1Loss | Робастная регрессия |
| Сегментация изображений | CrossEntropyLoss + DiceLoss | Совместное использование работает лучше |
| Распознавание речи | CTCLoss | Выравнивание последовательностей |
| Метрическое обучение / распознавание лиц | TripletMarginLoss | Изучение расстояния в пространстве признаков |
| Дистилляция знаний | KLDivLoss | Изучение распределения мягких меток |
Распространённые ошибки и примечания
Пример
output = torch.softmax(model(x), dim=1) # Избыточный softmax
loss = nn.CrossEntropyLoss()(output, targets) # Это будет применено снова внутри
# Правильно: передавайте логиты напрямую
output = model(x) # Сырые логиты
loss = nn.CrossEntropyLoss()(output, targets)
# Неправильно: передача сырых значений без сигмоиды в BCELoss
loss = nn.BCELoss()(model(x), targets) # Может выходить за пределы [0,1], численно нестабильно
# Правильно: используйте BCEWithLogitsLoss
loss = nn.BCEWithLogitsLoss()(model(x), targets)
# Неправильно: несоответствие типа метки (целое vs float)
targets = torch.tensor([1, 0, 1]) # Целочисленный тип
loss = nn.BCEWithLogitsLoss()(preds, targets) # Ошибка!
# Верно: BCEWithLogitsLoss требует метки типа float
targets = torch.tensor([1.0, 0.0, 1.0]) # Тип float
loss = nn.BCEWithLogitsLoss()(preds, targets) # Правильно
# Неверно: loss не извлечён через .item(), из-за чего граф вычислений продолжает накапливаться, а память GPU переполняется
total_loss += loss # loss — это тензор, и он удерживает граф вычислений
# Верно: используйте .item(), чтобы извлечь скаляр
total_loss += loss.item()
Полный пример обучения
Пример
import torch.nn as nn
import torch.optim as optim
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Модель & функция потерь & оптимизатор
model = MyModel().to(device)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(num_epochs):
model.train()
total_loss, correct = 0.0, 0
for inputs, labels in train_loader:
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs) # Сырые логиты
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * inputs.size(0) # .item() извлекает скаляр
correct += (outputs.argmax(1) == labels).sum().item()
avg_loss = total_loss / len(train_loader.dataset)
accuracy = correct / len(train_loader.dataset)
print(f"Эпоха {epoch+1} | Потери: {avg_loss:.4f} | Точность: {accuracy:.4f}")