The Nature of Code閱讀心得與Python實作:9.8 Ecosystem Simulation

更新 發佈閱讀 33 分鐘

9.1節曾提到過,GA是受到遺傳、演化等理論的啟發所發展出來的演算法,其主要目的是要以特定的方式來解決某些特定類型的問題;所以GA只是在模仿演化,而不是在模擬演化。

既然GA只是在模仿演化,那如果我們想要模擬演化,該怎麼做呢?要回答這個問題,先來看看GA做法下的演化和自然界的演化,這兩者之間有些什麼差異。

GA做法下的演化和自然界的演化,這兩種演化方式之間最明顯的差異是,在GA中,族群內的所有個體會同時出生、在相同時間進行繁殖,並在繁殖下一代後同時死亡,所以族群規模處於相當穩定的狀態;而在自然界中,族群內隨時會有個體出生、繁殖、死亡,所以族群規模隨時都在變動,甚至於會有極端的變化產生。另外,在GA中,會針對每個個體對環境的適應程度打分數,藉此來決定哪些個體能繁殖下一代,這也就是所謂的「適者生存」;但在自然界中,沒有打分數的人存在,能繁殖下一代的就是「適者」。所以,用比較賣弄學問的方式來說,GA的演化是離散式的,每間隔一段時間進行一次;而自然界的演化則是連續式的,隨時都在進行。

從上面的分析來看,要模擬自然界的演化過程,關鍵就在於要怎麼模擬不斷進行中的「適者生存」這件事。這事說來其實也不難,既然「適者生存」的內涵是「適者繁殖」,那只要讓活得越久的個體越有機會繁殖下一代,就可以模擬出「適者生存」的效果了。

既然能夠模擬出自然界「適者生存」的現象,那就可以模擬出自然界的演化過程了。接下來,就在GA的基礎上,實際來打造一個雖然簡單,但卻能夠進行演化的生態系統。

在動手寫程式之前,先來描述一下要打造的生態系統。

在這個生態系統中,有種叫做bloop的生物,其外型是個圓,會到處遊蕩;體型越大的移動速度越慢,體型越小的移動速度越快。

隨著時間過去,bloop的生命力會逐漸減少,最後會因為喪失生命力而死亡。所幸,在系統中有一些食物存在,當bloop吃下食物時,會讓生命力上升,而可以活得久一點。不幸死亡的bloop並不會就此消失,它們會變成其他同類延長生命所需的食物。

在繁殖方面,bloop的繁殖方式是無性生殖;在任何時刻,每個bloop都有一定的機率會生出和自己一模一樣的下一代,不過,也有一定的機率,下一代會因為突變而變得和親代有所不同。

以上就是我們要打造的生態系統概況。寫程式時,我們會用一個叫做World的類別來描述它;不過這個部分放在最後面再來寫,現在先來寫個Bloop類別,用來描述生態系統中的主角,也就是bloop。

既然bloop會在畫面上跑來跑去,那就可以用在第一章Mover類別時所用的架構來寫Bloop類別:

class Bloop:
def __init__(self, x, y):
# 取得顯示畫面及其大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

self.position = pygame.Vector2(x, y)

self.max_speed = 5
self.radius = 8

# 設定所在surface的格式為per-pixel alpha
w = h = 2*self.radius
self.surface = pygame.Surface((w, h), pygame.SRCALPHA)

color = (0, 0, 0, 200)
self.center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, color, self.center, self.radius)

# x、y方向上取1D Perlin noise之起始位置
self.xoff = 10000*random.random()
self.yoff = 10000*random.random()

def update(self):
vx = noise.pnoise1(self.xoff)*self.max_speed
vy = noise.pnoise1(self.yoff)*self.max_speed
velocity = pygame.Vector2(vx, vy)

self.position += velocity

self.xoff += 0.01
self.yoff += 0.01

def check_edges(self):
if self.position.x > self.width + self.radius:
self.position.x = -self.radius
elif self.position.x < -self.radius:
self.position.x = self.width + self.radius

if self.position.y > self.height + self.radius:
self.position.y = -self.radius
elif self.position.y < -self.radius:
self.position.y = self.height + self.radius

def show(self):
self.screen.blit(self.surface, self.position-self.center)

跟先前的做法一樣,我們把bloop族群放在一個list中,並用一個類別來管理;這個類別就是用來描述生態系統的World類別:

class World:
def __init__(self, population_size, width, height):
self.population_size = population_size

self.bloops = [None]*self.population_size
for i in range(self.population_size):
x = random.randint(0, width)
y = random.randint(0, height)
self.bloops[i] = Bloop(x, y)

到目前為止的寫法,其實就是在第四章處理粒子系統時的寫法;數量會變動的bloop就是粒子,而生態系統就是管理粒子的粒子系統。要想讓這個系統演化,需要在系統中加入兩個特性:

  • bloop死亡
  • bloop誕生

加入bloop死亡這個特性,是為了取代GA中的適應度函數及挑選過程;當bloop死亡之後,就不會被挑選出來繁殖下一代,因為它已經不存在了。那這個特性要怎麼寫呢?其實挺簡單的,在Bloop類別中加個存放bloop生命力數值的health屬性

class Bloop:
def __init__(self, x, y):
:
:
self.health = 100
:
:

當每次更新時,讓bloop的生命力下降

def update(self):
:
:
self.health -= 0.2
:
:

另外,在Bloop類別中加入is_dead()方法,用來檢查bloop的生命力是不是低於0;如果是的話,代表這個bloop已經死亡。

def is_dead(self):
return self.health < 0

僅僅加入health這個屬性來製造bloop的死亡還不夠,因為這意味著所有的bloop壽命都一樣長,能夠繁殖下一代的機率都一樣;這不符合「適者生存」的要求。為什麼呢?這是因為所有bloop的生命力一開始的時候都是100,而且下降的速率都一樣,所以壽命當然就會一樣長。

既然bloop的壽命都一樣長會導致系統無法符合「適者生存」的要求,那要怎麼讓它們的壽命長短各不相同呢?方法有不少,例如在系統中加入會捕食bloop的另一種生物,讓跑得快的bloop比較容易逃過被捕食的命運而活得比較久。另一種做法是撒些食物讓bloop吃;吃到食物的bloop可以增加生命力,就可以活得比較久。在這裡,我們會使用撒食物的方式,不過在這麼做之前,得先寫個描述食物的類別。

描述食物的類別挺簡單的,主要就是把食物的位置存放在一個list中;藉由新增或移除這個list中的元素,就可以新增或移除食物。程式碼如下:

class Food:
def __init__(self, num):
# 取得顯示畫面及其大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

# 撒下特定數量的食物
self.positions = [None]*num
for i in range(num):
x = random.randint(0, self.width)
y = random.randint(0, self.height)
self.positions[i] = pygame.Vector2(x, y)

self.size = 5

def add(self, position):
self.positions.append(position)

def show(self):
for position in self.positions:
rect = pygame.Rect(0, 0, self.size, self.size)
rect.center = (position.x, position.y)
pygame.draw.rect(self.screen, (255, 0, 0), rect)

def run(self):
# 有一定的機率會出現新的食物
if random.random() < 0.001:
x = random.randint(0, self.width)
y = random.randint(0, self.height)
self.add(pygame.Vector2(x, y))

self.show()

有了描述食物的Food類別之後,就可以在Bloop類別中加入讓bloop吃食物的方法了:

def eat(self, food):
for position in food.positions.copy():
if position.distance_to(self.position) < 2*self.radius:
# 移除被吃掉的食物並增加bloop的生命力
food.positions.remove(position)
self.health += 100

如果在bloop附近有任何食物出現,它就會把食物吃掉。這裡的「附近」,設定成距bloop所在位置2倍它的尺寸內的距離。

就這樣,吃下越多食物的bloop會活得越久,因而越有機會繁殖下一代;透過這樣子的機制,系統應該就會演化出最能夠吃到食物的bloop。

Genotype and Phenotype

bloop找到食物的能力跟它的尺寸和移動速度有關。長得比較大的bloop,因為在移動時掃過的面積比較大,所以比較容易找到食物;而移動速度比較快的bloop,因為可以在比較短的時間內跑遍比較大的範圍,所以可以找到比較多的食物。這也就是說,在是否能吃到食物這個問題上,bloop的尺寸和移動速度是效果相反的兩個因素。

既然在找食物方面,尺寸和速度的效果相反,而尺寸比較大的bloop移動速度比較慢,反之亦然,那在設計基因型時,就只需要一個變數就夠了:

class DNA:
def __init__(self):
self.genes = [random.uniform(0, 1)]

def mutate(self, mutation_rate):
if random.random() < mutation_rate:
self.genes[0] = random.uniform(0, 1)

在這裡,雖然只有一個變數,但我們還是用list來存放,這樣可以保留擴充的空間,以備將來有更複雜的設計時,可以不需要改太多東西。

表現型就是bloop本身,修改一下前面的Bloop類別,就可以利用DNA物件的實例來設定其大小和最大移動速率:

class Bloop:
def __init__(self, dna, x, y):
:
:
self.dna = dna
self.max_speed = 15*(1-self.dna.genes[0])
self.radius = 25*self.dna.genes[0]
:
:

在這裡,最大移動速率的範圍設定在0~15;而bloop的半徑,則設定在0~25。要注意的是,bloop的基因值為0時,最高移動速率是15;基因值為1時,最高移動速率是0,也就是呈靜止狀態。

Selection and Reproduction

在挑選方面,先前說過,bloop活得越久,越有機會繁殖下一代;所以bloop的壽命長短,就是它的適應度。

在繁殖方面,可以設定當兩個bloop相遇時,就會生出下一代;這樣活得越久的bloop,就越有機會遇到另一個bloop而生出下一代。這種設定方式除了影響bloop的繁殖方式之外,對演化結果也會有影響;因為除了找食物之外,bloop找到另一個bloop的能力,也同時會影響它是不是能夠繁殖下一代。

另一種比較簡單,也可以讓bloop生出下一代的方式,就是直接複製一個一模一樣,跟親代有相同基因的bloop出來。例如,可以設定在任何時刻,bloop都有0.05%的機率會生出下一代;這樣活得越久的bloop,一樣會有越大的機會生出下一代。

在這裡,我們採用的挑選、繁殖方式,是直接複製的方式。實作時,在Bloop類別加個reproduce()方法就可以了。

def reproduce(self):
if random.random() < 0.0005:
child_dna = copy.deepcopy(self.dna)
child_dna.mutate(0.01)

return Bloop(child_dna, self.position.x, self.position.y)
else:
return None

要注意的是,繁殖機率的大或小,對系統會有相當不一樣的影響。過大的繁殖機率,很容易會讓bloop族群的規模變得太大;而過小的繁殖機率,則可能會讓bloop生不如死,也就是生得少而死得多,最後在系統中沒有任何bloop存在。

到此為止,這個會演化的生態系統算是接近完工了,再補上一些小細節之後,就可以大功告成了。完工之後的WorldDNABloopFood等類別的完整程式碼如下:

class World:
def __init__(self, population_size, width, height):
self.population_size = population_size

self.bloops = [None]*self.population_size
for i in range(self.population_size):
x = random.randint(0, width)
y = random.randint(0, height)
self.bloops[i] = Bloop(DNA(), x, y)

self.food = Food(population_size)

def run(self):
self.food.run()

for bloop in self.bloops.copy():
bloop.run()
bloop.eat(self.food)

if bloop.is_dead():
# 如果bloop死了,將其變成食物
self.bloops.remove(bloop)
self.food.add(bloop.position)
else:
# 如果bloop生出下一代,將下一代加入族群中
child = bloop.reproduce()
if child is not None:
self.bloops.append(child)


class DNA:
def __init__(self):
self.genes = [random.uniform(0, 1)]

def mutate(self, mutation_rate):
if random.random() < mutation_rate:
self.genes[0] = random.uniform(0, 1)


class Bloop:
def __init__(self, dna, x, y):
# 取得顯示畫面及其大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

self.dna = dna
self.position = pygame.Vector2(x, y)

self.health = 100
self.max_speed = 15*(1-self.dna.genes[0])
self.radius = 25*self.dna.genes[0]

# 設定所在surface的格式為per-pixel alpha
w = h = 2*self.radius
self.surface = pygame.Surface((w, h), pygame.SRCALPHA)

color = (0, 0, 0, 200)
self.center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, color, self.center, self.radius)

# x、y方向上取1D Perlin noise之起始位置
self.xoff = 10000*random.random()
self.yoff = 10000*random.random()

def update(self):
vx = noise.pnoise1(self.xoff)*self.max_speed
vy = noise.pnoise1(self.yoff)*self.max_speed
velocity = pygame.Vector2(vx, vy)

self.position += velocity

self.health -= 0.2

self.xoff += 0.01
self.yoff += 0.01

def check_edges(self):
if self.position.x > self.width + self.radius:
self.position.x = -self.radius
elif self.position.x < -self.radius:
self.position.x = self.width + self.radius

if self.position.y > self.height + self.radius:
self.position.y = -self.radius
elif self.position.y < -self.radius:
self.position.y = self.height + self.radius

def is_dead(self):
return self.health < 0

def eat(self, food):
for position in food.positions.copy():
if position.distance_to(self.position) < 2*self.radius:
# 移除被吃掉的食物並增加bloop的生命力
food.positions.remove(position)
self.health += 100

def reproduce(self):
if random.random() < 0.0005:
child_dna = copy.deepcopy(self.dna)
child_dna.mutate(0.01)

return Bloop(child_dna, self.position.x, self.position.y)
else:
return None

def show(self):
self.screen.blit(self.surface, self.position-self.center)

def run(self):
self.update()
self.check_edges()
self.show()


class Food:
def __init__(self, num):
# 取得顯示畫面及其大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()

# 撒下特定數量的食物
self.positions = [None]*num
for i in range(num):
x = random.randint(0, self.width)
y = random.randint(0, self.height)
self.positions[i] = pygame.Vector2(x, y)

self.size = 5

def add(self, position):
self.positions.append(position)

def show(self):
for position in self.positions:
rect = pygame.Rect(0, 0, self.size, self.size)
rect.center = (position.x, position.y)
pygame.draw.rect(self.screen, (255, 0, 0), rect)

def run(self):
# 有一定的機率會出現新的食物
if random.random() < 0.001:
x = random.randint(0, self.width)
y = random.randint(0, self.height)
self.add(pygame.Vector2(x, y))

self.show()

現在,就來讓這個生態系統進行演化,看看哪種體型和移動速度的bloop會勝出。

Example 9.5: An Evolving Ecosystem

raw-image

主程式如下

# python version 3.10.9
import copy
import random
import sys

import noise # version 1.2.2
import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 9.5: An Evolving Ecosystem")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

population_size = 20
mutation_rate = 0.05
world = World(population_size, WIDTH, HEIGHT)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
# 按下滑鼠按鍵可以投放一隻新的bloop
x, y = event.pos
world.bloops.append(Bloop(DNA(), x, y))
elif event.type == pygame.MOUSEMOTION and event.buttons[0]:
# 按下滑鼠左鍵拖曳,可以連續投放新的bloop
x, y = event.pos
world.bloops.append(Bloop(DNA(), x, y))

screen.fill(WHITE)

world.run()

pygame.display.update()
frame_rate.tick(FPS)

從模擬的結果可以看出來,中等體型、中等速度的bloop比較容易在生存競爭中勝出。體型過大的bloop,因為行動過慢而不容易找到食物;速度過快的bloop,則會因為體型太小而難以找到食物。當然啦,異常總是難免的。例如,如果有一群體型很大的bloop同時死在同一個地方,導致那個地方充滿了食物。好巧不巧,剛好有隻體型很大的bloop路過此地,一次把所有的食物都吃了。就這樣,這隻體型很大的bloop,因為吃得飽飽的,所以可以活很長的一段時間,而成為演化過程中的一個異數。不過話又說回來了,即便會有異常出現,但這種異常有多少機率會發生,而又能持續多久呢?!看看恐龍就知道了。

留言
avatar-img
ysf的沙龍
20會員
165內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2025/11/17
本節介紹互動式挑選(interactive selection),也就是由人來指定適應度函數值的GA。
Thumbnail
2025/11/17
本節介紹互動式挑選(interactive selection),也就是由人來指定適應度函數值的GA。
Thumbnail
2025/10/27
到目前為止,我們已經讓火箭具有演化的能力,不管目標物的位置怎麼改變,都能夠自動調整飛行路線,朝目標物飛過去。接下來,要來升級我們的火箭,讓火箭藉由演化的方式,可以自己找出避開障礙物的路線。
Thumbnail
2025/10/27
到目前為止,我們已經讓火箭具有演化的能力,不管目標物的位置怎麼改變,都能夠自動調整飛行路線,朝目標物飛過去。接下來,要來升級我們的火箭,讓火箭藉由演化的方式,可以自己找出避開障礙物的路線。
Thumbnail
2025/10/27
這一節要用GA來設計一款具有演化功能的智慧火箭。
Thumbnail
2025/10/27
這一節要用GA來設計一款具有演化功能的智慧火箭。
Thumbnail
看更多
你可能也想看
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
《破·地獄》不是一部關於死亡的電影,而是一場關於理解的修行。導演陳茂賢以殯儀文化為背景,透過魏道生、郭文與家人的生命交錯,揭示「破的不是地獄,而是人心的恐懼」。每個角色都在差異中學習轉化自己——不是誰對誰錯,而是願意被不同改變。當儀式點亮黑暗,活人也學會與不可承受之重和解。
Thumbnail
《破·地獄》不是一部關於死亡的電影,而是一場關於理解的修行。導演陳茂賢以殯儀文化為背景,透過魏道生、郭文與家人的生命交錯,揭示「破的不是地獄,而是人心的恐懼」。每個角色都在差異中學習轉化自己——不是誰對誰錯,而是願意被不同改變。當儀式點亮黑暗,活人也學會與不可承受之重和解。
Thumbnail
本文探討比利時炸薯條(frietkot)從街頭小吃到被列為非物質文化遺產的文化價值,剖析其獨特的兩段式油炸工藝、在地醬料文化、以及在公共生活中的角色。文章對比了臺灣鹽酥雞攤的相似之處,並討論了其在申遺過程中的爭議與保護措施,為初訪者提供了體驗建議。
Thumbnail
本文探討比利時炸薯條(frietkot)從街頭小吃到被列為非物質文化遺產的文化價值,剖析其獨特的兩段式油炸工藝、在地醬料文化、以及在公共生活中的角色。文章對比了臺灣鹽酥雞攤的相似之處,並討論了其在申遺過程中的爭議與保護措施,為初訪者提供了體驗建議。
Thumbnail
LINE GO,台灣指標性 MaaS(Mobility as a Service)平台,迎來品牌升級兩週年,今(1)日公布亮眼成績單:用戶數突破 470 萬,穩定成長;LINE GO TAXI 在去 (2024)年累積的叫車里程更可繞行地球 1,367 圈,並成功打造全台最大減碳車隊。
Thumbnail
LINE GO,台灣指標性 MaaS(Mobility as a Service)平台,迎來品牌升級兩週年,今(1)日公布亮眼成績單:用戶數突破 470 萬,穩定成長;LINE GO TAXI 在去 (2024)年累積的叫車里程更可繞行地球 1,367 圈,並成功打造全台最大減碳車隊。
Thumbnail
AI程式設計時代來臨,軟體架構需從人類中心設計轉變為AI優化設計,例如降低抽象層級、檔案結構優化、Context集中化等,並以Netflix Dispatch為例,說明AI友好架構的具體實踐,包括Domain Module設計、簡化層級結構、函數式編程和Monorepo架構等。
Thumbnail
AI程式設計時代來臨,軟體架構需從人類中心設計轉變為AI優化設計,例如降低抽象層級、檔案結構優化、Context集中化等,並以Netflix Dispatch為例,說明AI友好架構的具體實踐,包括Domain Module設計、簡化層級結構、函數式編程和Monorepo架構等。
Thumbnail
一宮市位於愛知縣西北部,名古屋市與岐阜市的中間,濃尾平原的中央,擁有清澈的木曾川河水和溫和的氣候,自然條件優越,自古以來,這裡作為尾張國西北部的經濟中心和真清田神社的寺町而繁榮昌盛,一宮市的紡織歷史悠久,可以追溯到平安時代,在江戶時代,它以條紋棉和絲綢織物的產地而聞名,明治以後,作為工業化毛紡織業的
Thumbnail
一宮市位於愛知縣西北部,名古屋市與岐阜市的中間,濃尾平原的中央,擁有清澈的木曾川河水和溫和的氣候,自然條件優越,自古以來,這裡作為尾張國西北部的經濟中心和真清田神社的寺町而繁榮昌盛,一宮市的紡織歷史悠久,可以追溯到平安時代,在江戶時代,它以條紋棉和絲綢織物的產地而聞名,明治以後,作為工業化毛紡織業的
Thumbnail
我會不斷的重新認識你,無論有多麼難以理解。
Thumbnail
我會不斷的重新認識你,無論有多麼難以理解。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News