sopt:一个简单的python最优化库

引言

    最近有些朋友总来问我有关遗传算法的东西,我是在大学搞数学建模的时候接触过一些最优化和进化算法方面的东西,以前也写过几篇博客记录过,比如遗传算法的C语言实现(一):以非线性函数求极值为例和C语言实现粒子群算法(PSO)一等,如果对原理有兴趣的话可以去我的博客具体查看:Lyrichu's Blog。所以突发奇想,干脆把以前写的一些进化算法比如遗传算法(GA),粒子群算法(PSO),模拟退火算法(SA)以及最近看的基于梯度的一些优化算法比如Gradient Descent,SGD,Momentum等整理一下,写成一个python库,方便那些有需要的朋友使用。断断续续花了几天的时间,初步完成了一些基本功能,这里简单介绍一下。

sopt 简介


     sopt是simple optimization的简称,目前我已经将代码托管到pypi,地址是 sopt,可以直接通过pip命令下载安装使用,由于项目只依赖numpy,所以在windows和linux环境下安装都很方便,直接 pip install sopt即可。项目的github地址是 sopt。目前sopt包含的优化方法如下:

  • 遗传算法(Genetic Algorithm,GA)
  • 粒子群算法(Particle Swarm Optimization,PSO)
  • 模拟退火算法(Simulated Anealing,SA)
  • 随机游走算法(Random Walk):
  • 梯度下降法(Gradient Descent,GD)
  • 动量优化算法(Momentum)
  • 自适应梯度算法(AdaGrad)
  • RMSProp
  • Adam
    由于只是一个初步的版本,后续如果有时间的话,会加上更多的优化算法进去。目前所有的优化算法暂时只支持连续实函数的优化;除了基于梯度的几个优化算法,GA、PSO、SA以及Random Walk都同时支持无约束优化线性约束优化以及非线性约束优化。具体我会在下面详细说明。

    sopt 使用详解以及实例演示


    1.SGA 使用


        SGA是Simple Genetic Algorithm的简称,是最基本的遗传算法。其编码方式采用二进制编码,选择方法采用轮盘赌法,交叉方法采用单点交叉,变异方式采用均匀变异,默认是求函数的最小值。下面是sopt中SGA的一个简单使用实例:

    from sopt.SGA import SGA
    from math import sin
    def func1(x):
    return (x[0]-1)**2 + (sin(x[1])-0.5)**4 + 2
    if __name__ == '__main__':
    sga = SGA.SGA(func = func1,func_type = 'min',variables_num = 2,
        lower_bound = 0,upper_bound = 2,generations = 20,
        binary_code_length = 10)
    # run SGA
    sga.run()
    # show the SGA optimization result in figure
    sga.save_plot()
    # print the result
    sga.show_result()

    运行结果如下:

    -------------------- SGA config is: --------------------
    lower_bound:[0, 0]
    generations:20
    cross_rate:0.7
    variables_num:2
    mutation_rate:0.1
    func_type:min
    upper_bound:[2, 2]
    population_size:100
    func:
    binary_code_length:10
    -------------------- SGA caculation result is: --------------------
    global best generation index/total generations:3/20
    global best point:[1.00488759 0.45356794]
    global best target:2.00003849823336
    用图像展示为图1所示:

    图1 SGA 运行结果
    上面定义的目标函数为\(f(x_1,x_2)=(x_1-1)^2+(sin(x_2)-0.5)^4+2\),其中\(0,函数的最小值点为(1,0.5236),最小值为2,与寻找到的最小值点(1.00488759 0.45356794)以及最小值2.00003849823336是非常接近的。
    SGA类的全部参数定义如下:
  • lower_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的下界,如果是一个数字的话,我们认为所有的下界都是一样的(必填);
  • upper_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的上界,如果是一个数字的话,我们认为所有的上界都是一样的(必填);
  • variables_num:表示目标函数变量的个数(必填);
  • func(必填):表示目标函数;
  • cross_rate:GA的交叉率,默认为0.7;
  • mutation_rate:变异率,默认为0.1;
  • population_size:种群数目,默认为100;
  • generations:进化代数,默认是200;
  • binary_code_length:GA二进制编码的长度,默认是10;
  • func_type:函数优化类型,求最小值取值为'min',求最大值取值为'max',默认是'min';

    2. GA 使用


        GA是相比SGA更加一般的遗传算法实现,其对于编码方式,选择方式,交叉方式以变异方式等会有更多的支持。首先还是看一个例子:

    from sopt.GA.GA import GA
    from sopt.util.functions import *
    from sopt.util.ga_config import *
    from sopt.util.constraints import *
    class TestGA:
    def __init__(self):
        self.func = quadratic11
        self.func_type = quadratic11_func_type
        self.variables_num = quadratic11_variables_num
        self.lower_bound = quadratic11_lower_bound
        self.upper_bound = quadratic11_upper_bound
        self.cross_rate = 0.8
        self.mutation_rate = 0.05
        self.generations = 200
        self.population_size = 100
        self.binary_code_length = 20
        self.cross_rate_exp = 1
        self.mutation_rate_exp = 1
        self.code_type = code_type.binary
        self.cross_code = False
        self.select_method = select_method.proportion
        self.rank_select_probs = None
        self.tournament_num = 2
        self.cross_method = cross_method.uniform
        self.arithmetic_cross_alpha = 0.1
        self.arithmetic_cross_exp = 1
        self.mutation_method = mutation_method.uniform
        self.none_uniform_mutation_rate = 1
        #self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints = None
        self.complex_constraints_method = complex_constraints_method.penalty
        self.complex_constraints_C = 1e6
        self.M = 1e8
        self.GA = GA(**self.__dict__)
    def test(self):
        start_time = time()
        self.GA.run()
        print("GA costs %.4f seconds!" % (time()-start_time))
        self.GA.save_plot()
        self.GA.show_result()
    if __name__ == '__main__':
    TestGA().test()

    上面代码的运行结果为:

    GA costs 6.8320 seconds!
    -------------------- GA config is: --------------------
    func:
    code_type:binary
    complex_constraints:None
    global_generations_step:200
    cross_method:uniform
    mutation_method:uniform
    cross_rate:0.8
    lower_bound:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    tournament_num:2
    variables_num:11
    complex_constraints_method:penalty
    none_uniform_mutation_rate:1
    population_size:100
    mutation_rate:0.05
    generations:200
    arithmetic_cross_alpha:0.1
    func_type:min
    mutation_rate_exp:1
    cross_rate_exp:1
    arithmetic_cross_exp:1
    M:100000000.0
    select_method:proportion
    upper_bound:[11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11]
    cross_code:False
    binary_code_length:20
    complex_constraints_C:1000000.0
    -------------------- GA caculation result is: --------------------
    global best target generation index/total generations:149/200
    global best point:[ 1.07431037  2.41401426  2.4807906   4.36291634  4.90653029  6.13753427
      6.58147963  7.7370479   9.42957347 10.46616122 10.87151134]
    global best target:2.2685208668743204
        其中sopt.util.functions预定义了一些测试函数,quadratic11是一个11元变量的二次函数,函数原型为:\(quadratic11(x_1,...,x_{11})=(x_1-1)^2 +(x_2-2)^2 + ... +(x_{11}-11)^2 + 1\),其中\(1 \le x_1,...,x_{11} \le 11\),函数的最小值点在(1,2,...,11)处取得,最小值为1。另外还定义了其他几个测试函数为:
  • quadratic50:和quadratic11类似,只是变量个数变为50,取值范围变为1-50;
  • quadratic100:和quadratic11类似,只是变量个数变为100,取值范围变为1-100;
  • quadratic500:和quadratic11类似,只是变量个数变为500,取值范围变为1-500;
  • quadratic1000:和quadratic1000类似,只是变量个数变为1000,取值范围变为1-1000;
  • Rosenbrock:函数原型为:\(Rosenbrock(x_1,x_2)=100(x_1^2-x_2)^2+(1-x_1)^2\),其中\(-2.048 \le x_1,x_2 \le 2.048\),这个函数有很多局部极小值点,函数在(-2.048,-2.048)处取得最大值;
  • shubert2函数:函数定义为,\(shubert2(x_1,x_2)=\prod_{i=1}^{2}(\sum_{j=1}^{5}(jcos((j+1)*x_i+j))\),其中\(-10 \le x_1,x_2 \le 10\),函数有760个局部极小值点,全局极小值为-186.731,这里为了将适应度函数变换为正,我们在原函数的基础上加上1000,这样函数的全局极小值点就变为813.269;
  • shubert函数,将shubert2函数中的两个变量拓展为n个变量(\(\prod_{i=1}^{n}\))就可以得到一般的shubert函数了。
        对于每一个优化方法,我们都预定了一个形如xx_config的模块,其中定义了该优化方法的一些常用默认参数,比如ga_config中就定义了一些GA的一些常用优化参数,ga_config.basic_config定义了一些基础参数设置,比如basic_config.generations是一个默认进化代数,basic_config.mutation_rate是默认的变异参数;而ga_config.cross_method则预定义了所有支持的交叉方法,比如cross_method.uniform表示均匀交叉,cross_method.one_point表示单点交叉等;ga_config.mutation_method等也是类似的。有了这些预定义变量,可以免去我们手动输入很多参数取值以及传入方法字符串的麻烦(有时候可能会写错)。
        观察上面的运行结果,我们发现对于quadratic11函数,最终找到的全局极小值为2.2685208668743204,和真实的全局极小值点1已经比较接近了;运行耗时6秒多,似乎有些长,这是因为我们种群数目设为100,进化代数设为200,比较大,而且又是采用二进制20位编码,再加上python脚本语言的运行效率问题,所以稍慢。为了做一个对比,这里我们将目标函数从quadratic11变为Rosenbrock函数,其他参数设置保持不变,得到的结果如下:

    GA costs 1.7245 seconds!
    -------------------- GA config is: --------------------
    population_size:100
    lower_bound:[-2.048, -2.048]
    mutation_rate_exp:1
    select_method:proportion
    code_type:binary
    global_generations_step:200
    generations:200
    mutation_method:uniform
    complex_constraints_method:penalty
    binary_code_length:20
    cross_method:uniform
    arithmetic_cross_alpha:0.1
    func:
    upper_bound:[2.048, 2.048]
    cross_code:False
    mutation_rate:0.05
    cross_rate_exp:1
    complex_constraints_C:1000000.0
    cross_rate:0.8
    variables_num:2
    M:100000000.0
    complex_constraints:None
    none_uniform_mutation_rate:1
    tournament_num:2
    arithmetic_cross_exp:1
    func_type:max
    -------------------- GA caculation result is: --------------------
    global best target generation index/total generations:75/200
    global best point:[-2.04776953 -2.04537109]
    global best target:3901.4655271502425
    图2是每代最优值的计算结果:

    图2 GA Rosenbrock 函数运行200代寻优结果
    替换成Rosenbrock函数之后,函数的运行时间从6秒多减少到1秒多(函数变量个数明显减少了),这说明GA的运行时间与目标函数的变量个数是显著相关的。最后找到的全局极大值点为(-2.04776953,-2.04537109)和真实全局极大值点(-2.048,-2.048)已经很接近了。
        SGA类是GA类的父类,所以GA类和SGA类有一些公共的属性,比如generations,population_size,func_type等。和SGA一样的参数这里就不再列举了,如下是GA特有的一些参数:
  • cross_rate_exp:cross_rate指数递增的参数取值,即变异率按照\(r_t=r_0 \beta^t\),这里\(r_t\)表示t次迭代的交叉率,\(r_0\)是初始的交叉率,\(\beta\)就是这里的cross_rate_exp,默认取值为1,一般设置为一个比1稍大的数字,比如1.0001等;
  • mutation_rate_exp:mutation_rate指数递增参数取值,具体含义和cross_rate_exp类似;
  • code_type:采用什么样的编码方式,有三种取值:'binary'表示二进制编码(默认),'gray'表示采用格雷编码,'real'表示采用实数编码。其中格雷编码是一种改进的二进制编码,其优点在于进行交叉、变异等操作时,改变了染色体基因的某几个位置,二进制编码对应的实数值可能会发生非常大的变化,而格雷编码一般不会。二进制编码与格雷编码的转换可以参考附录;
  • cross_code:这是一个布尔值,表示是否对二进制编码或格雷码采用交叉编码的方式。具体说明参考附录;
  • select_method:选择操作方法,有如下的6种取值:
  1. 'proportion':表示比例选择(轮盘赌法)
  2. 'keep_best':表示保留最优个体
  3. 'determinate_sampling':表示确定性采样
  4. 'rssr':remainder stochastic sampling with replacement的缩写,表示无放回余数随机选择
  5. 'rank':表示排序选择
  6. 'stochastic_tournament':表示随机锦标赛法(以上6种选择方式详细说明请参考附录)
  • rank_select_probs:仅当select_method取值为'rank'时,该参数才起作用,表示采用排序选择时,适应度从低到高每个个体被选择的概率,默认为None,采用\(p_i=\frac{i}{\sum_{j=1}^{n}j},i=1,2,...,n\)的方式计算,其中\(n\)表示种群个数,\(p_i\)表示第\(i\)个个体被选择的概率,所有概率之和为1,如果取值不为None,你应该传入一个size为population_size的ndarray,所有元素按照递增排序,数组和为1;
  • tournament_num:如果select_method取值为'stochastic_tournament',该值表示锦标赛竞争者数量;
  • cross_method:交叉方法,有如下4种取值:
  1. 'one_point':表示单点交叉
  2. 'two_point':表示双点交叉
  3. 'uniform':表示均匀交叉
  4. 'arithmetic':表示算术交叉,只有当编码采用实数编码时,才能使用算术交叉
  • arithmetic_cross_alpha:算术交叉的系数,算术交叉的公式为:\(x_{new1}=\alpha x_1 + (1-\alpha)x_2,x_{new2}=\alpha x_2 + (1-\alpha)x_1\),这里的\(\alpha\)即表示arithmetic_cross_alpha,其默认值为0.1;
  • 'arithmetic_cross_exp':算术交叉系数按照指数递增,即\(\alpha_t = \alpha r^t\),这里\(\alpha_t\)表示第t代的算术交叉系数,\(\alpha\)是初始交叉系数,\(r\)即是这里的arithmetic_cross_exp,默认取值为1,一般取一个比1稍大的数,比如1.0001,t是进化代数;
  • 'mutation_method':变异方法,有如下5种取值:
  1. simple:即简单变异,随机选择一个染色体上的基因,按照变异概率进行变异
  2. uniform:即均匀变异,每个染色体上的每个基因都按照变异概率进行变异
  3. boundary:边界变异,每个基因进行变异以后的取值只能去边界值,比如说各以0.5的概率取得其上界和下界,这种变异方式仅适用于实数编码的情况,一般在目标函数的最优点靠近边界时使用
  4. none_uniform:非均匀变异,我们不是取均匀分布的随机数去替换原来的基因,而是在原来基因的基础上做一点微小的随机扰动,扰动以后的结果作为新的基因值,具体来说,我们采用的是这样的变异方式:1)if random(0,1) = 0,\(x_k^{'} = x_k + \bigtriangleup (t,U_{max}^{k}-x_k)\);2)if random(0,1) = 1,\(x_k{'} = x_k - \bigtriangleup (t,x_k -U_{min}{k})\) 其中 \(\bigtriangleup(t,y)=y(1-r^{(1-t/T)b})\),\(r\)是0-1均匀分布的一个随机数,\(T\)是最大进化代数,\(b\)是一个系统参数,表示随机扰动对于进化代数的依赖长度,默认值为1
  5. gaussian:高斯变异,即使用高斯分布去替代均匀分布来进行变异,由高斯分布的特点可以知道,这种变异方式也是在原个体区域附近的某个局部区域进行重点搜索,这里高斯分布的均值\(\mu\)定义为\(\mu = \frac{U_{min}^{k}+U_{max}^{k}}{2}\),标准差\(\sigma=\frac{U_{max}^{k}-U_{min}^{k}}{6}\)
  • none_uniform_mutation_rate:即均匀分布none_uniform中定义的系统参数\(b\);
  • complex_constraints:复杂约束,默认值为None,即没有复杂约束,只有简单的边界约束,如果有复杂约束,其取值应该是\(\[func_1,func_2,...,func_n\]\),
    其中\(func_i\)表示第\(i\)个复杂约束函数名,比如对于一个复杂约束函数\(func_1:x_1^2+x_2^2 < 3\),其复杂约束函数应该这样定义:

    def func1(x):
    x1 = x[0]
    x2 = x[1]
    return x1**2 + x2**2 - 3
  • complex_constraints_method:复杂约束求解的方法,默认是penalty即惩罚函数法,暂时不支持其他的求解方式;
  • complex_constraints_C:采用penalty求解复杂约束的系数\(C\),比如对于某一个约束\(x_1^2+x_2^2 < 3\),GA在求解过程中,违反了该约束,即解满足\(x_1^2+x_2^2 \ge 3\),那么我们对目标函数增加一个惩罚项: \(C(x_1^2+x_2^2-3)\),\(C\)一般取一个很大的正数,默认值为\(10^6\)
        GA类的参数很多可以调节,所以相比之下使用起来更加麻烦,特别是对于复杂函数的寻优,默认参数未必是最好的,可能需要你根据目标函数的特点自行尝试,选择最优的参数组合。

    3. PSO 使用


        PSO算法的原理可以参考我之前的博客C语言实现粒子群算法(PSO)一和C语言实现粒子群算法(PSO)二,这里不再赘述。还是先看一个实例代码:

    from time import time
    from sopt.util.functions import *
    from sopt.util.pso_config import *
    from sopt.PSO.PSO import PSO
    from sopt.util.constraints import *
    class TestPSO:
    def __init__(self):
        self.func = quadratic11
        self.func_type = quadratic11_func_type
        self.variables_num = quadratic11_variables_num
        self.lower_bound = quadratic11_lower_bound
        self.upper_bound = quadratic11_upper_bound
        self.c1 = basic_config.c1
        self.c2 = basic_config.c2
        self.generations = 200
        self.population_size = 100
        self.vmax = 1
        self.vmin = -1
        self.w = 1
        self.w_start = 0.9
        self.w_end = 0.4
        self.w_method = pso_w_method.linear_decrease
        #self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints = None
        self.complex_constraints_method = complex_constraints_method.loop
        self.PSO = PSO(**self.__dict__)
    def test(self):
        start_time = time()
        self.PSO.run()
        print("PSO costs %.4f seconds!" %(time()-start_time))
        self.PSO.save_plot()
        self.PSO.show_result()
    if __name__ == '__main__':
    TestPSO().test()

    运行结果为:

    PSO costs 1.1731 seconds!
    -------------------- PSO config is: --------------------
    complex_constraints_method:loop
    c1:1.49445
    lower_bound:[1 1 1 1 1 1 1 1 1 1 1]
    w_end:0.4
    w_method:linear_decrease
    complex_constraints:None
    func:
    upper_bound:[11 11 11 11 11 11 11 11 11 11 11]
    generations:200
    func_type:min
    w:1
    c2:1.49445
    w_start:0.9
    population_size:100
    vmin:-1
    vmax:1
    variables_num:11
    -------------------- PSO calculation result is: --------------------
    global best generation index/total generations: 198/200
    global best point: [ 1.          1.99999999  2.99999999  4.          5.          6.
      7.00000001  7.99999999  9.00000001 10.00000001 11.        ]
    global best target: 1.0
    图3 PSO 求解 quadratic11 200代运行结果 上面的代码意图应该是非常明显的,目标函数是\(quadratic11\),最终求得的最小值点几乎就是全局最小值点。下面是PSO类中所有参数的具体定义:
  • variables_num:变量个数(必填);
  • lower_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的下界,如果是一个数字的话,我们认为所有的下界都是一样的(必填);
  • upper_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的上界,如果是一个数字的话,我们认为所有的上界都是一样的(必填);
  • func:目标函数名(必填);
  • func_type:函数优化类型,求最小值取值为'min',求最大值取值为'max',默认是'min';
  • c1:PSO 参数c1,默认值为1.49445;
  • c2:PSO 参数c2,默认值为1.49445;
  • generations:进化代数,默认值为100;
  • population_size:种群数量,默认为50;
  • vmax:粒子最大速度,默认值为1;
  • vmin:粒子最小速度,默认值为-1;
  • w:粒子惯性权重,默认为1;
  • w_start:如果不是使用恒定的惯性权重话,比如使用权重递减策略,w_start表示初始权重;
  • w_end:相应的,w_end表示末尾权重;
  • w_method:权重递减的方式,有如下5种取值:
  1. 'constant':权重恒定
  2. 'linear_decrease':线性递减,即\(w_t = w_{end} + (w_{start} - w_{end})\frac{(T-t)}{T}\),其中\(T\)表示最大进化代数,\(w_t\)表示第\(t\)代权重
  3. 'square1_decrease':第一种平方递减,即:\(w_t = w_{start} - (w_{start}-w_{end})(\frac{t}{T})^2\)
  4. 'square2_decrease':第二种平方递减,即:\(w_t = w_{start} - (w_{start}-w_{end})(\frac{2t}{T}-(\frac{t}{T})^2)\)
  5. 'exp_decrease':指数递减,即:\(w_t = w_{end}(\frac{w_{start}}{w_{end}})^{\frac{1}{1+\frac{10t}{T}}}\)
  • complex_constraints:复杂约束,默认为None,具体含义参考GA类的complex_constraints
  • complex_constraints_method:求解复杂约束的方法,默认为'loop',即如果解不满足复杂约束,则再次随机产生解,直到满足约束,暂时不支持其他的求解方式。

    4. SA 使用


        SA,即模拟退火算法,是基于概率的一种寻优方法,具体原理可以参考我的博客模拟退火算法(SA)求解TSP 问题(C语言实现)。sopt中SA的使用实例如下:

    from time import time
    from sopt.util.functions import *
    from sopt.Optimizers.SA import SA
    from sopt.util.sa_config import *
    from sopt.util.constraints import *
    class TestSA:
    def __init__(self):
        self.func = Rosenbrock
        self.func_type = Rosenbrock_func_type
        self.variables_num = Rosenbrock_variables_num
        self.lower_bound = Rosenbrock_lower_bound
        self.upper_bound = Rosenbrock_upper_bound
        self.T_start = 100
        self.T_end = 1e-6
        self.q = 0.9
        self.L = 100
        self.init_pos = None
        #self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints_method = complex_constraints_method.loop
        self.SA = SA(**self.__dict__)
    def test(self):
        start_time = time()
        self.SA.run()
        print("SA costs %.4f seconds!" %(time()-start_time))
        self.SA.save_plot()
        self.SA.show_result()
    if __name__ == '__main__':
    TestSA().test()

    运行结果如下:

    SA costs 0.2039 seconds!
    -------------------- SA config is: --------------------
    func_type:max
    complex_constraints:None
    q:0.9
    complex_constraints_method:loop
    T_start:100
    steps:17500
    T_end:1e-06
    L:100
    func:
    variables_num:2
    lower_bound:[-2.048 -2.048]
    init_pos:[-2.03887265 -2.02503927]
    upper_bound:[2.048 2.048]
    -------------------- SA calculation result is: --------------------
    global best generation index/total generations:2126/17500
    global best point: [-2.03887265 -2.02503927]
    global best target: 3830.997799328349
    图4 SA求解Rosenbrock函数 SA类的具体参数含义如下:
  • variables_num:变量个数(必填);
  • lower_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的下界,如果是一个数字的话,我们认为所有的下界都是一样的(必填);
  • upper_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的上界,如果是一个数字的话,我们认为所有的上界都是一样的(必填);
  • func:目标函数名(必填);
  • func_type:函数优化类型,求最小值取值为'min',求最大值取值为'max',默认是'min';
  • T_start:初始温度,默认值为100;
  • T_end:末尾温度,默认值为\(10^{-6}\);
  • q:退火系数,默认值为0.9,一般取一个在[0.9,1)之间的数字;
  • L:每个温度时的迭代次数,即链长,默认值为100;
  • init_pos:初始解的取值,默认值为None,此时初始解是随机生成的,或者你也可以指定一个用ndarray表示的初始解;
  • complex_constraints:复杂约束,默认为None,表示没有约束,具体定义同GA 的 complex_constraints;
  • complex_constraints_method:求解复杂约束的方法,默认为'loop',即如果解不满足复杂约束,则再次随机产生解,直到满足约束,暂时不支持其他的求解方式。

    5. Random Walk 使用


        Random Walk 即随机游走算法,是一种全局随机搜索算法,具体原理可以参考我的博客介绍一个全局最优化的方法:随机游走算法(Random Walk)。sopt中Random Walk的使用实例如下:

    from time import time
    from sopt.util.functions import *
    from sopt.util.constraints import *
    from sopt.util.random_walk_config import *
    from sopt.Optimizers.RandomWalk import RandomWalk
    class TestRandomWalk:
    def __init__(self):
        self.func = quadratic50
        self.func_type = quadratic50_func_type
        self.variables_num = quadratic50_variables_num
        self.lower_bound = quadratic50_lower_bound
        self.upper_bound = quadratic50_upper_bound
        self.generations = 100
        self.init_step = 10
        self.eps = 1e-4
        self.vectors_num = 10
        self.init_pos = None
        # self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints = None
        self.complex_constraints_method = complex_constraints_method.loop
        self.RandomWalk = RandomWalk(**self.__dict__)
    def test(self):
        start_time = time()
        self.RandomWalk.random_walk()
        print("random walk costs %.4f seconds!" %(time() - start_time))
        self.RandomWalk.save_plot()
        self.RandomWalk.show_result()
    if __name__ == '__main__':
    TestRandomWalk().test()

    运行结果为:

    Finish 1 random walk!
    Finish 2 random walk!
    Finish 3 random walk!
    Finish 4 random walk!
    Finish 5 random walk!
    Finish 6 random walk!
    Finish 7 random walk!
    Finish 8 random walk!
    Finish 9 random walk!
    Finish 10 random walk!
    Finish 11 random walk!
    Finish 12 random walk!
    Finish 13 random walk!
    Finish 14 random walk!
    Finish 15 random walk!
    Finish 16 random walk!
    Finish 17 random walk!
    random walk costs 1.0647 seconds!
    -------------------- random walk config is: --------------------
    init_step:10
    eps:0.0001
    generations_nums:9042
    lower_bound:[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
     1 1 1 1 1 1 1 1 1 1 1 1 1]
    complex_constraints_method:loop
    walk_nums:17
    complex_constraints:None
    vectors_num:10
    upper_bound:[50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50
     50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50
     50 50]
    variables_num:50
    func:
    generations:100
    func_type:min
    -------------------- random walk caculation result is: --------------------
    global best generation index/total generations:8942/9042
    global best point is: [ 1.00004803  1.99999419  3.00000569  3.99998558  5.00002455  5.99999255
      6.99992476  7.99992864  9.00000401  9.99994717 10.99998155 12.00002429
     13.0000035  13.99998567 15.00000421 16.00001454 16.99997252 17.99998041
     19.00002491 20.00003141 21.00004182 21.99998565 22.99997668 23.99999821
     24.99995881 25.99999359 27.00000443 28.00005117 28.99998132 30.00004136
     31.00002021 32.00000616 33.00000678 34.00005423 35.00001799 36.00000051
     37.00002749 38.00000203 39.00007087 39.9999964  41.00004432 42.0000158
     42.99992991 43.99995352 44.99997267 46.00003533 46.9999834  47.99996778
     49.00002904 50.        ]
    global best target is: 1.0000000528527013
    图5 Random Walk 求解quadratic50 经过实验发现,Random Walk 具有非常强的全局寻优能力,对于quadratic50这种具有50个变量的复杂目标函数,它也可以很快找到其全局最优点,而且运行速度也很快。RandomWalk类的具体参数含义如下:
  • variables_num:变量个数(必填);
  • lower_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的下界,如果是一个数字的话,我们认为所有的下界都是一样的(必填);
  • upper_bound:一个int或者float的数字,或者是长度为variables_num的ndarray,表示每个变量的上界,如果是一个数字的话,我们认为所有的上界都是一样的(必填);
  • func:目标函数名(必填);
  • func_type:函数优化类型,求最小值取值为'min',求最大值取值为'max',默认是'min';
  • generations:每个step的最大迭代次数,默认是100;
  • init_step:初始步长(step),默认值为10.0;
  • eps:终止迭代的步长,默认为\(10^{-4}\);
  • vectors_num:使用 improved_random_walk时随机产生向量的个数,默认为10;
  • init_pos:初始解的取值,默认值为None,此时初始解是随机生成的,或者你也可以指定一个用ndarray表示的初始解;
  • complex_constraints:复杂约束,默认为None,表示没有约束,具体定义同GA 的 complex_constraints;
  • complex_constraints_method:求解复杂约束的方法,默认为'loop',即如果解不满足复杂约束,则再次随机产生解,直到满足约束,暂时不支持其他的求解方式。
    RandomWalk 除了提供基本的random_walk函数之外,还提供了一个更加强大的improved_random_walk函数,后者的全局寻优能力要更强。

    6. 求解带复杂约束的目标函数


        上面所述的各种优化方法求解的都是变量仅有简单边界约束(形如\(a \le x_i \le b\)),下面介绍如何使用各种优化方法求解带有复杂约束条件的目标函数。其实,求解方法也非常简单,以GA为例,下面的例子即对Rosenbrock函数求解了带有三个复杂约束条件的最优值:

    from time import time
    from sopt.GA.GA import GA
    from sopt.util.functions import *
    from sopt.util.ga_config import *
    from sopt.util.constraints import *
    class TestGA:
    def __init__(self):
        self.func = Rosenbrock
        self.func_type = Rosenbrock_func_type
        self.variables_num = Rosenbrock_variables_num
        self.lower_bound = Rosenbrock_lower_bound
        self.upper_bound = Rosenbrock_upper_bound
        self.cross_rate = 0.8
        self.mutation_rate = 0.1
        self.generations = 300
        self.population_size = 200
        self.binary_code_length = 20
        self.cross_rate_exp = 1
        self.mutation_rate_exp = 1
        self.code_type = code_type.real 
        self.cross_code = False
        self.select_method = select_method.proportion
        self.rank_select_probs = None
        self.tournament_num = 2
        self.cross_method = cross_method.uniform
        self.arithmetic_cross_alpha = 0.1
        self.arithmetic_cross_exp = 1
        self.mutation_method = mutation_method.uniform
        self.none_uniform_mutation_rate = 1
        self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints_method = complex_constraints_method.penalty
        self.complex_constraints_C = 1e8
        self.M = 1e8
        self.GA = GA(**self.__dict__)
    def test(self):
        start_time = time()
        self.GA.run()
        print("GA costs %.4f seconds!" % (time()-start_time))
        self.GA.save_plot()
        self.GA.show_result()
    if __name__ == '__main__':
    TestGA().test()

    运行结果如下:

    GA costs 1.9957 seconds!
    -------------------- GA config is: --------------------
    lower_bound:[-2.048, -2.048]
    cross_code:False
    complex_constraints_method:penalty
    mutation_method:uniform
    mutation_rate:0.1
    mutation_rate_exp:1
    cross_rate:0.8
    upper_bound:[2.048, 2.048]
    arithmetic_cross_exp:1
    variables_num:2
    generations:300
    tournament_num:2
    select_method:proportion
    func_type:max
    complex_constraints_C:100000000.0
    cross_method:uniform
    complex_constraints:[, , ]
    func:
    none_uniform_mutation_rate:1
    cross_rate_exp:1
    code_type:real
    M:100000000.0
    binary_code_length:20
    global_generations_step:300
    population_size:200
    arithmetic_cross_alpha:0.1
    -------------------- GA caculation result is: --------------------
    global best target generation index/total generations:226/300
    global best point:[ 1.7182846  -1.74504313]
    global best target:2207.2089435117955
    图6 GA求解带有三个复杂约束的Rosenbrock函数

    上面的constraints1,constraints2,constraints3是三个预定义的约束条件函数,其定义分别为:\(constraints1:x_1^2 + x_2^2 - 6 \le 0\);\(constraints2:x_1 + x_2 \le 0\);\(constraints3:-2-x_1 - x_2 \le 0\),函数原型为:

    def constraints1(x):
    x1 = x[0]
    x2 = x[1]
    return x1**2 + x2**2 -3
    def constraints2(x):
    x1 = x[0]
    x2 = x[1]
    return x1+x2
    def constraints3(x):
    x1 = x[0]
    x2 = x[1]
    return -2 -x1 -x2

    其实观察可以发现,上面的代码和原始的GA实例代码唯一的区别,就是其增加了self.complex_constraints = [constraints1,constraints2,constraints3]这样一句,对于其他的优化方法,其都定义了complex_constraintscomplex_constraints_method这两个属性,只要传入相应的约束条件函数列表以及求解约束条件的方法就可以求解带复杂约束的目标函数了。比如我们再用Random Walk求解和上面一样的带三个约束的Rosenbrock函数,代码及运行结果如下:

    from time import time
    from sopt.util.functions import *
    from sopt.util.constraints import *
    from sopt.util.random_walk_config import *
    from sopt.Optimizers.RandomWalk import RandomWalk
    class TestRandomWalk:
    def __init__(self):
        self.func = Rosenbrock
        self.func_type = Rosenbrock_func_type
        self.variables_num = Rosenbrock_variables_num
        self.lower_bound = Rosenbrock_lower_bound
        self.upper_bound = Rosenbrock_upper_bound
        self.generations = 100
        self.init_step = 10
        self.eps = 1e-4
        self.vectors_num = 10
        self.init_pos = None
        self.complex_constraints = [constraints1,constraints2,constraints3]
        self.complex_constraints_method = complex_constraints_method.loop
        self.RandomWalk = RandomWalk(**self.__dict__)
    def test(self):
        start_time = time()
        self.RandomWalk.random_walk()
        print("random walk costs %.4f seconds!" %(time() - start_time))
        self.RandomWalk.save_plot()
        self.RandomWalk.show_result()
    if __name__ == '__main__':
    TestRandomWalk().test()

    运行结果:

    Finish 1 random walk!
    Finish 2 random walk!
    Finish 3 random walk!
    Finish 4 random walk!
    Finish 5 random walk!
    Finish 6 random walk!
    Finish 7 random walk!
    Finish 8 random walk!
    Finish 9 random walk!
    Finish 10 random walk!
    Finish 11 random walk!
    Finish 12 random walk!
    Finish 13 random walk!
    Finish 14 random walk!
    Finish 15 random walk!
    Finish 16 random walk!
    Finish 17 random walk!
    random walk costs 0.1543 seconds!
    -------------------- random walk config is: --------------------
    eps:0.0001
    func_type:max
    lower_bound:[-2.048 -2.048]
    upper_bound:[2.048 2.048]
    init_step:10
    vectors_num:10
    func:
    variables_num:2
    walk_nums:17
    complex_constraints_method:loop
    generations:100
    generations_nums:2191
    complex_constraints:[, , ]
    -------------------- random walk caculation result is: --------------------
    global best generation index/total generations:2091/2191
    global best point is: [-2.41416736  0.41430367]
    global best target is: 2942.6882849234585
    图7 Random Walk求解带有三个复杂约束的Rosenbrock函数

    可以发现Random Walk 求解得到的最优解要比GA好,而且运行时间更快,经过实验发现,在所有的优化方法中,不论是求解带复杂约束还是不带复杂约束条件的目标函数,求解效果大体上排序是:Random Walk > PSO > GA > SA 。所以当你在求解具体问题时,不妨多试几种优化方法,然后择优选择。

    7. 基于梯度的系列优化方法


        上面所述的各种优化方法,比如GA,PSO,SA等都是基于随机搜索的优化算法,其计算是不依赖于目标函数的具体形式,也不需要知道其梯度的,更加传统的优化算法是基于梯度的算法,比如经典的梯度下降(上升)法(Gradient Descent)以及其一系列变种。下面就简要介绍sopt中GD,Momentum,AdaGrad,RMSProp以及Adam的实现。关于这些基于梯度的优化算法的具体原理,可以参考我之前的一篇博文深度学习中常用的优化方法。另外需要注意,以下所述的基于梯度的各种优化算法,一般都是用在无约束优化问题里面的,如果是有约束的问题,请选择上面其他的优化算法。下面是GradientDescent的一个使用实例:

    from time import time
    from sopt.util.gradients_config import *
    from sopt.util.functions import *
    from sopt.Optimizers.Gradients import GradientDescent
    class TestGradientDescent:
    def __init__(self):
        self.func = quadratic50
        self.func_type = quadratic50_func_type
        self.variables_num = quadratic50_variables_num
        self.init_variables = None
        self.lr = 1e-3
        self.epochs = 5000
        self.GradientDescent = GradientDescent(**self.__dict__)
    def test(self):
        start_time = time()
        self.GradientDescent.run()
        print("Gradient Descent costs %.4f seconds!" %(time()-start_time))
        self.GradientDescent.save_plot()
        self.GradientDescent.show_result()
    if __name__ == '__main__':
    TestGradientDescent().test()

    运行结果为:

    Gradient Descent costs 14.3231 seconds!
    -------------------- Gradient Descent config is: --------------------
    func_type:min
    variables_num:50
    func:
    epochs:5000
    lr:0.001
    -------------------- Gradient Descent caculation result is: --------------------
    global best epoch/total epochs:4999/5000
    global best point: [ 0.9999524   1.99991045  2.99984898  3.9998496   4.99977767  5.9997246
      6.99967516  7.99964102  8.99958143  9.99951782 10.99947879 11.99944665
     12.99942492 13.99935192 14.99932708 15.99925856 16.99923686 17.99921689
     18.99911527 19.9991255  20.99908968 21.99899699 22.99899622 23.99887832
     24.99883597 25.99885616 26.99881394 27.99869772 28.99869349 29.9986766
     30.99861142 31.99851987 32.998556   33.99849351 34.99845985 35.99836731
     36.99832444 37.99831792 38.99821067 39.99816567 40.99814951 41.99808199
     42.99808161 43.99806655 44.99801207 45.99794449 46.99788003 47.99785468
     48.99780825 49.99771656]
    global best target: 1.0000867498727912
    图7 GradientDescent 求解quadratic50 下面简要说明以下GradientDescent,Momentum等类中的主要参数,像func,variables_num等含义已经解释很多次了,不再赘述,这里主要介绍各类特有的一些参数。
  1. GradientDescent类:
  • lr:学习率,默认是\(10^{-3}\);
  • epochs:迭代次数,默认是100;
  1. Momentum类:
  • lr:学习率,默认是\(10^{-3}\);
  • beta:Momentum的\(\beta\)参数,默认是0.9;
  • epochs:迭代次数,默认是100;
  1. AdaGrad类:
  • lr:学习率,默认是\(10^{-3}\);
  • eps:极小的一个正数(防止分母为0),默认为\(10^{-8}\);
  • epochs:迭代次数,默认是100;
  1. RMSProp类:
  • lr:学习率,默认是\(10^{-3}\);
  • beta:RMSProp的\(\beta\)参数,默认是0.9;
  • eps:极小的一个正数(防止分母为0),默认为\(10^{-8}\);
  • epochs:迭代次数,默认是100;
  1. Adam类:
  • lr:学习率,默认是\(10^{-3}\);
  • beta1:Adam的\(\beta_1\)参数,默认是0.5;
  • beta2:Adam的\(\beta_2\)参数,默认是0.9;
  • eps:极小的一个正数(防止分母为0),默认为\(10^{-8}\);
  • epochs:迭代次数,默认是100;
    这里额外说一句,Adam原文作者给出的最优超参数为\(\beta_1 = 0.9,\beta_2 = 0.999\),但是我在实际调试过程中发现,\(\beta_1 = 0.5,\beta_2 = 0.9\)才能达到比较好的效果,具体原因目前不是很清楚。如果想要学习各个优化函数类的具体用法,还可以使用from sopt.test import *导入预定义的示例测试,来运行观察结果。比如下面的代码就运行了PSO的一个示例测试:

    from sopt.test import test_PSO
    test_PSO.TestPSO().test()

    结果:

    PSO costs 3.4806 seconds!
    -------------------- PSO config is: --------------------
    lower_bound:[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
     1 1 1 1 1 1 1 1 1 1 1 1 1]
    generations:200
    vmin:-1
    func:
    w_method:linear_decrease
    func_type:min
    population_size:100
    w_start:0.9
    complex_constraints_method:loop
    vmax:1
    c2:1.49445
    upper_bound:[50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50
     50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50 50]
    variables_num:50
    c1:1.49445
    w:1
    complex_constraints:None
    w_end:0.4
    -------------------- PSO calculation result is: --------------------
    global best generation index/total generations: 199/200
    global best point: [  1.           1.           2.94484328   4.13875216   5.00293498
       6.13124759   6.99713025   7.92116383   8.87648843  10.02066994
      11.0758768   12.02240279  13.01125368  13.98010373  14.98063168
      15.97776149  17.11878537  18.00246112  18.14780887  20.00637617
      21.00223704  22.00689373  23.14823218  24.0002456   24.98672157
      25.99141686  27.02112321  28.01540506  29.05403155  30.07304888
      31.00414822  32.00982867  32.99444884  33.9114213   34.96631157
      36.22871824  37.0015616   37.98907918  39.01245751  40.1371835
      41.0182043   42.07768102  42.87178292  43.93687997  45.05786395
      46.03778693  47.07913415  50.          48.9964866   50.        ]
    global best target: 6.95906097685

    附录

        附录会简要说明上文提到的一些概念,待有空更新。。。

    To do ...

  1. GA的并行化计算
  2. 小生境GA
  3. GA,PSO等求解离散函数最优解
  4. GA,PSO等实际应用举例
  5. 其他的一些最优化方法,比如牛顿法,拟牛顿法等
  6. 其他
热爱编程,热爱机器学习! github:http://www.github.com/Lyrichu github blog:http://Lyrichu.github.io 个人博客站点:http://www.movieb2b.com(不再维护)

你可能感兴趣的:(python,数据结构与算法,c/c++)