旅行销售员的演化:基于Python实现遗传算法

2018 年 8 月 5 日 论智
来源:Medium
编译:weakish

编者按:麦肯锡顾问Eric Stoltz以旅行推销员问题为例,简明地介绍了遗传算法的原理及Python实现。

借鉴自然选择的灵感,遗传算法(GA)是一种迷人的求解搜索和优化问题方法。尽管已经有很多关于GA的文章,这些文章很少演示如何一步一步地使用Python实现GA算法,解决一个比较复杂的问题。所以我写了这篇教程!读完这篇教程后,你将对如何从头实现GA算法有一个完整的了解。


导言

问题

这篇教程将使用GA求解旅行推销员问题(TSP):

给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短路由。

TSP示意图(作者:Xypron;许可:公有领域)

记住两条重要的规则:

  1. 每座城市仅访问一次(不重不漏)

  2. 必须返回起始城市

方法

让我们从一些定义开始:

  • 基因(gene): 一座城市(以(x, y)坐标表示)

  • 个体(individual)或染色体(chromosome): 满足以上条件的一个路由

  • 种群(population): 一组可能的路由(即,一组个体)

  • 父母(parents): 两个路由相组合,创建一个新路由

  • 交配池(mating pool): 一组父母,用来创建下一代种群(即创建下一代路由)

  • 适应度(fitness): 一个告诉我们每个路由有多好的函数(在我们的例子中,也就是距离多短)

  • 突变(mutation): 在种群中引入变化的方法,随机交换路由中的两个城市

  • 精英选择(Elitism): 将最好的个体保留至下一代

GA算法的进行过程如下:

  1. 创建种群

  2. 确定适应度

  3. 选择交配池

  4. 繁殖

  5. 变异

  6. 重复

现在,让我们来动手实现。

创建我们的遗传算法

导入一些机器学习的标准包:

  
  
    
  1. import numpy as np

  2. import random

  3. import operator

  4. import pandas as pd

  5. import matplotlib.pyplot as plt

创建城市和适应度类

我们首先创建City(城市)类,城市不过是(x, y)坐标。City类中包含distance函数(根据勾股定理计算一对城市间的距离)。

  
  
    
  1. class City:

  2.    def __init__(self, x, y):

  3.        self.x = x

  4.        self.y = y

  5.    def distance(self, city):

  6.        xDis = abs(self.x - city.x)

  7.        yDis = abs(self.y - city.y)

  8.        distance = np.sqrt((xDis ** 2) + (yDis ** 2))

  9.        return distance

  10.    def __repr__(self):

  11.        return "(" + str(self.x) + "," + str(self.y) + ")"

在Fitness(适应度)类中,我们将适应度(routeFitness)定义为路由距离(routeDistance)的倒数。我们想要最小化路由距离,因此较大的适应度较优。注意routeDistance的定义考虑到了我们需要回到起点。

  
  
    
  1. class Fitness:

  2.    def __init__(self, route):

  3.        self.route = route

  4.        self.distance = 0

  5.        self.fitness= 0.0

  6.    def routeDistance(self):

  7.        if self.distance ==0:

  8.            pathDistance = 0

  9.            for i in range(0, len(self.route)):

  10.                fromCity = self.route[i]

  11.                toCity = None

  12.                if i + 1 < len(self.route):

  13.                    toCity = self.route[i + 1]

  14.                else:

  15.                    toCity = self.route[0]

  16.                pathDistance += fromCity.distance(toCity)

  17.            self.distance = pathDistance

  18.        return self.distance

  19.    def routeFitness(self):

  20.        if self.fitness == 0:

  21.            self.fitness = 1 / float(self.routeDistance())

  22.        return self.fitness

创建种群

现在我们将生产初始种群(第一代)。我们需要一个能够生成满足条件的路由的函数(我们将在教程末尾创建城市列表)。我们通过随机选择访问城市的顺序创建一个个体:

  
  
    
  1. def createRoute(cityList):

  2.    route = random.sample(cityList, len(cityList))

  3.    return route

重复运行createRoute函数,直到我们有了足够的个体:

  
  
    
  1. def initialPopulation(popSize, cityList):

  2.    population = []

  3.    for i in range(0, popSize):

  4.        population.append(createRoute(cityList))

  5.    return population

确定适应度

接下来到了有意思的演化部分。为了模拟“适者生存”,我们利用Fitness排序种群中的每个个体。我们的输出是一个有序列表,包括路由ID和相应的适应度分数。

  
  
    
  1. def rankRoutes(population):

  2.    fitnessResults = {}

  3.    for i in range(0,len(population)):

  4.        fitnessResults[i] = Fitness(population[i]).routeFitness()

  5.    return sorted(fitnessResults.items(), key = operator.itemgetter(1), reverse = True)

选择交配池

选择用来产生下一代的父母有若干种方法。其中最常见的两种是适应度比例选择(fitness proportionate selection)锦标赛选择(tournament selection)。适应度比例选择又称为轮盘赌选择(roulette wheel selection)。

  • 适应度比例选择 (下面实现的版本):基于每个个体相对于种群的适应度分配选中的概率。可以看成适应度加权选择概率。

  • 锦标赛选择:从种群中随机选择一些个体,其中适应度最高的被选为父母的一方。重复这一过程选出父母的另一方。

另外也可以考虑精英选择。种群中表现最优的个体自动进入下一代,确保留存最成功的个体。

为清晰计,我们将分两步(selectionmatingPool)创建交配池。在selection函数中,我们将根据rankRoutes的输出判定选择哪些路由。我们为每个个体计算相对适应度权重(创建轮盘),然后将这些权重与一个随机数比较,以选择交配池。同时,我们打算保留最佳的路由,因此引入了精英选择。最后,selection函数返回路由ID的列表,供matingPool函数使用。

  
  
    
  1. def selection(popRanked, eliteSize):

  2.    selectionResults = []

  3.    df = pd.DataFrame(np.array(popRanked), columns=["Index","Fitness"])

  4.    df['cum_sum'] = df.Fitness.cumsum()

  5.    df['cum_perc'] = 100*df.cum_sum/df.Fitness.sum()

  6.    for i in range(0, eliteSize):

  7.        selectionResults.append(popRanked[i][0])

  8.    for i in range(0, len(popRanked) - eliteSize):

  9.        pick = 100*random.random()

  10.        for i in range(0, len(popRanked)):

  11.            if pick <= df.iat[i,3]:

  12.                selectionResults.append(popRanked[i][0])

  13.                break

  14.    return selectionResults

有了selection提供的路由ID,我们可以创建交配池了。

  
  
    
  1. def matingPool(population, selectionResults):

  2.    matingpool = []

  3.    for i in range(0, len(selectionResults)):

  4.        index = selectionResults[i]

  5.        matingpool.append(population[index])

  6.    return matingpool

繁殖

创建好了交配池后,我们可以通过交叉(crossover)过程生产下一代。例如,假设个体是由0和1组成的字符串,我们可以直接选定一个交叉点,铰接两个字符串以产生后代。

然而,TSP要求每个位置出现且仅出现一次。为了遵循这条规则,我们将使用一种特殊的繁殖函数有序交叉(ordered crossover)。在有序交叉中,我们随机选择父字符串的一个子集,然后用母字符串中的基因填充路由的剩余空位。填充时,按照基因在母字符串中出现的次序依次填充,跳过选中的父字符串子集中已有的基因。

有序交叉示意图(来源:Lee Jacobson)

  
  
    
  1. def breed(parent1, parent2):

  2.    child = []

  3.    childP1 = []

  4.    childP2 = []

  5.    geneA = int(random.random() * len(parent1))

  6.    geneB = int(random.random() * len(parent1))

  7.    startGene = min(geneA, geneB)

  8.    endGene = max(geneA, geneB)

  9.    for i in range(startGene, endGene):

  10.        childP1.append(parent1[i])

  11.    childP2 = [item for item in parent2 if item not in childP1]

  12.    child = childP1 + childP2

  13.    return child

在整个交配池上进行交叉操作(注意我们附加了精英选择):

  
  
    
  1. def breedPopulation(matingpool, eliteSize):

  2.    children = []

  3.    length = len(matingpool) - eliteSize

  4.    pool = random.sample(matingpool, len(matingpool))

  5.    for i in range(0,eliteSize):

  6.        children.append(matingpool[i])

  7.    for i in range(0, length):

  8.        child = breed(pool[i], pool[len(matingpool)-i-1])

  9.        children.append(child)

  10.    return children

变异

变异在GA中起到了重要作用,它通过引入新路由让我们得以探索解答空间的其他部分,避免收敛到局部最优值。和交叉类似,TSP问题中的变异需要特殊考虑。同样,如果我们的染色体由0和1组成,变异不过是给基因分配一个较低的由0变为1(或由1变0)的概率。

然而,因为我们需要遵守TSP的规则,我们无法丢弃城市。我们将转而使用交换变异(swap mutation)。这意味着,我们指定一个较低的概率,两座城市会在我们的路由中交换位置。

  
  
    
  1. def mutate(individual, mutationRate):

  2.    for swapped in range(len(individual)):

  3.        if(random.random() < mutationRate):

  4.            swapWith = int(random.random() * len(individual))

  5.            city1 = individual[swapped]

  6.            city2 = individual[swapWith]

  7.            individual[swapped] = city2

  8.            individual[swapWith] = city1

  9.    return individual

在整个种群上应用mutate函数:

  
  
    
  1. def mutatePopulation(population, mutationRate):

  2.    mutatedPop = []

  3.    for ind in range(0, len(population)):

  4.        mutatedInd = mutate(population[ind], mutationRate)

  5.        mutatedPop.append(mutatedInd)

  6.    return mutatedPop


重复

基本上差不多了。现在让我们把以上部分整合起来,创建一个产生下一代的函数。首先,我们计算当前种群的适应度。接着,我们选择潜在的父母,以创建交配池。最后,我们生产(交叉、变异)下一代。

  
  
    
  1. def nextGeneration(currentGen, eliteSize, mutationRate):

  2.    popRanked = rankRoutes(currentGen)

  3.    selectionResults = selection(popRanked, eliteSize)

  4.    matingpool = matingPool(currentGen, selectionResults)

  5.    children = breedPopulation(matingpool, eliteSize)

  6.    nextGeneration = mutatePopulation(children, mutationRate)

  7.    return nextGeneration


演化

我们已经具备了GA所需的一切!现在我们只需创建一个初始种群,然后就可以不断演化了。按照我们设定的代数演化后,将返回最佳路由。另外,我们将打印初始的路由距离,最终的路由距离,来看看我们提升了多少。

  
  
    
  1. def geneticAlgorithm(population, popSize, eliteSize, mutationRate, generations):

  2.    pop = initialPopulation(popSize, population)

  3.    print("Initial distance: " + str(1 / rankRoutes(pop)[0][1]))

  4.    for i in range(0, generations):

  5.        pop = nextGeneration(pop, eliteSize, mutationRate)

  6.    print("Final distance: " + str(1 / rankRoutes(pop)[0][1]))

  7.    bestRouteIndex = rankRoutes(pop)[0][0]

  8.    bestRoute = pop[bestRouteIndex]

  9.    return bestRoute


运行演化算法

万事俱备,只欠东风。我们现在只需要创建经过的城市列表。这里,我们将随机创建25座城市来演示GA算法:

  
  
    
  1. cityList = []

  2. for i in range(0,25):

  3.    cityList.append(City(x=int(random.random() * 200), y=int(random.random() * 200)))

只需一行代码即可运行演化算法。接下来艺术和科学的交汇之处:判断哪些假定效果最好。这里,我们将选择如下参数:每代100个个体,保留20个精英个体,给定基因的变异率为1%,运行500代:

  
  
    
  1. geneticAlgorithm(population=cityList, popSize=100, eliteSize=20, mutationRate=0.01, generations=500)


附加题: 绘制趋势

我们希望能查看距离随时间缩短的趋势,因此对geneticAlgorithm函数进行了一项小小的改动,保存每代的最短距离,并据此绘制图形。

  
  
    
  1. def geneticAlgorithmPlot(population, popSize, eliteSize, mutationRate, generations):

  2.    pop = initialPopulation(popSize, population)

  3.    progress = []

  4.    progress.append(1 / rankRoutes(pop)[0][1])

  5.    for i in range(0, generations):

  6.        pop = nextGeneration(pop, eliteSize, mutationRate)

  7.        progress.append(1 / rankRoutes(pop)[0][1])

  8.    plt.plot(progress)

  9.    plt.ylabel('Distance')

  10.    plt.xlabel('Generation')

  11.    plt.show()

结语

我希望这样亲手学习如何创建自己的GA算法是一个有意思的过程。请自行尝试,看看你可以得到多短的路由。或者进一步尝试在其他问题上实现GA;想象你将如何修改bredmutate函数来处理其他类型的染色体。我们这里只涉及一些皮毛!

本文配套的Jupyter Notebook见GitHub:ezstoltz/genetic-algorithm/geneticalgorithmTSP.ipynb

参考链接

  • http://www.theprojectspot.com/tutorial-post/applying-a-genetic-algorithm-to-the-travelling-salesman-problem/5

  • https://gist.github.com/turbofart/3428880

  • https://gist.github.com/NicolleLouis/d4f88d5bd566298d4279bcb69934f51d

  • https://en.wikipedia.org/wiki/Travelling_salesman_problem

原文地址:https://towardsdatascience.com/evolution-of-a-salesman-a-complete-genetic-algorithm-tutorial-for-python-6fe5d2b3ca35

登录查看更多
0

相关内容

最新《自动微分手册》77页pdf
专知会员服务
97+阅读 · 2020年6月6日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
机器学习速查手册,135页pdf
专知会员服务
336+阅读 · 2020年3月15日
算法与数据结构Python,369页pdf
专知会员服务
160+阅读 · 2020年3月4日
【新书】Python数据科学食谱(Python Data Science Cookbook)
专知会员服务
113+阅读 · 2020年1月1日
5大必知的图算法,附Python代码实现
AI100
4+阅读 · 2019年9月10日
《常用算法之智能计算 (四) 》:遗传算法
数盟
4+阅读 · 2018年12月21日
深度学习线性代数简明教程
论智
11+阅读 · 2018年5月30日
实战 | 用Python做图像处理(三)
七月在线实验室
15+阅读 · 2018年5月29日
基于Numpy实现神经网络:反向传播
论智
5+阅读 · 2018年3月21日
动手写机器学习算法:SVM支持向量机(附代码)
七月在线实验室
12+阅读 · 2017年12月5日
教程 | 基于遗传算法的拼图游戏解决方案
机器之心
109+阅读 · 2017年11月12日
GAFT:一个使用 Python 实现的遗传算法框架
Python开发者
10+阅读 · 2017年8月1日
Arxiv
3+阅读 · 2018年1月31日
Arxiv
4+阅读 · 2018年1月15日
Arxiv
3+阅读 · 2018年1月10日
Arxiv
3+阅读 · 2015年5月16日
VIP会员
相关VIP内容
最新《自动微分手册》77页pdf
专知会员服务
97+阅读 · 2020年6月6日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
机器学习速查手册,135页pdf
专知会员服务
336+阅读 · 2020年3月15日
算法与数据结构Python,369页pdf
专知会员服务
160+阅读 · 2020年3月4日
【新书】Python数据科学食谱(Python Data Science Cookbook)
专知会员服务
113+阅读 · 2020年1月1日
相关资讯
5大必知的图算法,附Python代码实现
AI100
4+阅读 · 2019年9月10日
《常用算法之智能计算 (四) 》:遗传算法
数盟
4+阅读 · 2018年12月21日
深度学习线性代数简明教程
论智
11+阅读 · 2018年5月30日
实战 | 用Python做图像处理(三)
七月在线实验室
15+阅读 · 2018年5月29日
基于Numpy实现神经网络:反向传播
论智
5+阅读 · 2018年3月21日
动手写机器学习算法:SVM支持向量机(附代码)
七月在线实验室
12+阅读 · 2017年12月5日
教程 | 基于遗传算法的拼图游戏解决方案
机器之心
109+阅读 · 2017年11月12日
GAFT:一个使用 Python 实现的遗传算法框架
Python开发者
10+阅读 · 2017年8月1日
Top
微信扫码咨询专知VIP会员