金某某的算法生活
IP:
0关注数
0粉丝数
0获得的赞
工作年
编辑资料
链接我:

创作·31

全部
问答
动态
项目
学习
专栏
金某某的算法生活

机器学习平台建设

本文从机器学习平台的架构开始,再到具体的功能,然后从需求的角度带给读者思考,找到合适的机器学习平台建设之路。最后,推荐了微软开源开放的机器学习平台OpenPAI,是可私有部署的机器学习训练平台。本文不少要点都可以展开为一篇文章,进行单独介绍,缩减编排是为了帮助各层次读者,了解机器学习平台的概况,起到综述的作用。如果读者对大数据、计算平台比较了解,能看到许多熟悉的内容,发现大数据平台与机器学习平台的相通之处。如果没有了解过其它的数据或计算平台,也能从需求与技术决策的角度来思考机器学习平台该如何建设。如果是人工智能应用的初学者,能对机器学习平台,以及生产环境的复杂性有一定的认识。机器学习不仅需要数据科学家研发新模型,软件工程师应用新模型,还需要软件工程师和运维工程师来建设机器学习平台。在应用机器学习的企业和团队中,建设机器学习平台是重要的一环。希望本文能在人工智能热潮中,给读者带来职业发展上的新思考、新方向。本文力图覆盖了机器学习平台的方方面面,但并不表示每个机器学习平台都需要所有的功能。要根据业务的特点以及发展阶段,对机器学习平台做好定位,逐步演进。要保证大方向上是正确的,满足当前的需求且有一定的前瞻性。同时也要避免过度设计,过早开发,以免分散精力,影响业务目标的达成。另外,还要管理好决策者、技术及业务团队对人工智能的期望。机器学习技术的发展阶段还不足以实现很多场景,除了从技术上不断创新勇攀高峰,也要让整个团队对人工智能的现状有清醒的认识,不可好高骛远。一、概述下图是较简化的机器学习平台架构,概括了机器学习平台的主要功能和流程。本章会进行简要介绍,在功能章节再展开详述。机器学习最主要的三个步骤可概括为:数据处理、建模以及部署。1、数据处理,即所有和数据相关的工作,包括存储、加工、采集和标记几大主要功能。前三者与大数据平台几乎一致,标记部分是机器学习平台所独有。数据存储较好理解,要根据存取的特点找到合适的存储系统。数据加工,也被称为ETL(Extract,Transform,Load),即将数据在不同的数据源间导入导出,并对数据进行聚合、变形、清洗等操作。数据采集,即从外部系统获得数据,包括通过网络爬虫来采集数据。数据标记,是将人类的知识附加到数据上,产生样本数据,以便训练出模型能对新数据推理预测。2、建模,即创建模型的过程,包括特征工程、试验、训练及评估模型。特征工程,即通过数据科学家(也称为算法工程师)的知识来挖掘出数据更多的特征,将数据进行相应的转换后,作为模型的输入。试验,即尝试各种算法、网络结构及超参,来找到能够解决当前问题的最好的模型。模型训练,主要是平台的计算过程,好的平台能够有效利用计算资源,提高生产力并节省成本。3、部署,是将模型部署到生产环境中进行推理应用,真正发挥模型的价值。部署这个词本身,可以仅仅代表将模型拷贝到生产环境中。但计算机软件的多年发展证明,提供一个好的服务需要考虑多种因素,并通过不断迭代演进,解决遇到的各种新问题,从而保持在较高的服务水平。4、对平台的通用要求,如扩展能力,运维支持,易用性,安全性等方面。由于机器学习从研究到生产应用都还处于快速发展变化的阶段,所以框架、硬件、业务上灵活的扩展能力显得非常重要。任何团队都需要或多或少的运维工作,出色的运维能力能帮助团队有效的管理服务质量,提升生产效率。易用性对于小团队上手、大团队中新人学习都非常有价值,良好的用户界面也有利于深入理解数据的意义。安全性则是任何软件产品的重中之重,安全漏洞是悬在团队头上一把剑,不能依靠运气来逃避问题。这里,不得不再分辨一下人工智能、机器学习、深度学习的含义,以便文中出现时,读者不会混淆。1、人工智能,是人们最常听到的说法。在不需要严谨表达时,一般都可以用这个词来表达一些不同以往的“智能”应用。而实际上,程序员写的每一行逻辑代码都是“人工智能”,每一个软件都饱含了“人工智能”,不是“人类智能”。如果要严谨的表达,“人工智能”和“软件”并没有什么区别,也不表达什么意义深刻的“智能“革新产品。但如果遵循普适的理解,那么“人工智能”一定是得有一些新奇的、超越以往的“人工智能”的东西,才能配得上这个词。比如,以往计算器(注意,不是计算机,是加减乘除的计算器)刚出现时,它就是新奇的事物,超越了人类的认知。在那个时刻,“计算器”就代表了“人工智能”的最高水平,是当之无愧的“人工智能”产品。那么,什么时候适合用“人工智能”这个词汇呢?如果别人在用这个词汇说明什么,那就跟着用就好了,不必过于严谨。如果觉得有什么超越以往的“智能”的事物,那就用“人工智能”来介绍它。放之当下(2018年),图像中识别出物体、语音中识别出文本、自动驾驶等等就可以称为“人工智能”了(本文也没少用)。但电灯能感应到人后自动点亮,就不足以说是“人工智能”了。2、机器学习。这是专业词汇,表达的是具有“学习“能力的软硬件产品,与程序员写就的代码相区别。可以认为,机器学习模型是一个函数,有输入输出,它的逻辑是数据驱动的,核心逻辑在数据中,不在代码中。机器学习的“学习过程“,如果也用函数来类比,那么就是首先给模型传入输入,获得输出。然后将模型的输出与期望的输出(即样本数据中的标记结果)进行比较。并根据比较结果来更新模型中的数据,以便下一次的模型输出能够与期望结果更接近。这个过程,和人学习时的题海战术很类似。由此看出,机器学习的学习过程是机器直接学习规律,改进数据,逐渐形成逻辑。而不是先有人类学习规律后,再写成代码。故称之为“机器学习”。3、深度学习。这是机器学习的子领域,但带来了非常大的变革,因此成为了流行的词汇。从字面上解释,所谓深度学习,即在机器学习时,数据组织成了多层次的、有“深度”的网络。传统成功的机器学习算法一般是三层,而深度学习能够实现多达上千层的网络。层次越多,可以认为机器学习模型就能越“聪明”,越有“智能”。深度学习成功的解决了大量和人类认知相关的问题,如:图像中识别物体、物体位置、人脸,语音中更精确的识别文字,文字中翻译、理解含义等。一方面,将机器学习模型的效果大大提升,另一方面,反而降低了机器学习模型应用的难度,让更多的人能够参与进来。最近的一次“人工智能“热潮,也是深度学习所带来的。二、功能机器学习平台上最重要的三个功能为:数据处理、建模、部署(也可称为推理)。每一个都可自成体系,成为一个独立的平台。本章从功能角度来描述机器学习平台,给读者以完整的认识。在不同的使用场景下,只需要部分功能,可删可减,不需要面面俱到。比如,采用预构建的人工智能云服务时,在建模、部署上并不需要投入,主要精力会在数据处理上。再比如,对于以研究为主的团队来说,利用公开数据集进行模型评估等工作,不需要数据处理,也不需要部署。甚至对于个人研究者,强大的平台也不是必须的,手工作坊就能满足需求。 再比如,团队需要比较强大的平台。虽然说工欲善其事必先利其器,但是,常常是业务需求生死攸关的情况,相比之下提升平台从而提高生产力的工作,还没到不做不可的时候。这时候,平台的功能如果不能产生立竿见影、显著的成效,可以缓一缓,先实现投入产出较高的功能,待以后再增量开发或重构。如何很好的平衡开发投入,是艺术也是持续的话题,这里就不展开了。在建设平台时,要注意合理利用现有的成果。比如,一些在发展初期的平台,其实已经解决了核心需求,可以直接拿来用。还有一些大数据平台,通过改造也能很好的解决机器学习的计算问题。再不济,多使用开源的小模块,在系统中减少一些重复开发的工作。总之,平台对服务稳定性、时效性、生产力、成本等各方面有很大的价值,但建设平台不是一朝一夕的事情,也没有一个平台能满足各种需求。比如关系型数据库经过了多年的成熟发展,除了流行的几种数据库外,也不断的有满足新需求的新数据库出现。如,理论容量无限的分布式关系数据库,还有不少大企业根据自己的应用情况开发的高性能数据库系统,以及兴起的各种NoSQL数据库。在使用数据库时,不少团队会组合多种数据库来满足需求。在建设机器学习平台时也如此,除了用已有的平台外,可能还需要自己搭建一些周边的支持系统。数据机器学习的本质即通过数据来理解信息,掌握知识。因此,数据是机器学习的知识来源,没有数据,计算机就无处学习知识,巧妇难做无米之炊。绝大部分机器学习系统需要样本数据,并从而进行学习。对于Alphago这样的强化学习系统,数据全部从规则中生成,则不需要外部的数据。自动驾驶虽然也涉及到强化学习的部分,却需要与实际环境交互的数据,数据的收集难度就更高了。人类文明早期就开始了数据的利用。结绳记事的信息中,就有相当一部分是产量等数据信息。前些年流行的大数据系统更是将数据的作用进一步发挥出来,并产生了丰富、成熟的分布式存储系统、数据加工流程、数据采集等平台和工具。机器学习平台可直接重用这些大数据平台中的工具。数据标记是机器学习特有的数据需求,数据标记就是在数据上加上人类知识,形成样本数据的过程。数据的建设上要根据需求来定。如,强化学习不需要数据采集系统;小数据量的业务也不需要强大的分布式存储系统;企业数据已经有了强大的数据加工能力,尽量不要再建立新的数据加工流水线。数据采集数据采集,即将系统外部的数据导入到机器学习平台中。包括企业内部的数据导入,企业间的数据交换,以及通过网络爬虫抓取数据等。对接对于企业内部的机器学习应用,可充分挖掘内部数据的潜力。内部数据一般已有较成熟的数据解决方案,可尽量借用现有功能,尽量通过已有的功能来做数据加工、整理。安全对于跨数据中心、跨安全域的数据传输,要注意保护接口安全。不能假设外部人员不知道接口地址、规范,就能幸运的一直安全下去。要通过威胁建模来分析数据接口的受攻击面,找出解决方案,从而减小数据的泄露风险。不仅要对数据传输通道加密,增加认证、授权功能,还要从流程上保证密钥等关键数据的安全性,发生泄露后还要有预案能快速、平稳的更新密钥。网络瓶颈跨数据中心传输大量数据时,网络通常是瓶颈。遇到瓶颈时,除了增加投入,提升带宽外,还可以从技术方面进行优化。首先看看传输的数据是否还有压缩空间,其次可重新审查一下传输的数据是否都是需要的,有没有可以去掉的部分。在有的系统中,某些数据的实时性要求不强,可以在收集数据方进行细粒度的缓存,减少数据的重复传输。除此之外,还要注意数据传输过程是否会占用大量带宽,对双方的其它业务系统的网络延迟,带宽等产生负面影响。这时,可考虑错峰传输数据,在非业务高峰期进行数据传输,或者隔离数据采集的带宽。网络爬虫网络爬虫,即从互联网获取网页,并从中抽取信息。随着互联网的发展,网络中的数据越来越多,网络爬虫也得以发展成熟。在使用网络爬虫抽取数据时,要注意遵守相关的法律法规,互联网协议,以及道德规范。如果被抓取方没有提供专门的数据接口,网络爬虫的数据采集效率会相对较低,且易受页面改动的影响,抓取失败。这种情况下,首先要通过人工分析确定目标网站的页面级联关系,页面结构等信息。然后来制定爬取逻辑,并抽取出需要的数据。在爬取网页时,如果爬取速度过快,有可能会影响到目标网站的正常访问。不仅影响目标网站的业务,也会影响数据爬取过程。所以要规划好一定的爬取速度。同时,应该有警报机制,对抓取中的异常预警,及时改进爬虫。有的网站信息量较大,前来爬取数据的也较多,通常会有一定的反爬虫策略。反爬虫策略主要分成两个阶段:检测。有的网站会检查请求的数据格式,可以发现明显是网络爬虫的情况。还有的网站会对每个IP和cookie等的访问频率做分钟、小时级统计,超过一定阈值后,即视为网络爬虫。对于流行的搜索引擎,还会采用机器学习,对网络爬虫的行为建模,能更有效的检测到网络爬虫。应对。检测到网络爬虫后,接下来就会采取不同的应对策略。简单的直接封禁IP或网段,但一般会过期解禁。还有的会采用图片验证码等方式,确认是人类还是爬虫在访问,从而决定是否展示关键数据。还有的会对网络爬虫返回缓存的假数据,来影响爬虫结果。还有的情况,并不检测是否是网络爬虫,直接对关键数据进行保护。如电子商务网站将商品价格用图片显示,增加数据分析的难度。还有的采用自定义字体,将ascii编码映射到不同的字符上。另外,有的网站会建立白名单,允许一部分合作伙伴采集数据,白名单内的网站会采取不同的检测、应对方法,甚至不做检测。因此,网络爬虫在设计中要考虑以上因素,确定合理的爬取策略。如通过代理服务器来更换IP。严格模仿网站请求发送数据,或通过浏览器内核来生成数据请求。利用机器学习或人工服务,来识别图片中的信息等等。网络爬虫应用已经非常广泛了,可以评估一下已有的软件,找到适合自己的方案。隐私保护保护客户隐私是企业数据处理时不可忽视的一环,每次将数据传输到新环境都会增加数据泄露的风险。此时,一方面应仔细分析新系统的安全性,另一方面要避免将与建模无关的数据采集到机器学习平台中。如果机器学习过程需要一些隐私数据,如地址、电话等,应进行脱敏处理。从而减小数据泄露后产生的影响。不少机器学习推理应用中,能获得用户的真实数据。使用真实数据,能够弥补测试数据集和真实数据集的差异,训练出更精准的模型。与此同时,要让用户充分知情,注意保护用户隐私,不要滥用数据。数据存储机器学习平台的整个流程中几乎都会产生数据。除了采集阶段的原始数据外,还有加工过的中间数据,训练好的模型,用户数据等等。对各类数据要考虑不同的需求,选择不同的数据存储方案。数据存储的方案很多,在速度、容量、可靠性等各方面的性能上都能做到很高的水准。但每项指标的提升,都意味着成本的增加。在数据存储方案上,不能盲目追求性能,要量力而行。选择对的,而不是贵的。可靠性可靠性即数据是否能在极端情况下正常使用,不会丢失。实际上,任何系统都不能保证在任何极端情况下都能正常使用。只能通过不断的演进来避免常见问题,从而搭建出可靠性不断逼近100%的可靠系统。提升可靠性的常见方法,即进行数据冗余,对整个系统都避免单点依赖产生的风险。从网络、电力来源、主机、硬盘的各个方面都保证一个设备坏了之后,不影响整个服务。对硬件上的冗余方法不再详述,此处只讨论软件解决方案。软件上的可靠性,其实主要依赖于数据的复制备份来实现。说起来简单,但具体的实现上却比较复杂。根据保障的场景不同,主要分为两种情况:高可用。即出现问题后,系统只允许丢失秒级的数据,而且要在数秒之内恢复。由于要求响应时间很快,所以高可用一般都在同一个数据中心,甚至相邻的机架上实现。数据会在不同的服务器间进行高速复制,及时保证有两三份或者更多的数据存在,并且几秒钟就检测一下服务器的可访问性,随时准备将不可达服务器从集群中剥离。高可用一般用于解决机器的故障问题。虽然一台服务器故障的故障概率比较低,但在机房中成千上万台服务器时,几乎每天都会有服务器坏掉。通过高可用集群,偶发的服务器物理损坏几乎不影响系统的使用。灾备。即出现灾难性问题后,系统仅丢失分钟级的数据,且要在十分钟或一小时内恢复服务。常见的灾难性问题包括地震、强烈天气、火灾等极端事件。由于要防止地震等地区性问题,灾备需要两个数据存储位置相距800公里以上。为了保证灾备的效果,灾备的数据中心之间也会进行持续的数据复制。为了提高资源的利用率,进行灾备的数据中心可能也要承担业务工作,数据中心之间的数据同步传输是双向的。这样虽然增加了系统设计上的复杂性,但可提高资源的利用效率。分布式文件系统是比较流行的保障数据存储可靠性的方案。有的分布式文件系统不仅能提供高可用,还能提供灾备的解决方案。一致性一致性即数据从各个方面来看,信息都是一致的。可分为两个层面来讨论:数据备份。在提高可靠性的分布式系统中,每份数据都有两到三份。系统在不断的保持各份数据都是一致的。当服务器出现故障时,就有可能出现几份数据不一致的情况。这时候,可取其中的某一份作为主数据,从这份数据来重新创建备份。这样有一定的概率会丢失数据,但保证了数据的一致性。在出现这种情况时,要以一致性优先,无论如何都要保证分布式系统备份数据间的一致,否则潜在的问题会更多。冗余信息。在不少系统中,因为性能、历史遗留问题等原因,会存在一定的数据冗余。一般会在数据更新时逐步刷新这些信息。当数据正在更新时出了问题,就有可能产生数据的不一致。传统的关系型数据库通过事务来处理这类问题,将前面更新的一半内容恢复还原。如果系统中不支持事务,或在分布式业务系统中,就需要设计一个可靠的同步机制来实现类似的恢复还原的功能。比如,银行系统中要完成跨行转账,需要从一个账户中扣除金额,再给另一个账户增加金额。其中涉及到的系统包括双方银行,中间的支付系统,甚至更多的系统。其中要通过多种同步、超时、重试的机制来保障账户金额的一致性,并尽量保证交易的成功进行。又比如,系统中可能会有些缓存信息。当原始信息刷新后,依赖于业务需求,缓存信息也需要一定的更新策略,要么即时刷新,要么定时刷新。要保持数据的一致性就会牺牲访问速度,不是所有的数据都需要一致性。这要根据业务的形态来决定。一般来说,如果业务没有要求,就尽量保证访问速度,提高系统响应能力。访问速度根据业务的不同,有的数据可能经常写入很少读取,有的则相反。还有的数据存取的时候需要越快越好,而有的数据则对速度不敏感。如,备份数据几乎都是写入,且对读取时的延迟不敏感。而机器学习中的学习参数则需要在内存中保障最快的读取速度。当机器学习模型在推理应用时,对于模型只有读取需求,不需要写入。访问速度和一致性、可靠性的需求都有所冲突,鱼和熊掌不能兼得。数据访问从CPU寄存器、缓存到内存、网络、磁盘、物理距离,数据延迟的数量级逐步增加。(见下表)同时,每单位容量的价格也随之快速下降。在实际应用中,数据库、分布式文件系统等,由于要处理更多的通用问题,或数据规模较大,同时要保证可靠性、一致性,并不能达到上面的理论速度。通常会慢一个或数个数量级。传输数据用的光纤,达不到理论光速。另外,访问速度应该从整个系统来计算,有时存储本身并不是瓶颈。比如通过各个组件的运算,最终需要两秒钟才能返回结果。可以看出,通过存储也很难提升其速度。只能从算法、流程上来提升,如增加缓存等(这也意味着产生一致性问题)。版本控制版本控制,即哪个版本的数据会被使用。在业务数据处理时,会经常产生新的数据。在多种场景下都需要对新数据进行版本控制。在新数据写入完毕前,不应该可读取,否则会读取到部分数据。在写入完成后,应尽快允许读取,这样能够拿到最新的结果。如果有缓存时,在数据更新后,可能需要触发缓存同步更新。在新数据中发现问题时,在某些场景下,应该允许切换回旧数据。这部分功能和业务结合紧密,因此通常需要自己实现相关功能。数据加工数据加工通常可称为ETL(Extract, Transform,Load),即将数据导出、转换、加载(可称为保存)。总之,数据加工可以抽象为数据的导入导出和形态的转换,描述了数据加工的通用流程。成熟的ETL系统会提供大量的组件适配不同的数据源,以及丰富的数据变换操作,并解决了很多稳定性相关的问题。使用这样的ETL系统,能够达到事半功倍的效果。在数据加工过程中,有可能需要多次ETL的数据加工才能得到最终需要的数据。这时,系统需要在完成前置的ETL任务后,触发下一轮的ETL任务。要考虑到上一节中提到的数据一致性、可靠性造成的延迟,防止下一轮ETL没有在最新的数据上运行,或因为找不到数据而出现错误。导入导出导入导出,即将数据从各种异构的数据源中导出,并导入到另一个目的数据源中。导入导出的主要功能是支持关系型数据库、NoSQL数据库、json、csv、内存、webapi、程序结构体等各种形式的接口,能将数据无缝的在各种数据源中导入导出。各种数据源之间的数据结构并不一定能一一映射,如有的NoSQL能够存储树形结构的数据,而关系型数据库只能存储二维的数据表。在导入导出过程中,可以对数据进行过滤、映射,可以选择出部分数据列,也可以增加查询条件,选择出部分数据条目。在导入时可以将不同名的数据列进行映射改名。转换在数据平台中,数据转换是核心功能之一。常用的SQL、MapReduce等都是对数据的转换。数据转换可以串联起来,对数据进行多次处理。也可以并联起来,将两个数据源合并,或一份数据输出两种形态的数据。将数据转换从导入导出中单独抽象出来,不必关心源数据存放在哪里,支持什么样的操作。所有的操作都会在ETL的转换过程中完成。大大方便了异构数据的处理,在数据需求非常多、数据源复杂的场景下,对效率提升非常高。但是,通过通用的转换流程处理数据,就不能使用数据源里原生的数据处理过程,在某些场景下数据转换效率会大幅降低。通过组合不同的数据转换方式,能完成绝大部分的数据转换操作。在一些通用数据转换组件无法完成操作,或者效率太低的情况下,可以实现接口写出专用转换组件。数据转换的主要种类如下。单条数据内的变化。树形结构的调整。这种转换可以进行移动、复制、增加、删除树形结构内的节点,以及改名等操作。还可以将树形结构扁平化为只有一层节点。这样变化后,可以将键值对导入到关系型数据库中。机器学习中,大部分数据也是像关系型数据库一样的键值对,而不是树形结构。字段计算。在机器学习中,要对NULL值赋值,或者通过正则表达式来抽取出信息。这些都通过修改字段值,或增加新的计算字段来实现。数据打包拆包。在数据处理流程中,有些操作通过批处理效率会更高。这时需要将多条数据打包成一条,以便后面的流程可以进行批量操作。完成批量操作后,再拆成多条数据。分拆单条数据。树形结构中包含的数组,如果要在接下来的数据中单独处理则需要将数组分拆成多条数据。分拆后的数据,还可能需要包含原始数据的某些字段。聚合多条数据。聚合可以统计唯一键、进行求和等操作。如果聚合的数据量非常大,有可能需要外存来缓存。跨语言互操作。有时候ETL工具本身的语言和用户使用的目标语言不一致。可通过互操作接口来调用用户代码,如通过网络接口序列化数据。这样用户可以用目标语言写出业务相关的转换组件,集成到ETL工具中。数据流的合并与分支。如果多种数据需要合并,则可通过数据流的合并操作来进行。数据流合并,从独立的数据导出操作开始,在某一步中合并起来。或者在数据转换过程中,根据主键查询出新的数据后,直接合并。如果数据需要同时输出两种格式,如原始数据与聚合内容都需要输出,则可以在中间对数据进行分支,通过两条流水线来分别处理。定制化。如果原生的转换组件不足以处理复杂的业务逻辑,或者为了提高处理效率。可以实现转换组件的接口实现定制的逻辑。校验与清洗大数据中异常数据是常见问题,大数据通常只进行一些聚合操作,少量的异常数据可以忽略不计。在机器学习应用中,所有数据都会影响模型的计算结果,在某些算法下,异常数据会对模型效果有非常大的影响。所以要对数据进行校验和修复,将异常数据在数据处理过程中过滤掉。对于常见的数据异常,要从数据源头调研,检查数据来源上是否有缺陷,并进行修复。数据清洗是对数据进行修正。有些数据虽然不属于数据异常,但不利于机器学习。要么将数据标准化,要么忽略掉这类数据。比如,数值列中的空值,可能需要替换为零;在自然语言处理中,如果文本只有一两个字,基本没法生成有意义的模型,可直接忽略掉整条数据;有些空白字段可以训练一个小的机器学习模型来预测、填充,转换成正常数据。可视化平台的核心目标是提高生产力。如果将提高生产力分解开来,其中很重要的方法就是降低学习成本,节约日常操作时间。可视化是降低学习成本,节约操作时间的重要方法。数据处理过程可以完全是代码,也可以用json文件一样的配置方法来实现。使用配置来实现,就能很容易的接入各种系统,实现可视化。可视化后,一些数据的导入导出工作还可以交给非开发人员使用,节约沟通交流的时间。这也会让新人更容易上手,更快形成生产力。数据的可视化也能让人对数据处理过程有直观的感觉,甚至能发现一些数据处理中的错误。样本数据样本数据包含了数据以及从数据中期望得到的知识,也称为标记数据。有监督学习(supervisedlearning)必须有样本数据才能训练出模型,从而将知识应用到新数据中。虽然从机器学习的分类上来看还有无监督学习、强化学习等不需要样本数据的场景。但有监督学习的应用更为广泛。数据标记的过程是机器学习中,将人类知识赋予到数据上的过程。有了好的标记数据,才能训练出好的模型。如果标记数据的质量不高,会直接影响到训练出的模型质量。比如,人类的标记数据正确率在90%,那么模型的理论最好成绩也不会高于90%。标记后的样本数据是团队独有的财富。它的价值不仅在于标记人员投入的时间、经济成本,也在于独有的样本数据带来的模型效果的提升。模型丢了可以重新训练,样本数据丢了,重新标记的时间成本和机会成本可能无法承受。因此,一方面要做好数据备份等工作,防止数据丢失,另一方面还要做好内部、外部的保密工作,防止样本数据泄露。一般的数据标记方法是让有一定背景知识的人来标记,然后将数据和标记结果及其关联保存起来。训练时,会将数据和标记结果同时输入机器学习模型中,让模型来学习两者间的关系。数据标记方法的复杂程度不一,如:图片分类,即给定一张图片,对其进行分类。标记方法比较简单,给每张图片进行分类即可,标记速度也比较快。如工业应用中经常要检测流水线上是否有次品。可让人来判断成品、次品图片,然后点击相应的分类,或者将图片放入不同分类的文件夹即可。文本分类,即给定一段文本,判断它的分类。如:正面评价、负面评价、不相关评价、中性评价、垃圾广告等。这种数据的标记需要标记者通读文本,并领会其中的含义,也比较简单。在舆情监测领域有广泛的应用。目标检测,即给定一张图片,将其中需要检测的目标标示出来,并进行分类。这种标示方法有两种,一种是用矩形框将目标标示出来;另一种需要将目标的每个像素都标示出来。可以看出,这两种标记方法的工作量都大于前面两种标记任务,标记像素更需要大量的时间才能标记得非常准确。自动驾驶领域,就需要对行人、车辆、道路标识等信息标记其位置。语音识别,即将音频转换为文字。虽然这类数据已经较多,但仍然不能满足需求。如诗词、方言、特定噪音下的数据等都需要人工标记来丰富数据。语音识别应用非常广泛,最常见的就是对视频、音频内容自动生成字幕。 除了人工标记,有些领域可以利用已有数据来形成样本数据。如机器翻译可以利用大量已有的双语翻译资料。但机器翻译一般是逐句翻译,所以还要找出双语句子对。如果需要的应用领域没有足够的双语资料,翻译效果也会受影响。数据使用在机器学习中,数据的使用上也有基本的原则。一般说来,会将数据按比例随机分成三组:训练集、验证集、测试集。训练集用来在训练中调整模型中的参数,使模型能够拟合出最佳效果。通常,训练集在所有数据中占的比例最高,大部分数据都用来进行训练。验证集用来在每轮拟合之后评估拟合效果。有的训练过程会在数据达到了一定的性能后停止训练,这就是用验证集来评估的。通过在验证集上进行推理预测,能够实时的了解模型当前的训练进度。可以了解训练是否在收敛,收敛的速度如何等信息。在有些训练过程中,验证集不是固定的集合。在每轮训练前,将数据随机分配至训练集和验证集。这样,参与模型训练过程的数据规模就更大了。测试集用于最终模型准备发布之前的评估。测试集就像最终考试一样,一般对一个模型只用一次,甚至对于一些训练结果不好的模型不使用。测试集用来解决模型的过拟合问题。过拟合,即模型只能很好的预测训练时使用的数据,对实际数据预测效果会明显打折扣。因此,在模型训练过程中要严格限制测试集的使用次数,否则,它和验证集就没有区别,无法发现过拟合的问题。标记工具标记工具提供了用户界面,帮助数据标记人员高效的标记数据。根据不同的标记任务,标记工具的用户界面会有所不同,但后面的处理流程是相似的。标记工具最重要的任务是提升生产力,让标记人员的时间花在知识推理上,而不是操作工具、等待工具响应中。标记工具的用户界面开发过程并不复杂,开发成本也不高。只要遵循基本的用户体验设计方法,以生产力为目标,就能做出易用、高效的标记工具。因此,几乎每个团队、平台都会开发自己的标记工具。标记工具要紧密结合到整个数据处理过程中,从源数据到标记后数据的存储。除了要做好备份外,标记后的样本数据要融入整个数据流水线,尽快将标记的数据投入到模型训练中,让新数据尽早发挥作用。在线标记用户数据收集的过程从产品设计实现,到后端数据处理的整个流程。有的场景下,用户不仅会提供数据,还会提供数据标记,从而能够减少标记数据上的投入。将这些标记数据及时补充到样本数据中,随着用户的使用,产品就会越来越好。如,在搜索引擎中,排名第一的结果是模型计算出的第一个结果,接下来的结果相关性依次降低。而用户第一次点击的结果,则是用户标记的相关度最高的结果。因此,可以将用户的搜索关键词、第一次点击的结果保存下来作为样本数据,用于模型的下一轮训练。通过不断的迭代训练,用户就会感觉到搜索引擎越来越聪明,排在前面的结果就有自己想要的。外包标记工作比较枯燥,如果长时间进行数据标记工作,人会感到厌烦。如果需要的样本数据量很大,团队人手不够,或者要解决其它更重要的事情,又或者标记数据的工作不是持续性的,没必要组建自己的标记团队。这时,外包数据标记工作是个很好的选项。随着机器学习的火热,数据标记工作已经逐步标准化。一些被称为“数字富士康”的公司,有成百上千人的团队专门进行数据标记工作,还有流程来保证标记质量的稳定。外包数据标记工作时,最重要的是要保证标记结果的质量。虽然成熟的外包团队能够很好的控制质量,但对于业务相关的知识可能并不熟悉。需要团队悉心教导、传授知识,才能保证数据标记结果的质量。众包众包也是一种将工作外包的方式,不同点在于是将任务直接包给个人,而不是公司。个人与项目的耦合也非常松散,完全按照工作量计费。众包的优势在于经济成本低廉,一旦运营好了,横向扩充标记速度非常容易。有一些公益性质的众包项目,人们甚至愿意免费参与。众包的技巧在于管理。通过合理的架构、结果评估以及激励方式,就能得出好的结果。比如,ImageNet是一个众包的图片数据集,标注了1400万张图片的两万多个分类,以及超过100万张图片的边框。基于ImageNet的大赛让图像认知领域得到了长足的发展,对深度学习的发展起到了至关重要的推动作用。一些数据标注的众包平台可以直接使用,能够节省众包的管理成本和风险。但是,平台的质量参差不齐,需要认真的评估。其它方法如果实在找不到更多标记数据,可以通过一些机器学习中独特的方法来增加数据。这些方法虽然效果有限,但也有一定的价值。数据增广(Data Augmentation)。主要应用于图像领域,可通过往图片中增加噪点数据,翻转、小角度旋转、平移、缩放等方法将一张图片变为多张图片。从而增加样本数据总量。迁移学习(Transfer Learning)。可用于深度学习的多层网络中。在其它某个有丰富样本数据的领域先训练出较高质量的模型。然后将其输出端的一些隐藏层用本领域的样本数据重新训练,则可以得出较好的效果。一些图片分类的云服务,有的通过这种方法来基于用户上传的少量图片进行学习、分类,得到性能不错的模型。半监督学习(Semi-supervised Learning)、领域自适应(Domain Adaptation)等其它方法。建模建模,也被称为训练(Training)模型。包括了两个主要部分,一是数据科学家进行试验,找到解决问题的最佳方案,本节称之为模型试验;二是计算机训练模型的过程,本节在平台支持中介绍。建模是数据科学家的核心工作之一。建模过程涉及到很多数据工作,称为特征工程,主要是调整、转换数据。数据科学家的主要任务是要让数据发挥出最大的价值,解决业务需求,或发现未知的问题,从而提升业务。建设机器学习平台时,要对数据科学家的工作有一定理解,才能建设出真正能帮助数据科学家的平台。模型试验特征工程与超参调整是建模过程中的核心工作。特征工程是指对数据进行预处理,使处理后输入模型的数据能更好的表达信息,并提升输出结果的质量。特征工程是机器学习中非常重要的一环。数据和特征工程决定了模型质量的上限,而算法和超参只是逼近了这个上限。超参调整包括选择算法、网络结构、初始参数等工作。这些工作不仅需要丰富的经验,也需要不断地试验来测试效果。特征工程与超参调整不是独立的过程。做完特征工程后,即要开始通过超参的组合来试验模型的效果。如果结果不够理想,就要从特征工程、超参两方面来思索、改进,经过一次次迭代,才能达到理想的效果。 特征工程特征工程的内涵非常丰富,在输入模型前的所有数据处理过程都可以归到特征工程的范畴。通过特征工程,将人的知识(通常称为先验知识)加入到数据中,并降维来减小计算规模,从而提高模型性能和计算效率。特征工程的数据处理过程大部分都可以在数据转换阶段进行。可以将常见的特征工程的处理函数抽象为数据转换组件,方便重用。特征工程的主要内容有:数据清洗。在数据部分已介绍过,即处理异常数据,或对数据进行修正。纠正数据偏离(bias)。在一些应用中,虽然有大量的数据,但数据分布并不均衡。如,在流水线检测缺陷产品的场景下,如果缺陷率为千分之一,那么原始数据里99.9%都是正常产品的图片,只有0.1%是缺陷数据。这种情况下,训练出来的模型质量不会太高。还有些情况下,数据偏离很难被发现。比如,互联网新闻图片下训练的分类模型,在用于手机摄像头照片分类时,效果会差很多。原因是新闻图片通常选择了颜色饱满丰富,构图优美的图片,和用户自己拍摄照片的色彩构图上有不小的差异。这种情况下,一旦发现了数据偏离就要从数据和算法两方面来调整。有的机器学习算法能够自动将小量数据的作用放大。但大部分情况下,需要人工对数据比重进行调整,并引入接近真实情况的数据集。数值变换。机器学习需要的数据都是数值,如果是浮点数,一般要调整到0\~1之间。枚举值要拆成一组布尔值。不同来源的数据还要做计量单位的统一。输入先验知识。即根据人的判断,将一些数据中难以直接学习到的信息抽取成特征,从而减小学习难度。如,自然语言的评价分类问题中,可以建词表,将正面词语和负面词语总结出来,对每句话的正负面词计数,并作为单独的特征列。这样对正面负面的分类判断会有较大的帮助。虽然数据维度会增加,但模型有了更多的人类先验知识,会判断得更准确。数据降维。是在尽量不丢失信息的情况下,减小单条数据的大小,从而减小计算量。比较直观的降维方式就是把图片缩小到一定的尺寸,减小像素数量。深度学习是在多层网络里,保留信息的同时,逐步给数据降维。机器学习算法中的主成分分析(PrimaryComponent Analysis, PCA)和线性判别分析(Linear DiscriminantAnalysis,LDA)是经典的降维方法,用线代的矩阵特征分解的算法,找出数据中区分度最大的特征,然后省略一些作用不大的特征。超参调整在机器学习中,模型能够自己学习改变的权重等数据,叫做参数。而不能通过机器学习改变,需要提前人为指定的参数叫做超参数(hyperparameter,简称超参)。这些参数会直接影响最终模型的效果,超参调整过程非常依靠数据科学家的经验。常见的超参如下:算法与网络结构。机器学习从经典算法到最近流行的深度学习算法,以及各种各样的深度学习网络结构。虽然每个算法的适用范围比较清晰,但同一个问题仍然有很多可选的算法来实现。每个算法还会有自己独有的超参需要调整。还有的算法还能改动网络深度,每层神经元数量,或者采用不同的方法来进行非线性化,误差传递,提高泛化性能。算法中能调整的超参非常多,有些超参值也是连续的。所有超参的排列组合数量可认为是无限的。批数据量大小,即每批数据的数量。如,每次取100条数据的平均值来更新模型。批数据量太小,会造成每批数据缺乏代表性,模型结果收敛时波动较大。而批数据量过大,会减少一些偏差很大特征的差异,无法学习到一些细节的信息。学习率,即每次迭代时对数值的修改幅度。早期的机器学习方法都是固定的学习率,调整起来比较复杂。现在一般都是动态的学习率,但仍然要调整一些学习幅度、用什么算法等。学习率过大,会造成结果震荡,无法很好的收敛。而学习率过小,会让收敛很慢,浪费计算资源。每组超参的效果,需要通过一定时间的训练之后才知道。因此,超参的调整过程非常耗时,数据科学家需要通过超参的各种组合来研究它们之间的关系,找到较好的搭配。平台可通过可视化界面,提交一组超参配置,并对数据结果进行比较,提高调参效率。自动化建模自动化建模(AutoML),主要是通过对超参的自动化选择,来提升建模工作的效率。超参调整问题,实际上是研究什么样的超参组合下会得到最好的模型效果。对某个具体问题,需要试验的超参组合可能有无穷多种。数据科学家其实就是在这个组合空间里找到效果较好的组合。通过自动化建模的方式,可部分、甚至全自动的寻找超参组合。寻找超参的方法包括穷尽法,随机搜索,通过概率模型发现超参关联,遗传算法,梯度优化,或组合前述方法等等。有的自动化建模方法可完全自动。只需要输入数据,即可探索从算法到网络结构等所有超参的组合。这种方法不需要有任何机器学习经验,但搜索过程较长,也用不上人的一些先验知识。有的自动化建模方法是半自动的。除了输入数据,还需要输入一些算法、参数限定的组合,即可自动找出这些组合中的较佳组合。这种方法需要一定的机器学习建模经验,但搜索时间较上者短,同时能结合人的先验知识,在较小的组合范围内搜索。组合模型在实际应用中,经常组合多个模型,以及规则代码才能完成整个功能。比如,在手写体识别时,需要先用目标检测模型将文本部分找出来,然后分割字符,最后通过分类模型识别单个字符。实际上,还会涉及到文本旋转、连笔等问题。这样的应用中,不能依靠一个单一的模型,输入了图片就期待这个强大的模型输出结果,而需要多个模型再加上一些算法才能得出结果。因此,机器学习平台需要有流水线的处理能力来支持模型、规则组合的应用。在建模阶段,多个模型可以分拆来处理,但在部署后的应用推理阶段,一般需要近实时的输出结果。有些情况下,组合模型与通用规则还不够,还需要对推理结果进行基于数据的特殊规则。如,在搜索引擎中,当结果中含有某些关键词时,会增加或降低其权重,从而达到人工调整搜索结果的效果。总之,模型试验时,不应考虑依靠一个模型就能达到最好的效果。要灵活的组合模型、自动规则以及人工调整的方法来获得最终的效果。平台支持模型实验阶段对平台支持有较多要求。好的工具能够提高生产力,减少人为错误,还能充分利用资源,让算力尽可能产生价值。平台除了本节提到的功能外,还需要扩展能力、运维支持、易用性、安全性等方面的功能,这些与部署后的平台功能有所重叠,在随后的章节中集中讨论。算力管理机器学习平台一般是计算密集型的平台。管理好算力才能提高生产力,节约成本。算力管理的基本思路是将所有计算资源集中起来,按需分配,让资源使用率尽量接近100%。 算力管理几乎对任何规模的资源都是有价值的。比如,一个用户,只有一个计算节点(如一块GPU)有多条计算任务时,算力管理通过队列可减少任务轮换间的空闲时间,比手工启动每条计算任务要高效很多。多计算节点的情况,算力管理能自动规划任务和节点的分配,让计算节点尽量都在使用中,而不需要人为规划资源,并启动任务。多用户的情况下,算力管理可以根据负载情况,合理利用其它用户的空闲资源。在节点数量上百甚至上千时,运维管理也是必不可少的功能。虚拟化。是将实际物理资源与运行时的逻辑物理资源进行隔离的技术。资源虚拟化后,能将一台性能很高的计算机拆分给多个不需要整台机器算力的任务使用,互不干扰。这样,在搭建机器学习平台的时候,按照性价比或最高性能的要求进行硬件采购即可,不必考虑实际任务的资源使用规模。在使用时,可灵活适配低资源要求的任务,而不会造成资源的闲置。Docker是目前比较流行的操作系统级的虚拟化方案,启动速度很快,还能将模型直接发布到部署环境。任务队列。任务队列的管理是算力管理中的重要方法。在一些小型团队中,成员们独自使用服务器,算力不能得到很好的规划。如果将计算机集中起来,通过提交任务的方式来申请计算资源,运行训练任务。一旦任务完成后就释放计算资源,这样能达到最高的使用效率。在计算任务较多时,还涉及到优化资源分配的问题。可按照比例为不同团队保障一定的资源。如,给团队A分配50%的资源,如果团队A没有使用到50%的资源,则空闲资源可分配给其它团队使用。若团队A有了更多的任务后,可立刻将其它团队的任务终止,或等任务完成后将团队A的任务优先执行。也可以按照任务优先级调度,保障某类任务的优先执行。训练任务在终止时,如果使用的框架支持保存点,下次启动时可以从保存点接着运行。如果不支持保存点,就要从头运行,这时候,终止任务会损失一定的算力。任务队列在分配资源时,还要注意到大小资源任务的抢占问题。如,某任务需要多个GPU来运行,但其它任务都只需要1个GPU运行。如果要保证100%的计算资源利用率,势必在每次释放出1个GPU后,立刻指派下一个只需要1个GPU的任务。这样会造成需要多个GPU的任务很难获得足够的资源开始执行。平台对物理资源的调度方式要能够解决这种问题,将某台满足条件计算机的空闲资源留存下来不分配,保障多GPU的任务的运行。代码集成。如果试验的模型输入输出比较固定后,可考虑进一步提高效率。在代码、配置提交后立刻执行配置好的训练任务,不需要手工配置任务模板并提交。代码集成带来的不仅是执行速度的提高,对于同样的输入输出更容易横向比较历史数据,获得更多的信息。分布式训练。一些训练任务花费的时间很长,即使最高性能的单台设备也无法快速完成训练,这时需要分布式训练来进一步提升速度。分布式训练通常由机器学习框架来具体实现。平台要做的是保留足够的资源,并通过参数等方式将IP、端口等信息传入到代码中,然后启动分布式训练任务。批量任务。在试验超参的过程中,经常需要对一组参数组合进行试验。批量提交任务能节约使用者时间。平台也可以将这组结果直接进行比较,提供更友好的界面。交互试验体验。在脚本开发中,不少用户习惯于在交互式工具中进行试验、开发,如:JupyterNotebook。交互式开发可以对某一段代码提供所见即所得的交互式体验,对调试代码的过程非常方便。虽然任务调度的方法对平台算力的利用率是最高的,但交互式开发在代码变化很大的时候,会提高人的开发效率。在交互试验的场景下,需要独占计算资源。机器学习平台需要提供能为用户保留计算资源的功能。如果计算资源有限,可对每个用户申请的计算资源总量进行限制,并设定超时时间。例如,若一周内用户没有申请延长时间,就收回保留资源。在收回资源后,可继续保留用户的数据。重新申请资源后,能够还原上次的工作内容。在小团队中,虽然每人保留一台机器自己决定如何使用更方便,但是用机器学习平台来统一管理,资源的利用率可以更高。团队可以聚焦于解决业务问题,不必处理计算机的操作系统、硬件等出现的与业务无关的问题。快速试验平台要通过标准化数据接口来提高试验的速度,也能横向比较试验的结果。如果团队的试验比较多,需要经常研究公开数据集、算法实现等。可以根据开源数据集来实现统一的数据处理接口,这样可以加快算法与数据集的对接,方便组合不同的数据集和算法。数据接口标准化后,可以通过在新算法上运行多个公开数据,或在新数据集上运行经典的算法,从而对新算法、新数据集进行自动的试验和评估。另外,考虑到数据的保密或减小计算规模,可以保留好几份不同规模生产数据。小数据集用于本地调试代码,中等规模的数据集用于评估模型效果,大规模数据集用于正式训练模型。当某种候选算法准备好后,能够快速切换到生产数据进行评估。可视化可视化也是机器学习领域的热点问题。包括结果分布、训练进度、训练效果对比等各方面的可视化。呈现出好的可视化效果,能让人直观的获得信息和经验,更容易理解信息。数据分布。经典的机器学习方法可以选择最主要的两三个特征,通过二维或三维坐标来展示数据的分布。通过数据分布的可视化,可以让人了解到数据聚类的效果、直接的看到数据的规律。训练进度。通过在模型中埋点(Telemetry),能够将模型的错误率、进度等信息保存下来。再绘制出图表,就能直观的看到模型错误率的收敛速度等信息。还可以对比多个模型的训练历史,对模型的效果和收敛速度的关系有更深的认识。可解释性。在深度学习模型中,可解释性是研究的重点,即为什么模型会得出这样的推理结果,这个结果受哪些神经元节点影响最大?通过高亮出相关的神经元,并对不同的推理结果、模型进行对比,能洞察出更多信息,帮助更快的调整超参。团队协作如果团队有一定的规模,甚至有多个小团队时,平台需要支持团队协作相关的功能。团队的分工上可能会横向根据工作内容分,如将数据预处理与建模团队分开;也可能根据垂直的模型来划分,如根据应用将调试不同模型的团队分开。平台建设时,要根据需求来决定团队合作相关的功能如何实现。团队协作时,主要需求是沟通和数据交换。沟通可通过邮件、消息平台来进行,本文不再讨论。而交换的数据包括样本数据、代码、脚本、训练好的模型以及一些配置文件等等,这些需要机器学习平台来实现。团队协作时,要防止数据误操作和丢失,并要方便共享、查找。分布式文件系统需要避免硬件问题造成数据意外丢失。另外,还要利用定期备份来防止数据被误删,如果改动比较频繁,可考虑每日备份。如果备份数据量较大,可通过差异备份来节约空间。有了数据备份,还要提供方便的搜索功能。除了要能够按照日期、相关人等方式来搜索外,按照机器学习模型对应的数据版本、效果、配置等来查找也是很重要的搜索方式。如果是代码、脚本等文本内容,可利用源代码管理工具获得更强的版本管理功能。有了统一的存储和搜索功能,团队能方便的共享资源。上下游团队约定好具体的流程后,可以大大加快诊断问题,协作的速度,就能将各自最新的成果尽快集成到业务系统中。部署绝大部分机器学习模型都用于推理预测,即输入数据,机器学习模型给出结果。模型创建好后,还需要部署后进行推理应用,产生价值。部署并不只是将模型复制到线上,还涉及到线上模型的管理等功能。 持续集成持续集成是将新的模型自动的、可控的集成到生产环境的过程。与之相对的是手工集成,即每次有新模型后,需要人工配置,将新模型一次性发布到生产环境中。新模型的集成对于某些业务是非常关键的,需要非常小心的测试、发布。虽然可以在模型的建模试验环节中进行很多试验,但模拟的试验和实际环境不完全一样。如果贸然发布,有可能给业务带来负面冲击。如,电子商务网站有几十万种商品,用户需要搜索才能找到自己想要的商品。如果发布的模型不能找出用户真正想要的商品,业务量会立刻出现显著的下降。在发布业务敏感模型时,通常需要平台能控制到达新模型的用户请求数量,从而观察新模型对业务的影响是正面的还是负面的,再决定是进一步部署新模型,还是撤回模型。在平台的运维体系还不能精确获得业务影响指标时,可以多花一些时间,进行灰度发布,将业务分阶段切换到新模型上。在灰度发布的过程中,逐步验证新模型能够正常使用,响应速度上没有显著下降,对业务没有显著负面影响,最终完成新模型的部署。有时候发布的新模型是和新代码相配合的,如果要回滚新模型,还涉及到将新代码同时回滚。操作上会更复杂,需要强大的运维体系和实践准则来保证整个过程的顺利进行。除了模型的持续集成外,还包括数据的持续集成,即将模型持续的在最新数据集上训练,这样可以响应最新的热点数据。平台要将数据的采集工作打通,能够不断的将新数据集成到生产环境中。另外,在生产环境中要控制好进行数据持续集成所需的资源,避免需求的资源过大,影响整个服务的性能。模型评估对模型效果的精确评估,有助于确定模型是否可以上线,或哪些方面需要继续改进。有些应用可以马上获得用户的标记结果,就能即时评估模型的效果。平台需要将模型的推理结果和用户标记结果组合到一起,汇总出模型的效果数据。平台可提供可视化的界面,帮助部署过程决策。对于不能获得用户标记结果的模型,要寻找评估模型效果的方法,要能近实时的看到模型效果。如,对于商品推荐模型,需要提高用户点击推荐商品的比例。要把点击数量汇总起来,用于评估新的推荐模型的有效性。如果暂不能评定模型效果,可以用A/B测试的方法,让两个模型在线上共存一段时间,随机接受输入。最后再评估通过哪个模型对最终业务绩效的影响更好。除了模型效果外,还需要评估计算资源负载和响应速度。如果模型有了较大的改动,可能会在执行性能上有较大变动。在资源紧张的情况下,如果没有注意到这些因素,可能会因为模型发布而造成服务负载过高,甚至影响会扩展到全线服务上,影响整个业务的稳定。在一些复杂的多模型组合下,特别是有很大的团队在平行开发模型时,就需要更复杂的模型评估方法。如,一些大型搜索引擎,会有多至数十个团队在改进搜索引擎的排序算法,可能每天都有新模型要发布。这时候需要建立一套强大的模型效果验证工具,假设评估中的模型之间没有关联,可以让小部分用户的输入随机使用这些模型的组合。然后通过算法计算出每个模型对结果的影响数据,从而决定模型最终是否能上线。下表为灰度发布比例与评估重点的建议。发布比例 评估重点 系统要求1\~5% 响应速度 模型效果 能够观测到单个请求的延迟情况,或对某类请求能聚合计算。模型效果的评估需要用户标记的支持。数据延迟在分钟级。10\~20% 系统资源负载 能够按服务器进行单台流量切换,从而能够观测到服务器的负载变化情况。50% 对业务的影响 有模型以及整个业务的指标定义和收集方式。特征抽取通过特征工程定义的数据加工流程后,数据才能作为模型的输入。在部署后的模型应用时,生产数据也需要通过一致的特征抽取过程,才能输入到模型中。如果有成熟、高效的数据加工过程,可直接将数据加工过程的代码用于生产环境,与模型一同发布即可。但是,在模型应用时的性能要求比试验更高,或者有可能运行环境不一致。比如,试验时可能用的是Python,而应用时需要C#。这时需要通过一组特征工程的数据测试集,来保证试验时和应用时的特征抽取工程能得出一致结果,从而能够给模型输入同样格式的数据。否则,模型的正确率可能会比试验时低,甚至有明显的差距。实时与批处理在模型应用中,大部分要进行实时处理。如,搜索引擎、图片识别、相关推荐等功能,都需要将结果尽快返回给用户。这对响应速度等各方面要求都较高。而有些应用不必给用户实时返回结果。比如,个性化的商品推荐邮件,可以在每天的访问低峰时段完成计算并发送给用户。在产品设计时,如果实在达不到实时返回的响应速度,可以将一些需要实时返回的结果做到近实时。比如,在一些依赖第三方数据的机票查询功能中,会设计搜索进行中的界面,管理好用户的期望。有些应用,对时间不是特别敏感,可以先行计算并缓存结果,这样也能做到近实时的效果。服务服务即通过web服务开放出的推理应用接口。现代运维体系对于如何提供内部服务有很多的沉淀,有包括虚拟化、容器化、微服务管理等各种工具和设计思想的支撑。本节会结合机器学习进行简单的介绍。负载均衡负载均衡(LoadBalancing)是现代服务的重要概念,即将访问请求通过一组服务器来支持,而不是依靠单台服务器。负载均衡的主要原理是通过中心点来统一提供服务,而真正实现访问请求的节点在中心点上注册,并定期向服务器发送信息,刷新节点健康状态。实际的访问请求有的通过中心点直接转发给节点。有的服务会将节点地址给到访问请求方,由访问请求方再次向节点地址发送请求。通过负载均衡可实现高可用、灾备、A/B测试、灰度发布等各种功能。负载均衡的实现方案也非常多,包括DNS、硬件集群、反向代理、服务发现等等,都是负载均衡的实现方案。理解了负载均衡的原理和多样的实现方式,以及不同方式间的优缺点,就可按需组合一些方法来实现高可用、灰度发布等功能。负载均衡的中心点也需要备份来提供高可用服务,不能依赖于单个节点。有的通过网络层的负载均衡来实现,即所有的请求会送达所有服务器,但只有真正处理访问请求的服务器进行答复。服务器间通常用对等组网的方式来选举出主服务器。数据包会同时发送到多个节点上,根据算法会有选中的服务器响应请求。如果选中的服务器没有响应,依据算法下一台可用的服务器来响应。容器化管理容器化管理是在操作系统级实现的虚拟化,如Docker(在算力管理中也提到了它)。通过容器化,可将服务分散在多个节点上。在此基础上来实现负载均衡,将新节点注册到服务上,新节点就可以马上对接用户请求,开始提供服务。通过容器化的服务,可随时更新服务的系统级依赖,如操作系统的补丁、硬件升级等。而且可提供从开发到服务的完全一致的环境,减少因为操作系统等不一致造成的生产环境问题。另外,通过容器化管理,可以轻松的进行服务扩容。只需要在服务器上使用同样的配置创建新的实例,即可实现扩容。在发布时的版本更替过程也类似,通过创建新Docker实例,关闭旧的实例,即可完成版本发布。在这个过程中,只要保证服务器是无状态的、可共存的即可。API版本管理在服务的不断开发中,根据新需求,会出现修改API接口的场景。而在复杂的应用中,API会被不少下游的服务或客户端调用。这时,要管理好API更改,防止造成服务的中断。一般的API更改都要分步骤进行。在发布新API时,要同时支持旧API。即使旧API不能加入新的功能,也需要保证它继续可用。发布后,通知并帮助下游团队尽快迁移到新API。最后,在数据上看到旧API已经没有访问量时,才可删除旧API及其对应的数据、模型等。因此,在API升级过程中,有一段时间新旧API会共存,甚至需要维护好两套数据加工流水线,要注意保证版本替换过程中服务的顺畅。数据收集在提供服务时收集到的数据,是用户场景下真正的数据,对改进模型的帮助非常大。收集数据时,可通过数据加工流水线将数据传送到模型试验、批处理训练直接使用。如果能收集到用户标记数据,则需要将用户原始输入数据与用户标记关联起来保存。一般来说,用户输入数据和用户标记数据会在两个请求中,因此,要注意进行正确的关联。特别是在用户有多次请求时,需要通过关联的id找到正确的标记与数据对。扩展能力近年,机器学习领域的变化非常大,新的算法、框架、硬件、理念等层出不穷。一个好的机器学习平台应该有足够的扩展能力,才能跟随时代演化。不求站在技术的最前端,但也不至于在出现颠覆式创新时,整个平台需要推倒重来。通用与专业在建设平台时,要把握的第一个方向就是,要通用平台,还是垂直场景下的专业平台。通用平台,即尽可能的支持所有的机器学习框架、硬件、网络结构。而垂直平台,即将某个方面实现好,做到极致。比如对TensorFlow支持到极致,或者对图像识别支持到极致等等。这和公司业务方向有很大的关系,在每一层都可以进行通用、垂直的选择。通用,意味着平台只做最少的工作,通过Kubernetes这样的容器编排软件来支持不同框架的配置镜像,并支持基础的硬件资源分配,任务管理等工作。平台会有最大的通用性,几乎能适应未来的任何变化。如,最近在研究上很热的自动化建模,超参自动搜索等,很可能会颠覆未来的建模过程。一旦有了成熟的方案,通用平台可以快速集成。但在使用通用平台时,需要自己配置docker镜像,并学习任务配置等功能,有一定的学习培训门槛。专业,即对某些框架、硬件提供深度的支持和定制,甚至对于某些场景做更多的定制化工作。如专门用于处理图像识别的平台,可直接在界面上提供上传图片、标记数据、性能对比等功能。定制的平台还可以很容易的集成选定框架下的模型优化、代码生成工具。这样能提供最好的使用体验,也能深度挖掘框架、硬件的功能,提高效率。但不足的是,如果和框架、硬件、业务绑定太紧,在有了重大变化时,平台能重用的部分可能会比较少。如,出现了新的硬件能显著降低计算成本,或业务上有了其它机器学习建模需求。通用和专业,一横一纵的两个方向并不是完全冲突的。这也要看团队当前所处的阶段。是在各种框架、算法间不断探索的试验阶段,还是已经找到了比较成熟的方案,应用上慢慢定型的应用阶段。如果框架选型已经完成,业务形态已经初步具备,构建专业化的平台会带来更高的生产力。也可以先构建一个通用平台,在上面对框架、硬件、业务组合做进一步的深度定制。在定制时,要考虑到框架、硬件、业务间不要耦合太紧,这样在某部分需要改动时,能最大程度的重用现有平台。当然,同时也不能为此而过度设计,重点是要支持好业务。比如:以前的ODBC(开放数据库连接)是一个几乎能兼容任何关系型数据库的协议,号称有了它之后,底层数据库可以轻松切换。但在实际应用中,需要切换数据库的情况非常少。而且当需要切换时,真正麻烦的并不是如何连接数据库,而是如何改动现有的SQL语言来兼容新的数据库系统。运维支持随着现代运维体系的不断发展,运维人员和使用的工具也越来越专业,运维理念也在不断的进步。而目标一直都很清晰:提升稳定性,降低成本。提升服务稳定性是所有在线服务提供商的追求,服务问题会直接影响到业务。此外,服务规模很大后,服务器消耗就成了服务成本中不可忽视的一部分。如何降低服务器成本也是现代化运维的研究方向。提升自动化水平是重点的方法,自动化水平的提高带来的益处很多,一方面是每个运维人员能够管理的服务器规模会随之提升,降低了管理成本,另一方面自动化会减少人工操作,减少手工错误的机会。运维支持从层次来看,从机房建设的基础设施运维,操作系统、DNS服务等的系统级运维,到软件发布、监控的运营运维,覆盖了从物理层到服务质量的所有层次。可以说,从一台服务器都没有到业务能够顺利运行,整个建设过程都属于运维的范畴。在运维支持时,最重要的是保证服务质量。这一点常常会被参与者所忽视,特别是开发人员。在遇到服务问题时,开发人员更容易想到的是哪里有缺陷,该如何修复。有时,服务问题可以暂时通过重启服务,版本回滚等方式临时修复,这种情况下应立即进行修复,而不是调研问题的根源。等服务恢复后,再调研出现问题的原因,并进行彻底修复。监控监控是运维服务中不可缺少的一环。在硬件层面,通过监控能发现硬件故障,及时进行替换,保证不丢失数据,不造成服务宕机。在服务层面,监控信息能帮助了解资源的使用情况,及时对资源进行调整,防止过载造成的服务问题。也可以帮助发现新代码部署的问题,从而终止发布。业务层面的监控还能在第一时间看到业务的健康状况,及时采取业务上的行动。运营世界级平台时,运维支持和服务监控都是7x24全天候工作。一般会有人员轮流值守,保证服务的稳定运行。值守人员在遇到无法处理的问题时,还会联系开发人员或第三方合作伙伴来深入调查问题,尽快恢复服务。有的监控还有警报的功能,在过载或者服务不正常时,通过短信、电话、邮件等方式通知到人,进一步采取行动。除了出现问题的警报,监控还要支持日常数据观测、发现异常。在用户还没感知到时,就开始解决问题。如建模的试验平台的瓶颈在哪里,是网络速度、存储容量、还是计算单元的数量等等。再如,当应用服务部分响应速度变慢时,可以帮助分析具体的瓶颈在哪里,及时做出调整。高可用与灾备在数据部分,已经简单介绍过高可用和灾备的概念。根据需求,平台可能也要提供高可用和灾备的能力。在高可用上,要充分利用各种负载均衡的方案,提高横向扩容的速度,同时要减少扩容时的手工操作以减少人为错误,将扩容过程自动化程度不断提高。另外,需要在监控中发现问题节点,及时将其终止服务,以免用户访问到这个节点出现异常。灾备用于应付突发情况。平时需要做好演练,才能保证关键时刻能起到作用。灾备的同时,通过跨地域的多点服务也能有效提高不同地方的访问速度,也其称为异地多活。异地多活对数据同步、多版本共存都是很大的挑战。配置化管理现代运维开始从自动化脚本管理向配置化管理来演变。配置化管理,是为每个服务创建好脚本和配置,根据负载、升级需求等,按需创建、启动实例。整个过程几乎全自动进行,不需要人工干预。在配置化管理的方法下,要准备好服务镜像,对应的服务部署脚本等。一旦需要增加服务器,就能自动分配资源,运行服务,并加入到负载均衡集群中。同样,从集群中删除服务器也需要配置化的流程管理。在这些过程中,要等服务准备好后,才能将其加入到集群中,否则部分用户流量可能会到达还没准备好的服务上,从而产生错误。日志日志是比监控信息更细的服务器运行情况,有些监控信息也是从日志中抽取的。线上服务不能像开发环境一样随时进行调试,一般的问题诊断都要依赖于日志。在建模时,除了对关键的指标进行埋点外,还需要在日志中记录一些辅助的信息,以便在生产环境中能够发现并定位问题。日志需要统一收集到一起,方便进一步的日志分析和聚合。日志分析功能需要按照关键词、时间、组件、物理机等各个维度来缩小问题的日志范围,从而能快速找到问题。还有一些人工智能运维系统(AIOps)能够自动定位到可能的问题点,加快问题的诊断。易用性机器学习平台的易用性也非常重要。一方面是在平台管理上的易用性,另一方面是平台用户的易用性。如果团队的产品就是机器学习平台本身,平台的部署、升级、扩容都要提高易用性,让新用户能够方便的做好运维。小团队部署机器学习平台时,没有太多精力进行系统调研,易用的平台是小团队的首选。所以,要考虑如何能让新用户很快的学会搭建平台,用上平台。有了新功能后,如何让用户知道,用户又如何升级。在用户需要对平台算力进行扩容时,是否通过简单配置就能完成。平台的可视化管理也很重要。除了第一步部署很难完全通过可视化用户界面来实现外,其它步骤理论上都是可以通过界面来实现。这样能大大减低平台的学习门槛。当然,并不是有了可视化界面,就能提高易用性。可视化界面的设计要遵循用户的使用场景,并尽量简化。平台使用时,也同样需要更高的易用性。不仅小团队需要直观易学的界面,大团队也有人员替换,易用的平台能让新人尽快熟悉工具、展开工作。平台除了需要提供数据集、训练任务、模型的可视化管理功能外,还要有便利的任务提交功能。由于任务提交通常是伴随着代码编辑进行的,所以可以在流行的集成开发环境上,开发插件来帮助提交任务。有的开源机器学习平台已经在一些集成开发环境中实现了任务提交、管理和数据管理的插件。安全性安全性是互联网服务运维最重要的话题。几乎每个公开服务每天都会受到攻击。最典型、最普遍的是无差别扫描攻击,即对每个IP进行自动的全面扫描,如果服务器有明显漏洞,就会被自动攻击。攻陷后,就会成为僵尸网络的一部分,被黑客所利用,开始进行无差别扫描工具,参加定点攻击,作为黑客跳板等各种黑客活动。服务器不仅会干坏事,还消耗带宽、计算资源等成本。除了无差别攻击,一些著名公司和网站还会受到定点攻击,黑客不仅通过工具扫描,还会人工分析漏洞,进行更高强度的渗透攻击。一旦被攻破,就存在数据泄露和服务被破坏的风险,甚至会让整个公司的业务陷于失败。安全保障是矛与盾之战,常常是道高一尺魔高一丈,是一场永不停息的战斗。有条件的团队可以请专业白帽黑客团队来进行渗透测试,或进行漏洞悬赏。找到漏洞后,要尽快修补。除了对已知漏洞的快速修补,还需要系统化的分析,进行威胁建模,对每一种攻击和自身的漏洞进行防护。下面列出了一些常见的安全方面的考虑,但远不是全集。只要建立了成体系的防护,一般的一两个漏洞也无法形成完整的攻击链条,不会对业务产生实际的威胁。认证授权认证授权是两个过程,认证是指的通过用户名、密码等方式,确认用户的身份。授权是在用户身份确认后,根据系统内的信息,决定给用户何种权限。统一认证。即多个系统都用同一套用户名密码来登录,修改密码、增删用户都是一体的。机器学习平台本身需要有认证授权的功能,让有权限的用户才能访问对应的资源。如果企业中资源过多,每个资源都是单独的认证体系,个人确实比较难以管理认证资源。这时需要提供统一认证来简化系统,让认证系统真正发挥作用。如果机器学习平台组合了带有认证功能的开源工具,要将这些工具的认证统一起来,方便使用。两阶段认证。即除了输入用户名密码外,还会依靠用户的其它电子设备或硬件来完成认证。用户名密码有可能会被钓鱼网站、用户计算机上的木马盗走,所以还需要第二阶段的认证来保证安全。一般是通过手机上定时刷新的二次认证码来进行第二阶段认证。也有通过短信、电话等方式进行第二阶段认证。用户权限管理。即管理如何给用户授权。一些业务系统还需要用户登录,也要防止用户信息被盗用。可参考一般的网站设计方法,对用户账户要防暴力破解,短信轰炸等攻击。内部管理。有的团队中,虽然有认证授权的机制,但实际上密码统一、账户互相借用,认证授权体系形同摆设,这都会造成漏洞。安全不仅是技术问题,同时也是管理问题。不仅要在工具上强制一些安全策略,在内部教育上也要有安全的一席之地。 网络安全网络安全除了一些协议层的攻击外,主要还在于平台的安全建设。在建设机器学习平台时,可能会组合一些开源工具。不少开源的平台级工具都建议在内网使用,默认安装也没有配置认证,在默认的网络端口监听服务。机器学习平台在内网部署时,虽然安全性要求要低一些,但也需要基本的保障。如果公司的网络安全建设不足,如果被攻破了,机器学习平台就会面临较大的安全威胁。在将机器学习平台提供给外部用户使用,甚至允许用户直接部署时,更需要提供全面的安全建议。如,哪些服务器需要开放哪些端口,端口的用途是什么。哪些服务器要放在内网,哪些需要放在防火墙边缘区域。如果直接开放了开源组件的端口,要注意开源社区上发布的安全补丁,及时进行升级。服务器安全服务器管理上要注意的事情不少。包括关掉不必要的服务、端口,管理好账户、密码,不要使用弱口令,及时屏蔽服务安装后的默认账户、危险功能等,经常性的注意安装安全补丁。这些都能大幅减小服务器的被攻击面。 每个服务部署的时候,应使用单独的账户部署服务。一旦某个服务被攻陷后,减小对其它服务以及整台服务器的威胁。代码安全一般的现代语言,在服务器端要注意防止SQL注入攻击。如果有内部反向代理的,还要注意网址的白名单过滤。如果是c/c++代码,还要注意防止缓冲区溢出攻击等问题。数据保护数据常常是业务的核心资产,需要进行重点保护。要对内部、外部数据使用者都做好数据隔离、数据保护工作,减少数据泄露的风险。同时,对用户数据需要做隐私保护。数据隔离。即对数据分级管理,没有权限的人不能访问相应的数据。对于核心的业务数据,要最小化授权,只给予必须的人以权限,从而减少数据的泄露风险。对于核心数据,如用户的联系方式等,可以在授权时记录日志。一旦数据泄露后,可以根据泄露数据反查到泄露渠道。机器学习平台建设时也需要考虑到数据隔离问题。日常试验的样本数据应该是业务数据的一个子集,并在此数据集上抹除用户数据及业务敏感数据。如果要更新数据集来提高数据集的质量,可以对试验数据集单独进行更新,不推荐将整个生产数据集暴露给所有团队成员。隐私保护。即保护用户的核心个人数据,特别是能帮助找到某个人的信息。保护好用户隐私不仅是对用户负责,也是对团队、企业的名声负责。泄露后危害最大的是用户密码,要对用户密码通过哈希方法加盐(salt)加密。对用户姓名、电话、地址等信息,可加密后存储,并保管好密码,仅让核心运维人员来保管、配置此密码。对于非密码字段可以不加盐,这样,同样的加密后的内容还能保持一致,不影响后面的数据聚集等操作。安全日志安全日志是安全的最后一关。当发现漏洞后,有可能通过安全日志找到攻击路径、攻击方法,以便封堵漏洞,评估损失。需要将系统、软件、审查的安全日志都定期备份到独立的位置,防止被攻击方抹除。三、需求与技术决策上文对机器学习平台及其要考虑的功能做了大致的介绍。可以看出,建设一个完整的机器学习平台有很大的投入。每个团队、业务需要根据自己的需求来有所取舍、有先有后,不是什么都要有、马上就要。本章会从多个角度帮助团队,分析应该建设什么样的机器学习平台,来支持业务需求。理解业务技术最终服务于业务。即使是纯粹的软件公司,也要思考软件的用户需要什么。全方位的理解了业务,在每次进行技术决策时,能够从问题的源头出发,做出对当前、中长期的最佳决策。对于小团队、小公司来说,理解业务相对容易。但业务也可能随时发生变化。对于中大型公司来说,特别是刚刚开始进行机器学习投入的公司,还处于探索阶段。机器学习平台支持的业务可能随时发生变化,要从公司业务、机器学习可能的应用来理解业务。理解业务,首先要了解目标客户,要思考机器学习平台的用户是谁。建设的平台是否直接给外部用户使用?如果是的话,是每个客户部署一个平台,还是在同一个平台上服务所有的客户?或者只给用户提供端到端的服务,平台给自己的团队使用?平台的使用者是数据科学家还是开发人员?业务规模以及发展速度的预期,决定了机器学习平台硬件投入的时间点以及成本的规划。如果业务规模较大,初期就要规划足够的算力、部署。如果团队强大,就要在运维、易用性上多下功夫。但也要注意,资源永远是有限的,要把好钢用在刀刃上,精力上要有所集中,解决当下最重要的痛点。定位定位,在机器学习平台建设时,要确定平台的目标,制定向此方向发展的路径。首先要确定团队服务的业务是某个垂直方向的应用,还是有可能成为跨业务的机器学习平台?如果是跨业务的,是要做成通用的机器学习平台解决底层架构问题,还是要做好某些专业方向,直接解决业务问题?定位以后,平台、开源工具的选型等工作就有了依据。管理期望当前的机器学习虽然在飞速发展,但能做的仍然有限,远远赶不上人类智能。媒体、业界对机器学习能做到的事情期待未免过高。机器学习从业者,包括数据科学家、应用开发者,应该帮助整个公司、业务部门都认识到机器学习的局限性。在业务模式设计上,不能将机器学习放在会有重大影响的决策位置,而是应该辅助人类做决策。比如,车辆驾驶中的车道自动保持功能,在启动时也会同时检测用户是否手握方向盘,在用户没有握住方向盘时警告。这样,在设计上就对功能有合理的期望,同时也管理好了用户的期望,就保证了安全。再比如,不少机器学习模型已经能帮助分析医学影像了,但仍然只能作为医生的辅助,不能直接给出定论,做出诊断。随着数据、特征工程的不断演进,机器学习模型效果会越来越好,同时也要看到,随着模型效果的提升,成本的投入也在指数级的增加。但无论如何,如果人不能做到100%准确的事情,机器学习模型也做不到。期望机器学习模型完全的解决某个问题,还是一个世界难题。比如,在手写识别的数字数据集中,即使人也很难分辨一些图片到底是什么数字,更不能期望机器学习模型能100%的识别正确。 服务规模机器学习平台要对当前和预期的服务规模做好预测。以此为依据,就能决定运维需求是否强烈、紧急。如果计算出来一两台服务器即可满足需求,而且不需要进行高可用、灾备的投入。那么手工搭建一两台服务器即可满足需求。待业务量有了数量级的增加后,再寻求自动化的运维解决方案。在估算服务规模时,也要以发展的眼光来看。如果是自建研发团队,可以在性能目标上稍稍超前6个月左右,不要太超前,也不能只满足当下。留出一定的时间,是为了给团队留出足够的设计演进的时间。如果是将系统进行外包,每次交付都有明确的目标,交付周期也较长,可设定更高的性能目标,这样能够有更多的时间来做好项目管理。数据特点数据是机器学习重要的外部输入,其特点会影响许多技术决策。数据来源。数据是否为企业内部数据,如果是的话,是否有很强的保密需求?如果不是的话,数据从何而来,是否有保密需求?如果数据有保密需求,一般是存储在企业内部平台上,不放到云中。如果数据在其它企业中,有可能整个系统都要构建在对方企业中。如果数据是通过网络抓取的,并在云平台中运行数据采集。那么,带宽、服务器等方面扩容较容易。进行私有部署时,就要从带宽、成本、机房大小来评估一下平台容量的瓶颈。数据量。数据量大概有多少?产生新数据的速度如何?数据采集、加工时的传输带宽需要多少?访问频率如何?如果数据量较大,需要建设较大的存储容量。如果自建机房,成本会较高,性能也不好保障。云平台可以按需建设,一次性投入也较小。如果数据访问频率很低,或几乎不访问,云平台中的冷数据存储的成本会更低。 如果数据需要在企业和云平台之间大量的传输,就不得不考虑到带宽是否满足需求,成本是否可控。如果带宽、网络延迟无法满足需求,可能就无法使用云平台。数据格式。数据是结构化的还是非结构化的?存储在数据库中还是文件系统中?如果当前的数据格式是非结构化的,没有标记信息,就要进行人工标记工作,建立样本数据库。如果现有数据已经有了足够的标记数据,可直接转换为机器学习直接可用的形式。数据格式也涉及到数据加工流水线的对接工作。如果已经有了大数据平台,尽可能重用现有平台能够减小开发和维护成本。但是,重用现有平台会带来更多的系统关联问题,如果现有平台不够稳定,会影响新的机器学习平台的稳定性。技术决策云服务与私有部署云服务已经占领了服务器市场很大一部分,不管是中小型企业还是一些大型企业,都开始将自己的整个业务构建到云上。有些云服务商直接提供了机器学习平台,有的提供了交互编程的云服务资源,还有的云服务商直接提供了GPU的虚拟机供使用。下面介绍机器学习平台所涉及到的服务和部署形式。云虚拟机。即在云平台中构建的虚拟机,资源隔离性和自由度非常高。云虚拟机是当前云服务中最常用的一种。在云虚拟机里能安装任何软件和服务,通过虚拟网络、防火墙等功能也能达到很高的安全性。资源可以按需申请,随时增减。还可以通过云服务的网关等来组合实现高可用、灾备的功能。因此,云虚拟机自由度非常高,但需要自己来搭建,需要一定的运维经验才能很好的使用。GPU等计算型虚拟机。即安装有GPU等高性能计算硬件的云虚拟机。随着机器学习的深入发展,大部分云服务商都提供了安装了GPU等的虚拟机。Nvidia的GPU是最流行的,云平台中的GPU服务器,大都采用了Nvidia的专业机器学习GPU,采购成本较高,使用成本也较高。云平台可以按用量计费。如果团队模型训练的时间不多,可以按小时租用,训练完成后立即回收,这样才能有效节省成本。云机器学习平台。云机器学习平台一般是为数据科学家进行模型试验、训练准备的。有的云机器学习平台也实现了一键部署等产品化的功能。不少云服务商提供了机器学习平台,提供的功能差异也较大。有的能进行分布式计算,有的提供了图形界面的模型创建工具,有的还提供了交互式的脚本编辑执行体验。云机器学习平台在使用上会比较简单,团队也可以避免开发机器学习平台的投入。但是,云平台的功能较固定,使用上如果遇到不支持的功能,一般不能扩展。云认知服务。如果团队没有数据科学家,或者初期想要验证概念。可使用一些已经做好的云服务,如图像识别、人脸识别、语音文本转换、语言理解、聊天机器人等服务。这样团队能将重点放在打造业务上。准备好数据,云认知服务也能给出较好的结果。但云认知服务提供的种类有限,不能覆盖所有行业的应用。云服务一般也不支持离线使用,需要联网才能调用。基于多云服务商的混合部署。在一些深度使用云服务的场景下,希望最大化服务的稳定性,灵活调整成本。这种需求下,可在多个云服务商申请资源,将其整合使用。结合自动化脚本以及配置化的管理,还能做到根据使用情况灵活调整在各家云服务商的资源规模。这样,不仅能够有效管理成本,还能规避某家云服务商的临时故障等问题,保障自己的服务质量。这种方式对运维能力有较高要求,如果资源使用不多,对资源价格不敏感,也不必采用此种方式。服务器托管。即自购服务器,并存放于专业的机房中,属于私有部署。专业机房有良好的基础设施建设,对断电有备案。一般也会接入多家网络运营商,并提供智能路由对访问加速。物理安全上也控制非常严格,防止非授权进入机房。这种方式相对于云服务成本较低。但安全性、运维上也需要团队自己操作。如果遇到服务器硬件损坏等问题,需要人工去机房解决。自建机房。即自购服务器,并自建机房存放服务器。这种方式的成本最低,物理管理也较方便。但需要从基础设施层进行运维,包括温度控制、布线、不间断电源、网络连接等等问题都需要解决。如果机房建设在团队的办公楼内,电源、网络保障上都没有专业机房高。网络带宽也无法灵活调整。云平台与私有部署的混合部署。一些企业会有私有部署的内部信息系统。依靠私有部署,企业员工访问内部系统会很快,对服务器的安全也会多一层企业网络的保障。同时,企业还会依靠云平台对外提供服务。企业内部和外部会有数据同步、传输等功能。云平台和私有部署的优缺点都在前面进行了分析,根据不同场景来决定业务部署在何处即可。决定私有部署还是用云服务商时,除了上一节定位时思考的问题,还有成本问题。私有部署是先投资后使用,还有不短的选址和机房建设周期,但后期使用上成本比较低。云服务比较灵活,使用多少,花多少钱。如果将时间线拉长,云服务的直接成本会比私有部署高。云服务有自己的管理、开发成本,但同时也提供了更高的高可用、灾备、安全等运维能力。出问题的概率较低,遇到问题一般也有较好的支持。所以,现在不仅是希望降低初期投资成本的中小企业在选择云服务,一些非专业信息产业的大企业也选择云服务,而不是私有部署。在机器学习平台用作训练时,私有部署的成本优势会很明显。如果团队包含数据科学家,并有很高的算力需求时,私有部署比较经济。如果建设好了机器学习平台的集群管理能力,实际上并不需要云平台中的高可用、灾备能力。如果有某台服务器出了问题,只是部分算力浪费了,重新训练即可,损失不大。另外,云平台中为了稳定,通常选用了高端的机器学习专用GPU等硬件,而自建机器学习平台可选用消费级的GPU,成本也会大幅降低。因此,可以考虑机器学习平台的建模试验部分自建机房进行,而推理服务部分放在云服务平台中。但实际情况千差万别,如果因此造成很大的数据传输量,可能会得不偿失。团队建设机器学习平台如何建设,和它的用户有直接关系。如果团队是机器学习平台主要的用户,就要考虑到团队的规模与分工。如果机器学习相关的应用开发一共就两三个人,平台在共享合作上就没有太大的价值。如果团队没有数据科学家,那么也不需要进行模型试验部分的建设,使用预构建的云认知服务即可。团队也有发展的过程,不是一蹴而就的。在团队发展初期,做好技术决策是重点,可以基于现有的机器学习平台来开发或直接使用。随着团队和业务的发展,对机器学习平台的需求会更明确,也会有一些强烈的需求。这时,如果团队有精力了,可以推倒重来,或者接着演进和重构。如果有外包参与,可根据和外包团队的分工,定义好接口及指标,机器学习平台可只建设团队需要的部分,以免双方有过多沟通上的成本。在是否建立数据科学家团队上,有较多的考虑。可以先从现有的云平台的预构建认知服务开始了解,看看是否已经满足了业务场景的需求。如果满足需求,可先构建出业务系统,并丰富数据。如果数据不能改进模型,或者云平台的预构建认知服务不能满足需求,就需要考虑建立自己的数据科学家团队。成本很多技术决策都会和成本相关。对于大中型企业,成本预算方法比较成熟时,直接按照计划对团队、服务器等资源投入做好规划即可。对于创业公司及小团队,要根据业务、资金情况量入而出,做好团队、平台建设。关于团队人员预算建设方面,根据市场情况决定即可,不再详述。部署上,初期可以先从云平台开始,不进行私有部署。大部分云平台都会有详细的定价策略,可以根据算力、存储、带宽的使用来预估大致的价格。在开发机器学习平台时,做好关键节点的横向扩容能力,即可满足初期的负载伸缩性需求。四、OpenPAI前文介绍了机器学习平台的功能以及建设机器学习平台要考虑的因素。本节会介绍OpenPAI,即微软的开源机器学习平台。它可用于企业私有部署,也可部署在云平台中。它解决了建模训练时的算力和资源管理的问题。OpenPAI的开发很活跃,问题也能得到及时响应,还在积极开发新功能中。OpenPAI代码及文档地址:https://github.com/Microsoft/pai特点OpenPAI是为数不多的用于私有部署的机器学习平台。在微软内部管理了几百块GPU,应用规模上得到了很好的验证。同时支持数台服务器的小规模部署。OpenPAI和Visual Studio、Visual Studio Code的Tools forAI工具集成在了一起,利用这些强大的集成开发环境同时,也可以方便的提交并管理训练任务、数据资源。OpenPAI是通用的机器学习平台。理论上支持任何计算密集型,甚至存储密集型的任务。通过预先配置的Docker,已经支持了包括TensorFlow,Keras,CNTK等流行的计算框架,并支持框架自带的分布式训练任务。硬件上已支持GPU、内存、CPU的分配,通过扩展,还可以支持更多的硬件类型。架构OpenPAI基于Kubernetes的容器化,Yarn的资源分配,并集成了HDFS作为存储管理等开源组件构建的通用的机器学习平台。用户提交作业(即机器学习训练任务)后,就会自动分配资源,创建Docker实例,并运行指定的命令。因此,OpenPAI是一个通用的算力管理平台。通过提供一系列预构建好的Docker,OpenPAI可以方便的执行TensorFlow,Keras,CNTK等流行的机器学习框架的训练任务。OpenPAI将复杂性包装了起来,只有master和worker两种角色。Master提供了webapi和界面,以及Kubernetes的master节点等;worker用于运行实际计算的docker实例。下图为OpenPAI的架构示意图。算力管理OpenPAI提供了作业的队列管理。另外,通过自己配置Docker,还能搭建出JupyterNotebook这样的交互式开发训练环境。在其队列管理中,用户只需要声明每个作业需要的资源类型,给定所需的docker镜像路径,代码位置等信息,即可等待系统分配资源,并运行训练作业。OpenPAI的作业支持机器学习框架的分布式运算。如,可指派两个实例作为参数服务器,另外4个实例作为计算节点。OpenPAI开始执行此作业时,会先保留足够的资源,然后启动这些节点,开始分布式计算。在节点启动后,OpenPAI即可知道所有节点的IP、ssh端口等信息,并将这些信息通过环境变量或命令行参数的方式传给运行的脚本,从而让每个节点能发现其它所有节点。在多用户的情况下,OpenPAI通过Virtual Cluster来解决资源分配问题。VirtualCluster可理解为虚拟的集群。不同的VirtualCluster有不同的资源配额,可以将用户分配到一个或多个VirtualCluster中。如果某个Virtual Cluster使用的资源超了,并且其它VirtualCluster有空闲资源,则OpenPAI会使用空闲资源来运行。通过这样的管理,能实现多种场景。如:两个小团队各分配一个50%资源的VirtualCluster。团队A每天都有训练任务在持续进行,对资源的需求是越多越好。而团队B的资源使用是突发性的,一旦有需求,希望其能尽快完成。平时,团队A会使用团队B的空闲资源来加速自己的训练,一旦团队B有了突发任务,团队A占用团队B的训练任务就会被停下来,让团队B的任务先训练。又如,将生产需要的模型训练和试验需要的模型训练都在一个集群中进行,各用一个VirtualCluster。给生产分配100%的资源,而模型试验分配0%的资源。这样,一旦生产资源有空闲就可用于试验,但会优先保障生产资源。作业提交给OpenPAI的每个训练任务,称为作业(Job)。作业由一个json配置文件描述。通过VisualStudio Tools for AI以及Visual Studio Code Tools forAI可以方便的提交训练作业和最新的代码。作业配置作业的json文件包含了训练任务的基本信息。下文是简化了的任务示例,对主要的参数进行介绍,以方便理解OpenPAI的用法。1、普通作业{ "jobName": "mnist_024", "image": "openpai/pai.example.tensorflow", "codeDir": "hdfs://\<ip address\>:\<port\>/folder/mnist", "taskRoles": [ { "taskNumber": 1, "cpuNumber": 4, "memoryMB": 4096, "gpuNumber": 2, "command": "python \$PAI_WORK_DIR/mnist/mnist.py" } ] }jobName:这是每个作业的名称,需要全局唯一。image:这是作业运行的docker镜像的地址。示例中是OpenPAI预构建的TensorFlow的镜像地址codeDir:它表示了将要运行的代码位置。在docker启动后,会将此目录的内容拷贝到docker中的/root下。例如本例中,会将mnist目录内容拷贝到docker中的/root/mnist下。taskRoles:这是作业需要的docker实例的定义模板数组。数组中每个项目是一个docker模板的定义。taskNumber:这个docker模板在实例化时的数量,即这个模板创建几个docker实例。在单机运行的docker实例中,这个数字应为1。cpuNumber、memoryMB、gpuNumber:对应于其名字,分别为CPU、内存、GPU的数量。这为当前任务模板定义了要使用资源的规格。OpenPAI会根据这些信息来分配计算节点。command:这是Docker实例启动后,运行的命令。这条命令运行结束后,Docker会被销毁。如果命令行返回值不为0,任务会被标记为失败。分布式作业{ "jobName": "mnist_024", "image": "openpai/pai.example.tensorflow", "codeDir": "hdfs://\<ip address\>:\<port\>/folder/mnist", "taskRoles": [ { "name": "ps_server", "taskNumber": 2, "cpuNumber": 2, "memoryMB": 8192, "gpuNumber": 0, "command": "python code/tf_cnn_benchmarks.py --ps_hosts=\$PAI_TASK_ROLE_ps_server_HOST_LIST --worker_hosts=\$PAI_TASK_ROLE_worker_HOST_LIST --job_name=ps" }, { "name": "worker", "taskNumber": 2, "cpuNumber": 2, "memoryMB": 16384, "gpuNumber": 4, "command": "python code/tf_cnn_benchmarks.py --variable_update=parameter_server --data_dir=\$PAI_DATA_DIR --ps_hosts=\$PAI_TASK_ROLE_ps_server_HOST_LIST --worker_hosts=\$PAI_TASK_ROLE_worker_HOST_LIST --job_name=worker" } ] }这是利用TensorFlow分布式训练功能的作业配置。此配置会分配两个参数服务器以及两个计算节点。分布式作业的具体内容和普通作业类似,但会有多个taskRoles中的节点。它们会被分配不同的资源。可以看到taskNumber数量为2,表示每种角色需要两个docker实例。在command参数中可以看到有一些$开头的变量。这些是OPENPAI内置变量。可以将分布式系统各个角色的主机地址、ssh端口传给所有节点。这样,在任何分布式组网的系统中,都能够拿到所有的节点信息。提交作业Visual Studio和Visual Studio Code中的插件管理可以找到Tools for AI插件。Tools forAI是微软为机器学习准备的扩展。配合Visual Studio和Visual StudioCode的集成开发环境,为机器学习开发提供了更强大的支持。Tools for AI其中一项功能就是远程训练任务管理,它支持OpenPAI作为远程服务器,能够提交、管理任务,管理存储。具体使用方法可参考OpenPAI的官方文档。下图为Visual Studio中OpenPAI的任务提交界面。 下图为Visual Studio中OpenPAI的作业管理界面。 下图为Visual Studio中OpenPAI的存储管理界面。 下图为Visual Studio Code中OpenPAI的作业管理界面。 下图为Visual Studio Code中OpenPAI的存储管理界面。运维OpenPAI可以管理大规模的GPU平台,提供了负载、服务器健康等多种工具。通过运维工具,能够观察集群的负载情况,找到集群瓶颈。另外,还能细粒度的看到某台服务器的负载情况,了解每台服务器的健康状况。通过OpenPAI平台,能够看到每个组件和每个作业的日志文件,进行更详细的分析。通过OpenPAI的web界面,也可以提交、管理训练作业。下图为OpenPAI的集群仪表盘。 下图为OpenPAI的服务器列表。 下图为某台服务器的负载情况
0
0
0
浏览量1198
金某某的算法生活

AI应用开发实战 - 手写算式计算器

主要知识点了解MNIST数据集了解如何扩展数据集实现手写算式计算器简介本文将介绍一例支持识别手写数学表达式并对其进行计算的人工智能应用的开发案例。本文的应用是基于前文“手写识别应用入门”中的基础应用进行扩展实现的。本文将通过这一案例,展示基本的数据整理和扩展人工智能模型的过程,以及介绍如何利用手写输入的特性来简化字符分割的过程。并且本文将演示如何利用Visual Studio Tools for AI进行批量推理,以便利用底层人工智能框架的并行计算,实现推理加速。此外,本文还将对该应用的主要代码逻辑进行分析、讲解。背景在“手写识别应用入门”中,我们介绍了能识别单个手写字母、基于MNIST数据集的人工智能应用,并且在我们的几次试验中,该应用表现良好,能比较准确地将手写的数字图形识别成对应的数字。那么,该应用能不能识别更多种类的手写字符,甚至是同时的出现多个字符呢?这样的情形有很多,比如生活中常见的数学表达式(形如1+2x3)。这样的复合情形更为常见,也更具现实意义。相比之下,如果一次识别仅能一个手写数字,应用就会有比较大的局限性。首先,我们可以尝试一下多个字符同时出现这类情形中最基本的特例,即一次出现两个数字的情况。请启动手写数字识别博客中构建的应用,并在现有的应用里一次写下两个数字,看看识别效果(为了更方便书写及展示效果,我们将前一示例中笔画的宽度由40调整为20。可以体验出这一改动对单个数字的识别并无大的影响):上图是一次试验的结果。进行多次试验,我们看到现有应用对两个数字的识别效果不尽人意。如上图所示,应用窗口右上角展示的结果准确地反应了模型对我们手写输入的推理结果(即result.First().First().ToString()),然而这一结果并不像期望的那样,是我们在左侧绘图区写下的“42”。其实对这个现象的解释已经蕴含在我们之前的博客内容中了。在“手写识别应用入门”的模型介绍章节中,我们对用于训练模型的MNIST数据集做了大致的介绍。归根结底,上述现象的症结在于:作为我们人工智能应用核心的模型,本身并不具备识别多个数字的能力——作为模型的源头,也即是训练数据的MNIST数据集只覆盖了单个的手写数字。并且,在应用的输入处理部分,我们并未对笔迹图形作额外的处理。这两点的综合结果就是,在写下多个数字的情况下,我们实际上在“强行”让AI模型做超出其适应性范围的推理。这属于AI模型的误用。其结果自然难以令人满意。那么,为了增强应用的可用性,我们能不能改善这款应用,让其能处理常见的数学表达式呢?这要求我们的应用既能识别数字和符号,又能识别同时出现的多个字符:首先对于多个数字这种情况,我们很自然地想到,既然MNIST模型已经能很好地识别单个数字,那我们只需要把多个数字分开,一个一个地让MNIST模型进行识别就好了;对于识别其他数学符号,我们可以尝试通过扩展MNIST模型的识别范围,也即扩展MNIST数据集来实现。两者合二为一,就是一种非常可行的解决方案。这样,我们就引入了两个新的子问题,即“扩充MNIST数据集”和“多个手写字符的分割”。结合上文陈述的问题和潜在的解决方案,本文将以“识别并计算简单的数学表达式”这一问题为导向,对现有的手写数字识别应用进行扩展。我们的目标是对克服现存的只能对单个数字进行识别这一局限,让新应用可以识别数字、加减乘除和括号这些能构成简单数学表达式的元素,并对识别出的数学表达式进行计算。本文希望通过这些,能最终获得一款更具现实意义的人工智能应用。最终的应用效果如下图:注意“识别可能出现的多种字符”和“识别同时出现的多个字符”是完全不同的,请注意区别。子问题:扩展MNIST数据集准备数据数据格式为了让我们的新模型能支持除了数字以外的字符,一个简单的做法是扩展MNIST数据集并尝试复用已有的模型训练算法(卷积神经网络)。在“手写识别应用入门”的数据预处理章节中,我们部分了解了MNIST数据集所采用的数据格式和规范。为了尽可能地复用已有的资源,我们有必要让扩展的那部分数据贴近原始的MNIST数据。Samples-for-ai样例库中使用的MNIST示例,在初运行时会从http://yann.lecun.com/exdb/mnist/下载MNIST数据集并作为训练数据。当我们顺利运行mnist.py脚本并完成训练后,我们可以在samples-for-ai\examples\tensorflow\MNIST\input目录中看到四个扩展名为.gz的文件,这四个文件就是从网上下载下来的MNIST数据集,即手写数字的位图和标记。不过这些文件是经过压缩的数据,我们使用的训练程序在下载完成后还会对这些压缩文件进行解压。训练程序只将解压后的数据储存在内存中,并没有回写到硬盘上,所以我们在input目录下找不到储存了原始位图数据的文件。小提示我们仍可以使用支持这种压缩格式的工具将其解压。并使用二进制工具查看其内容。从http://yann.lecun.com/exdb/mnist/页面上,我们可以了解到MNIST数据集的位图文件和标签文件的文件格式。其中最主要的,是用于训练的位图都是28x28尺寸的、单通道的灰度图,前景色(笔画)对应值为255(按颜色表示即是白色),背景色对应值0(按颜色表示即是黑色)。从之前博客中我们已经了解到,MNIST数据集是取反保存位图像素的,如果将其直接显示为位图,则和我们在界面上所见的白底黑字相反。结合页面上的描述和mnist.py中数据预处理部分的相关逻辑,我们了解到目前使用的卷积神经网络要求的最终的输入数据格式如下:根据上述的输入格式,我们已经可以确定我们扩展训练数据的方向了。这里我们需要注意,这些格式是最终输入到卷积神经网络的数据必须满足的,而非我们即将搜集、准备的新数据。虽然这表明了我们新搜集的数据不一定要精确地满足这些条件,这些输入格式仍然对我们的数据搜集起到重要的指导作用。收集并格式化数据搜集数据的方式多种多样。就本文的需要来说,我们可以在网络上搜索已有的数据集,可以自行开发小型应用以在触摸屏甚至手机上搜集手写图形,或者扫描手写文档并通过图像分割的方式提取运算符。并且,在搜集完原始数据之后,我们还可以通过缩放、扭曲、添加噪点等方式来扩展、增强我们的数据集,以获得更广泛的适应性。在我们搜集足够多的新图片后(考虑到原始MNIST数据集共70000张图片,我们搜集40000张左右比较合适,虽然数量不是绝对的),我们还需要对其进行一定的格式化,以方便我们最终将其作为神经网络的输入。位图部分所需的处理非常直观。我们可以参考前一篇手写数字识别博客中对应用图形界面上捕获的手写图形的处理方式,将搜集的图片(可能具有RGB通道)转换成28x28像素的、单通道的灰度图片,并且前景色(即笔画)色值为0(黑色),背景色色值为255(白色)。符合要求的位图样例如下:此处更需要注意的是对图片标记的处理。在原始MNIST数据集中我们看到整数0-9被用来标记对应的图形,这是非常自然的做法。因为我们此处要解决的是多分类问题,解决这类问题的一个先决条件就是我们必须为每个分类提供一一对应的标记。我们很容易就想到用诸如10、11、12来标记加号、减号、乘号等图形类别。这是可行的。此处我们不由得思考,延续已被占用的自然数取标记新类别,虽然可行,但让对应关系变得混乱了。10和加号、11和减号之间,并不像0-9的整数和图形之间有那么自然的联系。作为开发者的我们不禁想到,能否用ASCII表里加减乘除的字符对应的数值来做标记呢(如加号对应53,减号对应55)?这种标记的设定方法实际上是很难使用的,特别是本文中出现的MNIST训练程序基于的是TensorFlow框架,框架本身要求了标记占用的整数值必须小于类别总数。在保证标记和类别一一对应的前提条件下,我们接着已有0-9标记,再为我们新搜集的图形类别增加标记。此时我们需要清楚的定义标记到类别的对应关系,以便我们正确处理模型的输入和输出。我们用10-15分别表示加号、减号、乘号、除号、正括号、反括号。并且,为了便于训练,我们要求这六种数学符号对应的位图,分别放置于add、minus、mul、div、lp、rp这六个文件夹中,并且这六个文件夹需要在同一个目录下。如下图所示:训练模型为了支持我们新增的六种数学符号,我们需要修改原始的MNIST模型训练脚本(即之前所用的mnist.py)。训练模型所需的Python脚本,可以在这里找到:https://github.com/MS-UAP/edu/tree/master/AI301/self-built_mnist_extenstion这一仓库中:在./tensorflow_model/路径下,是支持扩展的MNIST数据集的训练脚本mnist_extension.py。这一脚本要求额外的命令行参数--extension_dir,用于指定我们扩展的六种数学符号的位图所在;在./extended_mnist_calculator/MNIST.App目录下,是本文这款应用的主体代码。我们会在下文中用到。上文中,我们要求新搜集的数据最后需要被格式化为以色值0(黑色)为前景色,以色值255(白色)为前景色的单通道位图。我们修改后的训练脚本会读取这些位图并其反色,以到达和原始MNIST数据同样的效果(也和我们应用中输入处理的部分一致)。假设我们存放add、minus等六个文件夹的目录是D:\extension_images,我们就可以在克隆好了的仓库的/training目录下,通过命令行执行:python mnist_extension.py --extension_dir D:\extension_images来启动针对包含了六种数学符号的扩展数据集的训练。该训练脚本在导入原始MNIST数据之后,还会从D:\extension_images目录分别读取六种新类别的数据。再混合新旧数据之后进行训练。可能的训练结果如下图:小提示混合新旧数据在这里非常有用。因为训练过程中,目前的脚本是一次仅将一部分数据用于迭代优化和模型参数更新。如果不进行混合,就会发生新数据迟迟不被利用的情况,影响模型的训练结果。我们对MNIST模型的训练是基于卷积神经网络的。并且上本中的脚本在处理扩展的符号位图之外,并没有对用于训练原始MNIST模型的卷积神经网络的结构进行修改。我们知道系统的结构决定其功能,那么我们针对原始MNIST数据设计的网络结构能否支撑扩展后的数据集呢?对这一问题最简单的回答就是进行一次训练并观察模型性能。用这种方法进行试验后,我们通过错误率(主要是Validation error,在此例中反映了每100次小批量训练之后,模型当前在整个验证集上的错误率;和Test error,在此例中反映了训练结束后模型在整个测试集上的错误率)发现新模型的性能还是不错的。足以支持我们接下来的应用。子问题:分割多个手写字符如上文所述,我们为了对多个同时出现的字符进行识别,还必须解决一个子问题,那就是要对这些同时出现的字符进行分割。我们注意到本文介绍的应用有一个特点,那就是最终用作输入的图形,是用户当场写下的,而非通过图片文件导入的静态图片。也就是说,我们拥有笔画产生过程中的全部动态信息,比如笔画的先后顺序,笔画的重叠关系等等。而且我们期望这些笔画基本都是横向书写的。考虑到这些信息,我们可以设计一种基本的分割规则:在水平面上的投影相重叠的笔画,我们就认为它们同属于一个数字。笔画和水平方向上投影的关系示意如下图:因此书写时,就要求不同的数字之间尽量隔开。当然为了尽可能处理不经意的重叠,我们还可以为重叠部分相对每一笔画的位置设定一个阈值,如至少进入笔画一端的10%以内。加入对重叠的容忍阈值后,对笔画的分割的结果可以参看下图。在分割后被认为是属于同一字符的笔画我们使用了相同的颜色绘制,并且用不同的颜色区分了不属于同一字符的笔画。在字符的上方,我们用一系列水平方向的半透明色块表现了每一笔画在水平方向上的的有效重叠区域和字符之间的重叠关系。应用这样的规则后,我们就能简便而又有效地对多个笔画进行分割,并能利用Visual Studio Tools for AI提供的批量推理功能,一次性对所有分割出的图形做推理。应用的构建和理解完成应用同“手写识别应用入门”类似,我们还是先于GitHub克隆主体的应用代码,再加以引用模型来完成本文中这款应用。按照训练模型一节中所述,获取上面提到的Git仓库后,我们可以通过Visual Studio打开./extended_mnist_calculator目录下的MnistDemo.sln解决方案,并和之前一样,在解决方案里添加AI Tools – Inference模型项目。不过与上一博客稍有不同的是,为了对我们扩展的新模型加以区分,我们需要将新模型项目命名为ExtendedModel(同时也是默认的命名空间名字),并将新的模型包装类命名为MnistExtension。并且这一次,在模型项目创建向导中,我们需要选择上文中训练出的新模型。新的Inference模型项目和模型包装类配置如下图:理解代码输入处理在新应用的代码部分,和我们在手写数字识别博客中介绍的代码比起来,差别最大的地方就在于如何处理输入。在上个案例中,我们只需要简单地将正方形区域中的图像格式调整一下,即可用作MNIST模型的输入。而在本文的案例中,我们必须先对笔画进行分割处理。分割笔画之后我们再将每一个笔画组合转换成MNIST模型所需的单个输入。新应用需要响应的界面事件,还是和之前一致:需要响应鼠标的按下、移动和抬起三类事件。我们对其中按下和移动的响应事件的修改比较简单,我们只需要在这些响应时间里对新写下的笔画做记录就好了。记录笔画的产生过程首先我们为窗体类新增一个List<Point>类型的字段,用于记录每次鼠标按下、抬起之间鼠标移动过的点,将这些点按顺序连接起来就形成了一道笔画。我们在鼠标按下事件里清空以前记录的所有鼠标移动点,以便记录这次书写产生的新一动点;并在鼠标抬起事件里将这些点转换成笔画对应的数据结构StrokeRecord(定义见后文)。同样的,我们也为窗体类新增一个List<StrokeRecord>类型的字段,用于记录已经写下的所有笔画。private List<Point> strokePoints = new List<Point>(); private List<StrokeRecord> allStrokes = new List<StrokeRecord>();在writeArea_MouseDown方法中新增以下语句用于清空以前记录的鼠标移动点:strokePoints.Clear();并在writeArea_MouseMove方法中记录鼠标这次移动所到达的点:strokePoints.Add(e.Location);在writeArea_MouseUp方法里将这次鼠标按下、抬起之间产生的所有点转换成笔画对应的数据结构。并且因为如果鼠标在抬起之前并没有移动,就不会有点被记录,在这之前我们还通过strokePoints.Any()先判断一下是否有点被记录。下面是转化移动点的代码:var thisStrokeRecord = new StrokeRecord(strokePoints); allStrokes.Add(thisStrokeRecord);包括构造函数在内的StrokeRecord结构定义如下:/// <summary> /// 用于记录历史笔画信息的数据结构。 /// </summary> class StrokeRecord { public StrokeRecord(List<Point> strokePoints) { // 拷贝所有Point以避免列表在外部被修改。 Points = new List<Point>(strokePoints); HorizontalStart = Points.Min(pt => pt.X); HorizontalEnd = Points.Max(pt => pt.X); HorizontalLength = HorizontalEnd - HorizontalStart; OverlayMaxStart = HorizontalStart + (int)(HorizontalLength * (1 - ProjectionOverlayRatioThreshold)); OverlayMinEnd = HorizontalStart + (int)(HorizontalLength * ProjectionOverlayRatioThreshold); } /// <summary> /// 构成这一笔画的点。 /// </summary> public List<Point> Points { get; } /// <summary> /// 这一笔画在水平方向上的起点。 /// </summary> public int HorizontalStart { get; } /// <summary> /// 这一笔画在水平方向上的终点。 /// </summary> public int HorizontalEnd { get; } /// <summary> /// 这一笔画在水平方向上的长度。 /// </summary> public int HorizontalLength { get; } /// <summary> /// 另一笔画必须越过这些阈值点,才被认为和这一笔画重合。 /// </summary> public int OverlayMaxStart { get; } public int OverlayMinEnd { get; } private bool CheckPosition(StrokeRecord other) { return (other.HorizontalStart < OverlayMaxStart) || (OverlayMinEnd < other.HorizontalEnd); } /// <summary> /// 检查另一笔画是否和这一笔画重叠。 /// </summary> /// <param name="other"></param> public bool OverlayWith(StrokeRecord other) { return this.CheckPosition(other) || other.CheckPosition(this); } }分割笔画在将新产生的笔画添加到所有笔画的列表中之后,我们就有了当前用户写下的所有笔画了,接下来我们要对这些笔画进行分组。本文在这里对上文所述的“快速”分割的实现非常简单。在按笔画在水平方向上最左端的坐标,将笔画有小到大排序后,我们从最左边开始扫描所有笔画。如果一个笔画还没有分组,我们就为它指定唯一分组编号,然后再看其右侧有哪些笔画和当前笔画在水平方向上的投影是有效重合的(如上文所述,此处有阈值10%),并将这些重合的笔画定为属于同一组。直到所有笔画都被扫描。allStrokes = allStrokes.OrderBy(s => s.HorizontalStart).ToList(); int[] strokeGroupIds = new int[allStrokes.Count]; int nextGroupId = 1; for (int i = 0; i < allStrokes.Count; i++) { // 为了避免水平方向太多笔画被连在一起,我们采取一种简单的办法: // 当1、2笔画重叠时,我们就不会在检查笔画2和更右侧笔画是否重叠。 if (strokeGroupIds[i] != 0) { continue; } strokeGroupIds[i] = nextGroupId; nextGroupId++; var s1 = allStrokes[i]; for (int j = 1; i + j < allStrokes.Count; j++) { var s2 = allStrokes[i + j]; if (s2.HorizontalStart < s1.OverlayMaxStart) // 先判断临界条件(阈值10%) { if (strokeGroupIds[i + j] == 0) { if (s1.OverlayWith(s2)) // 在考虑阈值的条件下做完整地判断重合 { strokeGroupIds[i + j] = strokeGroupIds[i]; } } } else { break; } } }之后即可按对应的分组编号将笔画归组:List<IGrouping<int, StrokeRecord>> groups = allStrokes .Zip(strokeGroupIds, Tuple.Create) .GroupBy(tuple => tuple.Item2, tuple => tuple.Item1) // Item2是分组编号, Item1是StrokeRecord .ToList();小提示为了方便理解笔画的分割效果,应用界面上预留了“显示笔画分组”的开关。勾选之后写下的笔画会像上文那样被不同的颜色标记出其所在的分组。为每个分组生成单一位图分割完成后,我们得到了一个数组groups,它的每个元素都是一个分组,包括了分组编号和组内的所有笔画。这里我们得到的每一个分组都对应着一个字符。如果分组里有多个笔画,那么这些笔画就是这个字符的组成部分(想象加号和乘号,它们都需要两笔才能写成)。我们可以想到,这个数组groups里的元素的顺序是很重要的,因为我们要保证最终识别出的表达式里的字符的顺序,才能正确地计算表达式。我们在循环中顺序访问groups的每个元素。命名循环变量为group:foreach (IGrouping<int, StrokeRecord> group in groups)循环变量group的类型是IGrouping<int, StrokeRecord>,它代表着一个分组,包括分组的编号(一个整数)和其中的元素(元素都是StrokeRecord)。IGrouping<TKey, TElement>泛型接口同时也是一个可迭代的IEnumerable<TElement>泛型接口,所以我们可以把group变量直接当做IEnumerable<StrokeRecord>类型的对象来使用。然后我们需要确定这个分组(即其中所有笔画组合成的图形)的位置区域,其中我们最关心水平方向上最左端、最右端的坐标(水平方向的坐标轴是从左向右的)。通过这两个坐标我们就能确定该分组在水平方向上的投影的长度。我们计算这个长度的目的,是为了在我们为每个分组生成单一位图时,尽量将这个分组的图形放置在单一位图的中间位置。虽然我们还是先创建一个大尺寸的正方形位图(边长为绘图区高度),但是分割后的图形在这个正方形区域上不再具有天然的位置。下面的代码进行了这些位置的计算,和居中该分组所需的水平方向的偏移量的计算:var groupedStrokes = group.ToList(); // IGrouping<TKey, TElement>本质上也是一个可迭代的IEnumerable<TElement> // 确定整个分组的所有笔画的范围。 int grpHorizontalStart = groupedStrokes.Min(s => s.HorizontalStart); int grpHorizontalEnd = groupedStrokes.Max(s => s.HorizontalEnd); int grpHorizontalLength = grpHorizontalEnd - grpHorizontalStart; int canvasEdgeLen = writeArea.Height; Bitmap canvas = new Bitmap(canvasEdgeLen, canvasEdgeLen); Graphics canvasGraphics = Graphics.FromImage(canvas); canvasGraphics.Clear(Color.White); // 因为我们提取了每个笔画,就不能把长方形的绘图区直接当做输入了。 // 这里我们把宽度小于 writeArea.Height 的分组在 canvas 内居中。 int halfOffsetX = Math.Max(canvasEdgeLen - grpHorizontalLength, 0) / 2;之后我们就在新创建出的位图上绘制当前分组内的笔画了(通过canvasGraphics对象进行绘制):foreach (var stroke in groupedStrokes) { Point startPoint = stroke.Points[0]; foreach (var point in stroke.Points.Skip(1)) { var from = startPoint; var to = point; // 因为每个分组都是在长方形的绘图区被记录的,所以在单一位图上,需要先减去相对于长方形绘图区的偏移量 grpHorizontalStart from.X = from.X - grpHorizontalStart + halfOffsetX; to.X = to.X - grpHorizontalStart + halfOffsetX; canvasGraphics.DrawLine(penStyle, from, to); startPoint = point; } }批量推理在新应用中,我们一次需要识别多个字符。而以前我们一次只需要识别一个字符,哪怕我们每次都为了识别一个字符调用了一次模型的推理方法(model.Infer(...))。不过我们现在已经准备好了多组数据,这使得我们有机会利用底层AI框架的并行处理能力,来加速我们的推理过程,还省去了手动处理多线程的麻烦。在这里我们采用Visual Studio Tools for AI提供的批量推理功能,一次对所有数据进行推理并得到全部结果。首先我们在为所得分组创建位图之前,需要先创建一个用于储存所有数据的动态数组:var batchInferInput = new List<IEnumerable<float>>();在处理所有分组的循环内部,处理完每个分组后,我们需要将该分组对应的像素数据暂时存放在动态数组batchInferInput中:// 1. 将分割出的笔画图片缩小至 28 x 28,与训练数据格式一致。 Bitmap clonedBmp = new Bitmap(canvas, ImageSize, ImageSize); var image = new List<float>(ImageSize * ImageSize); for (var x = 0; x < ImageSize; x++) { for (var y = 0; y < ImageSize; y++) { var color = clonedBmp.GetPixel(y, x); image.Add((float)(0.5 - (color.R + color.G + color.B) / (3.0 * 255))); } } // 将这一组笔画对应的矩阵保存下来,以备批量推理。 batchInferInput.Add(image);可以看到我们对每个分组的处理,都和以前对整个正方形绘图区的像素的处理,是完全一致的。唯一的不同是在以前的应用代码中,List<IEnumerable<float>>类型的数组(在上文中为batchInferInput变量)仅有一个元素,就是唯一一张位图的像素数据。而在本文中这个数组可能有很多元素,每个元素都是一组位图数据。对这样的位图数据集合进行批量推理后,得到的结果(即inferResult变量)是一个可枚举的类型,我们叫它“第一层枚举”。第一层枚举得到的每个元素也是一个可枚举类型,我们叫它“第二层枚举”。第一层枚举中的每个元素都对应着一组位图数据的推理结果。同时第一层枚举也是对应着批量推理的输入数组,枚举的结果总数和输入数组的长度相同。对于第二层枚举,由于我们的推理结果只是一个整数,所以第二层枚举总是只有一个元素。我们可以通过.First()将其取出。这里我们可以看到,在以前的应用代码里,我们通过inferResult.First().First()取出了唯一的结果,而在这里我们则需要考虑批量推理结果的二维结构。进行推理的代码如下:// 2. 进行批量推理 // batchInferInput 是一个列表,它的每个元素都是一次推量的输入。 IEnumerable<IEnumerable<long>> inferResult = model.Infer(batchInferInput); // 推量的结果是一个可枚举对象,它的每个元素代表了批量推理中一次推理的结果。我们用 仅一次.First() 将它们的结果都取出来,并格式化。 outputText.Text = string.Join("", inferResult.Select(singleResult => singleResult.First().ToString()));计算表达式至此,我们对于多个手写字符的识别就完成了。我们已经得到了可以表示用户手写图形的、易于计算机程序处理的字符串。接下来我们开始对字符串记载的数学表达式进行计算。本文需要计算的数学表达式的格式,由上文的数据准备和模型训练部分可知,是相对简单的。其中只涉及数字0-9、加减乘除和小括号。对这样的表达式进行求值,是一种非常典型的问题。因为这样的数学表达式有非常清晰、确定的语法规则,对其最直观的处理方法,就是先根据其语法进行解析,构造语法树后进行求值即可。或者,因为这种问题非常经典,我们也可以寻找已有的组件来解决这个问题。本文直接复用System.Data.DataTable类提供的Compute方法来进行表达式的计算。这个方法完全支持本文案例中出现的表达式语法。因为表达式的计算这部分逻辑边界非常清晰,我们引入一个独立的方法来获取最后的结果:string EvaluateAndFormatExpression(List<int> recognizedLabels)EvaluateAndFormatExpression方法接受一个标签序列,其中我们仍在用整数10-15来表示各种数学符号。在这个方法内我们对字符标签做两种映射,分别将标签序列转换成用于输入到计算器进行求值的,和用于在用户界面上展示的。EvaluateAndFormatExpression方法的返回结果形如“(3+2)÷2=2.5”。其中各种符号皆采用传统的数学写法。该方法的实现如下:private string EvaluateAndFormatExpression(List<int> recognizedLabels) { string[] operatorsToEval = { "+", "-", "*", "/", "(", ")" }; string[] operatorsToDisplay = { "+", "-", "×", "÷", "(", ")" }; string toEval = string.Join("", recognizedLabels.Select(label => { if (0 <= label && label <= 9) { return label.ToString(); } return operatorsToEval[label - 10]; })); var evalResult = new DataTable().Compute(toEval, null); if (evalResult is DBNull) { return "Error"; } else { string toDisplay = string.Join("", recognizedLabels.Select(label => { if (0 <= label && label <= 9) { return label.ToString(); } return operatorsToDisplay[label - 10]; })); return $"{toDisplay}={evalResult}"; } 同时需要注意的是,根据表达式求值方案的不同,我们可能需要对表达式中的字符进行对应的调整。比如当我们希望在用户界面上将除号显示为更可读的“÷”时,我们采用的求值方案可能并不支持这种除号,而只支持C#语言中的除号/。那么我们在将识别出的结果输入到表达式计算器中之前,还需要对识别的结果进行合适的映射。常见问题新模型对括号和数字1的识别很差这是一种非常容易出现的情况。因为在手写时,正反小括号和数字1极易混淆。这一问题有时会在扩展数据中体现。我们观察到原始MNIST数据集中(参见上文的数据可视化),很多数字1的形状和弯曲程度已经和括号相近。如果我们在扩展数据部分不做明显的区分,并且我们采用的卷积神经网络对这样微小的数据差别不敏感的话,就会导致造型相近的字符被错误识别的情况。同理,这样的问题还可能发生在加号和乘号之间。因为加号和乘号的形状基本完全一样,只是靠角度得以区分。如果我们搜集的扩展数据里,这两种符号各自都具有一定的旋转角度,以致角度区分不够明显,这也会导致模型对其识别能力不强的情况出现。扩展问题经过一番扩展,我们的新应用已经具备一些不错的功能,初步满足了现实规格的应用需求。从本文的案例中,我们也能得到关于如何将人工智能和传统的技术手段融合起来,帮助我们更好地解决问题的一些启示。当然,这款新应用仍然不够强大和健壮。对此,我们注意到有这样一些问题仍待解决:笔画分割的算法相对比较简单、粗糙,如何提升整体的分割效果,以顺利处理重叠、连笔、噪点等可能情况?作为一款计算器应用,本文介绍的新应用具备的特性还是很少。如何增加新的数学计算特性,比如开根号、分数或者更多的数学符号?欢迎使用Markdown编辑器你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。新的改变我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写文章:全新的界面设计 ,将会带来全新的写作体验;在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;全新的 KaTeX数学公式 语法;增加了支持甘特图的mermaid语法1 功能;增加了 多屏幕编辑 Markdown文章功能;增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;增加了 检查列表 功能。功能快捷键撤销:Ctrl/Command + Z重做:Ctrl/Command + Y加粗:Ctrl/Command + Shift + B斜体:Ctrl/Command + Shift + I标题:Ctrl/Command + Shift + H无序列表:Ctrl/Command + Shift + U有序列表:Ctrl/Command + Shift + O检查列表:Ctrl/Command + Shift + C插入代码:Ctrl/Command + Shift + K插入链接:Ctrl/Command + Shift + L插入图片:Ctrl/Command + Shift + G合理的创建标题,有助于目录的生成直接输入1次#,并按下space后,将生成1级标题。输入2次#,并按下space后,将生成2级标题。以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。如何改变文本的样式强调文本 强调文本加粗文本 加粗文本标记文本删除文本引用文本H2O is是液体。210 运算结果是 1024.插入链接与图片链接: link.图片: 带尺寸的图片: 当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。如何插入一段漂亮的代码片去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.// An highlighted block var foo = 'bar';生成一个适合你的列表项目1项目2项目3 计划任务 完成任务创建一个表格一个简单的表格是这么创建的: 设定内容居中、居左、居右使用:---------:居中使用:----------居左使用----------:居右 SmartyPantsSmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如: 创建一个自定义列表MarkdownText-to- HTML conversion toolAuthorsJohnLuke如何创建一个注脚一个具有注脚的文本。注释也是必不可少的Markdown将文本转换为 HTML。KaTeX数学公式您可以使用渲染LaTeX数学表达式 KaTeX:你可以找到更多关于的信息 LaTeX 数学表达式here.新的甘特图功能,丰富你的文章关于 甘特图 语法,参考 这儿,UML 图表可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::这将产生一个流程图。:graph LR A[长方形] -- 链接 --> B((圆)) A --> C(圆角长方形) B --> D{菱形} C --> D关于 Mermaid 语法,参考 这儿,FLowchart流程图我们依旧会支持flowchart的流程图:关于 Flowchart 流程图 语法,参考 这儿.导出与导入导出如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。导入如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,继续你的创作。
0
0
0
浏览量1612
金某某的算法生活

微软认知服务应用秘籍 – 漫画翻译篇

概述微软认知服务包括了影像、语音、语言、搜索、知识五大领域,通过对这些认知服务的独立或者组合使用,可以解决很多现实世界中的问题。作为AI小白,我们可以选择艰难地攀登崇山峻岭,也可以选择像牛顿一样站在巨人的肩膀上。本章节的内容就以"漫画翻译"为例,介绍如何灵活使用微软认知服务来实现自己的AI梦想。日本漫画非常著名,如海贼王,神探柯南等系列漫画在中国的少年一代中是非常普及。国内专门有一批志愿者,全手工翻译这些漫画为中文版本,过程艰辛复杂,花费时间很长。能否使用AI来帮助加快这个过程呢?小提示:漫画是有版权的,请大家要在尊重版权的前提下做合法的事。漫画翻译,要做的事情有三步:调用微软认知服务,用OCR(光学字符识别)服务识别出漫画上所有文字;调用微软认知服务,用Text Translate(文本翻译)服务把日文翻译成中文;自己写逻辑代码把中文文字贴回到以前的漫画中,覆盖以前的日文,生成新的漫画帧。下图是展示最后的翻译效果,左侧是原漫画,右侧是翻译成中文的结果:环境准备安装Windows 10版本 1803,低一些的Windows 10版本也可以使用。Windows 7也可以运行本示例程序,但不建议使用,Windows 7的官方技术支持到2020/01/14结束。小提示:如果您的机器不能运行Windows 10,说明硬件性能还是有些不够的。AI是建立在软硬件快速发展的基础上的,不建议您使用低配置的机器来做AI知识的学习。安装Visual Studio 2017 Community。点击这里下载,对于本案例,安装时选择".NET桌面开发"即可满足要求。申请微软认知服务密钥申请OCR服务密钥点击进入此页面: https://azure.microsoft.com/zh-cn/products/ai-services/?activetab=pivot:azureopenai%E6%9C%8D%E5%8A%A1tab 在上图所示页面中"计算机影像"下点击"免费试用":根据自己的实际情况选择以上三个选项之一,这里以选择第一个"来宾"选项为例:选择一个热爱的国家/地区,在上下两个复选框上("我同意","我接受")都打勾,点击"下一步":上图中以选择"Microsoft"账户为例继续:最后得到了上面这个页面,这里的密钥(Key)和终结点(Endpoint)要在程序中使用,请保存好!小提示:上面例子中的密钥只能再使用1天了,因为是7天的免费试用版本。所以当你的程序以前运行正常,某一天忽然从服务器不能得到正常的返回值时并且得到错误代码Unauthorized (401),请首先检查密钥状态。小提示:当试用的Key过期后,你是无法再申请试用Key的,只能申请正式的Key,这就要通过Azure门户。在Azure门户中申请好Computer Vision服务(包括OCR服务)的Key后,它会告诉你Endpoint是…../vision/v1.0,这个不用管它,在code里还保持……/vision/v2.0就可以了,两者的Key是通用的。申请Text Translate文本翻译服务密钥用自己的Azure账号登录Azure门户:在上图中点击左侧的"All resources":在上图中点击上方的 "+ Add"图标来创建资源,得到资源列表如下 :在上图中点击右侧列表中的"AI + Machine Learning",得到下图的具体服务项目列表:这里有个坑,文本翻译不在右侧的列表中,需要点击右上方的"See all"来展开所有项目:哦,好吧,还是没有!保持耐心,继续点击Cognitive Services栏目的右侧的"More"按钮,得到更详细的列表:还是没有?卷滚一下看看?到底,到底!OK,终于有了Translator Text,就是Ta:创建这个服务时,我们选择F0就可以了。如果要是做商用软件的话,你可以选择S1或其他,100万个字符才花10美元,不贵不贵!使用VS Tools for AI是不是以上申请Key的过程太复杂了?那是因为Azure内容庞杂,网页设计层次太多!其实这个过程是可以简化的,因为我们有个Visual Studio Tools for AI扩展包!打开VS2017,菜单上选择"工具(Tools)->扩展和更新(Extensions and Updates)",在弹出的对话框左侧选择"联机(Online)",在右侧上方输入"AI" 进行搜索,会看到"Microsoft Visual Studio Tools for AI"扩展包,下载完毕后关闭VS,这个扩展包就会自动安装。安装完毕后,再次打开VS2017,点击菜单View->Server Explorer。如果安装了Tools for AI,此时会看到以下界面:在AI Tools->Azure Cognitive Services下,可以看到我已经申请了2个service,ComputerVisionAPI和TranslateAPI就是我们想要的,这两个名字是自己在申请服务时指定的。假设你还没有这两个服务,那么在Azure Cognitive Services上鼠标右键,然后选择Create New Cognitive Service,出现以下对话框:在每个下拉框中显示的内容可能会每个人都不一样,绝大多数是用下拉框完成填充的,很方便。假设我想申请TextTranslation服务,那么我在Service Name上填写一个自己能看懂的名字就行了,比如我填写了"TranslateAPI",这样比较直接。同理可以创建ComputerVisionAPI服务。服务的名字不会在Code中使用。小结我们废了老鼻子劲,得到了以下两个REST API的Endpoint和相关的Key:OCR服务Endpoint: https://westcentralus.api.cognitive.microsoft.com/vision/v2.0Text Translate文本翻译服务Endpoint: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0小提示:以上两个Endpoint的URL是目前最新的版本,请不要使用旧的版本如v1.0等等。咱们是洗洗睡了,还是写代码?看天色还早,继续写代码吧!构建代码构建这个PC桌面应用,我们需要几个步骤:在得到第一次的显示结果后,经过测试,有很大可能会根据结果再对界面进行调整,实际上也是一个局部的软件工程中的迭代开发。界面设计启动Visual Studio 2017, 创建一个基于C#语言的WPF(Windows Presentation Foundation)项目:PF是一个非常成熟的技术,在有界面展示和交互的情况下,使用XAML设计/渲染引擎,比WinForm程序要强101倍,再加上有C#语言利器的帮助,是写PC桌面前端应用的最佳组合。给Project起个名字,比如叫"CartoonTranslate",选择最新的.NET Framework (4.6以上),然后点击"OK"。我们先一起来设计一下界面:Input URL:用于输入互联网上的一张漫画图片的URLEngine:指的是两个不同的算法引擎,其中,OCR旧引擎可以支持25种语言,识别效果可以接受;而Recognize Text新引擎目前只能支持英文,但效果比较好。Language:制定当前要翻译的漫画的语言,我们只以英文和日文为例,其它国家的漫画相对较少,但一通百通,一样可以支持。右侧的一堆Button了解一下:Show:展示Input URL中的图片到下面的图片区OCR:调用OCR服务Translate:调用文本翻译服务,将日文或者英文翻译成中文下侧大面积的图片区了解一下:Source Image:原始漫画图片Target Image:翻译成中文对白后的漫画图片界面设计代码我们在MainWindow.xaml文件里面填好以下code:<Window x:Class="CartoonTranslate.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:CartoonTranslate" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Grid.Row="0"> <TextBlock Grid.Row="0" Text="Input URL:"/> <TextBox x:Name="tb_Url" Grid.Row="1" Width="600" Text="http://stat.ameba.jp/user_images/20121222/18/secretcube/2e/19/j/o0800112012341269548.jpg"/> <Button x:Name="btn_Show" Content="Show" Click="btn_Show_Click" Width="100"/> <Button x:Name="btn_OCR" Content="OCR" Click="btn_OCR_Click" Width="100"/> <Button x:Name="btn_Translate" Content="Translate" Click="btn_Translate_Click" Width="100"/> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock Text="Engine:"/> <RadioButton x:Name="rb_V1" GroupName="gn_Engine" Content="OCR" Margin="20,0" IsChecked="True" Click="rb_V1_Click"/> <RadioButton x:Name="rb_V2" GroupName="gn_Engine" Content="Recognize Text" Click="rb_V2_Click"/> <TextBlock Text="Language:" Margin="20,0"/> <RadioButton x:Name="rb_English" GroupName="gn_Language" Content="English"/> <RadioButton x:Name="rb_Japanese" GroupName="gn_Language" Content="Japanese" IsChecked="True" Margin="20,0"/> </StackPanel> <Grid Grid.Row="3"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Source Image" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Column="2" Text="Target Image" VerticalAlignment="Center" HorizontalAlignment="Center"/> <Image x:Name="imgSource" Grid.Column="0" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/> <Image x:Name="imgTarget" Grid.Column="2" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/> <Canvas x:Name="canvas_1" Grid.Column="0"/> <Canvas x:Name="canvas_2" Grid.Column="2"/> </Grid> </Grid> </Window>处理事件关于XAML语法的问题不在本文的讨论范围之内。上面的XAML写好后,编译时会出错,因为里面定义了很多事件,在C#文件中还没有实现。所以,我们现在把事件代码补上。局部变量定义(在MainWindow.xaml.cs的MainWindow class里面): // using “OCR” or “Recognize Text” private string Engine; // source language, English or Japanese private string Language; // OCR result object private OcrResult.Rootobject ocrResult;按钮"Show"的事件点击Show按钮的事件,把URL中的漫画的地址所指向的图片加载到窗口中显示: private void btn_Show_Click(object sender, RoutedEventArgs e) { if (!Uri.IsWellFormedUriString(this.tb_Url.Text, UriKind.Absolute)) { // show warning message return; } // show image at imgSource BitmapImage bi = new BitmapImage(); bi.BeginInit(); bi.UriSource = new Uri(this.tb_Url.Text); bi.EndInit(); this.imgSource.Source = bi; this.imgTarget.Source = bi; }在上面的代码中,同时给左右两个图片区域赋值,显示两张一样的图片。按钮"OCR"的事件点击OCR按钮的事件,会调用OCR REST API,然后根据返回结果把所有识别出来的文字用红色的矩形框标记上:private async void btn_OCR_Click(object sender, RoutedEventArgs e) { this.Engine = GetEngine(); this.Language = GetLanguage(); if (Engine == "OCR") { ocrResult = await CognitiveServiceAgent.DoOCR(this.tb_Url.Text, Language); foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { if (line.Convert()) { Rectangle rect = new Rectangle() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Width = line.BB[2], Height = line.BB[3], Stroke = Brushes.Red, //Fill =Brushes.White }; this.canvas_1.Children.Add(rect); } } } } else { } }在上面的代码中,通过调用DoOCR()自定义函数返回了反序列化好的类,再依次把返回结果集中的每个矩形生成一个Rectangle图形类,它的left和top用Margin的方式来定义,width和height直接赋值即可,把这些Rectangle图形类的实例添加到canvas_1的Visual Tree里即可显示出来(这个就是WPF的好处啦,不用处理绘图事件,但性能不如用Graphics类直接绘图)。按钮"Translate"的事件点击Translate按钮的事件:private async void btn_Translate_Click(object sender, RoutedEventArgs e) { List<string> listTarget = await this.Translate(); this.ShowTargetText(listTarget); } private async Task<List<string>> Translate() { List<string> listSource = new List<string>(); List<string> listTarget = new List<string>(); if (this.Version == "OCR") { foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { listSource.Add(line.TEXT); if (listSource.Count >= 25) { List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans"); listTarget.AddRange(listOutput); listSource.Clear(); } } } if (listSource.Count > 0) { List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans"); listTarget.AddRange(listOutput); } } return listTarget; } private void ShowTargetText(List<string> listTarget) { int i = 0; foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { string translatedLine = listTarget[i]; Rectangle rect = new Rectangle() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Width = line.BB[2], Height = line.BB[3], Stroke = null, Fill =Brushes.White }; this.canvas_2.Children.Add(rect); TextBlock tb = new TextBlock() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Height = line.BB[3], Width = line.BB[2], Text = translatedLine, FontSize = 16, TextWrapping = TextWrapping.Wrap, Foreground = Brushes.Red }; this.canvas_2.Children.Add(tb); i++; } } }上面这段代码中,包含了两个函数:this.Translate()和this.ShowTargetText()。我们先看第一个函数:最难理解的地方可能是有个"25"数字,这是因为Translate API允许一次提交多个字符串并一起返回结果,这样比你提交25次字符串要快的多。翻译好的结果按顺序放在listOutput里,供后面使用。再看第二个函数:先根据原始文字的矩形区域,生成一些白色的实心矩形,把它们贴在右侧的目标图片上,达到把原始文字覆盖(扣去)的目的。然后再根据每个原始矩形生成一个TextBlock,设定好它的位置和尺寸,再设置好翻译后的结果(translatedLine),这样就可以把中文文字贴到图上了。选项按钮的事件点击Radio Button的事件:private void rb_V1_Click(object sender, RoutedEventArgs e) { this.rb_Japanese.IsEnabled = true; } private void rb_V2_Click(object sender, RoutedEventArgs e) { this.rb_English.IsChecked = true; this.rb_Japanese.IsChecked = false; this.rb_Japanese.IsEnabled = false; } private string GetLanguage() { if (this.rb_English.IsChecked == true) { return "en"; } else { return "ja"; } } private string GetEngine() { if (this.rb_V1.IsChecked == true) { return "OCR"; } else { return "RecText"; } } API数据访问部分我们需要在CatroonTranslate工程中添加以下三个.cs文件:CognitiveServiceAgent.csOcrResult.csTranslateResult.cs与认知服务交互CognitiveServiceAgent.cs文件完成与REST API交互的工作,包括调用OCR服务的和调用翻译服务的代码:using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Web; namespace CartoonTranslate { class CognitiveServiceAgent { const string OcrEndPointV1 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/ocr?detectOrientation=true&language="; const string OcrEndPointV2 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/recognizeText?mode=Printed"; const string VisionKey1 = "4c20ac56e1e7459a05e1497270022b"; const string VisionKey2 = "97992f0987e4be6b5be132309b8e57"; const string UrlContentTemplate = "{{\"url\":\"{0}\"}}"; const string TranslateEndPoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from={0}&to={1}"; const string TKey1 = "04023df3a4c499b1fc82510b48826c"; const string TKey2 = "9f76381748549cb503dae4a0d80a80"; public static async Task<List<string>> DoTranslate(List<string> text, string fromLanguage, string toLanguage) { try { using (HttpClient hc = new HttpClient()) { hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", TKey1); string jsonBody = CreateJsonBodyElement(text); StringContent content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); string uri = string.Format(TranslateEndPoint, fromLanguage, toLanguage); HttpResponseMessage resp = await hc.PostAsync(uri, content); string json = await resp.Content.ReadAsStringAsync(); var ro = Newtonsoft.Json.JsonConvert.DeserializeObject<List<TranslateResult.Class1>>(json); List<string> list = new List<string>(); foreach(TranslateResult.Class1 c in ro) { list.Add(c.translations[0].text); } return list; } } catch (Exception ex) { Debug.WriteLine(ex.Message); return null; } } private static string CreateJsonBodyElement(List<string> text) { var a = text.Select(t => new { Text = t }).ToList(); var b = JsonConvert.SerializeObject(a); return b; } /// <summary> /// /// </summary> /// <param name="imageUrl"></param> /// <param name="language">en, ja, zh</param> /// <returns></returns> public static async Task<OcrResult.Rootobject> DoOCR(string imageUrl, string language) { try { using (HttpClient hc = new HttpClient()) { ByteArrayContent content = CreateHeader(hc, imageUrl); var uri = OcrEndPointV1 + language; HttpResponseMessage resp = await hc.PostAsync(uri, content); string result = string.Empty; if (resp.StatusCode == System.Net.HttpStatusCode.OK) { string json = await resp.Content.ReadAsStringAsync(); Debug.WriteLine(json); OcrResult.Rootobject ro = Newtonsoft.Json.JsonConvert.DeserializeObject<OcrResult.Rootobject>(json); return ro; } } return null; } catch (Exception ex) { Debug.Write(ex.Message); return null; } } private static ByteArrayContent CreateHeader(HttpClient hc, string imageUrl) { hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", VisionKey1); string body = string.Format(UrlContentTemplate, imageUrl); byte[] byteData = Encoding.UTF8.GetBytes(body); var content = new ByteArrayContent(byteData); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return content; } } } 小提示:以上两个Key是无法直接使用的,请使用自己申请的Key。其中,DoTranslate()函数和DoOCR()函数都是HTTP调用,很容易理解。只有CreateJsonBodyElement函数需要解释一下。前面提到过我们一次允许给服务器提交25个字符串做批量翻译,因此传进来的是个List<string>,经过这个函数的简单处理,会得到以下JSON格式的数据作为HTTP的Body:// JSON Data as Body [ {“Text” : ”第1个字符串”}, {“Text” : ”第2个字符串”}, …….. {“Text” : ”第25个字符串”}, ] OCR服务的数据类定义 OcrResult.cs文件是OCR服务返回的JSON数据所对应的类,用于反序列化: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CartoonTranslate.OcrResult { public class Rootobject { public string language { get; set; } public string orientation { get; set; } public float textAngle { get; set; } public Region[] regions { get; set; } } public class Region { public string boundingBox { get; set; } public Line[] lines { get; set; } } public class Line { public string boundingBox { get; set; } public Word[] words { get; set; } public int[] BB { get; set; } public string TEXT { get; set; } public bool Convert() { CombineWordToSentence(); return ConvertBBFromString2Int(); } private bool ConvertBBFromString2Int() { string[] tmp = boundingBox.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (tmp.Length == 4) { BB = new int[4]; for (int i = 0; i < 4; i++) { int.TryParse(tmp[i], out BB[i]); } return true; } return false; } private void CombineWordToSentence() { StringBuilder sb = new StringBuilder(); foreach (Word word in words) { sb.Append(word.text); } this.TEXT = sb.ToString(); } } public class Word { public string boundingBox { get; set; } public string text { get; set; } } }需要说明的是,服务器返回的boundingBox是个string类型,在后面使用起来不太方便,需要把它转换成整数,所以增加了CovertBBFromString2Int()函数。还有就是返回的是一个个的词(Word),而不是一句话,所以增加了CombineWordToSentence()来把词连成句子。翻译服务的数据类定义TranslateResult.cs文件翻译服务返回的JSON所对应的类,用于反序列化:namespace CartoonTranslate.TranslateResult { public class Class1 { public Translation[] translations { get; set; } } public class Translation { public string text { get; set; } public string to { get; set; } } }小提示:在VS2017中,这种类不需要手工键入,可以在Debug模式下先把返回的JSON拷贝下来,然后新建一个.cs文件,在里面用Paste Special从JSON直接生成类就可以了。运行程序好啦,大功告成!现在要做的事就是点击F5来编译执行程序。如果一切顺利的话,将会看到界面设计部分所展示的窗口。我们第一步先点击"Show"按钮,会得到:再点击"OCR"按钮,等两三秒(取决于网络速度),会看到左侧图片中红色的矩形围拢的一些文字。有些文字没有被识别出来的话,就没有红色矩形。最后点击"Translate"按钮,稍等一小会儿,会看到右侧图片的变化:Wow! 大部分的日文被翻译成了中文,而且位置也摆放得很合适。习题与进阶学习增加容错代码让程序健壮目前的代码中没有很多容错机制,比如当服务器返回错误时,访问API的代码会返回一个NULL对象,在上层没有做处理,直接崩溃。再比如,当用户不按照从左到右的顺序点击上面三个button时,会产生意想不到的情况。改进本应用让其自动化和产业化本应用处理单页的漫画,并且提供了交互,目的是让大家直观理解工作过程,实际上这个过程可以做成批量自动化的,也就是输入一大堆URL,做后台识别/翻译/重新生成图片后,把图片批量保存在本地,再进行后处理。当然,识别引擎不是万能的,很多时候不可能准确识别/翻译出所有对白文字。所以,可以考虑提供一个类似本应用的交互工具,让漫画翻译从业者在机器处理之后,对有错误的地方进行纠正。小提示:请严格遵守知识产权保护法!在合法的情况下做事。使用新版本的Engine做字符识别还记得前面提到过新旧引擎的话题吗?我们在界面上做了一个Radio Button "Recognize Text",但是并没有使用它。因为这个新引擎目前还只能支持英文的OCR,所以,如果大家对漫威Marvel漫画(英文为主)感兴趣的话,就可以用到这个引擎啦,与旧OCR引擎相比,不能同日而语,超级棒!旧OCR引擎的文档在这里:https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/56f91f2e778daf14a499e1fc新Recognize Text引擎的文档在这里:https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/587f2c6a154055056008f200新的引擎在API交互设计上,有一个不同的地方:当你提交一个请求后,服务器会立刻返回Accepted (202),然后给你一个URL,用于查询状态的。于是需要在客户端程序里设个定时器,每隔一段时间(比如200ms),访问那个URL,来获得最终的OCR结果。返回的结果JSON格式也有所不同,大家可以自己试着实现一下:OCR纠错处理在下图中,如绿色椭圆区域所示,OCR引擎犯了一个小错误,它把上下两个不同对白气泡的文字框在了一起。这个是可以在自己的程序里做后期纠错处理来矫正的。大家可以仔细分析OCR的返回结果,看看如何实现。文档在这里:https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/56f91f2e778daf14a499e1fc聚类处理待翻译的文字观察力好的同学,可能会发现一个问题,如下图所示,左侧图的一个对白气泡里,有四句话,但其实它们是一句话,分开写到4列而已。这种情况带来的问题是:这四句话分别送给翻译引擎做翻译,会造成前后不连贯,语句不通顺。可以考虑的解决方案是,先根据矩形的位置信息,把这四句话合并成同一句话,再送给翻译引擎。这就是标准的聚类问题,通过搜索引擎可以找到一大堆参考文档,比如这些:https://www.ibm.com/developerworks/cn/analytics/library/ba-1607-clustering-algorithm/index.html
0
0
0
浏览量1382
金某某的算法生活

AI应用开发实战系列之三:手写识别应用入门

AI应用开发实战 - 手写识别应用入门手写体识别的应用已经非常流行了,如输入法,图片中的文字识别等。但对于大多数开发人员来说,如何实现这样的一个应用,还是会感觉无从下手。本文从简单的MNIST训练出来的模型开始,和大家一起入门手写体识别。在本教程结束后,会得到一个能用的AI应用,也许是你的第一个AI应用。虽然离实际使用还有较大的距离(具体差距在文章后面会分析),但会让你对AI应用有一个初步的认识,有能力逐步搭建出能够实际应用的模型。准备工作使用win10 64位操作系统的计算机参考上一篇博客AI应用开发实战 - 从零开始配置环境。在电脑上训练并导出MNIST模型。一、 思路通过上一篇文章搭建环境的介绍后,就能得到一个能识别单个手写数字的模型了,并且识别的准确度会在98%,甚至99%以上了。那么我们要怎么使用这个模型来搭建应用呢?大致的步骤如下:实现简单的界面,将用户用鼠标或者触屏的输入变成图片。将生成的模型包装起来,成为有公开数据接口的类。将输入的图片进行规范化,成为数据接口能够使用的格式。最后通过模型来推理(inference)出图片应该是哪个数字,并显示出来。是不是很简单?二、动手步骤一:获取手写的数字提问:那我们要怎么获取手写的数字呢?回答:我们可以写一个简单的WinForm画图程序,让我们可以用鼠标手写数字,然后把图片保存下来。首先,我们打开Visual Studio,选择文件->新建->项目。在弹出的窗口里选择Visual C#->Windows窗体应用,项目名称不妨叫做DrawDigit,解决方案名称不妨叫做MnistForm,点击确定。此时,Visual Studio也自动弹出了一个窗口的设计图。在DrawDigit项目上点击右键,选择属性,在生成一栏将平台目标从Any CPU改为x64。否则,DrawDigit(首选32位)与它引用的MnistForm(64位)的编译平台不一致会引发System.BadImageFormatException的异常。然后我们对这个窗口做一些简单的修改:首先我们打开VS窗口左侧的工具箱,这个窗口程序需要以下三种组件:1. PictureBox:用来手写数字,并且把数字保存成图片2. Label:用来显示模型的识别结果3. Button:用来清理PictureBox的手写结果那经过一些简单的选择与拖动还有调整大小,这个窗口现在是这样的:一些注意事项这些组件都可以通过右键->查看属性,在属性里修改它们的设置为了方便把PictureBox里的图片转化成Mnist能识别的格式,PictureBox的需要是正方形可以给这些控件起上有意义的名称。可以调整一下label控件大小、字体等,让它更美观。经过一些简单的调整,这个窗口现在是这样的:现在来让我们愉快地给这些组件添加事件!还是在属性窗口,我们选择某个组件,右键->查看属性,点击闪电符号,给组件绑定对应的事件。每次绑定后,会跳到代码部分,生成一个空函数。点回设计视图继续操作即可。然后我们开始补全对应的函数体内容。注意,如果在上面改变了控件的名称,下面的代码需要做对应的更改。废话少说上代码!using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Drawing.Drawing2D;//用于优化绘制的结果 using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using MnistModel; namespace DrawDigit { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private Bitmap digitImage;//用来保存手写数字 private Point startPoint;//用于绘制线段,作为线段的初始端点坐标 private Mnist model;//用于识别手写数字 private const int MnistImageSize = 28;//Mnist模型所需的输入图片大小 private void Form1_Load(object sender, EventArgs e) { //当窗口加载时,绘制一个白色方框 model = new Mnist(); digitImage = new Bitmap(pictureBox1.Width, pictureBox1.Height); Graphics g = Graphics.FromImage(digitImage); g.Clear(Color.White); pictureBox1.Image = digitImage; } private void clean_click(object sender, EventArgs e) { //当点击清除时,重新绘制一个白色方框,同时清除label1显示的文本 digitImage = new Bitmap(pictureBox1.Width, pictureBox1.Height); Graphics g = Graphics.FromImage(digitImage); g.Clear(Color.White); pictureBox1.Image = digitImage; label1.Text = ""; } private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { //当鼠标左键被按下时,记录下需要绘制的线段的起始坐标 startPoint = (e.Button == MouseButtons.Left) ? e.Location : startPoint; } private void pictureBox1_MouseMove(object sender, MouseEventArgs e) { //当鼠标在移动,且当前处于绘制状态时,根据鼠标的实时位置与记录的起始坐标绘制线段,同时更新需要绘制的线段的起始坐标 if (e.Button == MouseButtons.Left) { Graphics g = Graphics.FromImage(digitImage); Pen myPen = new Pen(Color.Black, 40); myPen.StartCap = LineCap.Round; myPen.EndCap = LineCap.Round; g.DrawLine(myPen,startPoint, e.Location); pictureBox1.Image = digitImage; g.Dispose(); startPoint = e.Location; } } private void pictureBox1_MouseUp(object sender, MouseEventArgs e) { //当鼠标左键释放时 //同时开始处理图片进行推理 //暂时不处理这里的代码 } } }将模型包装成一个C#是整个过程中比较麻烦的一步。所幸的是,Tools for AI对此提供了很好的支持。进一步了解,可以看这里。首先,我们在解决方案MnistForm下点击鼠标右键,选择添加->新建项目,在弹出的窗口里选择AI Tools->Inference->模型推理类库,名称不妨叫做MnistModel,点击确定,于是我们又多了一个项目,然后自己配置好这个项目的名称、位置,点击确定。然后弹出一个模型推理类库创建向导,这个时候就需要我们选择自己之前训练好的模型了~首先在模型路径里选择保存的模型文件的路径。这里我们使用在AI应用开发实战 - 从零开始配置环境博客中训练并导出的模型note:模型可在/samples-for-ai/examples/tensorflow/MNIST目录下找到,其中output文件夹保存了检查点文件,export文件夹保存了模型文件。对于TensorFlow,我们可以选择检查点的.meta文件,或者是保存的模型的.pb文件这里我们选择在AI应用开发实战 - 从零开始配置环境这篇博客最后生成的export目录下的检查点的SavedModel.pb文件,这时程序将自动配置好配置推理接口,见下图:类名可以自己定义,因为我们用的是MNIST,那么类名就叫Mnist好了,然后点击确定。这样,在解决方案资源管理器里,在解决方案MnistForm下,就多了一个MnistModel:双击Mnist.cs,我们可以看到项目自动把模型进行了封装,生成了一个公开的infer函数。然后我们在MnistModel上右击,再选择生成,等待一会,这个项目就可以使用了~步骤三:连接两个部分这一步差不多就是这么个感觉:I have an apple , I have a pen. AH~ , Applepen首先,我们来给DrawDigit添加引用,让它能使用MnistModel。在DrawDigit项目的引用上点击鼠标右键,点击添加引用,在弹出的窗口中选择MnistModel,点击确定。然后,由于MNIST的模型的输入是一个28×28的白字黑底的灰度图,因此我们首先要对图片进行一些处理。首先将图片转为28×28的大小。然后将RGB图片转化为灰阶图,将灰阶标准化到[-0.5,0.5]区间内,转换为黑底白字。最后将图片用mnist模型要求的格式包装起来,并传送给它进行推理。于是,我们在pictureBox1_MouseUp中添加上这些代码,并且在文件最初添加上using MnistModel;: private void pictureBox1_MouseUp(object sender, MouseEventArgs e) { //当鼠标左键释放时 //开始处理图片进行推理 if (e.Button == MouseButtons.Left) { Bitmap digitTmp = (Bitmap)digitImage.Clone();//复制digitImage //调整图片大小为Mnist模型可接收的大小:28×28 using (Graphics g = Graphics.FromImage(digitTmp)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.DrawImage(digitTmp, 0, 0, MnistImageSize, MnistImageSize); } //将图片转为灰阶图,并将图片的像素信息保存在list中 var image = new List<float>(MnistImageSize * MnistImageSize); for (var x = 0; x < MnistImageSize; x++) { for (var y = 0; y < MnistImageSize; y++) { var color = digitTmp.GetPixel(y, x); var a = (float)(0.5 - (color.R + color.G + color.B) / (3.0 * 255)); image.Add(a); } } //将图片信息包装为mnist模型规定的输入格式 var batch = new List<IEnumerable<float>>(); batch.Add(image); //将图片传送给mnist模型进行推理 var result = model.Infer(batch); //将推理结果输出 label1.Text = result.First().First().ToString(); } }最后让我们尝试一下运行~三、效果展示现在我们就有了一个简单的小程序,可以识别手写的数字了。赶紧试试效果怎么样~注意路径中不能有中文字符,否则可能找不到模型。进阶那么,如果要识别多个连写的数字,或支持字母该怎么做呢?大家多用用也会发现,如果数字写得很小,或者没写到正中,识别起来正确率也会不高。要解决这些问题,做成真正的产品,就不止这一个模型了。比如在多个数字识别中,可能要根据经验来切分图,或者训练另一个模型来检测并分割数字。要支持字母,则需要重新训练一个包含手写字母的模型,并准备更多的字母的数据。要解决字太小的问题,还要检测一下字的大小,做合适的放大等等。我们可以看到,一个训练出来的模型本身到一个实际的应用之间还有不少的功能要实现。希望我们这一系列的介绍,能够帮助大家将机器学习的概念带入到传统的编程领域中,做出更聪明的产品。
0
0
0
浏览量996
金某某的算法生活

AI应用开发实战系列之二:从零开始搭建macOS开发环境

一、工具介绍Viusal Studio codeVisual Studio Code 是微软继Visual Studio 宇宙第一IDE后出品的又一利器,是一款完全免费的文本编辑器。Visual Studio Code 支持Windows、Linux和Mac三大操作系统,有着一流的响应速度,不论是本身的启动,还是加载目录、打开浏览大文件的速度,都十分迅速;这款文本编辑器的可扩展能力也十分强大,在其活跃生态的支持下,提供了大量的插件供开发者自行配置,比如对各种小众语言的支持、数据访问、键盘布局等等;另外,它在配置上非常灵活,但大多是基于json的文本配置,使用起来不向图形界面那样简单易懂,但是熟悉后还是很方便的。Tools for AIVisual Studio Code 上的Tools for AI 是微软官方出品的一站式机器学习集成开发环境,其与VS code相结合,能让开发人员在同一个开发环境里,完成从编辑、训练、集成模型,到服务与应用的代码开发。Tools for AI 对训练任务的调度和管理做了很好的集成。现在、后端的计算平台支持本机、Linux服务器、微软的企业级计算资源管理平台、Azure的机器学习平台、Batch AI等。另外还能够管理各种远程的存储,直接在界面中上传数据、下载模型日志等文件。二、搭建开发环境Visual Studio Code 安装访问 https://code.visualstudio.com点击Download自动下载对应操作系统的Visual Studio Code插件安装首先打开VS Code,点击扩展图标首先安装好Python插件有插件自动更新或安装后,就会提示重新加载,点击重新加载后,VS Code就会重新启动,并且加载相应的插件。然后搜索Tools for AI,选择第一个安装。这里同样也要点击重新加载。同样,我们可以把中文包也安装上,这样就能显示中文了。安装Git访问 https://git-scm.com/downloads下载Mac适用的版本下载机器学习示例库打开终端,找到一个自己想用来存放这些文件的文件夹,在终端中输入git clone https://github.com/Microsoft/samples-for-ai/这时Git开始自动克隆示例库安装python这一步大家安装python3.5或3.6皆可,但更推荐大家安装python3.6,同时请一定选择64位的版本,否则很多机器学习框架都无法使用。访问 https://www.python.org/downloads/ 选择适用于macOS的64位安装包note:在python安装完成后,请在Application中找到python的安装文件,运行Certificates install.commands,安装常见的根证书,否则python脚本访问任何https网站时都会出现证书错误,这也会影响我们接下来的安装过程安装机器学习和机器学习的软件及依赖还是上一步的文件夹,进入installer目录,输入python ./install.py等待它安装完成。至此,环境搭建已经全部结束。三、运行示例代码从这一步开始,我们要开始真正进行训练了,如果你是第一次接触机器学习,那么你就可以训练出自己的第一个模型了!首先打开Visual Studio Code,选择文件->打开打开samples的总目录。我们使用tensorflow和MNIST来作为例子。MNIST是一个流行的示例数据集,是人手写的数字的图片集。我们可以用它来训练一个模型,让计算机识别出人手写的数字是什么。note:如果你的电脑安装了多个版本的python,请点击VS Code窗口下方的Python环境,程序将列出本机找到的所有python环境,我们要将其切换到正确的环境上本地调试及训练首先,点击AI Explorer来新建或者修改本地环境配置。在Local - Environment下右键,点击Add Configuration设置好name,并将上一步选择的python环境的路径填写进去点击右下角的Finish来刷新环境配置note:一定要点击Finish才能正确地刷新配置在配置好本地环境后,还需要添加一个运行作业的配置,这里选择查看->命令面板,输入AI: Edit Job Properties,然后回车。note:如果你的电脑安装了多个版本的python,请修改startupCommand中的Python改为Python3,然后点击Finish,这样可以确保在执行作业的时候使用python3运行程序右键convolutional.py,选择AI: Submit Job选择Local选择刚才配置好的运行环境可以看到屏幕下方有一个新的终端窗口,这时程序就已经开始对模型进行训练了远程训练由于Mac没有配置Nvidia的显卡,因此只能使用CPU训练模型,如果本机的性能不行,有的时候可能需要花费很长时间,这个时候,如果远程的服务器,特别是有专门的计算资源加持的话,速度会快很多。另外,很多机器学习的框架支持并行计算,远程训练时还可以接入并行的资源,进一步提高训练效率。Tools for AI支持多种远程训练的平台,包括Remote Machine、私有部署的PAI,以及Azure的Batch AI等,本系列博客以后会详细介绍如何使用这些资源。本篇博客主教讲解如何在远程Linux上进行训练。首先在AI Explorer中,在Remote Linux上点击鼠标右键,点击Add Configuration,然后填入自己服务器的信息,最后点击Finish完成设置然后,同样像上次一样选择AI: Submit Job,只是这次要选择刚才配置好的Remote Linux在提交完作业后,如果想要查看运行情况,则需要在Remote Linux中选择之前配置好的机器,点击鼠标右键,选择List Jobs,这时可以看到这台机器上提交过的任务列表点击我们刚刚提交的那个,就可以列出这个任务的所有细节同时可以通过点击页面上的Open Storage Explorer来查看该任务在远程机器下的目录。如果需要查看远程机器的其他目录,则在机器上右键,选择Open Storage Explorer,选择Custom Directory,然后输入你要访问的目录即可
0
0
0
浏览量2021
金某某的算法生活

AI应用开发实战系列之四 - 定制化视觉服务的使用

微软提供的定制化视觉服务。在机器学习应用中,任何情况下都需要一个或大或小的模型。而怎么得到这个模型是其中最复杂的部分。定制化视觉服务相当于在云端提供了一个生成模型的方法,把模型相关的复杂的算法都简化了。同时,它不仅能够让用户自己管理训练数据,定义自己的分类问题,而且支持一键训练,一键导出模型;不仅能导出适配所有主流框架的模型,而且可以生成REST接口,让程序通过接口获取图片分类的结果。这样给用户提供了多种集成模型的方法和选择,尽可能满足用户的各种需求,这也正是定制化视觉服务的强大之处。同时,通过定制化服务来生成模型,需要的数据量可以非常少,训练过程相对来说也很快。使用上也是非常的方便。一、准备微软账号使用该服务需要准备微软账号,可以直接在定制化视觉服务官方地址上创建。二、创建定制化视觉服务三、创建定制化视觉服务项目点击New Project,填写项目信息。这里不妨以一个熊的分类模型作为例子来实践吧。填写好Name和Description,这里Name不妨填写为BearClassification。随后选择Classification和General(compact),点击Create如果需要上传大量的图片数据,那么点击鼠标的方式肯定不够方便,微软同时提供了代码的支持,详见官方文档:https://docs.microsoft.com/en-us/azure/cognitive-services/custom-vision-service/home四、使用Windows ML构建应用这次不写Winform程序,而是搭建一个识别熊的UWP的AI应用,通过这个应用来教大家如何使用Windows ML导入模型。这部分的代码已经完成了,请使用git克隆samples-for-ai到本地,UWP项目的代码在/samples-for-ai/projects/BearClassificationUWPDemo中。在运行代码之前,请先安装开发UWP所需的工作负载,流程如下:打开Visual Studio Installer在工作负载中勾选Universal Windows Platform development在单个组件一栏中下拉到最下方,确认Windows 10 SDK(10.0.17134.0)已被勾选上,这是使用Windows ML开发的核心组件另外,请将您的操作系统更新到1803版本,否则本程序将不能安装。如果您将进行类似的开发,请将UWP项目设置成最低运行目标版本为17134,否则对于版本低于17134的用户,在运行时会出现:“Requested Windows Runtime type ‘Windows.AI.MachineLearning.Preview.LearningModelPreview’ is not registered.”详见:https://github.com/MicrosoftDocs/windows-uwp/issues/575安装需要的时间比较长,可以先看看UWP的视频教程,做一做头脑预热: https://www.bilibili.com/video/av7997007Visual Studio 和 Windows 更新完毕后,我们打开CustomVisionApp.sln,运行这个程序。你可以从必应上查找一些熊的图片,复制图片的URL,粘贴到输入框内,然后点击识别按钮;或者,点击浏览按钮,选择一张本地图片,点击确定,你就可以看到识别结果了:现在来看看这个程序是怎么实现的。我们来梳理一下这个应用的逻辑,这个应用的逻辑与上一篇博客中的手写数字识别大体上是一样的:导入模型按下按钮后,通过某种方式获取要用来识别的图片将图片交给模型识别将图片与识别结果展示在界面上1. 文件结构:文件结构见下图:Assets文件夹存放了这个项目的资产文件,比如程序图标等等,在本示例程序中,.onnx文件也存放在其中。Strings文件夹存放了用于本地化与全球化资源文件,这样可以支持不同的语言。ViewModel文件夹中则存放了本项目的关键代码,整个程序运行的逻辑都在ResultViewModel.cs中BearClassification.cs则是系统自动生成的模型包装文件MainPage.xaml是程序的UI布局文件2. 核心代码一:BearClassification.cs这部分的代码是自动生成的,教程详见链接:https://docs.microsoft.com/zh-cn/windows/uwp/machine-learning/将.onnx文件添加到UWP项目的Assets文件夹中,随后将自动生成一个对应的包装.cs文件,在本例中为BearClassification.cs。由于目前存在的一些BUG,生成的类名会有乱码,需要将乱码替换为别的字符串。修改BearClassification.onnx的属性->生成操作,将其改为内容,确保在生成时,能够调用到这个模型。生成的文件共有三个类:BearClassificationModelInput:定义了该模型的输入格式是VideoFrameBearClassificationModelOutput:定义了该模型的输出为一个list和一个dict,list存储了所有标签按照probability降序排列,dict则存储了标签与概率的键值对BearClassificationModel:定义了该模型的初始化函数与推理函数// 模型的输入格式为VideoFrame public sealed class BearClassificationModelInput { public VideoFrame data { get; set; } } // 模型的输出格式,其中包含了一个列表:classLabel和一个字典:loss // 列表中包含每种熊的标签,按照概率降序排列 // 字典中则包含了每种熊的标签和其概率,按照用户在创建模型时的添加顺序排列 public sealed class BearClassificationModelOutput { public IList<string> classLabel { get; set; } public IDictionary<string, float> loss { get; set; } public BearClassificationModelOutput() { this.classLabel = new List<string>(); this.loss = new Dictionary<string, float>(){...} } } // 模型的包装类,提供了两个函数 // CreateBearClassificationModel:从.onnx文件中创建模型 // EvaluateAsync:对输入对象进行评估,并返回结果 public sealed class BearClassificationModel { private LearningModelPreview learningModel; public static async Task<BearClassificationModel> CreateBearClassificationModel(StorageFile file) { ... } public async Task<BearClassificationModelOutput> EvaluateAsync(BearClassificationModelInput input) { ... } }3. 核心代码二:ResultViewModel.cs通过之前的运行可以发现:每次识别图片,UI中的内容需要进行频繁地更新,为了简化更新控件内容的代码逻辑,这个程序使用UWP开发中常用的MVVM(model-view-viewmodel)这一组合模式开发,使用“绑定”的方式,将UI控件与数据绑定起来,让数据与界面自动地同步更新,简化了代码逻辑,保证了ResultViewModel职责单一。绑定好之后,程序还需要一系列逻辑才能运行,这里就包括:导入与初始化模型:在程序一开始,需要调用LoadModel进行模型初始化工作。private async void LoadModel() { //导入模型文件,实例化模型对象 StorageFile modelFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/BearClassification.onnx")); model = await BearClassificationModel.CreateBearClassificationModel(modelFile); }图片推理:本程序提供了两种方式访问图片资源:通过URL访问网络图片通过文件选取器访问本地图片private async void EvaluateNetPicAsync() { try { ... //BearClassification要求的输入格式为VideoFrame //程序需要以stream的形式从URL中读取数据,生成VideoFrame var response = await new HttpClient().GetAsync(BearUrl); var stream = await response.Content.ReadAsStreamAsync(); BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream.AsRandomAccessStream()); VideoFrame imageFrame = VideoFrame.CreateWithSoftwareBitmap(await decoder.GetSoftwareBitmapAsync()); //将videoframe交给函数进行识别 EvaluateAsync(imageFrame); } catch (Exception ex){ ... } } private async void EvaluateLocalPicAsync() { try { ... // 从文件选取器中获得文件 StorageFile file = await openPicker.PickSingleFileAsync(); var stream = await file.OpenReadAsync(); ... // 生成videoframe BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream); VideoFrame imageFrame = VideoFrame.CreateWithSoftwareBitmap(await decoder.GetSoftwareBitmapAsync()); // 将videoframe交给函数进行识别 EvaluateAsync(imageFrame); } catch (Exception ex){ ... } } private async void EvaluateAsync(VideoFrame imageFrame) { //将VideoFrame包装进BearClassificationModelInput中,交给模型识别 //模型的输出格式为BearClassificationModelOutput //其中包含一个列表,存储了每种熊的标签名称,按照probability降序排列 //和一个字典,存储了每种熊的标签,和对应的probability //这里取出输出中的字典,并对其进行降序排列 var result = await model.EvaluateAsync(new BearClassificationModelInput() { data = imageFrame }); var resultDescend = result.loss.OrderByDescending(p => p.Value).ToDictionary(p => p.Key, o => o.Value).ToList(); //根据结果生成图片描述 Description = DescribResult(resultDescend.First().Key, resultDescend.First().Value); Results.Clear(); foreach (KeyValuePair<string, float> kvp in resultDescend) { Results.Add(resourceLoader.GetString(kvp.Key) + " : " + kvp.Value.ToString("0.000")); } }五、使用其他方法构建应用同样,用之前使用Visual Studio Tools for AI提供的推理类库生成器也能够构建相似的应用。想看视频教程的请移步:【教程】普通程序员一小时入门AI应用——看图识熊(不含公式,包会)该教程讲解了如何使用模型浏览工具Netron想看图文教程请继续往下看:1. 界面设计创建Windows窗体应用(.NET Framework)项目,这里给项目起名ClassifyBear。注意,项目路径不要包含中文。在解决方案资源管理器中找到Form1.cs,双击,打开界面设计器。从工具箱中向Form中依次拖入控件并调整,最终效果如下图所示:左侧从上下到依次是:Label控件,将内容改为“输入要识别的图片地址:”TextBox控件,可以将控件拉长一些,方便输入URLButton控件,将内容改为“识别”Lable控件,将label的内容清空,用来显示识别后的结果。因为label也没有边框,所以在界面看不出来。可以将此控件的字体调大一些,能更清楚的显示推理结果。右侧的控件是一个PictureBox,用来预览输入的图片,同时,我们也从这个控件中取出对应的图片数据,传给我们的模型推理类库去推理。建议将控件属性的SizeMode更改为StretchImage,并将控件长和宽设置为同样的值,保持一个正方形的形状,这样可以方便我们直观的了解模型的输入,因为在前面查看模型信息的时候也看到了,该模型的输入图片应是正方形。2. 查看模型信息在将模型集成到应用之前,我们先来看一看模型的基本信息,比如模型需要什么样的输入和输出。打开Visual Studio中的AI工具菜单,选择模型工具下的查看模型,会启动Netron模型查看工具。该工具默认不随Tools for AI扩展一起安装,第一次使用时可以按照提示去下载并安装。Netron打开后,点击Open model选择打开之前下载的BearModel.onnx文件。然后点击左上角的汉堡菜单显示模型的输入输出。上图中可以看到该模型需要的输入data是一个float数组,数组中要求依次放置227*227图片的所有蓝色分量、绿色分量和红色分量,后面程序中调用时要对输入图片做相应的处理。上图中还可以看到输出有两个值,第一个值loss包含所有分类的得分,第二个值classLabel是确定的分类的标签,这里只需用到第二个输出即可。3. 封装模型推理类库由于目前模型推理用到的库只支持x64,所以这里需要将解决方案平台设置为x64。打开解决方案资源管理器,在解决方案上点右键,选择配置管理器。在配置管理器对话框中,点开活动解决方案平台下拉框,选择新建在新建解决方案平台对话框中,输入新平台名x64,点击确定即可下面添加模型推理类库,再次打开解决方案资源管理器,在解决方案上点右键,选择添加,然后选择新建项目。添加新项目对话框中,将左侧目录树切换到AI Tools下的Inference,右侧选择模型推理类库,下方填入项目名称,这里用Model作为名称。确定以后会出现检查环境的进度条,耐心等待一会就可以出现模型推理类库创建向导对话框。点击模型路径后面的浏览按钮,选择前面下载的BearModel.onnx模型文件。注意,这里会出现几处错误提示,我们需要手动修复一下。首先会看到“发现不支持的张量的数据类型”提示,可以直接点确定。确定后如果弹出“正在创建项目…”的进度条,一直不消失,这里只需要在类名后面的输入框内点一下,切换下焦点即可。然后,我们来手动配置一下模型的相关信息。类名输入框中填入模型推理类的名字,这里用Bear。然后点击推理接口右侧的添加按钮,在弹出的编辑接口对话框中,随便起个方法名,这里用Infer。输入节点的变量名和张量名填入data,输出节点的变量名和张量名填入classLabel,字母拼写要和之前查看模型时看到的拼写一模一样。然后一路确定,再耐心等待一会,就可以在解决方案资源管理器看到新建的模型推理类库了。还有一处错误需要手动修复一下,切换到解决方案资源管理器,在Model项目的Bear目录下找到Bear.cs双击打开,将函数Infer的最后一行return r0;替换为List<List<string>> results = new List<List<string>>(); results.Add(r0); return results;至此,模型推理类库封装完成。相信Tools for AI将来的版本中会修复这些问题,直接选择模型文件创建模型推理类库就可以了。4. 使用模型推理类库首先添加对模型推理类库的引用,切换到解决方案资源管理器,在ClassifyBear项目的引用上点右键,选择添加引用。在弹出的引用管理器对话框中,选择项目、解决方案,右侧可以看到刚刚创建的模型推理类库,勾选该项目,点击确定即可。在Form1.cs上点右键,选择查看代码,打开Form1.cs的代码编辑窗口。添加两个成员变量// 使用Netron查看模型,得到模型的输入应为227*227大小的图片 private const int imageSize = 227; // 模型推理类 private Model.Bear model;回到Form1的设计界面,双击Form的标题栏,会自动跳转到代码页面并添加了Form1_Load方法,在其中初始化模型推理对象private void Form1_Load(object sender, EventArgs e) { // 初始化模型推理对象 model = new Model.Bear(); }回到Form1的设计界面,双击识别按钮,会自动跳转到代码页面并添加了button1_Click方法,在其中添加以下代码:首先,每次点击识别按钮时都先将界面上显示的上一次的结果清除// 识别之前先重置界面显示的内容 label1.Text = string.Empty; pictureBox1.Image = null; pictureBox1.Refresh();然后,让图片控件加载图片bool isSuccess = false; try { pictureBox1.Load(textBox1.Text); isSuccess = true; } catch (Exception ex) { MessageBox.Show($"读取图片时出现错误:{ex.Message}"); throw; }如果加载成功,将图片数据传给模型推理类库来推理。if (isSuccess) { // 图片加载成功后,从图片控件中取出227*227的位图对象 Bitmap bitmap = new Bitmap(pictureBox1.Image, imageSize, imageSize); float[] imageArray = new float[imageSize * imageSize * 3]; // 按照先行后列的方式依次取出图片的每个像素值 for (int y = 0; y < imageSize; y++) { for (int x = 0; x < imageSize; x++) { var color = bitmap.GetPixel(x, y); // 使用Netron查看模型的输入发现 // 需要依次放置227 *227的蓝色分量、227*227的绿色分量、227*227的红色分量 imageArray[y * imageSize + x] = color.B; imageArray[y * imageSize + x + 1* imageSize * imageSize] = color.G; imageArray[y * imageSize + x + 2* imageSize * imageSize] = color.R; } } // 模型推理类库支持一次推理多张图片,这里只使用一张图片 var inputImages = new List<float[]>(); inputImages.Add(imageArray); // 推理结果的第一个First()是取第一张图片的结果 // 之前定义的输出只有classLabel,所以第二个First()就是分类的名字 label1.Text = model.Infer(inputImages).First().First(); }注意,这里的数据转换一定要按照前面查看的模型的信息来转换,图片大小需要长宽都是227像素,并且要依次放置所有的蓝色分量、所有的绿色分量、所有的红色分量,如果顺序不正确,不能达到最佳的推理结果。5. 测试编译运行,然后在网上找一张熊的链接填到输入框内,然后点击识别按钮,就可以看到识别的结果了。注意,这个URL应该是图片的URL,而不是包含该图片的网页的URL。六、下一步?本篇文章我们学会了使用定制化视觉服务与在UWP应用中集成定制化视觉服务模型。这里我提两个课后习题:(想不到吧)当训练含有多个标签、大量图片数据时,如何做到一键上传图片并训练?如何通过调用REST接口的方式完成对图片的推理?
0
0
0
浏览量1858
金某某的算法生活

AI应用开发实战系列之一: 从零开始配置环境

零、前提条件一台能联网的电脑,使用win10 64位操作系统请确保鼠标、键盘、显示器都是好的一、Windows下开发环境搭建本教材主要参考了如下资源:官方github教程:https://github.com/microsoft/vs-tools-for-ai斗鱼tv教程:https://v.douyu.com/show/V6Aw87OBmXZvYGkg本教程分为五步:- 安装VS:难度一星- 安装python:难度一星- 安装CUDA和cuDNN:这是本教程最繁琐的一步,这一步直接拉高本教程的平均难度。- 配置机器学习环境:这是本教程最简单的一步,为了方便用户配置环境,微软提供了一键安装工具!没错,一键安装工具!业界良心阿!- 安装VS Tools For AI插件:难度一星note:本教程对各个软件需要使用的版本都做出了明确说明,请安装指定的版本请放轻松,接下来的傻瓜教程不需要动脑子,你甚至可以打开手机边刷微博边配置环境0.安装Git访问 https://git-scm.com/download/win选择64-bit Git for Windows Setup下载双击.exe开始安装选择好自己的安装路径,一路next,直到Adjusting your PATH environment请选择Use Git from the Windows Command Prompt这一步就已经将Git添加到环境变量中了,然后就可以直接在命令行里使用Git啦。然后继续next,直到安装结束1.安装VS访问 https://www.visualstudio.com/zh-hans/products/在产品中点击Visual Studio 2017选择Community版本下载打开Visual Studio Installer进行如下的配置:仅选择.NET桌面开发与Python开发即可仅选择.NET桌面开发与Python开发即可仅选择.NET桌面开发与Python开发即可note:请自行决定Visual Studio的安装路径等待数分钟,时长视网络状况而定,这个时候你可以去泡一杯茶,或者听一首歌,如果你的网络不是很好,那你可以去看集美剧或者别的什么,等待安装结束。note:坐 和 放宽2.安装python访问 https://www.python.org/downloads/选择版本3.5.4或3.6.5 ,Windows x86-64 executable installer下载。打开安装包,在安装前,请选择Add Python 3.X to PATH,随后按照默认选项安装即可。点选后,程序将自动将Python加入环境变量,这样避免在安装后手动配置环境变量。安装结束后,请进行如下操作验证python是否安装成功1.同时按下 win 与 R,在弹出的输入框里输入cmd 2.在弹出的窗口中输入 python 3.输入exit()退出 4.输入python -m pip install -U pip以更新pip到最新版本note: pip是一个用来管理python包的工具自此,你已经完成了python的安装,在朝着AI技术大牛的路上又前进了一步!note:请伸出大拇指给自己一个赞?3.安装CUDA与cuDNN如果你的电脑装有Nvidia的显卡,请进行这一步配置,否则请跳过。首先通过操作系统更新,升级显卡驱动到最新版。3.1 安装CUDA打开 https://developer.nvidia.com/cuda-toolkit-archive选择CUDA 9.0 进行安装。点击后,选择如下的配置:note:请选择local版本下载,一旦失败还可以重新再来;如果使用network版本,一旦失败,需要重新下载1.4GB的安装包打开安装包,进行安装,请自行配置CUDA的安装路径,并手动将CUDA库添加至PATH环境变量中。note:在Windows中,CUDA的默认安装路径是:“C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\bin”3.2 安装cuDNNnote:打起精神!这是操作最复杂的一步!访问 https://developer.nvidia.com/rdp/cudnn-archive 找到我们需要的cuDNN版本:cuDNN v7.0.5 (Dec 5, 2017), for CUDA 9.0cuDNN v7.0.5 Library for Windows 10点击链接,等待着你的并不是文件下载,而是:↑这就是本教程里最麻烦的一步:在下载cuDNN之前需要注册Nvidia会员并验证邮箱。不过还好可以微信登录,省掉一些步骤。一番令人窒息的操作之后,我们终于得到了cuDNN,我们把文件解压,取出这个路径的cudnn64_7.dll,复制到CUDA的bin目录下即可。默认的地址是:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\binnote:到这里,我们已经完成了本教程最复杂的一步了4.安装机器学习的软件及依赖这一步虽然是整个教程最简单的一步,甚至比把大象关进冰箱更简单。你只需要:win + R ,打开cmd,在命令行中输入: cd c:\ //选择一个你喜欢的路径 md AI //在这里创建一个AI目录 cd AI //打开这个目录 //克隆仓库到本地 git clone https://github.com/Microsoft/samples-for-ai.git cd samples-for-ai //打开这个目录 cd installer //还有这个目录 python.exe install.py //开始安装然后刷会微博,等待安装结束即可。成功之后是这样的:或者你觉得自己不怕麻烦,那么请访问:https://github.com/Microsoft/vs-tools-for-ai/blob/master/docs/zh-hans/docs/prepare-localmachine.md根据教程按步安装,相信我,你会回来选择一键安装的。note:就差一步啦!成功就在眼前!5.安装tools for ai插件打开Visual Studio,选择工具->扩展和更新->选择“联机”->搜索“AI”就像这样:等待下载完成之后,关闭Visual Studio,没错,关闭Visual Studio,系统将自动安装AI插件。安装完毕后再次打开Visual Studio,你将在界面上看到这样的内容:那么恭喜你!安装成功!note:千里之行始于足下,恭喜你成功地完成了环境的搭建,接下来就已经可以使用Visual Studio Tools For AI进行开发了?二、离线模型的训练6.14日更新GitHub上的samples-for-ai进行了一定的更新,目前MNIST文件夹下只有一个mnist.py文件,下述步骤中,请使用最新的mnist.py文件在进行完环境搭建后,我们马上就可以开始训练第一个模型了,我们选择tensorflow下的MNIST作为第一个例子。MNIST的介绍请参考这个链接 https://www.tensorflow.org/versions/r1.1/get_started/mnist/beginners首先我们打开这个路径:C:\AI\samples-for-ai\examples\tensorflow,如果你在别的目录下克隆了目录,那么请打开你对应的目录。然后双击TensorflowExamples.sln就像这样:note:如果存在多个Python环境,你需要为Visual Studio的AI项目设置默认的Python环境。例如,手动安装的Python 3.5与Visual Studio 2017 Python开发负载自动安装了64位的Python 3.6如果要为Visual Studio设置全局的默认Python环境,请打开工具->Python -> Python环境。然后,选择自己需要的Python版本,点击将此作为新项目的默认环境。然后在解决方案资源管理器中,选择MNIST,单击右键,选择设为启动项目然后选择MNIST中的mnist.py,单击右键,选择在不调试的情况下启动然后程序就开始运行了,就像这样:等待一段时间之后,模型就训练好了!这个时候打开MNIST所在的文件夹,MNIST下是否多了三个文件夹?分别是input和output还有export,这三个文件夹分别存储了训练模型的输入文件、训练时的检查点文件,还有最终导出的模型文件检查点文件:模型文件:可能存在的问题GPU ran out of memory方法一:修改convolutional.py第45行或第47行的BATCH_SIZE或EVAL_BATCH_SIZE为一个更小的数字。具体修改哪一个,需要视你在程序运行的哪个部分得到了ERROR决定。方法二:不使用GPU训练,在项目MNIST上单击右键,选择属性(R)修改环境变量为CUDA_VISIBLE_DEVICES=" "
0
0
0
浏览量397
金某某的算法生活

ChatGPT - 横看成岭侧成峰

定义ChatGPT 是什么?ChatGPT是由OpenAI开发的一个人工智能聊天机器人程序,由 OpenAI 公司于2022年11月推出。该程序使用基于GPT-3.5架构的大型语言模型并通过强化学习进行训练。ChatGPT以对话方式进行交互,可以用于包括自动文本生成、自动问答、自动摘要等在内的多种任务。如:在自动文本生成方面,ChatGPT可以根据输入的文本自动生成类似的文本,在自动问答方面,ChatGPT可以根据输入的问题自动生成答案。这是 ChatGPT 自己的介绍:注:自我介绍不错,但是 “转移学习”, 应该翻译为 “迁移学习” 才精准。ChatGPT 背后的技术是 OpenAI 公司的 GPT 大型语言模型框架,OpenAI 也得到了微软公司的大力投资,ChatGPT 是这样回答 “你怎么分析微软公司持续投资 OpenAI 公司?” 这个问题的:微软公司持续投资 OpenAI 公司,可能是因为以下原因:先进技术: OpenAI 是一家先进的人工智能研究机构,其研究成果和技术在人工智能领域具有高度的创新性和潜力,微软公司可能希望能够通过投资获得这些先进技术。市场机会: 随着人工智能技术的不断发展,人工智能市场机会也在不断增加,微软公司可能希望能够通过投资 OpenAI 来获得这些市场机会。合作机会: OpenAI 和微软公司在人工智能领域有着高度的互补性,微软公司可能希望嗯,chatGPT 最后一句话没有讲完,它也没有解释。网上流传了 chatGPT 能做的各种事情,我们的博主也写了不少文章: https://blog.csdn.net/nav/advanced-technology/chatgpt我认为,GPT 之所以破圈,掀起了大众的热潮,有几个原因:能够像人类处理上下文,并层层递进和用户一起解决问题。能用自然语言对话,把 AI 的门槛降低到了 0。这个突破可能 IT 专业人士觉得无所谓,其实,这个突破跨越了 “创新的鸿沟 Chasm” (见下图)。 以前的各种 AI 突破,都是在业界人士引起巨大兴趣,但是对于大众,并没有产生真正的触动,这次的 ChatGPT,让大部分用户 (Early Majority)都能亲手亲身体会到 AI 对自己的帮助,可以说,星星之火已经出现燎原之势。能接受 prompt , 用户可以告诉 GPT,以 [角色] 身份完成某个 [任务],满足这些 [条件],并用这样的 [格式] 展现出来 …” 这就把 GPT 的能力通过类似 API 调用的方式提供给了用户, 用户的想象力和丰富的组合产生了百花齐放的结果。 有人说, GPT 就是 AI 时代的操作系统。ChatGPT 可以生成各种内容,可以回答不少技术问题,大家对它的态度刚开始是好奇,惊艳,随后就有不同的反应,特别是在问答和教育领域 –限制、禁止 chatGPT英文 IT 界最大的问答平台 StackOverflow 禁止在它的平台上使用 ChatGPT 生成的内容。 主要原因是ChatGPT 生成的回答 看起来非常高质量,非常容易生成,但是准确度低。这会导致很多用户大量生产此类内容,对平台和想得到正确答案的用户都是有害的。这是 StackOverflow 官网的解释截屏:一个用户 (Alex Poole)的评论得到了很多赞:(我翻译成中文)不管 AI 生成的答案正确与否,“StackOverflow是一个面向专业程序员和热心编程爱好者的问答网站" 这句话同时适用于 【提问】 和【回答】的部分。在我看来,那些只是将提问粘贴到 AI 那里,然后把 AI 的回答再复制回来的人, 不算是专业程序员,也不算是热心编程爱好者。也许这些人对 AI 的回答进行了检查和测试,如果他们真的能够验证这是一个好的正确答案,他们应该能够自己写出来。这并不一定意味着互联网上没有(或不会有)人工智能提供服务的地方,但它并不属于 StackOverflow。下面是我用 deepl.com 服务得到的翻译:美国的一些学校也禁止学生使用学校的网络连到 ChatGPT,主要原因是:“虽然该工具可能能够提供快速和简单的问题答案,但它并不能培养批判性思维和解决问题的能力,而这些能力对学术和终身成功至关重要” (报道链接) 但是,学校防止不了学生用其他方式连到 ChatGPT,这样, 自己有手机流量的,就能用,没有手机流量的,就不能用。 或者是,某个地区的学生,可以通过某种方式上网的少部分学生,就能得到这种服务,这是否造成了一种不公平呢?出错了怎么办?在大约 10 年前,我在研究院的同事就开发了一个 AI 的写作助手,你输入上一句, 写作助手就会根据上下文建议下一句,当时的 AI 技术和数据量还是比较初级,所以它的一些建议看起来很幼稚可笑。这个研究项目在小范围演示后,就停下来了, 十年后,大不一样了,这是我刚才随手给 ChatBot 出的题目:如果 ChatBot 聊天聊出问题了怎么办? 这也是实际发生的情况。 我试着和她聊 “在犯罪的边缘试探” 的笑话,ChatGPT 倒是很冷静:2016 年,微软就发布过一个有感情的聊天机器人 – Tay,但是 16 小时之后,就把她下线了。因为她在和用户聊天的过程中,从用户那里学到了很多骂人、歧视、以及其他恶劣的行为。 这个聊天机器人有 “自觉” 和 “良心” 么,知道自己的言语会伤害别人么? 要找机会直接问她。从我有限的测试来看, ChatGPT (他背后的高人) 很明智地吸取了前人的教训,给自己画了一些红线,不敢越雷池半步。共存ChatGPT 能快速地给出看起来很有信心的答案,学生拷贝就好了,这是作弊么? 也有老师说,这就像 20 年前 Google 出现的时候一样,当时也有老师不让学生从 Google 上直接找答案,后来怎么样了呢?打不过你,就加入你好了。这其实反映了 人工智能 (简单或复杂的算法) 辅助人类工作的一个趋势。 下面是一些例子:Word 编辑器的文字拼写自动纠错功能(这算作弊么)?以及后续不断提高的语法建议功能,Excel 的自动数据自动填充、计算功能, PowerPoint 的 “幻灯片自动排版” 功能以前初学者翻译文章,要一个词一个词地查纸板的字典,后来,有网络上的词典,便携式的电子词典,手机 app,现在,很多的翻译工具能翻译整段话,而且效果不错,就像本文上面用的 deepl.com 的例子。代码编辑器中的 IntelliSence 自动完成变量名和函数名的功能,自动生成函数API 的注释,演化到最近的自动生成测试用例,自动完成函数功能,等。wolframalpha.com 网站的自动解题并列出步骤画出优美的图形…艺术创造中的美颜,AI 作画等。更不用说在股市交易中的 AI 算法的作用 …这些进步,在当时都引起一些争议,现在看来,都好像是挺自然的了。当然,AI 从解决单个离散的任务,发展到可以完成一整套考试,这是极大的飞跃 – 最近世界著名的沃顿商学院的教授 Christian Terwiesch 让 ChatGPT 做课程的期末试卷,AI 居然得了大约是 B 或者 B- 的分数。 考虑到 AI 的发展速度,我们不难想象不久的将来, AI 在 MBA 课程中碾压人类,就像 AlphaGO 碾压人类围棋手那样… 那么,上 MBA 学院的价值是增加了还是减少了呢?在我们软件工程师的面试中, 我们经常要考察基本的算法,例如快速排序,红黑树,等,如果一个候选人说, 这些算法都有 API 和别人经过考验的算法库,我只管调用就好了! 你作为面试官,你怎么办呢? 我们在考试和面试中,经常要考察这个人的 “技能”, 那么,会用 ChatGPT,算是一个有价值的技能么? 技能的反面是什么呢?鼓励使用 ChatGPT打不过你,就加入你好了,而且还要充分利用这个工具。 我今天看到这个 npr.org 的报道,有老师要求所有学生都用 ChatGPT 来做作业, 他认为,在讨论和创意阶段,这个工具还是很不错的。这位老师说,他对于 AI 工具对于教学的影响,又激动,又焦虑,但是,还是要跟上时代啊!著名数学家陶哲轩设想,考试也许应该走向 “open books, open AI” 的模式 – 又开卷,又允许允许 AI 工具。 在这种情况下,我们的试卷应该怎么设计,才能区分出学生的 好/中/差呢?分清楚 【参考】还是 【作弊】在一些人抱怨 ChatGPT 导致作弊的时候,他们可能忘了,作弊是人类有史以来就有的行为,现在也不例外,可以搜索一下 “论文代写” 或者是 “毕业设计 源码” – 搜索这些东西的人,收集资料呢,还是直接拿现成的方案去当作自己的作品呢? 这就不得而知了。这个 BBC的报道 也提到肯尼亚的大型作业代写的服务。 一旦开始了作弊,大家就从 “学术道德” 的高地上走下来了,随之而来的是各种环节的作弊行为:这些代写作业的人,都是在肯尼亚一带的非洲同胞,但是他们都头像都是白人这些代写作业的账户上面的五星好评,也是假的,这也有一个假账号和假评价的交易市场这给了顾客(欧美的学生)一种印象,是 “高水平的西方人在替我写文章,老子的钱花得值”!从报道中的数据可以推导出,一个一周工作七天的代写文章者,可以挣到相当于当地成年人的平均工资。 这对于很多接受了高等教育但是在当地找不到工作的年轻人来说,这是一个很不错的出路。当然,人们也开发了各种检测作弊的工具,对于 ChatGPT,也有检测工具出现了 GPTZero!如果所有人都能用上 ChatGPT,而且,检测 ChatGPT 的技术也在提高,那么,对肯尼亚一带的人肉论文代写行业,有什么影响呢? 我们能训练 ChatGPT 去检测作弊行为么?AI 贵有自知之明ChatGPT 是怎么看自己产生的内容被学生使用的呢? 请看它的直接回答:理论上,ChatGPT 属于哪个层次的 AI 呢? 如果低层次的 AI 都这么厉害了,高级的 AI 出现后,我们怎么办呢?(图片来自:汪军教授的演讲)当然,人和 AI 在各个领域的较量才刚刚开始,魔高一尺,道高一丈,双方的较量的过程和结果是合作,共存,融合,还是对抗?最终谁能胜出?让我们 拭目以待 (说到拭目以待,说不定有人正在做机器眼睛接入人脑的实验咧)!后续 … 更大的余波最近有些作者把 ChatGPT 也列为自己文章的合作者,并发表了学术论文,这引起了很多专家的担忧和争论,在写完博客正文后,我在网上还看到了这样的报道: 卫报的报道 和其他网友的说明:https://weibo.com/1924545467/MqwNKgeKw核心决定是:主要科学期刊出版商决定禁止将 ChatGPT 列为署名作者,但是允许真人的作者在论文准备阶段使用 ChatGPT 等工具,只要说明使用的情况即可。ChatGPT 在全球的教育界也引起了持续的争论。 根据这个文章收集的信息,我们可以看到:关于英语教育 在拥有12年教龄的旧金山高中英语教师赫尔曼看来,ChatGPT能瞬间完成主要文本类型的写作,这颠覆了英语教学的基本功能,英语教育的地位也岌岌可危。美国企业研究所的高级研究员庞迪西奥却认为,赫尔曼的观点只代表了部分知识精英,而知识精英推崇的各种思维训练,都必须建立在相当的知识储备上。由于多数学生的知识和语言能力尚未达到基础水平,写作训练依然必不可少。庞迪西奥还讽刺了当前对ChatGPT的过度推崇:“与其说创造内容,不如说ChatGPT更擅长通过编造和修辞,来弥补谈话的空白;这个软件在模仿人类胡说八道方面,能力着实出色。”关于 技术与人文之辩 加拿大作家马奇认为,相比科技的突飞猛进,人文学者长期醉心于宏大叙事的建构与批判,反而忽视语言、历史、伦理、政治等方面的现实问题。而ChatGPT等新技术越来越擅长完成论文等 “文科工作”,人文和技术的差距将继续拉大。然而,福布斯专栏作家里姆认为,不能过分夸大ChatGPT对论文乃至人文教育的影响——就论文写作而言,最重要的是学生不断思考和尝试创作的过程。人文学者应学会驾驭新技术,并教会学生熟练运用这些工具。南方科技大学的于仕琪老师也在微博上分享了自己的感受:我这学期教"C/C++程序设计"课,刚布置了第一个作业,然后我把作业要求输入到ChatGPT,看了结果后心情很复杂。第一步ChatGPT编出来的程序实现了基本要求,高级要求虽然没实现,但给出了实现建议。于是我让它优化,它就给出了很好的解决方案。第三步,它在我的要求下,它写出了一份还不错的报告。我觉得,如何评价学生面临巨大挑战!人类学习数学有很长时间了,如果用卖日杂货物算钱这个场景,大致经历了 在地上写写画画 - 石板上写写画画 - 算筹 - 部分人的心算 - 珠算 - 计算器 - 集成到其他工具中 现在你去买东西,就是不断拿货物扫码,然后你也扫自己的wx/支付宝付钱,都不用算如何找零。 但是,青少年要学习算术么? 要! 为什么? 因为数学是一个理解世界,锻炼大脑分析能力的工具,没有这个能力,人类就不能进行其他的更高层的 思考、尝试、创作 的过程,所以,我们在享受越来越便利的计算成果的同时,还是要从基本的算术学起。编程语言,软件工程也是同理。ChatGPT 给我们提供了方便的工具,让我们更快地就能看到很多解题的方法,很多初步的结果,但是我们自己的大脑还是要经历漫长的学习过程才能在编程和软件工程的领域实现 思考、尝试、创作的过程, IT 专业的老师面临的挑战,就是在日新月异的工具进步中,如何能持续打造这样一个训练的环境, 让学生可以 思考、尝试、创作 。
0
0
0
浏览量2014
金某某的算法生活

【干货】快速部署微软开源GPU管理利器: OpenPAI

介绍不管是机器学习的老手,还是入门的新人,都应该装备上尽可能强大的算力。除此之外,还要压榨出硬件的所有潜力来加快模型训练。OpenPAI作为GPU管理的利器,不管是一块GPU,还是上千块GPU,都能够做好调度,帮助加速机器学习的模型训练过程。关于什么是OpenPAI,请参考介绍视频:微软开源GPU集群管理利器。本文提供了简化的OpenPAI安装步骤。如果有更复杂的安装要求或部署环境,请参考官网。准备工作环境要求如下:推荐Ubuntu 16.04 LTS(暂不支持CentOS等其它Linux系统)。静态IP地址。能够访问外网,可下载Docker Hub的镜像文件。为集群中每台机器提供统一的用户名密码,并有sudo权限。有统一的时间同步服务(默认即可)。推荐干净环境进行安装。如果已经安装了Docker,API版本必须大于等于1.26。各台计算机之间网络可达。安装过程1. 安装用于配置的docker即管理、安装整个OpenPAI的docker(在官方文档中称为dev-box)。以后的管理、配置工作都会在这个docker中进行。登录进某台计算机(可选用集群中的机器),确保有sudo权限。然后按顺序执行下列命令。安装docker,如果安装有更新的版本可跳过。sudo apt-get -y install docker.io# 拉取,并启动dev-box sudo docker run -itd -e COLUMNS=$COLUMNS -e LINES=$LINES -e TERM=$TERM -v /var/lib/docker:/var/lib/docker -v /var/run/docker.sock:/var/run/docker.sock -v /pathHadoop:/pathHadoop -v /pathConfiguration:/cluster-configuration --pid=host --privileged=true --net=host --name=dev-box docker.io/openpai/dev-box # 登录dev-box sudo docker exec -it dev-box /bin/bash2. 配置安装环境以下脚本需要修改一下安装环境相关的信息。machines表示GPU集群的服务器IP,ssh-username和ssh-password分别代表登录这些服务器要用到的用户名、密码。注意:第一台会作为master节点,其余节点作为worker。关于master/worker可参考视频介绍。暂时推荐不要用GPU服务器做master角色,或将worker角色部署到master上,因为这样可能会造成资源紧张,从而造成master进程的内存不够用。所以,master节点可以用没有GPU的服务器,推荐8核16G或以上配置。cd /pai/pai-management cat << EOF > quick-start.yaml machines: - 192.168.1.2 - 192.168.1.3 ssh-username: <用户名> ssh-password: <密码> EOF CONFIG_PATH=/cluster-configuration rm $CONFIG_PATH/*3. 安装节点根据quick-start的基本信息,在/cluster-configuration目录中生成配置文件。配置文件的具体内容可参考github,这里就不详细介绍了。python paictl.py cluster generate-configuration -i quick-start.yaml -o $CONFIG_PATH安装kubenetespython paictl.py cluster k8s-bootup -p $CONFIG_PATH安装并启动OpenPAI相关服务python paictl.py service start -p $CONFIG_PATH运行最后一步时,如果网速很慢或服务器很多,有可能会花一天。完成后,即可在浏览器中试着访问第一台服务器的web地址。因为服务器还需要启动一会儿,可能并不能马上看到结果。等一会儿,或者多试几次即可。默认的用户名、密码如下,可点击右上方的login连接登录。建议第一时间改掉。admin admin-password好了!大功告成!可以参考github中的任务模板来配置自己的任务模板。也可以看看Github中的文档来探索更多高级功能。接下来就可以看看前面的介绍视频来学习如何从Tools for AI来提交任务了。如果集群比较小,可以给集群去掉end-to-end测试用例,从而节省资源。(参考常见问答)常见问题遇到问题,可在官网提交Issue。如何删除end-to-end测试任务?如果没有足够的服务器资源,建议在部署过程中删除掉end-to-end测试。否则,它会定期进入队列,以测试系统是否可用。在dev-box中运行:python paictl.py service delete -p $CONFIG_PATH -n end-to-end-test安装过程中出现 ... is not ready yet. Please wait for a moment!,该怎么办?这种一般是网络问题造成的,可以进入以下网址(注意替换master IP),将出现pull image错误的pods删掉,加快Kubernetes重新pull的速度。http://<替换成master的IP>:9090/#!/pod?namespace=default
0
0
0
浏览量370
金某某的算法生活

微软认知服务应用秘籍 – 君子动口不动手

概述科技的不断发展带动着人们生活质量不断的提升,其中一方面就体现在日常家庭生活中,智能设备层出不穷,给人们的生活带来了很大的便利。以电视为例,几十年前的电视还是按钮式的,每次换台还要跑到电视跟前;后来使用遥控器控制成为了主流,人们可以舒服的窝在沙发里看电视;再后来随着互联网及移动通信技术的发展,电视、机顶盒、空调等,都可以在手机上进行控制,再也不用几个遥控器之间来回倒腾了,还不耽误刷微博;近年来随着人工智能的发展,语音识别、合成技术日趋成熟,家电已经可以理解语音指令,能够按照人类的交流方式进行人机交互了,人们只需要动嘴说说话就可以完成各种操控。    一种智能家居的典型场景如下图所示,以智能音箱为核心,所有设备通过WiFi与智能音箱连接并可以接受智能音箱的控制指令。人们可以和音箱打招呼,音箱能够自然的回答问好;也可以询问天气,音箱能够自动根据当前位置搜索并回答未来一天的天气;还可以直接对着音箱说一声"把灯关掉",音箱应该能够控制关掉灯,或者主动询问"要关哪个灯",在得到明确指令后再执行指定的操作。本文将借助微软认知服务中的多个服务实现一个简单的智能家居应用,来模拟一个语音控制开关灯的场景,期望能给予大家一些启示,并期望大家可以利用更多的微软认知服务扩展出更多炫酷实用的功能。流程这一节我们看一下语音控制开关的流程是怎样的。从场景上来看很简单,人直接说"开灯",灯就可以打开,当然说"请把灯打开",灯也应该能够打开。但是对于传统的程序员来说,实现起来主要的难点在于如何让程序理解人类说的话,尤其是用户一般都是用很自然的语言去说,用户的习惯不一样,语法结构也不一样。为了解决这个难点,这里我们将借助微软认知服务里的语音转文本服务和语言理解服务来赋予程序理解人类说话的能力。要做到这些,主要分三个步骤:第一步,借助语音转文本服务,将用户的语音输入识别成对应的文字,比如"开灯"、"请把灯打开"等等。第二步,借助语言理解服务来将自然语言转为程序可以理解的意图。不管用户是说"开灯"还是"请把灯打开",语言理解服务都可以识别出用户的意图是打开(TurnOn)。第三步,按照识别出的用户的意图去控制灯打开或关闭。语音转文本服务语音转文本服务提供将音频流转录为文本的能力,微软的语音转文本服务采用了和微软小娜相同的技术。应用程序借助此服务可以轻松地将声音转录为文字,之后可以直接显示或做更进一步的使用。语音服务提供SDK和REST API两种使用方式。使用SDK可以将服务更方便的集成到应用程序中,并且可以提供额外的功能,如实时的中间转录结果、静默一段时间自动停止、转录超长的音频等。目前提供的SDK有.Net、C/C++、Java版本,如果使用其它编程语言,可以考虑更通用的REST API方式,但是不能提供SDK中所有的功能。在线体验语音转文本服务的在线体验地址是https://azure.microsoft.com/zh-cn/services/cognitive-services/speech-to-text/,可以先通过此页面对该服务有个初步的认识。如果设备有麦克风,可以点击开始录音,然后对着麦克风说话,查看语音转文本的效果;如果没有麦克风,也可以点击下方的两个示例体验一下。可以看到网页在录音的过程中,同时显示语音转文本的中间结果,并按照最新播放的内容不断纠正旧文本、显示新文本,最终给出了一个最佳的文本结果。申请试用后面要在程序中调用语音转文本服务,必须要有服务密钥才可以。试用密钥的有效期是 30 天,每月 5000 个事务,每分钟 20 个。每个账号只能申请一次试用。申请步骤:打开申请试用页面:https://azure.microsoft.com/zh-cn/try/cognitive-services/?api=speech-services找到语音服务,点击右侧的获取API密钥在弹出页面点击来宾7天试用下面的开始使用(不用管这里显示的7天,语音服务现在还是预览版,有30天的试用期,申请完成后显示30天)在服务条款页面勾选同意,选择国家/地区为中国,下一步选择要使用的账号,笔者这里选择Microsoft登录后可以看到密钥申请成功,如下图所示这里要注意图中重点标出的部分,一个是终结点中westus,这个是当前密钥可使用的区域,试用密钥都是westus;另一个是下面的密钥1和密钥2。区域和密钥稍后在后面程序代码中要用到,大家可以单独记下来或者保持该网页不要关闭,方便后续使用。在Azure中申请使用上面30天的试用密钥过期后如果想继续免费使用该服务,还可以到Azure门户中申请密钥,前提是首先要有Azure账户。如果还没有Azure账户,可以免费注册一个。打开免费注册页面,https://azure.microsoft.com/zh-cn/free/ai/,点击免费开始,然后按提示一步步补充完整注册信息。注册过程中需要验证手机号及信用卡,而且会看到1美元的预付款,不过不用担心,这1美元只是用来验证信用卡是否可用,会在几天后返还。有了Azure账户后,打开Azure门户网站,https://portal.azure.com/,点击创建资源,搜索找到Speech(预览),按提示一步步创建,就可以得到对应的密钥。语言理解服务语言理解服务,Language Understanding Intelligence Service,简称LUIS。后面文中使用LUIS来代替语言理解服务。LUIS提供在线的API服务,可以将用户输入的自然语言描述的文本,转换成为计算机能够直接使用的结构化的信息,这样应用程序就可以借助LUIS理解人类自然语言的输入。在线体验LUIS的在线体验场景是https://azure.microsoft.com/zh-cn/services/cognitive-services/language-understanding-intelligent-service/。打开网页后,可以看到如下图所示的示例。可以选择一条指令来观察灯光的变化,也可以通过输入自定的指令文本来控制灯光,可以试试自己习惯的语法,看能否正确的控制灯光变化。基本术语LUIS应用程序在使用LUIS的过程中,我们最初会接触到app这个词,这里的app是指LUIS应用程序。一个LUIS应用程序其实对应的就是一个语言理解模型。通常情况下,一个LUIS应用程序(即一个模型),是用来解决一个特定域(主题)内的语言理解问题的。举个例子,对于旅行相关的主题,如预订机票、酒店等,可以创建一个LUIS应用程序,而对于购物相关的主题,如搜索商品、下单,可以再创建另外一个LUIS应用程序。意图(Intent)意图(Intent),表示用户想要执行的任务或操作。比如询问天气、预订机票等,都是意图,在控制开关灯的例子中,开灯和关灯就是意图。实体(Entity)实体(Entity),想当于上面意图中的参数。比如对象、时间、地点等,都可以标记为实体。举个例子,帮我预定明天北京飞往西雅图的飞机,这里用到了三个实体:明天、北京、西雅图,分别表示了时间、始发地、目的地三个参数。另外,实体在不同的意图之间是可以共享的。举个例子,明天天气怎么样,这个语句的意图是询问天气,而明天是其中的一个实体,表示时间参数,和上一个例子中的实体是一样的。语句(Utterance)语句(Utterance),是LUIS应用需要处理的用户的输入。在训练时提供的语句应该是尽可能包含不同的说话方式或不同的语法的,这样训练出来的结果会更好一些。定制语言理解服务LUIS提供一些预先构建好的域、实体及意图,覆盖了比较全的场景。一种常见的做法是添加预构建的内容,然后迭代完成自定义的模型。我们这里的演示的场景比较简单,直接动手从头定制一个语音控制开关灯需要用到的LUIS应用程序。登录LUIS打开LUIS网站https://www.luis.ai,并登录对应的微软账号。如果是第一次登录,网站还会请求访问对应账号的一些信息,点Yes继续。网站首次加载较慢,需要耐心等待,必要的时候可以刷新重新再次加载。如果页面跳转到了欢迎页面https://www.luis.ai/welcome,可以翻到页面最下方,点击Create LUIS app,然后在下一个页面补充缺失的信息,将Country设为China,并勾选I agree条款,然后点击Continue。直到看到My Apps页面,才算登录完成,如下图所示创建LUIS应用点击Create new app,创建一个新的LUIS应用。注意,Culture要选择Chinese,Name随意,这里使用LightControl。然后点击Done完成创建。添加意图创建完LUIS应用程序后会直接进入Intents页面,也可以通过点击左侧的Intents进入。然后点Create new intent创建一个新的意图。在弹出窗中输入意图的名称,这里使用TurnOn表示开灯的意图,完成后点击Done。注意这里意图的名称在后面程序中会用到,拼写及大小写要保持前后一致。完成后会进入TurnOn意图的设置页面,可以在输入框中输入不同的语句,然后回车就可添加到语句列表中。这里添加了4条语句:打开、开灯、请开灯、把灯打开。点击左侧Intents回到意图页面,然后重复上面的步骤,添加TurnOff意图表示关灯,并添加4条语句:关闭、关灯、请关灯、关闭灯泡。添加实体意图是必需的,而实体不是必需的。可以根据具体的场景来决定是否添加实体,比如有需要控制客厅灯和卧室灯两个灯,应该定义个实体来标记房间是客厅还是卧室,而不是使用多个意图分别控制不同的灯。LUIS支持多种类型的实体来应对各种复杂的情况,包括简单实体、列表实体、正则表达式、复合实体等,这里不详细展开,有兴趣的可以参考https://docs.microsoft.com/zh-cn/azure/cognitive-services/luis/luis-concept-entity-types里的介绍。在我们下面的例子中只控制一个灯,这里就不添加实体了。训练添加完意图和实体后就可以训练了,可以看到右上角有个带红点的Train按钮,点击就可以直接训练。训练速度非常快,几秒种之后看到红点变成绿点就表示训练完成了。测试点击右上角的Test,会滑出Test侧边栏,在输入框中输入测试语句并回车,可以看到对应的意图及得分。这里使用的测试语句是请帮我把灯打开,可以看到识别到的意图是TurnOn,得分是0.86,意图正确且比较接近1,结果还不错。发布点击顶部的PUBLISH进入发布页面,在页面中点击Publish按钮就直接发布了。发布成功后可以看到右侧的版本号及发布时间。将页面翻到底部可以找到服务发布后的区域、密钥和终结点。要注意这里的终结点是以q=结尾的,需要在终结点的最后拼上要测试的语句,比如请帮我把灯打开,得到一个完整的链接,然后再从浏览器里打开,就可以看到识别后的结果。识别结果是以JSON格式返回的,其中topScoringIntent是最匹配的意图,我们后面要用到。迭代LUIS应用在发布后可以继续改进数据,添加新语句或意图或实体,然后再训练,再发布。在这样的周期中反复迭代,最终达到最佳的效果。在上一步发布的终结点最后拼上一个新的语句"北京天气怎么样",然后在浏览器进行测试,可以发现最终topScoringIntent是TurnOff,这显然是不对的,而且得分只有0.14,和其它几个意图的得分差不多。我们需要对模型改进一下,再进行一次迭代。回到LUIS的意图页面,我们可以看到这里有一个None意图,这是创建完LUIS应用程序后自带的一个意图,可以直接打开该意图的页面,在其中添加一条语句无关语句,比如"北京天气怎么样"。还一种做法是通过审查终结点上得到的语句来改进模型。点击左侧的Review endpoint utterances,进入审查页面,可以看到我们刚才在终结点上查询的语句,我们可以把"北京天气怎么样"右侧的Aligned Intent改为None,然后点击右侧的对勾,此时,该语句就会被自动加到了None意图中。然后再次训练、发布,再使用之前的终结点查询"北京天气怎么样",可以看到结果正确,topScoringIntent是None意图。Tips:最佳实践是None意图中语句的数量应占总语句数量的10%~20%。查看应用程序ID点击SETTINGS进入应用程序设置页面就可以看到Application ID,某些情况下会用到。密钥管理如果要在程序中调用LUIS服务,必须要有密钥。LUIS 中有两种密钥:创作密钥和终结点密钥。创作密钥是在首次登录LUIS时自动分配的,终结点密钥是在Azure门户申请的。终结点密钥不是必需的,可以在下面的示例中继续使用创作密钥。但是创作密钥的免费配额要比终结点密钥低得多,所以还是建议在LUIS应用程序发布后,申请一个终结点密钥并分配给LUIS应用程序供外部调用。创作密钥创作密钥是在登录LUIS时自动创建的免费密钥,创建的多个LUIS应用程序使用同一个密钥,且每月只能调用1千次。创作密钥的值可以在User settings中找到,点击右上账户名出现下拉菜单,再点击Settings,就可以看到创作密钥:也可以在发布页面的终结点处看到创作密钥:终结点密钥终结点密钥是在Azure门户中申请的密钥,申请的免费密钥的配额是月1万次调用,每秒最多5次调用,远大于创作密钥的配额。终结点密钥的申请方法和前面介绍的在Azure门户中申请语音服务密钥的方法类似,在创建资源时搜索LUIS即可找到Language Understanding服务,按步骤创建个免费密钥即可。这里我们换一种申请方式,在Visual Studio中借助Tools for AI来申请密钥。确保已安装Visual Studio 2017并安装了Tools for AI扩展。  如果没有安装,请参考AI应用开发实战 - 从零开始配置环境中Windows下开发环境搭建的第1节安装VS和第5节安装Tools for AI插件。确保Visual Studio中登录了对应的微软账号。  如果没有登录,可以在Visual Studio的右上角看到登录按钮,点击后按步骤登录。确保可以看到Azure订阅  在视图菜单中,选择Cloud Explorer,如下图点击账户管理,确保可以刷出你的订阅,如果没有刷出,请尝试重启Visual Studio并稍后再试。创建认知服务  在视图菜单中,选择服务器资源管理器,找到AI工具,在Azure认知服务上点右键,创建新认知服务。  选择已有的订阅,新申请的账户通常会有个名为免费试用的订阅。选择一个已有的资源组,如果没有资源组,需要在Azure上先创建一个。API类型选择LUIS。服务名可以随便起,这里使用LUIS。位置可以先东亚,East Asia。定价申请免费的,直接选F0。点击确定来创建。  创建成功后可以在Azure认知服务下看到名为LUIS的服务,右键选择管理密钥,就可以看到申请的密钥1和密钥2。新申请的密钥需要几分钟的部署时,之后才可以使用。分配密钥新申请的终结点密钥需要分配到LUIS应用程序中才可以使用对应的终结点进行查询。回到LUIS的发布页面,翻到最下面的Resources and Keys,切换到Asia Regions,点击Add Key按钮。在弹出的Assign a key to your app对话框中,依次选择刚才使用的订阅和创建的密钥,点击Add Key按钮。可以看到Resoures and Keys中Asia Regions下出现了终结点密钥和使用该密钥的终结点地址。构建智能家居应用程序的源代码在https://github.com/MS-UAP/edu/tree/master/AI301/LightControl,将源代码下载到本地后,用Visual Studio 2017打开解决方案文件LightControl\LightControl.sln。界面设计在解决方案资源管理器中找到Form1.cs,双击打开对应的设计界面,如下图所示左侧是一个图片控件,可以显示灯打开时和关闭时的图片,来模拟真实的开关灯操作。在解决方案资源管理器中可以看到LightOn.png和LightOff.png两张图片,用来显示灯处在不同的状态下。右侧是一个文本框,用来显示一些日志,比如语音转文本过程中的中间结果、最终结果以及识别出的意图等信息。通过日志,我们可以看到语音服务和LUIS是否已正常连接并正常工作,如果出现异常,也会在这里显示异常信息,方便对问题进行排查。在Form窗体上点右键,查看代码,打开该窗体的代码页面。首先,在构造函数中,控件初始化完成后,让图片控件显示一张关闭着的灯的图片: public Form1() { InitializeComponent(); pictureBox1.Load("LightOff.png"); }然后,封装要用到的一些界面操作,例如,在右侧文本框中追加日志输出,模拟打开灯,关闭灯等: private void Log(string message, params string[] parameters) { MakesureRunInUI(() => { if (parameters != null && parameters.Length > 0) { message = string.Format(message + "\r\n", parameters); } else { message += "\r\n"; } textBox1.AppendText(message); }); } private void OpenLight() { MakesureRunInUI(() => { pictureBox1.Load("LightOn.png"); }); } private void CloseLight() { MakesureRunInUI(() => { pictureBox1.Load("LightOff.png"); }); } private void MakesureRunInUI(Action action) { if (InvokeRequired) { MethodInvoker method = new MethodInvoker(action); Invoke(action, null); } else { action(); } } 集成语音服务SDK和语言理解服务SDK这个代码示例中,我们用语音服务SDK来处理音频转文本,用语言理解服务SDK来提取文本中的意图。首先添加SDK的引用。切换到解决方案资源管理器,在LightControl下的引用上点右键,选择管理NuGet程序包。在打开的NuGet包管理器中,依次搜索并安装下面3个引用:Microsoft.CognitiveServices.SpeechNewtonsoft.JsonMicrosoft.Azure.CognitiveServices.Language.LUIS.Runtime然后回到Form1.cs的代码编辑页面,引用命名空间using Microsoft.CognitiveServices.Speech; using Microsoft.Azure.CognitiveServices.Language.LUIS.Runtime;然后配置两个服务用到的密钥、区域及终结点本文前面申请到的语音服务的30天试用密钥是6d04e77c6c6f4a02a9cf942f6419ffaf,区域是westus。前面定制的LUIS应用程序的ID是130e348f-d131-41d1-96b2-a29d42cc1d96,密钥这里示例先用创作者密钥58c57e08c8d540a4aa2196588eb69f8a,终结点字符串比较长,但是LUIS SDK中只需配置到域名即可,不需要后面的路径,所以这里的终结点是https://westus.api.cognitive.microsoft.com // 设置语音服务密钥及区域 const string speechKey = "6d04e77c6c6f4a02a9cf942f6419ffaf"; const string speechRegion = "westus"; // 设置语言理解服务终结点、密钥、应用程序ID const string luisEndpoint = "https://westus.api.cognitive.microsoft.com"; const string luisKey = "58c57e08c8d540a4aa2196588eb69f8a"; const string luisAppId = "130e348f-d131-41d1-96b2-a29d42cc1d96";然后初始化SDK添加成员变量语音识别器和意图预测器,并在Form1_Load函数中初始化,挂载对应的事件处理函数。Tips:Form1_Load函数需通过在Form窗体设计界面直接双击窗体的标题栏来添加。// 语音识别器 SpeechRecognizer recognizer; // 意图预测器 Prediction intentPrediction; private void Form1_Load(object sender, EventArgs e) { try { SpeechFactory speechFactory = SpeechFactory.FromSubscription(speechKey, speechRegion); // 设置识别中文 recognizer = speechFactory.CreateSpeechRecognizer("zh-CN"); // 挂载识别中的事件 // 收到中间结果 recognizer.IntermediateResultReceived += Recognizer_IntermediateResultReceived; // 收到最终结果 recognizer.FinalResultReceived += Recognizer_FinalResultReceived; // 发生错误 recognizer.RecognitionErrorRaised += Recognizer_RecognitionErrorRaised; // 启动语音识别器,开始持续监听音频输入 recognizer.StartContinuousRecognitionAsync(); // 设置意图预测器 LUISRuntimeClient client = new LUISRuntimeClient(new ApiKeyServiceClientCredentials(luisKey)); client.Endpoint = luisEndpoint; intentPrediction = new Prediction(client); } catch (Exception ex) { Log(ex.Message); } } 然后补充完整几个事件处理函数语音转文本时会不断的接收到中间结果,这里把中间结果输出到日志窗口中// 识别过程中的中间结果 private void Recognizer_IntermediateResultReceived(object sender, SpeechRecognitionResultEventArgs e) { if (!string.IsNullOrEmpty(e.Result.Text)) { Log("中间结果: " + e.Result.Text); } }识别出现错误的时候,也把错误信息输出到日志窗口// 出错时的处理 private void Recognizer_RecognitionErrorRaised(object sender, RecognitionErrorEventArgs e) { Log("识别错误: " + e.FailureReason); }静默几秒后,SDK会认为语音结束,此时返回语音转文本的最终结果。这里拿到结果后,在日志窗口中显示最终结果,并进一步处理文本结果 // 获得音频分析后的文本内容 private void Recognizer_FinalResultReceived(object sender, SpeechRecognitionResultEventArgs e) { if (!string.IsNullOrEmpty(e.Result.Text)) { Log("最终结果: " + e.Result.Text); ProcessSttResultAsync(e.Result.Text); } }添加处理文本的函数,这里从文本中获取意图,然后根据意图的值,来执行开灯或关灯操作 private async void ProcessSttResultAsync(string text) { // 调用语言理解服务取得用户意图 string intent = await GetIntentAsync(text); // 按照意图控制灯 if (!string.IsNullOrEmpty(intent)) { if (intent.Equals("TurnOn", StringComparison.OrdinalIgnoreCase)) { OpenLight(); } else if (intent.Equals("TurnOff", StringComparison.OrdinalIgnoreCase)) { CloseLight(); } } }然后,添加对LUIS SDK的调用,可以从文本中获取意图 private async Task<string> GetIntentAsync(string text) { try { var result = await intentPrediction.ResolveAsync(luisAppId, text); Log("意图: " + result.TopScoringIntent.Intent + "\r\n得分: " + result.TopScoringIntent.Score + "\r\n"); return result.TopScoringIntent.Intent; } catch (Exception ex) { Log(ex.Message); return null; } }编译运行,并对着麦克风说出指令,就可以看到对应的效果了。同时我们可以看到语音转文本的中间结果在不断变化,说明服务端会根据后续接收到的音频不断进行调整,最终返回一个最佳的结果。扩展和习题本文通过介绍语音转文本服务及语言理解服务,并将两个服务集成在一个程序中完成了个模拟的智能家居应用。回头看一下我们的场景非常简单,这里提出一些改进作为习题供大家练习:现在只能控制一个灯,可以考虑控制更多的灯,客厅灯,卧室灯,等等,可以考虑在LUIS中增加实体来实现这个目标。如果每次开灯或关灯时,智能家居都可以用人类的语音反馈给人"灯已打开"、"已经把灯关上了",这样的话可以得到更好的体验。微软认知服务也提供了文本转语音的服务,在不久的将来还会支持开发者定义自己的语音字体,可以定制自己喜欢的声音,使得用户的体验更好。实现了多个灯的控制及语音反馈以后,还可以考虑让智能家居应用支持多轮对话。比如,当人说开灯的时候,智能家居可以询问"要打开哪里的灯",并按照后续补充的指令打开对应的灯。当然还可以举出更多的场景使得智能家居更完美,大家可以充分种用微软提供的认知服务,考虑并设计自己的智能家居应用。
0
0
0
浏览量1235
金某某的算法生活

微软认知服务应用秘籍 – 支持跨平台客户端的视觉服务中间层

不断演进的应用场景初级应用场景—宅在家里场景:Bob同学有一天在网上看到了一张建筑物的图片,大发感慨:"好漂亮啊!这是哪里?我要去亲眼看看!"Bob同学不想问别人,可笑的自尊心让他觉得这肯定是个著名的建筑,如果自己不知道多丢脸!怎么解决Bob同学的烦恼呢?我们看看微软认知服务是否能帮助到Bob同学,打开这个链接:https://azure.microsoft.com/zh-cn/services/cognitive-services/computer-vision/向下卷滚屏幕,到"识别名人和地标"部分,在"图像URL"编辑框里输入了这张图片的网络地址,然后点击"提交",一两秒后,就能看到关于这张图片的文字信息了(见下图),原来这个建筑叫做"Space Needle"!但是呢,不太人性化,因为是JSON文件格式的,幸好Bob同学是个程序员,Bob同学想把这个场景做成一个实际的应用,以帮助他人解决类似问题。Bob同学刚学习了微软认知服务的应用教程,于是打开Windows 10 PC,启动VS2017,安装了Visual Studio Tools for AI后,先在Server Explorer->AI Tools->Azure Cognitive Services上点击鼠标右键,Create New Cognitive Service,API Type选择ComputerVision (如果已经有了就不需要重复申请了),得到了Key和Endpoint,按照《漫画翻译篇》教程所讲述的过程,照猫画虎,花了一两个小时,就把应用做好了。开发技术文档在这个链接里面。目前Bob的同学的应用架构是这样的:(上图中右侧的框图内的文字是“地标识别”,下同) 中级应用场景—出门在外Bob同学很满意地试着自己的作品,长城,天安门,故宫……都能认出来!但是,Bob同学忽然想到,如果出门在外遇到一个漂亮建筑,没有PC,只有手机怎么办?于是Bob同学又启动了VS2017,创建了一个Xamarin项目,重用了PC上的code,把这个场景搞定了:拿起Android或者iOS手机,对着建筑物一框,几秒后就会有结果返回,告诉用户眼前的这个建筑叫什么名字。太方便啦!所以,Bob同学的应用架构进化了一些:高级应用场景—扩展信息Bob同学用手机给很多同学们安装后显摆了几天,有人问他:"Space Needle是啥?""这个……这个……哦!你可以在Bing上搜索一下啊!""你的程序能不能顺便帮我们搜索一下呢?""嗯……啊……当然啦!"硬着头皮说了这句话后,Bob同学赶紧回去查微软认知服务的网站了。Bingo! 在这里了:https://azure.microsoft.com/zh-cn/services/cognitive-services/bing-entity-search-api/与前面的教程里描述的类似,申请了搜索服务后,也得到了Endpoint和Key,照猫画虎地把客户端改了一下,增加了搜索服务的功能,衔接到了地标识别逻辑的后面,也就是把地标识别的结果"Space Needle"作为关键字传送给实体搜索服务,然后再把结果展示出来。注意这里要申请的API在Bing.Search.v7里面,技术文档在这个链接里面。于是Bob同学的应用架构变成了这个样子:(上图中右侧的框图内的文字是“实体搜索”,下同)这个图的连接线看着好奇怪,黄色的线为什么不连接到左侧的客户端上呢?这里特意这样画,为了表示黄色的连接(REST API调用)是接在蓝色的连接之后的,有依赖关系。在下一个场景里,大家会看到更复杂的例子。终级的应用场景—并发处理在一阵手忙脚乱的部署之后,所有的同学的手机都可以使用这个新App了,Bob同学很自豪。这时,学习委员走过来了(也是体育课代表),问Bob:"出门旅游的机会不多,我想用这个App做更多的日常的事情,比如扫一张照片,就能知道这个明星的名字和背景资料,或者是照一件衣服就能知道在哪里买,还有看到一个电话号码后,想用手机扫一下就能记录下来……这些能办到吗?"Bob同学边听边镇静地点头,其实后背都湿透了,嘴上不能服软:"我回去想想办法吧!"Bob同学翻阅了微软认知服务的所有技能,在纸上画了一个草图,来解决学习委员的问题:(上图中右侧的框图内的文字是“名人识别”,下同)同时有三根蓝线都从同一个客户端连接到不同的认知服务上,是因为客户端程序并不知道要识别的物体是建筑物呢,还是人脸呢,或是电话号码呢?需要一个个的去尝试调用三个API,如果返回有效的结果,就代表识别出了该实体的类型。画完图后,本来以为会轻松的Bob同学,忽然发现他需要不断更新三个客户端的代码:PC,Android,iOS,来满足更多的学习委员的需要(如同右侧那个上下方向的箭头一样是可扩充的),然后再分别发布出去!并且他意识到了另外一个问题:每个客户端需要访问认知服务四次才能完成这个场景!不但网络速度对用户体验造成了影响,而且流量就是钱啊!如果将来需要支持更多的识别类型,连接线的增长速率将会是几何级别的!My Omnipotent God!Tell Me How!重构Bob同学想起了刚买到的《构建之法》第三版,仔细阅读了第9,10,11三章,明白了一些基本的概念:需求是不断演进的,任何一个软件都需要不断迭代定位典型用户(学习委员)和常用场景(出门旅游还是宅在家里)在需求分析阶段,要搞清楚在现实世界里,都有哪些实体,如何抽象出我们真正关心的属性和方法PM/用户提出的需求,程序员需要认真理解,深入到实际问题中进行抽象,找到实体和属性/方法在软件系统中的表现,构建框架,然后再编码(想明白了再动手,不能头疼医头,脚疼医脚)"我要重构!"房间里响起了Bob同学的呐喊声,把隔壁邻居吓了一跳:"这小伙子是不是又失恋了?"小提示:需求的"演进"与"变化"是两回事儿,不要混为一谈来掩盖项目经理对需求的分析与把握的不足。简单地举例来说,当项目经理说"地标识别看上去很少有人用,废掉吧,咱们做个名人识别",这个属于需求变化。认知服务应用构建方式两种构建方式的比较微软认知服务应用方式有两大类:用客户端直接访问认知服务客户端通过中间服务层访问认知服务第一种模式很好理解:微软认知服务7x24小时在云端提供服务,开发者在智能手机或者PC上编写客户端应用程序,调用REST API直接访问云端。但是这种模式有一些潜在的问题,如:客户端代码量大逻辑复杂客户端需要密集发布并持续维护客户端与服务器端耦合度高客户端多次访问服务器网络安全性低无论客户端有多少,依赖的认知服务有多少,其实还是下图所示的模式:目前Bob同学就是使用这种方式,来不断演进他的应用,终于遇到了棘手的问题。为什么呢?因为客户端一旦发布到用户手里,对发布者来说就比较被动了,需要非常小心地维护升级,每次都要全面测试,测试点多而复杂。即使有应用商店可以帮助发布,但要把所有用户都升级到最新版本,还是需要很长时间的,这意味着你还需要向后兼容。第二种模式可以用简单的图来表示:有规模的商业化应用,一般都采用这种模式搭建应用架构,以便得到以下好处:客户端代码量小逻辑简单客户端不需要密集发布和维护客户端与认知服务的耦合度低客户端单次访问服务器网络安全性高拉个表格,一目了然:如果有了中间服务层,客户端的工作就简化到只做与中间服务层通信,提交请求,接收数据,用户交互等等,而复杂的商业逻辑,可以在中间服务层实现。而且在更新业务逻辑的时候,大多数情况下,只需要修改中间服务层的代码,无需更新客户端。对于多种客户端的支持问题,用微软VS2017提供的跨平台Xamarin架构可以解决,开发者只需要写C#程序,就可以把应用部署在Windows/Android/iOS设备上,一套代码搞定。中间层服务也不是十全十美,带来的问题有二:1)需要云端支持,要花钱的;2)图片传输的过程会发生两次,第一次是从客户端到中间层,第二次是中间层到微软认知服务,这样会增加网络时间上的开销。但是有个好消息是第二次传输所花费的时间要比第一次小一个数量级,因为是服务器对服务器的通信,如果你自己的服务器也放在Azure上,那么和微软认知服务的服务器就可能在一个大机房里了,局域网的速度!并且,这个开销与客户端多次访问服务器相比,也是占优的选择,所以大家可以在有条件的情况下尽量使用第二种方式做商业应用。另外一种分类方式如果关注于对认知服务的使用,也可以用另外一种分类方式:单独使用某个服务串行使用两个以上的服务并行使用两个以上的服务串并行混合使用三个以上的服务比如上面的最后的场景,实际上是第四种方式:先并行使用了地标识别、名人识别、OCR,然后又串行使用了实体搜索服务。合理的应用架构我们来帮助Bob同学重新设计一下他的应用架构:上图只是个粗略的架构,中间服务层具体如何实现呢?我们常听到的一句话是"这个问题你只要充值就能解决了" 没错,做信仰充值:先安装Visual Studio 2017 and Tools for AI,再接着往下看。从零开始构建中间服务层环境要求与基本步骤环境要求:强烈建议使用Windows 10 较新的版本(笔者使用的是Version 1803)。使用Windows 7也应该可以,但是笔者没有做过具体测试。至少8G内存。只有4G的话可能会比较吃力。CPU主频2.5GHz以上,最好是i7。1.9GHz + i5的配置比较吃力。可以访问互联网上的微软认知服务基本步骤:安装Visual Studio 2017 Community或以上版本,注意要安装服务器开发包,否则找不到第4步的模板。下载安装Microsoft Visual Studio Tools for AI扩展包,安装完后重启VS2017。在Server Explorer中的AI Tools->Azure Cognitive Services菜单上,点击鼠标右键,申请两个认知服务:Bing.Search.V7和ComputerVision。关于如何申请服务,请看本系列文章的上一篇。在VS2017中创建一个ASP.NET Core Web Application,在里面编写中间服务层的逻辑代码。利用简单的客户端进行测试。下面我们展开第4步做详细说明。创建应用服务在VS2017中创建一个新项目,选择Web->ASP.NET Core Web Application,如下图:给项目取个名字叫做"CognitiveMiddlewareService",Location自己随便选,然后点击OK进入下图:在上图中选择"API",不要动其他任何选项,点击OK,VS一阵忙碌之后,就会生成下图的解决方案:这是一个最基本的ASP.NET Core Web App的框架代码,我们将会在这个基础上增加我们自己的逻辑。在写代码之前,我们先一起搞清楚两个关于ASP.NET Core框架的基本概念。ASP.NET Core的两个基本概念依赖注入ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,这是一种在类及其依赖关系之间实现控制反转 (IoC) 的技术,原文链接在这里。简单的说就是:定义一个接口定义一个类实现这个接口在框架代码Startup.cs中注册这个接口和类在要用到这个服务的类(使用者)的构造函数中引入该接口,并保存到成员变量中在使用者中直接使用该成员变量->方法名称我们在后面的代码中会有进一步的说明。发起HTTP请求框架提供了一种机制,可以通过注册IHttpClientFactory用于创建HttpClient实例,这种方式带来以下好处:提供一个集中位置,用于命名和配置HttpClient实例通过委托HttpClient中的处理程序来提供中间层服务管理基础HttpClientMessageHandler实例的池和生存期,避免在手动管理HttpClient生存期时出现常见的DNS问题添加可配置的记录体验,以处理HttpClientFactory创建的客户端发送的所有请求以上是原文提供的解释,链接在这里。可能比较难理解,但坊间一直流传着HttpClient不能释放的问题,所以用IHttpClientFactory应该至少可以解决这个问题。但是在使用它之前,我们需要安装一个NuGet包。在解决方案的名字上点击鼠标右键,在出现的菜单中选择"Manage NuGet Packages…",在出现的如下窗口中,输入"Microsoft.extensions.http",然后安装Microsoft.Extensions.Http包:安装完毕后,需要在Startup.cs文件里增加依赖注入:services.AddHttpClient()。文件目录组织方式和层次关系先在生成好的框架代码的基础上,建立下图所示的文件夹:CognitiveServicesMiddlewareServiceProcessorsControllers是基础框架带的文件夹,不需要自己创建。创建这些文件夹的目的,是让我们自己能够缕清逻辑,写代码时注意调用和被调用的关系,用必要的层次来体现软件的抽象。以本案例来说,模块划分与层次抽象应该如下图所示(下图中带箭头的实线表示调用关系):基础服务层蓝色的层,也就是CognitiveServices文件夹,包含了两个访问认知服务的基础功能:VisionService和EntitySearchService。它们返回了最底层的结果:VisionResult和EntityResult。这一层的每个服务,只专注于自己的网络请求与接收结果的任务,不管其它的事情。如果认知服务编程接口有变化,只修改这一层的代码。集成服务层黄色的层,也就是MiddlewareService文件夹,是我们自己包装认知服务的逻辑层,在这个层中的代码,每一个服务都是用串行方式访问认知服务的:在用第一个输入(假设是图片)得到第一个认知服务的返回结果后(假设是文字),再把这个返回结果输入到第二个认知服务中去,得到内容更丰富的结果。它们返回了集成后的结果:LandmarkResult和CelebrityResult,这两个结果的定义已经对认知服务返回的结果进行了进一步的抽象和隔离,其目的是让后面的逻辑代码只针对这一层的抽象进行处理,不必考虑更底层的数据结构。任务调度层绿色的层,也就是Processors文件夹,是包装业务逻辑的代码,在本层中做任务分发,用并行方式同时访问两个以上的认知服务,将返回的结果聚合在一起,并根据需要进行排序,最后生成要返回的结果AggregatedResult。CognitiveServices文件夹在这个文件夹中,我们需要添加以下文件:IVisionService.csVisionService.csVisionResult.csIEntitySearchService.csEntitySearchService.csEntityResult.csHelper.csIVisionService.cs - 访问影像服务的接口定义,需要依赖注入using System.IO; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public interface IVisionService { Task<Landmark> RecognizeLandmarkAsync(Stream imgStream); Task<Celebrity> RecognizeCelebrityAsync(Stream imgStream); } } VisionService.cs - 访问影像服务的逻辑代码 using Newtonsoft.Json; using System; using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class VisionService : IVisionService { const string LandmarkEndpoint = "https://eastasia.api.cognitive.microsoft.com/vision/v2.0/models/landmarks/analyze"; const string CelebrityEndpoint = "https://eastasia.api.cognitive.microsoft.com/vision/v2.0/models/celebrities/analyze"; const string Key1 = "0e290876aed45d69f6fb97bb621f71"; const string Key2 = "9799f09b87e4be6b2be132309b8e57"; private readonly IHttpClientFactory httpClientFactory; public VisionService(IHttpClientFactory cf) { this.httpClientFactory = cf; } public async Task<Landmark> RecognizeLandmarkAsync(Stream imgStream) { VisionResult result = await this.MakePostRequest(LandmarkEndpoint, imgStream); if (result?.result?.landmarks?.Length > 0) { return result?.result?.landmarks[0]; } return null; } public async Task<Celebrity> RecognizeCelebrityAsync(Stream imgStream) { VisionResult result = await this.MakePostRequest(CelebrityEndpoint, imgStream); if (result?.result?.celebrities?.Length > 0) { return result?.result?.celebrities[0]; } return null; } private async Task<VisionResult> MakePostRequest(string uri, Stream imageStream) { try { using (HttpClient httpClient = httpClientFactory.CreateClient()) { using (StreamContent streamContent = new StreamContent(imageStream)) { streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); using (var request = new HttpRequestMessage(HttpMethod.Post, uri)) { request.Content = streamContent; request.Headers.Add("Ocp-Apim-Subscription-Key", Key1); using (HttpResponseMessage response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { string resultString = await response.Content.ReadAsStringAsync(); VisionResult result = JsonConvert.DeserializeObject<VisionResult>(resultString); return result; } else { } } } return null; } } } catch (Exception ex) { return null; } } } }小提示:上面的代码中的Key1/Key2是不可用的,请用自己申请的Key和对应的Endpoint来代替。VisionResult.cs – 认知服务返回的结果类,用于反序列化using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class VisionResult { public Result result { get; set; } public string requestId { get; set; } } public class Result { public Landmark[] landmarks { get; set; } public Celebrity[] celebrities { get; set; } } public class Landmark { public string name { get; set; } public double confidence { get; set; } } public class Celebrity { public virtual string name { get; set; } public virtual double confidence { get; set; } } }IEntitySearchService.cs – 访问实体搜索服务的接口定义,需要依赖注入using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public interface IEntitySearchService { Task<string> SearchEntityAsync(string query); } }EntitySearchService.cs – 访问实体搜索服务的逻辑代码using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class EntitySearchService : IEntitySearchService { const string SearchEntityEndpoint = "https://api.cognitive.microsoft.com/bing/v7.0/entities?mkt=en-US&q="; const string Key1 = "a0be81df8ad449481492a11107645b"; const string Key2 = "0803e4673824f9abb7487d8c3db6dd"; private readonly IHttpClientFactory httpClientFactory; public EntitySearchService(IHttpClientFactory cf) { this.httpClientFactory = cf; } public async Task<string> SearchEntityAsync(string query) { using (HttpClient hc = this.httpClientFactory.CreateClient()) { string uri = SearchEntityEndpoint + query; string jsonResult = await Helper.MakeGetRequest(hc, uri, Key1); Debug.Write(jsonResult); return jsonResult; } } } }小提示:上面的代码中的Key1/Key2是不可用的,请用自己申请的Key和对应的Endpoint来代替。EntityResult.cs – 认知服务返回的结果类,用于反序列化using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class EntityResult { public string _type { get; set; } public Querycontext queryContext { get; set; } public Entities entities { get; set; } public Rankingresponse rankingResponse { get; set; } } public class Querycontext { public string originalQuery { get; set; } } public class Entities { public Value[] value { get; set; } } public class Value { public string id { get; set; } public Contractualrule[] contractualRules { get; set; } public string webSearchUrl { get; set; } public string name { get; set; } public string url { get; set; } public Image image { get; set; } public string description { get; set; } public Entitypresentationinfo entityPresentationInfo { get; set; } public string bingId { get; set; } } public class Image { public string name { get; set; } public string thumbnailUrl { get; set; } public Provider[] provider { get; set; } public string hostPageUrl { get; set; } public int width { get; set; } public int height { get; set; } public int sourceWidth { get; set; } public int sourceHeight { get; set; } } public class Provider { public string _type { get; set; } public string url { get; set; } } public class Entitypresentationinfo { public string entityScenario { get; set; } public string[] entityTypeHints { get; set; } } public class Contractualrule { public string _type { get; set; } public string targetPropertyName { get; set; } public bool mustBeCloseToContent { get; set; } public License license { get; set; } public string licenseNotice { get; set; } public string text { get; set; } public string url { get; set; } } public class License { public string name { get; set; } public string url { get; set; } } public class Rankingresponse { public Sidebar sidebar { get; set; } } public class Sidebar { public Item[] items { get; set; } } public class Item { public string answerType { get; set; } public int resultIndex { get; set; } public Value1 value { get; set; } } public class Value1 { public string id { get; set; } } }Helper.cs – 帮助函数using Microsoft.AspNetCore.Http; using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; namespace CognitiveMiddlewareService.CognitiveServices { public class Helper { public static byte[] GetBuffer(IFormFile formFile) { Stream stream = formFile.OpenReadStream(); MemoryStream memoryStream = new MemoryStream(); formFile.CopyTo(memoryStream); var buffer = memoryStream.GetBuffer(); return buffer; } public static MemoryStream GetStream(byte[] buffer) { if (buffer == null) { return null; } return new MemoryStream(buffer, false); } public static async Task<string> MakeGetRequest(HttpClient httpClient, string uri, string key) { try { using (var request = new HttpRequestMessage(HttpMethod.Get, uri)) { request.Headers.Add("Ocp-Apim-Subscription-Key", key); using (HttpResponseMessage response = await httpClient.SendAsync(request)) { if (response.IsSuccessStatusCode) { string jsonResult = await response.Content.ReadAsStringAsync(); return jsonResult; } } } return null; } catch (Exception ex) { return null; } } } }MiddlewareService文件夹在这个文件夹中,我们需要添加以下文件:ICelebrityService.csCelebrityService.csCelebrityResult.csILandmarkService.csLandmarkService.csLandmarkResult.csICelebrityService.cs – 包装多个串行的认知服务来实现名人识别的中间服务层的接口定义,需要依赖注入using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public interface ICelebrityService { Task<CelebrityResult> Do(byte[] imgData); } }CelebrityService.cs – 包装多个串行的认知服务来实现名人识别中间服务层的逻辑代码using CognitiveMiddlewareService.CognitiveServices; using Newtonsoft.Json; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public class CelebrityService : ICelebrityService { private readonly IVisionService visionService; private readonly IEntitySearchService entityService; public CelebrityService(IVisionService vs, IEntitySearchService ess) { this.visionService = vs; this.entityService = ess; } public async Task<CelebrityResult> Do(byte[] imgData) { // get original recognized result var stream = Helper.GetStream(imgData); Celebrity celebrity = await this.visionService.RecognizeCelebrityAsync(stream); if (celebrity != null) { // get entity search result string entityName = celebrity.name; string jsonResult = await this.entityService.SearchEntityAsync(entityName); EntityResult er = JsonConvert.DeserializeObject<EntityResult>(jsonResult); if (er?.entities?.value.Length > 0) { // isolation layer: decouple data structure then return abstract result CelebrityResult cr = new CelebrityResult() { Name = er.entities.value[0].name, Description = er.entities.value[0].description, Url = er.entities.value[0].url, ThumbnailUrl = er.entities.value[0].image.thumbnailUrl, Confidence = celebrity.confidence }; return cr; } } return null; } } }小提示:上面的代码中,用CelebrityResult接管了实体搜索结果和名人识别结果的部分有效字段,以达到解耦/隔离的作用,后面的代码只关心CelebrityResult如何定义的即可。CelebrityResult.cs – 抽象出来的名人识别服务的返回结果namespace CognitiveMiddlewareService.MiddlewareService { public class CelebrityResult { public string Name { get; set; } public double Confidence { get; set; } public string Url { get; set; } public string Description { get; set; } public string ThumbnailUrl { get; set; } } }ILandmarkService.cs – 包装多个串行的认知服务来实现地标识别的中间服务层的接口定义,需要依赖注入using CognitiveMiddlewareService.CognitiveServices; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public interface ILandmarkService { Task<LandmarkResult> Do(byte[] imgData); } }LandmarkService.cs – 包装多个串行的认知服务来实现地标识别的中间服务层的逻辑代码using CognitiveMiddlewareService.CognitiveServices; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace CognitiveMiddlewareService.MiddlewareService { public class LandmarkService : ILandmarkService { private readonly IVisionService visionService; private readonly IEntitySearchService entityService; public LandmarkService(IVisionService vs, IEntitySearchService ess) { this.visionService = vs; this.entityService = ess; } public async Task<LandmarkResult> Do(byte[] imgData) { // get original recognized result var streamLandmark = Helper.GetStream(imgData); Landmark landmark = await this.visionService.RecognizeLandmarkAsync(streamLandmark); if (landmark != null) { // get entity search result string entityName = landmark.name; string jsonResult = await this.entityService.SearchEntityAsync(entityName); EntityResult er = JsonConvert.DeserializeObject<EntityResult>(jsonResult); // isolation layer: decouple data structure then return abstract result LandmarkResult lr = new LandmarkResult() { Name = er.entities.value[0].name, Description = er.entities.value[0].description, Url = er.entities.value[0].url, ThumbnailUrl = er.entities.value[0].image.thumbnailUrl, Confidence = landmark.confidence }; return lr; } return null; } } }小提示:上面的代码中,用LandmarkResult接管了实体搜索结果和地标识别结果的部分有效字段,以达到解耦/隔离的作用,后面的代码只关心LandmarkResult如何定义的即可。LandmarkResult.cs – 抽象出来的地标识别服务的返回结果namespace CognitiveMiddlewareService.MiddlewareService { public class LandmarkResult { public string Name { get; set; } public double Confidence { get; set; } public string Url { get; set; } public string Description { get; set; } public string ThumbnailUrl { get; set; } } }Processors文件夹在这个文件夹中,我们需要添加以下文件:IProcessService.csProcessService.csAggregatedResult.csIProcessService.cs – 任务调度层服务的接口定义,需要依赖注入using System.Threading.Tasks; namespace CognitiveMiddlewareService.Processors { public interface IProcessService { Task<AggregatedResult> Process(byte[] imgData); } }ProcessService.cs – 任务调度层服务的逻辑代码using CognitiveMiddlewareService.MiddlewareService; using System.Collections.Generic; using System.Threading.Tasks; namespace CognitiveMiddlewareService.Processors { public class ProcessService : IProcessService { private readonly ILandmarkService landmarkService; private readonly ICelebrityService celebrityService; public ProcessService(ILandmarkService ls, ICelebrityService cs) { this.landmarkService = ls; this.celebrityService = cs; } public async Task<AggregatedResult> Process(byte[] imgData) { // preprocess // todo: create screening image classifier to get a rough category, then decide call which service // task dispatcher: parallelized run 'Do' // todo: put this logic into Dispatcher service List<Task> listTask = new List<Task>(); var taskLandmark = this.landmarkService.Do(imgData); listTask.Add(taskLandmark); var taskCelebrity = this.celebrityService.Do(imgData); listTask.Add(taskCelebrity); await Task.WhenAll(listTask); LandmarkResult lmResult = taskLandmark.Result; CelebrityResult cbResult = taskCelebrity.Result; // aggregator // todo: put this logic into Aggregator service AggregatedResult ar = new AggregatedResult() { Landmark = lmResult, Celebrity = cbResult }; return ar; // ranker // todo: if there have more than one result in AgregatedResult, need give them a ranking // output generator // todo: generate specified JSON data, such as Adptive Card } } }小提示:大家可以看到上面这个文件中有很多绿色的注释,带有todo文字的,对于一个更复杂的系统,可以用这些todo中的描述来设计独立的模块。AggregatedResult.cs – 任务调度层服务的最终聚合结果定义using CognitiveMiddlewareService.MiddlewareService; namespace CognitiveMiddlewareService.Processors { public class AggregatedResult { public LandmarkResult Landmark { get; set; } public CelebrityResult Celebrity { get; set; } } }其他文件的修改ValuesControllers.cs 注意Post的参数从[FromBody]变成了[FromForm],以便接收上传的图片流数据using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using CognitiveMiddlewareService.CognitiveServices; using CognitiveMiddlewareService.Processors; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; namespace CognitiveMiddlewareService.Controllers { [Route("api/[controller]")] public class ValuesController : Controller { private readonly IProcessService processor; public ValuesController(IProcessService ps) { this.processor = ps; } // GET api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public async Task<string> Post([FromForm] IFormCollection formCollection) { try { IFormCollection form = await this.Request.ReadFormAsync(); IFormFile file = form.Files.First(); var bufferData = Helper.GetBuffer(file); var result = await this.processor.Process(bufferData); string jsonResult = JsonConvert.SerializeObject(result); // return json formatted data return jsonResult; } catch (Exception ex) { Debug.Write(ex.Message); return null; } } } }Startup.csusing CognitiveMiddlewareService.CognitiveServices; using CognitiveMiddlewareService.MiddlewareService; using CognitiveMiddlewareService.Processors; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace CognitiveMiddleService { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<IProcessService, ProcessService>(); services.AddScoped<IVisionService, VisionService>(); services.AddScoped<ILandmarkService, LandmarkService>(); services.AddScoped<ICelebrityService, CelebrityService>(); services.AddScoped<IEntitySearchService, EntitySearchService>(); services.AddHttpClient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); } } }除了第一行的services.AddMvc()以外,后面所有的行都是我们需要增加的依赖注入代码。层次关系总结总结一下,从调用关系上看,是这个次序:Controller -> ProcessService -> LandmarkService/CelebrityService -> VisionService/EntitySearchService其中:·           Controller是个Endpoint·           ProcessService负责任务调度·           LandmarkService/CelebrityService是个集成服务,封装了串行调用底层服务的逻辑·           VisionService/EntitySearchService是基础服务,相当于最底层的原子操作从数据结构上看,进化的顺序是这样的:VisionResult/EntityResult -> CelebrityResult/LandmarkResult -> AggregatedResult其中:·           VisionResult/EntityResult是最底层返回的原始结果,主要用于反序列化·           CelebrityResult/LandmarkResult是集成了多个原始结果后的抽象结果,好处是隔离了原始结果中的一些噪音,解耦,只返回我们需要的字段·           AggregatedResult是聚合在一起的结果,主要用于排序和生成返回JSON数据完整的中间服务层系统栈有的人会问了:有必要搞这么复杂吗?这几个调用在一个帮助函数里不就可以搞定了吗?确实是这样,如果不考虑应用扩展什么的,那就用一个帮助函数搞定;如果想玩儿点大的,那么下面这张图就是一个完整系统的Stack图,这个系统通过组合调用多种微软认知服务/微软地图服务/微软实体服务等,能够提供给用户的智能设备丰富的视觉对象识别体验。上图包含了以下层次:·           EndpointsØ  两个Endpoint,一个处理图片输入,另一个处理文本输入·           Processing and ClassifierØ  包含图像/文字的预处理/预分类·           Task DispatcherØ  并行调用多种服务并协调同步关系·           API agent and RecognizerØ  组合调用各种API,内置的识别器(比如正则表达式)·           APIsØ  各种认知服务API·           ProcessorsØ  隔离层/聚合层/排序器的组合称呼·           Adaptive Card GeneratorØ  生成微软最新推出的Adaptive Card技术的数据,供跨平台客户端接收并渲染·           Assistant ComponentØ  其它辅助组件对中间服务层的测试基本概念与环境搭建做好了一个中间层服务,不是说简单地向Azure上一部署就算完事儿了。任何一个商用的软件,都需要严格的测试,对于普通的手机/客户端软件的测试,相信很多人都知道,覆盖功能点,各种条件输入,等等等等。对于中间层服务,除了功能点外,性能方面的测试尤其重要。如何进行测试呢?工欲善其事必先利其器,先看工具:ASP.NET Core Web API有一套测试工具,请看这个链接:https://docs.microsoft.com/en-us/aspnet/core/test/?view=aspnetcore-2.1,它讲述了一些列的方法,我们不再赘述,本文所要描述的是三种面向场景的测试方法:负载(较重的压力)测试,(较轻的压力)性能测试,(中等的压力)稳定性测试。不是以show code为主,而是以讲理念为主,懂得了理念,code容易写啦。对于一个普通的App,我们用界面交互的方式进行测试。对于一个service,它的界面就相当于REST API,我们可以从客户端发起测试,自动化程度较高。在Visual Studio 2017,有专门的Load Test工具可以帮助我们完成在客户端编写测试代码,调整各种测试参数,然后发起测试,具体的链接在这里。有了工具,再看方法和理念:在本文中,我们主要从概念上讲解一下针对含有认知服务的中间服务层的测试方法,因为认知服务本身如果访问量大的话,是要收取费用的!小提示:各个认知服务的费用标准不同,请仔细阅读相关网页,以免在进行大量的测试时引起不必要的费用发生。负载测试 Load Test测试目的模拟多个并发用户访问中间层服务,集中发生在一个持续的时间段内,以衡量服务质量。负载测试不断的发展下去,负载越来越大,就会变成极限测试,最终把机器跑瘫为止。这种测试可以帮助开发者知道在单机环境下能支持多少用户,进而决定在Azure上要申请多少机器。测试环境注意!我们不是在测试认知服务的性能,是要测试自己的中间层服务的性能,所以如下图所示:要把认知服务用一个模拟的mock up service来代替,这个mock up service可以自己简单地用ASP.NET搭建一个,接收请求后,不做任何逻辑处理,直接返回JSON字符串,但是中间需要模拟认知服务的处理时间,故意延迟2~3秒。另外一个原因是,认知服务比较复杂,可能不能满足很高的QPS的要求,而用自己的模拟服务可以到达极高的QPS,这样就不会正在测试中产生瓶颈。网络环境为局域网内部,亦即客户端、中间层、模拟服务都在局域网内部即可,这样可以避免网络延迟带来的干扰。测试方法与结果在本例中,我们测试了8轮,每轮都模拟不同的并发用户数持续运行一小时,最终结果如下:从图表可以看出,CPU/Memory/QPS都是线性增长的,意味着是可以预测的。延迟(Latency)是平缓的,不会因为并发用户变多而变慢,很健康。可靠性测试 Stability Test测试目的在一个足够长的时间内持续测试服务,中等负载,以检查其可靠性。"足够长"一般定义为12小时、48小时、72小时等等。可以认为,被测对象只要跑够了预定的时长,就算是稳定性过关了。测试环境同理,我们要测试的是中间层服务,而不是认知服务。测试环境与上面相同,也是使用模拟的认知服务,因为72小时的测试时间,会发送大量的请求,很可能超出了当月限额而收取费用。网络环境仍然使用局域网。测试方法与结果模拟10个并发用户,持续向中间层服务发请求12小时,测试结果如下表:从CPU/Memory/Latency/QPS上来看,在12个小时内,都保持得非常稳定,说明服务器不会因为长时间运行而变得不稳定。性能测试 Performance Test测试目的测试端对端(e2e)的请求/响应时间。这是针对某个服务场景的测试,想得到具体的数值,所以不需要很大的负载压力。测试环境这次我们需要使用真实的认知服务,网络环境也使用真实的互联网环境。亦即需要把中间服务层部署到互联网上后进行测试,因为用模拟环境和局域网测试出来的数据不能代表实际的用户使用情况。 测试方法与结果模拟1个用户,持续向中间服务层发送请求1小时。然后模拟3个并发用户,持续向中间服务层发送请求10分钟。这两种方法都不会对认知服务带来很大的压力。在得到了一系列的数据以后,每组数据都会有响应时间,我们把它们按照从长(慢)到短(快)的顺序排列,得到下图(其中横坐标是用户数,纵坐标是响应时间):一般来说,我们要考察几个点,P90/P95/P99,比如P90的含义是:有90%的用户的响应时间小于等于2449ms。这意味着如果有极个别用户响应时间在10秒以上时,是一种正常的情况;如果很多用户(比如>5%)都在10秒以上就不正常了,需要立刻检查服务器的运行状态。最后得到的结果如下表,亦即性能指标: Server-side processing time: 服务器从接收到请求到发送回结果所花费的时间Test client e2e latency: 客户端从发送请求到接收响应所经历的时间习题与进阶学习增加OCR服务以提供识别文字的功能在集成服务层增加可以识别具有标准模式的文字的服务,比如电话号码、网络地址、邮件地址,这需要同时在基础服务层增加OCR底层服务,并在任务调度层增加一个并行任务。部署到实际的Azure环境提供真实服务在本地测试好服务器的基本功能后,部署到Azure上去,看看代码在实际环境中运行会有什么问题。因为我们不能实时地监控服务器,所以需要在服务层上增加log功能。开发Android/iOS应用来提供影像/视觉感知可以选择像Bob同学那样,先用第一种方式直接访问微软认知服务,然后一步步演进到中间层服务模式。建议使用VS2017 + Xamarin利器来实现跨平台应用。图像基本分类在任务调度层,增加一个本地的图像分类器,如同"todo"里的preprocess,能够把输入图片分类成"有人脸"、"有地标"、"有文字"等,然后再根据信心指数调用名人服务或地标服务,以减轻服务器的负担,节省费用。比如,当"有地标"的信心指数小于0.5时,就终止后面的调用。这需要训练一个图片分类器,导出模型,再用Tools for AI做本地推理代码。
0
0
0
浏览量1070
金某某的算法生活

新手一小时就写出人工智能应用 - 看图识熊

如何安装必要的工具并配置环境呢,请看这个详细的解说今后会有更详细的文字版在这个专题出现。如果有对这个教程有疑问,请在这里留言。
0
0
0
浏览量2011
金某某的算法生活

微软认知服务应用秘籍 – 与机器人聊知识

建立知识库什么叫QnA Maker?知识库,就是人们总结出的一些历史知识的集合,存储、索引以后,可以被方便的检索出来供后人查询/学习。QnA Maker是用于建立知识库的工具,使用 QnA Maker,可以根据 FAQ(常见问题解答)文档或者 URL 和产品手册等半结构化内容打造一项问题与解答服务。可以生成一个问题与解答模型,以便灵活地应对用户查询,即用户不必输入精确的查询条件,而是提供以自然对话方式受训的机器人来响应。下图中是知识库与Bot Service的结合使用架构图:与"半结构化数据"并列的是"结构化数据"和"非结构化数据",其中结构化数据可以用关系式数据库来解决,非结构化数据用搜索引擎技术来解决。实际上搜索引擎就是把散落在互联网各个角落的非结构信息变成半结构化或结构化信息。不同于搜索引擎,本文介绍的基于半结构化数据的QnA系统实现方式,是基于小规模数据量的,比如Million级别,而搜索引擎的技术要高级很多,因为要面对Billion级别的数据。但是从原理上讲,大家可以管中窥豹可见一斑。在Azure中申请QnA Maker服务用MSA登录Azure门户,点击"创建资源",然后点击"AI + Machine Learning":在下图中点击"查看全部":在下图中点击"更多:"在下图中点击"QnA Maker":在下图中的有红色*的输入框中,输入必要的信息,比如在Name中输入" SchoolQASystem":点击"创建"后,稍等一会儿,会得到以下通知消息:小提示:可以点击"固定到仪表板",方便后续查找。至此,我们的QnA服务已经申请好了,下一步是建立知识库,填写数据。在QnA Maker网站上建立知识库用Edge浏览器打开https://www.qnamaker.ai,登录自己的MSA账号。如果是第一进入该网站,你的My knowledge bases将会是空白页,点击Create a knowledge base来建立自己的第一个知识库:小提示:这里用的MSA账号应该与申请认知服务的MSA账号相同。STEP 1我们已经做过了,现在在STEP 2中从下拉列表中选择自己的相关信息:在STEP 3中填写一个知识库的名字,比如SchoolQASystemKB:在STEP 5中点击"Create your KB"来建立知识库:小提示:STEP 4可以运行用户通过提供一个静态网页或者一个固定格式的文件,来自动提取问题和答案。稍等一会儿,进入如下页面:我们以学校中常用的一些问答信息为例,点击"+ Add QnA pair"填写如下数据:当然可以根据实际情况,填写其它一些信息。需要注意的是,每个Question需要有唯一的一个Answer来对应,而每个Answer可以有很多个Question,是N:1的关系。小提示:以上数据均为虚构,请填写符合自己学校实际情况的数据。数据填写的差不多了(一次填写不完没关系,可以以后修改),点击"Save and train"按钮,稍等一会儿,点击那个"Test"按钮,进行在线测试。在问题框中输入"报销"或者一个整句“医疗费如何报销”,可以很快得到系统的回复,如下图就是测试结果:此时也可以点击"Inspect"来看细节,比如Confidence score的值是79.75分,也可以在左侧填写更多的问题来对应这个答案,比如"看病报销":觉得满意后,点击"PUBLISH"按钮来发布这个知识库:稍等一会儿,得到如下信息:好啦!到目前为止,我们已经成功建立了第一个知识库。小提示:保留这个网页或者请记住上图中的信息,一会儿我们还会用到它。下面我们有两个选择:1)写代码来访问这个知识库,提供界面供他人使用。2)用微软的另外一项技术 – 聊天机器人技术,以问答方式来提供访问该知识库的界面。这个内容我们在下一个大章节讲述。用代码访问QnA知识库打开利器VS2017,新建一个Windows Desktop WPF项目,给个名字叫QAClient:在MainWindow.xaml中填写如下XAML界面设计代码:<Window x:Class="QAClient.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:QAClient" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="40"/> </Grid.RowDefinitions> <TextBox Name="tb_Dialog" Grid.Row="0"/> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="80"/> </Grid.ColumnDefinitions> <TextBox Name="tb_Question" MaxLines="1" Grid.Column="0"/> <Button Name="btn_Send" Content="Send" Grid.Column="1" Click="btn_Send_Click"/> </Grid> </Grid> </Window>在MainWindow.xaml.cs中添加按钮事件处理函数: public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private async void btn_Send_Click(object sender, RoutedEventArgs e) { // send http post request to qa service Answers results = await QAServiceAgent.DoQuery(this.tb_Question.Text); if (results.answers != null && results.answers.Length > 0) { this.tb_Dialog.Text += "问:" + this.tb_Question.Text + "\r\n"; this.tb_Dialog.Text += results.answers[0].ToString() + "\r\n"; } } } 在工程中添加QAServiceAgent.cs文件,填写以下内容:using System; using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace QAClient { class QAServiceAgent { const string Endpoint = "/knowledgebases/90690b7-dae-4e0d-bda9-16c05e0f163/generateAnswer"; const string Host = "https://openmindqnamaker.azurewebsites.net/qnamaker"; const string Key = "e7e3c51-dc5-4d65-aa3-8da1024c3e13"; const string ContentType = "application/json"; // {"question":"<Your question>"} public static async Task<Answers> DoQuery(string question) { try { using (HttpClient hc = new HttpClient()) { hc.DefaultRequestHeaders.Add("authorization", "EndpointKey " + Key); string jsonBody = CreateJsonBodyElement(question); StringContent content = new StringContent(jsonBody, Encoding.UTF8, ContentType); string uri = Host + Endpoint; HttpResponseMessage resp = await hc.PostAsync(uri, content); string json = await resp.Content.ReadAsStringAsync(); var ro = Newtonsoft.Json.JsonConvert.DeserializeObject<Answers>(json); return ro; } } catch (Exception ex) { Debug.WriteLine(ex.Message); return null; } } private static string CreateJsonBodyElement(string question) { string a = "{\"question\":\"" + question + "\"}"; return a; } } }小提示:上面代码中的Endpoint和Key已经经过修改,是不可用的,请用你自己申请的数据来代替。在工程中添加另外一个文件Result.cs,用于反序列化JSON数据:namespace QAClient { public class Answers { public Answer[] answers { get; set; } } public class Answer { public string[] questions { get; set; } public string answer { get; set; } public float score { get; set; } public int id { get; set; } public string source { get; set; } public object[] metadata { get; set; } public override string ToString() { return string.Format("Answer: {0}, Score:{1}", answer, score); } } }代码完成!搓搓双手,按Ctrl+F5走起一波!哇哦!好俊的界面:在下方的输入框中输入"报销"、"开学日期"、"补考"等问题,都会得到预定的答案。输入"校长是谁"就没有match到任何答案,因为我们没有在数据中准备这个问题。我们是用客户端形式做了一个问答界面,当然也可以在网站上用REST API实现同样的功能。同时,微软提供了Bot Service,下一章我们看看如何在不写任何代码的情况下,完成机器人与QnA服务的集成。建立对话机器人服务什么是机器人服务?机器人是用户使用文本、图形(卡片)或语音通过聊天的方式进行交互的应用。它可以是一个简单的问答对话,也可以是一个复杂的机器人,允许用户使用模式匹配、状态跟踪和与现有业务服务完美集成的人工智能技术通过智能的方式与服务进行交互。常见的机器人服务有以下几类:商务/金融服务,如银行提供的在线客服信息服务,如政府部门的常用信息问答服务产品服务,如企业提供的产品咨询服务总之,机器人服务就是用计算机代替人来完成一些具有标准化流程的人机对话服务。微软提供的机器人服务的概述在这个链接里面,建立一个机器人服务和一般的软件其实没多大区别,也要经过以下几个步骤然后再迭代:计划:确定需求,需要什么类型的机器人服务构建:选择工具/语言/框架等等测试:机器人其实知识界面,后端连接了一堆智能服务,要通过机器人界面测试好所有功能发布:发布的Azure上或者自己的Web服务器上连接:可以将机器人连接到以有的客户端软件上,方便用户接入,比如Cortana、Skype等评估:根据运行日志获得基本运行指标,如流量、延迟、故障等等,作为迭代的依据创建对话机器人用MSA登录Azure门户,点击"创建资源":小提示:此MSA账号需要与前面的QnA服务的MSA账号相同。选择"AI + Machine Learning",在右侧选择"Web App Bot":在上图中选择Web App Bot,在右侧弹出的窗口中点击"创建"按钮,得到下图:在上图中填写必要的信息,比如机器人名称是"SchoolQnAWebBot",机器人模板要选择"Question and Answer",可以关闭Application Insights来简化过程。小提示:记住要点击"选择"按钮,否则不生效。最后点击"创建"按钮,稍等一会儿,得到以下通知:小提示:此时可以固定到仪表板,方便以后访问。连接知识库在仪表板上点击这个机器人,然后点击左侧的"网络聊天测试":在上图中下方输入"报销",机器人傻傻的回了一句"you said 报销"。因为这个机器人刚刚建立,还没有连到上面创建的知识库上,所以它并不知道如何回答你的问题。所以点击左侧的"应用程序设置",看到一个吓人的页面,不要慌,镇定地向下卷滚,直到看到QnA开头的项目,一共有三个,如下图:这三个值本来是空的,我们需要用以前得到一个信息来填写:把颜色对应的信息填写到空白处就可以了。填写完毕后,点击最上方的"保存"按钮,稍等一会儿,这个Bot service会被重新编译部署。等到部署完毕收到通知后,可以再测试一下,输入"报销"并回车,机器人就会给你返回知识库中的答案:WoW! 我们没写一行code,就完成了知识库和机器人的连接,龙颜大悦,喝口燕窝银耳汤,看看还有什么好玩儿的!连接已有应用这个机器人虽然已经建立起来了,可是用什么常见的客户端来激活这个机器人呢?我们在下图中点击左侧那个"信道"看看:很神奇的样子,好像可以连接这么多种客户端!我们用Skype先试验一下。点击Skype图标,进入一个页面,但是可以不管它,退回到"信道"页面,可以看到下图:点击那个"Skype"文字链接(不是点击图标),会另起一个网页:点击"Add to Contacts",如果机器上安装了Skype UWP版(如下图),就可以启动它了。如果没有安装,刚才那个网页会给你一个"Download Skype"的选项。Skype启动后,SchoolQnAWebBot会作为一个联系人出现在对话中,我们可以问它一些事情,如下图所示,"报销"、"补考"、"开学"等,机器人都可以回答。但是输入比如"谁是校长"之类的不在知识库里的词汇,机器人就只能装傻充愣了。
0
0
0
浏览量1819
金某某的算法生活

人工智能实战

人工智能实战系列,介绍以微软AI 平台为主的实用工具和各种典型案例,手把手教你人工智能的开发。
0
0
0
浏览量2212
金某某的算法生活

第九章:OpenCV自适应直方图均衡CLAHE C++源代码分享

一、引言最近收到几个网友提供OpenCV中CLAHE的源代码的请求,在此直接将OpenCV4.54版本CLAHE.CPP的源码分享出来。二、OpenCV源代码的下载下载地址:https://sourceforge.net/projects/opencvlibrary/files/有3.4.10–4.5.4的版本,但下载很慢,老猿费了很大的劲,大家可以考虑专门的下载工具下载。如果实在下不下来,请关注老猿Python的微信公号给老猿发消息。三、CLAHE C++源代码/*M/// // // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. // // By downloading, copying, installing or using the software you agree to this license. // If you do not agree to this license, do not download, install, // copy or use the software. // // // License Agreement // For Open Source Computer Vision Library // // Copyright (C) 2013, NVIDIA Corporation, all rights reserved. // Copyright (C) 2014, Itseez Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistribution's of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Redistribution's in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * The name of the copyright holders may not be used to endorse or promote products // derived from this software without specific prior written permission. // // This software is provided by the copyright holders and contributors "as is" and // any express or implied warranties, including, but not limited to, the implied // warranties of merchantability and fitness for a particular purpose are disclaimed. // In no event shall the copyright holders or contributors be liable for any direct, // indirect, incidental, special, exemplary, or consequential damages // (including, but not limited to, procurement of substitute goods or services; // loss of use, data, or profits; or business interruption) however caused // and on any theory of liability, whether in contract, strict liability, // or tort (including negligence or otherwise) arising in any way out of // the use of this software, even if advised of the possibility of such damage. // //M*/ #include "precomp.hpp" #include "opencl_kernels_imgproc.hpp" // ---------------------------------------------------------------------- // CLAHE #ifdef HAVE_OPENCL namespace clahe { static bool calcLut(cv::InputArray _src, cv::OutputArray _dst, const int tilesX, const int tilesY, const cv::Size tileSize, const int clipLimit, const float lutScale) { cv::ocl::Kernel k("calcLut", cv::ocl::imgproc::clahe_oclsrc); if(k.empty()) return false; cv::UMat src = _src.getUMat(); _dst.create(tilesX * tilesY, 256, CV_8UC1); cv::UMat dst = _dst.getUMat(); int tile_size[2]; tile_size[0] = tileSize.width; tile_size[1] = tileSize.height; size_t localThreads[3] = { 32, 8, 1 }; size_t globalThreads[3] = { tilesX * localThreads[0], tilesY * localThreads[1], 1 }; int idx = 0; idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(src)); idx = k.set(idx, cv::ocl::KernelArg::WriteOnlyNoSize(dst)); idx = k.set(idx, tile_size); idx = k.set(idx, tilesX); idx = k.set(idx, clipLimit); k.set(idx, lutScale); return k.run(2, globalThreads, localThreads, false); } static bool transform(cv::InputArray _src, cv::OutputArray _dst, cv::InputArray _lut, const int tilesX, const int tilesY, const cv::Size & tileSize) { cv::ocl::Kernel k("transform", cv::ocl::imgproc::clahe_oclsrc); if(k.empty()) return false; int tile_size[2]; tile_size[0] = tileSize.width; tile_size[1] = tileSize.height; cv::UMat src = _src.getUMat(); _dst.create(src.size(), src.type()); cv::UMat dst = _dst.getUMat(); cv::UMat lut = _lut.getUMat(); size_t localThreads[3] = { 32, 8, 1 }; size_t globalThreads[3] = { (size_t)src.cols, (size_t)src.rows, 1 }; int idx = 0; idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(src)); idx = k.set(idx, cv::ocl::KernelArg::WriteOnlyNoSize(dst)); idx = k.set(idx, cv::ocl::KernelArg::ReadOnlyNoSize(lut)); idx = k.set(idx, src.cols); idx = k.set(idx, src.rows); idx = k.set(idx, tile_size); idx = k.set(idx, tilesX); k.set(idx, tilesY); return k.run(2, globalThreads, localThreads, false); } } #endif namespace { template <class T, int histSize, int shift> class CLAHE_CalcLut_Body : public cv::ParallelLoopBody { public: CLAHE_CalcLut_Body(const cv::Mat& src, const cv::Mat& lut, const cv::Size& tileSize, const int& tilesX, const int& clipLimit, const float& lutScale) : src_(src), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), clipLimit_(clipLimit), lutScale_(lutScale) { } void operator ()(const cv::Range& range) const CV_OVERRIDE; private: cv::Mat src_; mutable cv::Mat lut_; cv::Size tileSize_; int tilesX_; int clipLimit_; float lutScale_; }; template <class T, int histSize, int shift> void CLAHE_CalcLut_Body<T,histSize,shift>::operator ()(const cv::Range& range) const { T* tileLut = lut_.ptr<T>(range.start); const size_t lut_step = lut_.step / sizeof(T); for (int k = range.start; k < range.end; ++k, tileLut += lut_step) { const int ty = k / tilesX_; const int tx = k % tilesX_; // retrieve tile submatrix cv::Rect tileROI; tileROI.x = tx * tileSize_.width; tileROI.y = ty * tileSize_.height; tileROI.width = tileSize_.width; tileROI.height = tileSize_.height; const cv::Mat tile = src_(tileROI); // calc histogram cv::AutoBuffer<int> _tileHist(histSize); int* tileHist = _tileHist.data(); std::fill(tileHist, tileHist + histSize, 0); int height = tileROI.height; const size_t sstep = src_.step / sizeof(T); for (const T* ptr = tile.ptr<T>(0); height--; ptr += sstep) { int x = 0; for (; x <= tileROI.width - 4; x += 4) { int t0 = ptr[x], t1 = ptr[x+1]; tileHist[t0 >> shift]++; tileHist[t1 >> shift]++; t0 = ptr[x+2]; t1 = ptr[x+3]; tileHist[t0 >> shift]++; tileHist[t1 >> shift]++; } for (; x < tileROI.width; ++x) tileHist[ptr[x] >> shift]++; } // clip histogram if (clipLimit_ > 0) { // how many pixels were clipped int clipped = 0; for (int i = 0; i < histSize; ++i) { if (tileHist[i] > clipLimit_) { clipped += tileHist[i] - clipLimit_; tileHist[i] = clipLimit_; } } // redistribute clipped pixels int redistBatch = clipped / histSize; int residual = clipped - redistBatch * histSize; for (int i = 0; i < histSize; ++i) tileHist[i] += redistBatch; if (residual != 0) { int residualStep = MAX(histSize / residual, 1); for (int i = 0; i < histSize && residual > 0; i += residualStep, residual--) tileHist[i]++; } } // calc Lut int sum = 0; for (int i = 0; i < histSize; ++i) { sum += tileHist[i]; tileLut[i] = cv::saturate_cast<T>(sum * lutScale_); } } } template <class T, int shift> class CLAHE_Interpolation_Body : public cv::ParallelLoopBody { public: CLAHE_Interpolation_Body(const cv::Mat& src, const cv::Mat& dst, const cv::Mat& lut, const cv::Size& tileSize, const int& tilesX, const int& tilesY) : src_(src), dst_(dst), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), tilesY_(tilesY) { buf.allocate(src.cols << 2); ind1_p = buf.data(); ind2_p = ind1_p + src.cols; xa_p = (float *)(ind2_p + src.cols); xa1_p = xa_p + src.cols; int lut_step = static_cast<int>(lut_.step / sizeof(T)); float inv_tw = 1.0f / tileSize_.width; for (int x = 0; x < src.cols; ++x) { float txf = x * inv_tw - 0.5f; int tx1 = cvFloor(txf); int tx2 = tx1 + 1; xa_p[x] = txf - tx1; xa1_p[x] = 1.0f - xa_p[x]; tx1 = std::max(tx1, 0); tx2 = std::min(tx2, tilesX_ - 1); ind1_p[x] = tx1 * lut_step; ind2_p[x] = tx2 * lut_step; } } void operator ()(const cv::Range& range) const CV_OVERRIDE; private: cv::Mat src_; mutable cv::Mat dst_; cv::Mat lut_; cv::Size tileSize_; int tilesX_; int tilesY_; cv::AutoBuffer<int> buf; int * ind1_p, * ind2_p; float * xa_p, * xa1_p; }; template <class T, int shift> void CLAHE_Interpolation_Body<T, shift>::operator ()(const cv::Range& range) const { float inv_th = 1.0f / tileSize_.height; for (int y = range.start; y < range.end; ++y) { const T* srcRow = src_.ptr<T>(y); T* dstRow = dst_.ptr<T>(y); float tyf = y * inv_th - 0.5f; int ty1 = cvFloor(tyf); int ty2 = ty1 + 1; float ya = tyf - ty1, ya1 = 1.0f - ya; ty1 = std::max(ty1, 0); ty2 = std::min(ty2, tilesY_ - 1); const T* lutPlane1 = lut_.ptr<T>(ty1 * tilesX_); const T* lutPlane2 = lut_.ptr<T>(ty2 * tilesX_); for (int x = 0; x < src_.cols; ++x) { int srcVal = srcRow[x] >> shift; int ind1 = ind1_p[x] + srcVal; int ind2 = ind2_p[x] + srcVal; float res = (lutPlane1[ind1] * xa1_p[x] + lutPlane1[ind2] * xa_p[x]) * ya1 + (lutPlane2[ind1] * xa1_p[x] + lutPlane2[ind2] * xa_p[x]) * ya; dstRow[x] = cv::saturate_cast<T>(res) << shift; } } } class CLAHE_Impl CV_FINAL : public cv::CLAHE { public: CLAHE_Impl(double clipLimit = 40.0, int tilesX = 8, int tilesY = 8); void apply(cv::InputArray src, cv::OutputArray dst) CV_OVERRIDE; void setClipLimit(double clipLimit) CV_OVERRIDE; double getClipLimit() const CV_OVERRIDE; void setTilesGridSize(cv::Size tileGridSize) CV_OVERRIDE; cv::Size getTilesGridSize() const CV_OVERRIDE; void collectGarbage() CV_OVERRIDE; private: double clipLimit_; int tilesX_; int tilesY_; cv::Mat srcExt_; cv::Mat lut_; #ifdef HAVE_OPENCL cv::UMat usrcExt_; cv::UMat ulut_; #endif }; CLAHE_Impl::CLAHE_Impl(double clipLimit, int tilesX, int tilesY) : clipLimit_(clipLimit), tilesX_(tilesX), tilesY_(tilesY) { } void CLAHE_Impl::apply(cv::InputArray _src, cv::OutputArray _dst) { CV_INSTRUMENT_REGION(); CV_Assert( _src.type() == CV_8UC1 || _src.type() == CV_16UC1 ); #ifdef HAVE_OPENCL bool useOpenCL = cv::ocl::isOpenCLActivated() && _src.isUMat() && _src.dims()<=2 && _src.type() == CV_8UC1; #endif int histSize = _src.type() == CV_8UC1 ? 256 : 65536; cv::Size tileSize; cv::_InputArray _srcForLut; if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0) { tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_); _srcForLut = _src; } else { #ifdef HAVE_OPENCL if(useOpenCL) { cv::copyMakeBorder(_src, usrcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0, tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101); tileSize = cv::Size(usrcExt_.size().width / tilesX_, usrcExt_.size().height / tilesY_); _srcForLut = usrcExt_; } else #endif { cv::copyMakeBorder(_src, srcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0, tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101); tileSize = cv::Size(srcExt_.size().width / tilesX_, srcExt_.size().height / tilesY_); _srcForLut = srcExt_; } } const int tileSizeTotal = tileSize.area(); const float lutScale = static_cast<float>(histSize - 1) / tileSizeTotal; int clipLimit = 0; if (clipLimit_ > 0.0) { clipLimit = static_cast<int>(clipLimit_ * tileSizeTotal / histSize); clipLimit = std::max(clipLimit, 1); } #ifdef HAVE_OPENCL if (useOpenCL && clahe::calcLut(_srcForLut, ulut_, tilesX_, tilesY_, tileSize, clipLimit, lutScale) ) if( clahe::transform(_src, _dst, ulut_, tilesX_, tilesY_, tileSize) ) { CV_IMPL_ADD(CV_IMPL_OCL); return; } #endif cv::Mat src = _src.getMat(); _dst.create( src.size(), src.type() ); cv::Mat dst = _dst.getMat(); cv::Mat srcForLut = _srcForLut.getMat(); lut_.create(tilesX_ * tilesY_, histSize, _src.type()); cv::Ptr<cv::ParallelLoopBody> calcLutBody; if (_src.type() == CV_8UC1) calcLutBody = cv::makePtr<CLAHE_CalcLut_Body<uchar, 256, 0> >(srcForLut, lut_, tileSize, tilesX_, clipLimit, lutScale); else if (_src.type() == CV_16UC1) calcLutBody = cv::makePtr<CLAHE_CalcLut_Body<ushort, 65536, 0> >(srcForLut, lut_, tileSize, tilesX_, clipLimit, lutScale); else CV_Error( CV_StsBadArg, "Unsupported type" ); cv::parallel_for_(cv::Range(0, tilesX_ * tilesY_), *calcLutBody); cv::Ptr<cv::ParallelLoopBody> interpolationBody; if (_src.type() == CV_8UC1) interpolationBody = cv::makePtr<CLAHE_Interpolation_Body<uchar, 0> >(src, dst, lut_, tileSize, tilesX_, tilesY_); else if (_src.type() == CV_16UC1) interpolationBody = cv::makePtr<CLAHE_Interpolation_Body<ushort, 0> >(src, dst, lut_, tileSize, tilesX_, tilesY_); cv::parallel_for_(cv::Range(0, src.rows), *interpolationBody); } void CLAHE_Impl::setClipLimit(double clipLimit) { clipLimit_ = clipLimit; } double CLAHE_Impl::getClipLimit() const { return clipLimit_; } void CLAHE_Impl::setTilesGridSize(cv::Size tileGridSize) { tilesX_ = tileGridSize.width; tilesY_ = tileGridSize.height; } cv::Size CLAHE_Impl::getTilesGridSize() const { return cv::Size(tilesX_, tilesY_); } void CLAHE_Impl::collectGarbage() { srcExt_.release(); lut_.release(); #ifdef HAVE_OPENCL usrcExt_.release(); ulut_.release(); #endif } } cv::Ptr<cv::CLAHE> cv::createCLAHE(double clipLimit, cv::Size tileGridSize) { return makePtr<CLAHE_Impl>(clipLimit, tileGridSize.width, tileGridSize.height); }
0
0
0
浏览量2029
金某某的算法生活

第十一章:OpenCV-Python+Moviepy 结合进行视频特效处理

一、引言Moviepy 是一个 Python 的音视频剪辑库,OpenCV 是一个图形处理库,我们知道视频的一帧就是一幅图像,因此在处理视频时可以结合 OpenCV 进行帧处理,将二者结合可以用来进行一些不错的视频特效的处理。老猿在此介绍两个这方面的应用案例供大家参考。二、给视频添加雪花飘落特效2.1、实现原理雪花特效可以给视频增加特殊的效果,要给视频加雪花特效,是基于以下原理来实现的:每个视频都是由一个个视频帧构成,每个视频帧都是一副静态的图像,通过视频帧的连续显示形成动态视频;实现视频雪花飘落,就是在视频的每帧图像中添加雪花,并在前后相连的视频帧中变化雪花的位置,形成雪花下飘带横向移动的效果;雪花的图片本身是一个矩形,矩形内有黑色背景和白色的雪花,在将雪花图片添加到视频帧时,需要确保黑色部分不会遮盖帧图像的内容,只有白色的雪花前景色才可以遮挡帧图像内容。这就需要通过图像的阈值处理确得到雪花图像的二值图,用该二值图及其补图作为掩膜,二值图作为雪花原始图片与自身与运算的掩码来获取雪花图片的前景色,补图作为帧图片雪花对应位置的子图与子图自身与运算的掩膜来获取雪花黑色背景部分对应的帧图像作为背景色;对视频帧调用雪花融合图像的函数进行动态融合雪花的处理;Moviepy 的 fl_image 是视频剪辑基类 VideoClip 的方法,该方法用于对视频的帧图像进行变换,其参数包括一个对帧图像进行变换的函数image_func,具体变换由应用实现对图像进行变换处理的一个函数,然后将该函数作为 image_func 的值参传入fl_image,Moviepy 就会调用该函数完成对视频每帧图像的处理生成新的剪辑2.2、具体实现2.2.1、雪花图片本次实现的案例对应的雪花图片为:文件名为:f:\pic\snow.jpg。上述图片的雪花是标准的雪花图像,但图像比较大,在视频中直接展示这么大的雪花就很假,老猿经过测试,发现将其缩小到原图像的五分之一以内比较象真正的雪花。2.2.2、实现流程2.2.3、关键实现2.2.3.1、初始化雪花图片的雪花是固定大小的,而真正的雪花大小是不同的,为了模拟真正的雪花效果,需要有各种不同大小和角度的雪花,为此每片雪花需要根据图片雪花图像进行随机的大小和角度调整。一帧图像中的雪花数量至少是几十到几百片,如果每次融合图像时,都需要从图片雪花图像进行大小和旋转角度的变换,是非常消耗系统的性能的,影响视频的生成耗时。为了提升处理性能,老猿只在程序开始初始化时一次批量生产各种不同大小、不同旋转角度的各种雪花,后续程序生成雪花时,直接从批量生成的雪花中取一个作为要生成的雪花,而不用每次从基本的雪花图像开始进行变换。生成各种雪花形状的示例代码:# -*- coding: utf-8 -*- import cv2,random import numpy as np from opencvPublic import addImgToLargeImg,readImgFile,rotationImg snowShapesList = [] #雪花形状列表 snowObjects=[] #图片中要显示的所有雪花对象 def initSnowShapes(): """ 从文件中读入雪花图片,并进行不同尺度的缩小和不同角度的旋转从而生成不同的雪花形状,这些雪花形状保存到全局列表中snowShapesList """ global snowShapesList imgSnow = readImgFile(r'f:\pic\snow.jpg') imgSnow = cv2.resize(imgSnow, None, fx=0.3, fy=0.3) #图片文件中的雪花比较大,需要缩小才能象自然的雪花形象 minFactor,maxFactor = 50,100 #雪花大小在imgSnow的0.5-1倍之间变化 for factor in range(minFactor,maxFactor,5): #每次增加5%大小 f = factor*0.01 imgSnowSize = cv2.resize(imgSnow, None, fx=f, fy=f) for ange in range(0,360,5):#雪花0-360之间旋转,每次旋转角度增加5° imgRotate = rotationImg(imgSnowSize,ange) snowShapesList.append(imgRotate) 2.2.3.2、产生一排雪花每帧图像除了保留上帧图像中未飘落出图像范围的雪花外,同时还会从顶部生成一排数量随机的雪花,形成生生不息的雪花。下面是从顶部初始化生成一排雪花的代码:def generateOneRowSnows(width,count): """ 产生一排雪花对象,每个雪花随机从snowShapesList取一个、横坐标位置随机、纵坐标初始为0 :param width: 背景图像宽度 :param count: 希望的雪花数 :y:当前行对应的竖直坐标 :return:一个包含产生的多个雪花对象信息的列表,每个列表的元素代表一个雪花对象,雪花对象包含三个信息,在snowShapesList的索引号、初始x坐标、初始y坐标(才生成固定为0) """ global snowShapesList line = [] picCount = len(snowShapesList) for loop in range(count): imgId = random.randint(0,picCount-1) xPos = random.randint(0,width-1) line.append((imgId,xPos,0)) return line2.2.3.3、将所有雪花对象融合到背景图像上帧图像中的雪花在当前帧中需要随机下落一定位置,并在一定幅度内横向漂移,当有雪花落到图像底部之下时,需要释放对应对象以节省资源。def addSnowEffectToImg(img): """ 将所有snowObjects中的雪花对象融合放到图像img中,融合时y坐标随机下移一定高度,x坐标左右随机小范围内移动 """ global snowShapesList,snowObjects horizontalMaxDistance,verticalMaxDistance = 5,10 #水平方向左右漂移最大值和竖直方向下落最大值 rows,cols = img.shape[:2] maxObjsPerRow = int(cols/100) snowObjects += generateOneRowSnows(cols, random.randint(0, maxObjsPerRow)) snowObjectCount = len(snowObjects) rows,cols = img.shape[0:2] imgResult = np.array(img) for index in range(snowObjectCount-1,-1,-1): imgObj = snowObjects[index] #每个元素为(imgId,x,y) if imgObj[2]>rows: #如果雪花的起始纵坐标已经超出背景图像的高度(即到达背景图像底部),则该雪花对象需进行失效处理 del(snowObjects[index]) else: imgSnow = snowShapesList[imgObj[0]] x,y = imgObj[1:] #取该雪花上次的位置 x = x+random.randint(-1*horizontalMaxDistance,horizontalMaxDistance) #横坐标随机左右移动一定范围 y = y+random.randint(1,verticalMaxDistance) #纵坐标随机下落一定范围 snowObjects[index] = (imgObj[0],x,y) #更新雪花对象信息 imgResult = addImgToLargeImg(imgSnow,imgResult,(x,y),180) #将所有雪花对象图像按照其位置融合到背景图像中 return imgResult #返回融合图像2.2.3.4、实现视频雪花飘落特效视频合成下面的代码调用 addSnowEffectToImg 实现对《粉丝记事本》视频的雪花飘落特效:from moviepy.editor import * def addVideoSnowEffect(videoFileName,resultFileName): clip = VideoFileClip(videoFileName) newclip = clip.fl_image(addSnowEffectToImg, apply_to=['mask']) newclip.write_videofile(resultFileName) if __name__ == '__main__': addVideoSnowEffect(r'f:\video\fansNote.mp4',r'f:\video\fansNote_snow.mp4')2.2.4、雪花飘落效果三、制作灯光秀短视频3.1、视频内容设计基于多张静态景物叠加特效声光特效可以将静态图片变成有趣的灯光秀视频,老猿为了武汉这个英雄之城解封一周年用 Python 制作了个解封庆祝的灯光秀短视频。老猿是个没有艺术细胞的人,因此这个视频内容只能说仅能代表是个视频而已,对最终的内容表现大家就不需要过多评价。在创作该视频前,老猿对视频进行了简单规划,将创作视频分为片头、视频内容和片尾三部分:片头:5 秒时间,展现一幅黄鹤楼的照片,并带上“武汉重启一周年灯光秀”的标题视频内容:全长 35 秒,每隔 2 秒随机展现一张武汉灯光秀景观图,并在视频中附上向上滚动的文字“热烈庆祝武汉重启一周年!”、“武汉万岁!中国万岁!”,并在视频的左下角和右下角用红绿蓝三色画三条向上晃动的线条表示彩色激光片尾:25 秒,每隔 2 秒随机展现武汉的一张风景照,并展现“制片:老猿 Python”等制作信息。3.2、开发设计3.2.1、视频图片处理视频中用到的图片都来源于互联网,为了减少图片加载的时间和统一初始化,在程序中通过全局变量将片头使用图像、视频内容使用图像、片尾使用图像分别使用了三个全局变量进行保存,其中后两者为列表类型。为了确保视频输出,所有图片都调整到了统一大小。3.2.2、灯光效果处理在视频内容部分,左下角和右下角发射的彩色激光,采用在背景图片中根据时间动态绘制彩色线条,实现彩色激光晃动照射的效果,为了限制晃动范围,设定了激光终点的 x 值的最小值和最大值。激光终点的位置根据时间动态计算,并在到达 x 值的最小值或最大值时自动回扫。3.2.3、帧图像的生成上面介绍的图像处理,全部集中在一个函数中处理,该函数仅带一个参数时间 t,判断时间来决定现在生成的内容是片头、内容还是片尾,然后据此来进行帧图像的生成。生成时,需要判断图像是否切换,因此需要记录上一次切换的时间和切换后的图像,确保未达到切换时间前用上次图像作为帧图像的背景,达到切换时间要求后切换新的图像作为后续帧图像生成的背景。为此在该函数中使用了两个全局变量来记录当前帧图像背景图片和上次切换时间。3.2.4、输出到视频为了将视频输出到文件,通过 VideoWriter_fourcc 指定视频输出文件类型,采用 OpenCV 的 VideoWriter 类来进行视频输出,通过 Open 方法指定输出文件,通过 write 方法将一帧帧视频写入。3.3、具体实现3.3.1、总流程加载片头、视频内容、片尾需要使用的图像;创建新视频文件按照帧率 24、时长 65 秒,构建 24*65 张帧图像,并逐一输出到视频文件。3.3.2、全局变量初始化全局变量主要是视频中需要使用的图片,分成片头使用图片 hhlImg、灯光秀图片列表 lightShowImgList 和片尾风景图片列表 whImgList 。另外 preImg,preTime 用于记录上次切换视频图片的图片和切换时间。hhlImg = cv2.resize(readImgFile(r'f:\pic\武汉\黄鹤楼.jpg'),(800,600))lightShowImgList = [cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_桥.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_武汉江边.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥底部.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_一桥远景.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\灯光秀_远桥.jpg'),(800,600))]whImgList =  [cv2.resize(readImgFile(r'f:\pic\武汉\东湖1.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\东湖2.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\东湖樱园樱花.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\武大牌楼远观.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大樱花2.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园顶高拍照.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园门洞.jpg'),(800,600)),                    cv2.resize(readImgFile(r'f:\pic\武汉\武大樱园入口.jpg'),(800,600)),cv2.resize(readImgFile(r'f:\pic\武汉\武大老图书馆.jpg'),(800,600))]preImg,preTime = None,03.3.3、实现给背景图像添加彩色激光照射效果lightShowImg 函数实现给背景图像指定点发射彩色激光的特效,激光发射点固定,由参数 lightStartPos 指定,终点随参数 t 在一定范围内变化,终点 x 坐标受参数 minX, maxX 控制,同一个发射源的三激光之间的间距受参数 distance 控制。def lightShowImg(bg,minX, maxX,distance,lightStartPos,t): """ 实现在背景图像上添加当前散发彩色激光的处理 :param bg: 背景图像 :param minX:灯光终点的最大x坐标 :param maxX:灯光终点的最小x坐标 :param distance: 不同灯光之间的间距 :param lightStartPos: 灯光发射点 :param t: 时间t :return: 添加了发射灯光的图像 """ x = (minX+int(t*200))%(maxX*2) #按时间t计算灯光终点的x坐标,该坐标可以超出背景图像范围 img = np.array(bg) if x>maxX: #到达最大范围,需要回扫 x = 2*maxX-x color1,color2,color3 = (255,0,0),(0,255,0),(0,0 ,255 ) #蓝、绿、红三色 cv2.line(img, lightStartPos, (x, 0), color1, 4) cv2.line(img, lightStartPos, (x + distance, 0), color2, 4) cv2.line(img, lightStartPos, (x - distance, 0), color3, 4) return img3.3.4、帧图片生成makeframe 函数实现帧图片生成,带一个参数 t 表示当前帧对应的剪辑时间,在函数内根据剪辑时间来判断是生成片头内容、灯光秀内容还是片尾内容生成 t 时刻对应帧图像返回:对于片头,采用黄鹤楼照片作为背景,并在图片中央显示“武汉重启一周年灯光秀”;对于视频内容,则每 2 秒从灯光秀图片队列中随机取一张图片作为当前背景图像,并调用 lightShowImg 增加左下角和右下角的彩色激光效果,同时动态向上滚动显示“热烈庆祝武汉重启一周年”、“武汉万岁!中国万岁!”等标语;对于片尾,则每 2 秒从武汉风景图片队列中随机取一张图片作为当前背景图像,同时动态向上滚动显示制作信息。def makeframe(t): #生成t时刻的视频帧图像 global preImg,preTime if t<5:#5秒片头 img = imgAddText(hhlImg,'武汉重启一周年灯光秀',64,(255,0,0),vRefPos='C') elif t<40:#5-40秒灯光秀 minX, maxX = 200, 1200 # 灯光横向扫射范围 lightStartPos1 = (0, 600) # 灯光1的发射点坐标设置为左下角 lightStartPos2 = (800, 600) # 灯光2的发射点坐标设置为右下角 if (t-preTime)>2 or preImg is None: img = np.array(random.choice(lightShowImgList)) preImg,preTime = img,t else: img = preImg img = lightShowImg(img, minX, maxX, 80,lightStartPos1, t) img = lightShowImg(img, minX-100, maxX+100,48, lightStartPos2, t) t = int((t-4)*40)%img.shape[0] img = imgAddText(img,'热烈庆祝武汉重启一周年!',48,(0,0,255),vRefPos=-80-t,) img = imgAddText(img, '武汉万岁!中国万岁!',48,(255,0,255),vRefPos=-t) else:#片尾 if (t-preTime)>2 or preImg is None: img = np.array(random.choice(whImgList)) preImg,preTime = img,t else: img = preImg t = int((t - 39) * 20) % img.shape[0] img = imgAddText(img,"制片:老猿Python",36,(255,255,255),vRefPos=-120-t) img = imgAddText(img, "https://www.infoq.cn/u/laoyuanpython/publish",24,(255,255,255),vRefPos= -60-t) img = imgAddText(img, "2021年4月8日", 24, (255,255,255), vRefPos=-t) return img3.3.5、制作视频文件函数 buildVideoByCV 用于制作视频文件,按指定格式生成一个新的视频文件,然后循环调用 makeframe 生成一帧帧图像写入视频文件中。def buildVideoByCV(): videoMake = cv2.VideoWriter() fourcc = cv2.VideoWriter_fourcc(*'MP4V') #https://blog.csdn.net/whudee/article/details/108689420 fps = 12 videoMake.open(r"F:\video\lightShowCV.MP4", fourcc, fps, (800,600)) for t in range(65*fps): img = makeframe(t*1.0/fps) videoMake.write(img) print(f'\r视频制作进度:{(t*100.0)/(66*fps):4.2f}%',end='') videoMake.release()上述函数构建后只需要调用 buildVideoByCV 函数即可完成视频制作。3.3.6、视频效果四、小结本文介绍了制作视频雪花飘落特效和灯光秀的原理、实现的思想以及流程,并利用 Python+OpenCV+Moviepy 提供了关键的实现代码,可以供大家理解图像融合、图像制作视频、Moviepy 视频变换的完整案例。
0
0
0
浏览量2036
金某某的算法生活

第十三章:OpenCV-Python自适应直方图均衡类CLAHE及方法详解

一、引言对比度受限的自适应直方图均衡在OpenCV中是通过类CLAHE来提供实现的,老猿没研究过C++中的应用,但OpenCV-Python中应用时与普通的Python类构建对象的机制有所不同,老猿做了相关测试,在此简单介绍一下。二、CLAHE类及方法介绍2.1、简介CLAHE类是OpenCV中进行对比度受限的自适应直方图均衡的基类,其类继承关系如下:其父类和子类老猿没有研究过,在此就不展开介绍。该类有2个重要属性:tilesGridSize:图像被分成称为“tiles”(瓷砖、地砖、小方地毯、片状材料、块状材料)的小块,在OpenCV中,tilesGridSize默认为8x8 ,即整个图像被划分为8纵8横共64块。然后对每一个块进行直方图均衡处理clipLimit:裁剪限制,此值与对比度受限相对应,对比度限制这个参数是用每块的直方图的每个bins的数和整图的平均灰度分布数的比值来限制的。 裁剪则是将每块图像直方图中超过ClipLimit的bins多出的灰度像素数去除超出部分,然后将所有bins超出的像素数累加后平均分配到所有bins。具体算法老猿将在研究清楚后单独介绍。2.2、成员方法简介2.2.1、apply方法apply方法用于对图像应用对比度受限自适应直方图均衡变换处理。调用语法: dst = cv.CLAHE.apply( src[, dst] )参数说明:src:输入图像,图像类型为CV_8UC1 or CV_16UC1,即8位或16位灰度图dst:输出图像,类型同输入图像补充说明:在进行该方法调用前,必须已经设置了对比度受限自适应直方图均衡算法的受限对比度ClipLimit以及图像分块的行数和列数tiles2.2.2、collectGarbage方法collectGarbage方法应该是进行内存垃圾回收的,没有参数和返回值,老猿认为由于Python的内存管理机制,该方法没有什么意义,同时测试没有发现执行该方法起何作用,因此很可能是个无用的方法。2.2.3、getClipLimit方法getClipLimit方法是用于获取当前CLAHE对象设置的ClipLimit值返回。调用语法: retval = cv.CLAHE.getClipLimit()2.2.4、getTilesGridSize方法getClipLimit方法是用于获取当前CLAHE对象设置的tilesGridSize返回。调用语法: retval = cv.CLAHE.getTilesGridSize()2.2.5、setClipLimit方法方法是用于设置当前CLAHE对象的ClipLimit值,无返回值。调用语法: None = cv.CLAHE.setClipLimit( clipLimit )2.2.6、setTilesGridSize方法方法是用于设置当前CLAHE对象的tilesGridSize值,无返回值。调用语法: None = cv.CLAHE.setTilesGridSize( tileGridSize )三、CLAHE对象的构建3.1、CLAHE构造方法研究从上面的方法介绍中,没有看到CLAHE的构造方法,在OpenCV文档中,确实没有这个CLAHE的构造方法,在Python的CLAHE.py模块中,有该类的构造方法的定义:def __init__(self, *args, **kwargs): # real signature unknown pass没有任何参数的说明,在老猿找到的该类的C++类的构造方法如下:int CLAHE(kz_pixel_t* pImage, unsigned int uiXRes, nsigned int uiYRes, kz_pixel_t Min, kz_pixel_t Max, unsigned int uiNrX, unsigned int uiNrY, unsigned int uiNrBins, float fCliplimit); 老猿以C++构造方法为依据构建了Python中的CLAHE类,构造方法确实返回了CLAHE对象,但以此调用相关方法全部出现代码异常退出。基本上说明OpenCV-Python中CLAHE的构造方法不可用,至于C++中是否能使用,老猿没有去研究。3.2、createCLAHE函数由于OpenCV-Python中CLAHE的构造方法无法使用,同时在OpenCV中,提供了单独的全局函数createCLAHE,因此OpenCV-Python中CLAHE对象的构建必须通过createCLAHE函数。调用语法:retval = cv.createCLAHE( clipLimit=40,tileGridSize=(8,8))参数及返回值说明:clipLimit、tileGridSize请参考前面关于类的介绍,返回值为创建的类对象,该对象的clipLimit、tileGridSize由createCLAHE函数的参数指定。四、示例代码下面的代码使用OpenCV-Python对读入的图像进行对比度受限自适应直方图均衡处理: import cv2 def testLocalHistEqu(): img = readImgFile(r'f:\pic\valley.png', True) print('666') clahe = cv2.createCLAHE(clipLimit=200, tileGridSize=(5, 5)) cl2 = clahe.getClipLimit() clahe.setClipLimit(20) cl1 = clahe.getClipLimit() clahe.setTilesGridSize((8,8)) imgEquA = clahe.apply(img) 上面的代码中创建CLAHE对象后,对对象的属性进行了读写操作,其实这些代码完全没有必要。五、小结本文介绍了OpenCV-Python对比度受限自适应直方图均衡变换的CLAHE类及其方法,并通过代码介绍了相关方法的使用。在单图像的对比度受限自适应直方图均衡时,只需要使用createCLAHE创建CLAHE对象,然后调用该对象apply方法就可以完成对比度受限自适应直方图均衡处理,该类的其他方法用处不大,但如果是要进行多次对比度受限自适应直方图均衡处理,且需要设置不同的分块数和受限阈值,则可以通过提供的方法直接修改对象属性再进行均衡处理即可。
0
0
0
浏览量2021
金某某的算法生活

第六章:OpenCV自适应直方图均衡CLAHE的clipLimit的含义及理解

一、引言关于自适应直方图均衡CLAHE的clipLimit的介绍,网上介绍的资料不多,可能对很多大佬来说,这个知识点很简单,但对于没这方面基础知识的,则不好理解,老猿今天结合OpenCV CLAHE源代码中对于clipLimit的赋值处理来解读一下。二、CLAHE涉及clipLimit的关键源代码CLAHE涉及clipLimit的关键源代码摘要如下:CLAHE_Impl::CLAHE_Impl(double clipLimit, int tilesX, int tilesY) : clipLimit_(clipLimit), tilesX_(tilesX), tilesY_(tilesY) { } void CLAHE_Impl::apply(cv::InputArray _src, cv::OutputArray _dst) { ... int histSize = _src.type() == CV_8UC1 ? 256 : 65536; ... if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0) { tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_); _srcForLut = _src; } ... const int tileSizeTotal = tileSize.area(); ... int clipLimit = 0; if (clipLimit_ > 0.0) { clipLimit = static_cast<int>(clipLimit_ * tileSizeTotal / histSize); clipLimit = std::max(clipLimit, 1); } ... } void CLAHE_Impl::setClipLimit(double clipLimit) { clipLimit_ = clipLimit; } double CLAHE_Impl::getClipLimit() const { return clipLimit_; } ...三、代码解读以上代码就是OpenCV自适应直方图均衡CLAHE对应源代码中关于clipLimit赋值处理的相关代码,暂不涉及使用。可以看到,类设置方法中对clipLimit设置后,其值会保存在类私有变量clipLimit_ 中,最终进行apply自适应直方图均衡处理时,采用局部变量clipLimit = clipLimit_ * tileSizeTotal / histSize,并取clipLimit 和1中间的最大值。可以看到,我们创建CLAHE对象或调用setClipLimit传入的clipLimit参数,最终被转换为了该值乘以tileSizeTotal (分块像素数)除以histSize(每个分块的直方图组数),这个转换是干什么呢?是得到每个分组的平均像素数量,如果灰度比较平均的话,每种级别(对应直方图分组数)的灰度所对应的像素数应该相等,当用该平均值乘以clipLimit,得到的是超过平均值clipLimit倍的像素数,这个值就是裁剪的限制值,对于超过这个值的分组就得裁剪,具体怎么裁剪我们在下篇博文再介绍。四、小结OpenCV自适应直方图均衡CLAHE中的参数clipLimit,是CLAHE的裁剪限制值,其值表示是各分块图像的直方图分组平均像素的倍数,当一个分组中的像素数超过这个倍数值乘以直方图分组平均像素的值(即限制值),则表示该分组对应灰度的像素数需要裁剪。
0
0
0
浏览量2024
金某某的算法生活

第七章:OpenCV源代码赏析: Mat对象step属性含义及使用深入分析

一、引子有个CSDN粉丝博友“CP猫”前2天和我联系,说他也在研究CLAHE算法,遇到了OpenCV Mat对象的step属性访问的问题,问为啥一个象step这样的数组可以强制转为为一个整数输出,且输出值为数组的第一个元素,为此他昨天还专门写了篇博文《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》。正好这近两个月来我一直断断续续的在研究CLAHE算法,在初始阶段恰好也有他这样的疑问,后来通过深入分析代码终于理解了,结合CP猫博主的疑问,老猿就此深入分析一下。二、关于step属性的含义在CP猫博友的文章《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》介绍了一下step属性,原文是这么介绍的:“step是个数组,用于存储每一维元素的大小(单位字节),如step[0]就是一维元素的个数,具体到图像来说,step[0]就是图像每行像素占用的字节数,step[1]就是每列像素占用的字节数”。以上说法不全对,其中有几个说法有问题:step不是一个数组,而是一个结构化的类型,提供了通过类似数组下标方式访问的方法,在访问上可以姑且认为是个数组。这个方法挺有用,随后详细介绍,大家可以好好品味一下;step中通过下标能访问的数据如果认为是一个数组,这个数组的每个元素并不是存储每一维元素的大小,step[1]更不是每列像素占用的字节数。怎么解释step的含义呢?《Mat中step的含义》的介绍,之所以不引用原文是因为 d_a_r_k博友转载博文给出的原文链接已经不可访问。step这里指出的是图像在各个梯级上的字节数大小,而这里的梯级指的是构成图像的各层次。以三维的Mat数据布局为例:三维图像由一个一个平面(第一级)构成,每一个平面由一行一行(第二级)像素构成,每行由一个一个像素点(第三级)构成。因此三维图像中step[0]是面占用空间的大小,step[1]是行占用空间的大小,step[2]是像素点占用空间的大小。同理:二维图像由一行一行(第一级)构成,而每一行又由一个一个点(第二级)构成。step[0]是行占用空间的大小,step[1]是像素点占用空间的大小。因此Mat中的step[0]就是每个图像构成要素的第一级在内存中占据的字节数量。例如,二维图像中step[0]就是每一行(第一级)在矩阵内存中,占据的字节的数量。也就是说step[i]就是第i+1级在矩阵内存中占据的字节的数量。所以在CP猫博友的文章《为什么OpenCV图像Mat矩阵的step属性能转换为整数输出?》的代码案例中,输出信息为:图像的分辨率为:1023×681step =28FC40,stepCast =3069,step[0]=3069,step[1]=3三、为什么step对象可以转换成整型呢?弄清楚了step的含义,我们来分析CP猫博友的问题。要弄清楚这个问题,必须阅读OpenCV相关的源码。3.1、MatStep相关源代码Mat对象的step属性实际上是一个MatStep对象,MatStep的定义在源码的build\include\opencv2\core\Mat.hpp文件下,具体定义如下:struct CV_EXPORTS MatStep { MatStep(); explicit MatStep(size_t s); const size_t& operator[](int i) const; size_t& operator[](int i); operator size_t() const; MatStep& operator = (size_t s); size_t* p; size_t buf[2]; protected: MatStep& operator = (const MatStep&); };其相关实现代码在build\include\opencv2\core\Mat.inl.hpp内,具体代码如下:/ MatStep inline MatStep::MatStep() { p = buf; p[0] = p[1] = 0; } inline MatStep::MatStep(size_t s) { p = buf; p[0] = s; p[1] = 0; } inline const size_t& MatStep::operator[](int i) const { return p[i]; } inline size_t& MatStep::operator[](int i) { return p[i]; } inline MatStep::operator size_t() const { CV_DbgAssert( p == buf ); return buf[0]; } inline MatStep& MatStep::operator = (size_t s) { CV_DbgAssert( p == buf ); buf[0] = s; return *this; }3.2、源代码分析针对CP猫博友的问题,我们来看几个关键的源代码:MatStep的两个operator[]方法,这个是决定了MatStep对象可以下标访问的实现方法,就是重载下标访问符号"[]";MatStep的数据实际上就是放在类型为size_t大小为2的buf缓冲区中,因此它实际上是只能支持二维图像的处理;类型转换没那么直观,实际上是通过重载operator size_t()重载size_t来访问的,size_t实际上vc定义的标准数据类型,其类型实际上就是无符号整型。具体定义如下:#ifdef _WIN64 typedef unsigned __int64 size_t; #else typedef unsigned int size_t; endif因此CP猫博友在将step使用 static_cast<int>(img.step)转换成int型时,实际上就调用了重载的size_t()操作符,因此返回了缓冲区的第一个元素。三、小结本文详细介绍了OpenCV Mat对象step属性含义,并基于OpenCV关于MatStep类型的源代码对step数据的访问机制进行了深入分析,从而解答了博友关于数组为什么强制类型转换会变为一个返回数组第一个元素的问题。
0
0
0
浏览量2026
金某某的算法生活

第十章:OpenCV自适应直方图均衡CLAHE图像和分块大小不能整除的处理

一、引言最近一个月来都在研究OpenCV 中CLAHE算法的一些问题,如:图像横向和纵向分块大小与图像的宽和高不能整除怎么处理?CLIP的剪裁是怎么实施的?解决棋盘效应的具体插值处理过程怎样?彩色图像怎么处理?到处找资料,也看了部分博客所谓的源代码,结果还是没有找到答案,看来没有捷径,干脆直接下载了一份OpenCV的源代码来阅读。可惜自从没有亲手做C语言相关开发后,手上的机器连C++运行环境都没有,先直接读代码。本来想等所有问题都有答案时再写博文,不过这一阵子单位和家里事情都很多,没有多少时间,只能慢慢来,解决一个问题就发布一篇博文。二、OpenCV源代码的下载下载地址:https://sourceforge.net/projects/opencvlibrary/files/有3.4.10–4.5.4的版本,但下载很慢,老猿费了很大的劲,大家可以考虑专门的下载工具下载。如果实在下不下来,请关注老猿Python的微信公号给老猿发消息。三、自适应直方图均衡CLAHE图像和分块大小不能整除的处理当图像的宽(或高)不是对应横向(或纵向)分块数的整数倍时,老猿认为对于分块的处理有多种方式:将每个分块横向或纵向多加1个像素,最后一个分块的大小比前面分块小;将每个分块横向或纵向减去1个像素,最后一个分块的大小比前面分块大;将图像裁剪或补齐到可以整除的大小。通过阅读源代码,OpenCV中采用将图像补齐到可以整除的大小,即对于图像的宽(或高)不是对应横向(或纵向)分块数的整数倍时,将对应宽(或高)补齐到可以整除的最少像素素。具体处理的源代码如下: if (_src.size().width % tilesX_ == 0 && _src.size().height % tilesY_ == 0) { tileSize = cv::Size(_src.size().width / tilesX_, _src.size().height / tilesY_); _srcForLut = _src; } else { { cv::copyMakeBorder(_src, srcExt_, 0, tilesY_ - (_src.size().height % tilesY_), 0, tilesX_ - (_src.size().width % tilesX_), cv::BORDER_REFLECT_101); tileSize = cv::Size(srcExt_.size().width / tilesX_, srcExt_.size().height / tilesY_); _srcForLut = srcExt_; } }小结OpenCV自适应直方图均衡CLAHE图像和分块大小不能整除时,采用的是将图像补齐到能整除大小,补齐是按边界镜像的方式(cv::BORDER_REFLECT_101)补齐。
0
0
0
浏览量2019
金某某的算法生活

第八章:OpenCV VideoWriter报错

一、问题代码几个月前有段代码能正常执行的,部分代码如下:def buildVideoByCV(): videoMake = cv2.VideoWriter() fourcc = cv2.VideoWriter_fourcc(*'MP4V') #https://blog.csdn.net/whudee/article/details/108689420 fps = 12 videoMake.open(r"g:\video\lightShowCV.MP4", fourcc, fps, (800,600)) for t in range(65*fps): img = makeframe(t*1.0/fps) videoMake.write(img) if t%20==0:print(f'\r视频制作进度:{(t*100.0)/(66*fps):4.2f}%',end='') videoMake.release()二、报错信息今天执行时报错:视频制作进度:95.96%OpenCV: FFMPEG: tag 0x5634504d/'MP4V' is not supported with codec id 12 and format 'mp4 / MP4 (MPEG-4 Part 14)'OpenCV: FFMPEG: fallback to use tag 0x7634706d/'mp4v'三、解决办法将fourcc = cv2.VideoWriter_fourcc(*'MP4V')改成:fourcc = cv2.VideoWriter_fourcc(*'mp4v')即可,只需要将编码的大写MP4V改成小写即可。四、小结本文介绍了OpenCV VideoWriter报错: FFMPEG: tag ‘MP4V‘ is not supported 错误的解决办法,只需要将编码‘MP4V’换成‘mp4v’即可。
0
0
0
浏览量2017
金某某的算法生活

第十六章:OpenCV-Python图形图像处理:自用的一些工具函数功能及调用语法介绍

一、getTextSize语法:getTextSize(text,fontSize=24)说明:获取text对应文本在字体大小为fontSize时的宽度和高度,单位为像素,返回的是一个宽度和高度二元组。二、readImgFile语法:readImgFile(filename,bConvertGray=False,bConvertBinImg=False,bConvertKernal = False)说明:将图像文件读入,根据参数决定是否转换为灰度图、二值图和核矩阵,支持中文文件名和目录名的处理。三、replaceImgBySpecImgRefPoint语法:replaceImgBySpecImgRefPoint(largeImg,largeImgRefPoint,smallImg,smallImgRefPoint,overlyBlackMaxValue=None)说明:将一个小图像的内容copy到大图像中,要求小图像的smallImgRefPoint对应点和大图像的largeImgRefPoint对应点重合。largeImgRefPoint/smallImgRefPoint都是(X,Y)形式,其值都是对应点在自身图像的位置。overlyBlackMaxValue:为None表示大图像对应区域用小图像简单替代,否则根据smallImg是否有对应灰度值小于overlyBlackMaxValue的像素, 如果有结果图像中这部分像素则保持大图部分的像素不变,其他部分则使用小图对应像素替代返回已经copy或融合小图像的大图像四、replaceImgRegionBySpecImg语法:replaceImgRegionBySpecImg(srcImg, regionTopLeftPos, specImg,overlyBlackMaxValue=None)说明:该函数调用replaceImgBySpecImgRefPoint完成处理,将srcImg的regionTopLeftPos开始位置的一个矩形图像替换为specImg,用specImg对应图像的左上角与srcImg的regionTopLeftPos对齐。overlyBlackMaxValue: 为None表示大图像对应区域用小图像简单替代,否则根据smallImg是否有对应灰度值小于overlyBlackMaxValue的像素, 如果有结果图像中这部分像素则保持大图部分的像素不变,其他部分则使用小图对应像素替代。返回已经copy或融合小图像的大图像。五、previewImg语法:previewImg(img)说明:调用操作系统默认图像阅读器预览img图像矩阵对应图像。六、preparePreviewImg语法:preparePreviewImg(imgTitle=None,img=None,firstImg=False,fontSize=36,color=(0,0,255))说明:将图像img加到预览图像列表,在图像下方增加imgTitle对应的文字说明,firstImg如果为True,将前面已经放到预览列表的图像清空。注意加入进去后并不立即显示图像,只有后续调用previewImgList函数时才会将在列表中所有图像一起在一个大合成图片中显示。该函数会返回img下方添加了imgTitle说明的图片矩阵,imgTitle的字体和颜色由fontSize和color指定。七、previewImgList()语法:previewImgList()说明:将通过preparePreviewImg放到列表中的图像合并排列后展示出来。八、imgAddText语法:imgAddText(img,text,fontSize=24,color=(0,0,255),vRefPos=‘B’,hRefPos = ‘C’,extendVPos=None,extendLines=0,extendColor=128)说明:给图像增加文字,根据参数确认文字添加位置,并确认是否需要在图像顶部或底部增加图像高度param img: 源图像param text: 需要增加的文字font:字体文件,或类似cv2.FONT_HERSHEY_SIMPLEX定义的字体,如font=‘STFANGSO.TTF’fontSize:字体大小color:字体颜色,BGR格式param vRefPos:文字添加的垂直对齐方式,'T’为顶部对齐,'B’为底部对齐,正数字n顶部下移n行,负数底部上移n行param hRefPos:水平对齐方式,'L’左对齐,'R’右对齐,'C’居中,数字n左边空n列param extendVPos:在什么位置扩展图像高度,None不扩展,'T’顶部扩展,否则为底部扩展,参数vRefPos的对齐方式是基于扩展后的图像param extendLines:扩展高度的列数extendColor:扩展颜色return:增加了文字的图像九、mergeImg语法:mergeImg(imgList)说明:将imgList的图像矩阵列表中的图像合并成一张大图像。十、print2DMatrix语法:print2DMatrix(matrix)说明:将2阶矩阵按照行和列方式打印输出每个元素十一、cmpMatrix语法:cmpMatrix(m1,m2)说明:比较两个矩阵是否一致,一致返回True,否则False。十二、addWeightedDistinguishBLK语法:addWeightedDistinguishBLK(img1, alpha, img2, beta, sigma, gamma=0.0)说明:相同大小的图像img1和img2权重相加,但图像img2中像素为黑色的部分取img1的像素权重为sigma, 参数img1, alpha, img2, beta, gamma与addWeighted的参数相同,sigma为img1中对应img2黑色部分范围的权重十三、addWeightedSmallImgToLargeImgDstgshBLK语法:addWeightedSmallImgToLargeImgDstgshBLK(largeImg,alpha,smallImg,beta,sigma,gamma=0.0,regionTopLeftPos=(0,0))说明:将小图像与大图像指定位置的内容融合,但对小图像透明部分单独处理,取大图像sigma的权重部分。十四、constructAffineMatrix语法:constructAffineMatrix(rotationAngle=0,xShearAngle=0,yShearAngle=0,translationX=0,translationY=0,scaleX=1,scaleY=1)说明:构建各种仿射变换的基础变换矩阵,详细参数及返回值如下: :param rotationAngle: 旋转角度,图像旋转时使用,逆时钟为正、顺时针为负,如顺时针旋转30°,则值为-30 :param xShearAngle: 水平错切角,水平错切时使用 :param yShearAngle: 垂直错切角,垂直错切时使用 :param translationX: x轴平移距离 :param translationY: y轴平移距离 :param scaleX: 水平方向缩放因子 :param scaleY: 竖直方向缩放因子 :return: 构建的3*3矩阵 补充说明: 本函数只能构建旋转、错切、平移、缩放四种情况的一种矩阵,参数只取一种情况进行矩阵构造, 取的情况按照旋转、错切、平移、缩放从高到低的优先级排列,高优先级的值非0则低优先级的值忽略。 如果返回的矩阵为3*3矩阵,如果该矩阵立即调用warpAffine进行仿射变换,需要通过切片方式取前2行传入warpAffine,如果需要与其他仿射矩阵相乘, 则必须保持3*3矩阵,相乘的结果再进行切片处理,因为两个2*3的矩阵之间没法相乘(矩阵乘法要求第一个矩阵的列数等于第二个矩阵的行数)十五、translation语法:translation(img,x,y,size)说明:对图像进行平移变换,img为输入图像,x、y分别表示x和y轴的平移距离,size为目标图像大小。十六、matrixMultiply语法:matrixMultiply(*mList)说明:对mList中对应的二维矩阵进行连乘,返回结果矩阵。十七、constructRectFrom4Points语法:constructRectFrom4Points(pointList)说明:从4个点对应的四边形构建一个矩形。依据一个四边形构建一个和坐标轴平行的矩形,构建原则是取四边形左上角的点为矩形的左上角点,四边形上边和下边x、y坐标的最大差距作为矩形横边的边长,以及侧边的边长。十八、overlyImgs语法:overlyImgs(bottomImg,topImg,blackMaxVal=0)说明:将两个图叠加,叠加时上层图非透明部分遮挡下层图对应部分,上层图透明部分则不遮挡下层图blackMaxVal:灰度值小于等于该值的都作为透明黑色处理,如果是彩色图像则以彩色转灰度后的值作为比较十九、addImgToLargeImg语法:addImgToLargeImg(smallImg,largeImg,largPos,blackMaxVal=0)说明:将小图像放到大图像中,小图像左上角与大图像的largPos位置重合,叠加时上层图非透明部分遮挡下层图对应部分,上层图透明部分(灰度值小于等于blackMaxVal的像素)则不遮挡下层图如果小图像放置到的位置导致小图像超出范围,则截掉超出范围的部分如果largPos的坐标出现负数,则将小图像在负数范围内的内容去除,从该坐标为0的位置开始显现未截除部分blackMaxVal:灰度值小于等于该值的都作为透明黑色处理,如果是彩色图像则以彩色转灰度后的值作为比较二十、rotationImg语法:rotationImg(img,angle)说明:将图像img逆时针旋转angle度,返回旋转后的图像。
0
0
0
浏览量2020
金某某的算法生活

第十四章:数字图像处理:OpenCV-Python中的直方图均衡知识介绍及函数equalizeHis

一、引言在《数字图像处理:直方图均衡(Histogram Equalization)的原理及处理介绍 》(链接:https://blog.csdn.net/LaoYuanPython/article/details/119857829)中介绍了数字图像处理中应用直方图均衡进行图像增强的原理、应用示例,本文将介绍对应处理方法在OpenCV中的实现以及基于OpenCV-Python的应用样例。二、直方图均衡的作用在前面的博文中已经介绍了直方图均衡的作用,OpenCV中也简单进行了说明,相当简洁,在此从与前文稍有不同的另外一种方式介绍一下。考虑如下代表同一个图像内容的两个不同灰度直方图:左边的图,其像素值仅局限于某个特定的值范围。例如,较亮的图像将把所有像素限制在高值上。但是一幅好的图像应该会有来自图像所有区域的像素。因此需要将这个直方图拉伸到两端(如上边右边图所示),这就是直方图均衡化的作用。这通常会提高图像的对比度来增强图像。三、OpenCV中基于Numpy实现的直方图均衡的样例代码3.1、代码样例下面的代码是OpenCV4.1中文官方文档提供的、基于直方图均衡原理使用Numpy实现的图像直方图均衡代码示例:import numpy as np import cv2 as cv from matplotlib import pyplot as plt img = cv.imread('wiki.jpg',0) hist,bins = np.histogram(img.flatten(),256,[0,256]) cdf = hist.cumsum() cdf_normalized = cdf * float(hist.max()) / cdf.max() #将值的区间上限限制在直方图像的y轴最大值范围内,确保累积曲线和直方图的值域范围对应 plt.plot(cdf_normalized, color = 'b') #绘制累积曲线 plt.hist(img.flatten(),256,[0,256], color = 'r') plt.xlim([0,256]) plt.legend(('cdf','histogram'), loc = 'upper left') plt.show() cdf_m = np.ma.masked_equal(cdf,0) cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min()) cdf = np.ma.filled(cdf_m,0).astype('uint8') img2 = cdf[img]3.2、样例代码解读以上代码先用numpy的histogram函数生成图像的直方图矩阵,该函数的两个返回值说明如下:bins:numpy一维数组,各直方图组的下界以及最后一组的上界,如像素值∈[0,255]区间,划分为8个组,则bins的返回值为有9个元素的一维数组[0,32,64,96,128,160,192,224,256],其中256是最后一组的上界hist:numpy一维数组,返回的直方图数据,即灰度值落在各bins区间的像素数目,第一个值对应第一组bins的像素数,如图像数组为:img = np.array([[1, 0, 3], [4, 0, 5], [2, 4, 3]], dtype=np.uint8),则hist数据为[9,0,0,0,0,0,0,0]得到直方图数组hist后,计算每个像素值的所有小于等于该像素值的像素个数累计值(即直方图的积分,其来由请参考《数字图像处理:直方图均衡(Histogram Equalization)的原理及处理介绍 》),这个用numpy的cumsum累积函数即可以实现。累计值(直方图积分)数组生成后为了将积分曲线和直方图数据共用坐标系且方便对比,将累计值的区间压缩到了hist纵坐标的空间内,同时利用matplotlib绘制出该图像的直方图和累积值对应的曲线。如图:最后使用掩码数组对每个像素值对应的累计像素个数进行均衡变换,得到均衡后的图像img2。下面是上图的均衡后结果图像及对应直方图:3.3、由样例延伸得到的结论上图的样例图像总体比较亮,均衡后改变的只是对比度。对于图像整体比较暗的情况,直方图均衡能得到类似的效果。这种效果的本质是使所有图像具有相同的照明条件。3.4、关于直方图均衡算法本案例的直方图计算对应的计算公式看起来与在《数字图像处理:直方图均衡(Histogram Equalization)的原理及处理介绍 》介绍的有所不同,在该文中介绍的均衡后的图像像素值与均衡前的像素值的映射关系为:其中rk表示原图像中灰度值为k的灰度值,sk表示经过变换后rk映射到均衡后图像的灰度值。从上面的代码可以看出,该算法与此至少看起来有些不同,也确实不同。具体的分析对比老猿在付费专栏的文章中《数字图像处理:OpenCV直方图均衡算法研究及模拟实现》进行了详细分析。四、OpenCV直方图均衡函数equalizeHist详解4.1、概述上面第三部分介绍的直方图均衡处理的算法及案例,是OpenCV官方提供的基于Numpy实现的直方图均衡处理的样例。该样例是为了说明OpenCV的直方图均衡算法,实际上OpenCV提供了直方图均衡的一体化函数equalizeHist,经老猿测试上面样例的算法与OpenCV的直方图均衡函数equalizeHist的实现细节方面还是有些差异。这些内容老猿在《数字图像处理:OpenCV直方图均衡算法研究及模拟实现》中进行介绍,本部分主要详细介绍OpenCV-Python提供的直方图均衡函数equalizeHist。4.2、equalizeHist语法说明语法equalizeHist函数的语法非常简单,调用语法如下:dst = cv.equalizeHist( src[, dst] )参数及返回值说明参数src为要均衡的输入图像,必须是8bit单通道图像,即灰度图,dst是原图像大小相等经过直方图均衡处理后的输出图像,参数dst可以不传入。函数处理过程该函数对应算法使图像的亮度正常化并增加图像的对比度,具体处理步骤如下:√ 计算输入图像的直方图H;√ 对直方图H进行归一化,使直方图bins的总和为255;√ 计算直方图的积分(integral of the histogram),计算公式为:√ 使用H′作为查找表( look-up table)对图像进行变换,具体像素值的变换公式为:dst(x,y)=H'(src(x,y))4.3、案例下面读入2幅经典的直方图均衡使用的案例图像进行均衡处理。相关代码如下:import cv2 from opencvPublic import preparePreviewImg,previewImgList,readImgFile,cmpMatrix,print2DMatrix def test(): img1 = readImgFile(r'f:\pic\valley.png',True) img2 = readImgFile(r'f:\pic\火星卫星.JPG',True) imgEqu1 = cv2.equalizeHist(img1) imgEqu2 = cv2.equalizeHist(img2) preparePreviewImg('输入图像:山谷',img1,fontSize=40,color=(255,255,255)) preparePreviewImg('山谷图均衡效果',imgEqu1,fontSize=40,color=(255,255,255)) preparePreviewImg('https://blog.csdn.net/LaoYuanPython', None, fontSize=36, color=(255,255,255)) preparePreviewImg('输入图像:火星卫星',img2,fontSize=40,color=(255,255,255)) preparePreviewImg('火卫图均衡效果', imgEqu2, fontSize=40, color=(255,255,255)) previewImgList() test()以上代码中,opencvPublic是老猿常用的图像处理自定义方法的公用模块,本文使用的自定义公用模块函数preparePreviewImg,previewImgList,readImgFile,cmpMatrix,print2DMatrix,其功能请参考《https://blog.csdn.net/LaoYuanPython/article/details/111351901 OpenCV-Python图形图像处理:自用的一些工具函数功能及调用语法介绍》中的介绍。下面是最后预览图像:从上面两张样例图及其均衡后结果图像对比可以看到,无论输入图像光线是否充足,对于对比度不是很明显的图像,直方图均衡都能有效增强图像的对比度,并使得图像都能模拟出相同的照明条件。五、小结本文介绍了OpenCV官方提供的直方图均衡原理、算法及算法实现样例,以及OpenCV-Python中的直方图均衡函数equalizeHist的调用语法、参数及返回值说明、处理过程描述,最后提供了一个使用equalizeHist函数对经典的两张直方图均衡样例图的处理代码和处理效果。通过相关内容的介绍,有助于大家理解直方图均衡的原理、算法及OpenCV中的处理方法。————————————————
0
0
0
浏览量2027
金某某的算法生活

第四章:Moviepy+OpenCV-python 结合进行音视频剪辑处理的一种建议模式

一、引言如今短视频和自媒体大行其道,不会点视频剪辑技能都不好说自己会玩自媒体,音视频剪辑工具大受欢迎,作为万能的编程语言 Python,也早就有了自己的音视频剪辑库 Moviepy。MoviePy 能处理的视频是 ffmpeg 格式的,支持的文件类型包括:*.mp4 *.wmv *.rm *.avi *.flv *.webm *.wav *.rmvb 等 ,可用于进行视频的剪切、拼接、标题插入、视频合成、视频处理或创建高级效果,同时更适合批量进行视频剪辑处理。OpenCV 是一个基于 Apache2.0 许可(开源)发行的跨平台计算机视觉和机器学习软件开源库,可以运行在 Linux、Windows、Android 和 Mac OS 操作系统上。 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。OpenCV-Python 是 OpenCV 适配 Python 的一个图像处理和计算机视觉处理库。二、一些 Moviepy 无法支持可由 OpenCV 完成的视频处理场景我们知道视频是一帧帧图像和音频构成的,在进行视频处理时,当 Moviepy 无法完成的一些处理,就可以借用 OpenCV 来完成,二者的结合可以制作一些复杂的高级特效。下面老猿列举一些 Moviepy 无法支持可借由 OpenCV 完成的特效:对图像进行灰度变换,例如直方图均衡,以调整视频的对比度以及均衡图像的背景色在视频内容的任意位置增加特定文字或几何图形,如形成弹幕效果对视频内容进行特定的透视变换对彩色视频三色进行分离修复视频背景的噪点进行复杂的背景处理,如增加雪花飘落效果将灰度视频转成彩色视频…只要是图像处理中能用的技术在视频中都可以使用,而不是简单的视频合成。\三、Moviepy 结合 OpenCV-Python 的音视频剪辑开发模式要实现 Moviepy 结合 OpenCV-Python 的音视频剪辑处理,可以按照构建单独图像处理函数、调用 fl_image 进行剪辑的帧图像处理、输出剪辑内容三个步骤来实现。3.1、图像处理函数图像处理函数是用于真正对剪辑的每帧图像进行剪辑加工的函数,当每帧图像的处理模式统一时,可以使用单一的函数来进行图像处理。图像处理函数的名字只要符合 Python 的函数命名要求就行,但该函数只能带一个参数和输出一个结果,输入参数就是要处理图像对应的 numpy 矩阵,输出结果就是加工处理后的结果图像,也是 numpy 矩阵。下面是一给图像加入雪花特效的函数示例:def addSnowEffectToImg(img): """ 将所有snowObjects中的雪花对象融合放到图像img中,融合时y坐标随机下移一定高度,x坐标左右随机小范围内移动 """ global snowShapesList,snowObjects horizontalMaxDistance,verticalMaxDistance = 5,10 #水平方向左右漂移最大值和竖直方向下落最大值 rows,cols = img.shape[:2] maxObjsPerRow = int(cols/100) snowObjects += generateOneRowSnows(cols, random.randint(0, maxObjsPerRow)) snowObjectCount = len(snowObjects) rows,cols = img.shape[0:2] imgResult = np.array(img) for index in range(snowObjectCount-1,-1,-1): imgObj = snowObjects[index] #每个元素为(imgId,x,y) if imgObj[2]>rows: #如果雪花的起始纵坐标已经超出背景图像的高度(即到达背景图像底部),则该雪花对象需进行失效处理 del(snowObjects[index]) else: imgSnow = snowShapesList[imgObj[0]] x,y = imgObj[1:] #取该雪花上次的位置 x = x+random.randint(-1*horizontalMaxDistance,horizontalMaxDistance) #横坐标随机左右移动一定范围 y = y+random.randint(1,verticalMaxDistance) #纵坐标随机下落一定范围 snowObjects[index] = (imgObj[0],x,y) #更新雪花对象信息 imgResult = addImgToLargeImg(imgSnow,imgResult,(x,y),180) #将所有雪花对象图像按照其位置融合到背景图像中 return imgResult 3.2、调用图像处理函数加工剪辑的每帧图像并输出目标剪辑moviepy 音视频剪辑模块的视频剪辑基类 VideoClip 的 fl_image 方法用于进行对剪辑帧数据进行变换。调用语法:fl_image(self, image_func, apply_to=None)。参数说明:image_func:参数 image_func 是对剪辑帧进行图像变换的函数,带一个参数,参数就是要处理的帧图像 numpy 矩阵,image_func 函数的返回值为经过变换后的帧apply_to:apply_to 表示变换是否需要同时作用于剪辑的音频和遮罩,其值可以为’mask’、‘audio’、[‘mask’,‘audio’]在实现剪辑加工处理时,只需要将上面的图像函数作为参数传递给 fl_image,就可以对整个剪辑进行帧图像的变换处理。如下面的示例代码就是调用上面给图像加雪花特效的函数 addSnowEffectToImg:def addVideoSnowEffect(videoFileName,resultFileName): clip = VideoFileClip(videoFileName) newclip = clip.fl_image(addSnowEffectToImg, apply_to=['mask']) newclip.write_videofile(resultFileName)上述代码中,videoFileName 是要处理的视频剪辑文件名,clip 是将该视频文件加载到内存准备剪辑,newclip 就是经过变换后的目标剪辑,resultFileName 是输出的结果视频文件。3.3、对同一个剪辑应用多种不同图像处理前面 2 个步骤介绍的是对一个视频进行统一方式的处理,如果需要针对同一个视频的不同时间段进行不同的视频特效处理,如片头加上文字标题、中间加上弹幕特效、结尾加上鸣谢文字等,则需要区分剪辑的时间位置调用多个图像处理函数,这有多种方法来实现,但是为了模式统一,建议使用 Moviepy 的 subclip 函数将不同时段剪辑单独截取成不同的子剪辑,然后分别对每个子剪辑设置对应的图像处理函数,再针对每个剪辑调用 fl_image 来处理,各个子剪辑处理完成后再拼接成一个剪辑输出即可。四、小结本文简单介绍了 Moviepy 库和 OpenCV-python 库,并讨论了 Moviepy 结合 OpenCV-python 进行视频剪辑的适用的一些场景,同时给出了这种剪辑处理模式的推荐实现方案,对使用 Python 进行视频剪辑处理感兴趣的同仁可以按照该模式去进行尝试。
0
0
0
浏览量2022
金某某的算法生活

第二章:OpenCV-Python 图像平滑处理2

一、图像平滑处理简介图像平滑处理的基本概念非常直观,它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。平滑线处理滤波器也称均值滤波器,所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍了使用filter2D实现图像平滑处理,本文将介绍另外一个OpenCV-Python的函数blur实现平滑处理。二、blur介绍2.1、简介blur是OpenCV用于进行图像模糊处理的函数,该函数使用归一化的盒装滤波器进行均值滤波处理。盒状滤波器的所有元素都相等,其元素为浮点数。blur的核矩阵进行了归一化处理,每个元素值=1/(滤波器核高×核宽),因此核矩阵的所有元素和值为1。对系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》的介绍。2.2、语法说明语法dst = cv.blur( src, ksize[, dst[, anchor[, borderType]]] ) 参数说明src:输入图像,可以是任何通道数的图像,处理时是各通道拆分后单独处理,但图像深度必须是CV_8U, CV_16U, CV_16S, CV_32F 或CV_64F;dst:结果图像,其大小和类型都与输入图像相同;ksize:卷积核(convolution kernel )矩阵大小,如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1),表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响;borderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。从以上介绍可知,blur函数就是在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍的filter2D的一种用于均值滤波的特定应用。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2 import numpy as np from opencvPublic import cmpMatrix def smoothingByFiler2D(): img = cv2.imread('f:\\pic\\opencvLogo.JPG') kernal = np.ones((5, 5), np.float32) / 25 dst = cv2.filter2D(img, None, kernal,delta=0) return dst def smoothingByBlur(): img = cv2.imread('f:\\pic\\opencvLogo.JPG') ksize = (5,5) dst = cv2.blur(img, ksize) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(dst), plt.title('Blurred') plt.xticks([]), plt.yticks([]) plt.show() return dst d1 = smoothingByBlur() d2 = smoothingByFiler2D() if(cmpMatrix(d1,d2)): #对比两个结果矩阵是否一致 print('d1==d2') else: print('d1!=d2')结果输出:文字输出:d1==d2可以看到输出图像比输入图像变模糊了,且blur处理的结果矩阵与filter2D处理的结果完全一样。四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数blur的Python语法及参数,并用之进行了对图像的均值滤波平滑处理,可以看到其模糊化处理结果与filter2D完全一样,实际上它是filter2D一种特定场景的应用。
0
0
0
浏览量2033
金某某的算法生活

第五章:OpenCV自适应直方图均衡CLAHE的裁剪处理过程

一、引言在《OpenCV自适应直方图均衡CLAHE的clipLimit的含义及理解》,老猿结合源代码介绍了clipLimit裁剪限制值的意义,但没有来得及介绍具体裁剪过程,下面就结合源代码介绍一下自适应直方图均衡的裁剪过程。二、CLAHE涉及裁剪处理的关键源代码裁剪处理在类 CLAHE_Impl的apply方法里调用CLAHE_CalcLut_Body类的函数对象来实现的,CLAHE_Impl是createCLAHE生成CLAHE实例时真正使用的类,而CLAHE_CalcLut_Body类是生成真正的直方图灰度映射和进行裁剪的类。涉及裁剪的代码在CLAHE_CalcLut_Body的operator函数中。相关的关键源代码如下: template <class T, int histSize, int shift> void CLAHE_CalcLut_Body<T,histSize,shift>::operator ()(const cv::Range& range) const { ... // clip histogram if (clipLimit_ > 0) { // how many pixels were clipped int clipped = 0; for (int i = 0; i < histSize; ++i) { if (tileHist[i] > clipLimit_) { clipped += tileHist[i] - clipLimit_; tileHist[i] = clipLimit_; } } // redistribute clipped pixels int redistBatch = clipped / histSize; int residual = clipped - redistBatch * histSize; for (int i = 0; i < histSize; ++i) tileHist[i] += redistBatch; if (residual != 0) { int residualStep = MAX(histSize / residual, 1); for (int i = 0; i < histSize && residual > 0; i += residualStep, residual--) tileHist[i]++; } } ... } } 三、代码解读以上代码就是OpenCV自适应直方图均衡CLAHE直方图裁剪的对应源代码。上述代码中,tileHist为当前处理块的直方图分组数据,clipLimit_是《OpenCV自适应直方图均衡CLAHE的clipLimit的含义及理解》介绍的裁剪限制值,某个分组中的像素数超过这个限制值则需要裁剪。具体裁剪处理时,对于直方图分组像素数超过clipLimit_的,则将该分组中超过clipLimit_的像素数累加到clipped局部变量中,然后将该直方图分组像素数强制设置为clipLimit_。上述过程对当前块的所有分组都处理完成后,将超出后累加的clipped变量值按分组数平均分配到各分组中,如果存在不够平均分配的部分,则等间距按顺序插入到分组中,直到所有超出部分都分配到了对应分组。如直方图分组是256个,累加的clipped值是1027个,则先每个分组值加4,然后将剩余4个分别累加到第0、85、 170三个位置的直方图分组中。四、小结OpenCV自适应直方图均衡CLAHE中的参数clipLimit,是CLAHE的裁剪限制值,当图像的各分块图像的直方图分组的像素数据超过这个限制值就需要裁剪。裁剪时,将个分组中像素数超出限制值的强制值为限制值,并将所有分组中超出部分累加后平均分配到各分组。
0
0
0
浏览量2022
金某某的算法生活

第三章:OpenCV-Python 图像平滑处理3

一、图像平滑处理简介图像平滑处理的基本概念非常直观,它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。平滑线处理滤波器也称均值滤波器,所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍了使用filter2D实现图像平滑处理、在《OpenCV-Python 图像平滑处理2:blur函数及滤波案例》介绍了使用blur实现图像模糊处理,本文将介绍另外一个OpenCV-Python的函数boxFilter实现平滑处理。二、boxFilter介绍2.1、简介boxFilter也是OpenCV用于进行图像模糊处理的函数,该函数使用盒装滤波器进行均值滤波平滑处理。盒状滤波器的所有元素都相等,其元素为浮点数。boxFilter的核矩阵的元素取值有两种,与归一化参数normalize 有关。boxFilter对应核矩阵为:这里的α取值规则如下:对系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》的介绍。2.2、语法说明语法1 dst = cv.boxFilter( src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]] )参数说明src:输入图像,可以是任何通道数的图像,处理时是各通道拆分后单独处理,但图像深度必须是CV_8U, CV_16U, CV_16S, CV_32F 或CV_64F;ddepth:输出图像深度(请参考《图像表示的相关概念:图像深度、像素深度、位深的区别和关系》),如果目标图像深度和输入图像深度相同,则传值-1,老猿测试在Python中此时取值None、0效果也一样,注意ddepth在这里必须传值,不能使用默认值。dst:结果图像,其大小和类型都与输入图像相同;ksize:卷积核(convolution kernel )矩阵大小,如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1),表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响normalize:核矩阵是否归一化处理的标记,为True进行归一化处理,否则不进行归一化处理,默认值为TrueborderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。从以上介绍可知,boxFilter函数与blur函数一样,就是在《OpenCV-Python 图像平滑处理1:卷积函数filter2D详解及用于均值滤波的案例》介绍的filter2D的一种用于均值滤波的特定应用,而blur函数又是boxFilter函数归一化处理的特例。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2 import numpy as np from opencvPublic import cmpMatrix def smoothingByBlur(): img = cv2.imread('f:\\pic\\opencvLogo.JPG') ksize = (5,5) dst = cv2.blur(img, ksize) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(dst), plt.title('Averaging') plt.xticks([]), plt.yticks([]) plt.show() return dst def smoothingByBoxFilter(): img = cv2.imread('f:\\pic\\opencvLogo.JPG') ksize = (5,5) dst = cv2.boxFilter(img, ddepth=-1, ksize=ksize,normalize =True) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(dst), plt.title('Averaging') plt.xticks([]), plt.yticks([]) plt.show() return dst d1 = smoothingByBlur() d2 = smoothingByBoxFilter() if(cmpMatrix(d1,d2)): #对比两个结果矩阵是否一致 print('d1==d2') else: print('d1!=d2')结果输出:文字输出:d1==d2可以看到输出图像比输入图像变模糊了,且boxFilter归一化的处理的结果矩阵与filter2D、blur处理的结果完全一样。当不进行归一化处理时,图像处理效果如下:四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数boxFilter的Python语法及参数,并用之进行了对图像的均值滤波平滑处理,可以看到其归一化的模糊化处理结果与filter2D、blur函数完全一样,实际上它是filter2D一种特定场景的应用,而blur又是boxFilter函数归一化处理的特例
0
0
0
浏览量2026
金某某的算法生活

第一章:OpenCV-Python 图像平滑处理1

一、图像平滑处理简介图像平滑处理属于图像空间滤波的一种,用于模糊处理和降低噪声。模糊处理经常用于图像预处理任务中,例如在(大)目标提取之前去除图像中的一些琐碎细节,以及桥接直线或曲线的缝隙。模糊处理后的图像,可以通过阈值处理、形态处理等方式进行再加工,从而去除一些噪点。平滑滤波器包括线性滤波器和非线性滤波器,平滑线性空间滤波器的输出(响应)是包含在滤波器模板邻域内的像素的简单平均值。平滑线性空间滤波器有时也称为均值滤波器,它们属于低通滤波器。平滑线性滤波器的基本概念非常直观。它使用滤波器模板确定的邻域内像素的平均/加权平均灰度值代替图像中每个像素的值。所有系数都相等(非加权平均)的空间均值滤波器也称为盒状滤波器。非线性滤波器可能有多种,统计排序滤波器是常用的,如中值滤波、最小值滤波(如图像腐蚀)、最大值滤波(如图像膨胀)都属于统计排序滤波器。更多关于图像平滑处理知识的介绍请参考《数字图像处理:线性和非线性滤波的平滑空间滤波器(Smoothing Spatial Filters)》的介绍。二、filter2D介绍2.1、简介filter2D是OpenCV使用卷积核对图像进行卷积运算的函数,该函数能对图像进行任意的线性滤波处理,具体滤波方式由核矩阵确认。该函数其实执行的是相关操作而不是卷积操作,计算公式如下:关于相关和卷积的关系请参考《《数字图像处理》空间滤波学习感悟2:空间相关与卷积的概念、区别及联系》,不过对应系数相等的盒状滤波来说,由于核矩阵的对称性,卷积和相关的处理结果相同。2.2、语法说明语法dst = cv.filter2D( src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]] )参数说明src:输入图像ddepth:目标图像深度(请参考《图像表示的相关概念:图像深度、像素深度、位深的区别和关系》),如果目标图像深度和输入图像深度相同,则传值-1,老猿测试在Python中此时取值None、0效果也一样。针对输入图像对应的目标图像,该参数的可选传值对应关系如下:kernel:卷积核(convolution kernel ),如上概述所述,实际上是相关核(correlation kernel),为一个单通道的浮点数矩阵,如果针对图像不同通道需要使用不同核,则需要将图像进行split拆分成单通道并使用对应核逐个进行处理dst:结果图像anchor:核矩阵的锚点,用于定位核距中与当前处理像素点对齐的点,默认值(-1,-1)表示锚点位于内核中心,否则就是核矩阵锚点位置坐标,锚点位置对卷积处理的结果会有非常大的影响;delta:在将卷积处理后的像素值存储到dst之前,向其添加的可选值,老猿测试验证当有值时,卷积后的像素结果值会与delta相加,得到的结果作为最终输出的像素值,注意这个加法是饱和运算,超过255的被置为255;borderType:当要扩充输入图像矩阵边界时的像素取值方法,当核矩阵锚点与像素重合但核矩阵覆盖范围超出到图像外时,函数可以根据指定的边界模式进行插值运算。可选模式包括:注意:BORDER_WRAP在此不支持;经老猿测试,默认值为BORDER_DEFAULT ,与BORDER_REFLECT_101 、BORDER_REFLECT101相同2.4、返回值返回值为结果图像矩阵,因此输入参数中的dst参数无需输入。三、使用案例下面的案例脱胎于OpenCV帮助文档,代码对输入图像进行均值滤波:import cv2 import numpy as np import matplotlib.pyplot as plt def smoothingByOpenCV(): img = imread('f:\\pic\\opencvLogo.JPG') kernal = np.ones((5, 5), np.float32) / 25 dst = cv2.filter2D(img, None, kernal) plt.subplot(121), plt.imshow(img), plt.title('Original') plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(dst), plt.title('Averaging') plt.xticks([]), plt.yticks([]) plt.show() return dst smoothingByOpenCV()结果输出:可以看到输出图像比输入图像变模糊了。上面的代码kernal中各元素相加结果为1,没有改变图像整体的亮度,当将其改为:kernal = np.ones((5, 5), np.float32) / 5此时,输出结果如下:可以看到图像的整体零度提升,并扩展了前景色范围,这是因为卷积过程中卷积核的元素值变大导致卷积结果值相比原值整体变大导致的。如果不改变kernal,而改变delta参数,如: kernal = np.ones((5, 5), np.float32) / 25 dst = cv2.filter2D(img, None, kernal,delta=250)则输出图像为:这是因为delta设置为250后,导致结果图像大部分像素值达到饱和导致的。当然filter2D不只是用于均值滤波,所有线性滤波都可以实现,只需要将核矩阵根据滤波任务预置不同的元素即可。四、小结本文介绍了图像平滑处理及均值滤波等基础概念,并详细介绍了卷积函数filter2D的Python语法及参数,并用之进行了对图像的均值滤波处理,可以看到卷积核元素值以及相关参数如delta等对卷积处理结果的影响。整体卷积过程是将核矩阵和处理图像从左到右、从上到下移动逐一计算像素的卷积结果过程,为了更直观的了解卷积处理,老猿用Python、numpy矩阵运算以及OpenCV-Python的图像基础操作模拟实现了一个卷积程序,其效果与filter2D基本功能完全等价。在这个过程中用到了一些小技巧,有兴趣的同好请参考《卷积处理过程模拟:用Python实现OpenCV函数filter2D等效的卷积功能》一文的介绍。
0
0
0
浏览量2090
金某某的算法生活

第十二章:OpenCV-Python对比度受限的自适应直方图均衡CLAHE知识介绍

一、引言在前面的如下几篇文章中:《数字图像处理》第三章学习总结感悟2-1:直方图均衡处理数字图像处理:局部直方图处理(Local Histogram Processing)数字图像处理:使用直方图统计进行图像增强OpenCV-Python图像直方图计算详解、示例及图形呈现数字图像处理:OpenCV-Python中的直方图均衡知识介绍及函数equalizeHist详解老猿分别介绍了数字图像直方图处理相关的原理和概念以及应用案例,并提供了全局直方图均衡处理的OpenCV-Python的实现算法以及支持函数,但老猿并没有找到局部直方图处理、使用直方图统计信息单独的OpenCV函数。与此相关的一个OpenCV概念就是CLAHE(Contrast Limited Adaptive Histogram Equalization,对比度受限的自适应直方图均衡),这个概念老猿认为涵盖了局部直方图均衡处理和基于直方图统计信息进行图像增强的部分内容,下面就来介绍与此相关的知识。二、CLAHE概念全局直方图均衡,考虑的是图像的整体对比度,正如局部直方图处理介绍的那样,对大多数图像来说这并不是个好主意,当图像中同时存在亮区域范围和暗区域范围灰度值变化比较大的情况时,全局直方图的效果并不好。下面的一张书房图像除了雕像外整体偏暗,经全局直方图均衡后虽然整体对比度得到了改善,但雕像由于亮度过高在均衡后却反而丢失了大多数的信息。为了解决这个问题,使用了自适应直方图均衡( Adaptive Histogram Equalization)。CLAHE,对比度受限的自适应直方图均衡,英文全称Contrast Limited Adaptive Histogram Equalization。OpenCV推出CLAHE,就是在结合局部的对比度统计信息进行限定来实现局部直方图均衡。在这种情况下,图像被分成称为“tiles”(瓷砖、地砖、小方地毯、片状材料、块状材料)的小块,在OpenCV中,tileSize默认为8x8 。然后对分开的子图像逐一进行直方图均衡。这样在这些较小的子图像区域中,直方图将限制在一个较小的区域中(除非存在噪声)。老猿注:tiles才开始以为是分块的每块的行列像素多少,后来仔细研究算法才发现这是图像被分成了横向和纵向各多少块,如8×8就是整个图像被划分为8纵8横共64块。不过根据看到的算法而言,图像的像素的行数和列数必须能分别整除tiles的纵向和横向块数,如果不能整除的怎么处理尚未弄明白。如果图像中有噪音,噪音将被放大(amplified)。为了避免这种情况,应用了对比度限制(contrast limiting)。如果任何直方图bin超出指定的对比度限制(在OpenCV中默认为40),则在应用直方图均衡之前,将这些像素裁剪(clipped )并均匀地分布(distributed uniformly)到其他bin。均衡后,要消除图块边界中的伪影(artifacts),则需要应用双线性插值(bilinear interpolation )。裁剪则是将每块图像直方图中超过对比度限制bins多出的灰度像素数去除超出部分,然后将所有bins超出的像素数累加后平均分配到所有bins;三、OpenCV中的CLAHE支持关于OpenCV中的CLAHE的支持,请参考《OpenCV-Python自适应直方图均衡类CLAHE及方法详解 》(链接:https://blog.csdn.net/LaoYuanPython/article/details/120850922)的介绍。下面的代码片段显示了如何在OpenCV中应用CLAHE以及其效果:import numpy as np import cv2 as cv img = cv.imread('tsukuba_l.png',0) # create a CLAHE object (Arguments are optional). clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) cl1 = clahe.apply(img) cv.imwrite('clahe_2.jpg',cl1)查看下面的结果,并将其与上面的结果进行比较,尤其是雕像区域:四、CLAHE的测试下面的代码加载两个经典直方图均衡的案例图像,使用不同的clipLimit值进行处理,看最终的处理效果。4.1、测试源代码from opencvPublic import preparePreviewImg,previewImgList,readImgFile import cv2 def testLocalHistEqu(): img1 = readImgFile(r'f:\pic\valley.png', True) img2 = readImgFile(r'f:\pic\火星卫星.JPG', True) imgEqu1G = cv2.equalizeHist(img1) imgEqu2G = cv2.equalizeHist(img2) clahe = cv2.createCLAHE(clipLimit=4, tileGridSize=(8, 8)) imgEqu1A4 = clahe.apply(img1) imgEqu2A4 = clahe.apply(img2) clahe.setClipLimit(40) imgEqu1A40 = clahe.apply(img1) imgEqu2A40 = clahe.apply(img2) clahe.setClipLimit(100) imgEqu1A100 = clahe.apply(img1) imgEqu2A100 = clahe.apply(img2) preparePreviewImg('输入图像:山谷', img1, fontSize=38, color=(255, 0, 0)) preparePreviewImg('全均衡效果', imgEqu1G, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=4', imgEqu1A4, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=40', imgEqu1A40, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=100', imgEqu1A100, fontSize=38, color=(255, 0, 0)) preparePreviewImg('输入图像:火星卫星', img2, fontSize=38, color=(255, 0, 0)) preparePreviewImg('全均衡效果', imgEqu2G, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=4', imgEqu2A4, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=40', imgEqu2A40, fontSize=38, color=(255, 0, 0)) preparePreviewImg('自适应均衡效果:CL=100', imgEqu2A100, fontSize=38, color=(255, 0, 0)) previewImgList() print('好') testLocalHistEqu() 以上代码中,opencvPublic是老猿常用的图像处理自定义方法的公用模块,本文使用的自定义公用模块函数preparePreviewImg,previewImgList,readImgFile,cmpMatrix,print2DMatrix,其功能请参考《 OpenCV-Python图形图像处理:自用的一些工具函数功能及调用语法介绍》(https://blog.csdn.net/LaoYuanPython/article/details/111351901)中的介绍。4.2、输出图像从上述图像结果可以看到:1、对比度受限的自适应直方图均衡与全局直方图相比,对于暗区域的改善效果更好;2、clipLimit值越大,暗区域的改善效果越明显,但亮区域反而变模糊了。五、小结本文介绍了OpenCV-Python对比度受限的自适应直方图均衡CLAHE相关知识,可以看到CLAHE比全局直方图均衡对明暗分布不均匀的图像的改善效果更好,而进行CLAHE处理时,clipLimit值越大,对暗区域的改善效果越明显,但亮区域反而会起到反作用。
0
0
0
浏览量2032
金某某的算法生活

第十五章:OpenCV-Python图像处理:区分前景背景权重的图像融合案例

一、思路假设图像A和B融合,B为黑色背景,为了实现黑色背景的图像背景不遮挡图像A,实现类似透明的效果,采用如下思路:将黑色背景图像B对应的图像掩码求出,并得到该图像掩码求反的掩码反码;按照B图像的掩码和反码,将图像A分成两部分,分别与B图像的前景和背景范围对应,得到A1(对应B前景)和A2(对应B背景);让A1和B前景部分按对应权重融合,将A2部分和B背景部分按另外的权重融合,两融合结果图像相加,即得到最终的结果图像。二、实现具体的代码实现遵循上述思路,但编码的细节比较复杂一些,具体参考注释:def addWeightedDistinguishBLK(img1, alpha, img2, beta, sigma, gamma=0.0): """ 图像img1和img2权重相加,但图像img2中像素为黑色的部分取img1的像素权重为sigma 参数img1, alpha, img2, beta, gamma与addWeighted的参数相同,sigma为img1中对应img2黑色部分范围的权重 """ l = len(img2.shape) if l == 3:#是彩色图 row, col, channel = img2.shape if channel == 3: img2Gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) else: img2Gray = cv2.cvtColor(img2, cv2.COLOR_BGRA2GRAY) else:#是灰度图 img2Gray = img2 retval, img2Inv = cv2.threshold(img2Gray, 43, 255, cv2.THRESH_BINARY_INV) #将灰度小于43的像素作为黑色,img2Inv为img2黑色部分设为255,非黑色部分设为0的img2图像掩码反码 #为了对img2图像前景进行平滑,对img2图像掩码反码进行开、闭、膨胀运算, kernal = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)) img2Inv = cv2.morphologyEx(img2Inv, cv2.MORPH_OPEN, kernal) img2Inv = cv2.morphologyEx(img2Inv,cv2.MORPH_CLOSE,kernal) img2Inv = cv2.morphologyEx(img2Inv, cv2.MORPH_DILATE, kernal) retval, img2Mask = cv2.threshold(img2Inv, 0, 255, cv2.THRESH_BINARY_INV) #求img2的掩码 img1Transparent = cv2.bitwise_and(img1, img1, mask=img2Inv) #获得img1中与img2背景范围对应的部分 img1NotTransparent = cv2.bitwise_and(img1, img1, mask=img2Mask)#获得img1中与img2前景范围对应的部分 img2NotTransparent = cv2.bitwise_and(img2, img2, mask=img2Mask) #获得img2中前景部分 imgTmp = cv2.addWeighted(img1NotTransparent, alpha, img2NotTransparent, beta, gamma) #img1中与img2前景范围对应的部分与img2前景部分融合 dest = cv2.addWeighted(imgTmp, 1, img1Transparent, sigma, gamma) #将融合前景部分与img1对应img2背景部分融合 return dest def addWeightedSmallImgToLargeImgDstgshBLK(largeImg,alpha,smallImg,beta,sigma,gamma=0.0,regionTopLeftPos=(0,0)): "将小图像与大图像指定位置的内容融合,但对小图像透明部分单独处理,取大图像sigma的权重部分" srcW, srcH = largeImg.shape[1::-1] refW, refH = smallImg.shape[1::-1] x,y = regionTopLeftPos if (refW>srcW) or (refH>srcH): #raise ValueError("img2's size must less than or equal to img1") raise ValueError(f"img2's size {smallImg.shape[1::-1]} must less than or equal to img1's size {largeImg.shape[1::-1]}") else: if (x+refW)>srcW: x = srcW-refW if (y+refH)>srcH: y = srcH-refH destImg = np.array(largeImg) tmpSrcImg = destImg[y:y+refH,x:x+refW] tmpImg = addWeightedDistinguishBLK(tmpSrcImg, alpha, smallImg, beta,sigma,gamma) destImg[y:y + refH, x:x + refW] = tmpImg return destImg三、应用案例3.1、两幅图像大图像seaside.jpg:小图像Lotus.JPG:3.2、实现代码addWeightedSmallImgToLargeImgDstgshBLK在opencvPublic模块中提供: from opencvPublic import addWeightedSmallImgToLargeImgDstgshBLK,readImgFile def main(largeImg,smallImg): information = "老猿Python博客文章目录:https://blog.csdn.net/LaoYuanPython/article/details/109160152,敬请关注同名微信公众号" img1 = readImgFile(largeImg, False) #自定义读入图片文件的函数,具体功能请参考:https://blog.csdn.net/LaoYuanPython/article/details/111351901 img2 = readImgFile(smallImg, False) img = addWeightedSmallImgToLargeImgDstgshBLK(img1,1,img2,0.5,1) cv2.imwrite(r'f:\pic\addWeightedBlk.jpg',img) cv2.imshow('img',img) print(f"\n更多学习资料请参考:\n {information}") cv2.waitKey(0) main(r'f:\pic\seaside.JPG',r'f:\pic\lotus.JPG')3.3、输出图像和程序运行信息四、小结本文介绍了一种区分前景、背景按不同权重进行图像融合的思路、具体实现及应用案例,这种模式对于需要融合图像中存在黑色背景的图像时能实现将黑色作为透明处理,达到将带黑色背景的前景部分融合到另外的图像,使得融合后的图像更自然。
0
0
0
浏览量108
金某某的算法生活

OpenCV-Python图形图像处理

介绍使用OpenCV-Python进行图形图像处理的相关内容,目前主要是学习的随笔,后面将总结补充内容后变成零基础学习OpenCV-Python图形图像处理的教程。
0
0
0
浏览量2722

履历