Содержание курса 06 / 33

06 / 33 · Основы

Автоэнкодеры

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

Что нужно знать

Нейронные сети · Свёртки

На этой странице 9

Из блоков предыдущей заметки — свёрток, батч-нормализации (batch normalization) и LeakyReLU — мы можем построить первую нейросетевую модель курса. Пока она ничего не генерирует. Автоэнкодер обучается воспроизводить собственный вход; задача обучить сеть обратным распространением ошибки пропускать вход через узкий скрытый слой и обратно встречается уже у Rumelhart, Hinton and Williams (1986) как «задача кодирования» (encoding problem). Реконструкция — лишь предлог. На самом деле нам нужно представление (representation): компактный код, который сеть вынуждена построить по пути (Hinton and Salakhutdinov, 2006; Goodfellow, Bengio and Courville, 2016, глава 14). В этой заметке мы обучаем на MNIST обычный, шумоподавляющий и два разреженных (KL и L1) свёрточных автоэнкодера, смотрим, что улавливают их коды и как это измерить, и в конце обсуждаем пробел, который закроет вариационный автоэнкодер.

Обозначения. xx — входное изображение, PP — распределение данных, из которого оно взято. Энкодер fφf_\varphi с параметрами φ\varphi переводит xx в латентный код z=fφ(x)z = f_\varphi(x). Декодер gθg_\theta с параметрами θ\theta переводит код обратно в реконструкцию x^=gθ(z)\hat x = g_\theta(z). Как и в заметке о VAE, φ\varphi относится к энкодеру, а θ\theta — к декодеру. β\beta и λ\lambda — веса штрафов за разреженность, которые появятся ниже; они не связаны с расписанием шума (noise schedule) βt\beta_t из заметок о диффузии.

Архитектура: понижение разрешения, узкое место, повышение разрешения#

Энкодер fφf_\varphi переводит вход xx в латентный код zz, а декодер gθg_\theta переводит код обратно в пространство входов:

z=fφ(x),x^=gθ(z)=gθ(fφ(x)).z = f_\varphi(x), \qquad \hat{x} = g_\theta(z) = g_\theta\big(f_\varphi(x)\big).

Путь данных устроен просто — от входа через узкое место (bottleneck) zz к реконструкции:

x  → понижение   z  → повышение   x^.x \;\xrightarrow{\ \text{понижение}\ }\; z \;\xrightarrow{\ \text{повышение}\ }\; \hat{x}.

Узкое место меньше входа, поэтому сеть не может пропустить пиксели через себя без изменений. Ей приходится сохранить то, что нужно для восстановления входа, и отбросить остальное. В этом и состоит идея латентного пространства: каждый вход становится точкой в пространстве меньшей размерности, которое выучивается по данным.

Разрешение входа: 28×28→64×6428 \times 28 \to 64 \times 64. Цифры MNIST масштабируются до 64×6464 \times 64 (transforms.Resize((64, 64)), затем ToTensor(), который уже даёт значения пикселей в [0,1][0, 1]). Большее изображение позволяет модели понижать разрешение постепенно. Энкодер четыре раза уменьшает пространственный размер вдвое: свёртка с шагом (stride) 2, ядром 3 и дополнением (padding) 1 переводит HH в ⌊(H−1)/2⌋+1\lfloor (H-1)/2 \rfloor + 1 (формула размера выхода). Декодер четыре раза удваивает размер:

СторонаЭнкодер, 4 уменьшения вдвоеДекодер, 4 удвоенияРазмер входа восстановлен?
646464→32→16→8→464 \to 32 \to 16 \to 8 \to 44→8→16→32→644 \to 8 \to 16 \to 32 \to 64да
282828→14→7→4→228 \to 14 \to 7 \to 4 \to 22→4→8→16→322 \to 4 \to 8 \to 16 \to 32нет (32≠2832 \ne 28)

Если сторона делится на 24=162^4 = 16, как 6464, каждый этап уменьшает её ровно вдвое, и зеркальный декодер возвращается к размеру входа. При 28×2828 \times 28 те же четыре этапа оставили бы карту 2×22 \times 2 и выход 32×3232 \times 32.

Рисунок

Свёрточный автоэнкодер слой за слоем

Конвейер автоэнкодера с формами тензоровРяд из одиннадцати тензоров слева направо. Вход x формы 1 на 64 на 64; после свёрток с шагом 2 — 16 на 32 на 32, 32 на 16 на 16, 32 на 8 на 8 и 32 на 4 на 4; после обычной свёртки 3 на 3 — узкое место z формы 32 на 4 на 4, то есть 512 чисел. Затем декодер четыре раза повышает разрешение вдвое и применяет свёртку: 32 на 8 на 8, 32 на 16 на 16, 32 на 32 на 32, 16 на 64 на 64, а последняя свёртка с сигмоидой даёт реконструкцию x с крышкой формы 1 на 64 на 64. Красная стрелка от x с крышкой обратно к x подписана: MSE — сравнение выхода со входом.энкодер fφ: понижение разрешенияобучаемые фильтры, шаг 2декодер gθ: повышение разрешениябилинейное ×2, затем свёрткаs2s2s2s2св.×2×2×2×2вых.64×641 кан.32×3216 кан.16×1632 кан.8×832 кан.4×432 кан.4×432 кан.8×832 кан.16×1632 кан.32×3232 кан.64×6416 кан.64×641 кан.xx̂z: 32×4×4 = 512MSE(x̂, x): сравнение выхода со входомs2: свёртка 3×3, шаг 2 → BN → LeakyReLU(0.2) · св.: свёртка 3×3, шаг 1×2: билинейное ×2 → свёртка 3×3 → BN → LeakyReLU(0.2) · вых.: свёртка 3×3 → сигмоидаКонвейер автоэнкодера с формами тензоровТензоры расположены сверху вниз. Энкодер: x формы 1 на 64 на 64, затем после свёрток с шагом 2 — 16 на 32 на 32, 32 на 16 на 16, 32 на 8 на 8, 32 на 4 на 4, и после свёртки 3 на 3 — код z формы 32 на 4 на 4, 512 чисел. Декодер: повышение разрешения вдвое и свёртка дают 32 на 8 на 8, 32 на 16 на 16, 32 на 32 на 32, 16 на 64 на 64, а последняя свёртка с сигмоидой даёт x с крышкой формы 1 на 64 на 64. Красная стрелка слева идёт от x с крышкой обратно вверх к x и подписана MSE.энкодер fφ: понижение разрешенияобучаемые фильтры, шаг 2декодер gθ:билинейное ×2+ свёрткаs2s2s2s2св.×2×2×2×2вых.1×64×6416×32×3232×16×1632×8×832×4×432×4×432×8×832×16×1632×32×3216×64×641×64×64xx̂z= 512MSE(x̂, x)s2: свёртка 3×3, шаг 2 → BN → LeakyReLU×2: билин. ×2 → свёртка 3×3 → BN → LeakyReLUсв.: свёртка 3×3, шаг 1вых.: свёртка 3×3 → сигмоида
Каждая полоса — один тензор; её длина растёт с пространственным размером (логарифмическая шкала, без соблюдения масштаба), а толщина — с числом каналов. Четыре блока с шагом 2 сжимают вход 64×64 до 32×4×4 = 512 чисел, последняя свёртка 3×3 даёт код z, четыре блока «повышение разрешения + свёртка» возвращают размер 64×64, а выходная свёртка с сигмоидой даёт x̂. Функция потерь сравнивает выход со входом.

Модель. Каждый этап энкодера — блок из свёртки 3×33\times 3 с шагом 2, батч-нормализации и LeakyReLU с наклоном 0.20.2 (все они объяснены в предыдущей заметке). Последняя свёртка 3×33 \times 3 с шагом 1 выдаёт код. Каждый этап декодера повышает разрешение в 2 раза билинейной интерполяцией, а затем применяет свёртку 3×33\times 3, батч-нормализацию и LeakyReLU. Финальная свёртка переводит 16 каналов в 1, за ней следует сигмоида.

ТензорФорма (каналы × высота × ширина)Чисел
вход xx1×64×641 \times 64 \times 6440964096
после блока энкодера 116×32×3216 \times 32 \times 321638416384
после блока энкодера 232×16×1632 \times 16 \times 1681928192
после блока энкодера 332×8×832 \times 8 \times 820482048
после блока энкодера 4 и последней свёртки: код zz32×4×432 \times 4 \times 4512512
после блока декодера 132×8×832 \times 8 \times 820482048
после блока декодера 232×16×1632 \times 16 \times 1681928192
после блока декодера 332×32×3232 \times 32 \times 323276832768
после блока декодера 416×64×6416 \times 64 \times 646553665536
выход x^\hat x1×64×641 \times 64 \times 6440964096

Размер латентного кода. Узкое место имеет форму 32×4×432 \times 4 \times 4, то есть содержит 512512 чисел: развёрнутый в вектор латентный код 512-мерный. Это в 4096/512=84096 / 512 = 8 раз меньше чисел, чем во входе после масштабирования, но лишь примерно в 1.51.5 раза меньше, чем исходных пикселей 28×28=78428 \times 28 = 784. Сжатие умеренное: важна структура кода, а не его размер.

Как читать энкодер и декодер#

  • Энкодер работает как обучаемый фильтр, а не как фиксированное сжатие. Каждый шаг понижения разрешения — обучаемая свёртка 3×33\times3 с шагом 2. Фильтры извлекают признаки, а шаг снижает разрешение, так что сеть сама решает, что сохранится при каждом уменьшении вдвое; фиксированное масштабирование на это не способно. Классическая альтернатива — свёртка с шагом 1, за которой следует пулинг (pooling), уменьшающий размер вдвое по фиксированному правилу: max-пулинг (max pooling) оставляет наибольшее значение каждого окна 2×22 \times 2, усредняющий пулинг (average pooling) — их среднее. В классификаторах изображений замена max-пулинга свёрткой с шагом 2 не снизила долю правильных ответов (accuracy) в экспериментах Springenberg et al. (2015), а понижение разрешения при этом тоже становится обучаемым.
  • Декодер не сводится к увеличению изображения. Он работает по схеме повышение разрешения + свёртка. Билинейное повышение разрешения — фиксированная операция без параметров; детали восполняет обучаемая свёртка после него.
  • Сжатие возникает из архитектуры, а не из явного требования. Ни одному слою не предписано сжимать. Первый блок энкодера даже расширяет 40964096 входных значений до 1638416384 активаций. Код становится маленьким только потому, что формы тензоров сужаются к узкому месту.
  • Слои выучивают признаки, а не пиксели. Ранние слои реагируют на локальные паттерны, например границы и фрагменты штрихов. Более глубокие слои через своё рецептивное поле видят бо́льшую часть изображения и комбинируют эти паттерны в более крупные части цифры, поэтому каналы кодируют признаки, а не значения отдельных пикселей.

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

Функция потерь сравнивает выход со входом. Стандартный выбор — среднеквадратичная ошибка (mean squared error, MSE) по всем nn скалярным компонентам,

MSE(y^,y)=1n∑i=1n(yi−y^i)2,\mathrm{MSE}(\hat y, y) = \frac{1}{n}\sum_{i=1}^{n} \left(y_i - \hat{y}_i\right)^2,

усреднённая по данным:

Lrecon(φ,θ)=Ex∼P[MSE(gθ(fφ(x)), x)].\mathcal L_{\text{recon}}(\varphi, \theta) = \mathbb E_{x \sim P}\Big[\mathrm{MSE}\big(g_\theta(f_\varphi(x)),\ x\big)\Big].

Для автоэнкодера y=xy = x и y^=x^\hat y = \hat x. nn.MSELoss() в PyTorch усредняет по всем пикселям батча: n=64⋅64⋅64=262144n = 64 \cdot 64 \cdot 64 = 262144 для батча из 64 изображений.

MSE или BCE. Значения пикселей лежат в [0,1][0, 1], поэтому естественная альтернатива — бинарная кросс-энтропия (binary cross-entropy, BCE):

BCE(x^,x)=−1n∑i=1n[xilog⁡x^i+(1−xi)log⁡(1−x^i)],\begin{aligned} \mathrm{BCE}(\hat x, x) &= -\frac{1}{n}\sum_{i=1}^{n}\Big[x_i \log \hat x_i \\ &\qquad\quad + (1 - x_i)\log(1 - \hat x_i)\Big], \end{aligned}

усреднённая по пикселям, как это по умолчанию делает nn.BCELoss. Она трактует каждый пиксель как вероятность того, что он «включён», и это подходит для почти бинарных цифр MNIST.

Двум функциям потерь соответствуют два правдоподобия декодера. Для гауссовского декодера с фиксированной дисперсией s2s^2 на DD пикселях −log⁡N(x; x^, s2I)=12s2∥x−x^∥2+D2log⁡(2πs2)-\log \mathcal N(x;\, \hat x,\, s^2 I) = \frac{1}{2s^2}\|x - \hat x\|^2 + \frac D2 \log(2\pi s^2), поэтому MSE совпадает с его отрицательным логарифмом правдоподобия с точностью до положительного множителя и аддитивной константы. BCE — отрицательный логарифм правдоподобия декодера, в котором каждый пиксель — независимая бернуллиевская величина со средним x^i\hat x_i. Строго говоря, это верно только для бинарных пикселей. В масштабированных изображениях MNIST много серых значений; для них BCE по-прежнему минимальна при x^i=xi\hat x_i = x_i, но уже не является отрицательным логарифмом нормированной плотности на [0,1][0, 1] (Loaiza-Ganem and Cunningham, 2019). Заметка о VAE опирается на это прочтение функции потерь как правдоподобия.

Поскольку сигмоида уже применяется в forward, для BCE нужна nn.BCELoss; nn.BCEWithLogitsLoss ожидает «сырые» логиты и сама применяет сигмоиду, поэтому в сочетании с сигмоидой на выходе сигмоида применилась бы дважды. Численно более устойчивый вариант — убрать сигмоиду и применять nn.BCEWithLogitsLoss к логитам.

Диапазон входа должен подходить и к выходной активации, и к функции потерь. MNIST после ToTensor() уже лежит в [0,1][0, 1], поэтому дополнительная нормализация здесь не нужна; для других данных она обычно нужна. Для цветных изображений, например изображений CIFAR-10 размера 3×32×323 \times 32 \times 32, часто переводят [0,1][0, 1] в [−1,1][-1, 1] отображением x↦2x−1x \mapsto 2x - 1, которое переводит входы в диапазон, симметричный относительно нуля, а для показа переводят выходы обратно отображением x↦(x+1)/2x \mapsto (x + 1)/2. Тогда декодер должен уметь выдавать отрицательные значения (см. выходную активацию ниже), и BCE больше не подходит, поскольку её целевые значения должны лежать в [0,1][0, 1]; MSE подходит.

Возникающая структура#

Ничто в функции потерь не требует упорядоченного латентного пространства, а метки при обучении не используются. Тем не менее структура возникает. Если спроецировать 512-мерные коды тестовой выборки на плоскость с помощью t-SNE (van der Maaten and Hinton, 2008) и раскрасить точки по цифрам, видны кластеры по цифрам, разделённые чище, чем до обучения. Большинство цифр образует по одному компактному кластеру; двойки распадаются на две группы, а единицы образуют вытянутую полосу. Коды упорядочены по содержанию, а не по значениям отдельных пикселей.

t-SNE изображает окрестности, а не расстояния. Метод располагает точки так, чтобы ближайшие соседи каждого кода в 512-мерном пространстве оставались рядом с ним и на плоскости; перплексия, здесь 30, примерно задаёт, сколько соседей учитывается. Размеры кластеров на графике и промежутки между ними несут мало информации (Wattenberg, Viégas and Johnson, 2016). График призван показать, являются ли соседи кода кодами той же цифры, и даже это лишь качественное свидетельство: вложение может разрывать или склеивать окрестности (t-SNE и UMAP). Проверять это нужно в исходном 512-мерном пространстве, например по доле kk ближайших соседей кода с той же меткой или зондирующим классификатором ниже.

Измерение кода классификатором. График даёт лишь качественную картину. Число даёт зондирующий классификатор (probe): замораживаем энкодер, вычисляем коды размеченного набора, обучаем на них простой классификатор и сообщаем его долю правильных ответов на отложенных (held-out) изображениях (Alain and Bengio, 2016; они используют линейные классификаторы на слоях сети). Классификатор видит только код, поэтому его доля правильных ответов измеряет, сколько информации о классе код сохраняет в легко извлекаемом виде. Случайные свёрточные признаки уже информативны, поэтому точка отсчёта — не случайное угадывание, а тот же зондирующий классификатор на кодах необученного энкодера той же архитектуры. Обучение улучшило представление, только если обученный энкодер заметно превосходит эту точку отсчёта. Те же коды могут также заменить признаки Inception в FID, когда изображения не похожи на ImageNet.

Два эксперимента исследуют латентное пространство напрямую:

  • Интерполяция. Кодируем «7» и «2» и декодируем zα=(1−α)z1+αz2z_\alpha = (1 - \alpha) z_1 + \alpha z_2 для 8 кадров, α=0,17,…,1\alpha = 0, \tfrac17, \dots, 1. Крайние кадры — чёткие цифры, но средние выглядят как наложение двух начертаний, а не как цифры, плавно перетекающие одна в другую.
  • Сэмплирование. Для каждого класса цифр подгоняем к кодам обучающих изображений гауссиану с осями вдоль координат (диагональную): берём среднее и стандартное отклонение zz по каждой координате, так что 512 координат сэмплируются независимо. Берём по одному коду на класс и декодируем его. В сохранённом запуске шесть из десяти сэмплов — узнаваемые цифры, а 2, 4, 5 и 8 сильно искажены. Для каждого класса взят лишь один сэмпл.

Оба эксперимента показывают, почему обычный автоэнкодер выучивает представления, но ещё не является генеративной моделью. Ничто в обучении не заставляет произвольную точку латентного пространства декодироваться в правдоподобное изображение, а коды не следуют никакому распределению, из которого мы умеем сэмплировать: диагональная гауссиана игнорирует корреляции между координатами, сэмплирование по классам потребовало меток и статистик по каждому классу, а области между кластерами вообще не обучались во что-либо декодироваться. Вариационные автоэнкодеры закрывают именно этот пробел: они делают распределение кодов частью целевой функции обучения.

Варианты#

Шумоподавляющий автоэнкодер#

Шумоподавляющий автоэнкодер (denoising autoencoder; Vincent et al., 2008) меняет задачу, а не архитектуру:

  1. Испортить вход случайным искажением (corruption) q(x~∣x)q(\tilde x \mid x). В исходной статье случайная доля компонент входа обнуляется; другой распространённый выбор — аддитивный гауссовский шум (Vincent et al., 2010).
  2. Восстановить чистый вход по испорченному, x^=gθ(fφ(x~))\hat x = g_\theta(f_\varphi(\tilde x)), и вычислить функцию потерь относительно xx, а не x~\tilde x.
  3. В результате получается устойчивое представление: его признаки остаются стабильными, когда часть входа испорчена.

Сеть не может добиться успеха, выучив тождественное отображение, поэтому ей приходится выучить, какая структура в данных — сигнал, а какая — шум. Позже в курсе эта идея станет центральной. При гауссовском искажении обучение денойзера (модели шумоподавления) эквивалентно score matching — сопоставлению градиентов логарифма плотности — для сглаженной версии плотности данных (Vincent, 2011), а целевая функция предсказания шума диффузионной модели обучает денойзер на многих уровнях шума.

Рисунок

Шумоподавляющий автоэнкодер: искажаем вход, сравниваем с чистым изображением

Шаг обучения шумоподавляющего автоэнкодераСлева направо: чистое изображение цифры x; стрелка с подписью «добавить шум», x с тильдой равно x плюс 0.2 эпсилон, где эпсилон — стандартный нормальный шум, ведёт к зашумлённому изображению x с тильдой в крапинку; стрелка входит в автоэнкодер в форме песочных часов — энкодер f фи, код z, декодер g тета — с дропаутом в каждом блоке; стрелка выходит к гладкой реконструкции x с крышкой. Красная стрелка внизу идёт от x с крышкой обратно к чистому x и подписана: MSE от x с крышкой и x, цель — чистое изображение, а не x с тильдой.xчистая цифраx̃искажённый входx̂реконструкциядобавить шумx̃ = x + 0.2·εε ~ N(0, I)fφzgθDropout2d(0.3) после каждого блокапотери = MSE(x̂, x): цель — чистый x, а не x̃Шаг обучения шумоподавляющего автоэнкодераСверху вниз: чистая цифра x; стрелка с подписью «добавить шум», x с тильдой равно x плюс 0.2 эпсилон; зашумлённое изображение x с тильдой; автоэнкодер в форме песочных часов f фи, z, g тета с дропаутом в каждом блоке; реконструкция x с крышкой. Красная стрелка слева идёт от x с крышкой обратно вверх к чистому x и подписана: MSE от x с крышкой и x.xчистоеx̃искажённоеx̂реконструкциядобавить шумx̃ = x + 0.2·ε, ε ~ N(0, I)fφzgθDropout2d(0.3)потери = MSE(x̂, x) относительно чистого x
При обучении сеть видит только искажённый вход, а функция потерь сравнивает её выход с чистым изображением, поэтому копирование входа штрафуется. Пиксельные изображения — программно построенная игрушечная цифра с уровнем шума из ноутбука (σ = 0.2, без обрезки значений); кроме того, в шумоподавляющей модели ноутбука во время обучения после каждого блока применяется Dropout2d(0.3).

Здесь искажение — аддитивный гауссовский шум,

x~=x+0.2 ε,ε∼N(0,I),\tilde x = x + 0.2\,\varepsilon, \qquad \varepsilon \sim \mathcal N(0, I),

без обрезки значений, так что искажённые пиксели могут выходить за пределы [0,1][0, 1]. Кроме того, каждый блок энкодера и декодера применяет Dropout2d(0.3), который во время обучения случайно отключает целые карты признаков: это второй, внутренний вид искажения. Функция потерь — та же MSE относительно чистого изображения. После 5 эпох функция потерь шумоподавляющей модели на обучении равна 0.01020.0102 против 0.00210.0021 у обычного автоэнкодера. Эти числа нельзя сравнивать напрямую: шумоподавляющая модель решает более трудную задачу — с зашумлённым входом и включённым дропаутом (dropout).

На тестовых цифрах, искажённых тем же способом, обученная модель полностью убирает шум: фон получается равномерно чёрным. Штрихи выходят немного толще и глаже, чем в оригинале, а мелкие детали, например короткий крючок вверху нуля, теряются. Коды ведут себя так же, как у обычной модели. До обучения большинство цифр уже образует кластеры, а 4 и 9 перекрываются; после обучения кластеров десять, и снова двойки распадаются на две группы, а единицы образуют вытянутую полосу. Сэмплирование по классам, как выше, даёт в основном узнаваемые, но размытые цифры (по одному сэмплу на класс); 4 и 5 сильно искажены.

Разреженный автоэнкодер#

Разреженный автоэнкодер (sparse autoencoder) сохраняет функцию потерь реконструкции и добавляет штраф за разреженность (sparsity penalty), который поощряет редкие активации: каждый скрытый нейрон должен быть активен лишь на небольшой доле входов (Ng, 2011). Вместе с функцией потерь реконструкции это должно приводить к тому, что каждый вход задействует лишь несколько сильно активных нейронов. Тогда скрытый слой может быть большим, даже больше входа, и сеть при этом не сможет просто скопировать вход.

Распространены два варианта.

KL-штраф на промежуточных слоях. Скрытые нейроны используют сигмоидные активации после батч-нормализации, поэтому каждая активация лежит в (0,1)(0, 1) и её можно читать как частоту срабатывания (firing rate). Для каждого нейрона jj обозначим через ρ^j\hat\rho_j его среднюю активацию по батчу; Ng (2011) усредняет по всей обучающей выборке, и среднее по батчу служит оценкой этого среднего. Штраф сравнивает ρ^j\hat\rho_j с целевой частотой ρ\rho через KL-дивергенцию (KL divergence) между двумя распределениями Бернулли:

L=MSE(x^,x)+β∑jDKL(ρ ∥ ρ^j),\mathcal L = \mathrm{MSE}(\hat x, x) + \beta \sum_{j} D_{\mathrm{KL}}(\rho \,\|\, \hat\rho_j), DKL(ρ ∥ ρ^j)=ρlog⁡ρρ^j+(1−ρ)log⁡1−ρ1−ρ^j,D_{\mathrm{KL}}(\rho \,\|\, \hat\rho_j) = \rho \log\frac{\rho}{\hat\rho_j} + (1-\rho)\log\frac{1-\rho}{1-\hat\rho_j},

здесь ρ=0.05\rho = 0.05 и β=0.001\beta = 0.001.

Рисунок

Плотный и разреженный коды: к чему стремится штраф за разреженность

Плотные и разреженные активацииДве столбчатые диаграммы активаций 16 нейронов от 0 до 1. В плотном коде почти каждый столбец около 0.5. В разреженном коде один столбец высокий, около 0.7, а все остальные близки к нулю. Подпись внизу: KL-штраф равен бета, умноженному на сумму по нейронам KL между ро и средней активацией ро с крышкой j.плотный кодкаждый нейрон частично активен, около 0.500.51скрытые нейроны j = 1 … 16разреженный кодна этом входе активен 1 нейрон из 1600.51скрытые нейроны j = 1 … 16штраф: β · Σj KL(ρ ‖ ρ̂j), ρ̂j = среднее нейрона j по батчу; ρ = цельПлотные и разреженные активацииДве столбчатые диаграммы активаций 16 нейронов от 0 до 1. В плотном коде почти каждый столбец около 0.5. В разреженном коде один столбец высокий, около 0.7, а все остальные близки к нулю. Подпись внизу: KL-штраф равен бета, умноженному на сумму по нейронам KL между ро и средней активацией ро с крышкой j.плотный кодкаждый нейрон частично активен, около 0.500.51скрытые нейроны j = 1 … 16разреженный кодна этом входе активен 1 нейрон из 1600.51скрытые нейроны j = 1 … 16штраф: β · Σj KL(ρ ‖ ρ̂j)ρ̂j = среднее нейрона j по батчу, ρ = цель
Иллюстративные активации 16 сигмоидных нейронов для одного входа. При инициализации сигмоиды от выходов BatchNorm в среднем близки к 0.5, как в плотном коде. KL-штраф подталкивает среднюю по батчу частоту срабатывания каждого нейрона к малой целевой частоте ρ (0.05 в ноутбуке); ρ — цель не для отдельного столбца. Вместе с функцией потерь реконструкции это должно приводить к тому, что каждый вход задействует лишь несколько сильно активных нейронов, как в разреженном коде. Одному штрафу удовлетворило бы и слабое срабатывание каждого нейрона на уровне около 0.05 на каждом входе.

Это KL-дивергенция между монетой, которая «срабатывает с вероятностью ρ\rho», и монетой, которая «срабатывает с вероятностью ρ^j\hat\rho_j». Она равна нулю, только когда средняя частота срабатывания равна ρ\rho. Штраф ограничивает лишь среднюю частоту каждого нейрона: нейрон, который на каждом входе слабо срабатывает на уровне 0.050.05, тоже ему удовлетворяет. Разреженность для каждого отдельного входа должна возникнуть из взаимодействия с функцией потерь реконструкции. Сумма берётся по всем каналам и позициям четырёх промежуточных слоёв энкодера: 16⋅322+32⋅162+32⋅82+32⋅42=2713616\cdot 32^2 + 32 \cdot 16^2 + 32 \cdot 8^2 + 32 \cdot 4^2 = 27136 слагаемых. Декодер этой модели тоже отличается от остальных: вместо повышения разрешения со свёрткой в нём транспонированные свёртки (transposed convolution; ядро 4, шаг 2, дополнение 1). При расширении (dilation) 1 и без выходного дополнения (output padding) формула размера выхода ConvTranspose2d даёт удвоенную сторону:

Hout=(H−1)⋅2−2⋅1+4=2H.H_{\text{out}} = (H - 1)\cdot 2 - 2\cdot 1 + 4 = 2H.

L1-штраф на код. Это обычная архитектура плюс штраф с весом λ=0.001\lambda = 0.001 на среднее абсолютное значение 512 элементов кода, которое тоже усредняется по батчу:

L=MSE(x^,x)+λ⋅1512∑k=1512∣zk∣.\mathcal L = \mathrm{MSE}(\hat x, x) + \lambda \cdot \frac{1}{512}\sum_{k=1}^{512} |z_k|.

(Суб)градиент ∣zk∣|z_k| равен sign⁡(zk)\operatorname{sign}(z_k) и имеет постоянную величину, поэтому L1-штраф продолжает тянуть малые элементы к нулю, тогда как притяжение квадратичного штрафа вблизи нуля исчезает. Вследствие этого в точке минимума целевой функции с L1-штрафом часть элементов обычно в точности равна нулю: минимизация применяет к ним мягкое пороговое отсечение (soft thresholding) (Goodfellow, Bengio and Courville, 2016, раздел 7.1.2). Обычное градиентное обучение с Adam, как здесь, лишь приближает такие элементы к нулю, и они продолжают колебаться около него.

Ни один из вариантов здесь не работает хорошо, и причины поучительны. Если средние по батчу у скрытых сигмоид в начале близки к 0.50.5 — а батч-нормализация при инициализации центрирует их входы, — KL-слагаемое начинается примерно с 27136⋅0.001⋅DKL(0.05 ∥ 0.5)≈1327136 \cdot 0.001 \cdot D_{\mathrm{KL}}(0.05\,\|\,0.5) \approx 13. Это оценка по порядку величины, поскольку средние отдельных нейронов различаются; так как DKL(ρ ∥ ⋅)D_{\mathrm{KL}}(\rho\,\|\,\cdot) выпукла, разброс ρ^j\hat\rho_j вокруг среднего значения 0.50.5 может её только увеличить (неравенство Йенсена).

Записывалась только сумма двух слагаемых. Она падает с 8.858.85 в первую эпоху до 0.2140.214 в пятую и всё ещё быстро снижается (0.7880.788 в четвёртую эпоху), так что обучение не сошлось. Полностью чёрный выход дал бы MSE, равную среднему квадрату значения пикселя, — около 0.100.10 для масштабированных изображений MNIST (это значение вычислено по тестовой выборке MNIST с тем же билинейным масштабированием). Сохранённые реконструкции KL-модели хуже, чем у обычной модели, но далеко не пустые, и это наводит на мысль, что значительная часть 0.2140.214 приходится на штраф. Разделения это не ограничивает: чёрный выход — лишь одно допустимое базовое решение, а не верхняя граница ошибки реконструкции несошедшейся модели, которая может быть и больше. Вопрос решила бы раздельная запись слагаемого реконструкции и штрафа. Масштаб штрафа указывает в ту же сторону: при 27136 слагаемых вес β=0.001\beta = 0.001 слишком велик. Реконструкции KL-модели заметно хуже, а её график t-SNE раздроблен. Сэмплирование по классам из её кодов полностью проваливается: ни один из десяти декодированных сэмплов не является узнаваемой цифрой, это лишь обрывки штрихов.

У L1-варианта противоположная проблема. При λ=0.001\lambda = 0.001 штраф заметно не меняет обучение: функция потерь в конце равна 0.00220.0022, практически как 0.00210.0021 у обычной модели, и при одном запуске каждой модели эта разница в пределах шума. Всё остальное похоже на обычную модель: десять кластеров на графике t-SNE и реконструкции так же точны, как у обычной модели; сэмплы по классам несколько хуже: 0, 1, 6 и 7 чёткие, остальные цифры искажены. Кроме того, штраф можно обойти. Первый блок декодера применяет билинейное повышение разрешения и свёртку без смещения (обе операции линейны), а затем батч-нормализацию. Замена zz на rzr z при любом r>0r > 0 умножает выходы свёртки на rr, и батч-нормализация снова делит на этот множитель, пока r2σB2r^2\sigma_B^2 остаётся намного больше её ε\varepsilon (батч-нормализация не замечает масштаба своего входа). Поэтому энкодер мог бы снижать штраф, уменьшая общий масштаб zz и не делая код разреженным. Масштаб zz не измерялся, так что это возможный обход, а не наблюдаемый; его показало бы сравнение среднего ∣zk∣\lvert z_k\rvert до и после обучения. Вес штрафов за разреженность нужно подбирать с учётом функции потерь реконструкции, а архитектура не должна позволять их обходить.

Транспонированная свёртка или повышение разрешения + свёртка?#

Оба способа удваивают пространственный размер. Транспонированная свёртка выучивает собственный фильтр повышения разрешения, но когда размер ядра не делится на шаг, отпечатки ядра перекрываются неравномерно и дают шахматные артефакты (checkerboard artifacts). Масштабирование с последующей свёрткой избегает неравномерного перекрытия и обычно считается более надёжным выбором по умолчанию (Odena, Dumoulin and Olah, 2016). То же исследование добавляет две оговорки. Лучшие результаты в нём получены с масштабированием методом ближайшего соседа, а билинейное масштабирование, которое использует здесь обычная модель, авторам было трудно заставить работать. Ядро, размер которого делится на шаг, как ядро 4 с шагом 2 в KL-модели, устраняет неравномерное перекрытие, но обученные ядра всё равно могут давать артефакты. Два декодера здесь не сравниваются в равных условиях, так что выбор между ними — архитектурное решение, а не измеренный результат.

Зачем сигмоида на выходе?#

Свёрточные блоки используют LeakyReLU с наклоном 0.20.2, max⁡(0.2x, x)\max(0.2x,\ x) (Maas, Hannun and Ng, 2013). Почему такие ненасыщающиеся активации заменяют сигмоиды внутри глубоких сетей, объяснено в предыдущей заметке. У сигмоиды здесь другая задача. Она сжимает любое действительное число в (0,1)(0, 1) — диапазон интенсивностей пикселей, — то есть переводит неограниченный выход последней свёртки декодера в шкалу целевых значений. При x=0x = 0 она проходит через 0.50.5:

σ(x)=11+e−x,σ(0)=12.\sigma(x) = \frac{1}{1 + e^{-x}}, \qquad \sigma(0) = \tfrac12 .
Рисунок

LeakyReLU и сигмоида

Функции активации LeakyReLU и сигмоидаДва графика. Слева или сверху: LeakyReLU — прямая с наклоном 1 при положительных x и прямая с наклоном 0.2 при отрицательных x, которые встречаются в начале координат. Справа или снизу: сигмоида — S-образная кривая, которая растёт от 0 до 1 и проходит через 0.5 при x, равном 0, с пунктирными асимптотами на уровнях 0 и 1.LeakyReLU(x) = max(0.2x, x)x12наклон 1наклон 0.2σ(x) = 1 / (1 + e⁻ˣ)x100.5 при x = 0→ 1 при x → ∞→ 0Функции активации LeakyReLU и сигмоидаДва графика. Слева или сверху: LeakyReLU — прямая с наклоном 1 при положительных x и прямая с наклоном 0.2 при отрицательных x, которые встречаются в начале координат. Справа или снизу: сигмоида — S-образная кривая, которая растёт от 0 до 1 и проходит через 0.5 при x, равном 0, с пунктирными асимптотами на уровнях 0 и 1.LeakyReLU(x) = max(0.2x, x)x12наклон 1наклон 0.2σ(x) = 1 / (1 + e⁻ˣ)x100.5 при x = 0→ 1 при x → ∞→ 0
LeakyReLU с наклоном 0.2 — активация свёрточных блоков. На положительных входах это тождественная функция, а на отрицательных она сохраняет наклон 0.2 вместо нулевого наклона ReLU. Сигмоида сжимает любой вход в (0, 1) — диапазон интенсивностей пикселей, поэтому каждая модель в этой заметке ставит её на выход декодера; при x = 0 она равна 0.5.

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

Для целевых значений, масштабированных в [−1,1][-1, 1], у сигмоиды неподходящий диапазон: отрицательного значения она не выдаст никогда. Тогда на выходе обычно ставят tanh⁡\tanh — сигмоиду, растянутую на (−1,1)(-1, 1):

tanh⁡(x)=ex−e−xex+e−x=2 σ(2x)−1.\tanh(x) = \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}} = 2\,\sigma(2x) - 1 .

Например, генератор DCGAN заканчивается tanh⁡\tanh, а его обучающие изображения масштабированы в [−1,1][-1, 1] (Radford, Metz and Chintala, 2016, разделы 3–4). С MSE работает и линейный выход вообще без активации; его значения не ограничены, и для показа их обрезают до допустимого диапазона.

Ноутбук#

Эксперименты следуют ноутбуку об автоэнкодерах из репозитория HSE-LAMBDA Deep Generative Models (закреплённый коммит; ячейки нумеруются с 1, считая все ячейки подряд, в том числе текстовые). Все значения функции потерь и наблюдения о графиках выше взяты из его сохранённых выходов; MSE полностью чёрного выхода вычислена по датасету.

  • Ячейки 5–7: MNIST, масштабированный до 64×6464 \times 64 и загруженный как тензоры со значениями в [0,1][0, 1], батчами по 64 с drop_last=True (ячейка 7).
  • Ячейка 9: visualize_latent_space, которая разворачивает коды в векторы и проецирует их с помощью t-SNE (перплексия 30); она вызывается до и после обучения в ячейках 15 и 20. Она переключает модель в eval(), поэтому график до обучения строится с батч-нормализациями, которые ещё хранят начальные скользящие статистики (среднее 0, дисперсия 1) и потому фактически являются тождественными отображениями.
  • Ячейка 10 обозначает энкодер как fθf_\theta, а декодер как gϕg_\phi — буквы противоположны принятым в этой заметке. Ячейка 11: Block и AutoEncoder с формами из таблиц выше. Ячейки 12–13 выбирают nn.MSELoss и Adam с шагом обучения (learning rate) 10−310^{-3}; текстовая ячейка предлагает в качестве альтернативы nn.BCEWithLogitsLoss, что, как объяснено выше, привело бы к двойной сигмоиде. Ячейка 17 обучает модель 5 эпох.
  • Ячейки 22–25: реконструкции тестовых изображений, латентная интерполяция между первыми двумя тестовыми изображениями и сэмплирование из диагональных гауссиан по классам (compute_activation_stats, по кодам обучающих изображений).
  • Ячейки 26–37: шумоподавляющий автоэнкодер (add_noise с множителем 0.1 по умолчанию, которая вызывается с 0.2 и при обучении, ячейка 31, и для тестовых изображений, ячейка 35; DenoisingBlock с Dropout2d(0.3)), его t-SNE до и после обучения (ячейки 30 и 33), исходные, зашумлённые и очищенные тестовые изображения (ячейка 35) и сэмплирование по классам (ячейка 37).
  • Ячейки 38–49: автоэнкодер с KL-разреженностью (kl_divergence_activations, SparseAE_KL с декодером на транспонированных свёртках, ρ=0.05\rho = 0.05, β=0.001\beta = 0.001). Блоки его декодера тоже используют сигмоиды, и сам код пропускается через сигмоиду, поэтому гауссианы по классам в ячейке 49 подгоняются к кодам в (0,1)(0, 1). Картинка с разреженными активациями в ячейке 38 взята из Jordan (2018) — иллюстрированного введения в автоэнкодеры, которое рекомендует репозиторий.
  • Ячейки 50–59: автоэнкодер с L1-разреженностью (λ=0.001\lambda = 0.001): обучение, t-SNE (ячейка 56), реконструкции (ячейка 57) и сэмплирование по классам (ячейка 59).
  • Циклы обучения KL- и L1-моделей (ячейки 43 и 53) после прямого прохода запускают энкодер второй раз, чтобы получить активации для штрафа. Это удваивает вычисления энкодера, а в режиме обучения дважды за шаг обновляет скользящие средние батч-нормализации.
  • Сохранённые значения функции потерь на обучении по эпохам (среднее по 937 батчам эпохи):
МодельЯчейкаЭпоха 12345
обычная170.01030.00340.00260.00230.0021
шумоподавляющая310.03130.01370.01160.01070.0102
с KL-разреженностью (сумма двух слагаемых)438.85314.44302.33150.78770.2141
с L1-разреженностью (сумма двух слагаемых)530.00950.00340.00280.00240.0022

Домашнее задание 1 из того же репозитория (нумерация ячеек та же) обучает автоэнкодер на CIFAR-10, а затем использует его энкодер для FID; часть о FID описана в заметке о FID. В ноутбуке нет сохранённых выходов, а ячейки для решений пусты, поэтому он документирует только процедуру.

  • Ячейка 3: set_seed(42) задаёт начальные значения (seed) генераторов случайных чисел random в Python, NumPy и PyTorch на CPU и всех GPU и устанавливает cudnn.deterministic = True и cudnn.benchmark = False (что делает каждый из этих шагов).
  • Ячейки 6–13: CIFAR-10 (50 000 обучающих и 10 000 валидационных изображений размера 3×32×323 \times 32 \times 32, десять классов) с преобразованием ToTensor(), за которым следует x * 2 - 1, и denormalize_image, (x+1)/2(x + 1)/2, для показа. При обучении батчи по 128; валидационный загрузчик использует батчи из одного изображения.
  • Ячейки 15–19: студент пишет Encoder и Decoder из любых Conv2d, MaxPool2d, AvgPool2d, Upsample и ConvTranspose2d. Две проверки: развёрнутый код должен содержать меньше 3⋅32⋅32=30723 \cdot 32 \cdot 32 = 3072 чисел, а декодер должен возвращать изображение 3×32×323 \times 32 \times 32. Входы лежат в [−1,1][-1, 1], поэтому последнему слою декодера нужен tanh⁡\tanh или линейный выход, а не сигмоида.
  • Ячейки 20–22 и 32: classification_score — описанный выше зондирующий классификатор. Функция переводит энкодер в режим eval(), вычисляет развёрнутый код каждого изображения под torch.no_grad(), обучает GradientBoostingClassifier с n_estimators=50 и max_depth=5 на кодах 5000 случайных обучающих изображений и возвращает его долю правильных ответов на всех 10 000 валидационных изображениях. Ячейка 22 оценивает необученный Encoder(); ячейка 32 требует, чтобы доля правильных ответов обученного энкодера была больше чем в 1.05 раза выше, чем у необученного.
  • Ячейки 23–31: train (MSE, среднее значений функции потерь по батчам за один проход по обучающей выборке) и eval (то же на валидационной выборке, в режиме eval() и под torch.no_grad()); full_train обучает модель с AdamW при настройках по умолчанию (шаг обучения 10−310^{-3}, затухание весов (weight decay) 10−210^{-2}) 30 эпох, строит графики MSE на обучении и валидации по эпохам, и её нужно доработать так, чтобы она загружала сохранённые веса вместо повторного обучения.
  • Ячейка 33: десять валидационных изображений и их реконструкции.
  • Проблемы ноутбука. classification_score при каждом вызове выбирает новое случайное подмножество из 5000 обучающих изображений, поэтому необученный и обученный энкодеры проверяются на разных подмножествах; фиксированное подмножество сделало бы сравнение с порогом 5% менее шумным. Функция с именем eval в ячейке 27 перекрывает встроенную функцию Python eval. Ячейка 17 проверяет энкодер на torch.rand(1, 3, 32, 32) - 1, то есть на значениях в [−1,0)[-1, 0), а не в [−1,1][-1, 1]; проверяется только форма, так что вреда нет.

Дальше#

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

Литература#