Найти в Дзене
Бычков Дмитрий

Универсальный генетический алгоритм в MathCad

Оглавление

В этой статье я продемонстрирую как можно написать универсальный генетический алгоритм с переопределяемыми функциями в MathCad15.

Наверное у тебя может возникнуть вопрос "Зачем?" и "Почему?". На самом деле эти вопросы имеют один ответ: "Мне очень нравятся эволюционные алгоритмы и я могу говорить о них часы напролёт, и хочу просто рассказать о том, что так тоже можно".

Разумеется написанный алгоритм будет не "вообще" универсальный, а "разумно" универсальный.

Будем пользоваться "скрытой" возможностью маткада передавать функции в качестве аргументов в другие функции. Если вы умеете в C++ вы можете увидеть сходство с указателями на функции или даже шаблонными функциями. (когда-нибудь напишу статейку для версии ГА на плюсах и на других языках до которых успел дотянутся...)

Это не гайд и не учебный материал, просто демонстрация возможности, хотите - пользуйтесь, не хотите - не пользуйтесь.

Итак, начнем с самой структуры ГА (Генетического Алгоритма), она вполне стандартна:

  • Начальная популяция (nachPop)
  • Мутация (mutation)
  • Скрещивание (crossingover)
  • Вычисление приспособленности (adaption)
  • Отбор (selection)
Базовая структура ГА
Базовая структура ГА

Начальная популяция.

Для формирования ГА в обобщенном виде, нужно придумать функцию формирования начальной популяции для произвольной особи (заданной пользователем), но сама функция должна работать только на уровне популяции, т.е. функция должна обрабатывать только и только особей популяции без знания о том, как они устроены!

Устройство конкретной особи (точнее каждой особи) должно быть возложено на пользователя и быть определено позднее.

Чтобы сделать задуманное сформируем функцию например так:
передадим в неё параметр
k отвечающий за количество особей в популяции,
некоторые параметры
А и В, имеющие смысл минимального и максимального порога (границы) генерации начальных значений особей (мне было лень придумывать им осмысленные названия и я просто написал их так...)
и функцию
osob(A,B) которая будет генерировать конкретную особь.

Формирование начальной популяции
Формирование начальной популяции

Именно функцию osob(A,B) впоследствии и будет определять пользователь (главное не напутать с сигнатурой, что очень легко сделать в маткаде, хаха )

Функция примитивна донельзя, она просто в цикле заполняет массив особями сгенерированными функцией osob(A,B), и всё, остальное - не её забота.

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

Хорошо, дальше идут

мутации...

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

Например так:
На вход функция получает массив особей (популяцию)
Pop,
параметр
С имеющий смысл вероятности мутации некоторой особи (от 0 до 1),
функцию мутации
mut и её параметр maxMut соответствующий максимальной амплитуде мутаций.

Обобщенные мутации
Обобщенные мутации

Функция работает очень просто:

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

На очереди скрещивание.

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

-5

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

Вычисление приспособленности.

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

-6

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

Последняя функция.

Отбор.

Отбор можно было бы тоже сделать обобщенным, но он может и обойтись.

-7

В начале проверяем что бы размеры массивов совпадали.

Далее "склеиваем" два массива в матрицу, сортируем матрицу по нулевому столбцу и вырезаем первый столбец в количестве k особей, формируя тем самым список особей с самым низким значением функции fOpt из всей популяции.

Результат.

В итоге имеем следующий ГА на основе всех наших функций определенных выше. Я решил передавать в него управляющие параметры массивом PARAMS содержащим всё необходимое.

-8

Что с ним делать?

Примеры.

Пример 1. Я написал что это пример, но это отчасти и так и не так. Раз мы написали алгоритм, надо бы проверить работает ли он. Для этого можно воспользоваться какой-нибудь задачей ответ на которую нам известен.

Например найти минимум квадратной параболы. Мы зададим её так, что бы минимум был в точке х=10, это легко сделать:
y(x) = (x - 10)^2

Вот её график.

-9

Т.е. иными словами, функция минимум которой нам предстоит найти уже нам известна (нам известен и ответ, но ГА его ещё не знает, это мы и проверяем, найдет ли он его???).

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

Функция y(x) будет применяться в вычислении приспособленности особей, и следовательно её мы поместим в определяемую пользователем функцию fOpt.

Пусть она будет такой (особь - это просто число):

-10

Раз особь это просто число, то функция формирования случайной особи osob будет тоже простой: случайное число от минимума до максимума.

-11

Мутация - это увеличение или уменьшение нашего числа, на случайную величину ограниченную амплитудой мутации.

-12

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

-13

Запускаем алгоритм с набором параметров:

-14

И сам алгоритм с определенными выше функциями:

-15

Что мы видим в результате:

-16

Отлично, но хочется посмотреть на динамику поиска. Можно чуть доработать ГА, добавив запись лучшей особи в отдельный массив, и выводить этот массив, тогда можно будет посмотреть анимацию поиска решения:

-17

Пример 2. А что на счет задачи посложнее, для поиска по 2-м параметрам, какой-нибудь функции наподобие. Пусть это будет функция:
z(x,y) = (x - X0)^2 + (y - Y0)^2
где: X0 - оптимум по оси X; Y0 - оптимум по оси Y.
В нашем случае я задал им значения: X0=27 и Y0=11.63

Ясное дело что раз нам нужны 2 числа, то особь будет состоять из двух чисел, из массива длинной = 2.

Тогда функция - генератор особи - будет такой:

-18

Мутации аналогично предыдущему случаю, но с текущей функцией генерации особи:

-19

Сама функция минимум которой будем искать:

-20

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

-21

Раз у нас есть динамика сразу приведу её, вот так алгоритм искал точку оптимума (X0; Y0) путем скрещивания, мутаций и отбора:

-22

Пример 3. Ну ладно, здесь оптимум заранее подавался в функцию, это может показаться не совсем честно, что будет если взять какую-нибудь задачу поинтереснее, например задачу аппроксимации экспериментальных данных некоторой кривой.

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

-23

На графике выглядят они так:

-24

Попробуем аппроксимировать их "прямой"
y(x) = a*x + b

Я задал её отдельно в виде функции по имени polinom:

-25

Аппроксимировать - значит найти такие значения коэффициентов a и b, что отклонение (сумма квадратов отклонений) прямой от исходного набора точек будет минимально.

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

Мутации и скрещивание тоже не изменятся.

А вот функция по которой вычисляется приспособленность изменится (она подчеркивается потому, что названа как и предыдущая, не обращайте внимания...):

-26

В качестве приспособленности она возвращает сумму квадратов отклонений от набора точек (она просто считает чему равно значение прямой в известных из набора точках X и сравнивает эти значения с теми Y которые есть в наборе и соответствуют X)

Ну что ж, в результате запуска ГА получаем следующее решение:

-27

И даже можно глянуть динамику поиска:

-28

Пример 4. Попробуем посмотреть что будет в случае аппроксимации параболой:

-29

Здесь уже нужно найти три значения, значит и особь будет соответствующая:

-30

И вслед за этим совсем чуть-чуть изменится функция вычисления приспособленности особи (оптимизируемая функция):

-31

После запуска имеем такое решение:

-32

И динамика его поиска:

-33

Выглядит достойно))) или как минимум визуально правдоподобно.

Пример 5. Попробуем аппроксимировать данные полиномом произвольной степени:
y(x) = a + b*x + c*x^2 + ... + k*x^(n-1) + p*x^n

Функция polinom для него изменится, во первых для неё нужно очень много параметров - поэтому рационально предавать в неё непосредственно особь, а во вторых она будет иметь внутри цикл вычисляющий последовательно каждый член этого полинома (можно было бы использовать встроенную сумму):

-34

Особь будет иметь n+1 число элементов, где n - степень полинома. поэтому функция генерирующая особь, будет параметрически зависеть от параметра n:

-35

Функция вычисления приспособленности почти не изменится:

-36

В результате запуска ГА для n=4 (полином 4-й степени), получим следующее решение:

-37

И динамика поиска:

-38

Пример 6. Вообще, визуально, решение для n=4 выглядит как полином 3-й степени...

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

Искать будем решение для n=3 (количество чисел будет = 4):

-39

Здесь параметры A и B - массивы, поэтому для каждого параметра особи имеется свой собственный минимум и максимум генерации значения. (конечно можно задать их более универсально, но пока так)

Функция полинома от особи:

-40

Все остальное +- тоже самое, но теперь у нас появляются 3 массива с границами:

-41

Что бы пользоваться ими внутри ГА передадим их в параметры:

-42

И теперь параметры и функции передадим в ГА:

-43

В результате получаем:

-44

И динамика поиска:

-45

Итоги.

В результате непродолжительной деятельности удалось написать "универсальный" генетический алгоритм, НО его можно сделать гораздо универсальнее если добавить возможность проводить разные отборы и скрещивания.

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

п.с. вот кстати ссылка на исходник, на моём гугл-диске