Интересные результаты может дать сочетание ГА с другими методами. И в заключение остановимся на том, когда же следует использовать генетические алгоритмы. В одних обстоятельствах они хороши, в других - не очень. В общем случае генетические алгоритмы не находят оптимального решения очень трудных задач. Если оптимальное решение задачи (например, ЗКВ с очень большим числом городов) не может быть найдено традиционными способами - например, методом сцепления ветвей (branch and bound method), - то и генетический алгоритм вряд ли найдет оптимум. С другой стороны, вполне возможно, что генетический алгоритм найдет достаточно хорошее решение. В конце концов, коммивояжеру в любом случае надо ехать продавать свои товары, даже если мы не уверены в абсолютной оптимальности маршрута! Но есть примеры, когда в очень трудных задачах, в том числе и в ЗКВ, с помощью генетических алгоритмов были получены очень хорошие решения.
В двух случаях генетические алгоритмы очень хороши. Первый случай: когда не известен способ точного решения задачи. Если мы знаем, как оценить приспособленность хромосом, то всегда можем заставить генетический алгоритм решать эту задачу. Второй случай: когда способ для точного решения существует, но он очень сложен в реализации, требует больших затрат времени и денег, то есть, попросту говоря, дело того не стоит. Пример - создание программы для составления персонального расписания на основе техники покрытия множеств с использованием линейного программирования.
Несмотря на то, что конструирование хромосом и фитнес-функций может потребовать значительных усилий, генетические алгоритмы легко реализуются даже с нуля и способны решать широкий круг задач. Используя аналогию с развитием живых организмов от простых форм к более сложным, генетические алгоритмы приблизились к тому, чтобы стать общим методом решения задач. Другие "природоподобные" парадигмы, такие как моделирование отжига (simulated annealing) и табу-поиск (taboo search), тоже позволяют решать аналогичные задачи.
5. Пример ГА: Решение Диофантова уравнения
Рассмотрим диофантово (только целые решения) уравнение: a+2b+3c+4d=30, где a, b, c и d - некоторые положительные целые. Применение ГА за очень короткое время находит искомое решение (a, b, c, d). Конечно, Вы можете спросить: почему бы не использовать метод грубой силы: просто не подставить все возможные значения a, b, c, d (очевидно, 1 <= a,b,c,d <= 30) ? Архитектура ГА-систем позволяет найти решение быстрее за счет более 'осмысленного' перебора. Мы не перебираем все подряд, но приближаемся от случайно выбранных решений к лучшим. Для начала выберем 5 случайных решений: 1 =< a,b,c,d =< 30. Вообще говоря, мы можем использовать меньшее ограничение для b,c,d, но для упрощения пусть будет 30.
Таблица 2: 1-е поколение хромосом и их содержимое
Хромосома | (a,b,c,d) |
1 | (1,28,15,3) |
2 | (14,9,2,4) |
3 | (13,5,7,3) |
4 | (23,8,16,19) |
5 | (9,13,5,2) |
Чтобы вычислить коэффициенты выживаемости (fitness), подставим каждое решение в выражение a+2b+3c+4d. Расстояние от полученного значения до 30 и будет нужным значением.
Таблица 3: Коэффициенты выживаемости первого поколения хромосом (набора решений)
Хромосома | Коэффициент выживаемости |
1 | |114-30|=84 |
2 | |54-30|=24 |
3 | |56-30|=26 |
4 | |163-30|=133 |
5 | |58-30|=28 |
Так как меньшие значения ближе к 30, то они более желательны. В нашем случае большие численные значения коэффициентов выживаемости подходят, увы, меньше. Чтобы создать систему, где хромосомы с более подходящими значениями имеют большие шансы оказаться родителями, мы должны вычислить, с какой вероятностью (в %) может быть выбрана каждая. Одно решение заключается в том, чтобы взять сумму обратных значений коэффициентов, и исходя из этого вычислять проценты. (Заметим, что все решения были сгенерированы Генератором Случайных Чисел - ГСЧ).
Таблица 4: Вероятность оказаться родителем
Хромосома | Подходящесть |
1 | (1/84)/0.135266 = 8.80% |
2 | (1/24)/0.135266 = 30.8% |
3 | (1/26)/0.135266 = 28.4% |
4 | (1/133)/0.135266 = 5.56% |
5 | (1/28)/0.135266 = 26.4% |
Для выбора 5-и пар родителей (каждая из которых будет иметь 1 потомка, всего - 5 новых решений), представим, что у нас есть 10000-сторонняя игральная кость, на 880 сторонах отмечена хромосома 1, на 3080 - хромосома 2, на 2640 сторонах - хромосома 3, на 556 - хромосома 4 и на 2640 сторонах отмечена хромосома 5. Чтобы выбрать первую пару кидаем кость два раза и выбираем выпавшие хромосомы.
Таблица 5: Симуляция выбора родителей
Хромосома отца | Хромосома матери |
3 | 1 |
5 | 2 |
3 | 5 |
2 | 5 |
5 | 3 |
Каждый потомок содержит информацию о генах и отца и от матери. Вообще говоря, это можно обеспечить различными способами, однако в нашем случае можно использовать т.н. "кроссовер" (cross-over). Пусть мать содержит следующий набор решений: a1,b1,c1,d1, а отец - a2,b2,c2,d2, тогда возможно 6 различных кросс-оверов (| = разделительная линия):
Таблица 6: Кроссоверы между родителями
Хромосома-отец | Хромосома-мать | Хромосома-потомок |
a1 | b1,c1,d1 | a2 | b2,c2,d2 | a1,b2,c2,d2 or a2,b1,c1,d1 |
a1,b1 | c1,d1 | a2,b2 | c2,d2 | a1,b1,c2,d2 or a2,b2,c1,d1 |
a1,b1,c1 | d1 | a2,b2,c2 | d2 | a1,b1,c1,d2 or a2,b2,c2,d1 |
Есть достаточно много путей передачи информации потомку, и кроссовер - только один из них. Расположение разделителя может быть абсолютно произвольным, как и то, отец или мать будут слева от черты. А теперь попробуем проделать это с нашими потомками
Таблица 7: Симуляция кроссоверов хромосом родителей
Хромосома-отец | Хромосома-мать | Хромосома-потомок |
(13 | 5,7,3) | (1 | 28,15,3) | (13,28,15,3) |
(9,13 | 5,2) | (14,9 | 2,4) | (9,13,2,4) |
(13,5,7 | 3) | (9,13,5 | 2) | (13,5,7,2) |
(14 | 9,2,4) | (9 | 13,5,2) | (14,13,5,2) |
(13,5 | 7, 3) | (9,13 | 5, 2) | (13,5,5,2) |
Теперь мы можем вычислить коэффициенты выживаемости (fitness) потомков.
Таблица 8: Коэффициенты выживаемости потомков (fitness)
Хромосома-потомок | Коэффициент выживаемости |
(13,28,15,3) | |126-30|=96 |
(9,13,2,4) | |57-30|=27 |
(13,5,7,2) | |57-30|=22 |
(14,13,5,2) | |63-30|=33 |
(13,5,5,2) | |46-30|=16 |
Средняя приспособленность (fitness) потомков оказалась 38.8, в то время как у родителей этот коэффициент равнялся 59.4. Следующее поколение может мутировать. Например, мы можем заменить одно из значений какой-нибудь хромосомы на случайное целое от 1 до 30. Продолжая таким образом, одна хромосома, в конце концов, достигнет коэффициента выживаемости 0, то есть станет решением. Системы с большей популяцией (например, 50 вместо 5-и сходятся к желаемому уровню (0) более быстро и стабильно.
5.1 Заголовок класса
#include<stdlib.h>
#include<time.h>
#include<stdio.h>
#define MAXPOP 25
struct gene
{
int alleles[4];
int fitness;
float likelihood; // Тестнаравенство
operator==(gene gn)
for (int i=0;i<4;i++)
{
if (gn.alleles[i] != alleles[i]) return false;
}
return true;
class CDiophantine
public:
CDiophantine(int, int, int, int, int); // Конструктор с коэффициентами при а,b,c,d
intSolve(); // Вычисляет решение уравнения
gene GetGene(int i)
{ return population[i];}
protected:
int ca,cb,cc,cd; // Коэффициентыприа,b,c,d
int result;
gene population[MAXPOP]; // Массивизгенов- популяция
int Fitness(gene &); // Функцияприспособленности
void GenerateLikelihoods(); // Вычисляетвероятностивоспроизведения
float MultInv();
int CreateFitnesses();
void CreateNewPopulation();
int GetIndex(float val);
gene Breed(int p1, int p2);
Существуют две структуры: gene и класс CDiophantine. gene используется для слежения за различными наборами решений. Создаваемая популяция - популяция ген. Эта генетическая структура отслеживает свои коэффициенты выживаемости и вероятность оказаться родителем. Также есть небольшая функция проверки на равенство, просто чтобы сделать кое-какой другой код покороче. Теперь по функциям:
5.2 Функция Fitness
Вычисляет коэффициент выживаемости (приспособленности - fitness) каждого гена. В нашем случае это - модуль разности между желаемым результатом и полученным значением. Этот класс использует две функции: первая вычисляет все коэффициенты, а вторая - поменьше (желательно сделать ее inline) вычисляет коэффициент для какого-то одного гена.
CDiophantine::Fitness(gene&gn) //Вычисляет коэффициет приспособленности для данного гена
{
int total = ca * gn.alleles[0] + cb * gn.alleles[1] + cc * gn.alleles[2] + cd * gn.alleles[3];
return gn.fitness = abs(total - result);
}
int CDiophantine::CreateFitnesses() // Возвращает номер гена в популяции ,
{ // кот. явл. решением данного ур-я
float avgfit = 0;
int fitness = 0;
for(int i=0;i<MAXPOP;i++)
{
fitness = Fitness(population[i]);
// avgfit += fitness;
if(fitness== 0)
{
returni;
}
}
return0; //Возвращает 0 ,если среди генов данной популяции не нашлось решения
}
Заметим, что если fitness = 0, то найдено решение - возврат. После вычисления приспособленности (fitness) нам нужно вычислить вероятность выбора этого гена в качестве родительского.
5.3 Функция Likelihood
Как и было объяснено, вероятность вычисляется как сумма обращенных коэффициентов, деленная на величину, обратную к коэффициенту данному значению. Вероятности кумулятивны (складываются), что делает очень легким вычисления с родителями. Например:
Таблица
Хромосома | Вероятность |
1 | (1/84)/0.135266 = 8.80% |
2 | (1/24)/0.135266 = 30.8% |
3 | (1/26)/0.135266 = 28.4% |
4 | (1/133)/0.135266 = 5.56% |
5 | (1/28)/0.135266 = 26.4% |
В программе, при одинаковых начальных значениях, вероятности сложатся: представьте их в виде кусков пирога. Первый ген - от 0 до 8.80%, следующий идет до 39.6% (так как он начинает 8.8). Таблица вероятностей будет выглядеть приблизительно так: