0 建议学时
4学时
1 人工智能概述
2020中国人工智能产业年会在苏州召开,会上发布的《中国人工智能发展报告2020》显示,过去十年(2011-2020) ,中国人工智能专利申请量达389571件,占全球总量的74.7%,位居世界第一。
报告指出,中国在自然语言处理、芯片技术、机器学习等10多个人工智能子领域的科研产出水平居于世界前列。
人工智能被认为是二十一世纪三大尖端技术(基因工程、纳米科学、人工智能)之一。
人工智能的基石是数学,而这些数学算法的实现是由一个一个的函数组合得来。比如,我们计算
- 数据
- 算法
- 算力
2 遗传算法概述
一种通过模拟自然进化过程搜索最优解的方法。遗传算法是一种仿生算法,最主要的思想是物竞天择,适者生存。这个算法很好的模拟了生物的进化过程,保留好的物种,同样一个物种中的佼佼者才会幸运的存活下来。转换到数学问题中,这个思想就可以很好的转化为优化问题,在求解方程组的时候,好的解视为好的物种被保留,坏的解视为坏的物种而淘汰,设置好进化次数以后开始迭代,记录下这些解里面最好的那个,就是方程组的解。
遗传算法是类比自然界的达尔文进化实现的简化版本。
达尔文进化论的原理概括总结如下:
- 变异:种群中单个样本的特征(性状,属性)可能会有所不同,这导致了样本彼此之间有一定程度的差异;
- 遗传:某些特征可以遗传给其后代。导致后代与双亲样本具有一定程度的相似性;
- 选择:种群通常在给定的环境中争夺资源。更适应环境的个体在生存方面更具优势,因此会产生更多的后代。
3 基本概念
3.1 基因和染色体
在遗传算法中,我们首先需要将要解决的问题映射成一个数学问题,也就是所谓的“数学建模”,那么这个问题的一个可行解即被称为一条“染色体”。一个可行解一般由多个元素构成,每一个元素就被称为染色体上的一个“基因”。
比如说,对于如下函数而言,[1,2,3]、[1,3,2]、[3,2,1]均是这个函数的可行解(代入函数成立即为可行解),那么这些可行解在遗传算法中均被称为染色体。
这些可行解一共有三个元素构成。在遗传算法中,每个元素被称为染色体的一个基因。
3.2 适应度函数
在自然界中,似乎存在着一个上帝,它能够选择出每一代中比较优良的个体,而淘汰一些环境适应度较差的个体。那么在遗传算法中,如何衡量染色体的优劣呢?这就是由适应度函数完成的。适应度函数在遗传算法中扮演这个“上帝”的角色。
遗传算法在运行的过程中会进行次迭代,每次迭代都会生成若干条染色体。适应度函数会给本次迭代中生成的所有染色体打个分,来评判这些染色体的适应度,然后将适应度较低的染色体淘汰掉,只保留适应度较高的染色体,从而经过若干次迭代后染色体的质量将越来越优良。
3.3 交叉
遗传算法每一次迭代都会生成N条染色体,在遗传算法中,这每一次迭代就被称为一次“进化”。那么,每次进化新生成的染色体是如何而来的呢?答案就是“交叉”,你可以把它理解为交配。
交叉的过程需要从上一代的染色体中寻找两条染色体,一条是爸爸,一条是妈妈。然后将这两条染色体的某一个位置切断,并拼接在一起,从而生成一条新的染色体。这条新染色体上即包含了一定数量的爸爸的基因,也包含了一定数量的妈妈的基因。
那么,如何从上一代染色体中选出爸爸和妈妈的基因呢?这不是随机选择的,一般是通过轮盘赌算法完成。
在每完成一次进化后,都要计算每一条染色体的适应度,然后采用如下公式计算每一条染色体的适应度概率。
染色体i被选择的概率 = 染色体i的适应度 / 所有染色体的适应度之和
在进行交叉过程时,需要根据这个概率来选择父母染色体。适应度比较大的染色体被选中的概率就越高。这也就是为什么遗传算法能保留优良基因的原因。
3.4 变异
交叉能保证每次进化留下优良的基因,但它仅仅是对原有的结果集进行选择,基因还是那么几个,只不过交换了他们的组合顺序。这只能保证经过次进化后,计算结果更接近于局部最优解,而永远没办法达到全局最优解,为了解决这一个问题,我们需要引入变异。
变异很好理解。当我们通过交叉生成了一条新的染色体后,需要在新染色体上随机选择若干个基因,然后随机修改基因的值,从而给现有的染色体引入了新的基因,突破了当前搜索的限制,更有利于算法寻找到全局最优解。
3.5 复制
每次进化中,为了保留上一代优良的染色体,需要将上一代中适应度最高的几条染色体直接原封不动地复制给下一代。
假设每次进化都需生成条染色体,那么每次进化中,通过交叉方式需要生成条染色体,剩余的条染色体通过复制上一代适应度最高的条染色体而来。
4 通过例子说明进化过程
求的最小值,其中之间的整数。
4.1 第一轮
4.1.1 随机产生初始种群
4.1.2 交叉繁衍,变异
4.1.3 优胜劣汰
4.1.4 流程图
4.2 第二轮
4.2.1 交叉繁衍,变异
4.2.2 优胜劣汰
5 如何编写<遗传算法计算最小值>的程序?
根据流程图写出步骤,每个步骤标号:
细化所有的步骤,直到能和代码对应为止。
如何设计函数?哪些代码应该封装为函数?
5.1 基础知识回顾
- 复习1:函数定义
def fact(n):
s = 1
for i in range(1, n+1):
s*= i
return s
- 复习2:函数调用
a=fact(5)
print(a)
- 复习3:匿名函数
sum = lambda a,b: a+b
print(sum(1 , 3)) #调用函数打印其返回值
5.2 模块设计
5.3 核心代码
5.3.1 随机产生初始种群
lt=[21,42,8,57]
5.3.2 繁衍,交叉变异
# 繁衍,交换基因,x,y是数据,pos是从第几位开始交换
def exchange(x, y, pos):
xstr = '{0:06b}'.format(x) # 转换为6位二进制
ystr = '{0:06b}'.format(y) # 转换为6位二进制
tempxstr = '0b' + xstr[:pos] + ystr[pos:] # 临时存放
tempystr = '0b' + ystr[:pos] + xstr[pos:] # 临时存放
return int(tempxstr, 2), int(tempystr, 2) # 再转换为十进制
# 交叉变异
def mutate(x, pos):
xstr = '{0:06b}'.format(x) # 转换为6位二进制
if xstr[pos] == '0':
xstr = xstr[:pos] + '1' + xstr[pos + 1:]
else:
xstr = xstr[:pos] + '0' + xstr[pos + 1:]
return int(xstr, 2)
5.3.3 总体代码
import random
# 交叉变异
def mutate(x, pos):
xstr = '{0:06b}'.format(x) # 转换为6位二进制
if xstr[pos] == '0':
xstr = xstr[:pos] + '1' + xstr[pos + 1:]
else:
xstr = xstr[:pos] + '0' + xstr[pos + 1:]
return int(xstr, 2)
# 繁衍,交换基因,x,y是数据,pos是从第几位开始交换
def exchange(x, y, pos):
xstr = '{0:06b}'.format(x) # 转换为6位二进制
ystr = '{0:06b}'.format(y) # 转换为6位二进制
tempxstr = '0b' + xstr[:pos] + ystr[pos:] # 临时存放
tempystr = '0b' + ystr[:pos] + xstr[pos:] # 临时存放
return int(tempxstr, 2), int(tempystr, 2) # 再转换为十进制
lt=[21,42,8,57]
#lt=[1,2,3,4]
for gen in range(0,10): #迭代次数
son1,son2=exchange(lt[0],lt[1],3) #1、2号繁衍2个后代,加入lt
lt.append(son1)
lt.append(son2)
son3,son4=exchange(lt[2],lt[3],3) #3、4号繁衍2个后代,加入lt
lt.append(son3)
lt.append(son4)
son5=mutate(lt[2],random.randint(0,5)) #根据第3个父数据变异
lt.append(son5)
son6=mutate(lt[3],random.randint(0,5)) #根据第4个父数据变异
lt.append(son6)
lt=sorted(lt,key=lambda x:x*x-19*x+20) #根据目标函数排序
print("第{}代的种群为{}".format(gen+1,lt))
ls=lt[:4]
lt=ls
x=lt[0]
print("最小的X是{}, 对应的y值为{}".format(x,x*x-19*x+20))
知识点分析:
- int() 函数用于将一个字符串或数字转换为整型:
- class int(x, base=10);
- 如果x是数字,不能带第二个参数;如果是字符串,可以带第二个参数;
举例如下:
>>>int() # 不传入参数时,得到结果0
0
>>> int(3)
3
>>> int(3.6)
3
>>> int('12',16) # 如果是带参数base的话,12要以字符串的形式进行输入,12 为 16进制
18
>>> int('0xa',16)
10
>>> int('10',8)
8
- Python字符串提供区间访问方式,采用格式,表示字符串从到(不包括)的子字符串。如果或缺失,则表示字符串把开始或结束索引值设为默认值。
- 在Python中,lambda的语法是唯一的。其形式如下:
lambda argument_list: expression
其中,lambda是Python预留的关键字,argument_list和expression由用户自定义。
lambda函数有如下特性:
- lambda函数是匿名的:所谓匿名函数,通俗地说就是没有名字的函数。lambda函数没有名字;
- lambda函数有输入和输出:输入是传入到参数列表argument_list的值,输出是根据表达式expression计算得到的值;
- lambda函数一般功能简单:单行expression决定了lambda函数不可能完成复杂的逻辑,只能完成非常简单的功能。由于其实现的功能一目了然,甚至不需要专门的名字来说明。
5.3.4 运行结果
第1代的种群为 [9, 8, 12, 21, 26, 37, 41, 42, 56, 57]
第2代的种群为 [9, 9, 8, 8, 8, 12, 5, 5, 21, 28]
第3代的种群为 [9, 9, 9, 9, 8, 8, 8, 8, 12, 24]
第4代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第5代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第6代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第7代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第8代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第9代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
第10代的种群为 [9, 9, 9, 9, 9, 9, 9, 9, 13, 25]
最小的X是9,对应的y值为-70
5.4 思考与拓展
5.4.1 初始种群的规模与生成规则
初始种群:1,2,3,4
第1代的种群为[7, 4, 4, 3, 3, 2, 2, 1, 1, 20]
第2代的种群为[7, 7, 4, 4, 4, 4, 3, 3, 0, 19]
第3代的种群为[7, 7, 7, 7, 4, 4, 4, 4, 0, 20]
第4代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第5代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第6代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第7代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第8代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第9代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
第10代的种群为[7, 7, 7, 7, 7, 7, 7, 7, 3, 23]
最小的X是7,对应的y值为-64
初始种群:
第1代的种群为[9, 8, 12, 4, 15, 21, 26, 37, 41, 42, 52, 56, 57, 59, 63]
第2代的种群为[9, 9, 8, 8, 8, 12, 12, 5, 4, 15, 4, 17, 20, 21, 31]
第3代的种群为[9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 12, 12, 12, 24]
第4代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 8, 8, 8, 8, 12, 13, 25]
第5代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
第6代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
第7代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
第8代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
第9代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
第10代的种群为[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 25]
最小的X是9,对应的y值为-70
5.4.2 遗传规则的设计:交叉、变异
采用随机数生成变异基因的位置
#变异
def mutate(x):
s=random.randint(0,5) #随机取0-5中任意数
fb='{0:06b}'.format(x) #转换为6位二进制
if fb[pos]=='0':
sb=fb[:s] + '1' + fb[s+1:]
else:
sb=fb[:s] + '0' + fb[s+1:]
return int(sb,2)
6 课后延伸
- 启发式算法还包含:蚁群算法、模拟退火算法等,调研这些算法,并撰写报告;
- 请编写遗传算法部分的总结报告,包含:
(1)遗传算法的原理
(2)遗传算法的流程
(3)举一个计算实例说明其原理
(4)如何编写遗传算法的代码
(5)该算法的优缺点及如何改进
附录A:轮盘赌算法
该算法本质就是转盘,看转盘停下时停在转盘的哪个区域。而转盘停在哪个区域与转盘的区域面积正相关。
对于上面的转盘,当我们转动时,指针停在绿色区域概率就是0.6,蓝色是0.1,橙色是0.3。
在实际应用中,我们得到根据各个解的概率去选择一个解,就相当于轮盘赌。
附A.1 实现方法
(1)使用choice函数
在numpy.random中有一个choice函数,它可以根据各个标签对应的概率选标签,概率越大,被选概率越大。如果只选1个,就相当于转一次轮盘得到结果;选n个,就相当于转n次转盘,得到n个结果。
import numpy as np
x=np.random.choice(a=[1,2,3,4],size=1,replace=True,p=[0.1,0.2,0.3,0.4])
print(x)
其中size就是选的解的个数,这里size=1就是转一次盘得到的一个结果。p就是选a里面各个对应值的对应概率。p需要在0~1之间,并且各个概率和一定是1,p的个数与a里面值的个数一致。
(2)轮盘赌算法实现。
把各个概率累加,这样就得到长度为1的线段,每一段长度代表相应值的概率,概率越大,该线段占整个现电的比例越大。然后随机选取0~1之间的一个数,插入整个线段中。落入哪一段就选那个值。并且落入哪一段的概率与该段的长度(概率)成正相关,这样就相当于转轮盘选数了。
label=["a","b","c","d"] #需要选的值
p=[0.1,0.2,0.3,0.4] #上面选的值对应概率
cumsum_p=np.cumsum(p) #进行累加,每一个位置的值是前面所有值的和
cumsum_p=cumsum_p-np.random.rand() #相当于随机插入0~1之间的值
index=list(cumsum_p>0).index(True) #选择插入那段区间对应的位置
print(label[index]) #结果
上面算法把选择了把数进行累加的方法,然后与随机数0~1相减,变成小于零与大于零两部分,然后找第一个大于零的位置就是选的值的索引了。