Делаем элитизм. Жесткие и мягкие ограничения

Архив проекта: ga_7.zip

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

Жесткие и мягкие ограничения

Первое, с чем мы столкнулись – это способ описания задачи на уровне хромосом:

Напомню, что здесь каждый список – это отдельный маршрут от указанной точки до соответствующей вершины графа. Причем, в каждом списке должны присутствовать номера вершин (отсчет начинается с 0) от 0 до 5 без повторов. Это пример жесткого условия, заложенного в генах при решении данной задачи. То есть, оно не должно нарушаться в процессе создания особей нового поколения. Именно поэтому, мы использовали упорядоченное скрещивание (функция cxOrdered) и мутацию в виде обычной перетасовки элементов списка (функция mutShuffleIndexes). Применять здесь другие операторы скрещивания и мутации, которые бы нарушали жесткое условие задачи, недопустимо.

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

Также в нашем примере было еще одно жесткое ограничение, связанное с отсутствием некоторых ребер в графе. Здесь, в отличие от предыдущего ограничения, в хромосомах могут появляться маршруты не соответствующие структуре графа, то есть, проходящие по несуществующим ребрам. Мы допускали такое поведение, но штрафовали его значением inf = 100 единиц. В результате, такое жесткое ограничение могло быть нарушено, но за это особь получала штраф в установленном нами размере. Этот штраф заметно увеличивал длину маршрута и, как следствие, уменьшал приспособленность особи. Поэтому, была надежда, что такие особи постепенно отсеются в процессе отбора.

Вообще существует несколько известных стратегий обработки жестких ограничений:

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

Какой способ контроля жестких ограничений использовать в задаче, конечно же, решает разработчик ГА, исходя из конкретной задачи.

Мягкие ограничения

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

Как же учитывать вклад мягких ограничений в генетическом алгоритме? Очевидно, это делается на уровне вычисления функции приспособленности. Обычно, за каждое нарушение мягкого ограничения также накладывается определенный штраф (не очень большой), который несколько ухудшает приспособленность особи. То есть, с мягкими ограничениями мы обращаемся также как и в любой задаче оптимизации, пытаясь просто сократить их число (не обязательно до нуля).

Добавляем элитизм в ГА

Давайте теперь немного видоизменим стандартную функцию algorithms.eaSimple() пакета DEAP так, чтобы она могла реализовывать механизм элитизма в главном цикле работы ГА. Ранее мы с вами уже говорили, что для некоторых задач целесообразно в популяции гарантированно сохранять несколько лучших (элитных) особей, то есть, не терять лучшие решения и не переоткрывать их заново.

К сожалению, в пакете DEAP такой функционал отсутствует. Но его легко прописать самостоятельно. Итак, я возьму стандартную функцию eaSimple() и перепишу ее следующим образом:

from deap import tools
from deap.algorithms import varAnd
 
def eaSimpleElitism(population, toolbox, cxpb, mutpb, ngen, stats=None,
             halloffame=None, verbose=__debug__, callback=None):
    """Перелеланный алгоритм eaSimple с элементом элитизма
    """
 
    logbook = tools.Logbook()
    logbook.header = ['gen', 'nevals'] + (stats.fields if stats else [])
 
    # Evaluate the individuals with an invalid fitness
    invalid_ind = [ind for ind in population if not ind.fitness.valid]
    fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
    for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit
 
    if halloffame is not None:
        halloffame.update(population)
 
    hof_size = len(halloffame.items) if halloffame.items else 0
 
    record = stats.compile(population) if stats else {}
    logbook.record(gen=0, nevals=len(invalid_ind), **record)
    if verbose:
        print(logbook.stream)
 
    # Begin the generational process
    for gen in range(1, ngen + 1):
        # Select the next generation individuals
        offspring = toolbox.select(population, len(population) - hof_size)
 
        # Vary the pool of individuals
        offspring = varAnd(offspring, toolbox, cxpb, mutpb)
 
        # Evaluate the individuals with an invalid fitness
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = toolbox.map(toolbox.evaluate, invalid_ind)
        for ind, fit in zip(invalid_ind, fitnesses):
            ind.fitness.values = fit
 
        offspring.extend(halloffame.items)
 
        # Update the hall of fame with the generated individuals
        if halloffame is not None:
            halloffame.update(offspring)
 
        # Replace the current population by the offspring
        population[:] = offspring
 
        # Append the current generation statistics to the logbook
        record = stats.compile(population) if stats else {}
        logbook.record(gen=gen, nevals=len(invalid_ind), **record)
        if verbose:
            print(logbook.stream)
 
        if callback:
            callback[0](*callback[1])
 
    return population, logbook

Несмотря на большое число строк кода, все достаточно просто. Здесь используется объект halloffame (зал славы), который содержит список лучших особей в текущей популяции. Он и будет использован для хранения и дублирования элитных особей. Вначале мы определяем размер «зала славы» (переменная hof_size), а затем, в главном цикле делаем отбор индивидуумов меньше на эту величину (так как потом, добавим hof_size элитных особей в эту выборку). После этого выполняются стандартные шаги по скрещиванию и мутации отобранных особей. И только после этого добавляем в популяцию следующего поколения элитных особей. Это гарантирует их неизменность. Далее идет формирование нового списка «зала славы», так как после скрещивания и мутации в популяции могут появиться другие еще более лучшие особи. В результате, на каждой итерации мы всегда имеем неизменное число лучших индивидуумов (решений задачи).

Кроме того, в эту же функцию я добавил параметр callback, который позволяет вызывать пользовательскую функцию на каждой итерации работы ГА. С помощью этого параметры мы будем отображать текущее лучшее решение в виде графа. Для этого, мы объявим функцию show():

def show(ax):
    ax.clear()
    show_graph(ax, hof.items[0])
 
    plt.draw()
    plt.gcf().canvas.flush_events()

Создадим окно с осями при включенном интерактивном режиме:

plt.ion()
fig, ax = plt.subplots()

Вызовем созданную функцию ГА:

population, logbook = algelitism.eaSimpleElitism(population, toolbox,
                                        cxpb=P_CROSSOVER/LENGTH_D,
                                        mutpb=P_MUTATION/LENGTH_D,
                                        ngen=MAX_GENERATIONS,
                                        halloffame=hof,
                                        stats=stats,
                                        callback=(show, (ax, )),
                                        verbose=True)

Здесь мы в параметре callback указываем ссылку на функцию show(), а вторым элементом – кортеж параметров этой функции.

В конце пропишем функцию для получения статистики, отображения лучшего решения в консоли и отключения интерактивного режима:

maxFitnessValues, meanFitnessValues = logbook.select("min", "avg")
 
best = hof.items[0]
print(best)
 
plt.ioff()
plt.show()

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