Проблема, которую не решает обычная оптимизация

Классические методы оптимизации — градиентный спуск, генетические алгоритмы с элитизмом, CMA‑ES — заточены под одну вещь: найти один глобальный максимум функции приспособленности.

Но во многих задачах нас интересует не единственное решение, а набор разнообразных хороших решений:

Эволюционная робототехника. Нужно не одно «оптимальное» положение ног шагающего робота, а целая библиотека походок под разные повреждения — если у робота откажет один сустав, он должен уметь быстро подобрать альтернативную походку вместо повторной оптимизации с нуля.

Процедурная генерация контента в играх. Нужны не «лучшие» уровни, а уровни, покрывающие весь спектр: лёгкие/сложные, линейные/разветвлённые.

Дизайн и инженерия. Инженеру интересно увидеть весь фронт компромиссов (вес vs прочность vs стоимость), а не одну точку.

Открытые (так называемый open‑ended) эволюционные системы, где само понятие «лучшего» плохо определено, а интересна широта поведенческого репертуара.

Это направление получило название Quality‑Diversity (QD) оптимизации: цель — не максимизировать один скаляр, а заполнить пространство возможных поведений решениями, каждое из которых максимально хорошо в своей поведенческой нише.

MAP‑Elites — один из первых и самый концептуально простой алгоритм этого семейства.

Идея алгоритма

MAP‑Elites (Multi‑dimensional Archive of Phenotypic Elites) был предложен в 2015 году.

Оригинальный препринт:  — «Illuminating search spaces by mapping elites: a new algorithm for pursuing quality diversity» — https://arxiv.org/abs/1504.04909

Ключевая идея разделяет пространство решений на два независимых измерения:

Генотип — параметры решения, которые мы мутируем/скрещиваем.

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

Пространство поведенческих дескрипторов заранее дискретизируется в регулярную сетку ячеек (их еще называют ниши). Алгоритм поддерживает архив, где в каждой ячейке хранится максимум одно решение — то, что оказалось лучшим (по приспособленности) среди всех когда‑либо попавших в эту ячейку.

Как это работает пошагово:

1) Инициализировать пустой архив‑сетку N‑мерных ячеек.

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

3) Цикл до исчерпания бюджета оценок:

3.1) Выбрать случайное существующее решение из архива (уже заполненную ячейку).

3.2) Мутировать его (иногда — скрестить с другим случайным жителем архива).

3.3) Оценить потомка: посчитать приспособленность и поведенческий дескриптор.

3.4) Определить, в какую ячейку сетки попадает потомок.

3.5) Если ячейка пуста ИЛИ потомок лучше текущего обитателя этой ячейки — заменить.

По завершении архив представляет собой карту (map) «elite»‑решений — по одному лучшему решению на нишу.

Никакого отбора между нишами нет — решения конкурируют только внутри своей ниши. Это и даёт «иллюминацию» всего пространства, а не сходимость в одну точку.

В классическом MAP‑Elites родитель выбирается равновероятно среди всех заполненных ниш архива. Такая стратегия максимально проста и не создаёт смещения поиска, однако значительная часть вычислений со временем начинает тратиться на области, которые уже практически перестали улучшаться. Поэтому в последующих работах были предложены различные стратегии выбора родителей: отдавать предпочтение недавно открытым нишам, областям с низкой плотностью решений (границам архива), элитам, чьи потомки чаще создают новые или улучшают существующие ниши, а также использовать механизмы curiosity, увеличивающие вероятность выбора наиболее «продуктивных» решений.

Современные реализации MAP‑Elites (например, CMA‑ME и библиотеки pyribs или QDax) обычно идут ещё дальше и отделяют архив от механизма генерации новых решений. Вместо простой случайной мутации используются специализированные эмиттеры (emitters), которые самостоятельно выбирают родителей и применяют различные стратегии поиска (генетические операторы, CMA‑ES, суррогатные модели, градиентные методы и др.). При этом сам принцип MAP‑Elites остаётся неизменным: архив по‑прежнему хранит лучшее решение в каждой нише,

а различаются лишь способы поиска новых кандидатов.

Сложность

Пусть:

M — число ячеек в архиве (произведение числа делений по каждому измерению множества поведений),

T — число итераций (эволюционных поколений/оценок),

D — размерность генотипа,

E — стоимость одной оценки решения (симуляция/вычисление приспособленности).

По времени: каждая итерация — это выбор случайного родителя за O(1) (при хранении в виде массива/словаря), мутация за O(D),

вычисление дескриптора и приспособленности — доминирующая часть, O(E), и вставка/сравнение в ячейке за O(1).

Итого на одну итерацию — O(D + E), а весь алгоритм — O(T·(D + E)).

На практике E почти всегда доминирует (типичные оценки включают симуляцию физики, клеточных автоматов, инференс LLM и тому подобное), так что сложность алгоритма как такового — линейная по числу оценок,

это его отличие от многих QD‑методов (например, есть такой алгоритм, Novelty Search с k‑NN по архиву — там поиск соседей стоит O(log|archive|) или O(|archive|) на добавление).

Базовая имплементация

```python

import numpy as np

# Задача: найти x, y в [-5, 5], максимизируя fitness,

# ниши определяются по (x, y) с сеткой 20x20

BOUNDS = (-5.0, 5.0)

GRID_SIZE = 20 # число ячеек по каждой оси behavior space

N_ITERATIONS = 5000

MUTATION_SIGMA = 0.2

def fitness(genome):

x, y = genome

# произвольная многомодальная функция для иллюстрации

return -(x**2 + y**2) + 5 np.sin(3 * x) * np.cos(3 * y)

def behavior_descriptor(genome):

# в этой игрушечной задаче поведенческий дескриптор совпадает с генотипом,

# в реальных задачах это обычно совсем другое пространство признаков

return genome

def to_cell(bd):

lo, hi = BOUNDS

idx = ((bd - lo) / (hi - lo) * GRID_SIZE).astype(int)

return tuple(np.clip(idx, 0, GRID_SIZE - 1))

def random_genome():

return np.random.uniform(*BOUNDS, size=2)

def mutate(genome):

child = genome + np.random.normal(0, MUTATION_SIGMA, size=genome.shape)

return np.clip(child, *BOUNDS)

# архив: ключ - индекс ячейки, значение - (genome, fitness)

archive = {}

# 1) инициализация случайными решениями

for _ in range(200):

g = random_genome()

f = fitness(g)

cell = to_cell(behavior_descriptor(g))

if cell not in archive or f > archive[cell][1]:

archive[cell] = (g, f)

# 2) основной цикл MAP-Elites

for it in range(N_ITERATIONS):

parent_cell = list(archive.keys())[np.random.randint(len(archive))]

parent_genome, = archive[parent_cell]

child = mutate(parent_genome)

f_child = fitness(child)

cell = to_cell(behavior_descriptor(child))

if cell not in archive or f_child > archive[cell][1]:

archive[cell] = (child, f_child)

print(f"Заполнено ниш: {len(archive)} / {GRID_SIZE**2}")

best_cell = max(archive, key=lambda c: archive[c][1])

print("Лучшее решение:", archive[best_cell])

```

Вывод результатов:

Заполнено ниш: 379 / 400 # финальная заполненность не детерминированная, зависит от случайной инициализации и мутаций

Лучшее решение: (array([0.52733742, 0.00456922]), np.float64(4.721110167064826))

Как это интерпретировать?

1) Изначальные 200 случайных геномов при равномерном разбросе по сетке 20×20=400 ячеек попадают в ячейки далеко не равномерно — часть клеток просто не выпадает по закону больших чисел

(как при бросании 200 шаров в 400 корзин, часть корзин гарантированно останется пустой).

2) После 5000 итераций алгоритм успел заполнить почти все ячейки, но не все — некоторые ячейки так и не были достигнуты за выделенный бюджет оценок из-за локального характера мутации и случайной инициализации.

3) Мутация — локальный оператор, а не глобальный поиск. На каждой итерации ребёнок получается из родителя гауссовским шумом с σ=0.2.

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

4) В реальных задачах поведенческий дескриптор может быть совсем не совпадать с генотипом, и тогда «мосты» между нишами могут быть ещё более редкими.

5) Сама fitness‑функция. Многие комбинации (x, y) физически не бывают «лучшими ни в чём» — если приспособленность в каком‑то регионе всегда хуже, чем в соседних, то решения там просто быстро «проигрывают» и вымирают, но сама ячейка при этом всё равно может быть достигнута случайным блужданием — так что это не главная причина пустот, а скорее (3) и (1).

6) Из‑за (5) почти никогда не берут простую гауссовскую мутацию — используют направленный шаг вдоль линии между двумя случайными жителями архива, что резко улучшает связность архива и покрытие

Что значит «Лучшее решение»:

(array([0.52733742, 0.00456922]), np.float64(4.721110167064826))

Это геном (x, y) ≈ (0.527, 0.005) и его fitness ≈ 4.721 — генотип из той ниши архива, у которой fitness оказался максимальным среди всех 379 заполненных ячеек. Поскольку в этой игрушечной задаче behavior_descriptor = генотип, «лучшая ниша» здесь просто совпадает с глобальным (или локальным) максимумом функции ‑(x²+y²) + 5·sin(3x)·cos(3y) вблизи начала координат — то есть в этом вырожденном случае MAP‑Elites фактически выродился в обычный поиск максимума плюс попутное картирование окрестности, что и ожидаемо, раз здесь пространство ниш не несёт отдельной поведенческой информации, а дублирует генотип.

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

Поэтому в QD‑оптимизации ценятся не столько проценты заполнения, сколько покрытие достижимых ниш и разнообразие поведений внутри них.

Итого:

В этой статье мы рассмотрели классический MAP-Elites. В современных реализациях чаще используются более сложные стратегии генерации кандидатов — CMA-ME, GeneticEmitter, Surrogate-assisted MAP-Elites и другие. Именно эти методы сегодня составляют основу большинства исследований в области Quality-Diversity.

Комментарии (0)