从自然选择中汲取灵感,遗传算法 (GA) 是一种解决搜索和优化问题的迷人方法。虽然关于 GA 的文章很多(请参阅:此处和此处),但很少有人展示如何在 Python 中逐步实现 GA 以解决更复杂的问题。这就是本教程的用武之地!继续学习,到最后,您将完全了解如何从头开始部署 GA。
介绍
问题
在本教程中,我们将使用 GA 寻找旅行商问题 (TSP) 的解决方案。TSP描述如下:“给定一个城市列表和每对城市之间的距离,访问每个城市并返回起始城市的最短路线是什么?”
鉴于此,需要牢记两个重要规则:
- 每个城市只需要访问一次
- 我们必须返回出发城市,所以我们的总距离需要相应计算
该方法
让我们从一些定义开始,在 TSP 的上下文中重新表述:
- 基因:一个城市(表示为(x,y)坐标)
- 个体(又名“染色体”):满足上述条件的单一路线
- 人口:可能路线的集合(即个体的集合)
- Parents:两条路由合并创建一条新路由
- 交配池:用于创建我们的下一代种群(从而创建下一代路线)的父母的集合
- Fitness:一个函数,告诉我们每条路线有多好(在我们的例子中,距离有多短)
- 突变:一种通过随机交换路线中的两个城市来引入人口变化的方法
- 精英主义:一种将最优秀的人带入下一代的方式
我们的 GA 将按以下步骤进行:
1.创建人口
2.确定健身
3.选择交配池
4.品种
5.变异
6.重复
现在,让我们看看实际效果。
构建我们的遗传算法
虽然我们的 GA 的每个部分都是从头开始构建的,但我们将使用一些标准包来简化事情:
将 numpy 导入为 np、随机、运算符、将 pandas 导入为 pd、将 matplotlib.pyplot 导入为 plt
创建两个类:City 和 Fitness
我们首先创建一个City
类,允许我们创建和处理我们的城市。这些只是我们的 (x, y) 坐标。在 City 类中,我们distance
在第 6 行添加了一个计算(利用毕达哥拉斯定理),并__repr__
在第 12 行添加了一种将城市输出为坐标的更简洁的方法。
class City:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, city):
xDis = abs(self.x - city.x)
yDis = abs(self.y - city.y)
distance = np.sqrt((xDis ** 2) + (yDis ** 2))
return distance
def __repr__(self):
return "(" + str(self.x) + "," + str(self.y) + ")"
我们还将创建一个Fitness
类。在我们的例子中,我们将适应度视为路线距离的倒数。我们希望最小化路线距离,因此适应度分数越大越好。根据规则 #2,我们需要在同一个地方开始和结束,所以这个额外的计算在距离计算的第 13 行中考虑到了。
class Fitness:
def __init__(self, route):
self.route = route
self.distance = 0
self.fitness= 0.0
def routeDistance(self):
if self.distance ==0:
pathDistance = 0
for i in range(0, len(self.route)):
fromCity = self.route[i]
toCity = None
if i + 1 < len(self.route):
toCity = self.route[i + 1]
else:
toCity = self.route[0]
pathDistance += fromCity.distance(toCity)
self.distance = pathDistance
return self.distance
def routeFitness(self):
if self.fitness == 0:
self.fitness = 1 / float(self.routeDistance())
return self.fitness
创造人口
我们现在可以制作我们的初始种群(又名第一代)。为此,我们需要一种方法来创建一个函数来生成满足我们条件的路线(注意:我们将在本教程结束时实际运行 GA 时创建我们的城市列表)。为了创建个人,我们随机选择访问每个城市的顺序:
def createRoute(cityList):
route = random.sample(cityList, len(cityList))
return route
这产生了一个个体,但我们想要一个完整的种群,所以让我们在下一个函数中这样做。这就像循环遍历createRoute
函数一样简单,直到我们为我们的人口拥有尽可能多的路线。
def initialPopulation(popSize, cityList):
population = []
for i in range(0, popSize):
population.append(createRoute(cityList))
return population
注意:我们只需要使用这些函数来创建初始种群。后代将通过育种和突变产生。
确定健身
接下来,进化的乐趣开始了。为了模拟我们的“适者生存”,我们可以利用Fitness
对种群中的每个个体进行排名。我们的输出将是一个有序列表,其中包含路线 ID 和每个相关的健身分数。
def rankRoutes(population):
fitnessResults = {}
for i in range(0,len(population)):
fitnessResults[i] = Fitness(population[i]).routeFitness()
return sorted(fitnessResults.items(), key = operator.itemgetter(1), reverse = True)
选择交配池
关于如何选择将用于创建下一代的父母,有几个选项。最常见的方法是适应度比例选择(又名“轮盘赌选择”)或锦标赛选择:
- 适应度比例选择(下面实现的版本):每个个体相对于种群的适应度用于分配选择概率。将其视为被选中的适应度加权概率。
- 锦标赛选择:从种群中随机选择一定数量的个体,并选择群体中适应度最高的个体作为第一个父代。重复此操作以选择第二个父母。
另一个需要考虑的设计特征是精英主义的使用。通过精英主义,人口中表现最好的个人将自动传给下一代,从而确保最成功的个人能够持续存在。
为了清楚起见,我们将分两步创建交配池。首先,我们将使用 的输出rankRoutes
来确定在我们的函数中选择哪些路由selection
。在第 3-5 行中,我们通过计算每个个体的相对适应度权重来设置轮盘赌。在第 9 行,我们将随机抽取的数字与这些权重进行比较,以选择我们的交配池。我们还想保留我们的最佳路线,因此我们在第 7 行引入了精英主义。最终,该selection
函数返回路线 ID 列表,我们可以使用它在函数中创建交配池matingPool
。
def selection(popRanked, eliteSize):
selectionResults = []
df = pd.DataFrame(np.array(popRanked), columns=["Index","Fitness"])
df['cum_sum'] = df.Fitness.cumsum()
df['cum_perc'] = 100*df.cum_sum/df.Fitness.sum()
for i in range(0, eliteSize):
selectionResults.append(popRanked[i][0])
for i in range(0, len(popRanked) - eliteSize):
pick = 100*random.random()
for i in range(0, len(popRanked)):
if pick <= df.iat[i,3]:
selectionResults.append(popRanked[i][0])
break
return selectionResults
现在我们已经从函数中获得了构成交配池的路由 ID selection
,我们可以创建交配池了。我们只是从我们的人口中提取选定的个人。
品种
创建交配池后,我们可以在称为交叉(又名“育种”)的过程中创建下一代。如果我们的个人是 0 和 1 的串,并且我们的两个规则不适用(例如,假设我们正在决定是否在投资组合中包括一只股票),我们可以简单地选择一个交叉点并将两个串拼接在一起以产生后代。
然而,TSP 的独特之处在于我们需要恰好一次包含所有位置。为了遵守这个规则,我们可以使用一种特殊的育种函数,称为有序交叉。在有序交叉中,我们随机选择第一个父字符串的一个子集(见breed
下面函数中的第 12 行),然后用第二个父字符串的基因按照它们出现的顺序填充路径的剩余部分,而不复制任何基因从第一个父级中选择的子集(参见breed
下面函数中的第 15 行)。
接下来,我们将对此进行概括以创建我们的后代种群。在第 5 行中,我们使用精英主义来保留当前种群中的最佳路线。然后,在第 8 行,我们使用该breed
函数来填充下一代的剩余部分。
def breedPopulation(matingpool, eliteSize):
children = []
length = len(matingpool) - eliteSize
pool = random.sample(matingpool, len(matingpool))
for i in range(0,eliteSize):
children.append(matingpool[i])
for i in range(0, length):
child = breed(pool[i], pool[len(matingpool)-i-1])
children.append(child)
return children
变异
变异在 GA 中起着重要的作用,因为它通过引入新的路线来帮助我们避免局部收敛,这将使我们能够探索解决方案空间的其他部分。与交叉类似,TSP 在涉及突变时有特殊考虑。同样,如果我们有一条 0 和 1 的染色体,突变将简单地意味着分配一个基因从 0 变为 1 的低概率,反之亦然(继续之前的例子,后代投资组合中包含的股票是现在排除)。
但是,既然我们需要遵守我们的规则,我们就不能丢掉城市。相反,我们将使用交换突变。这意味着,以指定的低概率,两个城市将在我们的路线中交换位置。我们将为我们的mutate
功能中的一个人执行此操作:
def mutate(individual, mutationRate):
for swapped in range(len(individual)):
if(random.random() < mutationRate):
swapWith = int(random.random() * len(individual))
city1 = individual[swapped]
city2 = individual[swapWith]
individual[swapped] = city2
individual[swapWith] = city1
return individual
接下来,我们可以扩展mutate
函数来运行新的种群。
def mutatePopulation(population, mutationRate):
mutatedPop = []
for ind in range(0, len(population)):
mutatedInd = mutate(population[ind], mutationRate)
mutatedPop.append(mutatedInd)
return mutatedPop
重复
我们快到了。让我们将这些部分组合在一起,创建一个产生新一代的函数。首先,我们使用 对当前一代中的路线进行排名rankRoutes
。然后我们通过运行该函数来确定我们的潜在父母selection
,这允许我们使用该函数创建交配池matingPool
。最后,我们使用该函数创建新一代breedPopulation
,然后使用该mutatePopulation
函数应用变异。
def nextGeneration(currentGen, eliteSize, mutationRate):
popRanked = rankRoutes(currentGen)
selectionResults = selection(popRanked, eliteSize)
matingpool = matingPool(currentGen, selectionResults)
children = breedPopulation(matingpool, eliteSize)
nextGeneration = mutatePopulation(children, mutationRate)
return nextGeneration
运动中的进化
我们终于准备好了创建我们的 GA 的所有部分!我们需要做的就是创建初始种群,然后我们可以循环遍历任意多代。当然我们也想看看最佳路线以及我们改进了多少,所以我们在第 3 行捕获初始距离(记住,距离是适应度的倒数),在第 8 行捕获最终距离,以及最佳路线在第 9 行。
def geneticAlgorithm(population, popSize, eliteSize, mutationRate, generations):
pop = initialPopulation(popSize, population)
print("Initial distance: " + str(1 / rankRoutes(pop)[0][1]))
for i in range(0, generations):
pop = nextGeneration(pop, eliteSize, mutationRate)
print("Final distance: " + str(1 / rankRoutes(pop)[0][1]))
bestRouteIndex = rankRoutes(pop)[0][0]
bestRoute = pop[bestRouteIndex]
return bestRoute
运行遗传算法
一切就绪后,只需两步即可轻松解决 TSP:
首先,我们需要一个城市列表。对于这个演示,我们将创建一个包含 25 个随机城市的列表(看似数量很少的城市,但蛮力必须测试超过 300 条 sextillion 路线!):
cityList = []
for i in range(0,25):
cityList.append(City(x=int(random.random() * 200), y=int(random.random() * 200)))
然后,运行遗传算法是一行简单的代码。这就是艺术与科学相遇的地方;您应该了解哪些假设最适合您。在这个例子中,我们每一代有100个个体,保留20个精英个体,给定基因使用1%的突变率,跑500代:
geneticAlgorithm(population=cityList, popSize=100, eliteSize=20, mutationRate=0.01, generations=500)
额外功能:绘制改进图
很高兴知道我们的起点和终点距离以及建议的路线,但如果我们不了解我们的距离是如何随着时间的推移而改善的,那就太失职了。通过对我们的函数进行简单的调整geneticAlgorithm
,我们可以将每一代的最短距离存储在progress
列表中,然后绘制结果。
def geneticAlgorithmPlot(population, popSize, eliteSize, mutationRate, generations):
pop = initialPopulation(popSize, population)
progress = []
progress.append(1 / rankRoutes(pop)[0][1])
for i in range(0, generations):
pop = nextGeneration(pop, eliteSize, mutationRate)
progress.append(1 / rankRoutes(pop)[0][1])
plt.plot(progress)
plt.ylabel('Distance')
plt.xlabel('Generation')
plt.show()
以与以前相同的方式运行 GA,但现在使用新创建的geneticAlgorithmPlot
函数:
geneticAlgorithmPlot(population=cityList, popSize=100, eliteSize=20, mutationRate=0.01, generations=500)
结论
我希望这是学习如何构建自己的 GA 的一种有趣的实践方式。亲自尝试一下,看看您能获得多短的路线。或者更进一步,尝试在另一个问题集上实施 GA;了解如何更改breed
和mutate
函数以处理其他类型的染色体。我们只是在这里触及表面!