Фаза 03 · урок 05
Функции потерь
Цель урока: Модель, минимизирующая MSE на задаче классификации, будет уверенно предсказывать 0,5 для всего. Она минимизирует потери. Но остаётся бесполезной.
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Среднеквадратичная ошибка (MSE)
- Функция потерь кросс-энтропии
- Почему MSE не подходит для классификации
- Сглаживание меток
- Контрастивная функция потерь
- Фокальная функция потерь
- Дерево решений для выбора функции потерь
- Ландшафт функции потерь
- Соберите сами
- Шаг 1: MSE и её градиент
- Шаг 2: Бинарная кросс-энтропия
- Шаг 3: Категориальная кросс-энтропия с softmax
- Шаг 4: Сглаживание меток
- Шаг 5: Контрастивная функция потерь (упрощённая InfoNCE)
- Шаг 6: MSE против кросс-энтропии в классификации
- Использование
- Внедрите
- Упражнения
- Ключевые термины
- Дополнительное чтение
Ваша сеть сделала предсказание. Истинные данные говорят обратное. Насколько она ошиблась? Это число и есть потеря. Выберите неверную функцию потерь — и модель будет оптимизировать совсем не то.
Тип: Сборка Языки: Python Предварительные требования: Урок 03.04 (Функции активации) Время: ~75 минут
Цели обучения
- Реализовать с нуля MSE, бинарную кросс-энтропию, категориальную кросс-энтропию и контрастивную функцию потерь (InfoNCE) вместе с их градиентами
- Объяснить, почему MSE не подходит для классификации, продемонстрировав сбойный режим «предсказывать 0,5 для всего»
- Применить сглаживание меток к кросс-энтропии и описать, как оно предотвращает чрезмерно уверенные предсказания
- Выбрать подходящую функцию потерь для регрессии, бинарной классификации, многоклассовой классификации и задач обучения эмбеддингов
Проблема
Модель, минимизирующая MSE на задаче классификации, будет уверенно предсказывать 0,5 для всего. Она минимизирует потери. Но остаётся бесполезной.
Функция потерь — единственное, что ваша модель действительно оптимизирует. Не accuracy. Не F1-мера. Не любая метрика, которую вы показываете руководителю. Оптимизатор берёт градиент функции потерь и корректирует веса, чтобы сделать это число меньше. Если функция потерь не отражает то, что вам важно, модель найдёт математически самый дешёвый способ ей удовлетворить, и почти никогда это не будет тем, чего вы хотели.
Вот конкретный пример. У вас задача бинарной классификации: два класса, распределение 50/50. В качестве потерь вы используете MSE. Модель предсказывает 0,5 для каждого входа. Средняя MSE равна 0,25 — это минимально возможное значение, если ничего фактически не изучать. У модели нет способности различать классы, но технически она минимизировала потери. Переключитесь на кросс-энтропию — и ту же модель придётся подталкивать предсказания к 0 или 1, потому что -log(0.5) = 0.693 — ужасная потеря, тогда как -log(0.99) = 0.01 вознаграждает уверенные правильные предсказания. Выбор функции потерь определяет, будет ли модель учиться или обыгрывать метрику.
Ситуация ещё хуже в самоконтролируемом обучении, где нет даже меток. Контрастивная функция потерь полностью определяет сигнал обучения: что считается похожим, что — разным и насколько сильно модель должна раздвигать их в пространстве. Неверно задайте контрастивную функцию потерь — и ваши эмбеддинги схлопнутся в одну точку: каждый вход отобразится в один и тот же вектор. Технически нулевые потери. Полная бесполезность.
Концепция
Среднеквадратичная ошибка (MSE)
Стандартный вариант для регрессии. Вычислите квадрат разности предсказания и целевого значения, затем усредните по всем примерам.
MSE = (1/n) * sum((y_pred - y_true)^2)
Почему важно возведение в квадрат: оно квадратично штрафует большие ошибки. Ошибка 2 стоит в 4 раза дороже ошибки 1. Ошибка 10 стоит в 100 раз дороже. Поэтому MSE чувствительна к выбросам: один сильно неверный прогноз доминирует в потерях.
Численный пример: если ваша модель предсказывает цены на жильё и ошибается на $10,000 для большинства домов, но на $200,000 — для одного особняка, MSE будет агрессивно пытаться исправить этот единственный особняк, потенциально ухудшая качество на остальных 99 домах.
Градиент MSE по отношению к предсказанию равен:
dMSE/dy_pred = (2/n) * (y_pred - y_true)
Он линейно зависит от ошибки. Большим ошибкам соответствуют большие градиенты. Это преимущество для регрессии (большим ошибкам нужны большие исправления) и недостаток для классификации (уверенные неверные ответы нужно штрафовать экспоненциально, а не линейно).
Функция потерь кросс-энтропии
Функция потерь для классификации. Она основана на теории информации и измеряет расхождение между предсказанным распределением вероятностей и истинным распределением.
Бинарная кросс-энтропия (BCE):
BCE = -(y * log(p) + (1 - y) * log(1 - p))
Здесь y — истинная метка (0 или 1), а p — предсказанная вероятность.
Почему работает -log(p): когда истинная метка равна 1 и вы предсказываете p = 0.99, потеря составляет -log(0.99) = 0.01. Когда вы предсказываете p = 0.01, потеря равна -log(0.01) = 4.6. Эта разница в 460 раз объясняет, почему работает кросс-энтропия. Она жестоко наказывает уверенные неверные предсказания и почти не штрафует уверенные верные.
Градиент рассказывает ту же историю:
dBCE/dp = -(y/p) + (1-y)/(1-p)
Когда y = 1 и p близко к нулю, градиент равен -1/p и стремится к минус бесконечности. Модель получает огромный сигнал исправить ошибку. Когда p близко к 1, градиент мал. Всё уже правильно, исправлять нечего.
Категориальная кросс-энтропия:
Для многоклассовой классификации с целевыми значениями в one-hot-кодировке.
CCE = -sum(y_i * log(p_i))
В потери вносит вклад только истинный класс (поскольку все остальные y_i равны нулю). Если классов 10 и правильному классу назначена вероятность 0,1 (случайное угадывание), потеря равна -log(0.1) = 2.3. Если правильному классу назначена вероятность 0,9, потеря равна -log(0.9) = 0.105. Модель учится концентрировать массу вероятности на правильном ответе.
Почему MSE не подходит для классификации
Градиенты MSE становятся плоскими, когда предсказания близки к 0 или 1 (из-за насыщения сигмоиды). Кросс-энтропия компенсирует это: -log сокращает плоские области сигмоиды, давая сильные градиенты именно там, где они больше всего нужны.
Сглаживание меток
Стандартные one-hot-метки говорят: «это на 100% класс 3 и на 0% всё остальное». Это сильное утверждение. Сглаживание меток делает его мягче:
smooth_label = (1 - alpha) * one_hot + alpha / num_classes
При alpha = 0.1 и 10 классах вместо [0, 0, 1, 0, ...] целевое значение становится [0.01, 0.01, 0.91, 0.01, ...]. Модель нацеливается на 0,91 вместо 1,0.
Почему это работает: модели, пытающейся выдать ровно 1,0 через softmax, нужно отправить логиты в бесконечность. Это вызывает чрезмерную уверенность, ухудшает обобщение и делает модель хрупкой при сдвиге распределения. Сглаживание меток ограничивает целевое значение 0,9 (при alpha=0.1), удерживая логиты в разумном диапазоне. GPT и большинство современных моделей используют сглаживание меток или его эквивалент.
Контрастивная функция потерь
Ни меток. Ни классов. Только пары входов и вопрос: похожи они или различаются?
Контрастивная функция потерь в стиле SimCLR (NT-Xent / InfoNCE):
Возьмите одно изображение. Создайте два его аугментированных представления (обрезка, поворот, цветовой jitter). Это «положительная пара» — их эмбеддинги должны быть похожи. Каждое другое изображение в батче образует «отрицательную пару» — их эмбеддинги должны различаться.
L = -log(exp(sim(z_i, z_j) / tau) / sum(exp(sim(z_i, z_k) / tau)))
Здесь sim() — косинусное сходство, z_i и z_j — положительная пара, сумма берётся по всем отрицательным примерам, а tau (температура) управляет резкостью распределения. Меньшая температура = более сложные отрицательные примеры = более агрессивное разделение.
Численный пример: размер батча 256 означает 255 отрицательных примеров на одну положительную пару. Температура tau = 0.07 (значение по умолчанию SimCLR). Потеря выглядит как softmax по сходствам: она стремится сделать сходство положительной пары наибольшим среди всех 256 вариантов.
Триплетная функция потерь:
Использует три входа: якорь, положительный пример (того же класса), отрицательный пример (другого класса).
L = max(0, d(anchor, positive) - d(anchor, negative) + margin)
Отступ margin (обычно 0,2–1,0) задаёт минимальный разрыв между положительным и отрицательным расстояниями. Если отрицательный пример уже достаточно далеко, потери равны нулю: нет градиента, нет обновления. Это делает обучение эффективным, но требует тщательного подбора триплетов (выбора сложных отрицательных примеров, близких к якорю).
Фокальная функция потерь
Для несбалансированных наборов данных. Стандартная кросс-энтропия одинаково обрабатывает все корректно классифицированные примеры. Фокальная функция потерь уменьшает вес лёгких примеров:
FL = -alpha * (1 - p_t)^gamma * log(p_t)
Здесь p_t — предсказанная вероятность истинного класса, а gamma управляет фокусировкой. При gamma = 0 это стандартная кросс-энтропия. При gamma = 2 (значение по умолчанию):
- Лёгкий пример (
p_t = 0.9): вес =(0.1)^2 = 0.01. Практически игнорируется. - Сложный пример (
p_t = 0.1): вес =(0.9)^2 = 0.81. Полный сигнал градиента.
Фокальная функция потерь была предложена Lin и соавт. для обнаружения объектов, где 99% кандидатов-областей относятся к фону (лёгкие отрицательные примеры). Без неё модель тонет в лёгких фоновых примерах и никогда не учится обнаруживать объекты. С ней модель направляет свои возможности на важные сложные и неоднозначные случаи.
Дерево решений для выбора функции потерь
Ландшафт функции потерь
cross-entropy-loss
Соберите сами
Шаг 1: MSE и её градиент
def mse(predictions, targets):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
total += (p - t) ** 2
return total / n
def mse_gradient(predictions, targets):
n = len(predictions)
grads = []
for p, t in zip(predictions, targets):
grads.append(2.0 * (p - t) / n)
return grads
Шаг 2: Бинарная кросс-энтропия
Проблема log(0) реальна. Если модель предсказывает ровно 0 для положительного примера, log(0) равен минус бесконечности. Отсечение значений предотвращает это.
import math
def binary_cross_entropy(predictions, targets, eps=1e-15):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
total += -(t * math.log(p_clipped) + (1 - t) * math.log(1 - p_clipped))
return total / n
def bce_gradient(predictions, targets, eps=1e-15):
grads = []
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
grads.append(-(t / p_clipped) + (1 - t) / (1 - p_clipped))
return grads
Шаг 3: Категориальная кросс-энтропия с softmax
Softmax преобразует необработанные логиты в вероятности. Затем мы вычисляем кросс-энтропию по one-hot-целям.
def softmax(logits):
max_val = max(logits)
exps = [math.exp(x - max_val) for x in logits]
total = sum(exps)
return [e / total for e in exps]
def categorical_cross_entropy(logits, target_index, eps=1e-15):
probs = softmax(logits)
p = max(eps, probs[target_index])
return -math.log(p)
def cce_gradient(logits, target_index):
probs = softmax(logits)
grads = list(probs)
grads[target_index] -= 1.0
return grads
Градиент сочетания softmax + кросс-энтропия замечательно упрощается: для истинного класса это просто «предсказанная вероятность - 1», а для всех остальных классов — «предсказанная вероятность». Это изящное упрощение не случайно: именно поэтому softmax и кросс-энтропию используют вместе.
Шаг 4: Сглаживание меток
def label_smoothed_cce(logits, target_index, num_classes, alpha=0.1, eps=1e-15):
probs = softmax(logits)
loss = 0.0
for i in range(num_classes):
if i == target_index:
smooth_target = 1.0 - alpha + alpha / num_classes
else:
smooth_target = alpha / num_classes
p = max(eps, probs[i])
loss += -smooth_target * math.log(p)
return loss
Шаг 5: Контрастивная функция потерь (упрощённая InfoNCE)
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a < 1e-10 or norm_b < 1e-10:
return 0.0
return dot / (norm_a * norm_b)
def contrastive_loss(anchor, positive, negatives, temperature=0.07):
sim_pos = cosine_similarity(anchor, positive) / temperature
sim_negs = [cosine_similarity(anchor, neg) / temperature for neg in negatives]
max_sim = max(sim_pos, max(sim_negs)) if sim_negs else sim_pos
exp_pos = math.exp(sim_pos - max_sim)
exp_negs = [math.exp(s - max_sim) for s in sim_negs]
total_exp = exp_pos + sum(exp_negs)
return -math.log(max(1e-15, exp_pos / total_exp))
Шаг 6: MSE против кросс-энтропии в классификации
Обучите одну и ту же сеть из урока 04 (набор данных с кругом) с обеими функциями потерь. Наблюдайте, как кросс-энтропия сходится быстрее.
import random
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def make_circle_data(n=200, seed=42):
random.seed(seed)
data = []
for _ in range(n):
x = random.uniform(-2, 2)
y = random.uniform(-2, 2)
label = 1.0 if x * x + y * y < 1.5 else 0.0
data.append(([x, y], label))
return data
class LossComparisonNetwork:
def __init__(self, loss_type="bce", hidden_size=8, lr=0.1):
random.seed(0)
self.loss_type = loss_type
self.lr = lr
self.hidden_size = hidden_size
self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
self.b1 = [0.0] * hidden_size
self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
self.b2 = 0.0
def forward(self, x):
self.x = x
self.z1 = []
self.h = []
for i in range(self.hidden_size):
z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
self.z1.append(z)
self.h.append(max(0.0, z))
self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
self.out = sigmoid(self.z2)
return self.out
def backward(self, target):
if self.loss_type == "mse":
d_loss = 2.0 * (self.out - target)
else:
eps = 1e-15
p = max(eps, min(1 - eps, self.out))
d_loss = -(target / p) + (1 - target) / (1 - p)
d_sigmoid = self.out * (1 - self.out)
d_out = d_loss * d_sigmoid
for i in range(self.hidden_size):
d_relu = 1.0 if self.z1[i] > 0 else 0.0
d_h = d_out * self.w2[i] * d_relu
self.w2[i] -= self.lr * d_out * self.h[i]
for j in range(2):
self.w1[i][j] -= self.lr * d_h * self.x[j]
self.b1[i] -= self.lr * d_h
self.b2 -= self.lr * d_out
def compute_loss(self, pred, target):
if self.loss_type == "mse":
return (pred - target) ** 2
else:
eps = 1e-15
p = max(eps, min(1 - eps, pred))
return -(target * math.log(p) + (1 - target) * math.log(1 - p))
def train(self, data, epochs=200):
losses = []
for epoch in range(epochs):
total_loss = 0.0
correct = 0
for x, y in data:
pred = self.forward(x)
self.backward(y)
total_loss += self.compute_loss(pred, y)
if (pred >= 0.5) == (y >= 0.5):
correct += 1
avg_loss = total_loss / len(data)
accuracy = correct / len(data) * 100
losses.append((avg_loss, accuracy))
if epoch % 50 == 0 or epoch == epochs - 1:
print(f" Epoch {epoch:3d}: loss={avg_loss:.4f}, accuracy={accuracy:.1f}%")
return losses
Использование
PyTorch предоставляет все стандартные функции потерь с уже встроенной численной стабильностью:
import torch
import torch.nn as nn
import torch.nn.functional as F
predictions = torch.tensor([0.9, 0.1, 0.7], requires_grad=True)
targets = torch.tensor([1.0, 0.0, 1.0])
mse_loss = F.mse_loss(predictions, targets)
bce_loss = F.binary_cross_entropy(predictions, targets)
logits = torch.randn(4, 10)
labels = torch.tensor([3, 7, 1, 9])
ce_loss = F.cross_entropy(logits, labels)
ce_smooth = F.cross_entropy(logits, labels, label_smoothing=0.1)
Используйте F.cross_entropy (а не F.nll_loss вместе с ручным softmax). Она объединяет log-softmax и отрицательное логарифмическое правдоподобие в одну численно устойчивую операцию. Отдельно применять softmax, а затем брать логарифм менее устойчиво: вы теряете точность при вычитании больших экспонент.
Для контрастивного обучения большинство команд используют собственные реализации или библиотеки вроде lightly либо pytorch-metric-learning. Основной цикл всегда одинаков: вычислить попарные сходства, построить softmax по положительным и отрицательным примерам, выполнить обратное распространение.
Внедрите
Этот урок создаёт:
outputs/prompt-loss-function-selector.md— многократно используемый промпт для выбора подходящей функции потерьoutputs/prompt-loss-debugger.md— диагностический промпт на случай, когда кривая потерь выглядит неверно
Упражнения
-
Реализуйте функцию потерь Хьюбера (гладкая L1-функция потерь): это MSE для малых ошибок и MAE для больших. Обучите регрессионную сеть предсказывать
y = sin(x)с MSE и с Хьюбером, когда к 5% целевых значений обучения добавлен случайный шум (выбросы). Сравните итоговую ошибку на тесте. -
Добавьте фокальную функцию потерь в цикл обучения бинарной классификации. Создайте несбалансированный набор данных (90% класса 0, 10% класса 1). Сравните стандартную BCE и фокальную функцию потерь (
gamma=2) по полноте миноритарного класса после 200 эпох. -
Реализуйте триплетную функцию потерь с поиском полу-сложных отрицательных примеров. Сгенерируйте двумерные данные эмбеддингов для 5 классов. Для каждого якоря найдите самый сложный отрицательный пример, который всё же находится дальше положительного (полу-сложный). Сравните сходимость со случайным выбором триплетов.
-
Запустите сравнение MSE и кросс-энтропии, но во время обучения отслеживайте величины градиентов на каждом слое. Постройте график средней нормы градиента по эпохам. Убедитесь, что кросс-энтропия даёт более крупные градиенты на ранних эпохах, когда модель сильнее всего не уверена.
-
Реализуйте функцию потерь KL-дивергенции и убедитесь, что минимизация
KL(true || predicted)даёт те же градиенты, что и кросс-энтропия, когда истинное распределение — one-hot. Затем попробуйте мягкие цели (как при дистилляции знаний), где «истинное» распределение поступает с выхода softmax модели-учителя.
Ключевые термины
| Термин | Как обычно говорят | Что это на самом деле означает |
|---|---|---|
| Функция потерь | «Насколько модель ошибается» | Дифференцируемая функция, отображающая предсказания и цели в скаляр, который минимизирует оптимизатор |
| MSE | «Средняя квадратичная ошибка» | Среднее квадратов разностей между предсказаниями и целями; квадратично штрафует большие ошибки |
| Кросс-энтропия | «Потери для классификации» | Измеряет расхождение между предсказанным распределением вероятностей и истинным распределением с помощью -log(p) |
| Бинарная кросс-энтропия | «BCE» | Кросс-энтропия для двух классов: -(y*log(p) + (1-y)*log(1-p)) |
| Сглаживание меток | «Смягчение целей» | Замена жёстких целей 0/1 мягкими значениями (например, 0,1/0,9), чтобы предотвратить чрезмерную уверенность и улучшить обобщение |
| Контрастивная функция потерь | «Сблизить похожее, раздвинуть разное» | Функция потерь, обучающая представления: похожие пары сближаются, а непохожие отдаляются в пространстве эмбеддингов |
| InfoNCE | «Потери CLIP/SimCLR» | Нормализованная масштабированная температурой кросс-энтропия по оценкам сходства; рассматривает контрастивное обучение как классификацию |
| Фокальная функция потерь | «Решение для несбалансированных данных» | Кросс-энтропия, взвешенная через (1-p_t)^gamma, чтобы уменьшить вклад лёгких примеров и сосредоточиться на сложных |
| Триплетная функция потерь | «Якорь–положительный–отрицательный» | Приближает якорь к положительному примеру сильнее, чем к отрицательному, по крайней мере на величину отступа |
| Температура | «Ручка резкости» | Скалярный делитель логитов/сходств, управляющий остротой итогового распределения; меньше = резче |
Дополнительное чтение
- Lin и соавт., «Focal Loss for Dense Object Detection» (2017) — представила фокальную функцию потерь для работы с экстремальной несбалансированностью классов при обнаружении объектов (RetinaNet)
- Chen и соавт., «A Simple Framework for Contrastive Learning of Visual Representations» (SimCLR, 2020) — описала современный конвейер контрастивного обучения с функцией потерь NT-Xent
- Szegedy и соавт., «Rethinking the Inception Architecture» (2016) — представила сглаживание меток как технику регуляризации, которая теперь стандартна в большинстве крупных моделей
- Hinton и соавт., «Distilling the Knowledge in a Neural Network» (2015) — дистилляция знаний с мягкими целями и KL-дивергенцией, основополагающая работа для сжатия моделей
Источник: Loss Functions 03.04 — Функции активации · Фаза 3 — Основы глубокого обучения · Полный каталог · 03.06 — Оптимизаторы