启发式搜索算法4 -遗传算法实战:吊死鬼游戏

news2025/4/8 15:55:12

相关文章:
启发式搜索算法1 – 最佳优先搜索算法
启发式搜索算法2 – A*算法
启发式搜索算法2 – 遗传算法

有一个小游戏叫吊死鬼游戏(hangman),在学习英语的时候,大家有可能在课堂上玩过。老师给定一个英文单词,同学们就猜是什么单词,猜错一次老师就画一笔,如果把吊死鬼画出来就没有机会猜了,游戏结束。现在我们不限猜测的次数,让电脑也来玩一下,看它要多久才能猜中。

1 把背景剥离,找出问题核心,简化问题。

对于计算机来说,最简单就是盲目搜索,穷举所有情况,直到猜中为止。这样的穷举算法有兴趣的读者可以尝试用代码实现。这里当然要用刚学习的遗传算法,根据前面的介绍的算法思路,结合实际情况,来设置几个关键要素。

  • 字符从a至z视为基因,由这些字符生成的字符串被认为是染色体(个体),也就是一个解。
  • 适应能力得分就是用个体字符串与目标字符串比较,相同位置有相同字符的个数。
    因此,具有较高适应值的个体(猜中更多字母的解)将获得更多的繁殖权力。
  • 设置种群大小(一代个体的个数),初始种群的数量很重要,如果初始种群数量过多,算法会占用大量系统资源;如果初始种群数量过少,算法很可能忽略掉最优解。这次我们设置种群的数量为100,同一代会有一百个体。
  • 设置下一代组成,适应能力前10%直接进入下一代,这称为精英模式,适应能力前50%的个体拥有繁衍权力,则交配概率为50%,也就是必然产生下一代。一般取较大的交配概率,因为交配操作可以加快解区间收敛,使解达到最有希望的最佳解区域,但交配概率太高也可能导致过早收敛,则称为早熟,只找到局部最优解就停止进化。还有突变概率,这里设置为0.1,也可以说在组合新一代的每一个基因,有45%来自父亲基因,有45%来自母亲基因,有10%发生变异,随机产生一个基因。
  • 终止条件是适应能力值等于单词长度,也就是找到了目标单词,又或者到达最大进化代数(自定义参数配置),也会终止程序。

2.估算数据规模,算法复杂度

如果用穷举算法,可以估算时间复杂度,每一个位置尝试一遍26个字母,所以时间复杂度O(26^n),n为单词的长度。但对于遗传算法来说,由于太多不确定性,遗传算法的精度、可行度、计算复杂性等方面参数还没有有效的定量分析方法,这也算是它的一个缺点。

import random
GENES = 'abcdefghijklmnopqrstuvwxyz'  # 基因
class Individual(object): 
    '''此类代表种群中的个体'''
    def __init__(self, target, chromosome=None):
        self.target = target
        if chromosome: # 个体染色体,也就是所猜的单词解
            self.chromosome = chromosome
        else: # 若没有,创建一个随机的染色体
            self.chromosome = self.create_gnome()
        self.fitness = self.cal_fitness()  # 此个体的适用能力值
    @classmethod # 修饰符对应的函数不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数
    def mutated_genes(cls):
        # 基因突变,也就是从a-z中随机挑选一个字母
        global GENES 
        gene = random.choice(GENES) 
        return gene
    def create_gnome(self):
        # 初始化个体基因
        gnome_len = len(self.target) # 根据目标单词长度随机构建一个字符串
        return [self.mutated_genes() for _ in range(gnome_len)]
    def mate(self, parent, mutation=0.1):
        # 进行繁衍,产生新一代
        child_chromosome = [] # 后代染色体
        p1_proba = (1 - mutation) / 2 # 取得父母基因概率是一样
        for p1, p2 in zip(self.chromosome, parent.chromosome):
            # 遍历父母每一个基因,通过一定概率随机获取父母其中一方的基因,还有0.1概率发生变异
            prob = random.random() #获取一个0-1的随机数
            if prob < p1_proba: # 一半概率选择选择p1
                child_chromosome.append(p1) 
            elif prob < p1_proba*2: # 一半概率选择选择p2
                child_chromosome.append(p2) 
            else: # 剩下1-probability概率发生基因突变
                child_chromosome.append(self.mutated_genes()) 
        # 创建一个新个体,并返回
        return Individual(self.target, chromosome=child_chromosome) 
    def cal_fitness(self): 
        # 计算适应能力值,记录正确的字符数量
        fitness = 0
        for gs, gt in zip(self.chromosome, self.target): 
            if gs == gt: fitness+= 1
        return fitness
class GeneticAlgorithm(object):
    '''遗传算法'''
    def __init__(self, target, population_size=100,
                 proba_elitism=10, proba_crossover=50,
                 mutation=0.1, max_generation=100):
        self.population_size = population_size  # 种群大小
        self.target = target  # 目标单词
        self.proba_elitism = proba_elitism  # 精英模式的比例
        self.proba_crossover = proba_crossover  # 交配概率
        self.mutation = mutation  # 突变概率
        self.max_generation = max_generation  # 最大进化代数,也就是循环最多次数
        self.found = False  # 初始化时没有找到最优解
        self.generation = 1  # 当前进化的世代,创世纪是1
        self.population = []  # 种群,初始化为空
    def init_population(self):
        # 初始化第一代个体
        for _ in range(self.population_size):
            self.population.append(Individual(self.target))
    def main(self):
        # 主函数
        self.init_population() # 产生第一代个体
        # 若没有找到答案,并且没有达到最大进化代数,继续下一代演化
        while not self.found and self.generation < self.max_generation: 
            # 按照适应能力排序-内置排序法
            self.population = sorted(self.population, key = lambda x:x.fitness, reverse=True)
            # 一旦发现有适应值和目标长度一样,说明我们找到这个单词
            if self.population[0].fitness == len(self.target): 
                self.found = True
                break
            # 记录下一代个体
            new_generation = []
            # 精英模式,选择前10%的个体进入下一代
            s = int((self.proba_elitism*self.population_size)/100) 
            new_generation.extend(self.population[:s]) 
            # 90%的个体是通过上一代的交配得到
            s = int(((100 - self.proba_elitism)*self.population_size)/100)
            # 前50%有繁衍下一代的个体数量
            num_crossover =  int(self.proba_crossover * self.population_size / 100)
            for _ in range(s):
                # 前50%的个体可以有繁衍权力,在这些个体中随机挑选两个
                parent1 = random.choice(self.population[:num_crossover])  
                parent2 = random.choice(self.population[:num_crossover])
                child = parent1.mate(parent2, mutation=self.mutation) # 进行繁衍
                new_generation.append(child) # 得到新的一代个体
            self.population = new_generation # 新一代的个体代替上一代的
            print("第{}代\t单词: {}\t适应值: {}".format(
                self.generation, 
                "".join(self.population[0].chromosome), 
                self.population[0].fitness)) 
            self.generation += 1 # 记录进化的代数
        print("第{}代\t单词: {}\t适应值: {}".format(
            self.generation, 
            "".join(self.population[0].chromosome),
            self.population[0].fitness))

这里有两个类【Individual】是代表个体,【GeneticAlgorithm】是代表遗传算法。【Individual】个体在实例化的时候会先判断是否有染色体,如果没有就通过create_gnome()函数随机生成一个染色体,然后马上通过cal_fitness()函数计算它的适应能力值。mate()函数负责繁衍下一代新个体和mutated_genes()函数负责模拟基因突变。【GeneticAlgorithm】在实例化的时候就要设定遗传算法的各种配置,其中目标单词【target】是必要参数,其他参数是可选参数,都已经设定了默认值。然后通过调用main()函数来启动算法运行,在运算过程中把每一代适应能力值最大的个体打印出来,一起观察种群的进化过程。现在通过一些例子来验证代码是否可行。

ga = GeneticAlgorithm('generation')
ga.main()
# ---------结果-----------
ga = GeneticAlgorithm('generation')
第1代     单词: eikehaviro  适应值: 3
第2代     单词: eikehaviro  适应值: 3
第3代     单词: toneibxinn  适应值: 4
第4代     单词: binagavion  适应值: 5
第5代     单词: binagavion  适应值: 5
第6代     单词: emnenahion  适应值: 6
第7代     单词: emnenahion  适应值: 6
第8代     单词: emnenahion  适应值: 6
第9代     单词: ltneratton  适应值: 7
第10代    单词: eenaration  适应值: 8
第11代    单词: eeneration  适应值: 9
第12代    单词: eeneration  适应值: 9
第13代    单词: generation  适应值: 10

从打印在屏幕的信息中看到刚开始的适应能力值非常低,通过不断进化,适应值也逐步上升,到第十三代就找到答案,粗略估算每次100个个体,那么一共尝试了13*100=13000次就找到结果,效果还是不错的。因为遗传算法是具有不定性,当然不可能每次都是在第十三代就能得到结果,大家可以多尝试就会知道。同时可以尝试调整其他默认参数,观察参与变化对结果有什么影响。比如下面改变种群的个体数量,结果会发生什么变化。

target = 'announcement'
for p in range(100, 1000, 100): # 种群大小,每次增加100
    generate = 0
    for _ in range(10): # 每一种种群经过10次运算
        ga = GeneticAlgorithm(target, population_size=p)
        ga.main()
        generate += ga.generation # 统计总共经过多少次进化
    print('种群数量为%d, 总进化代数为%d, 平均每次通过%.1f进化得到结果' % (p, generate, generate/10))
# ---------结果-----------
种群数量为100, 总进化代数为199, 平均每次通过19.9进化得到结果
种群数量为200, 总进化代数为144, 平均每次通过14.4进化得到结果
种群数量为300, 总进化代数为132, 平均每次通过13.2进化得到结果
种群数量为400, 总进化代数为128, 平均每次通过12.8进化得到结果
种群数量为500, 总进化代数为131, 平均每次通过13.1进化得到结果
种群数量为600, 总进化代数为121, 平均每次通过12.1进化得到结果
种群数量为700, 总进化代数为115, 平均每次通过11.5进化得到结果
种群数量为800, 总进化代数为118, 平均每次通过11.8进化得到结果
种群数量为900, 总进化代数为118, 平均每次通过11.8进化得到结果

从结果我们看到,适当增加种群的个体数量,可以更快地获得结果。对于其他参数,大家也可以通过同样的方法进行测试,以下是其他参数的测试结果汇总到表,每一次只改变一个参数的值,其他参数按默认值。
在这里插入图片描述
从结果中知道,突变概率确实不能太大,越大需要更多次进化,并且在大于0.35就不能求出结果,因为程序设置了最大迭代次数就100。再查看繁衍权力这个参数,当它在40时候,也就是种群前40%的个体能够有机会繁衍下一代,得到进化代数结果是最小的。在解决实际问题的时候大家也可以通过这样的调试方式,找到合适的参数值。此时再用调试过的参数,种群数是400,突变概率是0.15,繁衍权力是40,重新测试一下,观察程序是否真的能提高效率。

ga = GeneticAlgorithm('generation', population_size=400,
    proba_crossover=40, mutation=0.15)
ga.main()
# ---------结果-----------
第1代 单词: uedpwxdkon  适应值: 3
第2代 单词: uedpwxdkon  适应值: 3
第3代 单词: gefzrbvbin  适应值: 4
第4代 单词: gexewgticx  适应值: 5
第5代 单词: oeteravign  适应值: 6
第6代 单词: xeneratijn  适应值: 8
第7代 单词: xeneratijn  适应值: 8
第8代 单词: xeneratijn  适应值: 8
第9代 单词: generation  适应值: 10

看到调优过的参数起作用了,比刚才少了4代,在第九代就找到答案了。也许这只是一个意外,那么再来做一个更严谨的测试,同样运行10次,验证是否真实提高效率。

generate1 = 0
generate2 = 0
for _ in range(10): # 经过10次运算
    ga = GeneticAlgorithm(target)
    ga.main()
    generate1 += ga.generation
    ga = GeneticAlgorithm(target, population_size=400,
        proba_crossover=40, mutation=0.15)
    ga.main()
    generate2 += ga.generation
print('默认参数:总进化代数为%d, 平均每次通过%.1f进化得到结果' % (
    generate1, generate1/10))
print('调优参数:总进化代数为%d, 平均每次通过%.1f进化得到结果' % (
    generate2, generate2 / 10))
# ---------结果-----------
默认参数:总进化代数为148, 平均每次通过14.8进化得到结果
调优参数:总进化代数为101, 平均每次通过10.1进化得到结果

完整代码可以查看:打开

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1636354.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python人脸识别全面教程

目录 第一部分&#xff1a;人脸识别基础 1.1 人脸检测 1.2 人脸识别算法 1.3 深度学习在人脸识别中的应用 1.4 人脸识别库 第二部分&#xff1a;人脸识别高级技术 2.1 特征提取与人脸编码 人脸编码示例 2.2 人脸识别流程 人脸识别流程示例 2.3 多人脸识别与跟踪 多…

LabVIEW航空发动机主轴承试验器数据采集与监测

LabVIEW航空发动机主轴承试验器数据采集与监测 随着航空技术的迅速发展&#xff0c;对航空发动机性能的测试与监测提出了更高的要求。传统的数据采集与监测方法已难以满足当前高精度和高可靠性的需求&#xff0c;特别是在主轴承试验方面。基于LabVIEW的航空发动机主轴承试验器…

翻译《The Old New Thing》 - How do I cover the taskbar with a fullscreen window?

How do I cover the taskbar with a fullscreen window? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20050505-04/?p35703 Raymond Chen 2005年5月5日 如何用全屏窗口覆盖任务栏&#xff1f; 很多时候&#xff0c;人们总是想得太多。…

[1688]jsp工资投放管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 工资投放管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0…

【数据结构】为了节省空间,对于特殊矩阵我们可以这样做……

特殊矩阵的压缩存储 导读一、数组与矩阵1.1 数组1.2 数组与线性表1.3 数组的存储结构1.4 矩阵在数组中的存储1.4.1 行优先存储1.4.2 列优先存储 二、特殊矩阵及其压缩存储三、对称矩阵及其存储3.1 方阵与对称矩阵3.2 对称矩阵的存储3.3 压缩存储的手动实现3.3.1 行优先存储3.3.…

虹科Pico汽车示波器 | 免拆诊断案例 | 起动机免拆诊断故障 2 例

电磁开关、换向器烧蚀及炭刷磨损均会导致起动机偶尔不工作&#xff0c;使发动机偶尔无法起动。由于故障是偶发的&#xff0c;且没有故障代码&#xff0c;这往往会让维修人员无从下手&#xff0c;而用Pico示波器测量起动电流&#xff0c;就会让这些“亚健康状态”一目了然。 案例…

一个好用的MQTT客户端软件

软件功能如下&#xff0c;实现的协议版本是 3.1.1 仅实现了常用的 CONNECT , PUBLISH , SUBSCRIBE 及相应的应答报文。支持以 Hex 格式显示接收的原始报文&#xff08;方便初学者学习&#xff09;。支持所有字段的自定义配置。支持保存与加载配置文件。 软件界面如下所示&…

刷代码随想录有感(50):路径总和

题干&#xff1a; 代码; class Solution { public:bool traversal(TreeNode* node, int count){if(node NULL)return false;if(!node -> left && !node -> right && count 0)return true;if(!node -> left && !node -> right &&…

出现 xx has no default (no arg) constructor 解决方法

目录 1. 问题所示2. 原理分析3. 解决方法 1. 问题所示 执行脱敏函数的时候&#xff0c;出现如下问题&#xff1a; Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Class com.example.test.ChineseNameDesensitizatio…

LLM 构建Data Multi-Agents 赋能数据分析平台的实践之③:数据分析之二(大小模型协同)

一、概述 随着新一代信息技术在产业数字化中的应用&#xff0c;产生了大量多源多模态信息以及响应的信息处理模式&#xff0c;数据孤岛、模型林立的问题也随之产生&#xff0c;使得业务系统臃肿、信息处理和决策效率低下&#xff0c;面对复杂任务及应用场景问题求解效率低。针…

触发器的查看和删除

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 如果想查看当前所有的触发器信息&#xff0c;可以使用数据字典 user_triggers&#xff0c;这个数据字典有很多字段可以查看所有触发器的名称、类型、表名、拥有者等信息。 …

【Vue 2.x】学习vue之一基础部分

文章目录 Vue 一基础部分第一章1、git两个分支主分支子分支 使用方法方式1&#xff1a;采用命令的方式操作分支方式2&#xff1a;在idea中使用git的分支 向git远程仓库提交时忽略文件使用git时的一些冲突注意事项 2、Vue问题1&#xff1a;什么是Vue&#xff1f;问题2&#xff1…

华为5700配置

恢复出厂设置&#xff0c;清空配置 1、更改名字 system-view sysname tp-10-50-01-04 2、配置管理接口 int vlan 1 ip add 10.50.1.4 255.255.254.0 quit 2、链路汇聚 interface eth-trunk 1 mode lacp quit 3、绑定端口 interface eth-trunk 1 trunkport gigabitethernet …

ubuntu入门

基础命令 cd 切换命令 ls 查看当前目录下所有的文件 cp a.c b.c 拷贝a.c 到 b.c touch a.c 创建a.c文件 mkdir file 创建文件夹file rm file 删除文件 rmdir 删除test文件夹 rmdir test/ mv 移动文件 mv a.c b.c 把a.c 替换成b.c ifconfig 查看电脑网络信息 rm xx 删…

STM32独立看门狗,实现单片机自动重启

今天学习了一下独立看门狗&#xff0c;看门狗的主要作用就是防止程序中有死循环或是不知道的bug&#xff0c;而造成在while循环中没有及时喂狗&#xff0c;程序就会控制单片机重启复位&#xff0c;从而不至于影响程序一直不能正常工作。 其实看门狗的应用也不是很复杂&#xf…

【webrtc】MessageHandler 6: 基于线程的消息处理:StunRequest实现包发送和超时重传

G:\CDN\rtcCli\m98\src\p2p\base\stun_request.cc使用OnMessage 实现包的发送和包的超时重传StunRequest 一个StunRequest 代表是一个独立的请求的发送STUN消息 要不是发送前构造好的,要不就是按照需要构建的使用StunRequestManager: 每一个STUNRequest 携带一个交互id 写入m…

利用Wget批量下载网页链接中的数据

1、电脑下载安装好Wget程序&#xff0c;具体操作流程可参照&#xff1a; wget 的安装与使用&#xff08;Windows&#xff09;_wget windows-CSDN博客https://blog.csdn.net/m0_45447650/article/details/125786723?ops_request_misc%257B%2522request%255Fid%2522%253A%25221…

设计模式 --6组合模式

文章目录 组合模式应用场景组合模式概念组合模式结构图透明方式和安全方式什么时候使用组合模式公司管理系统使用 组合模式来构架组合模式的好处 组合模式应用场景 整体和部分可以被一致性对待 比如人力资源部 财务部的管理功能可以复用于分公司的功能 可以引入一种 树状的结构…

LLM之RAG实战(三十八)| RAG分块策略之语义分块

在RAG应用中&#xff0c;分块是非常重要的一个环节&#xff0c;常见的分块方法有如下几种&#xff1a; Fixed size chunkingRecursive ChunkingDocument Specific ChunkingSemantic Chunking a&#xff09;Fixed size chunking&#xff1a;这是最常见、最直接的分块方法。我们…

final、finally、finalize有什么区别?

引言 在Java编程语言中&#xff0c;final、finally和finalize是三个具有不同用途和语义的关键字或方法。它们在编程和面试中经常被提及&#xff0c;因此理解它们之间的区别是非常重要的。 题目 final、finally、 finalize有什么区别&#xff1f; 典型回答 final&#xff1…