Градиентный вывод через пользовательскую функцию потерь

Вопрос или проблема

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

ПРИМЕЧАНИЕ: Моя задача – внедрить эту настраиваемую функцию потерь в нашу индивидуальную библиотеку машинного обучения на C++. Но для этого мне нужно “вручную” вычислить градиенты для последнего слоя сети.

Итак, если я запускаю свою сеть:

results = my_network(inputs)

А затем мою функцию потерь:

loss = my_loss_fn(inputs, results, targets)
loss.backward()

Наконец, если я выведу цепочку grad_fn для “results”, я увижу:

цепочка grad_fn "results":
SqueezeBackward1
DivBackward0
SliceBackward
SqueezeBackward1
Col2ImBackward
TransposeBackward0
MulBackward0
FftC2RBackward
ViewAsComplexBackward
TransposeBackward0
ViewBackward
LeakyReluBackward0 <=== Это последний слой my_network
NativeBatchNormBackward
SlowConvTranspose2DBackward

Так что, если я правильно понимаю автограду, мне нужно будет реализовать каждую из этих grad_fn, чтобы получить градиентный вывод для LeakReluLayer?

ИЗМЕНЕНИЕ: Для сети я просто использую пару простых слоев. Она не предназначена для выполнения какой-либо задачи:

class NetTest(nn.Module):
def __init__(self, in_chans, out_chans, kernel, stride, padding):
    super().__init__()

    #self.conv = nn.Conv2d(in_channels=in_chans, out_channels=out_chans, kernel_size=kernel, stride=stride, padding=padding)
    self.trans = nn.ConvTranspose2d(in_channels=in_chans, out_channels=out_chans, kernel_size=kernel, stride=stride, padding=padding, output_padding=0)
    self.batchnorm = nn.BatchNorm2d(num_features=out_chans, eps=0,momentum=.1)
    self.leaky_relu = nn.LeakyReLU()

def forward(self, x):
    #x = self.conv(x)
    x = self.trans(x)
    x = self.batchnorm(x)
    x = self.leaky_relu(x)

    return x

Функция потерь выглядит следующим образом (не уверен, откуда она взялась):

def wsdr_fn(x_, y_pred_, y_true_, eps=1e-8):
    # x_ - это вход сети в формате STFT
    # y_pred_ - это результаты сети. Они уже обработаны через istft
    # y_true_ - это цель в формате STFT
    y_true_ = torch.squeeze(y_true_, 1)
    y_true = torch.istft(y_true_, n_fft=N_FFT, hop_length=HOP_LENGTH, normalized=True)
    x_ = torch.squeeze(x_, 1)
    x = torch.istft(x_, n_fft=N_FFT, hop_length=HOP_LENGTH, normalized=True)
    y_pred = y_pred_.flatten(1)
    y_true = y_true.flatten(1)
    x = x.flatten(1)

    def sdr_fn(true, pred, eps=1e-8):
            num = torch.sum(true * pred, dim=1)
            den = torch.norm(true, p=2, dim=1) * torch.norm(pred, p=2, dim=1)
        return -(num / (den + eps))

    # истинный иEstimated шум
    z_true = x - y_true
    z_pred = x - y_pred

    a1 = torch.sum(y_true**2, dim=1)   
    a = torch.sum(y_true**2, dim=1) / (torch.sum(y_true**2, dim=1) + torch.sum(z_true**2, dim=1) + eps)
    wSDR = a * sdr_fn(y_true, y_pred) + (1 - a) * sdr_fn(z_true, z_pred)

    return torch.mean(wSDR)

Во-первых, довольно нестандартно передавать входные данные в функцию потерь, если только у вас нет настраиваемой функции потерь, которая требует их специально. Это может происходить, например, когда вы пытаетесь сделать распределение выводов соответствующим (или наоборот игнорировать) распределение, основанное на входах, но это не так уж часто. Можете объяснить, что делает ваша функция потерь?

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

Ответ или решение

Понимание градиентов через пользовательскую функцию потерь в PyTorch

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

Структура сети и функция потерь

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

class NetTest(nn.Module):
    def __init__(self, in_chans, out_chans, kernel, stride, padding):
        super().__init__()
        self.trans = nn.ConvTranspose2d(in_channels=in_chans, out_channels=out_chans, kernel_size=kernel, stride=stride, padding=padding)
        self.batchnorm = nn.BatchNorm2d(num_features=out_chans, eps=0, momentum=.1)
        self.leaky_relu = nn.LeakyReLU()

    def forward(self, x):
        x = self.trans(x)
        x = self.batchnorm(x)
        x = self.leaky_relu(x)
        return x

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

Градиенты и grad_fn

Когда вы вызываете loss.backward(), PyTorch автоматически вычисляет градиенты по всему графу вычислений, начиная с выходной переменной loss и проходя назад через все операции, которые были выполнены. Каждый раз, когда вы применяете операции к тензору, PyTorch создает специальный объект, называемый grad_fn, который хранит информацию о том, как был получен этот тензор.

В вашем случае вы наблюдаете цепочку grad_fn для results, где в конечном итоге градиенты будут рассчитываться для слоя LeakyReLU (LeakyReluBackward0).

Вот несколько шагов, которые помогут вам реализовать алгоритм расчета градиентов для слоя LeakyReLU вручную:

  1. Вычислите градиенты для выходных тензоров:
    Используйте производные вашей функции потерь по отношению к выходам сети. Это выражается как ∂L/∂y, где L — это ваша функция потерь, и y — это выходы вашей модели.

  2. Перенесите градиенты назад через слои:
    Вам нужно будет применить правило последовательной производной для каждого слоя. Например, для LeakyReLU, градиент будет равен 0, если вход меньше или равен 0, и 1 (или другой коэффициент, зависящий от параметра) в противном случае.

  3. Композируйте градиенты для свертки и нормализации:
    Как результат градиента, поступающего из LeakyReLU, вы будете вычислять производные и для предыдущих слоев (например, BatchNorm2d, ConvTranspose2d). Не забудьте учитывать правила градиента, в зависимости от операции (например, правила свертки и сложения).

  4. Убедитесь, что вы обрабатываете все аспекты градиента:
    Некоторые операции могут потребовать дополнительных вычислений, таких как продольное сжатие (SqueezeBackward1) или изменение формы (ViewBackward). Вам нужно будет учитывать поведение этих операций при расчете производных.

Заключение

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

Если у вас появятся дополнительные вопросы или вам понадобится более подробная информация по конкретным аспектам, просто дайте знать!

Оцените материал
Добавить комментарий

Капча загружается...