🔴 Сложный ⏱️ 60 минут

Generative Adversarial Networks

📋 Содержание урока

Generative Adversarial Networks 🎭

По-русски эту архитектуру называют генеративно-состязательными сетями, сокращённо GAN, и дальше в уроке мы будем использовать именно это название. До сих пор в курсе нейросети, с которыми ты работал, решали задачи распознавания: получить изображение и сказать, что на нём — кошка или собака, спам это письмо или нет, какое слово должно идти следующим. GAN устроена принципиально иначе: она не распознаёт готовые данные, а создаёт новые — изображения лиц, которых никогда не существовало, картины в стиле художника, которого никогда не было, звуки, тексты, синтетические примеры для обучения других моделей. И делает она это не напрямую, а через конкуренцию двух нейросетей, обучающихся одновременно и друг против друга.

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

Именно эта динамика — соревнование, в котором прогресс одного участника вынуждает прогрессировать другого, — и есть суть состязательного обучения (adversarial training), на котором построен GAN. Роль фальшивомонетчика играет нейросеть-генератор: она получает на вход случайный шум — набор случайных чисел, не несущих никакого смысла сам по себе, — и превращает его в синтетические данные, которые должны выглядеть как настоящие. Роль полицейского-эксперта играет нейросеть-дискриминатор: она получает данные (настоящие из обучающей выборки или сгенерированные) и выдаёт вероятность того, что перед ней — оригинал, а не подделка. Обе сети обучаются одновременно, попеременными шагами, и ошибка одной сети становится сигналом для обучения другой.

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

История

Идея, лежащая в основе GAN, родилась в 2014 году, и история её появления стала одной из самых известных легенд в машинном обучении. Иэн Гудфеллоу (Ian Goodfellow), тогда аспирант Монреальского университета, вечером сидел с друзьями-исследователями в баре и обсуждал проблему, которая в то время казалась почти неразрешимой: как научить нейросеть генерировать реалистичные изображения. Существовавшие на тот момент генеративные подходы — например, основанные на явном моделировании плотности вероятности данных — давали изображения размытыми и невыразительными, потому что сеть пыталась усреднённо описать все возможные варианты сразу, вместо того чтобы выдать один чёткий, убедительный пример.

По рассказу самого Гудфеллоу, во время того разговора в баре друзья предложили несколько сложных статистических схем для решения задачи, и ни одна не выглядела многообещающей. И тогда у него возникла идея: что, если вместо того чтобы одной сети напрямую пытаться описать распределение данных, заставить две сети соревноваться друг с другом — одну обучить генерировать, а вторую обучить отличать сгенерированное от настоящего, и использовать сигнал ошибки второй сети для обучения первой? Гудфеллоу утверждает, что уже в тот вечер, вернувшись домой, реализовал первую версию идеи на компьютере, и уже к утру у него заработал рабочий прототип. Статья «Generative Adversarial Networks» была опубликована в том же 2014 году и почти сразу стала одной из самых цитируемых работ в области глубокого обучения.

Реакция сообщества была стремительной: идея оказалась одновременно простой для понимания и мощной на практике, и в последующие несколько лет вокруг неё выросло целое направление исследований — десятки архитектурных вариаций, приёмы стабилизации обучения, применения от синтеза изображений до генерации музыки. Янн Лекун, один из отцов-основателей глубокого обучения, впоследствии назвал состязательное обучение «самой интересной идеей в машинном обучении за последние десять лет» — оценка, которую по сей день часто цитируют именно в связи с GAN.

Состязательное обучение и минимаксная игра

Интуиция

Вернёмся к метафоре фальшивомонетчика и полицейского и попробуем сформулировать её чуть точнее. У обоих участников противоположные цели, и это принципиально отличает обучение GAN от обучения обычной сети, которую ты видел раньше: там была одна функция потерь, и единственная сеть её минимизировала, шаг за шагом уменьшая ошибку. В GAN функция «потерь» одна на двоих, но одна сторона хочет её минимизировать, а другая — максимизировать. Это уже не задача оптимизации в привычном смысле, а игра двух игроков с противоположными интересами, и раздел математики, который формально описывает такие ситуации, называется теорией игр.

Дискриминатору выгодно, чтобы он мог уверенно отличать настоящие данные от сгенерированных — то есть он хочет, чтобы одна и та же величина была как можно больше. Генератору, наоборот, выгодно как можно сильнее сбить дискриминатора с толку — то есть он хочет, чтобы та же самая величина была как можно меньше. Такая постановка называется минимаксной игрой (minimax game): один игрок минимизирует общую цель, другой — максимизирует её, и оба действуют одновременно, подстраиваясь под текущее поведение соперника.

Формула

Минимаксная целевая функция GAN. Пусть $p_{data}(x)$ — истинное распределение реальных данных (например, распределение настоящих фотографий лиц в пространстве пикселей), а $p_z(z)$ — заранее выбранное простое распределение шума (обычно стандартное нормальное или равномерное), из которого генератор берёт входной вектор. Генератор — это функция $G(z)$, дискриминатор — функция $D(x)\in[0,1]$, выдающая вероятность того, что $x$ — реальные данные. Обучение GAN формулируется как одна общая величина $V(D,G)$, которую генератор и дискриминатор оптимизируют в противоположных направлениях:

$$\min_G \max_D V(D,G) = \mathbb{E}_{x\sim p_{data}(x)}\big[\log D(x)\big] + \mathbb{E}_{z\sim p_z(z)}\big[\log\big(1-D(G(z))\big)\big]$$

Дискриминатор максимизирует $V$: он хочет, чтобы $D(x)$ было близко к $1$ на реальных данных и $D(G(z))$ было близко к $0$ на сгенерированных. Генератор минимизирует ту же самую $V$: он хочет, чтобы $D(G(z))$ было близко к $1$, то есть чтобы дискриминатор принимал подделки за настоящие данные. При фиксированном генераторе оптимальный дискриминатор имеет явный вид:

$$D^{*}_{G}(x) = \frac{p_{data}(x)}{p_{data}(x) + p_{g}(x)}$$

где $p_g$ — распределение данных, которое неявно задаёт генератор (распределение значений $G(z)$ при $z\sim p_z$).

Разбор примеров

Пример 1 (оптимальный дискриминатор в конкретной точке). Пусть в какой-то точке пространства данных $x_0$ плотность реальных данных $p_{data}(x_0)=0{,}8$, а плотность сгенерированных данных в той же точке $p_g(x_0)=0{,}2$ — то есть настоящих объектов, похожих на $x_0$, в четыре раза больше, чем сгенерированных. По формуле оптимального дискриминатора: $D^{*}(x_0) = 0{,}8/(0{,}8+0{,}2) = 0{,}8/1{,}0 = 0{,}8$. Это разумно: если реальных примеров вокруг $x_0$ значительно больше, чем сгенерированных, идеальный дискриминатор должен быть достаточно уверен (вероятность $0{,}8$), что конкретный объект в этой точке — настоящий.

Пример 2 (значение игры в глобальном оптимуме). Глобальный оптимум минимаксной игры достигается, когда распределение генератора в точности совпадает с распределением данных: $p_g = p_{data}$. Подставим это равенство в формулу оптимального дискриминатора: $D^{*}(x) = p_{data}(x)/(p_{data}(x)+p_{data}(x)) = p_{data}(x)/(2p_{data}(x)) = 0{,}5$ для любого $x$. Это означает, что идеальный дискриминатор в точке глобального оптимума не может сделать ничего лучше, чем подбросить монетку — распределения неотличимы, и вероятность $0{,}5$ для любого объекта в равной степени обоснована как «настоящий», так и «поддельный». Подставив $D^*=0{,}5$ обратно в $V(D,G)$: $V(D^{*},G^{*}) = \log(0{,}5) + \log(1-0{,}5) = \log(0{,}5)+\log(0{,}5) = 2\log(0{,}5) = -2\log 2 = -\log 4 \approx -1{,}386$.

Пример 3 (связь с дивергенцией Йенсена — Шеннона). Можно показать (доказательство опирается на подстановку оптимального $D^*_G$ обратно в $V$ и алгебраические преобразования логарифмов), что при оптимальном дискриминаторе значение игры выражается через дивергенцию Йенсена — Шеннона (Jensen-Shannon divergence, $JSD$) между $p_{data}$ и $p_g$ — величину, измеряющую, насколько два распределения вероятностей различаются, и равную нулю тогда и только тогда, когда распределения совпадают:

$$C(G) = \max_D V(D,G) = -\log 4 + 2\cdot JSD(p_{data}\,\|\,p_g)$$

Поскольку $JSD$ всегда неотрицательна, минимум $C(G)$ по генератору достигается ровно при $JSD(p_{data}\|p_g)=0$, то есть при $p_g=p_{data}$, и в этой точке $C(G)=-\log4$ — то самое значение, которое мы вычислили в примере 2 напрямую. Этот результат — ключевое теоретическое обоснование всей идеи: обучая генератор минимизировать $V$ против оптимального дискриминатора, мы на самом деле неявно минимизируем расстояние между распределением сгенерированных данных и распределением настоящих данных, даже не вычисляя это расстояние явно.

Почему это важно

Минимаксная формулировка — это не просто красивая математическая обёртка, а объяснение того, почему GAN вообще способна генерировать реалистичные данные без явного задания того, что значит «реалистично». Ни генератор, ни дискриминатор не получают напрямую формулу плотности $p_{data}$ — они видят только конкретные примеры данных. Тем не менее, как показывает пример 3, соревнование двух сетей в теории сходится к тому же результату, к которому пришлось бы прийти, если бы мы явно минимизировали расстояние между распределениями. Понимание этой теоретико-игровой природы задачи также объясняет, почему обучение GAN на практике устроено иначе, чем обучение обычной сети — именно этому посвящены следующие два раздела урока.

Архитектура: генератор и дискриминатор

Интуиция

Генератор и дискриминатор — это две отдельные нейросети с разными задачами и, как правило, зеркальной структурой. Генератор решает задачу, которую можно назвать «раскрытием» информации: на входе — компактный вектор случайных чисел, скажем, из ста значений, на выходе — целое изображение, скажем, из нескольких тысяч пикселей. Чтобы совершить такое расширение размерности, генератор обычно устроен как последовательность слоёв, каждый из которых увеличивает пространственный размер карты признаков — например, транспонированные свёртки (их также называют деконволюциями, хотя формально это не обратная операция к свёртке), которые ты можешь считать операцией, действующей противоположно обычной свёртке: не уменьшают, а увеличивают пространственные размеры.

Дискриминатор, наоборот, решает уже знакомую тебе задачу — задачу бинарной классификации, ровно такую же по сути, как задачи из уроков про логистическую регрессию и свёрточные сети (уроки 313, 335–338): на входе изображение, на выходе одно число от $0$ до $1$ — вероятность того, что вход настоящий. Устроен дискриминатор поэтому как обычный свёрточный классификатор: несколько слоёв свёртки, постепенно уменьшающих пространственный размер и увеличивающих число каналов, а в конце — полносвязный слой с сигмоидой.

Формула

Генератор. Отображение $G_\theta: \mathbb{R}^k \to \mathbb{R}^n$ с обучаемыми весами $\theta$, где $k$ — размерность латентного (скрытого) шумового вектора (типичные значения $k=100$ или $k=128$), а $n$ — размерность данных (например, $n = 64\times64\times3$ для цветного изображения $64\times64$). Типичная структура: полносвязный слой, проецирующий $z\in\mathbb{R}^k$ в небольшой тензор (например, $4\times4\times1024$), за которым следует цепочка транспонированных свёрток, на каждом шаге удваивающих пространственный размер, пока не будет достигнут целевой размер изображения; последний слой обычно завершается функцией активации $\tanh$, ограничивающей пиксели диапазоном $[-1,1]$. Дискриминатор. Отображение $D_\phi: \mathbb{R}^n \to [0,1]$ с обучаемыми весами $\phi$ — обычный бинарный классификатор: цепочка свёрточных слоёв, на каждом шаге уменьшающих пространственный размер вдвое и увеличивающих число каналов, завершающаяся полносвязным слоем с сигмоидой. Размер выхода транспонированной свёртки при входном пространственном размере $H_{in}$, ядре $k$, шаге (stride) $s$ и паддинге $p$:

$$H_{out} = (H_{in}-1)\cdot s - 2p + k$$

Разбор примеров

Пример 1 (число параметров первого слоя генератора). Пусть латентный вектор $z\in\mathbb{R}^{100}$ проецируется полносвязным слоем в тензор $7\times7\times256$ — то есть на выходе $7\times7\times256=12\,544$ значения. Число параметров полносвязного слоя (без учёта смещений) равно произведению размерности входа на размерность выхода: $100\times12\,544 = 1\,254\,400$ параметров — больше миллиона только на один первый слой, который ещё даже не начал работать с пространственной структурой изображения.

Пример 2 (форма выхода после цепочки транспонированных свёрток, MNIST-подобный генератор). Продолжим пример 1: тензор $7\times7\times256$ проходит через транспонированную свёртку с ядром $k=4$, шагом $s=2$, паддингом $p=1$. По формуле: $H_{out} = (7-1)\times2 - 2\times1 + 4 = 12-2+4=14$, то есть карта признаков становится $14\times14$. Применим вторую такую же транспонированную свёртку к результату: $H_{out}=(14-1)\times2-2\times1+4=26-2+4=28$ — и мы получили классический размер изображения MNIST $28\times28$ ровно за два шага удвоения от базового тензора $7\times7$.

Пример 3 (число параметров первого слоя дискриминатора и сравнение с генератором). Пусть дискриминатор для тех же изображений $28\times28\times1$ начинает со свёрточного слоя с ядром $5\times5$, дающего $64$ выходных канала — по формуле параметров свёртки из уроков про CNN-архитектуры ($k\times k\times C_{in}\times C_{out}$, урок 338): $5\times5\times1\times64 = 1\,600$ параметров. Это заметно меньше, чем $1\,254\,400$ параметров первого слоя генератора из примера 1 — и это типичная картина: генератору, которому нужно «развернуть» компактный шум в полноразмерные, детализированные данные, обычно требуется больше параметров на входных слоях, чем дискриминатору, чья задача — сжать данные до одного числа.

Почему это важно

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

Обучение поочерёдными шагами

Интуиция

Раз у генератора и дискриминатора противоположные цели относительно одной и той же величины $V(D,G)$, обучать обе сети одновременно, одним общим градиентным шагом, не получится — шаг, уменьшающий $V$ для генератора, увеличивал бы её для дискриминатора, и наоборот. Вместо этого GAN обучают поочерёдными шагами (alternating training): сначала фиксируют веса генератора и делают один или несколько шагов обучения дискриминатора, обновляя только его веса; затем фиксируют уже обученный на этом шаге дискриминатор и делают шаг обучения генератора, обновляя только его веса. Затем цикл повторяется заново, уже с обновлёнными обеими сетями, тысячи раз подряд.

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

Формула

Шаг обучения дискриминатора (генератор $G$ зафиксирован). На минибатче из $m$ реальных примеров $x_1,\dots,x_m\sim p_{data}$ и $m$ шумовых векторов $z_1,\dots,z_m\sim p_z$ дискриминатор делает шаг градиентного подъёма (поскольку он максимизирует $V$), минимизируя эквивалентную функцию потерь бинарной кросс-энтропии:

$$L_D = -\frac{1}{m}\sum_{i=1}^{m}\Big[\log D(x_i) + \log\big(1-D(G(z_i))\big)\Big], \qquad \phi \leftarrow \phi - \eta\nabla_\phi L_D$$

Шаг обучения генератора (дискриминатор $D$ зафиксирован). На новом минибатче шумовых векторов генератор делает шаг градиентного спуска по своей функции потерь. Теоретически, из минимаксной формулировки, это $\log(1-D(G(z)))\to\min$, но на практике почти всегда используют эквивалентную по направлению, но лучше себя ведущую несатурирующую (non-saturating) функцию потерь:

$$L_G = -\frac{1}{m}\sum_{i=1}^{m}\log D(G(z_i)), \qquad \theta \leftarrow \theta - \eta\nabla_\theta L_G$$

Разбор примеров

Пример 1 (численный расчёт потерь дискриминатора на маленьком батче). Пусть $m=3$, дискриминатор выдал на реальных примерах $D(x_1)=0{,}9$, $D(x_2)=0{,}8$, $D(x_3)=0{,}7$, а на сгенерированных — $D(G(z_1))=0{,}3$, $D(G(z_2))=0{,}2$, $D(G(z_3))=0{,}4$. Тогда $L_D = -\frac{1}{3}\big[(\log0{,}9+\log0{,}1)+(\log0{,}8+\log0{,}2)+(\log0{,}7+\log0{,}3)\big]$. Считаем по слагаемым (натуральный логарифм): $\log0{,}9\approx-0{,}105$, $\log0{,}1\approx-2{,}303$, сумма первой пары $\approx-2{,}408$; $\log0{,}8\approx-0{,}223$, $\log0{,}2\approx-1{,}609$, сумма второй пары $\approx-1{,}832$; $\log0{,}7\approx-0{,}357$, $\log0{,}3\approx-1{,}204$, сумма третьей пары $\approx-1{,}561$. Итого сумма трёх пар $\approx-5{,}801$, и $L_D = -\frac{1}{3}\times(-5{,}801) \approx 1{,}934$.

Пример 2 (потери генератора на том же наборе фейковых предсказаний, несатурирующий вариант). Используем те же $D(G(z_1))=0{,}3$, $D(G(z_2))=0{,}2$, $D(G(z_3))=0{,}4$, но теперь это уже новый шаг — фейки, на которых учится генератор. По формуле несатурирующих потерь: $L_G = -\frac{1}{3}\big[\log0{,}3+\log0{,}2+\log0{,}4\big] \approx -\frac{1}{3}\times(-1{,}204-1{,}609-0{,}916) = -\frac{1}{3}\times(-3{,}729) \approx 1{,}243$. Генератор будет двигать свои веса в направлении, уменьшающем это значение, то есть в направлении, повышающем $D(G(z))$ — заставляющем дискриминатор больше верить в подлинность фейков.

Пример 3 (одна итерация обучения от начала до конца, пошагово). Представь минибатч размера $m=2$. Шаг 1: берём $2$ реальных примера из обучающей выборки и $2$ случайных шумовых вектора, пропускаем шум через текущий генератор, получаем $2$ фейковых примера. Шаг 2: подаём все $4$ примера (с правильными метками — $1$ для реальных, $0$ для фейков) в дискриминатор, считаем $L_D$ (как в примере 1), делаем один шаг оптимизатора, обновляя только веса дискриминатора $\phi$. Шаг 3: берём $2$ новых шумовых вектора, пропускаем через уже слегка обновлённый генератор, пропускаем результат через уже слегка обновлённый на шаге 2 дискриминатор, считаем $L_G$ (как в примере 2), делаем один шаг оптимизатора, обновляя только веса генератора $\theta$. Эта пара шагов — одна итерация обучения GAN; вся тренировка — это тысячи таких итераций подряд, при этом веса дискриминатора и генератора никогда не обновляются на одном и том же шаге оптимизатора одновременно.

Почему это важно

Поочерёдная схема обучения — не техническая деталь реализации, а прямое следствие того, что $V(D,G)$ не может минимизироваться и максимизироваться одним и тем же градиентным шагом. Понимание этого механизма напрямую объясняет, почему код обучения GAN на PyTorch или TensorFlow всегда содержит два отдельных оптимизатора (один для параметров дискриминатора, другой — для параметров генератора) и два отдельных вызова backward()/step() внутри одной итерации, а не один общий, как в обычной сети из уроков про backpropagation (урок 330). Это же поочерёдное, взаимно зависимое обновление — прямая причина проблем, разбираемых в следующем разделе: неустойчивость обучения GAN возникает именно потому, что оба игрока постоянно подстраиваются под движущуюся, меняющуюся на каждом шаге цель.

Проблемы обучения: коллапс мод и нестабильность

Интуиция

На практике обучение GAN заметно капризнее, чем обучение обычной сети, и у этого есть две наиболее характерные причины. Первая — коллапс мод (mode collapse): реальные данные почти всегда содержат много разных видов объектов (мод распределения) — разные лица, разные позы, разные стили, — а генератор иногда обнаруживает, что достаточно производить лишь один или несколько «удачных» вариантов, которые стабильно обманывают текущий дискриминатор, чтобы получать низкий $L_G$. Зачем рисковать и пробовать разнообразие, если один и тот же удачный трюк снова и снова срабатывает? В результате генератор перестаёт использовать информацию из разных значений $z$ содержательно — разным шумовым векторам на входе соответствует почти одинаковый, однообразный выход, и разнообразие сгенерированных данных резко падает, даже если по отдельности эти данные выглядят реалистично.

Вторая причина — общая нестабильность состязательного обучения. В обычной задаче минимизации одна сеть спускается по одной, фиксированной для текущего батча поверхности функции потерь, и при разумной скорости обучения этот спуск в целом монотонно снижает ошибку. В GAN обе сети постоянно меняют «ландшафт» задачи друг для друга: как только генератор чуть улучшается, дискриминатору приходится заново подстраиваться под новый вид фейков, и наоборот. Формально это означает, что одновременный градиентный спуск-подъём двух игроков не обязан сходиться к точке равновесия игры (седловой точке минимаксной задачи) — вместо этого он может циклически «вращаться» вокруг неё, никогда её не достигая, а лосс-кривые обеих сетей могут скакать вверх-вниз без ясной тенденции к снижению, что резко контрастирует с привычными графиками обучения обычных сетей.

Формула

Насыщение (saturation) градиента исходной функции потерь генератора. Если использовать теоретическую формулировку $L_G^{sat} = \log(1-D(G(z)))\to\min$ напрямую, то на ранних шагах обучения, когда дискриминатор ещё легко отличает слабые фейки от настоящих данных, $D(G(z))\approx0$, и производная $L_G^{sat}$ по $D(G(z))$ равна:

$$\frac{\partial L_G^{sat}}{\partial D(G(z))} = \frac{-1}{1-D(G(z))}$$

При $D(G(z))\to0$ это значение стремится к $-1$ — то есть градиент почти не растёт по модулю даже тогда, когда генератор работает совсем плохо, и обучение почти останавливается именно в тот момент, когда генератору нужнее всего сильный сигнал для улучшения. Несатурирующая версия $L_G = -\log D(G(z))$ даёт производную

$$\frac{\partial L_G}{\partial D(G(z))} = \frac{-1}{D(G(z))}$$

которая при $D(G(z))\to0$ стремится к $-\infty$ — то есть даёт сильный, а не исчезающий сигнал именно тогда, когда генератор ошибается сильнее всего.

Разбор примеров

Пример 1 (численное сравнение насыщающего и несатурирующего градиента). Пусть на раннем шаге обучения дискриминатор уверенно распознаёт фейк: $D(G(z))=0{,}001$. Для насыщающей формулировки производная по $D(G(z))$ равна $-1/(1-0{,}001) = -1/0{,}999\approx-1{,}001$ — почти не отличается от $-1$, то есть сигнал слабый и почти не зависит от того, насколько именно плох фейк. Для несатурирующей формулировки производная равна $-1/0{,}001=-1000$ — сигнал в тысячу раз сильнее. Именно эта разница в масштабе градиента и есть причина, по которой несатурирующий вариант из предыдущего раздела почти всегда используют на практике вместо теоретически «чистой» формулировки.

Пример 2 (иллюстрация коллапса мод на игрушечном распределении). Пусть настоящие данные — это три отдельных кластера точек на плоскости, например, вокруг центров $(-5,0)$, $(0,0)$ и $(5,0)$, каждый кластер представляет свою «моду» распределения. Допустим, генератор на каком-то шаге обучения обнаружил, что все свои выходы, независимо от входного $z$, можно стянуть в окрестность только центра $(0,0)$ — и в этой точке дискриминатору сложно отличить сгенерированные точки от части настоящих данных (тех, что тоже лежат возле $(0,0)$). Дискриминатор, обучаясь на этом конкретном батче, обновляет веса так, чтобы лучше отделять точки возле $(0,0)$, а генератор, обучаясь избегать именно этой, уже разоблачённой области, может «перескочить» целиком к другому кластеру, например $(5,0)$, — но по-прежнему производя лишь одну моду за раз, а не все три одновременно. Внешне генератор кажется «прогрессирующим» (он избегает разоблачения), но фактического разнообразия, соответствующего всем трём кластерам сразу, как не было, так и нет.

Пример 3 (mode collapse через призму дивергенции Йенсена — Шеннона). В разделе про минимаксную игру мы показали, что при оптимальном дискриминаторе значение игры равно $C(G)=-\log4+2\cdot JSD(p_{data}\|p_g)$, и минимум $C(G)=-\log4$ достигается только при полном совпадении $p_g=p_{data}$. Если из-за коллапса мод генератор покрывает только одну треть реального распределения (условно, одну моду из трёх в примере 2), то $p_g$ заметно отличается от $p_{data}$, а значит $JSD(p_{data}\|p_g)>0$, и значение игры $C(G)$ остаётся выше теоретического минимума — численно это означает, что даже если для конкретного, узкого батча дискриминатор временно обманут, глобально задача ещё не решена, и это несовпадение будет продолжать «подталкивать» обучение, часто вызывая ту самую нестабильность и колебания, о которых говорилось в интуиции.

Почему это важно

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

Практика: 30 заданий

Базовые задания (1–10)

Задание 1: Зачем генератору на входе нужен случайный вектор $z$, а не, например, всегда один и тот же фиксированный вектор?


Задание 2: В точке $x_0$ плотность реальных данных $p_{data}(x_0)=0{,}6$, плотность сгенерированных данных $p_g(x_0)=0{,}3$. Найди значение оптимального дискриминатора $D^*(x_0)$.


Задание 3: Батч из двух реальных примеров с $D(x_1)=0{,}95$, $D(x_2)=0{,}85$ и двух фейков с $D(G(z_1))=0{,}1$, $D(G(z_2))=0{,}15$. Посчитай $L_D$.


Задание 4: Для тех же двух фейков из задания 3 ($D(G(z_1))=0{,}1$, $D(G(z_2))=0{,}15$) посчитай несатурирующие потери генератора $L_G$.


Задание 5 (машинное обучение): Какой знакомой тебе по прошлым урокам задачей является дискриминатор по своей структуре и какая функция активации стоит на его последнем слое?


Задание 6: Транспонированная свёртка: вход $4\times4$, ядро $k=4$, шаг $s=2$, паддинг $p=1$. Найди пространственный размер выхода.


Задание 7: Подтверди, что при $p_g=p_{data}$ значение игры $V(D^*,G^*)$ равно $-\log4$, и посчитай это число приближённо.


Задание 8 (машинное обучение): Чем схема обучения GAN отличается от обучения обычной сети (например, свёрточного классификатора) с точки зрения количества функций потерь и оптимизаторов в цикле обучения?


Задание 9: Дискриминатор выдаёт $D(G(z))=0{,}002$ на сгенерированном примере. Сравни, во сколько раз производная несатурирующих потерь по $D(G(z))$ больше по модулю, чем производная исходных (насыщающих) потерь в этой точке.


Задание 10: Видит ли генератор когда-либо реальные данные $x$ напрямую в процессе обучения (не через дискриминатор)?

Средние задания (11–20)

Задание 11: Полносвязный слой генератора проецирует $z\in\mathbb{R}^{128}$ в тензор $4\times4\times512$. Посчитай число параметров этого слоя (без смещений).


Задание 12: Первый свёрточный слой дискриминатора: ядро $5\times5$, $3$ входных канала (RGB), $64$ выходных канала. Посчитай число параметров.


Задание 13 (машинное обучение): Опиши на собственном примере (можно игрушечном, не из урока), как выглядел бы коллапс мод, если генератор обучается создавать рукописные цифры от 0 до 9.


Задание 14: Дискриминатор выдаёт на сгенерированном примере $D(G(z))=0{,}0005$. Найди производную насыщающих потерь и несатурирующих потерь по $D(G(z))$ в этой точке.


Задание 15 (код): Реализуй на PyTorch простой полносвязный генератор для игрушечных данных: вход $z\in\mathbb{R}^{16}$, скрытый слой $64$ нейрона с ReLU, выход $2$ числа (точка на плоскости) без финальной активации.

import torch.nn as nn

class ToyGenerator(nn.Module):
    def __init__(self, z_dim=16, hidden_dim=64, out_dim=2):
        super().__init__()
        # твой код здесь
        pass

    def forward(self, z):
        # твой код здесь
        pass

Задание 16 (код): Реализуй парный к заданию 15 дискриминатор: вход $2$ числа, скрытый слой $64$ нейрона с ReLU, выход — одна вероятность через сигмоиду.

import torch.nn as nn

class ToyDiscriminator(nn.Module):
    def __init__(self, in_dim=2, hidden_dim=64):
        super().__init__()
        # твой код здесь
        pass

    def forward(self, x):
        # твой код здесь
        pass

Задание 17 (машинное обучение): В некоторых реализациях дискриминатор обучают $k=5$ шагов на каждый $1$ шаг генератора. В чём практический смысл такого соотношения и в чём риск, если взять $k$ слишком большим?


Задание 18: Даны два простых распределения на двух исходах: $p_{data}=(0{,}9,\ 0{,}1)$ и $p_g=(0{,}5,\ 0{,}5)$. Средняя точка $m=(0{,}7,\ 0{,}3)$. Не вычисляя JSD целиком по формуле, объясни качественно, будет ли она близка к нулю или заметно больше нуля, и почему.


Задание 19 (машинное обучение): Объясни, почему на практике почти никогда не используют теоретическую формулировку $L_G^{sat}=\log(1-D(G(z)))\to\min$ напрямую, хотя именно она следует из исходной минимаксной постановки.


Задание 20: Минибатч размера $m=3$: реальные $D(x_1)=0{,}7$, $D(x_2)=0{,}6$, $D(x_3)=0{,}9$; фейки (на новом шаге генератора) $D(G(z_1))=0{,}25$, $D(G(z_2))=0{,}3$, $D(G(z_3))=0{,}2$. Посчитай $L_D$ по первой тройке и $L_G$ (несатурирующий) по второй тройке.

Продвинутые задания (21–30)

Задание 21: Выведи формулу оптимального дискриминатора $D^*_G(x)$, максимизируя подынтегральное выражение $p_{data}(x)\log D(x) + p_g(x)\log(1-D(x))$ поточечно по $D(x)\in[0,1]$.


Задание 22 (машинное обучение): Подставь $p_g=p_{data}$ в общую формулу $C(G)=-\log4+2\cdot JSD(p_{data}\|p_g)$ и убедись, что результат совпадает со значением, посчитанным напрямую в примере 2 раздела про минимаксную игру.


Задание 23: Батч из двух примеров: реальный $x_1$ с $D(x_1)=0{,}55$ и фейк $G(z_1)$ с $D(G(z_1))=0{,}45$. Покажи на числах, что улучшение позиции дискриминатора (рост $D(x_1)$ и падение $D(G(z_1))$) одновременно означает ухудшение позиции генератора.


Задание 24 (код): Напиши скелет одной итерации обучения GAN на PyTorch (без полной реализации сетей) — с двумя отдельными оптимизаторами и правильным порядком шагов zero_grad/backward/step для дискриминатора и генератора.

# generator, discriminator - уже созданные модели
# opt_g, opt_d - отдельные оптимизаторы для generator и discriminator
# real_batch - батч реальных данных
# criterion - функция потерь (например, nn.BCELoss())

def train_step(generator, discriminator, opt_g, opt_d, criterion, real_batch, z_dim):
    # твой код здесь
    pass

Задание 25 (машинное обучение): Почему сходимость одновременного градиентного спуска-подъёма (simultaneous gradient descent-ascent) к равновесию игры не гарантирована, в отличие от сходимости обычного градиентного спуска к минимуму выпуклой функции?


Задание 26: Батч из трёх реальных и трёх фейковых примеров: $D(x)=(0{,}8,0{,}75,0{,}9)$, $D(G(z))=(0{,}2,0{,}35,0{,}15)$ (используются на одном и том же шаге дискриминатора). Посчитай $L_D$, затем, используя эти же три значения $D(G(z))$ уже как результат следующего, независимого шага генератора, посчитай $L_G$.


Задание 27 (машинное обучение): Во время обучения ты замечаешь, что почти все сгенерированные изображения в батче выглядят практически одинаково, хотя шумовые векторы $z$ для них были разными. Сформулируй, какая это проблема, и предложи одно направление для диагностики.


Задание 28: Генератор для изображений $28\times28$ строит цепочку из базового тензора $7\times7\times128$ через две транспонированные свёртки с $k=4$, $s=2$, $p=1$. Посчитай пространственный размер после каждого слоя и убедись, что итоговый размер — $28\times28$.


Задание 29 (машинное обучение): GAN, обученная на фотографиях лиц, способна генерировать фотореалистичные лица людей, которых не существует — та же технология лежит в основе deepfake-видео. Назови одну техническую причину, почему такие изображения могут быть неотличимы от настоящих, и одну практическую меру, которая может честно ограничивать риск злоупотребления.


Задание 30 (машинное обучение): Сравни структурно цикл обучения GAN с циклом обучения обычного бинарного классификатора (например, логистической регрессии, урок 313): что принципиально отличается, помимо наличия двух сетей?

Частые ошибки

  • Ожидают, что $L_D$ и $L_G$ будут одновременно монотонно убывать, как лосс обычной сети. В состязательном обучении это невозможно структурно: снижение $L_D$ (дискриминатор становится точнее) почти всегда сопровождается ростом $L_G$ (генератору стало труднее обманывать), и наоборот — скачущие, немонотонные кривые для GAN являются ожидаемым, а не аварийным поведением (раздел про нестабильность).

  • Считают, что низкий loss дискриминатора или генератора напрямую означает хорошее визуальное качество генерации. Как показано в задании 30, лосс каждой сети зависит от постоянно меняющегося соперника и не измеряет напрямую реалистичность или разнообразие сгенерированных данных — качество нужно оценивать отдельно, визуальным осмотром или специализированными метриками, а не только по значению функции потерь.

  • Путают mode collapse с обычным переобучением (overfitting). Переобучение — это когда модель хорошо работает на обучающих данных, но плохо на новых. Коллапс мод — это когда генератор в принципе перестаёт использовать разнообразие входного шума $z$, производя малое число повторяющихся вариантов вне зависимости от объёма и качества обучающих данных; это проблема самой динамики состязательного обучения, а не классического переобучения.

  • Думают, что после обучения используются обе сети — и генератор, и дискриминатор. На практике для генерации новых данных используется исключительно обученный генератор $G$; дискриминатор нужен только на этапе обучения как обучающий сигнал и после его завершения, как правило, просто отбрасывается.

  • Полагают, что несатурирующая функция потерь генератора и исходная минимаксная формулировка $\log(1-D(G(z)))$ дают одинаковое поведение при обучении. Как показано в разборе задания 19 и разделе про нестабильность, эти формулировки эквивалентны лишь по направлению оптимизации в теории, но резко различаются по силе градиента на практике — именно поэтому почти все реализации используют несатурирующую версию, а не «теоретически чистую».

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

Главное запомнить

  • GAN состоит из двух нейросетей, обучающихся одновременно и с противоположными целями: генератор $G$ превращает случайный шум $z$ в синтетические данные, дискриминатор $D$ пытается отличить их от настоящих.

  • Формально обучение GAN — это минимаксная игра $\min_G\max_D V(D,G) = \mathbb{E}_{x\sim p_{data}}[\log D(x)] + \mathbb{E}_{z\sim p_z}[\log(1-D(G(z)))]$ — теоретико-игровая, а не обычная оптимизационная постановка задачи.

  • Оптимальный дискриминатор при фиксированном генераторе имеет явный вид $D^*_G(x)=p_{data}(x)/(p_{data}(x)+p_g(x))$, а значение игры в оптимуме равно $-\log4+2\cdot JSD(p_{data}\|p_g)$, что связывает состязательное обучение с минимизацией дивергенции Йенсена — Шеннона между распределениями данных и генерации.

  • Генератор строится как цепочка слоёв, увеличивающих пространственную размерность (например, транспонированные свёртки), а дискриминатор — как обычный свёрточный бинарный классификатор с сигмоидой на выходе.

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

  • На практике вместо теоретической функции потерь генератора $\log(1-D(G(z)))$ почти всегда используют несатурирующую версию $-\log D(G(z))$, потому что она даёт заметно более сильный градиент именно тогда, когда дискриминатор уверенно распознаёт фейки.

  • Коллапс мод (mode collapse) — генератор перестаёт использовать разнообразие входного шума и производит малое число повторяющихся вариантов, теряя разнообразие исходного распределения данных.

  • Нестабильность обучения GAN — структурное следствие того, что оба игрока подстраиваются под постоянно меняющуюся цель, из-за чего одновременный градиентный спуск-подъём не гарантированно сходится к равновесию игры, в отличие от обычного градиентного спуска на статичной функции потерь.

  • Применения GAN разнообразны — от синтеза изображений, дизайна и пополнения обучающих выборок (data augmentation) до технологий deepfake, где та же архитектура создаёт как творческие и практические возможности, так и реальные риски дезинформации.

Связь с темами курса

Сегодняшний урок открывает новый подблок курса про генеративные модели и опирается на несколько ранее пройденных тем сразу. Дискриминатор — это в точности тот же тип задачи, что и логистическая регрессия (урок 313): бинарная классификация с сигмоидой и функцией потерь в виде кросс-энтропии, только применённая внутри более крупной, состязательной схемы обучения. Архитектурно генератор и дискриминатор опираются на свёрточные сети из уроков 335–338 — те же принципы свёртки, только теперь свёртка используется в обе стороны: и для сжатия (дискриминатор), и, в транспонированном виде, для расширения (генератор). Сам процесс обучения — градиентный спуск и обратное распространение ошибки (урок 330) — остаётся тем же самым механизмом, что и в любой другой нейросети, только применённым дважды за итерацию, к двум разным наборам параметров.

Теоретическая часть урока — минимаксная формулировка и связь с дивергенцией Йенсена — Шеннона — опирается на понятия теории вероятностей, пройденные в блоке про случайные величины и распределения (уроки 226–240): плотность распределения, математическое ожидание как способ записи среднего по распределению, и общая идея меры «расстояния» между двумя распределениями вероятностей. Понимание того, что GAN на самом деле неявно минимизирует расхождение между распределением данных и распределением генератора, — это прямое применение вероятностного мышления к задаче генерации.

Следующий урок (346) познакомит тебя с вариационными автоэнкодерами (VAE) — принципиально другим подходом к генерации данных, который вместо состязательной игры двух сетей использует единую функцию потерь и явное вероятностное моделирование скрытого пространства. Сравнение GAN и VAE — типичный практический вопрос при выборе архитектуры для реальной задачи генерации, и имея за плечами сегодняшний урок, тебе будет проще увидеть, в чём именно их фундаментальное различие.

Интересные факты

  • Идея GAN, по словам самого Иэна Гудфеллоу, родилась за один вечер в баре в 2014 году в разговоре с друзьями-исследователями о том, как заставить нейросеть генерировать реалистичные изображения — и уже той же ночью, вернувшись домой, он написал и запустил первую рабочую версию кода.

  • Название «adversarial» (состязательный) отражает не просто метафору, а формальную связь с теорией игр: минимаксная формулировка GAN структурно родственна классическим играм с нулевой суммой, которые задолго до появления нейросетей изучались математиками и экономистами.

  • Сайт thispersondoesnotexist.com, показывающий сгенерированное GAN-подобной архитектурой (StyleGAN от NVIDIA) фотореалистичное лицо человека, которого никогда не существовало, при каждом обновлении страницы стал одной из самых наглядных публичных демонстраций возможностей состязательного обучения.

  • Янн Лекун, один из создателей современного глубокого обучения, назвал состязательное обучение «самой интересной идеей в машинном обучении за последние десять лет» — цитату, которую до сих пор часто приводят как признание значимости работы Гудфеллоу со стороны сообщества.

Лайфхаки

  • Если лосс-кривые GAN скачут вверх-вниз без явной тенденции к снижению, не спеши считать это багом — оцени качество обучения по сгенерированным примерам и метрикам разнообразия, а не только по значению $L_D$ и $L_G$, которые в состязательном обучении не обязаны монотонно убывать.

  • Используй несатурирующую функцию потерь генератора ($-\log D(G(z))$) вместо теоретической $\log(1-D(G(z)))$ практически всегда — на ранних шагах обучения это резко снижает риск исчезающего градиента, разобранный в разделе про нестабильность.

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

  • Не увеличивай число шагов обучения дискриминатора на каждый шаг генератора бездумно: небольшое соотношение (например, $1$:$1$ или $2$:$1$) часто работает не хуже более агрессивных схем, а слишком сильный дискриминатор рискует «задушить» обучающий сигнал для генератора.

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

  • Перед тем как писать собственную реализацию с нуля, изучи референсные архитектуры вроде DCGAN — набор проверенных практических рекомендаций (свёртки со страйдом вместо pooling, batch normalization в генераторе и дискриминаторе, определённые функции активации по слоям) снимает большую часть типичных проблем стабильности ещё до того, как они успевают проявиться.

Ты прошёл путь от простой метафоры о фальшивомонетчике и полицейском до строгой минимаксной формулировки, архитектуры генератора и дискриминатора, механики поочерёдного обучения и объяснения того, почему GAN на практике обучать труднее, чем обычную сеть. Это редкий случай в машинном обучении, когда за красивой и интуитивно понятной идеей стоит настолько же красивая теория — связь состязательной игры с минимизацией расстояния между распределениями вероятностей. Но с этой мощью приходит и ответственность: та же самая способность генератора делать синтетические данные неотличимыми от настоящих, которая приносит пользу в дизайне, синтезе обучающих данных и искусстве, лежит в основе deepfake-технологий, способных вводить людей в заблуждение и подрывать доверие к медиаконтенту в целом. Понимать, как устроена эта технология изнутри, — лучший способ использовать её осознанно и честно, а не просто эффектно. В следующем уроке ты увидишь, как во многом ту же задачу генерации решает принципиально другой, невраждующий подход — вариационные автоэнкодеры.

Понял тему? Закрепи в боте! 🚀

Попрактикуйся на задачах и получи персональные рекомендации от AI

💪 Начать тренировку
💬 Есть вопрос? Спроси бота!