Что выдаёт энкодер
Обычный автокодировщик переводит вход в одну точку и обратно. Реконструирует он хорошо, но генератором не является: возьмите случайную точку латентного пространства — и у декодера нет причин выдать что-то осмысленное.
Вариационный автокодировщик (Kingma and Welling, 2014) упорядочивает латентное пространство. Каждый вход кодируется в гауссиану, а не в точку:
и каждая такая гауссиана подтягивается к одному фиксированному априорному распределению . В коде энкодер заканчивается двумя «головами»: одна для , другая для .
Где обрывается градиент
Обучение максимизирует ELBO, а его слагаемое реконструкции — это матожидание по кодам:
Оцениваем его одним сэмплом на вход: берём , декодируем, сравниваем с . Функция потерь явно зависит от и — сдвинете , и коды сдвинутся вместе с ним.
Но в вычислительном графе дыра. Шаг «вытащить случайное число из » возвращает значение без производной: градиент функции потерь доходит до и останавливается. Декодер учится, а энкодер, задача которого — выдавать и , от слагаемого реконструкции не получает ничего.
Вынести случайность наружу
Решение — перенести случайность во вход графа. Сэмплируем шум, который не имеет отношения к сети, а затем сдвигаем и масштабируем его:
Распределение то же, что и раньше: сдвиг стандартной гауссианы на и масштаб на дают ровно . Модель не изменилась — изменилось только место, где живёт случайность.
Энкодер выдаёт , а не , поэтому перед сдвигом и масштабом нужно из него восстановить. В PyTorch шум даёт torch.randn_like — он сэмплирует той же формы, что и аргумент.
Градиенты теперь проходят
Теперь — обычная дифференцируемая функция и , а играет роль фиксированного входа:
Градиенты слагаемого реконструкции проходят через обратно в энкодер (Kingma and Welling, 2014, §2.4). Autograd сделает это сам — важно, что теперь ему есть что дифференцировать.
Есть и менее заметный выигрыш. Универсальная альтернатива — оценка через градиент логарифма плотности (score-function, REINFORCE) — тоже даёт несмещённые градиенты без репараметризации, но её дисперсия обычно гораздо выше. Репараметризованная оценка использует производную самой функции потерь, а она несёт куда больше информации на один сэмпл.
Почему log σ², а не σ
Почему энкодер предсказывает , а не ? Стандартное отклонение обязано быть положительным, а последний линейный слой сети может выдать любое число. Предсказание логарифма снимает ограничение бесплатно:
Заодно очень маленькие дисперсии остаются в удобном численном масштабе. Частая альтернатива — softplus; сам трюк работает с любой положительной .
KL без сэмплирования
Вторая половина функции потерь, , вообще не требует сэмплирования. Между двумя гауссианами у неё есть замкнутая форма; в сумме по латентным координатам
Она равна нулю ровно тогда, когда и во всех координатах, то есть когда апостериорное распределение совпадает с априорным. В коде это одна строка, и она дифференцируется по и напрямую.
Собираем шаг обучения
Всё вместе — один шаг обучения:
mu, logvar = encoder(x)
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
z = mu + std * eps
x_hat = decoder(z)
recon = ((x - x_hat) ** 2).sum(dim=1)
kl = -0.5 * (1 + logvar - mu**2 - logvar.exp()).sum(dim=1)
loss = (recon + kl).mean()
loss.backward()Квадратичная ошибка соответствует гауссовскому декодеру с фиксированной дисперсией — тогда это отрицательный ELBO с точностью до константы. Другая фиксированная дисперсия меняет вес KL-слагаемого — на этой идее построен β-VAE.