目录

前言

多目标遗传算法

多目标的优化结果

Pareto曲线及分层处理

拥挤距离

代码实现

优化结果

最后的话


前言

在很多的工程实践问题中,往往是多输入多输出的。而且最有意思的事情在于:多个输出指标总是互相矛盾的。把其中一个提高了,另外一个就会受到影响,顾此失彼。基于这样一种应用需求,单目标的遗传算法很明显已经不能满足工程实践的要求了,所以需要开拓多目标的优化算法,多目标的遗传算法就是在这样的背景下,好吧,是我自己需要进行多目标的优化,跟大背景没有太大的关联。本篇以基础知识为主,最后会加入入门级别的代码展示,只不过这个代码是我最近刚学多目标的时候编写的,写得很繁琐冗余,只能说是把多目标优化的功能实现了,代码质量很低,之后我会对代码进行优化,便于大家学习讨论。阅读这篇文章的时候认为大家已经掌握的遗传算法的基本知识了,如果单目标算法还不清楚的,建议阅读我之前的博客。

Python—标准遗传算法求函数最大值代码实现

多目标遗传算法

我参考着这篇博客的讲解思路,把我对多目标的理解记录一下,大家如果要看关于概念准确的定义,还是建议移步推荐的这篇博客。

多目标的优化结果

首先,需要讨论的是结果的展示问题。在单目标中,由于只有一个目标,所以我们在展示结果的时候,往往是给出的目标值的收敛曲线(有些时候也展示适应度曲线,看个人偏好了)。但是多目标优化过程不是这样的,咱以两个目标为例,分别以两个目标的结果作为坐标,得到的优化结果是什么?是一个点对吧,这一个点对于我们来说很重要,但是,由于是多目标的问题,我们很多时候就会好奇,如果我将其中一个目标做一点让步,就是它不要这么好,那另一个目标会不会变得更好呢?我们想知道它们的变化关系,所以,其实在关心这么一个点的同时,我们还想了解一条线,两个目标都还不错,但又不是最好的那条线。借一张推荐的博客里面的图说明一下这个问题。

python规划求解库 python求解多目标规划_多目标

这个图展示的就是两个优化目标f1,f2的结果,如果我们想目标值越小越好,那就是坐标上的散点越靠近左下角越好,其实在优化的过程中就会发现,我们能去评判红色圈圈中的四个点到底谁好吗?得结合实际情况去取舍对吧?那算法它没人那么聪明,它只能进行筛选,如果在编写算法的时候,没有这种保留一条最优曲线的思路,那四个点,算法就只给你留一个,最终结果展示的时候,就会遗漏很多的信息了。这个也是为什么多目标优化和单目标优化结果有本质不同的原因。

Pareto曲线及分层处理

理解了结果的形式,这条曲线就顺理成章的产生了,简单地说,这条曲线就是最优解的散点连成的线,有些朋友喜欢叫最优解前沿,其实是一个意思。这里要引入分层这个概念。举个例子,比如规定的种群数量NP为50,上面那条最优解曲线上一共是7个点,那对应的7个个体认为是基因表现比较好的7个,但是迭代需要选择50个呀,7个是不够的,我们就把这7个划分为第一层,接着,把这7个剔除了,再去寻找靠近坐标左下角的散点连成第二条曲线,划分为第二层,进行迭代直到所有的散点都分层完毕了,这个过程也就结束了。之后进行选择判断的时候,就从第一层开始挑个体,一直补充到NP数量。

拥挤距离

这个概念其实是为曲线服务的,其实理解的最好方式是去跑一遍代码,以有没有拥挤距离这个条件为变量,就知道这家伙是干啥用的了。我先用语言跟大家解释一下。比如第一条Pareto曲线上有十个点,大家先想想我们是希望这十个点靠的近好呢,还是希望它们散开好呢?想想我们画曲线的目的,不就是为了看变化情况嘛,那你这十个点一集中,我还看啥变化情况,不如画一个点得了。就是出于这样朴实无华的目的,我们引入了拥挤距离这个概念。

一句话概括:拥挤距离越大越好。值越大说明这条曲线散得越开,就很符合我们的要求。具体是这样用的,咱还是以NP为50举例,比如优化已经到了后期了,你的第一层上的点有70个,但是你只要取50个呀,多了好苦恼呀,这个时候我们就要加入拥挤距离作为筛选条件,取前五十个拥挤距离大的点,至于后面20个,虽然也是在第一层的,但由于它们离得近,就不要它们了,就这么任性。

拥挤距离的具体定义和求解大家看这篇文章,人家说得比较详细,我就不再赘述了。

多目标优化拥挤距离计算

代码实现

代码稍微补充一下呀,其实Python现在已经用多目标优化的库DEAP了,不想自己动手的同学可以学学怎么调用,这里以学习多目标遗传算法为主,就自己编写的。大家根据自己情况灵活选择就ok。

求解的问题是这样的,同时优化以下两个函数的最小值:

python规划求解库 python求解多目标规划_遗传算法_02

python规划求解库 python求解多目标规划_python规划求解库_03

x取值范围是0到pi/2.

直接上代码了:

import numpy as np
import matplotlib.pyplot as plt
import math as mt
#import seaborn as sns
#sns.set_style('darkgrid')

def funclsin(x):
    y=(x-1)**2
    return y

def funclcos(x):
    y=mt.cos(x)
    return y
#%%
pi=mt.pi
NP=70   #初始化种群数
L=10    #二进制位串长度
Pc=0.8  #交叉率
Pm=0.2  #变异率
G=20   #最大遗传代数
Xs=pi/2   #上限
Xx=0    #下限
f=np.random.randint(0,high=2,size=(NP,L))   #生成随机初始种群
x=np.zeros((1,140))[0].tolist()
tempx=np.zeros((1,140))[0].tolist()
f1trace=[]         #记录第一个目标函数
f2trace=[]         #记录第二个目标函数

#%%遗传算法循环
for i in range(G):
    print(i)
    f1=[]
    f2=[]
    nf=f                   #出新的种群,在其基础上进行交叉、变异
    for M in range(0,NP,2):
        p=np.random.rand()     #交叉
        if p<Pc:
            q=np.random.randint(0,high=2,size=(1,L))[0].tolist()
            for j in range(L):
                if q[j]==1:
                    temp=nf[M+1][j]
                    nf[M+1][j]=nf[M][j]
                    nf[M][j]=temp
    j=1
    while j<=(NP*Pm):
        h=np.random.randint(1,high=NP)  #变异
        for k in range(round(L*Pm)):
            g=np.random.randint(1,high=L)
            nf[h][g]=(not nf[h][g])+0         #取反操作,python和matlab不一样
        j+=1
    #交叉变异结束之后,新一代nf加上前代f共2*NP个个体进行筛选
    newf=np.vstack((f,nf))         #垂直方向累加两个f
    for j in range(newf.shape[0]):
        U=newf[j]
        m=0
        for k in range(L):
            m=U[k]*(2**(k-1))+m    #将二进制解码为定义域范围内十进制
        x[j]=Xx+m*(Xs-Xx)/(2**(L-1))
        f1.append(funclsin(x[j]))
        f2.append(funclcos(x[j]))
    fs=[]
#    plt.scatter(f1,f2)
#    plt.savefig(r'C:\Users\王耀\Desktop\Genetic Algorithm\pic\exx'+str(i),bbox_inches = 'tight',pad_inches = 0,dpi =100)
#    plt.close()
    for j in range(len(f1)):
        fs.append(f1[j])
        fs.append(f2[j])
    fs=np.array(fs)
    fs=fs.reshape(len(f1),2)                #把求解得到的适应度函数转换为多目标散点的形式
    fs=fs.tolist()
    ps=np.zeros((len(f1),len(f1)))                    #i,j=1,i支配j,我们希望点的值越小越好
    for k in range(0,len(f1)):
        for j in range(0,len(f1)):
            if fs[k][0]<fs[j][0] and fs[k][1]<fs[j][1]:
                ps[k][j]=1                  #这一步是这样的,ps是一个比较矩阵,如果100个点互相比(包括自己)
                                            #如果被置1了,就说明被支配了,全部比完之后,纵向全为0,则说明没有点支配自己
    jishu=[]
    for k in range(len(ps)):
        aa=0
        for j in range(len(ps)):
            if ps[j][k]==1:
                aa+=1
        jishu.append(aa)
        
    sortjishu=np.sort(jishu)                #升序排序    
    index= np.argsort(jishu)                #升序对应索引
    aaa=list(set(sortjishu))
    front=[[] for k in range(len(set(sortjishu)))]
    for k in range(len(set(sortjishu))):
        for j in range(len(index)):
            if sortjishu[j]==aaa[k]:
                front[k].append(index[j])    #记录对应的索引位置
                
#只是依靠分层的话,很可能最后会优化到一个点上
#所以需要加入拥挤距离来刻画点的分布情况
    f=[]
    for j in range(len(front)):
        if len(f)==NP:
#            print(2)
            break
        tempf=[]
        for k in range(len(front[j])):
            tempf.append(newf[front[j][k]])  #把第J层的染色体添加进入tempf中
        tempf1=[]
        tempf2=[]      
        for M in range(len(tempf)):          #计算第j层染色体的函数值,存在temf1.2中
            U=tempf[M]
            m=0
            for k in range(L):
                m=U[k]*(2**(k-1))+m    #将二进制解码为定义域范围内十进制
            tempx[M]=Xx+m*(Xs-Xx)/(2**(L-1))
            tempf1.append(funclsin(tempx[M]))
            tempf2.append(funclcos(tempx[M]))
            
        #对f1计算其拥挤距离,此时与f2无关。
        tempf1index=np.argsort(tempf1)   #tempf1升序排序
        distance=np.zeros(len(tempf1))
        #将两端位置值置为正负无穷
        for k in range(len(tempf1)):
            if tempf1[k]==max(tempf1):
                distance[k]=float('inf')
            elif tempf1[k]==min(tempf1):
                distance[k]=float('inf')
        #计算每个点在f1下的拥挤距离
        for k in range(len(tempf1index)):
            if abs(distance[tempf1index[k]])<10:      #把极值点排除在外
                distance[tempf1index[k]]=(tempf1[tempf1index[k+1]]-tempf1[tempf1index[k-1]])/(max(tempf1)-min(tempf1))
            else:
                continue
            
        #对f2计算拥挤距离,与f1无关
        tempf1index1=np.argsort(tempf2)   #tempf2升序排序
        distance1=np.zeros(len(tempf1))
        #将两端位置值置为正负无穷
        for k in range(len(tempf2)):
            if tempf2[k]==max(tempf2):
                distance1[k]=float('inf')
            elif tempf2[k]==min(tempf2):
                distance1[k]=float('inf')
                #计算每个点在f2下的拥挤距离
        for k in range(len(tempf1index1)):
            if abs(distance1[tempf1index1[k]])<10:      #把极值点排除在外
                distance1[tempf1index1[k]]=(tempf2[tempf1index1[k+1]]-tempf2[tempf1index1[k-1]])/(max(tempf2)-min(tempf2))
            else:
                continue
            
        sumdis=[]                          #当收敛到一个点时,拥挤距离就失去意义了
        
        for k in range(len(distance)):
            sundistance=distance[k]+distance1[k]
            sumdis.append(sundistance)
        disindex=np.argsort(sumdis)[::-1]  #sumdis降序排序(拥挤距离越大越好)
        for k in range(len(sumdis)):
            f.append(tempf[disindex[k]])
            if len(f)==NP:
                break
            else:
                continue
        f1=[]
        f2=[]      
        for j in range(len(f)):
            U=f[j]
            m=0
            for k in range(L):
                m=U[k]*(2**(k-1))+m    #将二进制解码为定义域范围内十进制
            x[j]=Xx+m*(Xs-Xx)/(2**(L-1))
            f1.append(funclsin(x[j]))
            f2.append(funclcos(x[j]))       
    plt.scatter(f1,f2)
    plt.savefig(r'C:\Users\王耀\Desktop\Genetic Algorithm\pic\exx1'+str(i),bbox_inches = 'tight',pad_inches = 0,dpi =100)
    plt.close()

 简单说一下需要注意的地方:

for j in range(len(f1)):
        fs.append(f1[j])
        fs.append(f2[j])
    fs=np.array(fs)
    fs=fs.reshape(len(f1),2)                #把求解得到的适应度函数转换为多目标散点的形式
    fs=fs.tolist()
    ps=np.zeros((len(f1),len(f1)))                    #i,j=1,i支配j,我们希望点的值越小越好
    for k in range(0,len(f1)):
        for j in range(0,len(f1)):
            if fs[k][0]<fs[j][0] and fs[k][1]<fs[j][1]:
                ps[k][j]=1                  #这一步是这样的,ps是一个比较矩阵,如果100个点互相比(包括自己)
                                            #如果被置1了,就说明被支配了,全部比完之后,纵向全为0,则说明没有点支配自己

这里的ps是一个比较矩阵(用二维列表写的),它的作用是对每个点进行比较,如果是在比较之后,处于被支配的点(比如在右上角的)我们就把它置1,这样在获得ps之后,统计每个个体1的数目,1越少,个体就越优秀。

f=[]
    for j in range(len(front)):
        if len(f)==NP:
#            print(2)
            break
        tempf=[]
        for k in range(len(front[j])):
            tempf.append(newf[front[j][k]])  #把第J层的染色体添加进入tempf中
        tempf1=[]
        tempf2=[]      
        for M in range(len(tempf)):          #计算第j层染色体的函数值,存在temf1.2中
            U=tempf[M]
            m=0
            for k in range(L):
                m=U[k]*(2**(k-1))+m    #将二进制解码为定义域范围内十进制
            tempx[M]=Xx+m*(Xs-Xx)/(2**(L-1))
            tempf1.append(funclsin(tempx[M]))
            tempf2.append(funclcos(tempx[M]))
            
        #对f1计算其拥挤距离,此时与f2无关。
        tempf1index=np.argsort(tempf1)   #tempf1升序排序
        distance=np.zeros(len(tempf1))
        #将两端位置值置为正负无穷
        for k in range(len(tempf1)):
            if tempf1[k]==max(tempf1):
                distance[k]=float('inf')
            elif tempf1[k]==min(tempf1):
                distance[k]=float('inf')
        #计算每个点在f1下的拥挤距离
        for k in range(len(tempf1index)):
            if abs(distance[tempf1index[k]])<10:      #把极值点排除在外
                distance[tempf1index[k]]=(tempf1[tempf1index[k+1]]-tempf1[tempf1index[k-1]])/(max(tempf1)-min(tempf1))
            else:
                continue
            
        #对f2计算拥挤距离,与f1无关
        tempf1index1=np.argsort(tempf2)   #tempf2升序排序
        distance1=np.zeros(len(tempf1))
        #将两端位置值置为正负无穷
        for k in range(len(tempf2)):
            if tempf2[k]==max(tempf2):
                distance1[k]=float('inf')
            elif tempf2[k]==min(tempf2):
                distance1[k]=float('inf')
                #计算每个点在f2下的拥挤距离
        for k in range(len(tempf1index1)):
            if abs(distance1[tempf1index1[k]])<10:      #把极值点排除在外
                distance1[tempf1index1[k]]=(tempf2[tempf1index1[k+1]]-tempf2[tempf1index1[k-1]])/(max(tempf2)-min(tempf2))
            else:
                continue
            
        sumdis=[]                          #当收敛到一个点时,拥挤距离就失去意义了

这一段代码是计算拥挤距离用的,拥挤距离是针对两个目标函数分别计算,最后再把距离相加进行比较。

优化结果

结果比较简单,因为咱这选的函数比较简单,主要是学习为主嘛。

下面是第一代的种群两个目标函数值的关系图:(迭代一次了)

python规划求解库 python求解多目标规划_python_04

 之后是迭代15次的结果:

python规划求解库 python求解多目标规划_遗传算法_05

 收敛速度和计算速度都是相当快的,大家可以更换测试函数再进行测验。

最后的话

在代码调试的过程中,有时会出现收敛到单点的情况(迭代次数大了之后),我不知道这个是不是跟Python的初始种群随机生成的机制有关系,了解的朋友可以私信我讨论一下。谢谢大家。