Давайте рассмотрим наипростейшую модель естественного отбора. В сети встречал модель с двумя параметрами-генами, а у нас будет всего один, при сохранении наглядности. Модель настолько элементарна, что её можно обсудить даже со своим ребёнком (проверил со своей шестилетней дочкой).
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
Комментарии (4)
Sly_tom_cat
03.12.2024 15:03Тут уже была статья про защиту дамы с косой (мол смертность от возраста дает преимущества) так там тоже питон и мизерныые популяции на довольно коротком интервале времени + мутный подбор параметров привели автора к совершенно неправильному выводу.
Переписала на golang, прогнал на много тысячных популяциях и десятках тысяч циклов/лет и с дополнительными хромосомами и с вероятностью размножения и прочими разными бантиками - только на крайне низкой популяции смертные начинают спорить с бессмертными, а чуть побольше - ни ни какими параметрами не добиться хотя бы паритета - бессмертные рвут смертных как тузик грелку.
Так что переписать с питона поделку на коленке на чем-то более серьезном - всегда бывает полезно.
danielsedoff
03.12.2024 15:03Вот статья так статья, есть с чем поиграть/воспроизвести и на что поглазеть.
logofios
Очень наглядно, любопытно. Интересно, что когда еда перемещается, то от переданного гена словно и нет никакой пользы, т.к. пользы от него нет для потомства
PashaWNN
Если еда перемещается постепенно, то польза таки есть. Так и в природе работает: внешняя среда редко меняется внезапно. Например, климатические изменения в целом очень неспешные, а живые существа адаптируются к ним за счёт описанных в статье принципов.