MEALPY的用途广泛,你可以使用它来分析算法参数、进行算法的定性和定量分析、分析算法的收敛速率、测试和分析算法的可扩展性和健壮性。该库目前的版本为3.0.1,共包含215种算法,其中包括190种官方算法(原始算法、混合算法、变体算法)和25种开发算法。
MEALPY的特点在于支持解决连续和离散问题,并且在新版本中,所有功能都被封装在类和对象之中,使得定义模型一次后,就可以解决多个问题。
二话不说,先上一个简单代码:
我想说,是的,你说的对,这不是为了举简单例子嘛(比官网例子还简单一点),如果我拿出下面公式,阁下如何应对?\(f(x)=\sqrt[3]{x^3-x^2-x+1+y^3-xy^2}\)所以现在我们假装不知道\(y=x^2\)的极值公式和图像,求一下x等于多少的时候,y的最小值。
problem_dict={"obj_func":objective_func,"bounds":FloatVar(lb=[-10000],ub=[10000]),"minmax":"min",}在mealpy库中,problem_dict是一个字典,它定义了优化问题的所有必要信息。这个字典通常包含下列关键字:
optimizer=GA.BaseGA(epoch=100,pop_size=50,pc=0.85,pm=0.1)optimizer.solve(problem_dict)这段代码做了以下两件主要事情:
让我们解释这段代码之前,先说一下什么是遗传算法
遗传算法的基本概念包括个体、种群、适应度、选择、交叉(或称为杂交,英文为crossover)、变异等,以下是这些概念的详细介绍:
在遗传算法中,每一个可能的解都称为一个“个体”,通常表示为一串“基因”,这可以是二进制值、实数或其他编码形式。我们上面例子里,因为输入只有一个,那么个体就是那个公式里的\(x\)浮点本身,或者是代码里目标方法(objective_func)的solution参数本身。
种群是一组个体的集合,算法从这个种群中选择个体来进行遗传操作并产生新一代种群。我们上面例子里,pop_size=50就是一开始有50个x(一般初始化,都是随机的,[-10000,10000]范围内随机生成50个x)
适应度是评价个体优劣的标准,它是一个函数,用于衡量个体解决问题的能力。个体的适应度越高,它被选中并遗传到下一代的机会就越大。因为我们设置了求最小值"minmax":"min",,可以认为Fitness等价于例子里的objective_func。
选择是根据个体的适应度从当前种群中选出一部分个体,用于生成下一代。常见的选择方法有轮盘赌选择、锦标赛选择,随机选择等。BaseGA类通过selection_process__方法实现了几种不同的选择策略:
classBaseGA(Optimizer): def__init__(self,epoch:int=10000,pop_size:int=100,pc:float=0.95,pm:float=0.025,**kwargs:object)->None: #...初始化代码 self.selection="tournament" if"selection"inkwargs: self.selection=self.validator.check_str("selection",kwargs["selection"],["tournament","random","roulette"]) #...初始化代码选择策略,如果不指定selection参数,则会使用默认的锦标赛选择。如果你想使用轮盘赌选择或随机选择,需要在创建BaseGA实例时通过selection参数明确指定。
例子使用方式,例如:
model=BaseGA(epoch=100,pop_size=50,pc=0.85,pm=0.1,selection="roulette")交叉(Crossover):交叉是遗传算法中产生新个体的主要方式。它模拟生物学中的杂交现象,通过组合两个“父母”个体的部分基因来产生“子代”个体。例子中pc=0.85表示交叉概率是85%,意味着每一代中有85%的个体将通过交叉来产生新的后代。在BaseGA类中,交叉(Crossover)是通过crossover_process__方法实现的。该方法定义了几种不同的交叉策略:
defcrossover_process__(self,dad,mom): ifself.crossover=="arithmetic": w1,w2=self.crossover_arithmetic(dad,mom) elifself.crossover=="one_point": cut=self.generator.integers(1,self.problem.n_dims-1) w1=np.concatenate([dad[:cut],mom[cut:]]) w2=np.concatenate([mom[:cut],dad[cut:]]) elifself.crossover=="multi_points": idxs=self.generator.choice(range(1,self.problem.n_dims-1),2,replace=False) cut1,cut2=np.min(idxs),np.max(idxs) w1=np.concatenate([dad[:cut1],mom[cut1:cut2],dad[cut2:]]) w2=np.concatenate([mom[:cut1],dad[cut1:cut2],mom[cut2:]]) else:#uniform flip=self.generator.integers(0,2,self.problem.n_dims) w1=dad*flip+mom*(1-flip) w2=mom*flip+dad*(1-flip) returnw1,w2在这个方法中,dad和mom参数表示两个父代的基因串。根据self.crossover的值,它决定使用哪种交叉策略。每种策略都会计算并返回两个子代w1和w2的基因串。
注意,实际的交叉操作是否发生是由交叉概率self.pc控制的,这个概率决定了在种群中有多少比例的个体会经历交叉过程。如果随机数小于self.pc,那么调用crossover_process__方法进行交叉;否则,子代直接继承父代的基因。
简单来说,BaseGA类中的交叉是通过拼接两个父代的某些部分来产生子代,具体的拼接方式取决于所选的交叉策略。BaseGA默认使用均匀交叉uniform,对于浮点数的均匀交叉(UniformCrossover),方法与处理二进制串或整数串的方式类似,但是要考虑到浮点数的连续性。在均匀交叉中,每个基因位点都有一个独立的概率决定是从父代1继承还是从父代2继承。
对于浮点数列表的均匀交叉,可以按如下方式进行:
因为在上面例子中,每个个体只包含一个浮点数,所以均匀交叉将简化为以下步骤:
变异是在个体的基因序列中随机改变某些基因的过程,这增加了种群的多样性,有助于算法跳出局部最优解,探索更广泛的搜索空间。在BaseGA类中,变异(Mutation)是通过mutation_process__方法实现的。该方法根据所选的变异策略来对子代进行变异操作。变异操作的目的是在遗传算法的演化过程中引入一些随机性,以避免算法过早收敛到局部最优解,并增加搜索全局最优解的可能性。
BaseGA类提供了以下几种变异策略:
在BaseGA类的mutation_process__方法中,根据变异概率self.pm来决定是否对子代进行变异操作(默认是翻转变异Flip)。以下是mutation_process__方法的代码实现(部分):
defmutation_process__(self,child):ifself.mutation_multipoints:ifself.mutation=="swap":#...Swapmutationlogic...else:#"flip"mutation_child=self.problem.generate_solution()flag_child=self.generator.uniform(0,1,self.problem.n_dims) 对于例子里单浮点数的个体,翻转变异将简化为在给定的值范围内重新生成一个随机浮点数。如果变异策略是swap、scramble或inversion,由于只有一个浮点数,这些策略将不适用或不产生任何效果。 optimizer=GA.BaseGA(epoch=100,pop_size=50,pc=0.85,pm=0.1)这行代码创建了一个遗传算法优化器的实例,并设置了几个重要的参数: optimizer.solve(problem_dict)这行代码调用优化器的solve方法,并传递之前定义的problem_dict作为参数。solve方法将使用遗传算法来寻找最优解或者尽可能接近最优解的解决方案。 problem_dict包含了目标函数obj_func(用于评估解的质量)、解的边界bounds(定义了解的搜索空间),以及优化目标minmax(指明是最小化问题还是最大化问题)。 通过执行这两行代码,遗传算法将运行指定的迭代次数(在本例中为100代),并在每一代中使用遗传操作(选择、交叉、变异)来改进解,最终返回找到的最佳解。这个最佳解将是根据目标函数评估得到的最优质的解,同时受到解空间边界的约束。 就看结果的最后两行,不要看中间过程,中间过程可以拿来分析,如果不想看到中间过程,可以在问题字典里添加"log_to":None,键值对。 #定义问题字典problem_dict={"obj_func":objective_func,"bounds":FloatVar(lb=[-10000],ub=[10000]),"minmax":"min","log_to":None,}迭代100代,从-10000到10000实数里,最终结果算出来,当\(x=35.09989745\)时,\(f(x)\)最小,且等于1232.002800987481然后,第一次接触遗传算法的,不禁会发出大大的疑问:搞了半天,就这????!!!!我一秒钟都得出是\(x=0\)了。 我知道你困了累了,但请别困,别累。从结果来看,那说明咱们上面说的原理没错,确实是一步一步的遗传变异,得到的结果。至于结果不如意,那肯定是需要继续调优的。不然那些调包侠,调参侠的称呼怎么来的? GA.BaseGA它在寻找全局最优解的过程中可能会找到局部最优解或者近似解。遗传算法的性能(即能否找到全局最优解以及找到解的速度)取决于多个因素,包括: 例子中目标函数是一个简单的单变量平方函数,它的全局最小值在x=0处(假装不知道)。遗传算法应该能够找到这个全局最小值,或者至少是一个非常接近的近似值。结果不尽如人意,可能有几个原因: 要改进结果,可以尝试以下方法: 根据上面的分析,我们可以挑一两个参数试试 代码其他不变,代数从100代变成10000代 optimizer=GA.BaseGA(epoch=10000,pop_size=50,pc=0.85,pm=0.1)结果:[-0.20769044]0.04313531784135752 看来结果是-0.2,还是不太能接受,继续努力优化。 从结果来看,\(x\)越来越小,那么,\(x\)边界值就可以不用从[-10000,10000]这么大范围筛选了,直接[-10,10] problem_dict={"obj_func":objective_func,"bounds":FloatVar(lb=[-10],ub=[10]),"minmax":"min",}optimizer=GA.BaseGA(epoch=10000,pop_size=50,pc=0.85,pm=0.1)结果:[7.35480744e-05]5.409319244888902e-09 $x=7.35480744\times10^{-5},f(x)=5.409319244888902\times10^{-9}$第二次结果看起来非常不错。运气不错哈。(不要脸!)