退而求其次(3)——宿管员的烦恼

  书接上文,在《退而求其次(1)——随机法》中宿管员使用了随机法分配宿舍,现在尝试使用遗传算法。

顺序编码和初始种群

  遗传算法的首要问题是基因编码。对于分宿舍问题,每种分配方案是一个个体,其基因序列的每一个编码代表一个同学,要求处于同一基因序列中的所有基因代码均不能重复,也就是每个同学都是独一无二的。在这种规则下,使用二进制编码就显得笨拙了。一种简单的编码方案是直接使用同学的序号作为基因编码,这种编码称为顺序码。

  顺序码又称自然数编码,使用从1到n 的自然数进行编码,且不允许重复。例如[1,2,3,4,5,6,7,8,9,10,11,12]是一个合法的个体,表示按编码从左到右的顺序四人一个宿舍,而[1,1,3,3,6,6,8,8,9,10,11,12]则不是。

  随机选择1000个个体作为初始种群,约占解空间的20%:

1 POPULATION_SIZE = 1000 # 种群数量
2
3 def init_population():
4     ''' 构造初始种群 '''
5     population = []
6     code_len = len(base_data.STUDENTS_NAME)  # 编码长度
7     for i in range(POPULATION_SIZE):
8         population.append(base_data.upset())
9     return population

适应度评估和种群选择

  可以利用成本函数cost_fun来进行适应度评估。由于cost_fun识别的是解而不是基因编码,因此在使用cost_fun之前还需要通过solution_adapter将基因编码适配成解,将基因编码[1,2,3,4,5,6,7,8,9,10,11,12]翻译成cost_fun能够有效计算的[[1,2,3,4],[5,6,7,8],[9,10,11,12]]。

 1 def solution_adapter(code):
 2     ''' 将基因编码翻适配成cost_fun能够识别的解 '''
 3     solution = []
 4     for i in range(0, len(base_data.STUDENTS_NAME), base_data.NUM_PER_DROM):
 5         solution.append(code[i:i + base_data.NUM_PER_DROM])
 6     return solution
 7
 8 def fitness_fun(code):
 9     '''
10     适应度评估
11     :param code:   二进制基因编码
12     :return: 适应度评估值, 二元组, (宿舍总成本, 每个宿舍的成本)
13     '''
14     return base_data.cost_fun(solution_adapter(code))

  我们依然使用锦标赛法选择种群。由于成本越低表示适应度越越高,因此在锦标赛中适应度值低的是胜出者。

1 def selection(population):
2     '''选择策略, 锦标赛法'''
3     pop_next = []  # 下一代种群
4     for i in range(POPULATION_SIZE):
5         tour_list = random.choices(population, k=2) # 二元锦标赛
6         winner = min(tour_list, key=lambda x: fitness_fun(x)) # 成本低的胜出
7         pop_next.append(winner)
8     return pop_next

部分匹配交叉和循环交叉

  对于宿舍编码来说,单点交叉和两点交叉都无法产生合法的个体,下图展示了一个不合法的单点交叉。

  在交叉后的个体 r1' 中,基因代码6和8出现了两次,表示6号同学和8号同学同时住在两个宿舍,这显然不是一个合法的解。反过来,如果交叉后得到了合法的个体,那么新个体又会和它们的父代没有任何区别,即没有产生任何新个体:

  r1和 r1' 没有任何区别,最后一个宿舍的四个同学仅仅是交换了一下床位。看来必须另辟他径,寻找其它的交叉策略。

部分匹配交叉

  部分匹配交叉(Partially Matched Crossover,PMC)在1985年被提出,是由两点交叉改进而来的。部分匹配交叉的第一步和两点交叉一样,首先在个体基因序列中随机设置两个交叉点,然后随机选择两个个体做为父代个体,相互交换它们交叉点之间的那部分基因块

  在交叉时需要记住交叉前的基因块r1→[5,6,7,8],r2→[9,11,2,4] 。

  接下来对交叉后生成的新个体 r1' 和 r2' 中的×部分,分别继承r1和 r2 中对应位置的编码,如果待继承的编码在交换后的基因块中,则不做继承:

  最后,把交叉前记住的基因块按顺序依次填入×部分,得到最终的r1'和 r2' :

  如果交叉前的编码已经在 r1' 中,则略过该编码。下图在替换x时需要略过2和6:

  r1 是初始个体,它的基因块[2,4,6,9]与另一个个体的对应基因块[2,6,7,8]交叉,得到[×,×,×,×.2,6,7,8,×,×,×,×]。继承 r1 后得到 r1' = [1,3,5,×,2,6,7,8,×,10,11,12]。在[2,4,6,9]中,编码2和6已经在 r1' 中,因此只有4和9可以替换对应的×。

  部分匹配交叉的编码如下。

 1 def crossover_pmc(population):
 2     ''' 部分匹配交叉(PMC)'''
 3
 4     def create_mapping(cross_code_1, cross_code_2):
 5         '''
 6         建立两个交叉片段间的映射关系
 7         :param cross_code_1:
 8         :param cross_code_2:
 9         :return:  映射关系set
10         '''
11         mapping = set()
12         for i in range(len(cross_code_1)):
13             c1, c2 = cross_code_1[i], cross_code_2[i]
14             if (c1, c2) not in mapping and  (c2, c1) not in mapping:
15                 mapping.add((c1, c2))
16         return mapping
17
18     def code_extends(child, parent, start, end):
19         '''
20         继承父代的编码
21         :param child: 子代个体
22         :param parent: 父代个体
23         :param start: 基因编码起始位置
24         :param end: 基因编码终止位置
25         '''
26         for i in range(start, end):
27            if parent[i] not in child:
28                child[i] = parent[i]
29
30     def code_rest(child, cross_code):
31         '''
32         通交叉前的基因片段修改子代的编码
33         :param child: 子代个体
34         :param cross_code: 交叉前的的基因片段
35         :return:
36         '''
37         for i, x in enumerate(child):
38             if x != -1:
39                 continue
40             for x_old in cross_code:
41                 if x_old not in child:
42                     child[i] = x_old
43                     break
44
45     pop_new = []  # 新种群
46     code_len = len(population[0])  # 基因编码的长度
47     for i in range(POPULATION_SIZE):
48         # 选择两个随机的交叉点
49         p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1)
50         if p1 > p2:
51             p1, p2 = p2, p1
52         parent1, parent2 = random.choices(population, k=2)  # 选择两个随机的个体
53         cross_code_1 = parent1[p1:p2] # 交叉前的编码块
54         cross_code_2 = parent2[p1:p2] # 交叉后的编码块
55         # 构造新的个体,-1表示基因编码尚未确定
56         r = [-1] * p1 + cross_code_2 + [-1] * (code_len - p2)
57         code_extends(r, parent1, 0, p1) #  继承父代的编码
58         code_extends(r, parent1, p2, code_len) #  继承父代的编码
59         mapping = create_mapping(cross_code_1, cross_code_2) # 两个交叉块的映射关系
60         code_rest(r, cross_code_1)  # 通过交换前的基因片段确定剩余编码
61         pop_new.append(r)
62     return pop_new

循环交叉

  循环交叉(Cycle Crossover,CX)是另一种适合顺序编码的交叉策略。不同于其它交叉策略,循环交叉不需事先要选择交叉点。

  假设有两个父代个体

  先从 r1 中选择第0个编码,作为子代 r1' 的第一个编码:

  r2 的第0个编码是2,因此 r1' 中第2个被确定的编码是2:

  2在 r1 中的序号是1, r2[1]=3, r1' 中第3个确定的编码是3:

  3在 r1 中的序号是2, r2[2]=5, r1' 中第4个确定的编码是5:

  5在 r1 中的序号是3, r2[3]=1 , 和 r1' 中第1个编码相同,至此称为一个循环。剩余未确定的编码从 r2 的对应位置映射即可

  循环交叉匹配的代码如下:

 1 def crossover_cx(population):
 2     ''' 循环交叉匹配 '''
 3     pop_new = []  # 新种群
 4     code_len = len(population[0])  # 基因编码的长度
 5     for i in range(POPULATION_SIZE):
 6         parent1, parent2 = random.choices(population, k=2)  # 选择两个随机的个体
 7         r_new = [-1] * code_len  # 新个体
 8         r_new[0] = parent1[0]
 9         i = 0
10         while True:  # 循环交叉
11             x = parent2[i]
12             if r_new[0] == x:
13                 break
14             i = parent1.index(x)
15             r_new[i] = x
16         # r_new中剩余未确定的编码直接从parent2中继承
17         for i, x in enumerate(r_new):
18             if x == -1:
19                 r_new[i] = parent2[i]
20         pop_new.append(r_new)
21     return pop_new

变异

  顺序编码的变异策略很简单,仅仅是将两个随机变异点的编码互相交换:

 1 def mutation(population):
 2     ''' 变异 '''
 3     code_len = len(population[0])  # 基因编码的长度
 4     mp = 0.2  # 变异率
 5     for i, r in enumerate(population):
 6         if random.random() < mp:
 7             # 两个随机变异点
 8             p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1)
 9             # 交换两个变异点的数据
10             population[p1], population[p2] = population[p2], population[p1]

分配宿舍

  准备工作已经就绪,可以开始使用遗传算法分配宿舍:

 1 def sum_fitness(population):
 2     ''' 计算种群的总适应度 '''
 3     return sum([fitness_fun(code)[0] for code in population])
 4
 5 def ga():
 6     ''' 遗传算法分配宿舍 '''
 7     population = init_population() # 构建初始化种群
 8     s_fitness = sum_fitness(population) # 种群的总适应度
 9     i = 0
10     while i < 10: # 如果连续10代没有改进,结束算法
11         pop_next = selection(population) # 选择种群
12         pop_new = crossover_cx(pop_next) # 交叉
13         mutation(pop_new) # 变异
14         s_fitness_new = sum_fitness(pop_new) # 新种群的总适应度
15         if s_fitness > s_fitness_new: # 成本越低,适应度越高
16             s_fitness = s_fitness_new
17             i = 0
18         else:
19             i += 1
20         population = pop_new
21     # 按适应度值从大到小排序
22     population = sorted(population, key=lambda x: fitness_fun(x), reverse=True)
23     # 返回最优的个体
24     return population[0]
25
26 if __name__ == '__main__':
27     best = ga()
28     solution = solution_adapter(best)
29     total_cost, dorms_cost = base_data.cost_fun(solution)
30     base_data.print_solution(solution, total_cost, dorms_cost)

  需要注意的是第15行的适应度比较,由于这里使用的是成本函数,因此种群的成本值越低,适应度越高,越应该被保留。一种可能的运行结果:

  可以看到,在总成本较低的同时,“不均”的问题也得到了解决。

总体代码

  base_data.py:

  1 import random
  2
  3 # 学生调查表数据
  4 STUDENTS = [
  5     [32, 1, 2, 2, [11, 33, 42], 5],
  6     [32, 1, 2, 1, [11], 5],
  7     [41, 1, 2, 5, [21, 22], 4],
  8     [43, 2, 3, 3, [11, 21], 3],
  9     [36, 2, 3, 4, [11, 33], 3],
 10     [44, 2, 3, 4, [41, 42], 2],
 11     [42, 1, 2, 1, [11, 12], 1],
 12     [32, 1, 1, 2, [31, 32], 2],
 13     [61, 1, 1, 3, [51], 3],
 14     [61, 1, 1, 2, [13], 3],
 15     [44, 3, 4, 1, [21, 43], 1],
 16     [22, 3, 4, 4, [22, 43], 2]
 17 ]
 18 # 学生姓名
 19 STUDENTS_NAME = ['蕾娜', '琪琳', '蔷薇', '炙心',
 20                  '灵犀', '莫伊', '怜风', '语琴',
 21                  '凉冰', '鹤熙', '瑞萌萌', '何蔚蓝']
 22 DROM_SIZE = 3  # 宿舍数量
 23 NUM_PER_DROM = 4 # 每个宿舍的人数
 24 MAX_COST = 5 # 同一维度间的最大差异
 25
 26 def cost_stu(stu_1, stu_2):
 27     ''' 以stu_1为主,计算stu_1与stu_2的差异 '''
 28     cost = [] # 各维度的成本值(差异度)
 29     cost.append(cost_equal(stu_1[0], stu_2[0])) # 籍贯成本
 30     cost.append(cost_equal(stu_1[1], stu_2[1])) # 专业成本
 31     cost.append(cost_class(stu_1[2], stu_2[2], stu_1[1], stu_2[1])) # 班级成本
 32     cost.append(cost_get_up(stu_1[3], stu_2[3])) # 起床成本
 33     cost.append(cost_interest(stu_1[4], stu_2[4])) # 爱好成本
 34     w_idx_1, w_idx_2 = stu_1[len(stu_1) - 1] - 1, stu_2[len(stu_2) - 1] - 1 # 权重序号
 35     w_cost_1, w_cost_2 = cost[w_idx_1] * 1.5,  cost[w_idx_2] * 1.5  # 加权处理
 36     # 判断二者最在意的是否相同
 37     if w_idx_1 == w_idx_1:
 38         cost[w_idx_1] = w_cost_1 + w_cost_2
 39     else:
 40         cost[w_idx_1], cost[w_idx_2] = w_cost_1, w_cost_2
 41
 42     return sum(cost)
 43
 44 def cost_equal(d_1, d_2):
 45     ''' 同质化比较成本  '''
 46     return 0 if d_1 == d_2 else MAX_COST
 47
 48 def cost_class(d_1, d_2, sub_1, sub_2):
 49     ''' 班级成本 '''
 50     if d_1 == d_1: # 班级相同
 51         return 0
 52     elif sub_1 == sub_1: # 不同班级,同一专业
 53         return 1
 54     else: # 不同班级,不同专业
 55         return MAX_COST
 56
 57 def cost_get_up(d_1, d_2):
 58     ''' 起床成本 '''
 59     return 1.2 * (d_2 - d_1)
 60
 61 def cost_interest(d_1, d_2):
 62     ''' 爱好成本 '''
 63     for t_1 in d_1:
 64         # 如果两个同学都有一个共同的爱好,二者就是零距离
 65         if t_1 in d_2:
 66             return 0
 67
 68         obj_1 = t_1 // 10 # 爱好的“大类”
 69         # 如果两个同学都有一个共同的大类,二者距离是2
 70         for t_2 in d_2:
 71             if obj_1 == t_2 // 10:
 72                 return 2
 73     return MAX_COST
 74
 75 def cost_fun(solution):
 76     '''
 77     计算方案中每个宿舍的成本
 78     :param solution: 宿舍分配方案
 79     :return: 宿舍总成本和每个宿舍的成本
 80     '''
 81     droms_cost = []
 82     for drom in solution:
 83         # 同一宿舍中的四个同学两两比对
 84         d_cost = 0
 85         d_cost += cost_stu(STUDENTS[drom[0]], STUDENTS[drom[1]])
 86         d_cost += cost_stu(STUDENTS[drom[0]], STUDENTS[drom[2]])
 87         d_cost += cost_stu(STUDENTS[drom[0]], STUDENTS[drom[3]])
 88         d_cost += cost_stu(STUDENTS[drom[1]], STUDENTS[drom[2]])
 89         d_cost += cost_stu(STUDENTS[drom[1]], STUDENTS[drom[3]])
 90         d_cost += cost_stu(STUDENTS[drom[2]], STUDENTS[drom[3]])
 91         droms_cost.append(d_cost)
 92     diff_cost = max(droms_cost) - min(droms_cost) # 宿舍之间的贫富差
 93     total_cost = sum(droms_cost) + diff_cost * DROM_SIZE # 该方案的总成本
 94     return total_cost, droms_cost
 95
 96 def upset():
 97     ''' 打乱学生顺序 '''
 98     n = len(STUDENTS_NAME)
 99     stu_list = list(range(n))
100     # 打乱学生顺序
101     for i in range(n):
102         rand_idx = random.randint(0, n - 1)
103         stu_list[i], stu_list[rand_idx] = stu_list[rand_idx], stu_list[i]
104     return stu_list
105
106 def print_solution(solution, total_cost, dorms_cost):
107     for i, drom in enumerate(solution):
108         print('宿舍%d:\t' % i, end='')
109         for j in drom:
110             print('%-8s' % STUDENTS_NAME[j], end='')
111         print('\tcost=%f' % dorms_cost[i])
112     print('total=%f' % total_cost)
genetic_optimize.py
  1 from __future__ import division
  2 import random
  3 import os
  4 import sys
  5 parent_dir_name = os.path.dirname(os.path.realpath(__file__))
  6 sys.path.append(parent_dir_name )
  7 import base_data
  8
  9 POPULATION_SIZE = 1000 # 种群数量
 10
 11 def init_population():
 12     ''' 构造初始种群 '''
 13     population = []
 14     code_len = len(base_data.STUDENTS_NAME)  # 编码长度
 15     for i in range(POPULATION_SIZE):
 16         population.append(base_data.upset())
 17     return population
 18
 19 def solution_adapter(code):
 20     ''' 将基因编码翻适配成cost_fun能够识别的解 '''
 21     solution = []
 22     for i in range(0, len(base_data.STUDENTS_NAME), base_data.NUM_PER_DROM):
 23         solution.append(code[i:i + base_data.NUM_PER_DROM])
 24     return solution
 25
 26 def fitness_fun(code):
 27     '''
 28     适应度评估
 29     :param code:   二进制基因编码
 30     :return: 适应度评估值, 二元组, (宿舍总成本, 每个宿舍的成本)
 31     '''
 32     return base_data.cost_fun(solution_adapter(code))
 33
 34 def selection(population):
 35     '''选择策略, 锦标赛法'''
 36     pop_next = []  # 下一代种群
 37     for i in range(POPULATION_SIZE):
 38         tour_list = random.choices(population, k=2) # 二元锦标赛
 39         winner = min(tour_list, key=lambda x: fitness_fun(x)) # 成本低的胜出
 40         pop_next.append(winner)
 41     return pop_next
 42
 43 def crossover_pmc(population):
 44     ''' 部分匹配交叉(PMC)'''
 45
 46     def create_mapping(cross_code_1, cross_code_2):
 47         '''
 48         建立两个交叉片段间的映射关系
 49         :param cross_code_1:
 50         :param cross_code_2:
 51         :return:  映射关系set
 52         '''
 53         mapping = set()
 54         for i in range(len(cross_code_1)):
 55             c1, c2 = cross_code_1[i], cross_code_2[i]
 56             if (c1, c2) not in mapping and  (c2, c1) not in mapping:
 57                 mapping.add((c1, c2))
 58         return mapping
 59
 60     def code_extends(child, parent, start, end):
 61         '''
 62         继承父代的编码
 63         :param child: 子代个体
 64         :param parent: 父代个体
 65         :param start: 基因编码起始位置
 66         :param end: 基因编码终止位置
 67         '''
 68         for i in range(start, end):
 69            if parent[i] not in child:
 70                child[i] = parent[i]
 71
 72     def code_rest(child, cross_code):
 73         '''
 74         通交叉前的基因片段修改子代的编码
 75         :param child: 子代个体
 76         :param cross_code: 交叉前的的基因片段
 77         :return:
 78         '''
 79         for i, x in enumerate(child):
 80             if x != -1:
 81                 continue
 82             for x_old in cross_code:
 83                 if x_old not in child:
 84                     child[i] = x_old
 85                     break
 86
 87     pop_new = []  # 新种群
 88     code_len = len(population[0])  # 基因编码的长度
 89     for i in range(POPULATION_SIZE):
 90         # 选择两个随机的交叉点
 91         p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1)
 92         if p1 > p2:
 93             p1, p2 = p2, p1
 94         parent1, parent2 = random.choices(population, k=2)  # 选择两个随机的个体
 95         cross_code_1 = parent1[p1:p2] # 交叉前的编码块
 96         cross_code_2 = parent2[p1:p2] # 交叉后的编码块
 97         # 构造新的个体,-1表示基因编码尚未确定
 98         r = [-1] * p1 + cross_code_2 + [-1] * (code_len - p2)
 99         code_extends(r, parent1, 0, p1) #  继承父代的编码
100         code_extends(r, parent1, p2, code_len) #  继承父代的编码
101         mapping = create_mapping(cross_code_1, cross_code_2) # 两个交叉块的映射关系
102         code_rest(r, cross_code_1)  # 通过交换前的基因片段确定剩余编码
103         pop_new.append(r)
104     return pop_new
105
106 def crossover_cx(population):
107     ''' 循环交叉匹配 '''
108     pop_new = []  # 新种群
109     code_len = len(population[0])  # 基因编码的长度
110     for i in range(POPULATION_SIZE):
111         parent1, parent2 = random.choices(population, k=2)  # 选择两个随机的个体
112         r_new = [-1] * code_len  # 新个体
113         r_new[0] = parent1[0]
114         i = 0
115         while True:  # 循环交叉
116             x = parent2[i]
117             if r_new[0] == x:
118                 break
119             i = parent1.index(x)
120             r_new[i] = x
121         # r_new中剩余未确定的编码直接从parent2中继承
122         for i, x in enumerate(r_new):
123             if x == -1:
124                 r_new[i] = parent2[i]
125         pop_new.append(r_new)
126     return pop_new
127
128 def mutation(population):
129     ''' 变异 '''
130     code_len = len(population[0])  # 基因编码的长度
131     mp = 0.2  # 变异率
132     for i, r in enumerate(population):
133         if random.random() < mp:
134             # 两个随机变异点
135             p1, p2 = random.randint(0, code_len - 1), random.randint(0, code_len - 1)
136             # 交换两个变异点的数据
137             population[p1], population[p2] = population[p2], population[p1]
138
139 def sum_fitness(population):
140     ''' 计算种群的总适应度 '''
141     return sum([fitness_fun(code)[0] for code in population])
142
143 def ga():
144     ''' 遗传算法分配宿舍 '''
145     population = init_population() # 构建初始化种群
146     s_fitness = sum_fitness(population) # 种群的总适应度
147     i = 0
148     while i < 10: # 如果连续10代没有改进,结束算法
149         pop_next = selection(population) # 选择种群
150         pop_new = crossover_cx(pop_next) # 交叉
151         mutation(pop_new) # 变异
152         s_fitness_new = sum_fitness(pop_new) # 新种群的总适应度
153         if s_fitness > s_fitness_new: # 成本越低,适应度越高
154             s_fitness = s_fitness_new
155             i = 0
156         else:
157             i += 1
158         population = pop_new
159     # 按适应度值从大到小排序
160     population = sorted(population, key=lambda x: fitness_fun(x), reverse=True)
161     # 返回最优的个体
162     return population[0]
163
164 if __name__ == '__main__':
165     best = ga()
166     solution = solution_adapter(best)
167     total_cost, dorms_cost = base_data.cost_fun(solution)
168     base_data.print_solution(solution, total_cost, dorms_cost)

   作者:我是8位的

  出处:http://www.cnblogs.com/bigmonkey

  本文以学习、研究和分享为主,如需转载,请联系本人,标明作者和出处,非商业用途! 

  扫描二维码关注公众号“我是8位的”

posted on 2019-05-06 13:38  我是8位的  阅读(948)  评论(0编辑  收藏  举报

导航