在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存在。
到此為止,這個會演化的生態系統算是接近完工了,再補上一些小細節之後,就可以大功告成了。完工之後的World、DNA、Bloop、Food等類別的完整程式碼如下:
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

主程式如下
# 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,因為吃得飽飽的,所以可以活很長的一段時間,而成為演化過程中的一個異數。不過話又說回來了,即便會有異常出現,但這種異常有多少機率會發生,而又能持續多久呢?!看看恐龍就知道了。


