Фаза 03 · урок 05

Функции потерь

Цель урока: Модель, минимизирующая MSE на задаче классификации, будет уверенно предсказывать 0,5 для всего. Она минимизирует потери. Но остаётся бесполезной.

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Основы глубокого обучения
Чтение
18 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Среднеквадратичная ошибка (MSE)
  5. Функция потерь кросс-энтропии
  6. Почему MSE не подходит для классификации
  7. Сглаживание меток
  8. Контрастивная функция потерь
  9. Фокальная функция потерь
  10. Дерево решений для выбора функции потерь
  11. Ландшафт функции потерь
  12. Соберите сами
  13. Шаг 1: MSE и её градиент
  14. Шаг 2: Бинарная кросс-энтропия
  15. Шаг 3: Категориальная кросс-энтропия с softmax
  16. Шаг 4: Сглаживание меток
  17. Шаг 5: Контрастивная функция потерь (упрощённая InfoNCE)
  18. Шаг 6: MSE против кросс-энтропии в классификации
  19. Использование
  20. Внедрите
  21. Упражнения
  22. Ключевые термины
  23. Дополнительное чтение

Ваша сеть сделала предсказание. Истинные данные говорят обратное. Насколько она ошиблась? Это число и есть потеря. Выберите неверную функцию потерь — и модель будет оптимизировать совсем не то.

Тип: Сборка Языки: 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 — диагностический промпт на случай, когда кривая потерь выглядит неверно

Упражнения

  1. Реализуйте функцию потерь Хьюбера (гладкая L1-функция потерь): это MSE для малых ошибок и MAE для больших. Обучите регрессионную сеть предсказывать y = sin(x) с MSE и с Хьюбером, когда к 5% целевых значений обучения добавлен случайный шум (выбросы). Сравните итоговую ошибку на тесте.

  2. Добавьте фокальную функцию потерь в цикл обучения бинарной классификации. Создайте несбалансированный набор данных (90% класса 0, 10% класса 1). Сравните стандартную BCE и фокальную функцию потерь (gamma=2) по полноте миноритарного класса после 200 эпох.

  3. Реализуйте триплетную функцию потерь с поиском полу-сложных отрицательных примеров. Сгенерируйте двумерные данные эмбеддингов для 5 классов. Для каждого якоря найдите самый сложный отрицательный пример, который всё же находится дальше положительного (полу-сложный). Сравните сходимость со случайным выбором триплетов.

  4. Запустите сравнение MSE и кросс-энтропии, но во время обучения отслеживайте величины градиентов на каждом слое. Постройте график средней нормы градиента по эпохам. Убедитесь, что кросс-энтропия даёт более крупные градиенты на ранних эпохах, когда модель сильнее всего не уверена.

  5. Реализуйте функцию потерь 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 — Оптимизаторы