01 / 33 · Основы
Что такое генеративное моделирование
Задача, которую решает курс, — научиться порождать новые данные по примерам из неизвестного распределения; почему это трудно; как компьютер превращает равномерные случайные числа в сэмплы любого распределения и как VAE, GAN, потоки и диффузионные модели подходят к задаче каждый по-своему.
На этой странице 10
Классификатор отвечает на вопрос об объекте: есть ли на картинке кошка? Генеративная модель порождает сам объект: новую картинку с кошкой, которой не было в обучающей выборке. В этой заметке мы точно сформулируем, что это значит, почему это трудно и как остальной курс подходит к задаче.
Задача: примеры из неизвестного распределения#
Будем считать набор данных независимыми сэмплами из некоторого распределения:
Для изображений — вектор значений пикселей, для текста — последовательность токенов, для звука — сигнал. Распределение неизвестно: никто не может записать плотность вероятности «фотографий кошек». У нас есть только выборка.
Цель — модель с параметрами , такая что и из которой можно сэмплировать новые объекты . Если приближение хорошее, новые сэмплы похожи на данные, но не копируют их.
Генеративное моделирование: от примеров к модели и обратно к примерам
То есть да: генеративное моделирование — это обучение сэмплированию из распределения, которого мы не знаем, через его приближение по примерам. Отсюда два вопроса. Что значит «приблизить», чтобы можно было обучать? И как компьютер вообще сэмплирует из распределения? Остальная часть заметки отвечает на оба.
Зачем нужна генеративная модель#
Порождение новых объектов — самое заметное применение, но не единственное:
- Генерация. Новые изображения, тексты, звук, молекулы, похожие на обучающие данные.
- Условная генерация. Модель порождает данные, соответствующие условию : картинку по текстовому запросу, недостающую часть фотографии, версию маленького изображения в высоком разрешении, перевод предложения.
- Оценка плотности. Если модель умеет вычислять , плотность — одна из возможных оценок аномальности: низкая плотность может указывать на необычный объект. Эта оценка зависит от представления (плотность меняется при смене единиц или переменных) и от того, насколько хорошо модель подогнана, а высокая плотность в точке — это ни высокая вероятность этой точки, ни гарантия, что объект выглядит типично; эти ограничения разобраны в заметке об обнаружении аномалий. Хорошие модели плотности — ещё и хорошие компрессоры, потому что оптимальный код тратит на около бит.
- Обучение представлений. Модели с латентной переменной (VAE) переводят данные в низкоразмерный код, направления которого часто имеют смысл, и учатся этому без разметки.
Что значит «приблизить»: дивергенция, которую минимизируем#
Чтобы обучать , нужно число, которое говорит, насколько она далека от . Стандартный выбор — расхождение Кульбака–Лейблера, KL-дивергенция (KL divergence) (Энтропия, кросс-энтропия и KL-дивергенция):
Первое слагаемое не зависит от . Поэтому минимизировать дивергенцию — значит максимизировать математическое ожидание логарифма правдоподобия данных под моделью, а если заменить ожидание средним по выборке, получится метод максимального правдоподобия:
Загвоздка в том, что для этого нужно знать как число. Для гибких моделей его часто невозможно вычислить, и семейства моделей в курсе отличаются главным образом тем, как они это обходят: точной формулой, нижней оценкой или вовсе другой целевой функцией.
Курс начинает с этой цели в простейшем случае: двумерная гауссиана подгоняется к гауссовским данным градиентным спуском по KL-дивергенции (Подгонка распределений с помощью KL).
Как компьютер вообще сэмплирует#
Модель генеративна, только если из неё можно сэмплировать. Компьютер не умеет напрямую порождать значения произвольного распределения: всё строится из одного примитива — равномерных случайных чисел — цепочкой преобразований. Весь раздел — одна идея:
Взять распределение, из которого сэмплировать легко, и преобразовать его сэмплы в сэмплы более сложного.
В цепочке пять звеньев, и на них держатся все дальнейшие модели, поэтому пройдём их медленно. Каждый шаг даёт идею, ключевую формулу в рамке, объяснение, почему она работает, и тот же шаг в нескольких строках кода. Кто уже знает сэмплирование обращением функции распределения и методом Бокса — Мюллера, может сразу перейти к разделу почему это трудно и к шести семействам.
Шаг 1: равномерные числа#
Компьютер не умеет подбрасывать монету. Процессор детерминирован: одна и та же программа на одних и тех же входных данных каждый раз делает одно и то же. Случайные числа в программах берутся из двух источников.
- Физический шум. Операционная система собирает события, которые никто не может предсказать, — точные моменты прерываний от клавиатуры, сети и диска, электрический шум внутри процессора — и смешивает их в общий пул, из которого программы читают (
/dev/urandomв Linux и macOS,os.urandomв Python). Эти биты действительно непредсказуемы, но вырабатываются медленно. Их используют для криптографических ключей и для выбора начальной точки, а не для миллионов чисел, нужных в вычислениях. - Генераторы псевдослучайных чисел. Короткое детерминированное правило превращает одно число в следующее. Генератор хранит внутреннее состояние и при каждом вызове обновляет его, , и возвращает число, вычисленное из него. Вся последовательность определяется начальным состоянием, которое задаётся начальным значением генератора (seed). Хороший генератор проходит большие опубликованные наборы статистических тестов; всех мыслимых тестов не проходит ни один генератор: тест, знающий правило, всегда предскажет следующее число.
Игрушечный генератор. Простейшее правило — линейный конгруэнтный генератор (linear congruential generator): умножить, прибавить, взять остаток (Knuth, 1997, §3.2.1). С 16 возможными состояниями:
Начнём с начального значения . Тогда , значит, . Дальше , значит, . Продолжая так же, получаем состояния
а деление на 16 даёт числа Через 16 шагов генератор возвращается к 7 и всё повторяется. По дороге он побывал в каждом состоянии ровно один раз, поэтому за цикл каждое из 16 значений встречается ровно один раз и ни одно не предпочтено. Именно это и значит «равномерное» для генератора.
Откуда берутся «случайные» числа
Что меняют настоящие генераторы. У игрушки два видимых недостатка. У неё всего 16 значений, и она повторяется через 16 шагов; к тому же каждое число связано с предыдущим простым правилом, поэтому пары соседних чисел ложатся на несколько прямых. Настоящие генераторы сохраняют идею состояния, обновляемого по фиксированному правилу, и устраняют оба недостатка.
- Огромное состояние. У PCG64, генератора NumPy по умолчанию, 128-битное состояние, и он повторяется лишь через чисел (O’Neill, 2014). У вихря Мерсенна (Mersenne Twister), генератора PyTorch на CPU, состояние в 19 937 бит и период (Matsumoto and Nishimura, 1998).
- Перемешивающий выход. Перед выдачей состояние перемешивается сдвигами, циклическими сдвигами и умножениями, поэтому между соседними числами не видно никакого узора. PCG64 проходит BigCrush — самую большую батарею тестов библиотеки TestU01, — а вихрь Мерсенна проваливает в ней только два теста линейной сложности (L’Ecuyer and Simard, 2007).
- Счётчики на GPU. Тысячам потоков GPU числа нужны одновременно, поэтому там PyTorch использует Philox. Он вычисляет -е число напрямую как перемешанную функцию от и начального значения, так что потокам не нужно ждать друг друга (Salmon et al., 2011).
От целого числа к числу из . Генератор возвращает 64-битные целые . У числа двойной точности 53-битная мантисса, поэтому библиотеки оставляют старшие 53 бита и масштабируют их:
Получается одно из равноотстоящих значений в , и все они равновероятны. С начальным значением 0 первый «сырой» выход NumPy равен , и формула даёт — ровно то первое число, которое возвращает np.random.default_rng(0).random().
Начальное значение. Начальное значение задаёт начальное состояние: библиотека разворачивает небольшое целое число в полное состояние. То же начальное значение при том же генераторе, тех же вызовах с теми же аргументами в том же порядке и той же сборке библиотеки даёт те же числа — поэтому эксперимент можно повторить. Это узкая гарантия. NumPy сохраняет сырые потоки своих генераторов от версии к версии, но построенные на них сэмплеры, например normal или multivariate_normal, могут меняться между выпусками или сборками (NumPy documentation), а целый запуск обучения на GPU добавляет собственные источники расхождений (обучение на практике). Разные начальные значения тоже не дают независимых потоков автоматически: в игрушечном генераторе выше любое начальное значение попадает на тот же цикл из 16 состояний, так что другое начальное значение лишь сдвигает ту же последовательность. Большие генераторы делают перекрытия очень маловероятными, а для параллельных потоков NumPy выводит дочерние начальные значения через SeedSequence.spawn (NumPy documentation).
В коде. Библиотека делает всё это одним вызовом:
import numpy as np
rng = np.random.default_rng(0) # начальное значение 0 задаёт всю последовательность
u = rng.random(100_000) # 100 000 чисел из U(0, 1)
print(u[:3].round(4)) # [0.637 0.2698 0.041 ]
print((u < 0.25).mean()) # 0.25015: четверть из них меньше 0.25
Шаг 2: любое одномерное распределение через обращение функции распределения#
Сначала дискретный пример. Пусть нужен исход , или с вероятностями , и . Разрежем отрезок на три куска ровно таких длин: , и . Затем возьмём и выдадим исход, в чей кусок оно попало. Равномерное число попадает в кусок с вероятностью, равной его длине, поэтому каждый исход выпадает с нужной вероятностью. Для , первого числа из шага 1, получается .
Сэмплирование дискретного распределения: разрезать отрезок [0, 1)
Точки разреза , и — это накопленные суммы вероятностей , и , то есть значения функции распределения (cumulative distribution function, CDF). Сэмплирование сводится к поиску: найти первый исход, накопленная сумма которого превышает .
Непрерывный случай — та же идея с бесконечным числом кусков. У непрерывной случайной величины функция распределения плавно растёт от 0 до 1: она говорит, сколько вероятности лежит левее . Будем считать её непрерывной и строго возрастающей, чтобы каждый уровень достигался ровно в одной точке. Трюк такой: возьмём равномерное и спросим, у какого значения накопилось ровно столько вероятности. Это значение и есть сэмпл:
Это метод обратного преобразования (inverse transform sampling; Devroye, 1986, §II.2; формально его разбирает заметка о функциях от случайных величин курса теории вероятностей).
Почему это работает. Зафиксируем любое число и вычислим функцию распределения выданного значения в три строки:
Первая строка — определение . Вторая верна, потому что возрастает: точка, где функция распределения достигает уровня , лежит не правее ровно тогда, когда не больше . Третья верна, потому что равномерное число не больше с вероятностью, равной длине отрезка . Значит, у функция распределения — ровно то распределение, которое мы хотели.
Разобранный пример: экспоненциальное распределение. У времени ожидания с интенсивностью функция распределения при . Решить уравнение относительно можно в три шага:
При :
| 0.1 | 0.5 | 0.9 | 0.99 | |
|---|---|---|---|---|
| 0.105 | 0.693 | 2.303 | 4.605 |
Половина равномерных чисел () попадает в короткий отрезок , а последний 1% разбросан по всему, что дальше 4.6. Форма экспоненциального распределения — много коротких ожиданий и длинный хвост долгих — получается сама собой.
В коде. Одна строка превращает равномерные числа в экспоненциальные (log1p(-u) точно вычисляет при малых ):
import numpy as np
u = np.random.default_rng(0).random(100_000)
x = -np.log1p(-u) # x = -ln(1 - u): экспоненциальное с интенсивностью 1
print(x.mean().round(3), np.median(x).round(3)) # 0.998 0.691 (теория: 1 и ln 2 = 0.693)
Нормальное распределение. Для стандартного нормального закона — это . Рецепт тот же, но у нет формулы в элементарных функциях. Библиотеки вычисляют её рациональными приближениями с точностью около 16 знаков, например алгоритмом Вичуры (Wichura, 1988). Рисунок показывает, почему рецепт даёт колокол: равные отрезки по попадают в короткие отрезки по там, где круто растёт, и в длинные там, где она пологая.
Из равномерного распределения в нормальное через обратную функцию распределения
Шаг 3: стандартное нормальное распределение через преобразование Бокса — Мюллера#
Численное обращение работает, но медленно. Бокс и Мюллер нашли точный рецепт, который превращает два независимых равномерных числа в два независимых стандартных нормальных (Box and Muller, 1958). Сначала строим расстояние и угол,
затем переходим к координатам:
Рецепт выглядит произвольным, но это шаг 2, применённый в полярных координатах. Будем считать случайной точкой на плоскости и описывать её углом и расстоянием до начала координат.
-
Угол равномерен. Совместная плотность двух независимых стандартных нормальных величин равна , где . Она зависит только от расстояния до начала координат, поэтому облако точек круговое. Все направления равновероятны: равномерно на и не зависит от . Угол даёт одно равномерное число: — это выше.
-
Квадрат расстояния экспоненциален. Вероятность, что точка окажется на расстоянии не больше от начала координат, — это интеграл плотности по этому кругу. В полярных координатах элемент площади равен , а интеграл по углу даёт и сокращает :
Это экспоненциальная функция распределения из разобранного примера с интенсивностью .
-
Сэмплируем квадрат расстояния по шагу 2. Разобранный пример даёт . Число тоже равномерно, поэтому можно взять само : и . (Само расстояние имеет распределение Рэлея.) В точной арифметике имеет вероятность ноль, но генератор чисел с плавающей точкой на может вернуть ровно 0, где логарифм равен ; поэтому код ниже подаёт в логарифм , которое лежит в .
-
Возвращаемся к координатам. и — это и есть формула в рамке.
Разобранный пример. Возьмём и .
- Расстояние: , значит, . Проверка: .
- Угол: , то есть , при этом и .
- Точка: .
Бокс — Мюллер по шагам: два равномерных числа → точка нормального облака
В коде. Рецепт даёт стандартное отклонение 1 по обеим координатам и нулевую корреляцию между ними. На практике вызывают нормальный генератор библиотеки, который использует более быстрый вариант того же принципа (в NumPy — метод зиккурата):
import numpy as np
rng = np.random.default_rng(1)
u1, u2 = 1 - rng.random(100_000), rng.random(100_000) # 1 - U лежит в (0, 1]: логарифм не встретит 0
r, phi = np.sqrt(-2 * np.log(u1)), 2 * np.pi * u2
z1, z2 = r * np.cos(phi), r * np.sin(phi)
print(z1.std().round(3), z2.std().round(3), np.corrcoef(z1, z2)[0, 1].round(3)) # 0.998 1.002 -0.002
print((np.abs(z1) < 1).mean().round(3)) # 0.685: привычные 68% в пределах одного стандартного отклонения
Шаг 4: любая гауссиана через аффинное преобразование#
Шаги 1–3 дают стандартный нормальный шум — круглое облако с центром в нуле. Гауссиана с любым средним и ковариацией получается из него сдвигом и линейным отображением. Возьмём любую матрицу с ; тогда
Среднее сдвигает облако, а матрица растягивает, поворачивает и связывает его координаты. Следующая заметка, Многомерные гауссовские распределения, показывает, как выбрать и почему это работает.
В коде. Множитель Холецкого — один из допустимых вариантов :
import numpy as np
mu = np.array([1.0, 2.0])
Sigma = np.array([[2.0, 0.8], [0.8, 1.0]])
A = np.linalg.cholesky(Sigma) # нижнетреугольная, A @ A.T == Sigma
z = np.random.default_rng(2).standard_normal((100_000, 2))
x = mu + z @ A.T
print(A.round(3).ravel()) # [1.414 0. 0.566 0.825]
print(x.mean(axis=0).round(2), np.cov(x.T).round(2).ravel()) # [1. 2.] [2. 0.8 0.8 1. ]
Шаг 5: всё остальное через обучаемое нелинейное отображение#
Гауссианы — это всего лишь эллипсоиды, а у изображений, речи и текста распределения гораздо сложнее эллипса. Чтобы получить форму реальных данных, шум пропускают через нелинейное отображение :
Распределение называют образом (pushforward) распределения шума при отображении . В генеративной модели — нейронная сеть с параметрами , и обучение подбирает так, чтобы образ совпал с данными.
Сэмплирование как цепочка преобразований шума
Это центральная картина курса: генеративная модель — это простой шум плюс обучаемое преобразование. У прямого генератора сэмплировать легко по построению: берём шум и один раз применяем сеть; некоторые семейства ниже платят за каждый сэмпл больше, например цепочкой из многих вызовов сети. Трудно в обратную сторону: подобрать , для чего нужен способ сравнить образ шума с данными.
Если обратимо и гладко, у плотности даже есть формула — замена переменных (change of variables)
с которой начинаются нормализующие потоки. Для произвольной сети такой формулы нет, и именно здесь семейства моделей расходятся.
Почему это трудно#
Обращение функции распределения решает задачу сэмплирования в одномерном случае, а у гауссианы есть формула в любой размерности. У реальных данных нет ни того, ни другого.
- Размерность. Цветное изображение — это чисел. Плотность на таком пространстве нельзя задать таблицей, и общей практичной процедуры сэмплирования обращением нет. Совместная функция распределения существует, но единственного обращения у неё нет; сэмплирование по координатам потребовало бы всех условных функций распределения, а их-то мы и не знаем.
- Концентрация. Естественные изображения занимают крошечную часть этого пространства — окрестность поверхности гораздо меньшей размерности (многообразия): почти любой случайный массив пикселей — шум. Модель должна поместить почти всю вероятность на это тонкое множество.
- Нормировка. Гибкая функция становится плотностью только после деления на — интеграл по всему пространству, который для нейронных сетей невычислим. Без него правдоподобие в методе максимального правдоподобия посчитать нельзя.
Шесть обходных путей#
Каждое семейство в курсе сохраняет картину «шум плюс преобразование» и отличается тем, чем жертвует, чтобы обучение стало возможным.
Шесть семейств: шум превращается в данные, отличается то, что можно посчитать
| Семейство | Как получить сэмпл | Что обучается | Что можно посчитать | Заметки курса |
|---|---|---|---|---|
| Вариационный автоэнкодер (VAE) | , затем декодер | декодер и энкодер | нижнюю оценку , ELBO | VAE |
| Генеративно-состязательная сеть (GAN) | , затем за один проход | генератор , обучаемый против дискриминатора | плотности нет, только сэмплы | GAN, WGAN, масштабирование, StyleGAN, α-GAN, CycleGAN, StarGAN, за пределами CycleGAN |
| Нормализующий поток (normalizing flow) | , затем цепочка обратимых отображений | обратимое с вычислимым якобианом | точное | Потоки, архитектуры |
| Модель, основанная на энергии (energy-based model, EBM) | начать с шума и вести шумную марковскую цепь (Ланжевена), исследующую ; шагов много, перемешивание может быть медленным | энергия , причём | с точностью до неизвестной константы ; сэмплы — через MCMC | Сэмплеры MCMC, EBM |
| Диффузионная модель (DDPM) | начать с шума и шаг за шагом очищать его до | сеть, обращающая один малый шаг зашумления | нижнюю оценку | Прямой процесс, обучение, score matching, латентная диффузия, редактирование перетаскиванием |
| Авторегрессионная модель | по одному значению или токену, каждое при уже полученных | условные распределения , каждое — softmax по значениям или токенам | точное | PixelRNN и PixelCNN, трансформеры |
Последняя заметка переносит эти инструменты с изображений на звук: поток превращает текст в речь, а авторегрессионный трансформер над токенами нейросетевого аудиокодека генерирует музыку.
Поскольку не каждая модель даёт правдоподобие, а правдоподобие не всегда отражает, насколько хороши сэмплы на вид, в курсе есть и метрики, которые считаются только по сэмплам: Inception Score, FID, LPIPS, точность и полнота и оценка людьми.
План курса#
- Основы. Гауссианы и сэмплирование, энтропия и KL-дивергенция, подгонка распределения минимизацией KL, база для обучения сетей (батч-нормализация, градиенты, свёртки) и автоэнкодеры.
- Модели с латентными переменными. VAE: латентный код, нижняя оценка правдоподобия и трюк репараметризации; β-VAE и дискретные коды VQ-VAE.
- Состязательные модели. GAN, дивергенция Йенсена–Шеннона, которую они неявно минимизируют, и GAN Вассерштейна; масштабирование GAN (ProGAN, SAGAN, BigGAN) и управление ими (StyleGAN); автоэнкодеры, обучаемые состязательными функциями потерь (α-GAN); перевод изображений без парных примеров (CycleGAN), между многими доменами (StarGAN) и дальше (MUNIT, CUT, SPADE, сверхразрешение).
- Оценка качества. IS, FID и LPIPS, точность и полнота, оценка людьми.
- Точное правдоподобие. Нормализующие потоки и архитектуры, в которых якобиан дёшев.
- Модели, основанные на энергии. Сэмплеры для плотностей, известных с точностью до константы (MALA, гамильтонов Монте-Карло, NUTS), и модели, основанные на энергии, обучаемые контрастивной дивергенцией.
- Диффузия. Прямой процесс зашумления и целевая функция обучения DDPM, score matching и динамика Ланжевена, латентная диффузия и Stable Diffusion, редактирование перетаскиванием точек с DragGAN и DragDiffusion.
- Авторегрессионные модели. Цепное правило как модель, PixelRNN и PixelCNN, трансформеры над пикселями и токенами изображений.
- Звук. Синтез речи по тексту с помощью потока и музыка от авторегрессионного трансформера над токенами нейросетевого кодека.
Что нужно запомнить#
- Данные — выборка из неизвестного ; генеративная модель — это , из которого можно сэмплировать и которое подогнано так, что .
- Минимизация — это метод максимального правдоподобия, и ему нужно как число.
- Любое сэмплирование начинается с равномерных чисел: обращение функции распределения в одномерном случае, Бокс — Мюллер для нормальных величин, аффинное отображение для любой гауссианы, обучаемое нелинейное отображение для всего остального.
- Каждая модель курса — шум плюс обучаемое преобразование; семейства отличаются тем, что умеют посчитать: оценку снизу (VAE, диффузия), ничего (GAN), правдоподобие с точностью до константы (модели, основанные на энергии) или точное правдоподобие (потоки, авторегрессионные модели).
Дальше#
Первая ступень лестницы, на которой строится курс, — гауссиана: как её ковариация задаёт форму данных и как аффинное отображение превращает стандартный шум в любую гауссиану. Это заметка Многомерные гауссовские распределения.
Литература#
- I. Goodfellow, Y. Bengio, A. Courville. Deep Learning. MIT Press, 2016. Гл. 20: глубокие генеративные модели.
- K. P. Murphy. Probabilistic Machine Learning: Advanced Topics. MIT Press, 2023. Часть IV: генерация, с обзором семейств моделей.
- C. M. Bishop. Pattern Recognition and Machine Learning. Springer, 2006. §11.1: базовые алгоритмы сэмплирования, включая метод обратного преобразования и Бокса — Мюллера.
- L. Devroye. Non-Uniform Random Variate Generation. Springer, 1986.
- G. E. P. Box, M. E. Muller. A note on the generation of random normal deviates. Annals of Mathematical Statistics, 29(2), 1958.
- D. E. Knuth. The Art of Computer Programming, vol. 2: Seminumerical Algorithms, 3rd ed. Addison-Wesley, 1997. §3.2.1: линейные конгруэнтные генераторы и условия, при которых они обходят все состояния.
- P. L’Ecuyer, R. Simard. TestU01: a C library for empirical testing of random number generators. ACM Transactions on Mathematical Software, 33(4), 2007.
- M. J. Wichura. Algorithm AS 241: the percentage points of the normal distribution. Applied Statistics, 37(3), 1988.
- M. Matsumoto, T. Nishimura. Mersenne Twister: a 623-dimensionally equidistributed uniform pseudo-random number generator. ACM Transactions on Modeling and Computer Simulation, 8(1), 1998.
- M. E. O’Neill. PCG: a family of simple fast space-efficient statistically good algorithms for random number generation. Technical report HMC-CS-2014-0905, Harvey Mudd College, 2014.
- J. K. Salmon, M. A. Moraes, R. O. Dror, D. E. Shaw. Parallel random numbers: as easy as 1, 2, 3. SC ‘11, 2011. Генератор Philox.
- NumPy developers. Random sampling: compatibility policy and parallel random number generation. NumPy documentation. Что начальное значение воспроизводит, а что нет;
SeedSequence.spawnдля параллельных потоков.