Вместо предисловия

Итак, вы опять не хотите анализировать задачу. Вам не нравится возиться с производной, искать градиент, доказывать сходимость – вы просто хотите, чтобы компьютер сам нашёл решение. Джон Голланд в 1975 году — конкретно в книге «Adaptation in Natural and Artificial Systems» — не просто придумал, а строго сформулировал генетический алгоритм (именно там впервые появился этот термин). Ирония в том, что студенты до сих пор сдают лабы по ГА, даже не понимая, что это простейший метод оптимизации, который работает за счёт того, что природа за миллиарды лет протестировала все возможные комбинации белков и оставила только работающие.

Кстати, один из учеников Голланда, Джон Коз, в 1992 году выпустил «Genetic Programming», где предложил эволюционировать не просто параметры, а целые программы. Благодаря ему генетическое программирование стало коммерчески успешной темой, а не только академической игрушкой.

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

Шаг 1. Кодируем задачу – «хромосома на заказ»

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

Какой тип хромосомы выбрать?

Главное правило: любая хромосома должна однозначно превращаться в решение, которое можно оценить. Для каждого набора генов у вас должна быть функция приспособленности (она же целевая функция, она же fitness). Чем больше число, тем лучше? Или чем меньше – зависит от задачи. Главное, чтобы можно было сравнивать.

Ну и зафиксируйте длину хромосомы и диапазоны генов. Иначе ваши дети родятся мутантами, которых даже мать не признает.

Шаг 2. Структура популяции: сколько бездарей брать в оборот

Популяция – это множество хромосом, с которыми вы работаете на текущем шаге (поколении). Размер популяции – это параметр, которым вы можете крутить.

Нормальный диапазон – 50–200 для большинства бытовых задач. Начинайте с этого, а потом подстраивайте под свой кошелёк по процессорному времени.

Также вам понадобятся:

Шаг 3. Отбор родителей – кого, зачем и почему

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

Обязательно знать:

Самый простой и надёжный способ – турнир с k=3. Не надо изобретать велосипед, если вы только начинаете.

Шаг 4. Кроссинговер: как правильно мешать гены

Взяли двух родителей, теперь нужно сделать детей. Кроссинговер – это когда вы меняете участки хромосом между родителями. Без кроссовера эволюция будет ползти, потому что мутации – редкие события.

Основные способы:

Что выбрать? Если ваши гены не сильно связаны между собой (например, параметры независимы), однородный кроссовер подойдет. Если есть смысл в порядке генов – лучше двухточечный. Для бинарных строк классика – одноточечный или двухточечный.

Важно! Не забудьте, что после кроссовера вы получили двух детей. Теперь их можно добавить в новую популяцию (или сначала применить мутацию, об этом ниже).

Шаг 5. Мутация: ломай всё, чтобы стать лучше

Мутация – это ваша страховка от вырождения. Иногда в популяции теряются полезные гены, и мутация может их вернуть. Или случайно найти локальный максимум, мимо которого прошёл кроссовер.

Параметры мутации:

Лучше делать мутацию на уровне генов: для каждого гена с вероятностью p_mut (например, 0.01 / длина хромосомы) применяем изменение. Это называется «погенная мутация».

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

Шаг 6. Селекция (формирование нового поколения)

Вы наделали детей. Старую популяцию надо куда-то девать. Полностью выбрасывать? А как же хорошие решения, которые мы нашли? Тут на сцену выходит элитизм.

Обязательно знать:

Самый простой рецепт:

Таким образом, размер популяции остаётся постоянным, что удобно.

Шаг 7. Алгоритм целиком (псевдокод для любого языка)

Классический ГА сдохнет в локальном минимуме быстрее, чем вы скажете «популяция выродилась». Вот что делают умные люди.

Шаг 8. Алгоритм целиком (псевдокод для любого языка)

Теперь соберём всё вместе. Даже на бейсике заведётся, если переписать аккуратно.

# Параметры
POP_SIZE = 100
MAX_GENERATIONS = 500
MUTATION_RATE = 0.05      # вероятность мутации на особь (вид 1)
MUTATION_STRENGTH = 0.1   # для вещественных генов
ELITE_COUNT = 2
TOURNAMENT_SIZE = 3

# Создать начальную популяцию (случайными хромосомами)
population = create_population(POP_SIZE)

# Основной цикл эволюции
for gen in range(MAX_GENERATIONS):
    # Оценить каждого
    for ind in population:
        ind.fitness = fitness(ind.chromosome)

    # Сортировка по убыванию фитнеса (если чем больше, тем лучше)
    population.sort(key=lambda x: x.fitness, reverse=True)

    # Элита
    new_population = population[:ELITE_COUNT].copy()

    # Пока не набрали новую популяцию
    while len(new_population) < POP_SIZE:
        # 1. Отбор родителей (турнир)
        parent1 = tournament(population, TOURNAMENT_SIZE)
        parent2 = tournament(population, TOURNAMENT_SIZE)

        # 2. Кроссинговер
        if random() < 0.7:   # вероятность кроссовера (70%)
            child1, child2 = crossingover(parent1, parent2)
        else:
            child1, child2 = parent1.copy(), parent2.copy()

        # 3. Мутация
        mutation(child1, MUTATION_RATE, MUTATION_STRENGTH)
        mutation(child2, MUTATION_RATE, MUTATION_STRENGTH)

        new_population.append(child1)
        if len(new_population) < POP_SIZE:
            new_population.append(child2)

    population = new_population

    # Логгируем лучший фитнес
    print(f"Поколение {gen}: лучший = {population[0].fitness}")

# Результат
best = population[0].chromosome

Функции create_population, fitness, tournament, crossingover, mutation и, конечно, fitness вы пишете сами под свои типы данных.

Шаг 9. Вариации и ловушки

Параллельные запуски
Если решили делать несколько независимых прогонов, то после их завершения формируете общую популяцию из лучших особей каждого прогона (например, по 2-3 штуки) и прогоняете ГА ещё несколько поколений. Это называется «метаэволюция» или просто «перестраховка параноика».

Что делать, если ГА не работает?

Когда ГА не стоит использовать?

Вместо заключения

Генетический алгоритм – это оружие массового поражения в руках ленивого, но умного программиста. Он прощает неаккуратную постановку, работает с дискретными и негладкими пространствами, не требует производных и вообще красив.

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

Пока вы тут запускаете свой ГА для подбора коэффициентов, упаковки коробок или оборудования, серьёзные дядьки проектируют космические аппараты.

Вывод: если ГА подходит для космоса, для вашей задачи склада или лабы по оптимизации он подойдёт тем более. Главное — не налажать с параметрами и вовремя добавить механизмы борьбы с преждевременной сходимостью.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Chat on Telegram