|
НАИБОЛЕЕ ПРИГОДНОЕ РЕШЕНИЕ уравнения f(x) = х + |sin(32х)| - это значение х, которое дает максимальное значение функции f(x). Ответ: х = 3,09346401 (в радианах), представляемый цепочкой 1111110000010100. |

СОГЛАСНО учению Дарвина, жизнь — это борьба, в которой лишь сильнейшие выживают и производят потомство. То, что утверждает жизнь, оказывается полезным и при решении задач на компьютере. В частности, программы, использующие так называемые генетические алгоритмы, находят решения путем применения механизма естественного отбора. Сначала алгоритм выдвигает возможные решения. Затем, подобно биологическим организмам, решения «скрещиваются», или обмениваются «генами», в каждом поколении. Иногда происходят мутации, или спонтанные изменения в генах. Только «сильнейшие» решения выживают и могут породить даже еще лучшие решения (см. статью Джона Холланда «Генетические алгоритмы» на с. 32). Чтобы понять, как работает генетический алгоритм, возьмем один из простейших и с его помощью проведем некоторые эксперименты.
Для реализации своего алгоритма я выбрал язык С, но с тем же успехом можно воспользоваться любым другим языком программирования. У меня ушло около двух дней на то, чтобы написать версию, которую мы здесь рассмотрим. Больше всего времени я потратил на создание сервисных процедур, чтобы программу было удобно выполнять, изменять установку параметров, показывать результирующие популяции и статистические данные об их поведении.
Советую разбить работу по созданию генетического алгоритма на два этапа. Сначала напишите процедуры (некоторые из них рассмотрены ниже), позволяющие следить за поддержанием жизнеспособности популяции и за поведением наиболее сильных ее представителей в каждом поколении. Ваша версия должна порождать случайные популяции особей, и в ней должны быть подпрограммы,
приписывающие этим особям тот или иной показатель жизнеспособности. Программа должна также демонстрировать особи и их жизнеспособность, сортировать их по уровню жизнеспособности и вычислять среднюю величину этого показателя в популяции. На втором этапе добавьте подпрограммы, реализующие сам генетический алгоритм. Другими словами, ваша программа должна выбирать родителей из имеющейся в данный момент популяции и модифицировать потомство этих родителей с помощью механизма перекрещивания и мутаций.
Так как я впервые составлял генетический алгоритм, то решил воспользоваться простой задачей, решение которой мне было известно. Такой подход позволил мне упростить отладку программы и лучше понять работу алгоритма. Приспособить же его для решения более сложных задач не составляло особого труда.
Для эксперимента я выбрал задачу об отыскании максимального значения функции f(x) = х + |sin(32x)|. Значения х (называемые областью определения функции) я ограничил диапазоном от 0 до т. Так что функция имела 32 периода колебаний, прибавляемых к прямой линии f(x) = х (см. рисунок слева). Поскольку f(x) всегда положительна, ею можно воспользоваться как критерием жизнеспособности. Цель заключалась в том, чтобы найти индивидуальную цепочку (значение х), которая давала бы наибольшее значение для f(x).
Первым делом надо решить, каким образом следует представлять структуры из области определения задачи, чтобы ваш генетический алгоритм мог их обрабатывать. Я представил значения х в виде двоичных цепочек, как это описано в статье Холланда. В своих первоначальных экспериментах я принял длину L цепочек равной 16 битам (L = 16). Таким образом, у меня было 2L = 216 = 65536 возможных значений х. (С увеличением длины цепочек повышается точность решения задачи.) Далее, значения х я поставил в соответствие этим 2L цепочкам следующим образом: цепочка 0000000000000000 представляет х = 0, 0000000000000001 — это х = 1(p/2L), 0000000000000010 — 2(p/2L) и так далее вплоть до 1111111111111111, что соответствует максимальному значению области определения, х = (2L - 1) * (p/2L) = p - p/2L. (Само значение p
|
ДВЕ СХЕМЫ соответствуют центральным областям каждого пика f(х). Они включают в себя и наиболее пригодные точки. Здесь показаны лишь три последних пика. |
исключается, поскольку не остается цепочки для его представления.) Чтобы проверить свою интуицию, попробуйте сообразить, какое значение представляет цепочка 1000000000000000?
Короче говоря, двоичные цепочки представляют 2L значений х, которые равномерно распределены на интервале между 0 и p. Следовательно, алгоритм производит поиск в пространстве двоичных цепочек, или, что эквивалентно, — поиск среди всех возможных значений х. Фрагмент программы на языке С на с. 84 (алгоритм отображения) показывает, каким образом можно реализовать это отображение от двоичной цепочки, которая хранится как элемент символьного массива в рамках языка С. Например, он отобразит 1000000000000000 (приблизительно) на точку p/2. Степень пригодности индивидуальной двоичной цепочки S как раз измеряется самой функцией f(x), где х = МарStringToX(S).
Теперь, когда мы располагаем этим 'отображением двоичных цепочек на область изменения функции f(x), было бы поучительно рассмотреть вопрос об отображении целевых областей так, как это было сделано у Холланда. Например, схема 0*************** состоит из всех цепочек, начинающихся с 0. Здесь * означает «безразличное» значение: не важно, что стоит на этом месте — 0 или 1. Последовательность О*************** отражается на первую половину области определения, т. е. между 0 и p/2. Последовательность 1**************** соответствует другой ее половине между p/2 и p. Для функции f(x) среднее значение на области 1*************** выше,
чем на 0***************. Таким образом, в случайной популяции цепочек естественно ожидать, что средняя пригодность особей в первой области выше, чем среди представителей второй.
В моей реализации присутствуют также некоторые подпрограммы, прослеживающие за особями в любой области, заданной пользователем (например, все цепочки, начинающиеся с 1 или заканчивающиеся 001, и так далее). В каждом поколении программа будет подсчитывать число особей, которые принадлежат каждой области. Она будет также вычислять среднюю пригодность среди этих особей, оценивая, таким образом, пригодность областей.
Чтобы проявить более глубокую интуицию, попытайтесь отразить другие последовательности на области изменения х и исследовать случайные особи, которые попадают в эти области. Можете ли вы указать последовательности, разбивающие каждое колебание в f(x) на восемь непрерывных областей? Какова средняя пригодность особей в этих областях?
ПОСЛЕ того как вы убедитесь, что первая версия вашей программы работает правильно, можно добавить процедуры, реализующие основу генетического алгоритма. Алгоритм, которым я воспользовался, представлен на с. 84.
Программа EvaluatePopulation представляет собой простой цикл по всем особям популяции, присваивающий каждой из них значение f(x) как меру ее пригодности, или жизнеспособности. Процедура DoTour-namentSelection проводит серию единоборств между случайно выбранными особями и обычно (но не всегда) копирует сильнейшую особь пары (i, j) в NewPop. Одна из версий этого алгоритма показана на с. 84. Здесь URandOl возвращает (равномерно распределенные) значения между 0 и 1. В результате более пригодные особи имеют тенденцию производить более многочисленное потомство в новом поколении по сравнению с менее пригодными. Тем не менее может оказаться полезным копировать сильнейшую особь новых популяций в каждом поколении.
Заметим, что
при значениях
Urand0l, больших 0,75, влияние фактора отбора становится более сильным (менее
пригодным цепочкам становится труднее попасть в NewPop), в то время как меньшие значения приводят к снижению влияния
механизма отбора. Если используется значение
0,5, то никакого отбора, вообще говоря, не будет.
Процедура ModifyPopulation должна случайным образом производить операцию скрещивания на некоторых парах в NewPop; обычно процент скрещивания равен 75 (т. е. скрещиваются 75% пар). В своем алгоритме я пользовался операцией «одиночной точки», которая в статье Холланда осуществляла кроссинговер. В моей версии также имеет место операция мутации, которая изменяет небольшое, случайно выбранное число битов отдельных цепочек, подставляя 0 вместо 1, и наоборот. Процент мутаций обычно составляет 0,5% на бит — другими словами, каждый бит имеет вероятность подвергнуться мутации, равную 0,005.
После того как работа по реализации алгоритма завершена, можно приступать к экспериментам. Попробуйте запустить систему со случайной популяцией из 200 особей и продолжайте эксперимент в течение 50 поколений, собирая статистику о том, как изменяется средняя пригодность, и отмечая наивысшие результаты, достигнутые отдельными особями в каждом поколении. Вы можете
|
Фрагменты программ АЛГОРИТМ ОТОБРАЖЕНИЯ — позволяет двоичным цепочкам представлять значения х. (Заметим: компилятор определяет М_Р1 как число p double scalefactor = (double) M_PI/pow((double) 2.0, (double) L); double MapStringToX (char * S) {/* поместить биты из S в правый конец г и сдвинуть их влево */} int i, r; for (I = г = 0; i < L; + +1, + +S) { г<<=1; /* сдвиг битов влево, поместить 0 в правый бит */ if (*S= =T) /*если в S 1, то ... */ ++г; /* заменить самый правый бит на 1*/ } return ((double) r * scalefactor); }
ОСНОВНОЙ ГЕНЕТИЧЕСКИЙ АЛГОРИТМ — повторно осуществляет операции скрещивания, мутаций и отбора /* создать структуру для хранения популяций */ struct PopStr { char Genome [L + 1]; double Fitness; } Pop[PopSize], NewPop[PopSize]; /* реализация алгоритма */ GenerateRandomPopulation (Pop); EvaluatePopulation (Pop); while (Not_Done) ( DoTournamentSelection (Pop, NewPop); ModifyPopulation (NewPop); Switch (Pop, NewPop); EvaluatePopulation (Pop); }
АЛГОРИТМ ОТБОРА — отбирает и копирует наиболее пригодные особи в следующее поколение while (NewPopNotFull) { /* выбрать случайно две особи из Pop */ i = random () % PopSize; j = random () % PopSize; /* возвращает случайное число между 0 и 1 */ if(URand01 ()<0.75) копировать сильнейшего из Pop[i], Pop[j] в NewPop else копировать слабейшего из Pop[i], Pop[J] в NewPop |
жете сравнивать полученные результаты для различной численности популяций и различных процентов скрещивания и мутаций. Мне интереснее всего было сравнивать средние результаты по нескольким прогонам программы при различных исходных значениях, вырабатываемых генератором случайных чисел.
Один из наиболее полезных экспериментов для понимания динамики эволюционирующих популяций заключается в том, чтобы прослеживать за особями из'восьми областей, разбивающих 32 периода f(x), о которых я говорил выше. Эти области определяются последовательностями
*****000********, *****001******** и так далее до *****111********. Заметим, что последовательности *****011******** и
*****100******** лежат в центральной части каждого
пика, которая содержит точки наибольшей пригодности.
В первоначально случайной популяции должно быть приблизительно равное число особей в каждой из восьми последовательностей. По мере работы алгоритма вы начнете замечать, что в среднем в двух центральных последовательностях будет накапливаться больше особей, чем в остальных шести. Такой результат получается, несмотря на то что многие члены центральных областей отображаются на низкие значения х и, следовательно, имеют низкую пригодность. Объяснение этого феномена состоит в том, что средняя пригодность центральных областей выше по сравнению с другими.
Пользуясь этими восемью областями, я пытался проследить, насколько хорошо алгоритм настраивается
на поиск особей, отличающихся высокой пригодностью. В своей программе я предусмотрел фиксирование в каждом поколении числа особей в каждой области, средней пригодности особей в этих областях и средней пригодности популяции в целом. Теоремы, упомянутые в статье Холланда, предсказывают, что число особей в областях с осредненной пригодностью, которая выше средней величины для всей популяции, должно возрастать, в то время как численность особей в областях с пригодностью ниже среднего уровня должна сокращаться.
При размере популяции в 400 особей, проценте скрещивания 75% и проценте мутаций 0,1% программа подтверждала предсказания теории в 67% случаев. Я пришел к выводу, что, чем выше процент скрещивания и мутаций и чем меньше численность популяции, тем реже подтверждаются предсказания. Эти результаты можно разумно объяснить, как мне кажется, тем, что высокая частота скрещивания и мутаций способствует разрешению хороших «строительных блоков», и для популяций меньшего размера ошибки в случайных выборках «размывают» предсказываемые закономерности.
После того как ваш генетический алгоритм поработает на многих поколениях, вы, вероятно, заметите, что большая часть популяции состоит из очень похожих, если не идентичных, цепочек. Этот результат, называемый сходимостью, объясняется тем, что генетический алгоритм загоняет популяцию во все более узкие целевые области. Зачастую популяция сходится к одной особи, которая не является оптимальной. По существу алгоритм застревает на локальном экстремуме. Сходимость может практически привести к вырождению, поскольку она означает, что скрещивание не даст существенного эффекта в поисках лучших особей. Скрещивая две идентичные цепочки, мы получим такие же две цепочки, и ничего нового не произойдет.
Поиск способов избежать нежелательной сходимости — очень активная область исследований. Один из возможных механизмов заключается в том, чтобы направлять процесс отбора, стремясь поддерживать разнообразие популяции. По существу механизм подобного рода стимулирует особи занимать много различных областей пространства поиска в численностях, пропорциональных средним значениям пригодности, ассоциированных с этими областями,
аналогично тому, как в естественной эволюции различные виды населяют свои ниши.
Конечно, вы можете применять генетический алгоритм не только к задачам оптимизации. Мне понадобилось всего около четырех часов на то, чтобы изменить описанную здесь программу и создать на ее основе версию, в которой особи представляют стратегии игры для одной из разновидностей «Дилеммы заключенного». В этой игре каждый из двух заключенных стоит перед выбором: либо проявить солидарность с соучастником (храня молчание о совместном преступлении) и получить три года, либо попытаться выйти на свободу, выдав сообщника. К сожалению, вам не известно, что собирается делать партнер: если вы молчите, а он выдает вас, то вы получаете 10 лет, а он выходит на свободу. Если же оба выдают друг друга, то каждый получает по семь
Мой модифицированный генетиче
ский алгоритм исследовал стратегии этой игры множество раз, играя с одним и тем же партнером. Двоичная цепочка каждой особи представляет два числа (р, q), где р — вероятность того, что особь будет молчать, если партнер молчал в предыдущей партии, a q — вероятность того, что особь промолчит, если партнер ее выдал. Например, (1;0) — это стратегия «око за око», а (0,1; 0,1) приблизительно соответствует стратегии «всегда выдавай».
Генетический алгоритм генерирует очень интересную динамику в процессе эволюции таких стратегий, особенно если двоичная цепочка каждой особи содержит некоторые биты, представляющие произвольную метку. Другой набор битов мог бы обозначать склонность особи играть только с теми партнерами, цепочки которых также содержат такую метку. На самом деле в популяции могут развиться особи, распознающие внешние «признаки»
(метки), ассоциированные с игроками, придерживающимися стратегии «око за око». Таким образом, они могут процветать, отыскивая в качестве партнеров солидарных особей и избегая предателей.
Из сравнительно простой генной структуры может развиться сложная эволюционная динамика. Например, могут появиться подражание и другие формы обмана: предатель может иметь метку, ассоциированную с солидарными игроками. Дав волю своему воображению, вы сможете построить и другие несложные генные структуры и, воспользовавшись генетическим алгоритмом, выудить из своего компьютера сложные эволюционные процессы.
Если вы желаете получить копию оптимизационной программы (написанной на языке С), пошлите запрос на английском языке по адресу:
Amateur Scientist, Scientific American, 415 Madison Avenue, New York, NY 10017-1111.