在这篇文章中,我们模拟了一个包含生物体和食物的环境,生物体为了生存必须尽可能多的消耗食物。在模拟环境中,生物体将由一个简单的、完全连接的神经网络来控制。神经网络的输入是一种标准化值,从-1到+1之间,这表示最近的食物颗粒的方向。这个方向是通过最近的食物颗粒(+/-180度)方向计算出来的。下面是两个生物体和食物颗粒的示例:
因为我们的输入范围从-1到+1,所以输出范围应当也从-1到+1,因此tanh函数将会成为理想的激活函数。下面是一个神经网络的图和它的输入、输出以及它的隐藏层:
由于神经网络仅仅是简单的矩阵乘法,因此利用NumPy只需要几行代码就可以了。在进化过程中,一些权重会被优化。最后,我将默认的隐藏节点数设置为5,这个可以根据你自己的喜好设置。接下来是编写Python代码模拟整个生态:
一、生物
生物类包含了神经网络以及更新它的速度和位置的函数。当一个生物体被初始化时,它的位置、方向、速度、加速度和神经网络的权重都会随机产生。下面是这个生物类的代码:
class organism():
def __init__(self, settings, wih=None, who=None, name=None):
self.x = uniform(settings['x_min'], settings['x_max']) # position (x)
self.y = uniform(settings['y_min'], settings['y_max']) # position (y)
self.r = uniform(0,360) # orientation [0, 360]
self.v = uniform(0,settings['v_max']) # velocity [0, v_max]
self.dv = uniform(-settings['dv_max'], settings['dv_max']) # dv
self.d_food = 100 # distance to nearest food
self.r_food = 0 # orientation to nearest food
self.fitness = 0 # fitness (food count)
self.wih = wih
self.who = who
self.name = name
# NEURAL NETWORK
def think(self):
# SIMPLE MLP
af = lambda x: np.tanh(x) # activation function
h1 = af(np.dot(self.wih, self.r_food)) # hidden layer
out = af(np.dot(self.who, h1)) # output layer
# UPDATE dv AND dr WITH MLP RESPONSE
self.nn_dv = float(out[0]) # [-1, 1] (accelerate=1, deaccelerate=-1)
self.nn_dr = float(out[1]) # [-1, 1] (left=1, right=-1)
# UPDATE HEADING
def update_r(self, settings):
self.r += self.nn_dr * settings['dr_max'] * settings['dt']
self.r = self.r % 360
# UPDATE VELOCITY
def update_vel(self, settings):
self.v += self.nn_dv * settings['dv_max'] * settings['dt']
if self.v < 0: self.v = 0
if self.v > settings['v_max']: self.v = settings['v_max']
# UPDATE POSITION
def update_pos(self, settings):
dx = self.v * cos(radians(self.r)) * settings['dt']
dy = self.v * sin(radians(self.r)) * settings['dt']
self.x += dx
self.y += dy
二、食物
食物类包含坐标位置和能量值,能量值的多少将直接影响生物体的健康。现在,这个能量值保持不变,并设为1,但如果你想要修改它,它可以被随机化或改变为任何值。此外,当食物被生物体消耗后,respawn函数就会被调用,用于重新生成食物颗粒的位置。这使得每个模拟时间内的食物颗粒总数保持不变。
class food():
def __init__(self, settings):
self.x = uniform(settings['x_min'], settings['x_max'])
self.y = uniform(settings['y_min'], settings['y_max'])
self.energy = 1
def respawn(self,settings):
self.x = uniform(settings['x_min'], settings['x_max'])
self.y = uniform(settings['y_min'], settings['y_max'])
self.energy = 1
三、进化
生物体将使用遗传算法(GA)进行优化,遗传算法(GA)是属于进化算法(EA)的更大范围。遗传算法模仿自然的生物进化过程,从最初的种群开始,通过选择、交叉和变异产生后代,形成一个最优的解决方案。对于本教程,EA计划如下:
1、选择:简单的选择形式被称为精英主义,即选择最优秀的下一代。
2、跨界:在精英主义阶段中随机选择个体作为父母,并产生一个新的后代。因此我们要处理神经网络的权重,下面的方程将两个父母之间的性状遗传给后代:
offspring=parent1(a)+parent2(1−a)
a是在0和1之间随机生成的值,而父1和父2表示神经网络的权重。因为有两个正在混合的矩阵(wih和who),因此方程实际上是这样的:
offspring_wih=parent_wih1(a)+parent_wih2(1−a)
offspring_who=parent_who1(a)+parent_who2(1−a)
3、突变:一旦产生新的后代就会产生一个介于0和1之间的随机数。如果这个值低于用户初始化的突变阈值,就会发生突变。在发生突变的情况下,两个神经网络权重矩阵中的一个随机权重将被一个随机值所取代,这个值在初始值的+/-10%之间。这将会在生物体中产生微小的变化,从而有可能产生更健康的生物体。突变效果仅限于原始值的+/-10%,因为我们希望避免在神经网络中造成灾难性的故障而导致整个生物群体瘫痪。
下面的图显示了这个EA方案:
这个EA代码程序如下所示:
ef evolve(settings, organisms_old, gen):
elitism_num = int(floor(settings['elitism'] * settings['pop_size']))
new_orgs = settings['pop_size'] - elitism_num
#--- GET STATS FROM CURRENT GENERATION ----------------+
stats = defaultdict(int)
for org in organisms_old:
if org.fitness > stats['BEST'] or stats['BEST'] == 0:
stats['BEST'] = org.fitness
if org.fitness < stats['WORST'] or stats['WORST'] == 0:
stats['WORST'] = org.fitness
stats['SUM'] += org.fitness
stats['COUNT'] += 1
stats['AVG'] = stats['SUM'] / stats['COUNT']
#--- ELITISM (KEEP BEST PERFORMING ORGANISMS) ---------+
orgs_sorted = sorted(organisms_old, key=operator.attrgetter('fitness'), reverse=True)
organisms_new = []
for i in range(0, elitism_num):
organisms_new.append(organism(settings, wih=orgs_sorted[i].wih, who=orgs_sorted[i].who, name=orgs_sorted[i].name))
#--- GENERATE NEW ORGANISMS ---------------------------+
for w in range(0, new_orgs):
# SELECTION (TRUNCATION SELECTION)
canidates = range(0, elitism_num)
random_index = sample(canidates, 2)
org_1 = orgs_sorted[random_index[0]]
org_2 = orgs_sorted[random_index[1]]
# CROSSOVER
crossover_weight = random()
wih_new = (crossover_weight * org_1.wih) + ((1 - crossover_weight) * org_2.wih)
who_new = (crossover_weight * org_1.who) + ((1 - crossover_weight) * org_2.who)
# MUTATION
mutate = random()
if mutate <= settings['mutate']:
# PICK WHICH WEIGHT MATRIX TO MUTATE
mat_pick = randint(0,1)
# MUTATE: WIH WEIGHTS
if mat_pick == 0:
index_row = randint(0,settings['hnodes']-1)
wih_new[index_row] = wih_new[index_row] * uniform(0.9, 1.1)
if wih_new[index_row] > 1: wih_new[index_row] = 1
if wih_new[index_row] < -1: wih_new[index_row] = -1
# MUTATE: WHO WEIGHTS
if mat_pick == 1:
index_row = randint(0,settings['onodes']-1)
index_col = randint(0,settings['hnodes']-1)
who_new[index_row][index_col] = who_new[index_row][index_col] * uniform(0.9, 1.1)
if who_new[index_row][index_col] > 1: who_new[index_row][index_col] = 1
if who_new[index_row][index_col] < -1: who_new[index_row][index_col] = -1
organisms_new.append(organism(settings, wih=wih_new, who=who_new, name='gen['+str(gen)+']-org['+str(w)+']'))
return organisms_new, stats
四、模拟
最后是模拟实际运行的关键代码,模拟函数将在每一代调用一次。模拟时间步骤则是通过将总模拟时间除以时间间隔dt来确定的。例如,如果模拟时间设置为100秒,而dt等于1/25秒,那么总共需要模拟2500个时间步骤。在每一个步骤中,将进行以下操作:
1、碰撞检测:检查生物和食物颗粒之间的碰撞。当检测到碰撞时,该生物体将得到更新,食物颗粒将在一个新的随机位置上重生。
2、寻找最近的食物颗粒:对于每一个生物体,必须确定最近的食物颗粒。
3、确定最近的食物颗粒的方向:一旦最近的食物颗粒被一个生物体确定,就必须计算出这个颗粒的方向。
4、查询神经网络:由于使用了更新的方向值,因此每个生物的神经网络都将变得不同。
5、更新生物体:基于神经网络的响应,生物体的速度和位置都得到了更新。
下面是模拟函数:
def simulate(settings, organisms, foods, gen):
total_time_steps = int(settings['gen_time'] / settings['dt'])
#--- CYCLE THROUGH EACH TIME STEP ---------------------+
for t_step in range(0, total_time_steps, 1):
# PLOT SIMULATION FRAME
#if gen == settings['gens'] - 1 and settings['plot']==True:
if gen==49:
plot_frame(settings, organisms, foods, gen, t_step)
# UPDATE FITNESS FUNCTION
for food in foods:
for org in organisms:
food_org_dist = dist(org.x, org.y, food.x, food.y)
# UPDATE FITNESS FUNCTION
if food_org_dist <= 0.075:
org.fitness += food.energy
food.respawn(settings)
# RESET DISTANCE AND HEADING TO NEAREST FOOD SOURCE
org.d_food = 100
org.r_food = 0
# CALCULATE HEADING TO NEAREST FOOD SOURCE
for food in foods:
for org in organisms:
# CALCULATE DISTANCE TO SELECTED FOOD PARTICLE
food_org_dist = dist(org.x, org.y, food.x, food.y)
# DETERMINE IF THIS IS THE CLOSEST FOOD PARTICLE
if food_org_dist < org.d_food:
org.d_food = food_org_dist
org.r_food = calc_heading(org, food)
# GET ORGANISM RESPONSE
for org in organisms:
org.think()
# UPDATE ORGANISMS POSITION AND VELOCITY
for org in organisms:
org.update_r(settings)
org.update_vel(settings)
org.update_pos(settings)
return organisms
五、结果
在创建所有的主要组件之后,最终代码就可以组装起来了。注意:在上面的文章中,忽略了一些更小的函数,因为它们太简单了,阅读它们要比直接阅读代码花费更长的时间。所有的代码我都放在GitHub上了,你可以点击这里下载。
我使用了matplotlib来显示模拟。当你运行整个代码时,输出应该与此类似:
以上为译文
文章原标题《
Evolving Simple Organisms using a Genetic Algorithm and Deep Learning from Scratch with Python
》,作者:Nathan,译者:黄小凡,审校:袁虎。
文章为简译,更为详细的内容,请查看原文