Статья Автор: Деникина Н.В., Деникин А.В.

Autograd и цепное правило

Как модель узнаёт, в какую сторону изменять веса

Мы умеем получать предсказание из входных чисел и параметров. Но пока параметры произвольны, ответ тоже может быть далёк от правильного. Чтобы начать обучение, нужен способ оценить расхождение. Для одного числового ответа возьмём ошибку L = 0.5 · (pred - y)², где pred — предсказание, а y — правильное значение. Квадрат делает ошибку неотрицательной и сильнее выделяет большие отклонения. Множитель 0.5 пока нужен для удобства: при вычислении производной он сократится с двойкой, появившейся от квадрата.

Само значение ошибки сообщает, насколько неудачен текущий ответ, но ещё не подсказывает, какие параметры менять. Для этого нужна производная. Она описывает чувствительность результата к небольшому изменению выбранной величины. Если производная ошибки по весу положительна, небольшое увеличение веса в данной точке ведёт к росту ошибки. Если отрицательна — к уменьшению. Собранные вместе производные по всем параметрам образуют градиент.

Рассмотрим модель pred = w · x + b при x = 2, y = 1, w = 0.2 и b = 0.1. Её предсказание равно 0.5, а ошибка — 0.125. Чтобы понять влияние веса, нужно пройти две зависимости: вес влияет на предсказание, а предсказание влияет на ошибку. По цепному правилу dL/dw = (dL/dpred) · (dpred/dw). Здесь первая производная равна pred - y = -0.5, вторая — x = 2, поэтому dL/dw = -1. Для смещения получаем dL/db = -0.5, поскольку оно входит в предсказание с коэффициентом 1.

Механизм autograd выполняет такую работу автоматически. Когда мы создаём параметры с requires_grad=True, PyTorch отслеживает операции, зависящие от них. Получается граф вычислений: запись того, как одни результаты были получены из других. При вызове loss.backward() библиотека проходит по этим связям в обратную сторону и применяет цепное правило. Это не перебор слегка изменённых весов, а вычисление производных выполненных операций.



Программа подтверждает ручной расчёт: предсказание равно 0.500, ошибка — 0.125, производные по весу и смещению равны -1.000 и -0.500. Они записаны в поля w.grad и b.grad. При этом сами w и b не изменились. Обратный проход только подготовил сведения для изменения параметров. Решение о размере шага и само обновление появятся позже, когда мы подключим оптимизатор.

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

Зачем перед новым шагом очищать градиенты

Есть ещё одна особенность, которую легко принять за ошибку: очередной вызов backward() прибавляет вычисленные производные к уже накопленным в параметрах. Это позволяет при необходимости собирать вклад нескольких порций данных. Однако в обычном учебном цикле нам нужен градиент текущего шага, поэтому прежнее накопление следует очистить. Проверим это на тех же числах, каждый раз заново вычисляя ошибку и создавая новый граф.



После первого прохода производные равны -1 и -0.5, после второго — уже -2 и -1. Это сумма двух одинаковых вкладов; параметры между проходами не менялись. После очистки и нового вычисления снова получаются -1 и -0.5. Здесь мы очистили поля напрямую методом zero_(). В обычной модели этим управляет optimizer.zero_grad(); он может сбрасывать градиенты в None, что также убирает прежнее накопление.

Нужно различать повторное вычисление ошибки и попытку ещё раз пройти по тому же графу. После обычного backward() сохранённые для обратного прохода промежуточные данные обычно освобождаются. Поэтому второй вызов на том же объекте loss без специальных настроек часто приводит к ошибке. Нашему циклу такие настройки не нужны: после изменения весов мы выполняем новый прямой проход и получаем новый граф для актуальных параметров.

Когда производные не нужны

Если модель уже обучена и мы хотим узнать её ответ, строить граф для будущего обратного прохода незачем. Контекст torch.no_grad() отключает такое отслеживание для вычислений внутри него. Формула предсказания остаётся прежней, но память для ненужной цепочки производных не расходуется.



Мы снова получили 0.5, теперь как готовый ответ. Похожая по назначению команда detach() отделяет конкретный тензор от отслеживания производных, например перед выводом промежуточных значений. Метод item() извлекает обычное число для печати или истории ошибки. Такие действия уместны для наблюдения за обучением, но если отделить от графа результат, через который должна передаваться производная, нужная связь потеряется.

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

Проверим понимание

Разделяйте три события: вычисление ответа, вычисление производных и изменение параметров. Пока мы подробно разобрали только первые два.


Какие утверждения верны?

Печать