遗传算法简介
遗传算法是一种基于生物进化思想而开发的一种优化算法,是一种选择技术和有限制的挑战性问题领域中不错的搜索和优化方法。遗传算法主要是通过模拟进化过程来寻找问题的最优解。一般来说,遗传算法的过程通常包括选择、交叉、突变以及全局搜索等基本操作。其中选择和交叉分别实现群体进化的优趋势和继承机制,而突变主要负责保证进化的多样性。遗传算法被广泛应用于各种领域的优化问题中,比如数字优化和机器学习中的参数调整以及神经网络等等。
下降函数表达式定义
下降函数 $f$ 表达式定义如下:
def f(x):
return (x+10)*math.sin(5*x)+7*math.cos(4*x)
这里,我们使用python 编程语言中的 math 模块,其中函数 $f(x)$ 就是我们所要求的函数的极小值。
遗传算法求解函数极小值过程
变量的定义
变量的定义主要包括:
个体编码方式
种群规模
迭代最大次数
交叉概率
变异概率
我们采用二进制编码方式。种群规模取 $1000$,迭代次数取 $100$,交叉概率取 $0.7$,变异概率为 $0.05$。
pop_size = 1000
n_generations = 100
cx_pb = 0.7
mut_pb = 0.05
初始种群的生成
我们可以采用随机数的方法来生成初始种群。在这个例子中,我们的初始种群为 $1000$ 个个体,每个个体的二进制编码长度为 $25$。
def init_population(pop_size,ind_size):
population = []
for i in range(pop_size):
individual = []
for j in range(ind_size):
individual.append(random.randint(0,1))
population.append(individual)
return population
population = init_population(pop_size,ind_size)
适应度函数的定义
适应度函数是遗传算法中非常重要的一个因素,它主要用于描述某个个体的优良程度大小。在这个例子中,我们采用 $-f(x)$ 的方式来作为适应度函数。
def aptitude(individual):
x = decode(individual)
return -f(x)
交叉操作
交叉是遗传算法中至关重要的一步,因为它是实现个体优趋势性和继承性的核心机制。在这个例子中,我们采用单点交叉的方法,即随机生成一个交叉点,然后将两个个体分为左右两部分,交换左右两部分的基因片段,最终生成两个子代。
def cross(parent1,parent2):
xover_point = random.randint(0,len(parent1))
child1 = parent1[:xover_point] + parent2[xover_point:]
child2 = parent2[:xover_point] + parent1[xover_point:]
return child1,child2
变异操作
变异是为了保证搜索空间的多样性,避免陷入局部最优解。在这个例子中,我们采用最简单的 **单点变异操作**,即随机生成一个变异点,将这个基因取反,最终生成一个新的变异体。
def mutation(individual,mut_pb):
individual_new = individual.copy()
for i in range(len(individual_new)):
if random.random() < mut_pb:
individual_new[i] ^= 1
return individual_new
选择操作
选择是为了保留种群中的精英和多样性,可以使用不同的选择策略。在此我们采用**轮盘赌选择**,根据种群中各个个体的适应度进行选择。
def roulette_wheel_selection(population,aptitude_values):
choose_prob = [x/sum(aptitude_values) for x in aptitude_values]
choosen_index = np.random.choice(len(population),size=1,p=choose_prob)
return population[choosen_index[0]]
主流程代码
主流程是遗传算法的核心,我们通过不断重复选择、交叉和变异等基本操作,来不断探索问题的最优解。在代码中,我们还会记录每次迭代后全局最优解。
def ga_dynamic(pop_size,n_generations,cx_pb,mut_pb):
population = init_population(pop_size,ind_size)
history_best_fitness = []
for i in range(n_generations):
offspring_population = []
aptitude_values = [aptitude(individual) for individual in population]
history_best_fitness.append(max(aptitude_values))
# elitism 保留最优个体
elitism_index = np.argmax(aptitude_values)
offspring_population.append(population[elitism_index])
# 产生 offspring_size 个子代,进行交叉和变异操作
for j in range(int((pop_size-1)/2)):
parent1 = roulette_wheel_selection(population,aptitude_values)
parent2 = roulette_wheel_selection(population,aptitude_values)
if random.random() < cx_pb: # crossover
children = cross(parent1,parent2)
else:
children = [parent1,parent2]
child1 = mutation(children[0],mut_pb)
child2 = mutation(children[1],mut_pb)
offspring_population.append(child1)
offspring_population.append(child2)
population = offspring_population.copy()
best_individual = max(population,key=lambda x: aptitude(x))
x = decode(best_individual)
return x, f(x), history_best_fitness
结果分析
我们通过上述代码来实现遗传算法对函数进行极值求解,这里主要讨论在不同迭代次数、交叉概率、变异概率下,算法的效果有何变化。具体结果如下:
迭代次数对结果的影响
首先,我们尝试通过修改迭代次数来探究其对算法效果的影响。我们分别设置 $50, 100, 200, 400$ 四种迭代次数,在每种情况下再采样 $10$ 次,观察所有采样结果下的最小值、最大值、平均值与标准差。结果如下图所示:
从上图看出,在迭代次数越大的情况下,算法能够更好的逐渐逼近这个最小值,同时各采样值之间的差异也开始逐渐降低。这表明算法在更长的时间内可以得到较为准确的结果,而随着迭代次数进一步增加,算法就会更优化。
交叉概率对结果的影响
接着,我们尝试通过修改交叉概率来探究其对算法效果的影响。我们分别设置 $0.2, 0.4, 0.6,$ 和 $0.8$ 四种交叉概率,在每种情况下再采样 $10$ 次,观察所有采样结果下的最小值、最大值、平均值与标准差。结果如下图所示:
从上图可以明显看出,随着交叉概率的增加,算法的最小值不断逼近全局最优值,同时各采样值之间的差异也不断降低。这表明在交叉概率比较大的情况下,算法的收敛速度会更快。
变异概率对结果的影响
最后,我们尝试通过修改变异概率来探究其对算法效果的影响。我们分别设置 $0.01, 0.02, 0.05,$ 和 $0.1$ 四种变异概率,在每种情况下再采样 $10$ 次,观察所有采样结果下的最小值、最大值、平均值与标准差。结果如下图所示:
从上图可以看出,随着变异概率的增加,算法的最小值逐渐增大并趋于稳定,同时其它采样指标之间的差异也逐渐降低。这表明我们的算法在更合理的情况下能够更好的降低变异概率,以保证最终能够得到更好的结果。
总结
通过这次试验,我们成功的利用遗传算法的思想对函数进行了求解,同时探究了在不同参数设置下求解效果的变化情况。当然,无论对于遗传算法还是其他的优化算法,其本质都是透过过去的探索,来不断寻找未来的秘密。这里我们的任务是如何在形形色色的参数设定中,找到最优解以及最好的算法时间。希望这份代码和实验能够对广大学者有所帮助。