参考书籍:白话强化学习与pytorch 高杨
遗传算法
遗传算法的关键步骤:
- 基因编码
- 设计初始群体
- 计算适应度
- 产生下一代
背包问题
背包问题是一种组合优化的NP问题。问题为:有N件物品和一个容量为V的背包,第i件物品的质量是w[i],价值是v(i),求将哪些物品装入背包满足不超过背包容量且可使得总价值最大化?
这种问题一般验证一种猜想的解比算出问题的一组解简单的多。
看如下具体的问题,假设有一个背包,总容量为80kg,此外,有6件物品可以放入背包,参数如下表。
最简单的思路是用穷举法,即把每种情况都列出来,每种物体都有两种状态:存在(1)和不存在(0),共有2^(6)个组合,把每种组合的质量和价值求出来,超过80kg的不要,再在剩下的组合找到最大价值对应的组合即为最优解。但是次方法只能针对物品数量少的时候可用,如果物品数量多达128个,那么就对应3.4*10的38次方,如果一个一个穷举速度太慢。这时遗传算法的优势就体现出了。
求解方法
先看看上述6个物品对应的遗传算法流程:
1.基因编码
总共6个物品,每个物品的有无都可作为独立的一个基因片段,0对应不存在,1对应存在。
上图染色体对应的是物品2,3,6存在。
2.设置初始种群
随机设置4个初始生物个体,用染色体表示它们。
- 100100:对应物品1,4存在
- 101010:对应物品1,3,5存在
- 010101:对应物品2,4,6存在
- 101011:对应物品1,3,5,6存在
3.适应度计算
本例中用物品总价值来作为适应度函数,那么4个初始个体的适应度可计算如下:
-
100100:15+45=60
-
101010:15+35+55=105
-
010101:25+45+70=140
-
101011:15+35+55+70=175
同时,还要计算总质量: -
100100:15+25=35
-
101010:10+20+30=60
-
010101:15+25+35=75
-
101011:10+20+30+35=95
-
那么染色体101011直接淘汰,剩下3个染色体,它们会经历一个筛选的过程,更强大的染色体会有更多的机会生存,这里用各自价值占总价值的比率来进行筛选,选到谁就可以继续繁殖。
-
100100:每次筛选的概率为60/305
-
101010:每次筛选的概率为105/305
-
010101:每次筛选的概率为140/305
这里假设染色体101010和010101各繁殖了两次,4.产生下一代
染色体101010和010101被筛选出来后,需要通过基因重组来产生下一代,如下:
第一对从第3位断开,尾部交换后,产生2个。第2对从第4位后面断开,尾部交换后,产生2个。这样就产生4个不同的基因。这里没有考虑基因突变,断点位置也是随机选的。5.迭代计算
继续重复3,4的步骤,结果如下:
-
101101:质量90,价值165——淘汰
-
010010:质量45,价值80
-
101001:质量65,价值120
-
010110:质量70,价值125
可以看到总体的价值和为325,比上一代的价值305好,但是上一个有一个价值为140的超强个体,这一代却没有。
在一次完整的计算中,迭代会有几十代或更久,如果发生连续几代适应函数基本不增加或减少的情况,说明函数已经收敛了。
下面是书中所附代码,(个人觉得存在问题,原因在于迭代结束的条件设置有问题。书中所使用的收敛条件是连续两代的价值函数和最大值不在增加即跳出循环。个人感觉这会依赖于实验结果的随机性,不过此例子用来理解遗传算法的基本思想是再好不过了,特此贴上,以备日后再学习)。
# coding=utf-8
import random
#背包问题
#物品质量价格
X = {
1: [10, 15],
2: [15, 25],
3: [20, 35],
4: [25, 45],
5: [30, 55],
6: [35, 70]}
#终止界限
FINISHED_LIMIT = 5
#重量界限
WEIGHT_LIMIT = 80
#染色体长度
CHROMOSOME_SIZE = 6
#遴选次数
SELECT_NUMBER = 4
max_last = 0
diff_last = 10000
#判断退出
def is_finished(fitnesses):
global max_last
global diff_last
max_current = 0
for v in fitnesses:
if v[1] > max_current:
max_current = v[1] # 得到当前最大的价值
diff = max_current - max_last # 价值差
if diff < FINISHED_LIMIT and diff_last < FINISHED_LIMIT: # 这里判断连续两代的改变量如果都小于5,则停止迭代
return True
else:
diff_last = diff
max_last = max_current
return False
#初始染色体样态
def init():
chromosome_state1 = '100100'
chromosome_state2 = '101010'
chromosome_state3 = '010101'
chromosome_state4 = '101011'
chromosome_states = [chromosome_state1,
chromosome_state2,
chromosome_state3,
chromosome_state4]
return chromosome_states
#计算适应度
def fitness(chromosome_states):
fitnesses = []
for chromosome_state in chromosome_states: # 遍历所有的染色体
value_sum = 0
weight_sum = 0
for i, v in enumerate(chromosome_state): # 将一个可遍历的数据对象组合为一个索引序列,同时列出数据和数据下标
if int(v) == 1: # 对染色体中的1,即存在的物品体重和价格求和
weight_sum += X[i + 1][0]
value_sum += X[i + 1][1]
fitnesses.append([value_sum, weight_sum])
return fitnesses
#筛选
def filter(chromosome_states, fitnesses):
#重量大于80的被淘汰
index = len(fitnesses) - 1
while index >= 0:
index -= 1
if fitnesses[index][1] > WEIGHT_LIMIT:
chromosome_states.pop(index) # 弹出不符合条件的染色体
fitnesses.pop(index) # 弹出不符合条件的适应度
#遴选
selected_index = [0] * len(chromosome_states)
for i in range(SELECT_NUMBER): # 随机选择染色体,然后得到相应的索引
j = chromosome_states.index(random.choice(chromosome_states)) #随机选择染色体,然后得到相应的索引
selected_index[j] += 1
return selected_index
#产生下一代
def crossover(chromosome_states, selected_index):
chromosome_states_new = []
index = len(chromosome_states) - 1 # 遍历完所有的染色体组的染色体(其中下标-1代表最后一个染色体的索引)
while index >= 0:
print('index:',index)
print('chromosome_states:',chromosome_states)
index -= 1
chromosome_state = chromosome_states.pop(index) # 弹出后的染色体
for i in range(selected_index[index]):
chromosome_state_x = random.choice(chromosome_states) # 随机选择一个染色体
pos = random.choice(range(1, CHROMOSOME_SIZE - 1))
chromosome_states_new.append(chromosome_state[:pos] + chromosome_state_x[pos:]) #[:pos],取前pos位,[pos:],取pos后几位。
chromosome_states.insert(index, chromosome_state) # 恢复原染色体组
return chromosome_states_new # 返回得到的新的染色体组
if __name__ == '__main__':
#初始群体
chromosome_states = init()
n = 100 #迭代次数
while n > 0:
n -= 1
#适应度计算
fitnesses = fitness(chromosome_states)
if is_finished(fitnesses): #如果两代的价值函数和不再增加,就跳出循环
break
print('1:', fitnesses)
#遴选
selected_index = filter(chromosome_states, fitnesses)
print('2:', selected_index)
#产生下一代
chromosome_states = crossover(chromosome_states, selected_index)
fitnesses = fitness(chromosome_states)
print(chromosome_states)
print(fitnesses)
##################################结果
G:\annconda\python.exe D:/白话强化学习/DRLPytorch-master/ga/bag.py
1: [[60, 35], [105, 60], [140, 75], [175, 95]]
2: [1, 1, 2]
index: 2
chromosome_states: ['100100', '101010', '010101']
index: 1
chromosome_states: ['100100', '101010', '010101']
index: 0
chromosome_states: ['100100', '101010', '010101']
1: [[155, 85], [130, 70], [80, 45], [125, 70]]
2: [0, 2, 2]
index: 2
chromosome_states: ['100101', '010010', '010110']
index: 1
chromosome_states: ['100101', '010010', '010110']
index: 0
chromosome_states: ['100101', '010010', '010110']
['010110', '010110', '010101', '010101']
[[125, 70], [125, 70], [140, 75], [140, 75]]
上面结果是某一次的运行结果。该程序运行结果每次都会不同。收敛的速度会受到很多因素而变化,如基因的长度,基因重组时的方案,每一代产生个体的数量,基因变异的程度等等。通过此例子主要是理解遗传算法的基本原理以及python中内部函数的用法。