И почему особенности Фурье меняют всё
Делиться

Введение
Множество Мандельброта — один из самых красивых математических объектов, когда-либо обнаруженных, фрактал настолько сложный, что как бы вы ни приближали изображение, вы будете находить бесконечное количество деталей. Но что, если бы мы попросили нейронную сеть изучить его?
На первый взгляд, это кажется странным вопросом. Множество Мандельброта полностью детерминировано: в нём нет данных, шума, скрытых правил. Но эта простота делает его идеальной площадкой для изучения того, как нейронные сети представляют сложные функции.
В этой статье мы рассмотрим, как простая нейронная сеть может научиться аппроксимировать множество Мандельброта, и как гауссовы Фурье-признаки полностью преобразуют её производительность, превращая размытые аппроксимации в чёткие фрактальные границы.
Попутно мы разберемся, почему стандартные многослойные персептроны (MLP) испытывают трудности с высокочастотными паттернами (проблема спектрального смещения) и как ее решают фурье-преобразователи.
Множество Мандельброта
Множество Мандельброта определяется на комплексной плоскости. Для каждого комплексного числа (cin mathbb{C}) мы рассматриваем итеративную последовательность:
$$z_{n+1} = z_n^2 + c, z_0 = 0$$
Если эта последовательность остается ограниченной, то (c) принадлежит множеству Мандельброта.
На практике мы аппроксимируем это условие с помощью алгоритма времени выхода . Алгоритм времени выхода итерирует последовательность до фиксированного максимального числа шагов и отслеживает величину (|z_n|). Если (|z_n|) превышает выбранный радиус выхода (обычно 2), последовательность гарантированно расходится, и (c) классифицируется как находящаяся за пределами множества Мандельброта. Если последовательность не выходит за пределы максимального числа итераций, предполагается, что (c) принадлежит множеству Мандельброта, при этом количество итераций часто используется для визуализации.
Превращение множества Мандельброта в задачу обучения
Для обучения нашей нейронной сети нам необходимы две вещи. Во-первых, мы должны определить задачу обучения, то есть, что модель должна предсказывать и на основе каких входных данных. Во-вторых, нам нужны размеченные данные: большой набор пар «вход-выход», полученных из этой задачи.
Определение проблемы обучения
По своей сути, множество Мандельброта определяет функцию на комплексной плоскости. Каждая точка (c = x +iy in mathbb{C}) отображается на результат: либо последовательность, сгенерированная итерацией, остается ограниченной, либо она расходится. Это сразу же наводит на мысль о задаче бинарной классификации, где на вход подается комплексное число, а на выход указывается, находится ли число внутри множества или нет.
Однако такая формулировка создает трудности для обучения. Граница множества Мандельброта бесконечно сложна, и сколь угодно малые возмущения в (c) могут изменить результат классификации. С точки зрения обучения, это приводит к сильно разрывной целевой функции, что делает оптимизацию нестабильной и неэффективной с точки зрения данных.
Для получения более плавной и информативной целевой функции обучения мы переформулируем задачу, используя информацию о времени выхода из последовательности, представленную в предыдущем разделе. Вместо предсказания бинарной метки модель обучается предсказывать непрерывную переменную, полученную из количества итераций, на которых последовательность выходит из последовательности.
Для получения непрерывной целевой функции мы не используем напрямую исходное количество итераций выхода. Исходное количество итераций выхода является дискретной величиной, и его использование привело бы к разрывам, особенно вблизи границы множества Мандельброта, где небольшие изменения (c) могут вызывать большие скачки в количестве итераций. Для решения этой проблемы мы используем сглаженное значение времени выхода, которое включает в себя количество итераций выхода для получения непрерывной целевой функции. Кроме того, мы также применяем логарифмическое масштабирование, которое распределяет ранние значения выхода и сжимает большие, что приводит к более сбалансированному распределению целевой функции.
def smooth_escape(x: float, y: float, max_iter: int = 1000) -> float: c = complex(x, y) z = 0j for n in range(max_iter): z = z*z + c r2 = z.real*z.real + z.imag*z.imag if r2 > 4.0: r = math.sqrt(r2) mu = n + 1 — math.log(math.log(r)) / math.log(2.0) # smooth # логарифмическая шкала для разброса небольшого mu v = math.log1p(mu) / math.log1p(max_iter) return float(np.clip(v, 0.0, 1.0)) return 1.0
При таком определении множество Мандельброта становится задачей регрессии . Нейронная сеть обучается аппроксимировать функцию $$f : mathbb{R}^2 rightarrow [0,1]$$
отображение пространственных координат ((x, y)) в комплексной плоскости на плавное значение времени выхода.
Стратегия выборки данных
Равномерная выборка на комплексной плоскости была бы крайне неэффективной, поскольку большинство точек находятся далеко от границы и несут мало информации. Для решения этой проблемы набор данных смещен в сторону граничных областей путем избыточной выборки и фильтрации точек, значения которых попадают в заданный диапазон.
def build_boundary_biased_dataset( n_total=800_000, frac_boundary=0.7, xlim=(-2.4, 1.0), res_for_ylim=(3840, 2160), ycenter=0.0, max_iter=1000, band=(0.35, 0.95), seed=0, ): «»» — Смесь равномерных выборок + выборок с граничной полосой. — 'band' выбирает точки с целевым значением в (низкой, высокой полосе), которые, как правило, концентрируются вблизи границы. «»» rng = np.random.default_rng(seed) ylim = compute_ylim_from_x(xlim, res_for_ylim, ycenter=ycenter) n_boundary = int(n_total * frac_boundary) n_uniform = n_total — n_boundary # Равномерный набор Xu = sample_uniform(n_uniform, xlim, ylim, seed=seed) # Пул границ: передискретизация, затем фильтрация по полосе pool_factor = 20 pool = sample_uniform(n_boundary * pool_factor, xlim, ylim, seed=seed + 1) yp = np.empty((pool.shape[0],), dtype=np.float32) for i, (x, y) in enumerate(pool): yp[i] = smooth_escape(float(x), float(y), max_iter=max_iter) mask = (yp > band[0]) & (yp < band[1]) Xb = pool[mask] yb = yp[mask] if len(Xb) < n_boundary: # Если полоса слишком строгая, автоматически ослабляем ее keep = min(len(Xb), n_boundary) print(f"[warn] Пограничная полоса слишком строгий; получено {len(Xb)} граничных точек, используется {keep}." Xb = Xb[:keep] yb = yb[:keep] n_boundary = keep n_uniform = n_total - n_boundary Xu = sample_uniform(n_uniform, xlim, ylim, seed=seed) else: Xb = Xb[:n_boundary] yb = yb[:n_boundary] yu = np.empty((Xu.shape[0],), dtype=np.float32) for i, (x, y) in enumerate(Xu): yu[i] = smooth_escape(float(x), float(y), max_iter=max_iter) X = np.concatenate([Xu, Xb], axis=0).astype(np.float32) y = np.concatenate([yu, yb], axis=0).astype(np.float32) # Перемешиваем один раз perm = rng.permutation(X.shape[0]) return X[perm], y[perm], ylim
Базовая модель: Глубокий остаточный многослойный перцептрон
В нашей первой попытке используется глубокий остаточный многослойный персептрон, который принимает на вход необработанные декартовы координаты ((x, y)) и предсказывает значение плавного выхода.
# Базовый класс модели MLPRes(nn.Module): def __init__( self, hidden_dim=256, num_blocks=8, act=»silu», dropout=0.0, out_dim=1, ): super().__init__() activation = nn.ReLU if act.lower() == «relu» else nn.SiLU self.in_proj = nn.Linear(2 , hidden_dim) self.in_act = activation() self.blocks = nn.Sequential(*[ ResidualBlock(hidden_dim, act=act, dropout=dropout) for _ in range(num_blocks) ]) self.out_ln = nn.LayerNorm(hidden_dim) self.out_act = activation() self.out_proj = nn.Linear(hidden_dim, out_dim) def forward(self, x): x = self.in_proj(x) x = self.in_act(x) x = self.blocks(x) x = self.out_act(self.out_ln(x)) return self.out_proj(x) # Класс остаточного блока ResidualBlock(nn.Module): def __init__(self, dim: int, act: str = «silu», dropout: float = 0.0): super().__init__() activation = nn.ReLU if act.lower() == «relu» else nn.SiLU # Пред-нормальный (LayerNorm значительно повышает стабильность глубоких остаточных MLP) self.ln1 = nn.LayerNorm(dim) self.fc1 = nn.Linear(dim, dim) self.ln2 = nn.LayerNorm(dim) self.fc2 = nn.Linear(dim, dim) self.act = activation() self.drop = nn.Dropout(dropout) if dropout and dropout > 0 else nn.Identity() # необязательно: небольшая инициализация для последнего слоя, чтобы начать работу с почти идентичными параметрами nn.init.zeros_(self.fc2.weight) nn.init.zeros_(self.fc2.bias) def forward(self, x): h = self.ln1(x) h = self.act(self.fc1(h)) h = self.drop(h) h = self.ln2(h) h = self.fc2(h) return x + h
Эта нейронная сеть обладает достаточной пропускной способностью: глубокая, остаточная, обучена на большом наборе данных и имеет стабильную оптимизацию.
Результат

Общая форма множества Мандельброта отчетливо видна. Однако мелкие детали вблизи границы заметно размыты. Области, которые должны демонстрировать сложную фрактальную структуру, выглядят чрезмерно гладкими, а тонкие нити либо плохо выражены, либо полностью отсутствуют.
Дело не в разрешении, данных или глубине. Так что же идёт не так?
Проблема спектрального смещения
В нейронных сетях существует хорошо известная проблема, называемая спектральным смещением :
Они, как правило, сначала изучают низкочастотные функции и испытывают трудности с представлением функций с быстрыми колебаниями или мелкими деталями.
Граница Мандельброта характеризуется высокой степенью нерегулярности и заполнена мелкомасштабными структурами, особенно вблизи её границы. Для её описания нейронной сети необходимо было бы представлять высокочастотные вариации выходных данных при изменении (x) и (y).
Фурье-характеристики: кодирование координат в частотном пространстве
Одно из наиболее элегантных решений проблемы спектрального смещения было предложено в 2020 году Танчиком и др. в их статье «Фурье-признаки позволяют сетям обучаться высокочастотным функциям в низкоразмерных областях».
Идея заключается в преобразовании входных координат перед подачей их в нейронную сеть. Вместо того чтобы подавать исходные координаты ((x, y)), мы передаем их синусоидальную проекцию случайных направлений в многомерном пространстве.
Формально:
$$gamma(x)=[sin(2 pi Bx),cos(2 pi Bx)]$$
где (B in mathbb{R}^{d_{in}×d_{feat}}) — случайная гауссова матрица.
Это отображение действует как случайное разложение по базису Фурье , позволяя сети легче представлять высокочастотные детали.
class GaussianFourierFeatures(nn.Module): def __init__(self, in_dim=2, num_feats=256, sigma=5.0): super().__init__() B = torch.randn(in_dim, num_feats) * sigma self.register_buffer(«B», B) def forward(self, x): proj = (2 * torch.pi) * (x @ self.B) return torch.cat([torch.sin(proj), torch.cos(proj)], dim=-1)
Многомасштабные гауссовы Фурье-характеристики
Одной частотной шкалы может быть недостаточно. Множество Мандельброта демонстрирует структуру на всех уровнях разрешения (отличительная особенность фрактальной геометрии).
Для этого мы используем многомасштабные гауссовы Фурье-преобразования , объединяя несколько частотных диапазонов:
class MultiScaleGaussianFourierFeatures(nn.Module): def __init__(self, in_dim=2, num_feats=512, sigmas=(2.0, 6.0, 10.0), seed=0): super().__init__() # разделение признаков по масштабам k = len(sigmas) per = [num_feats // k] * k per[0] += num_feats — sum(per) Bs = [] g = torch.Generator() g.manual_seed(seed) for s, m in zip(sigmas, per): B = torch.randn(in_dim, m, generator=g) * s Bs.append(B) self.register_buffer(«B», torch.cat(Bs, dim=1)) def forward(self, x): proj = (2 * torch.pi) * (x @ self.B) return torch.cat([torch.sin(proj), torch.cos(proj)], dim=-1)
Это фактически обеспечивает сеть многоуровневой частотной базой, идеально соответствующей самоподобной природе фракталов.
Финальная модель
Финальная модель имеет ту же архитектуру, что и базовая модель, единственное отличие заключается в использовании MultiScaleGaussianFourierFeatures.
class MLPFourierRes(nn.Module): def __init__( self, num_feats=256, sigma=5.0, hidden_dim=256, num_blocks=8, act=»silu», dropout=0.0, out_dim=1, ): super().__init__() self.ff = MultiScaleGaussianFourierFeatures( 2, num_feats=num_feats, sigmas=(2.0, 6.0, sigma), seed=0 ) self.in_proj = nn.Linear(2 * num_feats, hidden_dim) self.blocks = nn.Sequential(*[ ResidualBlock(hidden_dim, act=act, dropout=dropout) for _ in range(num_blocks) ]) self.out_ln = nn.LayerNorm(hidden_dim) activation = nn.ReLU if act.lower() == «relu» else nn.SiLU self.out_act = activation() self.out_proj = nn.Linear(hidden_dim, out_dim) def forward(self, x): x = self.ff(x) x = self.in_proj(x) x = self.blocks(x) x = self.out_act(self.out_ln(x)) return self.out_proj(x)
Динамика обучения
Обучение без использования преобразований Фурье
Модель постепенно изучает общую форму множества Мандельброта, но затем останавливается. Дополнительное обучение не приводит к добавлению новых деталей.
Обучение с использованием характеристик Фурье
Здесь сначала появляются крупные структуры, за которыми следуют постепенно более мелкие детали. Модель продолжает уточнять свои предсказания, вместо того чтобы остановиться на достигнутом уровне.
Окончательные результаты
Обе модели использовали одну и ту же архитектуру, набор данных и процедуру обучения. Сеть представляет собой глубокий остаточный многослойный перцептрон (MLP), обученный как регрессионная модель на основе формулировки плавного времени выхода.
- Набор данных: 1 000 000 выборок из комплексной плоскости, при этом 70% точек сосредоточены вблизи границы фрактала благодаря смещенной выборке данных.
- Архитектура: Остаточный MLP с 20 остаточными блоками и скрытым измерением в 512 единиц.
- Функция активации: SiLU
- Обучение: 100 эпох, размер пакета 4096, оптимизатор на основе Adam, планировщик с косинусным отжигом.
Единственное различие между двумя моделями заключается в способе представления входных координат. Базовая модель использует исходные декартовы координаты, тогда как вторая модель использует многомасштабные Фурье-преобразования для представления данных.
Глобальный обзор


Увеличение 1 Вид


Увеличить 2 просмотра


Выводы
Фракталы, такие как множество Мандельброта, являются крайним примером функций, в которых преобладают высокочастотные структуры. Аппроксимация их непосредственно из исходных координат заставляет нейронные сети синтезировать все более детальные колебания, задача, для решения которой многослойные перцептроны плохо подходят.
В этой статье показано, что ограничение заключается не в архитектурных возможностях, объеме данных или оптимизации, а в способе представления информации .
Кодируя входные координаты с помощью многомасштабных гауссовых преобразований Фурье, мы переносим большую часть сложности задачи в пространство входных данных. Высокочастотная структура становится явной, что позволяет обычной нейронной сети аппроксимировать функцию, которая в своей исходной форме была бы слишком сложной.
Эта идея выходит за рамки фракталов. Нейронные сети, основанные на координатах, используются в компьютерной графике, обучении на основе физических принципов и обработке сигналов. Во всех этих областях выбор входного кодирования может определять разницу между плавными приближениями и богатой, высокодетализированной структурой.
Примечание к визуальным элементам
Все изображения, анимации и видеоролики, представленные в этой статье, были сгенерированы автором на основе результатов работы описанных выше моделей нейронных сетей. Внешние средства рендеринга фракталов или сторонние визуальные ресурсы не использовались. Полный код, использованный для обучения, рендеринга изображений и генерации анимаций, доступен в прилагаемом репозитории.
Полный код
Репозиторий на GitHub
Источник: towardsdatascience.com



























