Variational Autoencoders 🌀
Сегодняшняя тема — вариационные автоэнкодеры (Variational Autoencoders, дальше по тексту просто VAE) — ещё одно семейство генеративных моделей, но устроенное принципиально иначе, чем состязательная игра генератора и дискриминатора из урока 345. Вместо того чтобы обучать сеть обманывать критика, VAE решает задачу генерации через идею сжатия и восстановления: научи сеть аккуратно упаковывать данные в компактное представление и разворачивать их обратно, а потом заставь это компактное представление быть устроенным настолько аккуратно, что из него можно черпать совершенно новые, никогда не виденные примеры.
Здесь стоит сразу вспомнить урок 323 про метод главных компонент (Principal Component Analysis, PCA) — вероятно, самый прямой математический родственник VAE во всём курсе. PCA сжимает данные, находя несколько направлений максимальной дисперсии и проецируя точки на них; десятки признаков превращаются в несколько чисел почти без потери информации. VAE решает ровно ту же задачу — сжать многомерные данные в компактный код, — но делает это нелинейно, через глубокую нейросеть, и с дополнительной вероятностной надстройкой, которая PCA попросту не нужна. Если PCA — это линейка, вычерчивающая прямую линию сквозь облако точек, то VAE — это гибкая нелинейная поверхность, которая умеет изгибаться вдоль сложных, искривлённых закономерностей данных, вроде многообразия человеческих лиц или рукописных цифр.
Между PCA и VAE стоит ещё один, куда более простой персонаж — обычный (не вариационный) автоэнкодер. Это нейросеть из двух частей: энкодер сжимает вход в низкоразмерный код, декодер восстанавливает из этого кода исходный вход, а обучение сводится к минимизации ошибки восстановления — ровно та же цель, что и у PCA, только достигаемая градиентным спуском по весам нелинейной сети, а не через разложение ковариационной матрицы на собственные векторы. Обычный автоэнкодер отлично сжимает и восстанавливает, но, как будет подробно показано в первом разделе этого урока, для генерации совершенно новых данных он практически бесполезен — его скрытое пространство (латентное пространство, latent space) устроено хаотично, и случайная точка в нём почти никогда не декодируется во что-то осмысленное.
Именно эту проблему решает вариационная надстройка, придуманная в 2013 году и давшая имя всему семейству моделей. Ты увидишь, как замена одной точки в латентном пространстве на целое распределение вероятностей превращает хаотичный, дырявый скрытый код в гладкое, непрерывное пространство, из которого можно осмысленно сэмплировать; как техническая проблема — недифференцируемость случайного сэмплирования — решается изящным приёмом репараметризации; и как функция потерь VAE, объединяющая ошибку восстановления с KL-дивергенцией, заставляет всю эту конструкцию работать согласованно. По пути ты увидишь, как VAE применяется для обнаружения аномалий и почему современные модели генерации изображений вроде Stable Diffusion и DALL-E во многом наследуют идеи, впервые оформленные именно здесь.
История
Вариационные автоэнкодеры появились в статье «Auto-Encoding Variational Bayes» («Автокодирование вариационного Байеса»), опубликованной в конце 2013 года Дидериком Кингмой (Diederik P. Kingma) и Максом Веллингом (Max Welling) из Амстердамского университета. Это тот самый Дидерик Кингма, чьё имя ты уже встречал в уроке 293 — он же несколькими годами позже стал соавтором Adam, самого популярного адаптивного метода оптимизации в глубоком обучении. Название статьи указывает на её реальное происхождение: авторы не изобретали генеративные модели с нуля, а взяли классическую задачу байесовского вывода — оценить апостериорное распределение скрытых переменных по наблюдаемым данным, идею, которая восходит ещё к теореме Байеса из урока 232, — и показали, как приблизить эту задачу нейросетью, обучаемой обычным градиентным спуском.
Почти одновременно и независимо к очень похожему результату пришла другая группа — Данило Резенде (Danilo Rezende), Шакир Мохамед (Shakir Mohamed) и Даан Вирстра (Daan Wierstra) из DeepMind, опубликовавшие в 2014 году статью «Stochastic Backpropagation and Approximate Inference in Deep Generative Models» («Стохастическое обратное распространение и приближённый вывод в глубоких генеративных моделях»). Обе команды практически синхронно открыли один и тот же центральный технический приём — способ пропустить градиент через операцию случайного сэмплирования, — что в истории науки случается нередко, когда область созрела для конкретного следующего шага: вариационный вывод (variational inference) в статистике разрабатывался десятилетиями, а глубокие нейросети с обратным распространением к 2013 году уже стали рабочим инструментом, и соединение этих двух линий было почти неизбежным.
Первые несколько лет VAE воспринимались в первую очередь как элегантная теоретическая конструкция, органично связывающая байесовскую статистику и глубокое обучение, — при этом сгенерированные VAE изображения на практике заметно уступали по резкости результатам GAN, появившимся годом позже (урок 345). Тем не менее именно идеи VAE — вероятностное латентное пространство, регуляризация через KL-дивергенцию, идея постепенного превращения простого распределения в сложное — оказались на удивление живучими: сегодня они прослеживаются в архитектуре VQ-VAE (используется как компонент во многих современных генеративных системах для изображений и звука) и, что особенно важно для практика, в самой логике диффузионных моделей (diffusion models) — технологии, лежащей в основе Stable Diffusion, DALL-E и большинства современных генераторов изображений по текстовому описанию, к чему этот урок ещё вернётся в разделе про связь с другими темами курса.
Обычный автоэнкодер и его ограничения
Интуиция
Возьми нейросеть и раздели её на две половины. Первая половина, энкодер (encoder), получает на вход исходные данные — скажем, изображение $28\times28$ пикселей, то есть вектор из $784$ чисел, — и через несколько слоёв сжимает его до маленького вектора из, например, $32$ чисел. Вторая половина, декодер (decoder), получает эти $32$ числа и через симметричные слои пытается восстановить исходные $784$ числа как можно точнее. Обучается вся конструкция целиком, из конца в конец, единственной целью: сделать восстановленное изображение как можно более похожим на исходное. Ни энкодеру, ни декодеру никто не объясняет, что именно означают эти $32$ промежуточных числа, — сеть сама, в процессе обучения, находит компактное представление, из которого декодер способен восстановить исходный вход с минимальной ошибкой.
Это в точности та же цель, что и у PCA из урока 323: сжать данные в низкоразмерное представление с минимальной потерей информации. Более того, между ними есть строгая математическая связь: если и энкодер, и декодер сделать линейными (без нелинейных функций активации между слоями), а функцию потерь взять как среднеквадратичную ошибку, то оптимальное решение такой задачи обучения охватывает ровно то же самое подпространство, что и первые $d$ главных компонент, найденные через разложение ковариационной матрицы на собственные векторы (результат, строго доказанный Эрве Бурларом и Ивом Кампом в 1988 году, а также Пьером Балди и Куртом Хорником в 1989-м). Обычный автоэнкодер с нелинейными слоями — это прямое нелинейное обобщение той же самой задачи: вместо того чтобы искать прямую или плоскость, наилучшим образом приближающую облако точек (задача Пирсона, урок 323), сеть ищет произвольно изогнутую, нелинейную поверхность меньшей размерности, на которую данные укладываются с минимальной ошибкой.
Формула
Автоэнкодер. Энкодер — функция $f_\phi: \mathbb{R}^n \to \mathbb{R}^d$ с обучаемыми параметрами $\phi$, где $d \ll n$ (латентная размерность заметно меньше размерности входа). Декодер — функция $g_\theta: \mathbb{R}^d \to \mathbb{R}^n$ с параметрами $\theta$. Код (латентное представление) точки $x$: $z = f_\phi(x)$. Реконструкция: $\hat{x} = g_\theta(z) = g_\theta(f_\phi(x))$. Функция потерь (реконструкции):
$$\mathcal{L}(\phi,\theta) = \mathbb{E}_{x}\left[\, \|x - g_\theta(f_\phi(x))\|^2 \,\right].$$Оба $\phi$ и $\theta$ обучаются одновременно градиентным спуском, минимизируя эту функцию по всей обучающей выборке.
Разбор примеров
Пример 1 (числовой расчёт ошибки реконструкции). Пусть на вход подан вектор признаков клиента (нормированные время сессии и число просмотренных страниц) $x = (4{,}0;\ 6{,}0)$, а декодер после сжатия и восстановления выдал $\hat{x} = (3{,}5;\ 6{,}2)$. Квадраты отклонений по координатам: $(4{,}0-3{,}5)^2 = 0{,}25$ и $(6{,}0-6{,}2)^2 = 0{,}04$. Среднеквадратичная ошибка на этом примере: $(0{,}25+0{,}04)/2 = 0{,}145$. Именно эту величину, усреднённую по всем примерам обучающей выборки, автоэнкодер и минимизирует — чем ближе восстановленный вектор к исходному, тем меньше вклад этого примера в общую функцию потерь.
Пример 2 (архитектура и коэффициент сжатия). Автоэнкодер для изображений MNIST устроен так: энкодер $784 \to 256 \to 128 \to 32$, декодер зеркально $32 \to 128 \to 256 \to 784$ с сигмоидой на выходе (пиксели нормированы в $[0,1]$). Коэффициент сжатия — отношение размерности входа к размерности узкого места (латентного пространства): $784/32 \approx 24{,}5$. Иными словами, каждое изображение цифры описывается $32$ числами вместо $784$ пикселей, почти в $25$ раз компактнее, а декодер обучен восстанавливать из этих $32$ чисел изображение, визуально почти неотличимое от исходного, — совершенно так же, как в уроке 323 несколько первых главных компонент почти полностью восстанавливали исходные признаки квартир или клиентов.
Пример 3 (обнаружение аномалий через ошибку реконструкции). Автоэнкодер обучен только на транзакциях обычных, легитимных покупок по кредитной карте — сеть видит миллионы примеров нормального поведения и учится сжимать и восстанавливать именно такие паттерны с маленькой ошибкой. Когда через сеть пропускают мошенническую транзакцию с необычной комбинацией признаков (сумма, время, местоположение, частота операций), которую сеть никогда раньше не видела в такой конфигурации, декодер восстанавливает её заметно хуже: ошибка реконструкции для обычной транзакции — скажем, $0{,}02$, а для подозрительной — $0{,}87$, на порядок выше. Задав порог (например, $0{,}3$), можно автоматически помечать транзакции с большой ошибкой реконструкции как потенциально аномальные — это один из самых практичных сценариев применения автоэнкодеров в индустрии, работающий даже без вариационной надстройки.
Почему это важно
Обычный автоэнкодер — это не игрушечная модель для одного конкретного применения, а рабочая лошадка снижения размерности, предобработки признаков и обнаружения аномалий, причём его нелинейность даёт ему решающее преимущество перед PCA всякий раз, когда истинная структура данных искривлена, а не укладывается в плоскость. Но у этой конструкции есть фундаментальный изъян, из-за которого её нельзя напрямую использовать для генерации новых данных: ничто в функции потерь автоэнкодера не заставляет латентное пространство быть устроенным связно и предсказуемо. Энкодер вправе разместить коды похожих изображений где угодно, лишь бы декодер потом успешно их восстанавливал, — а значит между областями, которые сеть видела при обучении, могут оставаться обширные «дыры», не соответствующие ничему осмысленному. Именно эта проблема — прямой мост к следующему разделу.
Идея VAE: вероятностное латентное пространство
Интуиция
Возьми обученный обычный автоэнкодер и попробуй его «обмануть» — сгенерировать что-то новое, взяв случайную точку в его латентном пространстве и пропустив через декодер. На практике результат почти всегда оказывается бессмысленным шумом. Причина в том, что энкодер видел лишь конечное число обучающих примеров и разместил их коды где-то в $32$-мерном (или сколько угодно мерном) пространстве без всякой заботы о том, что происходит в промежутках между ними, — латентное пространство обычного автоэнкодера напоминает архипелаг из отдельных островков-точек, а не сплошной, равномерно заселённый континент. Декодер выучил, как обрабатывать именно эти островки, и понятия не имеет, что делать со случайной точкой в открытом море между ними.
VAE решает эту проблему смелым сдвигом постановки задачи: энкодер перестаёт выдавать одну-единственную точку кода для каждого входа. Вместо этого для каждого $x$ он выдаёт параметры целого распределения вероятностей в латентном пространстве — как правило, нормального распределения, заданного вектором средних $\mu$ и вектором дисперсий $\sigma^2$ (по одному числу на каждую латентную координату, если предполагать координаты независимыми). Дальше из этого распределения случайно сэмплируется конкретная точка $z$, и именно она подаётся на вход декодеру. На следующей эпохе обучения тот же самый вход $x$ снова даст те же $\mu$ и $\sigma^2$, но сэмплирование выдаст уже слегка другую точку $z$ — а значит декодер вынужден научиться правильно восстанавливать исходный $x$ не из одной конкретной точки, а из целого облака точек вокруг $\mu$. Это и есть ключевой сдвиг: сеть больше не может расставить коды по отдельным изолированным островкам, потому что каждый код теперь размазан облаком случайного шума, и соседние облака неизбежно начинают перекрываться — латентное пространство становится сплошным, связным континентом вместо архипелага.
Формула
Вероятностный энкодер VAE. Энкодер приближает апостериорное распределение $q_\phi(z \mid x)$, которое обычно параметризуют диагональным нормальным распределением:
$$q_\phi(z \mid x) = \mathcal{N}\big(z;\ \mu_\phi(x),\ \operatorname{diag}(\sigma_\phi^2(x))\big),$$где $\mu_\phi(x)$ и $\sigma_\phi^2(x)$ — векторы, вычисляемые сетью-энкодером по входу $x$. Код для декодера сэмплируется: $z \sim q_\phi(z \mid x)$. После обучения генерация нового объекта делается без всякого энкодера: берётся случайная точка из априорного распределения $z \sim \mathcal{N}(0, I)$ и подаётся сразу на декодер $g_\theta(z)$.
Разбор примеров
Пример 1 (числовой пример параметров распределения). Энкодер обрабатывает фотографию лица и выдаёт для одной из латентных координат $\mu = 2{,}0$, $\sigma^2 = 0{,}5$ (то есть $\sigma \approx 0{,}707$). Это означает, что код данного конкретного лица по этой координате — не строго число $2{,}0$, а случайная величина, которая с высокой вероятностью попадёт в диапазон примерно от $2{,}0 - 2\cdot0{,}707 \approx 0{,}59$ до $2{,}0 + 2\cdot0{,}707 \approx 3{,}41$ (правило двух сигм для нормального распределения). Любое другое лицо со схожими средними $\mu$ и достаточно широкими $\sigma$ будет давать облако сэмплов, пересекающееся с этим диапазоном, — а значит декодер обязан научиться выдавать похожие, правдоподобные лица для любой точки из пересечения, а не только для одной запомненной точки.
Пример 2 (плавная интерполяция вместо провала в бессмыслицу). Возьми две обученные точки латентного пространства VAE — код лица с широкой улыбкой $z_1$ и код того же лица с нейтральным выражением $z_2$ — и декодируй серию промежуточных точек $z_t = (1-t)z_1 + t\,z_2$ для $t$ от $0$ до $1$. У обученного VAE эта последовательность декодируется в плавную, постепенную анимацию перехода от улыбки к нейтральному лицу: каждая промежуточная точка попадает в область, которую при обучении «накрывали» облака сэмплирования множества похожих примеров, и декодер уверенно знает, что с ней делать. У обычного автоэнкодера из предыдущего раздела та же самая процедура почти всегда даёт на промежуточных шагах визуальный шум или артефакты — интерполяция проходит через незаселённые «дыры» латентного пространства, которых VAE своей случайной размазкой попросту не оставляет.
Пример 3 (обнаружение аномалий через несовпадение распределений). Ровно тот же сценарий обнаружения мошенничества из предыдущего раздела работает и для VAE, причём даже надёжнее: для типичной, часто встречающейся транзакции энкодер выдаёт уверенное, узкое распределение (маленькая $\sigma$, потому что сеть многократно видела похожие примеры и «знает», как их правильно закодировать), а для необычной транзакции — либо распределение с большой дисперсией (сеть не уверена, куда её поместить), либо смещённые $\mu$, далёкие от плотно заселённых областей латентного пространства. И то и другое обычно приводит к повышенной ошибке реконструкции после сэмплирования и декодирования, а саму степень несовпадения распределения конкретного примера со стандартным нормальным распределением (тем самым $\mathcal{N}(0,I)$, к которому будет явно применена регуляризация в разделе про функцию потерь) можно использовать как ещё один, дополнительный сигнал аномальности.
Почему это важно
Переход от точки к распределению — это не техническая прихоть, а единственный способ заставить нейросеть учитывать не только «как закодировать конкретный пример», но и «что должно происходить в окрестности этого кода». Именно эта окрестность и делает латентное пространство VAE пригодным для генерации: обучив модель, можно взять произвольную точку из простого, хорошо изученного распределения $\mathcal{N}(0,I)$ и с высокой вероятностью получить от декодера правдоподобный, ранее не существовавший объект — цифру, лицо, молекулу, — потому что почти вся область этого простого распределения при обучении была тем или иным образом «накрыта» облаками сэмплирования реальных примеров.
Приём репараметризации (reparametrization trick)
Интуиция
Идея из предыдущего раздела красива, но натыкается на серьёзное техническое препятствие. Нейросети обучаются через обратное распространение ошибки (backpropagation, урок 330) — метод, который требует вычислять производную выхода сети по каждому обучаемому параметру, продвигаясь по цепочке вычислений в обратном направлении. Но операция «сэмплировать случайную точку $z$ из распределения $\mathcal{N}(\mu, \sigma^2)$» — это не гладкая детерминированная функция от $\mu$ и $\sigma$, а стохастический узел вычислительного графа: при одних и тех же $\mu$, $\sigma$ результат сэмплирования каждый раз разный, и у самой операции сэмплирования попросту нет производной по её параметрам в привычном смысле — нельзя спросить «как изменится это конкретное случайное число, если чуть-чуть сдвинуть $\mu$», потому что оно не является функцией от $\mu$ каким-либо дифференцируемым образом.
Решение, независимо предложенное авторами VAE и группой Резенде в 2013–2014 годах, элегантно обходит эту проблему, не решая её напрямую: вместо того чтобы сэмплировать $z$ прямо из $\mathcal{N}(\mu, \sigma^2)$, можно сначала сэмплировать вспомогательную случайную величину $\varepsilon$ из фиксированного, не зависящего ни от каких параметров сети стандартного нормального распределения $\mathcal{N}(0, I)$, а затем получить нужное $z$ простым детерминированным преобразованием $\varepsilon$ с использованием $\mu$ и $\sigma$. Вся случайность оказывается «вынесена наружу», в независимый источник шума $\varepsilon$, а связь между параметрами сети и итоговым $z$ становится обычной, полностью дифференцируемой арифметической формулой — через неё градиент течёт без каких-либо проблем.
Формула
Приём репараметризации. Вместо прямого сэмплирования $z \sim \mathcal{N}(\mu_\phi(x), \sigma_\phi^2(x))$ вычисляют:
$$z = \mu_\phi(x) + \sigma_\phi(x) \odot \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, I),$$где $\odot$ — покоординатное умножение, а $\varepsilon$ сэмплируется независимо от параметров $\phi$ на каждом шаге обучения. Поскольку $\varepsilon$ трактуется как константа при вычислении производных, частные производные вычисляются напрямую:
$$\frac{\partial z}{\partial \mu} = 1, \qquad \frac{\partial z}{\partial \sigma} = \varepsilon,$$и градиент функции потерь свободно проходит через $z$ обратно к $\mu_\phi(x)$ и $\sigma_\phi(x)$, а значит и ко всем весам энкодера.
Разбор примеров
Пример 1 (числовой расчёт). Энкодер выдал для одной латентной координаты $\mu = 3{,}0$, $\sigma = 0{,}5$. На конкретном шаге обучения сэмплировано $\varepsilon = 0{,}8$ из $\mathcal{N}(0,1)$. Тогда $z = \mu + \sigma\cdot\varepsilon = 3{,}0 + 0{,}5\cdot0{,}8 = 3{,}4$. Если при обратном проходе градиент функции потерь по $z$ оказался равен, скажем, $g = -0{,}2$, то по цепному правилу градиент по $\mu$ равен $g \cdot \partial z/\partial\mu = -0{,}2\cdot1 = -0{,}2$, а градиент по $\sigma$ равен $g \cdot \partial z/\partial\sigma = -0{,}2\cdot0{,}8 = -0{,}16$ — оба градиента вычислены обычным способом, без каких-либо специальных приёмов для случайных узлов.
Пример 2 (почему логарифм дисперсии, а не сама дисперсия). На практике энкодер почти всегда обучают предсказывать не $\sigma^2$ напрямую, а $\log \sigma^2$ (логарифм дисперсии, log-variance), после чего дисперсию восстанавливают как $\sigma = \exp(0{,}5 \cdot \log\sigma^2)$. Если, например, сеть выдала $\log\sigma^2 = 0{,}4$, то $\sigma = \exp(0{,}5\cdot0{,}4) = \exp(0{,}2) \approx 1{,}221$. Смысл этого технического манёвра прост: $\sigma^2$ по определению обязана быть неотрицательной, а сырой выход линейного слоя нейросети может быть каким угодно вещественным числом, включая отрицательные, — предсказывая логарифм, сеть может свободно выдавать любое число, а операция $\exp(\cdot)$ автоматически гарантирует положительность итоговой дисперсии без необходимости как-то ограничивать веса сети или добавлять специальные функции активации.
Пример 3 (код на PyTorch).
import torch
def reparameterize(mu, logvar):
std = torch.exp(0.5 * logvar) # sigma = exp(0.5 * log(sigma^2))
eps = torch.randn_like(std) # epsilon ~ N(0, I), не зависит от параметров сети
z = mu + std * eps # z = mu + sigma * epsilon
return z
# encoder(x) возвращает mu и logvar одновременно
mu, logvar = encoder(x)
z = reparameterize(mu, logvar)
x_reconstructed = decoder(z)
Строка eps = torch.randn_like(std) — это ровно тот момент, где вся случайность «вынесена наружу» и не зависит от обучаемых весов; PyTorch не пытается (и не должен) вычислять градиент по eps, а строки mu + std * eps — обычные дифференцируемые арифметические операции, через которые autograd спокойно прокладывает путь для обратного распространения к параметрам энкодера.
Почему это важно
Без приёма репараметризации вся идея VAE осталась бы красивой, но практически неработающей теорией: без него энкодер физически невозможно было бы обучить сквозным градиентным спуском совместно с декодером, потому что стохастический узел сэмплирования разрывал бы цепочку производных ровно в том месте, где встречаются энкодер и декодер. Стоит запомнить и более общий урок этого приёма: он не привязан исключительно к нормальному распределению и к VAE — сама идея «разложить случайный узел на детерминированную функцию от параметров плюс независимый источник шума» стала стандартным инструментом всюду, где нужно обучать сеть, содержащую внутри себя случайность, и в следующем уроке про обучение с подкреплением (урок 347) ты встретишь очень похожую по духу проблему стохастических действий агента — только решаемую там уже другими методами, поскольку не любое действие агента допускает столь же гладкую репараметризацию.
Функция потерь VAE: реконструкция и KL-дивергенция
Интуиция
Теперь, когда энкодер выдаёт распределение, а не точку, и градиент умеет проходить через сэмплирование, остаётся собрать всё это в единую функцию потерь. Обучение VAE строго решает конкретную статистическую задачу — максимизировать вероятность того, что модель вообще способна породить наблюдаемые данные, — но эта вероятность напрямую невычислима, потому что требует интегрирования по всем возможным значениям $z$. Вместо этого VAE максимизирует нижнюю оценку этой вероятности, называемую ELBO (evidence lower bound, «нижняя граница на свидетельство»), которая раскладывается ровно на два интуитивно понятных слагаемых: насколько хорошо декодер восстанавливает данные по сэмплированному $z$, и насколько распределение $q_\phi(z\mid x)$, которое выдаёт энкодер, близко к простому, заранее выбранному априорному распределению $\mathcal{N}(0, I)$.
Первое слагаемое — уже знакомая ошибка реконструкции: чем точнее декодер восстанавливает $x$ по $z$, тем выше правдоподобие. Второе слагаемое — это именно тот регуляризатор, который заставляет облака сэмплирования разных примеров перекрываться и заполнять всё латентное пространство, а не разбегаться по своим изолированным уголкам: KL-дивергенция (Kullback-Leibler divergence) измеряет, насколько распределение $q_\phi(z\mid x)$ отличается от стандартного нормального распределения, и штрафует энкодер за отклонение от него. Без этого штрафа энкодер, стремясь минимизировать только ошибку реконструкции, естественным образом стал бы делать $\sigma$ как можно меньше (сводя задачу почти к детерминированному кодированию точкой, как в обычном автоэнкодере) — KL-член специально этому противодействует, вынуждая $\sigma$ оставаться разумно большой и центры $\mu$ разных примеров не разбегаться слишком далеко друг от друга.
Формула
Функция потерь VAE (отрицательный ELBO). Для одного примера $x$:
$$\mathcal{L}_{\text{VAE}}(\phi,\theta;x) = \underbrace{-\,\mathbb{E}_{z \sim q_\phi(z\mid x)}\big[\log p_\theta(x\mid z)\big]}_{\text{ошибка реконструкции}} \; + \; \underbrace{D_{\mathrm{KL}}\big(q_\phi(z\mid x)\ \|\ p(z)\big)}_{\text{регуляризация к }\mathcal{N}(0,I)},$$где $p(z) = \mathcal{N}(0, I)$ — априорное распределение. Для диагонального нормального $q_\phi(z\mid x) = \mathcal{N}(\mu,\operatorname{diag}(\sigma^2))$ и стандартного нормального прайора KL-дивергенция имеет замкнутую аналитическую формулу, суммируемую по всем $d$ латентным координатам:
$$D_{\mathrm{KL}}\big(q_\phi(z\mid x)\,\|\,\mathcal{N}(0,I)\big) = -\frac{1}{2}\sum_{i=1}^{d}\Big(1 + \log\sigma_i^2 - \mu_i^2 - \sigma_i^2\Big).$$
Разбор примеров
Пример 1 (числовой расчёт KL-дивергенции для одной координаты). Энкодер выдал $\mu = 1{,}0$, $\sigma^2 = 0{,}5$ (значит $\log\sigma^2 = \log 0{,}5 \approx -0{,}693$). Подставляем в формулу: $1 + (-0{,}693) - 1{,}0^2 - 0{,}5 = 1 - 0{,}693 - 1 - 0{,}5 = -1{,}193$. Умножаем на $-1/2$: $D_{\mathrm{KL}} \approx 0{,}5966$. Это означает, что распределение $\mathcal{N}(1{,}0;\,0{,}5)$ довольно заметно отличается от стандартного $\mathcal{N}(0,1)$ — модель «заплатит» примерно $0{,}5966$ дополнительных единиц функции потерь за то, что удерживает код этого примера так далеко от центра и с такой узкой дисперсией.
Пример 2 (проверка на крайнем случае). Если энкодер выдаёт ровно $\mu=0$, $\sigma^2=1$ (то есть в точности совпадает с прайором), KL-дивергенция должна обнулиться. Подставляем: $1 + \log(1) - 0^2 - 1 = 1 + 0 - 0 - 1 = 0$, и $D_{\mathrm{KL}} = -0{,}5\cdot0 = 0$. Это ожидаемый и важный контрольный случай: штраф равен нулю ровно тогда, когда распределение энкодера совпадает с прайором, — формула ведёт себя корректно на границе.
Пример 3 (совмещённый расчёт полной функции потерь и роль коэффициента $\beta$). Пусть для примера $x=(5{,}0;\,3{,}0)$ декодер восстановил $\hat{x}=(4{,}6;\,3{,}4)$: квадраты отклонений $0{,}4^2=0{,}16$ и $(-0{,}4)^2=0{,}16$, средняя ошибка реконструкции $0{,}16$. KL-дивергенция латентного распределения этого же примера, как в примере 1, равна $\approx0{,}5966$. При стандартном VAE (вес KL-слагаемого равен единице) полная функция потерь: $0{,}16 + 0{,}5966 = 0{,}7566$. В модификации $\beta$-VAE перед KL-слагаемым ставят дополнительный множитель $\beta$: $\mathcal{L} = \text{реконструкция} + \beta\cdot D_{\mathrm{KL}}$. При $\beta=4$ та же самая пара значений даёт $0{,}16 + 4\cdot0{,}5966 = 0{,}16+2{,}3864=2{,}5464$ — модель гораздо сильнее штрафуется за отклонение от прайора, что на практике обычно даёт более «раскладываемое» на независимые интерпретируемые факторы латентное пространство ценой чуть более размытой, менее точной реконструкции.
Почему это важно
Именно эта функция потерь — единственная причина, по которой VAE вообще называется «вариационным»: термин отсылает к вариационному выводу (variational inference), общему статистическому приёму приближения трудновычислимых апостериорных распределений более простыми, управляемыми семействами распределений, а вся конструкция с ELBO — прямое применение этой идеи к глубоким нейросетям. Баланс между двумя слагаемыми определяет всё поведение модели: перекос в сторону одной лишь ошибки реконструкции откатывает VAE обратно к поведению обычного автоэнкодера с бесполезным для генерации латентным пространством, а перекос в сторону KL-слагаемого (случай, известный как posterior collapse, «коллапс апостериорного распределения») заставляет энкодер игнорировать вход и всегда выдавать распределение, близкое к прайору, — декодер в таком случае перестаёт использовать информацию из $z$ вообще, и реконструкция становится одинаково блёклой для всех входов независимо от того, что на них изображено.
Практика: 30 заданий
Базовые задания (1–10)
Задание 1. Автоэнкодер восстановил вход $x=(4{,}0;\,6{,}0)$ как $\hat{x}=(3{,}5;\,6{,}2)$. Найди среднеквадратичную ошибку реконструкции (MSE).
Задание 2. Энкодер автоэнкодера устроен как $784 \to 256 \to 128 \to 32$. Найди коэффициент сжатия (отношение размерности входа к размерности латентного пространства).
Задание 3. Если энкодер и декодер автоэнкодера линейны (без нелинейных функций активации), а функция потерь — MSE, какому классическому методу снижения размерности из этого курса эквивалентно оптимальное решение по охватываемому подпространству?
Задание 4. Латентная координата VAE описывается нормальным распределением с $\mu=2$, $\sigma^2=1$. Запиши плотность этого распределения в стандартной записи.
Задание 5. Используя приём репараметризации, найди $z$, если $\mu=1{,}0$, $\sigma=2{,}0$, а сэмплированное значение вспомогательного шума $\varepsilon=0{,}5$.
Задание 6. Энкодер выдал логарифм дисперсии $\log\sigma^2=0{,}4$. Найди $\sigma$.
Задание 7. Объясни своими словами, почему нельзя напрямую вычислить градиент функции потерь по $\mu$ и $\sigma$, если $z$ сэмплируется прямо из $\mathcal{N}(\mu,\sigma^2)$ без приёма репараметризации.
Задание 8. Вычисли KL-дивергенцию $D_{\mathrm{KL}}(\mathcal{N}(0,1)\,\|\,\mathcal{N}(0,1))$ по формуле $-\tfrac12(1+\log\sigma^2-\mu^2-\sigma^2)$.
Задание 9. Почему в качестве априорного распределения $p(z)$ в VAE обычно выбирают именно стандартное нормальное распределение $\mathcal{N}(0,I)$, а не что-то более сложное?
Задание 10. Верно или неверно: если взять случайную точку в латентном пространстве обычного (не вариационного) автоэнкодера и декодировать её, результат обычно оказывается осмысленным объектом того же типа, что и обучающие данные. Обоснуй ответ.
Продвинутые задания (11–20)
Задание 11. Вычисли $D_{\mathrm{KL}}(\mathcal{N}(\mu,\sigma^2)\,\|\,\mathcal{N}(0,1))$ для $\mu=1$, $\sigma^2=0{,}5$.
Задание 12. Реконструкционная ошибка примера равна $0{,}16$, KL-дивергенция для этого же примера — $\approx0{,}5966$ (задание 11). Найди полную функцию потерь VAE для этого примера при стандартном весе KL-слагаемого, равном $1$.
Задание 13. Для тех же значений из задания 12 ($\text{реконструкция}=0{,}16$, $D_{\mathrm{KL}}\approx0{,}5966$) найди полную функцию потерь $\beta$-VAE с $\beta=4$ и объясни качественно, как рост $\beta$ обычно влияет на резкость реконструкции.
Задание 14. Выведи производную $\partial z/\partial\sigma$ для $z=\mu+\sigma\cdot\varepsilon$.
Задание 15. Объясни, почему плавная интерполяция между двумя точками латентного пространства (декодирование серии промежуточных точек $z_t=(1-t)z_1+tz_2$) — хороший практический тест, отличающий обученный VAE от обычного автоэнкодера.
Задание 16. Энкодер VAE для конкретного входа $x$ выдал $\mu\approx(0,0)$ и $\sigma\approx(1,1)$ — то есть почти в точности совпадающие с прайором $\mathcal{N}(0,I)$. Что это может означать с точки зрения того, насколько сеть использует информацию из $x$?
Задание 17. Автоэнкодер, обученный только на нормальных транзакциях, восстанавливает подозрительную транзакцию с заметно большей ошибкой, чем обычную. Объясни, почему это ожидаемо, а не случайность.
Задание 18. Латентное пространство VAE имеет размерность $d=16$. Сколько чисел должен выдавать энкодер на выходе (в отличие от обычного автоэнкодера с латентной размерностью $16$, где выход энкодера — ровно $16$ чисел)?
Задание 19. Энкодер устроен как $50 \to 10$ (полносвязный слой с $10$ выходами и смещениями), декодер — зеркально $10 \to 50$. Найди общее число обучаемых параметров такого обычного автоэнкодера.
Задание 20. Вес KL-слагаемого в функции потерь VAE устремили к нулю ($\beta\to0$). Опиши, во что фактически превращается модель и почему сгенерированные ею случайные $z\sim\mathcal{N}(0,I)$ перестанут быть надёжным источником осмысленных объектов.
Сложные задания (21–30)
Задание 21. Проверь формулу KL-дивергенции на числах: для $\mu=0{,}5$, $\sigma^2=2{,}0$ вычисли $D_{\mathrm{KL}}(\mathcal{N}(\mu,\sigma^2)\,\|\,\mathcal{N}(0,1))$.
Задание 22. Объясни на интуитивном уровне (без строгого вывода через неравенство Йенсена), почему максимизация ELBO — разумная замена невычислимой напрямую $\log p(x)$.
Задание 23. Вход $x=(5{,}0;\,3{,}0)$, декодер восстановил $\hat{x}=(4{,}6;\,3{,}4)$; латентное распределение этого примера — $\mu=1{,}0$, $\sigma^2=0{,}5$ (как в задании 11). Найди полную функцию потерь VAE для этого примера.
Задание 24. Объясни, почему изображения, сгенерированные классическим VAE, часто оказываются заметно более размытыми, чем изображения, сгенерированные GAN (урок 345).
Задание 25. Кратко опиши, в чём современные диффузионные модели (diffusion models), лежащие в основе Stable Diffusion и DALL-E, концептуально наследуют идеи VAE.
Задание 26. Порог ошибки реконструкции для обнаружения аномалий установлен на уровне $0{,}3$. Транзакция A даёт ошибку $0{,}18$, транзакция B — $0{,}52$. Какие из них система пометит как аномальные?
Задание 27. Объясни своими словами, почему оценку градиента, полученную через приём репараметризации, называют состоятельной (несмещённой) оценкой градиента истинного ожидаемого значения функции потерь по случайности сэмплирования.
Задание 28. Если для всех латентных координат $\sigma\to0$ (энкодер выдаёт почти нулевую дисперсию для любого входа), к какому поведению фактически приближается VAE и почему KL-регуляризация в этом случае должна сильно штрафовать модель?
Задание 29. Опиши качественно, что происходит с генерацией VAE в двух крайних случаях: (а) вес реконструкции устремлён к нулю относительно KL-слагаемого; (б) вес KL-слагаемого устремлён к нулю относительно реконструкции.
Задание 30. Сравни VAE и GAN (урок 345) по двум критериям — стабильность обучения и типичное визуальное качество генерации — и объясни, откуда берётся разница по каждому критерию.
Частые ошибки
-
Считают, что латентное пространство обычного автоэнкодера уже пригодно для генерации новых данных. Как разобрано в первом и втором разделах, ничто в обучении обычного автоэнкодера не заставляет коды соседних примеров располагаться рядом друг с другом — случайная точка почти всегда попадает в незаполненную область и декодируется в бессмысленный результат; для надёжной генерации нужна именно вероятностная надстройка VAE.
-
Путают приём репараметризации с обычным сэмплированием из $\mathcal{N}(\mu,\sigma^2)$. Разница принципиальна: репараметризация выносит всю случайность в независимый от параметров сети источник шума $\varepsilon\sim\mathcal{N}(0,I)$ и получает $z$ через детерминированную формулу $z=\mu+\sigma\varepsilon$ — только это делает всю цепочку дифференцируемой; прямое сэмплирование из $\mathcal{N}(\mu,\sigma^2)$ такой дифференцируемости не даёт.
-
Забывают про численную устойчивость и предсказывают $\sigma^2$ напрямую вместо $\log\sigma^2$. Сырой линейный выход слоя нейросети может быть отрицательным, а дисперсия обязана быть неотрицательной; предсказание логарифма дисперсии с последующим $\sigma=\exp(0{,}5\log\sigma^2)$ автоматически гарантирует положительность без искусственных ограничений на веса.
-
Считают функцию потерь VAE просто суммой двух независимых, никак не связанных штрафов. Реконструкция и KL-дивергенция находятся в постоянном напряжении друг с другом: слишком сильный акцент на одной из них ломает либо качество генерации (задание 20), либо содержательность латентного кода (posterior collapse, задание 16 и 29) — правильная настройка веса между ними (в том числе через $\beta$-VAE) требует осознанного баланса, а не автоматически хорошо работает при любом соотношении.
-
Путают VAE с GAN как взаимозаменяемые генеративные модели без разницы в подходе. VAE строит явную вероятностную модель латентного пространства и обучается через максимизацию ELBO, GAN обучается через состязательную игру генератора и дискриминатора без явного вероятностного латентного распределения (урок 345) — у них разные механизмы обучения, разные типичные слабые места (размытость у VAE против нестабильности и mode collapse у GAN) и разные сценарии, где один подход предпочтительнее другого.
-
Ожидают, что метод главных компонент (PCA) и автоэнкодер всегда дают идентичный результат. Эквивалентность строго верна лишь для линейного автоэнкодера с MSE-потерей (задание 3) — как только в архитектуру добавляются нелинейные функции активации, автоэнкодер способен выучивать существенно более гибкие, искривлённые многообразия, недоступные линейному PCA, и результаты двух методов на одних и тех же данных, вообще говоря, расходятся.
Главное запомнить
-
Автоэнкодер = энкодер, сжимающий вход в компактный код (латентное представление), плюс декодер, восстанавливающий вход из этого кода; обучение минимизирует ошибку реконструкции.
-
Линейный автоэнкодер с MSE-потерей теоретически эквивалентен PCA по охватываемому подпространству (урок 323) — нелинейный автоэнкодер обобщает эту идею на искривлённые многообразия данных.
-
Латентное пространство обычного автоэнкодера не регуляризовано и, как правило, непригодно для генерации: случайная точка в нём почти всегда декодируется в бессмысленный результат.
-
VAE заменяет точечный код на вероятностное распределение $q_\phi(z\mid x)=\mathcal{N}(\mu_\phi(x),\sigma_\phi^2(x))$, из которого сэмплируется $z$ перед подачей в декодер.
-
Приём репараметризации $z=\mu+\sigma\odot\varepsilon$, $\varepsilon\sim\mathcal{N}(0,I)$, выносит случайность в независимый источник шума и делает всю цепочку энкодер-декодер дифференцируемой для обычного обратного распространения ошибки.
-
Функция потерь VAE (отрицательный ELBO) складывается из ошибки реконструкции и KL-дивергенции между $q_\phi(z\mid x)$ и прайором $\mathcal{N}(0,I)$; для диагональных нормальных распределений KL-дивергенция вычисляется по замкнутой формуле $-\tfrac12\sum(1+\log\sigma_i^2-\mu_i^2-\sigma_i^2)$.
-
KL-слагаемое регуляризует латентное пространство к прайору, заставляя облака сэмплирования разных примеров перекрываться и делая пространство связным и пригодным для генерации из случайного $z\sim\mathcal{N}(0,I)$.
-
Ошибка реконструкции автоэнкодера или VAE — практичный сигнал для обнаружения аномалий: необычные, ранее не встречавшиеся данные восстанавливаются заметно хуже типичных.
-
VAE, как правило, обучается стабильнее GAN, но даёт более размытые результаты из-за попиксельной реконструкционной функции потерь; GAN обычно даёт более резкие результаты ценой менее стабильного обучения (сравнение подробно в задании 30).
-
Диффузионные модели, лежащие в основе Stable Diffusion и DALL-E, наследуют от VAE идею вероятностного отображения простого прайора в сложные данные через обучение по вариационной нижней границе правдоподобия.
Связь с темами курса
Самая прямая связь этого урока — с уроком 323 про метод главных компонент. Там ты видел, как снижение размерности решается точно, через разложение ковариационной матрицы на собственные векторы: PCA находит линейное подпространство, наилучшим образом приближающее облако точек, и делает это за один детерминированный алгебраический шаг. Автоэнкодер, разобранный в первом разделе этого урока, — прямое нелинейное обобщение той же самой задачи, решаемое уже не алгеброй, а градиентным спуском по весам нейросети; при линейных слоях и MSE-потере оба метода математически эквивалентны по охватываемому подпространству (задание 3), а с нелинейными слоями автоэнкодер получает возможность выучивать существенно более сложные, искривлённые многообразия данных, недоступные линейному PCA. VAE идёт ещё на шаг дальше PCA и обычного автоэнкодера — добавляет вероятностную надстройку, которой нет ни у одного из двух предыдущих методов, специально ради того, чтобы латентное пространство годилось не только для сжатия и восстановления, но и для генерации совершенно новых объектов.
Столь же важна связь с уроком 345 про генеративные состязательные сети. Обе модели решают одну и ту же задачу — генерацию новых, реалистично выглядящих данных, — но принципиально разными путями: GAN обучается через состязательную минимаксную игру между генератором и дискриминатором без явного вероятностного описания латентного пространства, а VAE строит явную вероятностную модель и обучается через максимизацию вариационной нижней границы правдоподобия (ELBO). Как подробно разобрано в задании 30 и в разделе про функцию потерь, эта разница в механизме обучения напрямую объясняет типичные сильные и слабые стороны каждого подхода: VAE обычно стабильнее в обучении, но даёт более размытые результаты; GAN обычно даёт более резкие результаты, но обучается менее предсказуемо и рискует потерей разнообразия (mode collapse). На практике многие современные системы комбинируют идеи обоих подходов (например, VAE-GAN — VAE с добавленным дискриминатором вместо чисто попиксельной реконструкционной потери), беря стабильность вероятностной регуляризации VAE и резкость состязательного обучения GAN.
Приём репараметризации из третьего раздела опирается на уже знакомое из урока 330 обратное распространение ошибки — без понимания того, как градиент вообще течёт по цепочке вычислений от функции потерь к весам сети, было бы невозможно оценить, почему сэмплирование разрывает эту цепочку и почему конкретно предложенное решение её восстанавливает. А сама функция потерь VAE своим названием и происхождением отсылает к теореме Байеса из урока 232: вариационный вывод, который лежит в основе ELBO, — это общий статистический приём приближения трудновычислимого байесовского апостериорного распределения более простым, управляемым семейством распределений, и VAE — один из самых успешных примеров того, как этот полувековой статистический аппарат соединился с современным глубоким обучением. Наконец, идеи вероятностного латентного пространства и обучения через вариационную нижнюю границу правдоподобия прямо предвосхищают диффузионные модели, лежащие в основе большинства современных генераторов изображений по тексту, — тема, к которой ты обязательно вернёшься при более глубоком погружении в современные генеративные архитектуры за пределами этого курса.
Интересные факты
-
Статья Кингмы и Веллинга «Auto-Encoding Variational Bayes» была впервые выложена на arXiv в конце 2013 года и с тех пор стала одной из самых цитируемых работ в области глубокого обучения — при этом сам Дидерик Кингма несколькими годами позже стал соавтором ещё одной чрезвычайно широко используемой работы, оптимизатора Adam (урок 293), что делает его редким примером исследователя, стоящего у истоков сразу двух независимых столпов современного глубокого обучения.
-
Приём репараметризации был независимо и почти одновременно открыт двумя разными исследовательскими коллективами — Кингмой и Веллингом в Амстердаме и Резенде, Мохамедом и Вирстрой в DeepMind, — что в истории науки часто служит признаком того, что идея буквально «витала в воздухе», поскольку все необходимые для неё компоненты (вариационный вывод в статистике и практичное обратное распространение ошибки в глубоких сетях) к тому моменту уже были готовы к соединению.
-
Классический VAE, обученный на датасете рукописных цифр MNIST, позволяет буквально «нарисовать» новую, никогда не существовавшую цифру простым движением ползунков по двум-трём латентным координатам — при достаточно малой латентной размерности можно интерактивно визуализировать всё двумерное латентное пространство как сетку сгенерированных цифр, наглядно наблюдая, как один конец пространства плавно перетекает в другой (например, от вытянутых «единиц» через «семёрки» к «девяткам»).
-
Хотя классические VAE давно уступили GAN и диффузионным моделям по фотореалистичности прямой генерации изображений, сама вариационная идея не осталась в прошлом: VQ-VAE (Vector Quantized VAE, «векторно-квантованный VAE») — модификация с дискретным, а не непрерывным латентным пространством — стала ключевым компонентом внутри нескольких современных систем генерации изображений и звука, работающих не напрямую с пикселями, а со сжатым латентным представлением, полученным именно вариационным автоэнкодером.
Лайфхаки
-
Прежде чем усложнять архитектуру VAE, всегда сначала обучи и провалидируй обычный (не вариационный) автоэнкодер на тех же данных с той же латентной размерностью — если он не может дать приемлемую реконструкцию, добавление вероятностной надстройки и KL-регуляризации проблему не решит, а лишь усложнит диагностику, откуда именно берётся плохое качество.
-
Если генерация из случайных $z\sim\mathcal{N}(0,I)$ выдаёт однообразные, малоинформативные результаты, первым делом проверь KL-дивергенцию по каждой латентной координате отдельно — если она близка к нулю почти везде, это явный признак posterior collapse (задание 16, 29), и стоит попробовать уменьшить вес KL-слагаемого (либо использовать один из специализированных приёмов борьбы с этой проблемой) прежде чем менять архитектуру энкодера или декодера.
-
При реализации на PyTorch или TensorFlow всегда предсказывай $\log\sigma^2$, а не $\sigma^2$ или $\sigma$ напрямую, и восстанавливай $\sigma$ через $\exp(0{,}5\cdot\log\sigma^2)$ — это не стилистическая деталь, а способ избежать отрицательных или обнуляющихся значений дисперсии, которые ломают и сэмплирование, и вычисление KL-дивергенции.
-
Если реконструкции VAE стабильно выходят размытыми и это критично для задачи, попробуй сначала не менять архитектуру целиком, а заменить попиксельную MSE-потерю на потерю, менее чувствительную к точному совпадению каждого пикселя (например, комбинацию с перцептивной потерей на признаках предобученной сети), прежде чем переходить на принципиально другую генеративную архитектуру вроде GAN или диффузионной модели.
-
Используй $\beta$-VAE (с $\beta>1$) как первый инструмент, если задача требует не столько фотореалистичной генерации, сколько интерпретируемого, «раскладываемого» на независимые факторы латентного пространства (например, отдельная координата отвечает за поворот объекта, отдельная — за цвет), — такая интерпретируемость обычно даётся ценой более размытой реконструкции, и стоит явно решить, какой из двух критериев важнее для конкретной задачи, прежде чем настраивать $\beta$.
-
Для задачи обнаружения аномалий не ограничивайся одной лишь ошибкой реконструкции как сигналом — у VAE дополнительно доступна KL-дивергенция апостериорного распределения конкретного примера от прайора, и совместное использование обоих сигналов (например, их взвешенной суммы) часто даёт более надёжное разделение нормальных и аномальных примеров, чем любой из сигналов по отдельности.
Идея, которую ты разобрал сегодня, устроена обманчиво просто: заставь сеть сжимать данные и восстанавливать их обратно, а потом заставь скрытый код быть немного «зашумлённым» и притянутым к общему стандартному распределению. За этой простотой стоит целая цепочка нетривиальных решений — вероятностный энкодер вместо детерминированного, приём репараметризации, разворачивающий недифференцируемое сэмплирование в дифференцируемую формулу, и функция потерь, честно балансирующая точность восстановления против структурированности латентного пространства. Ты увидел прямую линию, которая тянется от линейной алгебры PCA (урок 323) через нелинейный автоэнкодер к полноценной вероятностной генеративной модели, и рядом с ней — альтернативный, состязательный путь GAN (урок 345), решающий ту же задачу генерации совершенно иначе. В следующем уроке курс поворачивает в новую сторону — к обучению с подкреплением, где агент учится не восстанавливать или генерировать данные, а действовать в среде, получая награду за успешные решения, и, как ты уже знаешь из третьего раздела этого урока, некоторые из идей вокруг стохастических вычислительных узлов, вроде того же приёма репараметризации, снова всплывут там в новом обличье.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку