Давайте рассмотрим наипростейшую модель естественного отбора. В сети встречал модель с двумя параметрами-генами, а у нас будет всего один, при сохранении наглядности. Модель настолько элементарна, что её можно обсудить даже со своим ребёнком (проверил со своей шестилетней дочкой).
NB: Весь код в статье интерактивный, кликайте, чтобы открыть, запустить, попробовать свои идеи сразу на ходу. Используется Python + p5py, который разрабатывался для книги для детей, преподавания в Универе, детских кружках и школе.
Внимание: 21 гифка, 29 фрагментов кода и 12 ссылок на запускаемый код.
Прост, как localhost
У нас будет гордый птыц со всего одним геном, который хранит направление полёта из гнезда к еде. Альфа-ген. Птыц летит куда-то в соответствии со своим геном направления и каждый ход тратит на это энергию.
Если птыц добудет еду, он:
-
восстановит свою энергию;
-
вернётся в гнездо;
-
и там даст два потомка, передав свой ген с мутациями.
Если не добудет, то просто отразится от стенок и полетит назад. Когда энергия закончится, он умрёт. Гнездо — в центре экрана. Еда спавнится где повезёт.
Часть первая. Подготовка
Итак, нужна птица, направление и факт полёта. Для начала сама птица:
bird_position = Vector(width / 2, height / 2)
И её отображение:
def display_bird():
text_size(140)
text_align(CENTER, CENTER)
text("🐦", bird_position.x, bird_position.y)
Всё вместе (чтобы можно было запустить):
from p5py import *
import random
run()
# Устанавливаем размеры окна
size(400, 400)
# Инициализация позиции птицы
bird_position = Vector(width / 2, height / 2)
# Функция для отображения птицы
def display_bird():
text_size(140)
text_align(CENTER, CENTER)
text("🐦", bird_position.x, bird_position.y)
# Вечный игровой "цикл"
def draw():
background(30, 30, 40) # Устанавливаем цвет фона
display_bird() # Отображаем птицу
Код — нажмите, чтобы запустить сразу в браузере.
Поменяйте птицу на что-нибудь забавное. Можно исследовать не только в браузере. Модуль также доступен как
p5
для VS Code, но в нём не реализовано отображение эмоджи, поэтому придётся поменять наellipse(x, y, radius
).
Теперь направление. Единственный ген птицы — это угол полёта
. Он будет наследоваться и иногда мутировать.
bird_angle = random.uniform(-PI, PI)
И скорость:
bird_speed = 4
Клюв поверни, и полетели…
def move_bird():
# Обновляем координаты птицы в зависимости от её скорости и угла
bird_position.x += bird_speed * cos(bird_angle)
bird_position.y += bird_speed * sin(bird_angle)
Всё вместе:
from p5py import *
import random
run()
size(400, 400)
bird_position = Vector(width / 2, height / 2)
bird_angle = random.uniform(-PI, PI)
bird_speed = 4
def move_bird():
bird_position.x += bird_speed * cos(bird_angle)
bird_position.y += bird_speed * sin(bird_angle)
def display_bird():
text_size(140)
text_align(CENTER, CENTER)
text("🐦", bird_position.x, bird_position.y)
Где еда, Лебовски?!
Чтобы еда не спавнилась на курице, используем полярные координаты.
food_angle = rand(-PI, PI)
food_distance = 3 * width / 5
food_position = Vector(width / 2 + food_distance * cos(food_angle), height / 2 + food_distance * sin(food_angle))
food_size = 140
def display_food():
text_size(food_size)
text_align(CENTER, CENTER)
text("🍞", food_position.x, food_position.y)
Возвращайся, сделав круг
Пусть птица возвращается, если долетела до края экрана или до еды.
Если она нашла еду, то возвращается в гнездо в центре экрана, чтобы дать потомство — две новых птицы. Они наследуют всё множество родительских генов, то есть один ген — направление.
Сначала просто возвращение:
def move_bird():
global bird_angle
bird_position.x += bird_speed * cos(bird_angle)
bird_position.y += bird_speed * sin(bird_angle)
# Проверка на достижение края экрана
if (bird_position.x < 0 or bird_position.x > width or
bird_position.y < 0 or bird_position.y > height):
# Пусть летит в центр, инвертируем угол
invert_angle()
# Проверка на достижение пищи
if (dist(bird_position.x, bird_position.y, food_position.x, food_position.y) < food_size / 2):
invert_angle()
Классы
Уже сейчас понятно, что одна птица обречена на вымирание, если при рождении её угол не совпал с углом еды. Поэтому у нас будет много птиц. Мы же популяцию исследуем. Давайте сделаем рефакторинг и определим класс птиц. Так будет легче потом ими управлять.
class Bird:
def __init__(self, x, y, speed):
self.position = Vector(x, y)
self.angle = rand(-PI, PI)
self.speed = speed
def move(self):
self.position.x += self.speed * cos(self.angle)
self.position.y += self.speed * sin(self.angle)
if (self.position.x < 0 or self.position.x > width or
self.position.y < 0 or self.position.y > height):
self.invert_angle()
if (dist(self.position.x, self.position.y, food_position.x, food_position.y) < food_size / 2):
self.invert_angle()
def invert_angle(self):
self.angle = self.angle + PI
def display(self):
text_size(140)
text_align(CENTER, CENTER)
text("🐦", self.position.x, self.position.y)
bird = Bird(width / 2, height / 2, 4)
Энергия
Пусть у птицы будет энергия, которая уменьшается ход за ходом. Если птица нашла еду, то энергия восстанавливается.
INITIAL_ENERGY = 700 # Начальное количество энергии у птиц
А при движении энергия уменьшается.
def move(self):
if not self.alive:
return
# Тратим энергию
self.energy -= self.speed
# Проверка, осталась ли энергия
if self.energy <= 0:
self.alive = False
return
Заодно добавили атрибут alive
— жива ещё старушка или уже нет. Прозрачностью покажем, что жизнь на исходе и пора сожалеть об упущенных возможностях.
def display(self):
text_size(140)
text_align(CENTER, CENTER)
fill(255, lerp(self.energy, Bird.INITIAL_ENERGY, 0, 255, 0))
text("🐦", self.position.x, self.position.y)
Но применение прозрачности к эмоджи — ненадёжное дело: где-то работает, а где-то нет. Если что, можно заменить
text()
на простойellipse(x, y, r)
.lerp()
из p5.js (и p5py) соотносит один интервал с другим — масштабирует. Энергияself.energy
в интервале от 0 до максимальной будет масштабирована так, чтобы уместиться в диапазон от 0 до 255.
А если пернатое коснулось еды, то энергия восстановилась, птица ягодка опять.
if (dist(self.position.x, self.position.y, food_position.x, food_position.y) < food_size / 2):
self.invert_angle()
self.energy = Bird.INITIAL_ENERGY
Размножение
Прежде чем перейти к размножению, наплодим хотя бы сотню птиц.
Видно, как быстро вымирают те, кому «повезло» родиться с геном угла, не совпадающим с направлением на еду:
# Параметры симуляции
NUM_BIRDS = 100
# Создаем массив из птиц
birds = [Bird(Vector(width / 2, height / 2), rand(-PI, PI)) for _ in range(NUM_BIRDS)]
def draw():
background(30, 30, 40)
display_food()
for bird in birds:
bird.move()
bird.display()
Введём признак того, что птица накушалась и летит в гнездо размножаться:
self.is_returning = False
И главный фактор для нас, вероятность мутации гена:
MUTATION_RATE = 0.2 # Вероятность мутации направления у потомков
Добавим метод репродукции:
def invert_angle(self):
self.angle = self.angle + PI
def update(self):
self.move()
self.display()
# Проверяем, находится ли птица в круге и возвращается ли она
center_position = Vector(width / 2, height / 2) # Центр экрана
if self.is_returning and self.position.dist(center_position) < 5: # - радиус круга размножения
self.is_returning = False
return self.reproduce()
return []
def reproduce(self):
new_birds = []
self.invert_angle()
if rand(0, 1) < MUTATION_RATE:
angle = rand(-PI, PI) # Большая вариация угла
else:
angle = self.angle + rand(-0.1, 0.1) # Небольшая вариация угла
new_birds.append(Bird(Vector(200, 200), angle))
new_birds.append(Bird(Vector(200, 200), angle))
return new_birds
С вероятностью MUTATION_RATE
потомки будут иметь случайный угол. Во всех других случаях они будут иметь незначительное отклонение угла, чтобы визуально птицы не сливались.
Сначала все птицы гордо разлетаются из гнезда в разные стороны:
Но потом вымирают те, кому не повезло с геном и которые не нашли еды:
А кто еду нашёл, тот молодец. Притащил её в гнездо и произвёл потомков, у которых скопировался родительский ген.
Да, у нас тут для упрощения — клонирование, почкование, бесполое размножение. Если бы не это, можно было бы претендовать на полноценный генетический алгоритм.
Видно, как популяция приспособилась к окружающей среде и размножилась. Я добавил отображение хлебушка, который они домой несут, чтобы отличить счастливчиков по жизни.
Изменчивый мир. Стоит ли прогибаться?
Наступает самое интересное! Окружающая среда меняется: через какой-то интервал еда появится в новом месте (старую съели).
Пространство для экспериментов. Сейчас не реализовано, но можно уменьшать размер еды, когда птицы её растаскивают. И посмотреть, как такое изменение окружающей среды скажется на популяции.
FOOD_CHANGE_INTERVAL = 250 # Интервал изменения позиции еды
Для удобства перенесём работу с едой в класс:
class Food:
def __init__(self):
self.size = 140
self.get_random_position()
def get_random_position(self):
food_angle = rand(-PI, PI)
food_distance = 3 * width / 5
self.position = Vector(width / 2 + food_distance * cos(food_angle), height / 2 + food_distance * sin(food_angle))
def display(self):
text_size(self.size)
text_align(CENTER, CENTER)
fill(255)
text("🍞", self.position.x, self.position.y)
Запускайте наш главный код здесь и экспериментируйте.
Первым делом — баголёты
Сейчас есть смешной баг: NPC застревают в текстурах. Если еда появляется там, где уже есть птицы, то они начинают дёргаться почти на одном месте из-за того, что при смене угла (чтобы вернуться в гнездо) они не успевают улететь за пределы еды.
Как поправить? Будем менять направление только если птица не возвращается.
Часть вторая. Эксперименты
Как действует естественный отбор. Вариант первый: мутаций нет
# Параметры симуляции
NUM_BIRDS = 100
MUTATION_RATE = 0.0 # Вероятность мутации направления у потомков
FOOD_CHANGE_INTERVAL = 250 # Интервал изменения позиции еды
BIRD_SPEED = 5
-
Новые поколения находят еду, адаптируются, успешно размножаются.
-
Но потомки почти полностью копируют родителей, без отклонений. Они не экспериментируют и не ищут.
-
Окружающая среда меняется. Еда теперь в другом месте. Потомки не могут её найти.
-
Все умирают.
Вариант второй: мутаций слишком много
# Параметры симуляции
NUM_BIRDS = 100
MUTATION_RATE = 1.0 # Вероятность мутации направления у потомков
FOOD_CHANGE_INTERVAL = 250 # Интервал изменения позиции еды
BIRD_SPEED = 5
-
Дети всё время не похожи на родителей.
-
В итоге найденная родителями приспособленность к окружающим условиям игнорируется всеми детьми. Они ищут только своё. Часто не находят.
-
Потомки не успевают достаточно размножиться, ведь не используют «знания» родителей.
-
Все умерли.
Вариант третий: нужное для адаптации количество мутаций
# Параметры симуляции
NUM_BIRDS = 100
MUTATION_RATE = 0.3 # Вероятность мутации направления у потомков
FOOD_CHANGE_INTERVAL = 250 # Интервал изменения позиции еды
BIRD_SPEED = 5
-
Часть потомков пользуется находкой родителей и летят за уже разведанной едой, отчего быстро размножаются.
-
Другая часть потомков (30 %) занята исследованием нового пространства. В случае смены среды им удаётся найти еду и продолжить род.
-
Количество птиц резко увеличивается.
-
Популяция выживает в данных условиях.
Выводы
В этом эксперименте с птицами и едой мы наблюдаем простую и наглядную модель естественного отбора. Даже в упрощённом варианте становится очевидно: без мутаций потомки теряют способность адаптироваться к изменениям, а при избыточной изменчивости теряется преемственность и популяция становится нестабильной. Идеальный баланс — это сочетание небольших мутаций с сохранением ключевых свойств, позволяющее поддерживать как стабильность, так и гибкость.
На примере этого алгоритма видно, как важна изменчивость для выживания, но также и то, что стабильные базовые черты обеспечивают преемственность поколений. Наши эксперименты показывают, что адаптация к новому окружению — это не только про случайные изменения, но и про правильное наследование успешных черт. Всё «как в жизни»: да, изменения необходимы (ох), но слишком резкие шаги могут всё испортить.
Недостатки симуляции
-
Случайно еда может повторно оказаться на том же месте, что драматически увеличивает популяцию. Лучше бы сделать более-менее предсказуемое изменение среды. Например, вращение еды по кругу с прогнозируемыми промежутками. Сейчас проверим.
-
Скорость p5py при большом количестве птиц неприемлемо низкая. Нужно или переписать на JS, или ускорить сам модуль. Вы можете использовать черновую версию похожего кода на Processing. Она на порядок быстрее.
-
Можно добавить ограничение, чтобы больше XXX птиц не появлялось.
Часть третья. Более наглядные эксперименты
Если вы будете играть с предыдущим кодом, то поймёте, что из-за слишком больших случайностей в появлении еды общую закономерность иногда сложно заметить, и вероятность мутаций в 100 % часто прекрасным образом себя чувствует. Поэтому давайте упростим и сделаем более наглядный вариант. Вот готовый код. Его будем использовать в экспериментах дальше.
Еда теперь бегает по кругу:
class Food:
def __init__(self):
self.size = FOOD_SIZE
self.angle = 0
self.radius = width / 2 # Фиксированное расстояние
self.speed = 0.002 # Скорость вращения
def move(self):
self.angle += self.speed
self.position = Vector(width / 2 + self.radius * cos(self.angle),
height / 2 + self.radius * sin(self.angle))
def display(self):
text_size(self.size)
text_align(CENTER, CENTER)
fill(255)
text("🍞", self.position.x, self.position.y)
И репродукция упростилась: вместо охвата всех 360 градусов мы будем просто задавать диапазон мутации:
ANGLE_MUTATION_RANGE = 0.8 # Диапазон изменения угла
Вот так:
def reproduce(self):
if len(birds) >= MAX_BIRDS:
return [] # Если количество птиц достигло максимума, не создаваем новых
new_birds = []
self.invert_angle()
angle = self.angle + rand(-ANGLE_MUTATION_RANGE, ANGLE_MUTATION_RANGE)
new_birds.append(Bird(Vector(200, 200), angle))
new_birds.append(Bird(Vector(200, 200), angle))
return new_birds
Теперь крайние варианты и средний оптимальный видны очень отчетливо.
Первый вариант — мутаций нет:
ANGLE_MUTATION_RANGE = 0.0 # Диапазон изменения угла
Ха-ха, не успели. Пытаются найти еду, где её уже нет.
Второй вариант — сильные мутации:
ANGLE_MUTATION_RANGE = 1.0 # Диапазон изменения угла
Ищут не там и промахиваются. Слишком большой разброс.
Вариант третий — мутации ближе к оптимальным:
ANGLE_MUTATION_RANGE = 0.3 # Диапазон изменения угла
И через некоторое время можно, открыв птицефабрику, написать в «Упал, поднялся»:
Если хотите экспериментировать
Отключив отображение, можно запустить набор симуляций с целью определить оптимальную долю для данных окружающих условий (размер и скорость перемещения еды). На глаз 0.3
, но не факт.
Птичку жалко. Добавим интерактивность
Бездушный чёрствый ломоть хлеба сейчас перемещается сам, но мы можем, поменяв пару строк, управлять им мышкой. Проведем такую замену в классе Food:
def move(self):
self.position = Vector(mouse_x, mouse_y)
# self.angle += self.speed
# self.position = Vector(width / 2 + self.radius * cos(self.angle),
# height / 2 + self.radius * sin(self.angle))
Занятия с детьми
Я стараюсь дочке рассказывать и объяснять, чем занимаюсь и как что работает. Так и эта статья не прошла мимо. Вкратце, в этой модели получилась интересная педагогическая составляющая: если дети слишком сильно похожи на родителей (как некоторые родители требуют от детей: «прекрати рисовать, будь бухгалтером, как я»), то такая популяция слабо адаптивна и вымирает. Если же у нас другая крайность, когда дети уходят в полный отрыв от родителей так, что вообще ничему у них не учатся (крайняя степень конфликта «отцы и дети»), то и такая популяция вымирает, так как дети не используют находки и адаптивную приспособленность родителей. Для популяции идеален поиск среднего (aurea mediocritas), где дети учатся у родителей, но идут по жизни своим независимым путем.
Итак
Мы с вами написали самую простую визуализацию эволюции и естественного отбора всего с одним геном.
О важности девиации. Птицы с геном направления (угла) летят к еде и возвращаются, чтобы дать потомство. Если нет разнообразия, то при смещении еды все погибают, так как улетают в поисках и не могут вернуться. А если есть разнообразие, то отдельные девианты обязательно найдут еду и вернутся, оставив потомство с новым углом поиска, и из девиантов станут основным новым «видом» потомства.
Если вам понравилось быстро тестировать гипотезы в браузере, вот ещё статьи про p5py
:
-
Давайте-ка наваяем PumpKeen Game. Как Commander Keen, только про Pumpkin (тыкву). Хэллоуин же
-
Как я написал книгу для детей: «Мама, не отвлекай. Я Python учу!»
Модуль p5py
в бета-версии, узнать новости, обсудить ошибки, идеи и свои работы можно в общей группе в Telegram: @p5py_ru