GG
一文讲通OCR文字识别原理与技术全流程
一、好话说在前头,谁适合读本文?本文的作者在教育行业搞OCR识别工作,教育领域的OCR比较复杂,除了文字外,还有图片、表格、公式等等。即便同样是公式,在数学里要斜体,在化学里要正体,这都是行业规范。本文的读者是谁呢?读者是……最终谁会读,我不知道。但是,我定位以下人群为本文的读者,换句话说,我就是写给他们看的。1.1 公司领导:节省成本,沉淀技术很多企业领导,看到OCR属于人工智能范畴,很恐惧。哎呀,我们公司的员工,连正常的业务逻辑都写不好,交付个系统一堆Bug。现在需要使用OCR功能了,怎么办?买一个吧。不买难道自己做吗?那样,我还要建一个人工智能团队。这个情况,还真得具体分析。我了解到有一家公司,他们的OCR识别需求非常简单:仅仅识别0到9,共10个数字。而且,数据来源单一,保证透明背景纯色线条字迹。这种待识别的样本,非常规范。其实,随便找一本图像识别的书,翻开第一章,几乎都是在讲如何识别这类数字,这个例题已经20多年了。这在程序员中,被称为是Hello World级别的程序,是入门的第一课,没有难度。甚至谷歌公司觉得这太简单了,以至于人工智能受到了侮辱。于是,他们率先把入门的例子,由10个数字,改成了识别“轮船”、“汽车”、“青蛙”、“小鸟”等10类物体。但是,这家公司依然以30万一年的价格,购买了一个识别数字的OCR服务。这就像是买了辆大巴车,当电动车来用,一个人开着它走街串巷,维护成本高,利用率也低。因此,我感觉,领导不需要了解技术细节,但是需要大体了解它的成熟度和行业状态。本文会讲述做OCR的流程,以及每一步需要的资源支持,以便领导们可以盘点资源,量入为出。1.2 产品经理:了解过程,融会贯通产品经理经常被开发人员怼。一方面是开发人员性格过于刚直。另外就是产品经理,有时候确实不了解实现过程就乱提需求:比如,App主题色要随手机壳的颜色来变化。但是,我也见过那种开发出身的产品经理,他不仅懂产品,也懂技术。他经常把开发人员怼得一愣一愣的:怎么实现不了?这边有数据,那边也有,做一个关联,查询时别不加限制,那样太慢!开发人员则红着脸,遇到新需求时,先自己百度一下,做完了功课再去找这个产品经理辩论。最后,开发人员沮丧着回来,百度也不靠谱啊,原来是有实现思路的!因此,我感觉产品经理需要了解技术的实现过程,以便在关键节点上,可以提出产品侧的更优方案。本文会讲述实现OCR需要几个步骤,以及每个步骤的关键点是什么。我觉得产品经理有必要看。1.3 初级小白:解疑答惑,入门行业有人觉得OCR好神奇,怎么做到的?我不明白,谁来给我解释解释。这是对此感兴趣的求知者。也有人,非常喜欢图像识别,自己想学,但是经常会被拒之门外。这是怀有满腔热情和愤懑的技术小白。网络上,确实有很多大牛,博士硕士研究生,但是因为他们的水平很高,我们很难与他们对话。我曾经被鄙视:一个傅里叶变换,一句话就可以解释清楚,你却写了几千字,说了一堆废话。于是,我认识到“同等对话”很重要。如果我想要实现小康家庭的生活,那我去找全球商业大亨请教,可能起不到什么效果,反而去跟楼下五金店的老板拉拉家常,能有所收益。现在,我弄明白了OCR识别。同时,我也想起之前的迷茫和无助。现在,或许还存在很多曾经的我,我要自己帮一下自己。本文会讲整个OCR的完整流程,以及其中的难点和解决方案(思路以及术语关键词)。如果你是程序员的话,我在专业版里,还有代码详解。OCR这个行业,如果无法入门,掉头就走,一旦入门,爱不释手。好了,前言我说完了,也算是导读,如果觉得本文适合你,可以继续往下看了。二、OCR识别的全流程(科普版)OCR是一个简称,全称叫Optical Character Recognition,中文是:光学字符识别。它的本质是:把图像形状转变为文本字符。下面说一下,我在教育行业是如何应用OCR的。信息化教学越来越普及,很多教学素材都要搬到信息化平台,比如试卷试题。那么,纸质的试卷要电子化,就会用到OCR识别技术。这么一张图,需要识别成结构化(图片、文字、公式、表格可独立提取出来)的数据,识别结果如下所示:而且,识别结果还可以下载成word文档,便于老师校对并进行二次编辑:这就是OCR的一个典型应用。除此之外,我们常见的各种证件识别、名片识别、车牌识别等,也涉及OCR技术。我在入门OCR的时候,做过一个小功能,我把它作为一次学习总结和毕业小考,效果如下:上面这个例子,在github上已经完全开源。此例子基本囊括了OCR的全过程,下面我就以它作为样板,来讲一讲OCR的全流程。OCR技术的实现,总体上可以分为五步:预处理图片、切割字符、识别字符、恢复版面、后处理文字。中间的三步是核心,头尾两步最难。2.1 预处理图片我们买回来水果,需要洗一下再吃。如果运气不好的话,还需要挖掉虫眼和糙皮才能吃。我们把吃水果前的这些步骤,叫做CSG(吃水果)的预处理。在进行OCR之前,也需要对图片进行预处理。因为,一般待识别的图片千奇百怪,来源复杂:有拍照、有扫描、有截图。拿拍照来说,有夏至那天中午头儿,在阳光直射下拍的;也有人在傍晚,拿着大顶转着圈儿拍的。如果不进行预处理,OCR会很为难,就像你面对刚从粪池里捞上来的大枣一样为难。2.1.1 光影的预处理一般情况下,我们定义白色为背景,黑色为字体。但是,如果图片上有了光影,就会存在模糊状态。说它是背景吧,它不是白色的。要说它是文字吧,黑乎乎地一片儿,也认不出来有什么字符。这导致OCR经常人格分裂,这是……这不是……它是不是呢?智能出现了问题,人工一看,我给做个预处理吧,交给你的时候保证非黑即白,你专心做事就行。OCR很感动。2.1.2 倾斜的预处理理想条件下的文档图像,应该是水平的,这样方便切割方块字。但是,现实世界中,不管是人,还是素材,都很难摆正自己的位置。不正,切起来就复杂了。治图,如同治人,需要分门别类(强制升华文章格调)。上面这种倾斜最为常见,处理起来也最简单。只需要几句代码就能搞定,我会在以后专门介绍。基本原理就是找到文本的最小面积矩形(关键词:minAreaRect),然后旋转这个矩形,实现角度矫正,看下面这个动态图。但是,这种方法有时候也不灵,比如下面这张图。我们现在框一个矩形,完美!我们再把矩形摆正,完蛋!这种倾斜,无论怎么摆矩形都不行,因为矩形区域内的文字又有倾斜!这时候,就需要用另一种方法,叫做霍夫线变换(关键词:HoughLinesP。有时候搜索一个问题,都不知道该搜啥,此处我提供了关键词,其解决方案可直达灵魂)。霍夫线变换就是在图上找直线,因为图中的若干点,是可构成一条直线的。把这些直线画出来,你会发现玄机,看下面的动态图。一段若干行的文本,每一行的字都应该是在一条直线上的。从结果倒推过程,如果找到了一行直线,那么是不是就找到了一个文本行。当把这些直线摆正时,就实现了文档的矫正。看下面的动态图。2.1.3 扭曲的预处理上面讲的是平面的角度倾斜,此类情况在复印和扫描中较多(纸张放斜了)。这不算严重,顶多就如同用凉水泡方便面,问题不大。其实,我们遇到更多的图像是照片。拍照,问题就多了,会存在空间的扭曲。看下面的动态图(图是动态的,如果不是,等一等,或者你遇到盗版的作者了,正版作者是ITF男孩)。上面的图,问题就比较严重了,就如同用煤油泡方便面,还非得让别人吃,这叫扭曲。空间的扭曲,体现在视觉上就是远大近小。我们来矫正下面这张图,这张图应该是站在长城上拍的长廊,越远越小。肯定能矫正,就是步骤有点多。但是,换算成代码,也不会超过100行。下面这张动态图里,我把每一步对图片处理的方法也都列上了。总共9步,每一步都可以单拉出一篇文章来讲解如果你的图片来源很复杂,尤其是包含各种场景下的拍照,或者也有从漫山遍野捡来的野生图片、二手改装图片。那么,你的预处理工作将会比较费劲,没有难点,但是需要耗费人力物力,需要时间。如果,你的图片来源很简单。就像我开头讲的,0到9数字识别还购买OCR服务的例子。他们公司是用电子采集笔在电子方格上写数字,电子方格是统一的,笔是统一的,样本非常标准。这种情况,不需要预处理,直接进入下一步,切割字符(妈呀,这个转场,太丝滑了)。2.2 切割字符假设,通过了预处理,我们的图片都变成像下图这样规范。我忽然想到,我们是要做OCR字符识别的(你……干啥来的)。于是,我们需要切割字符,把每一个字……都给他(咬着牙,发狠的表情)……挖出来。为什么要把每个字符都切割出来?因为OCR最终是对单个字符进行识别的(识别26,其实是识别“2”和“6”)。并且,还需要对每个字符做好标记,因为识别完了,还得还原回去。识别完了,结果是一堆单蹦的“1”、“2”、“3”、“+”、“-”字符。我们需要根据它们的相对位置,还原成“8-7=1”。所以,我们就知道了,哪个题目做对了,做错了,从而给出批改结果。2.2.1 投影法实现分割上帝说要有光,就有了光。如果有姓尚的朋友,可以给孩子起名叫:尚有光。有光以后,当光投过来时,物体的背后就有了影。有影子的地方就有实体,没影子的地方就有空隙。那位说了(我也不知道哪位),你扯这些干什么?这是三岁小孩子都懂的常识。没错,三岁小孩子都知道。但是三十岁的大孩子不一定能想到,这个常识可以用来分割字符。2.2.2 切行假设我们拿着一根头发丝儿,横着收集像素点,从左侧插入,从右侧推出。把所有黑点都压缩到一起,把黑色素……嗯,黑色素堆到最右侧。就像下面的图这样。此时,我们就能清楚地知道哪个区域是有文字的行,哪个区域是白纸。这个价值两百五的操作,可以实现行的切分。这一招就是投影大法,三岁孩子都了解。2.2.3 切列切行是横着切,切列就得竖着切了。一定要先切行,再切列。多数情况,行是有行距的,每一行都会有明显的界限。但是列……如果把整个文档做投影的话,基本上就沦陷了。上面那样做投影,拆不出单个字符。因为一篇文档的字,就像城墙一样,磨砖对缝,无法切分。但是,换成对一行文本进行投影分析,就可以了。看下面这个图,非常之清晰。通过投影之间的间隙,我们就可以把每个字符切割开来。2.2.4 切字有了行与列切分的方法,相信把字符切出来,应该是不难的。其实就是很简单,代码也不复杂。全都是数组的分析。那么切出来的字,最终是这样。不是白纸黑字吗?为什么都变成了黑底白字呢?其实,这是故意的。为的就是要方便OCR进行识别。我们都知道(也可能不知道),在RGB色值中,0代表黑色,255代表白色。不管计算机的算力多么强大,一秒钟能运算多少亿万次,它的底层还是二进制,也就是101010。你可以简单地理解成它只认识数字。你看到字母A是A,计算机没有你那么厉害,计算机偷偷地在显示器上输出A这个图案,然后心里暗自记下这个物体是65。因此,任何文本、图片、音视频,最终都要被解析成数字,这样计算机才能干活。扯这些有什么用?你在逃避什么?黑白颠倒的问题呢?别急,马上。我们希望计算机识别图片上的字,而不是背景。所以,把背景置为黑色,也就0,把字符变成白色,也就是255,这样有利于计算机更专注于分析字符的痕迹。因为,0默认是忽略掉的。你看,说着说着,就谈到了人工智能的机器学习。哈哈,又转场了,真爽。2.3 识别字符图片究竟是怎么变为字符的?它还能自己学习。计算机通过学习一些样本之后,遇到一些从未遇到过的同类样本,也能正确地识别出结果,这很神奇。我想了一夜也没想明白。第二天,我带着孩子去公园,公园门口有一对大狮子。孩子指着狮子说,狗!我说,哦,那不是狗,那是狮子,跟狗有点像是不是。又走了一段路,公园里又出现一个麒麟的雕塑。孩子指着它说,狮子!我说,那不是狮子。孩子说,是狗。我说也不是狗,它叫:麒麟。我感觉到,孩子的大脑在反向矫正信息,这就是监督学习。当我给他看狗的图片时,我告诉她这是狗。她根据自己的认知,找了几个特征,构建了一个模型:长嘴+尖牙=狗。虽然只是看过图片,但是出门遇到真狗,她根据这个模型也认识对了。后来,她遇到了狮子,她修改了模型:长嘴+尖牙+鬃毛≠狗=狮子。后来,又遇到了麒麟,这个公式变得越来越复杂……决策项越来越多。人工智能,就是模拟的人类的神经元,构建神经网络来尝试寻找特征和结果的关系。如果对了,就给这个特征加分。如果,错了,就给这个特征减分。识别数字,也是一样。比如在学习识别数字6的时候,它随机认为只要有一个圈圈特征,就是数字6。验证其他样本时,发现这个随机特征是对的(不对就再换一个特征再试)。于是,它建立了一个模型:只要有圈这个特征,就是6。后来,这个模型遇到了数字0。加入新样本后,人工智能发现,0也有圈,但它不是6,也有可能是0。得再找一个特征,于是,新增一条,有勾就是6。后来,它又遇到了9。那勾在上面的就是6。后来,它又遇到了字母b……反正计算机有的是算力,能在很短的时间内完成这些学习。上面我是搂着说的,其实即便在32*32像素的小图片上,它随机上几十个特征去做验证,一点都不吃力。这就是识别字符的原理。具体到代码,也很简单,因为人工智能框架目前已经非常成熟。虽然,这篇是科普版,不是专业版,不适合讲代码,但是我还是非常想贴上一段代码,给大家看看。打破你的认知,人工智能的应用层很简单,别被忽悠了。举个例子,识别10类常见物体:飞机、自行车、鸟,猫,鹿、狗、青蛙、马、轮船、汽车。它的核心代码只有……6行。所以,OCR字符的识别从来不难。难的是两头,比如开头的预处理,以及下面要说的后处理。2.4 文本后处理识别出了字符,意义不大,有效地连接起来才能发挥作用。一定要记住我上面说的这句话,默读3遍以上。其实,这句话没啥用,只是有助于缓解紧张的气氛!对于类似的话,我认为是废话,因为没有任何指导意义,但是说的也没错。2.4.1 版面还原可能有人会觉得,我接下来讲的会比较跳跃,有点作者着急去厕所的感觉。这并不是什么写作风格,这篇文章我快写吐了,很想快点结束。或许我该搞一个系列专题,我比较喜欢讲述体系化的东西,不喜欢一次冒一个点,那样对别人没有什么深度价值。也可能有人觉得,版面还原不难(是的,进入正题了),字符我都拆开了,坐标也记录了,把识别的字符画上,不就还原了?!没错,说的很对,把识别的结果画上去,视觉上是还原了。但是,这依然属于单个字符识别的那一步,只不过做成了结果可视化,是坐标还原,并不是版面还原。我们期望的拆分和还原应该是下面这样:“10+2= 4-3= 5+6=11”这些文本从数据结构上应该是一行。而且,“10+2=”从数据结构上是一个基本单位。因为,我们要对基本单位做运算和批改。这才叫还原,其实并不简单。有点震惊,我拆字的时候,没有人跟我要求过这些规则。举个小例子,这个例子非常小,假设你识别出来了2个字,你现在有2个字符的数据:请问,这两个字,是不是在处于同一行?你通过肉眼无法判定,得计算。这就需要你用代码编写算法处理。如果你数学不好,那可能还真的是一个不小的挑战。从图上看,你的眼睛可能几毫秒就识别出来了,但是计算机没有眼睛,只有大脑。它就等着你告诉它要怎么去算什么数据。其实也好处理(话都让你说了,难也是你说的),看两个字在Y轴的重叠情况。如果重叠达到一定占比,那就可以认为这两组数据是处于同一行。其实字符与字符之间的关系还有很多情况。根据情况的不同,我们就可以做不同的判定。上图所示,如果文本1的矩形区域和公式1的矩形区域,在横向上有一定比例的重叠,那我们可以认为,它们是处于同一行。如果文本2的区域完全包含(重叠率100%)于表格1的区域中,那么我们可以认为文本2属于表格1。同样,文本2和文本3在纵向的重叠率,可以作为它俩是否位于同一列的一个指标。2.4.2 文本校正OCR识别的最终目的,是要获得一份准确的、结构化的文本内容。单个字符识别,其实是各自为战,前后不商量。就比如,遇到一个圆圈形状的字符图片。OCR识别就犯了难,它是数字“0”?汉字“〇”?大写字母“O”?小写字母“o”?中文句号“。”?还是“Q”忘了加尾巴……。啥都对,啥都不对。所以,需要矫正……校正。这两个词,都是高频词,尤其拼音打字jiaozheng,容易出错。其实,也好分辨。看语境,如果我前后提到了“文稿”,那么是“校正”的可能性就大。如果我刚刚说了“牙齿”、“视力”、“角度”啥的,那么基本上就应该是“矫正”了。OCR识别的最后一步校正也是一样。如果无法确定是数字“0”还是字母“o”,可以观察它相邻的几个字符,下面一图胜千言。单个字符识别不对没关系,后期智能校正可以结合语境来帮你纠正。这个步骤就叫做后处理。我想,OCR流程介绍的差不多了。下面该总结了。三、总结其实,我已经迫不及待地想睡觉了。但是,睡觉前,我还是想输出几个观点。3.1 OCR的投入:自己开发 vs 调用第三方?需要企业领导视自身业务需求和研发能力来确定。通过上面的流程讲解,其实我们也了解到,做OCR并不难,这在业界已经非常成熟了。如果,你的业务需求很单一,另外也有一两个喜欢研究技术的程序员(三年经验起),其实可以投入几个人、几个月搞一搞试试看效果。就算不成功,起码他们再跟第三方对接起来,也属于专业级别了。那么,如果你的业务需求复杂多样,是不是就要用第三方服务了。也不一定,需求太复杂,通用的第三方平台,不一定能满足你的个性化需求。我之前遇到过一个例子,也是在教育行业。他们有一个场景是用在填空题手写答案上。一般的手写识别,你就算写的80%正确,它会给你智能纠正,输出字符。但是,教育行业不行,写错了就是写错了,不要纠正。比如,武术的“武”,学生如果右下角写成了“戈”那样多了一撇,不要输出“武”,要输出不是字,并记录下学生的错字图片。这一下,没有一家平台可以对接。其实,自己研发是可以做到的。但是,研发这玩意有什么用?只有自己用。如果业务比较通用,且第三方费用不是很高的情况下,可以考虑购买服务。其实,不管是个人生活还是企业运转,总归都是要考虑成本的控制。最终都是资金限制了一切。所以,我说多少都是白扯。那种说,我有钱,但是找不到人才的老板,请联系我。3.2 OCR的重点在哪里?我认为是数据。现今而言,瓶颈已经不是技术了,数据量决定识别率。短期内,技术没有太多可提高的空间了。剩余的就是拼数据量。很多人觉得人工智能不智能,甚至智障。其实,有一部分原因就是训练数据太少。就拿智能问答来说,很多人问的问题,人工智能回答不好。原因就是,你问的这些问题它从来没有接触过。就像我和孩子去公园的例子,我一直给她看狗的图像,突然问她麒麟是什么,她会从狗的答案里去找类似的应对。我还是拿教育行业举例(我熟啊),如果我们拿一本鲁教版七年级地理上册,交给人工智能学习。如果它学完了,你问它书本上的知识,它绝对是回答准确。但是,你如果问它七年级下册的,它估计就蒙了。更何况,还有八年级、九年级呢?更何况学科还有物理、化学、生物呢?更何况,我们生活化的对话场景,不会出现在课本里呢!想让它聪明,得多少数据,谁又有这些数据?!OCR也是一样。识别那一块儿,大人写的字和小孩写的字,是有差别的,想要识别准确,肯定是样本越多越准确。后处理校正那一块儿,无他,只能是见多了才能识广。
GG
由检测2个矩形框的重叠程度,来理理解决问题的思路
一、开场:应用场景首先,什么是矩形框的重叠程度?两个矩形框的重叠关系存在很多种可能性:横向有交集、纵向有交集、完全无交集、部分或全部重叠。为什么要检测重叠程度?这其实是我在工作中遇到的一个问题。1.1 散点值只能获坐标,规整行无法取蹊径我刚刚完成了这么一个功能,就是把纸质试卷进行电子化,主要操作的,就是下面这样的试卷。我把它变成html版本的。还要能生成word文件并下载,对于里面的公式、表格也可以进行正常编辑。这里涉及到目标检测、OCR识别、word写入等技术。不过,今天的重点是:识别后的数据结构是散点式的,识别结果是由字符块组成的。[
{
"y2": 158,
"x2": 86,
"word": "有",
"x1": 68,
"y1": 144
},
{
"h": 158,
"w": 97,
"word": "效",
"x1": 73,
"y1": 144
},
{
"y2": 158,
"x2": 101,
"word": "值",
"x1": 85,
"y1": 144
},
{
"y2": 158,
"x2": 117,
"word": "是",
"x1": 97,
"y1": 144
},
{
"y2": 169,
"x2": 164,
"word": "(公式)\\frac { \\sqrt 2 } { 2 } N e _ { 0 }",
"x1": 118,
"y1": 133
}
]
你需要根据坐标,将它们排列成如下的结构:1.2 算重叠或许得排位,计包含定能获从属因为每个字都是一个小矩形,我们只有它们的坐标数据。于是,判断两个矩形的重叠情况就很有必要了。如下图所示,如果文本1的矩形区域和公式1的矩形区域,在横向上有一定比例的重叠,那我们可以认为,它们是处于同一行。如果文本2的区域完全包含(重叠率100%)于表格1的区域中,那么我们可以认为文本2属于表格1内的内容。同样,文本2和文本3在纵向的重叠率,可以作为它俩是否位于同一列的指标。二、表演:解决方案我们要解决的这个问题,很有意思,我感觉它上升到了艺术的高度,非常唯美(你可能觉得我有些癫狂,但我真的就是这么认为的)。之所以说它有艺术,就在于解决方案太多了,美不胜收,众里寻她。2.1 用穷举分门别类,使判断千丝万缕很简单啊,分情况做判断就行。如果是完全包含,也就是这个情况:以python代码举例:if b_x1 > a_x1 and b_x2 < a_x2 and b_y1 > a_y1 and b_y2 < a_y2:
print("重合率100%")
如果是完全无重叠,那就是这样:if b_x1 > a_x2 and b_y1 > a_y2: # 右下角:b左超a右,b上超a下,则无交集
print("重合率0%")
这是相对于右下角的情况。除此之外,还要考虑其他3个方位的判断。而且还要考虑两个目标互换位置的情况(谁是a框,谁是b框)。上面只是判断重叠率为100%和重叠率为0%这两种极端情况,这还是比较简单的。但是,我们已然发现有了很多的if判断。你可以试着想一下,如果要计算具体的重叠率,那需要怎么写?谁在上来谁在下,谁在左来谁在右,下面的下多少,右面的又右多少……不知怎么地,我忽然想起了刘三姐对山歌……反正我不敢采用分情况判断的写法,我害怕这样写,我和程序都会崩溃掉。2.2 图形、数组两方斗,几何、代数一家亲我们一直都在考虑空间的问题,谁左谁右,谁偏谁多少距离,这其实是几何问题。当几何问题解决繁琐时,我们可以考虑用袋鼠……带数……对!就是你纠正的那个词语。代数里面有个集合,集合和几何同音,可以试试从这个点切入。我们拿矩形框在X轴方向的重叠来看,所谓重叠,其实就是有多少个相同元素。矩形A和矩形B在X轴重叠情况,其实就是集合A和集合B,相同元素个数的占比。我们看到,矩形A和矩形B是有相同元素的,集合C同前两者没有相同元素,即没有任何重叠。这么一转,豁然开朗。我们来计算一下X方向的重叠情况:# a_x1, a_y1, a_x2, a_y2 = 矩形a的(左上角x, 左上角y, 右下角x, 右下角y)
# b_x1, b_y1, b_x2, b_y2 = 矩形b的(左上角x, 左上角y, 右下角x, 右下角y)
ax = [i for i in range(a_x1,a_x2+1)] # 生成a的元素集合
bx = [i for i in range(b_x1,b_x2+1)] # 生成b的元素集合
x_sam_len = len(set(ax) & set(bx)) # 求相同元素个数
x_len = min(a_x2-a_x1, b_x2-b_x1)+1 # 获取哪个集合更短
x_rate = x_sam_len/x_len # 相同元素个数/更短集合 = 重合率
注释很清晰,已经比代码多了,我也不敢再多嘴了。看,这里面没有一个if语句,也实现了重叠情况的判断和计算。总体的判断,下面一个方法就可以实现,可以直接复制来用,不用导入任何包:# 两个矩形区域重叠比率,输入a[x1,y1,x2,y2] b[x1,y1,x2,y2],输出横纵方向
def get_same_rate(a,b):
a_x1,a_y1,a_x2,a_y2 = a
b_x1,b_y1,b_x2,b_y2 = b
# X方向
ax = [i for i in range(a_x1,a_x2+1)]
bx = [i for i in range(b_x1,b_x2+1)]
x_sam_len = len(set(ax) & set(bx))
x_len = min(a_x2-a_x1, b_x2-b_x1)+1
x_rate = x_sam_len/x_len
# Y方向
ay = [i for i in range(a_y1,a_y2+1)]
by = [i for i in range(b_y1,b_y2+1)]
y_sam_len = len(set(ay) & set(by))
y_len = min(a_y2-a_y1, b_y2-b_y1)+1
y_rate = y_sam_len/y_len
return x_rate,y_rate
调用方法如下所示,我们实验几组看看效果:# %% [左上角x, 左上角y, 右下角x, 右下角y]
ra = [0,0,10,10]
rb = [4,4,8,8]
x_rate,y_rate = get_same_rate(ra,rb)
x_rate,y_rate # (1.0, 1.0)
ra = [0,0,10,10]
rb = [20,20,28,28]
x_rate,y_rate = get_same_rate(ra,rb)
x_rate,y_rate # (0.0, 0.0)
ra = [0,0,10,10]
rb = [8,8,18,10]
x_rate,y_rate = get_same_rate(ra,rb)
x_rate,y_rate # (0.27, 1.0)
效果还是不错的,可单独X,可单独Y,可以结合XY做综合判断,可盐可甜。三、谢幕:归纳总结编程是门艺术,大家对艺术的理解不一样,追求也不一样。今天快下班时,宝昌(一同事,你不认识不影响了解剧情)一直感叹:“哎呀~啧啧啧……fastapi太优雅了,优雅,好优雅!”。赞美之词,生于真心,溢于言表,晚上回去都会做美梦的那种赞赏。编程之美就在于,对于同一个功能,有好多种实现方式:可以嵌套5层for循环,也可以混成map排序;可以写if、else通过参数走分支,也可以子类重写父类方法直接同一句代码调用。说到这里,我又想起了一件事。你看,艺术家总是这么多愁善感。想听的听,不想听的可以撤了,走前别忘了点赞。话说,我回老家去买鸡,活鸡现宰。杀鸡店老板现场处理,因为过节,排队的人巨多,都催着赶紧弄。老板带着口罩,全身心投入,摘毛,冲洗,掏内脏,如入无人之境。在巨大工作量面前,他得心应手,弯腰捡个地下的工具,起身还要单独冲冲手。装袋前,会把肉重新冲一遍,擦干净手,装两层袋子。活鸡宰杀切割,送到你手里时,你接触不到任何油污。我敢说,济南任何一家超市都做不到这一点。真的非得是慢工才能出细活吗?习惯也很重要吧。我们老说时间不够,时间不够,所以代码写的不规范。但是,假设,假设有一天,时间给得很充足,那时你是否会,突然变出那个习惯去写注释、去思考效率问题。这就像,你放假了,有空闲了,屋子就会变干净吗?你自己回答。但是,对于爱干净的人来说,即便很忙,房子依然是整洁的。再就是,解决问题思路还得开放些,几何不行就用代数,代数不行就找我,我是编程表演艺术家TF男孩,擅长编程表演。提醒你早撤,现在晚了。下一次我表演的节目是:人工智能训练音频文件,实现语音识别,并控制LED灯转向。
GG
CNN基础识别-想为女儿批作业(三):图像裁剪和结果展示
一、亮出效果最近在线教育行业遭遇一点小波折,一些搜题、智能批改类的功能要下线。退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介: 作对了,能打对号;做错了,能打叉号;没做的,能补上答案。醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。二、实现步骤今天主要讲如何切分图片、计算结果,并将结果反馈出来。往期回顾2.1 准备数据 2.2 训练数据2.3 预测数据之前我们准备了数据,训练了数据,并且拿图片进行了识别,识别结果正确。到目前为止,看来问题不大……没有大问题,有问题也大不了。下面就是把图片进行切割识别了。2.4 切割图像上帝说要有光,就有了光。于是,当光投过来时,物体的背后就有了影。我们就知道了,有影的地方就有东西,没影的地方是空白。这就是投影。这个简单的道理放在图像切割上也很实用。我们把文字的像素做个投影,这样我们就知道某个区间有没有文字,并且知道这个区间文字是否集中。下面是示意图:2.4.1 投影大法最有效的方法,往往都是用循环实现的。要计算投影,就得一个像素一个像素地数,查看有几个像素,然后记录下这一行有N个像素点。如此循环。首先导入包:import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont
import PIL
import matplotlib.pyplot as plt
import os
import shutil
from numpy.core.records import array
from numpy.core.shape_base import block
import time
比如说要看垂直方向的投影,代码如下:# 整幅图片的Y轴投影,传入图片数组,图片经过二值化并反色
def img_y_shadow(img_b):
### 计算投影 ###
(h,w)=img_b.shape
# 初始化一个跟图像高一样长度的数组,用于记录每一行的黑点个数
a=[0 for z in range(0,h)]
# 遍历每一列,记录下这一列包含多少有效像素点
for i in range(0,h):
for j in range(0,w):
if img_b[i,j]==255:
a[i]+=1
return a
最终得到是这样的结构:[0, 79, 67, 50, 50, 50, 109, 137, 145, 136, 125, 117, 123, 124, 134, 71, 62, 68, 104, 102, 83, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, ……38, 44, 56, 106, 97, 83, 0, 0, 0, 0, 0, 0, 0]表示第几行总共有多少个像素点,第1行是0,表示是空白的白纸,第2行有79个像素点。如果我们想要从视觉呈现出来怎么处理呢?那可以把它立起来拉直画出来。# 展示图片
def img_show_array(a):
plt.imshow(a)
plt.show()
# 展示投影图, 输入参数arr是图片的二维数组,direction是x,y轴
def show_shadow(arr, direction = 'x'):
a_max = max(arr)
if direction == 'x': # x轴方向的投影
a_shadow = np.zeros((a_max, len(arr)), dtype=int)
for i in range(0,len(arr)):
if arr[i] == 0:
continue
for j in range(0, arr[i]):
a_shadow[j][i] = 255
elif direction == 'y': # y轴方向的投影
a_shadow = np.zeros((len(arr),a_max), dtype=int)
for i in range(0,len(arr)):
if arr[i] == 0:
continue
for j in range(0, arr[i]):
a_shadow[i][j] = 255
img_show_array(a_shadow)
我们来试验一下效果:我们将上面的原图片命名为question.jpg放到代码同级目录。# 读入图片
img_path = 'question.jpg'
img=cv2.imread(img_path,0)
thresh = 200
# 二值化并且反色
ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV)
二值化并反色后的变化如下所示:上面的操作很有作用,通过二值化,过滤掉杂色,通过反色将黑白对调,原来白纸区域都是255,现在黑色都是0,更利于计算。计算投影并展示的代码:img_y_shadow_a = img_y_shadow(img_b)
show_shadow(img_y_shadow_a, 'y') # 如果要显示投影
下面的图是上面图在Y轴上的投影从视觉上看,基本上能区分出来哪一行是哪一行。2.4.2 根据投影找区域最有效的方法,往往还得用循环来实现。上面投影那张图,你如何计算哪里到哪里是一行,虽然肉眼可见,但是计算机需要规则和算法。# 图片获取文字块,传入投影列表,返回标记的数组区域坐标[[左,上,右,下]]
def img2rows(a,w,h):
### 根据投影切分图块 ###
inLine = False # 是否已经开始切分
start = 0 # 某次切分的起始索引
mark_boxs = []
for i in range(0,len(a)):
if inLine == False and a[i] > 10:
inLine = True
start = i
# 记录这次选中的区域[左,上,右,下],上下就是图片,左右是start到当前
elif i-start >5 and a[i] < 10 and inLine:
inLine = False
if i-start > 10:
top = max(start-1, 0)
bottom = min(h, i+1)
box = [0, top, w, bottom]
mark_boxs.append(box)
return mark_boxs
通过投影,计算哪些区域在一定范围内是连续的,如果连续了很长时间,我们就认为是同一区域,如果断开了很长一段时间,我们就认为是另一个区域。通过这项操作,我们就可以获得Y轴上某一行的上下两个边界点的坐标,再结合图片宽度,其实我们也就知道了一行图片的四个顶点的坐标了mark_boxs存下的是[坐,上,右,下]。如果调用如下代码:(img_h,img_w)=img.shape
row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
print(row_mark_boxs)
我们获取到的是所有识别出来每行图片的坐标,格式是这样的:[[0, 26, 596, 52], [0, 76, 596, 103], [0, 130, 596, 155], [0, 178, 596, 207], [0, 233, 596, 259], [0, 282, 596, 311], [0, 335, 596, 363], [0, 390, 596, 415]]2.4.3 根据区域切图片最有效的方法,最终也得用循环来实现。这也是计算机体现它强大的地方。# 裁剪图片,img 图片数组, mark_boxs 区域标记
def cut_img(img, mark_boxs):
img_items = [] # 存放裁剪好的图片
for i in range(0,len(mark_boxs)):
img_org = img.copy()
box = mark_boxs[i]
# 裁剪图片
img_item = img_org[box[1]:box[3], box[0]:box[2]]
img_items.append(img_item)
return img_items
这一步骤是拿着方框,从大图上用小刀划下小图,核心代码是img_org[box[1]:box[3], box[0]:box[2]]图片裁剪,参数是数组的[上:下,左:右],获取的数据还是二维的数组。如果保存下来:# 保存图片
def save_imgs(dir_name, imgs):
if os.path.exists(dir_name):
shutil.rmtree(dir_name)
if not os.path.exists(dir_name):
os.makedirs(dir_name)
img_paths = []
for i in range(0,len(imgs)):
file_path = dir_name+'/part_'+str(i)+'.jpg'
cv2.imwrite(file_path,imgs[i])
img_paths.append(file_path)
return img_paths
# 切图并保存
row_imgs = cut_img(img, row_mark_boxs)
imgs = save_imgs('rows', row_imgs) # 如果要保存切图
print(imgs)
图片是下面这样的:2.4.4 循环可去油腻还是循环。 横着行我们掌握了,那么针对每一行图片,我们竖着切成三块是不是也会了,一个道理。横着的时候,字与字之间本来就是有空隙的,然后块与块也有空隙,这个空隙的度需要掌握好,以便更好地区分出来是字的间距还是算式块的间距。幸好,有种方法叫膨胀。膨胀对人来说不积极,但是对于技术来说,不管是膨胀(dilate),还是腐蚀(erode),只要能达到目的,都是好的。kernel=np.ones((3,3),np.uint8) # 膨胀核大小
row_img_b=cv2.dilate(img_b,kernel,iterations=6) # 图像膨胀6次
膨胀之后再投影,就很好地区分出了块。根据投影裁剪之后如下图所示:同理,不膨胀可截取单个字符。这样,这是一块区域的字符。一行的,一页的,通过循环,都可以截取出来。有了图片,就可以识别了。有了位置,就可以判断识别结果的关系了。下面提供一些代码,这些代码不全,有些函数你可能找不到,但是思路可以参考,详细的代码可以去我的github去看。def divImg(img_path, save_file = False):
img_o=cv2.imread(img_path,1)
# 读入图片
img=cv2.imread(img_path,0)
(img_h,img_w)=img.shape
thresh = 200
# 二值化整个图,用于分行
ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV)
# 计算投影,并截取整个图片的行
img_y_shadow_a = img_y_shadow(img_b)
row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
# 切行的图片,切的是原图
row_imgs = cut_img(img, row_mark_boxs)
all_mark_boxs = []
all_char_imgs = []
# ===============从行切块======================
for i in range(0,len(row_imgs)):
row_img = row_imgs[i]
(row_img_h,row_img_w)=row_img.shape
# 二值化一行的图,用于切块
ret,row_img_b=cv2.threshold(row_img,thresh,255,cv2.THRESH_BINARY_INV)
kernel=np.ones((3,3),np.uint8)
#图像膨胀6次
row_img_b_d=cv2.dilate(row_img_b,kernel,iterations=6)
img_x_shadow_a = img_x_shadow(row_img_b_d)
block_mark_boxs = row2blocks(img_x_shadow_a, row_img_w, row_img_h)
row_char_boxs = []
row_char_imgs = []
# 切块的图,切的是原图
block_imgs = cut_img(row_img, block_mark_boxs)
if save_file:
b_imgs = save_imgs('cuts/row_'+str(i), block_imgs) # 如果要保存切图
print(b_imgs)
# =============从块切字====================
for j in range(0,len(block_imgs)):
block_img = block_imgs[j]
(block_img_h,block_img_w)=block_img.shape
# 二值化块,因为要切字符图片了
ret,block_img_b=cv2.threshold(block_img,thresh,255,cv2.THRESH_BINARY_INV)
block_img_x_shadow_a = img_x_shadow(block_img_b)
row_top = row_mark_boxs[i][1]
block_left = block_mark_boxs[j][0]
char_mark_boxs,abs_char_mark_boxs = block2chars(block_img_x_shadow_a, block_img_w, block_img_h,row_top,block_left)
row_char_boxs.append(abs_char_mark_boxs)
# 切的是二值化的图
char_imgs = cut_img(block_img_b, char_mark_boxs, True)
row_char_imgs.append(char_imgs)
if save_file:
c_imgs = save_imgs('cuts/row_'+str(i)+'/blocks_'+str(j), char_imgs) # 如果要保存切图
print(c_imgs)
all_mark_boxs.append(row_char_boxs)
all_char_imgs.append(row_char_imgs)
return all_mark_boxs,all_char_imgs,img_o
最后返回的值是3个,all_mark_boxs是标记的字符位置的坐标集合。[左,上,右,下]是指某个字符在一张大图里的坐标,打印一下是这样的:[[[[19, 26, 34, 53], [36, 26, 53, 53], [54, 26, 65, 53], [66, 26, 82, 53], [84, 26, 101, 53], [102, 26, 120, 53], [120, 26, 139, 53]], [[213, 26, 229, 53], [231, 26, 248, 53], [249, 26, 268, 53], [268, 26, 285, 53]], [[408, 26, 426, 53], [427, 26, 437, 53], [438, 26, 456, 53], [456, 26, 474, 53], [475, 26, 492, 53]]], [[[20, 76, 36, 102], [38, 76, 48, 102], [50, 76, 66, 102], [67, 76, 85, 102], [85, 76, 104, 102]], [[214, 76, 233, 102], [233, 76, 250, 102], [252, 76, 268, 102], [270, 76, 287, 102]], [[411, 76, 426, 102], [428, 76, 445, 102], [446, 76, 457, 102], [458, 76, 474, 102], [476, 76, 493, 102], [495, 76, 511, 102]]]]它是有结构的。它的结构是:all_char_imgs这个返回值,里面是上面坐标结构对应位置的图片。img_o就是原图了。2.5 识别循环,循环,还是TM循环!对于识别,2.3 预测数据已经讲过了,那次是对于2张独立图片的识别,现在我们要对整张大图切分后的小图集合进行识别,这就又用到了循环。翠花,上代码!all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
model = cnn.create_model()
model.load_weights('checkpoint/char_checkpoint')
class_name = np.load('class_name.npy')
#遍历行
for i in range(0,len(all_char_imgs)):
row_imgs = all_char_imgs[i]
# 遍历块
for j in range(0,len(row_imgs)):
block_imgs = row_imgs[j]
block_imgs = np.array(block_imgs)
results = cnn.predict(model, block_imgs, class_name)
print('recognize result:',results)
上面代码做的就是以块为单位,传递给神经网络进行预测,然后返回识别结果。针对这张图,我们来进行裁剪和识别。看底部的最后一行recognize result: ['1', '0', '12', '2', '10']
recognize result: ['8', '12', '6', '10']
recognize result: ['1', '0', '12', '7', '10']
结果是索引,不是真实的字符,我们根据字典10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'转换过来之后结果是:recognize result: ['1', '0', '-', '2', '=']
recognize result: ['8', '-', '6', '=']
recognize result: ['1', '0', '-', '7', '=']
和图片是对应的:2.6 计算并反馈循环……我们获取到了10-2=、8-6=2,也获取到了他们在原图的位置坐标[左,上,右,下],那么怎么把结果反馈到原图上呢?往往到这里就剩最后一步了。再来温习一遍需求:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。实现分两步走:计算(是作对做错还是没错)和反馈(把预期结果写到原图上)。2.6.1 计算python有个函数很强大,就是eval函数,能计算字符串算式,比如直接计算eval("5+3-2")。所以,一切都靠它了。# 计算数值并返回结果 参数chars:['8', '-', '6', '=']
def calculation(chars):
cstr = ''.join(chars)
result = ''
if("=" in cstr): # 有等号
str_arr = cstr.split('=')
c_str = str_arr[0]
r_str = str_arr[1]
c_str = c_str.replace("×","*")
c_str = c_str.replace("÷","/")
try:
c_r = int(eval(c_str))
except Exception as e:
print("Exception",e)
if r_str == "":
result = c_r
else:
if str(c_r) == str(r_str):
result = "√"
else:
result = "×"
return result
执行之后获得的结果是:recognize result: ['8', '×', '4', '=']
calculate result: 32
recognize result: ['2', '-', '1', '=', '1']
calculate result: √
recognize result: ['1', '0', '-', '5', '=']
calculate result: 5
2.6.2 反馈有了结果之后,把结果写到图片上,这是最后一步,也是最简单的一步。但是实现起来,居然很繁琐。得找坐标吧,得计算结果呈现的位置吧,我们还想标记不同的颜色,比如对了是绿色,错了是红色,补齐答案是灰色。下面代码是在一个图img上,把文本内容text画到(left,top)位置,以特定颜色和大小。# 绘制文本
def cv2ImgAddText(img, text, left, top, textColor=(255, 0, 0), textSize=20):
if (isinstance(img, np.ndarray)): # 判断是否OpenCV图片类型
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
# 创建一个可以在给定图像上绘图的对象
draw = ImageDraw.Draw(img)
# 字体的格式
fontStyle = ImageFont.truetype("fonts/fangzheng_shusong.ttf", textSize, encoding="utf-8")
# 绘制文本
draw.text((left, top), text, textColor, font=fontStyle)
# 转换回OpenCV格式
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
结合着切图的信息、计算的信息,下面代码提供思路参考:# 获取切图标注,切图图片,原图图图片
all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
# 恢复模型,用于图片识别
model = cnn.create_model()
model.load_weights('checkpoint/char_checkpoint')
class_name = np.load('class_name.npy')
#遍历行
for i in range(0,len(all_char_imgs)):
row_imgs = all_char_imgs[i]
# 遍历块
for j in range(0,len(row_imgs)):
block_imgs = row_imgs[j]
block_imgs = np.array(block_imgs)
# 图片识别
results = cnn.predict(model, block_imgs, class_name)
print('recognize result:',results)
# 计算结果
result = calculation(results)
print('calculate result:',result)
# 获取块的标注坐标
block_mark = all_mark_boxs[i][j]
# 获取结果的坐标,写在块的最后一个字
answer_box = block_mark[-1]
# 计算最后一个字的位置
x = answer_box[2]
y = answer_box[3]
iw = answer_box[2] - answer_box[0]
ih = answer_box[3] - answer_box[1]
# 计算字体大小
textSize = max(iw,ih)
# 根据结果设置字体颜色
if str(result) == "√":
color = (0, 255, 0)
elif str(result) == "×":
color = (255, 0, 0)
else:
color = (192, 192,192)
# 将结果写到原图上
img_o = cv2ImgAddText(img_o, str(result), answer_box[2], answer_box[1],color, textSize)
# 将写满结果的原图保存
cv2.imwrite('result.jpg', img_o)
结果是下面这样的:
GG
opencv基础:文档倾斜矫正
一、缘起公司的HR姐姐知道我是做图像识别的程序员,她专门找到我。她说正好手里有一些文件扫描件,但是不规范,你能帮忙做一做吗?HR姐姐给我这样一张图,说这个机密文件的空白区域太大了,她想只要文字区域,要我用程序把文本区域标示出来。二、boundingRect 边界矩形这个需求太简单了。我首先想到的就是vc2里面的boundingRect方法,它就是专业框矩形区域的。通过灰度、反色、二值化,处理一下,最后交给boundingRect识别,我很快就做出来了,效果如下:代码如下:def boundingRect(image_path):
# 读入图片3通道 [[[255,255,255],[255,255,255]],[[255,255,255],[255,255,255]]]
image = cv2.imread(image_path)
# 转为灰度单通道 [[255 255],[255 255]]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 黑白颠倒
gray = cv2.bitwise_not(gray)
# 二值化
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 获取最小包裹正矩形 x-x轴位置, y-y轴位置, w-宽度, h-高度
x, y, w, h = cv2.boundingRect(thresh)
left, top, right, bottom = x, y, x+w, y+h
# 把框画在图上
cv2.rectangle(image,(left, top), (right, bottom), (0, 0, 255), 2)
# 将处理好的文件保存到当前目录
#cv2.imwrite('img2_1_rotate.jpg', image)
# 将处理好的文件弹窗展示
# cv2.imshow("output", image)
# cv2.waitKey(0)
boundingRect('img1_0_origin.jpg')
我想鼓起勇气去找HR姐姐,打算告诉她我的实现思路。怕见面之后磕磕巴巴讲不清楚,我就提前打好草稿。首先调用cv2.imread读入图片,这时候读取的是3通道的原图,读完了之后,如果调用cv2.imshow("output", image)展示一下,就是彩色原图。图形的数据是这样的[[[255,255,255],[255,255,255]],[[255,255,255],[255,255,255]]],每个像素点有RGB三个值。对于识别文本边界的话,这个数据还是有点复杂。因为计算机没有必要关心它是红的还是绿的,只需要关心有无就行。因此,需要调用cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)对图形进行灰度处理,处理后的图形数据变简单了,变为单通道的像素集合[[255 255],[255 255]]。此时,白色区域的数值接近255,黑色区域的数值接近0。我们更关注于黑色区域的字,它的值居然是0。这不可以。计算机一般对于0是忽略的,对于255是要关注的(这就是为什么很多文字识别的训练集都是黑底白字)。所以,需要来一个黑白反转,把我们关注的变为255。黑白反转的算法很简单,用255-就可以。255-255=0,255-0=255。这样黑的就变为白的,白的变为黑的。但是,灰度图是0~255之间的数字,会存在127、128这种不黑不白的像素。也会存在一些5、6、7这类毛边或者阴影,说它是字吧,还看不清,说不是吧,隐隐约约还有。人生就要果断地进行“断”、“舍”、离”,程序更要如此。要么是字,要么是空白,我就是这样坚决,这也是为了减少计算量。现在,图片只存在0或者255了。把它交给boundingRect它可以返回我们需要的数值。我把程序交付给了HR姐姐,我最终还是啥也没说。她很忙,对我说了声谢谢,她说稍后试试看。我激动地等她的回复,一下午一行代码也没写,反复看给她程序的代码,看哪里有没有疏漏。三、minAreaRect 最小面积矩形HR姐姐叫我去一趟。我特意先去了一趟厕所。我想象着该如何回应她的感谢,我要面带微笑,我说客气啥,都是应该的。不行,我得表现的傲娇一些,这都是小事,分分钟搞定,以后有这种事记得找我。这样是不是不够友好……我还是见到了她,她说程序好像有点问题,识别出的小卡片不是她想要的,我看了看。原来是这个机密文件扫描斜了,所以框出来也是斜的。这属于异常情况。因为她不是产品经理,所以我忍住没有发火。我说我回去再看看。我找到了另一种方法,就是minAreaRect,它可以框选出一个区域的最小面积。即便是图片倾斜了,为了达到最小面积,它的框也得倾斜。有了倾斜角度,再旋转回来,就是正常的了,最终测试成功。我又开始对着镜子训练了,我这次一定要鼓起勇气告诉她实现思路,同时,把上次失败的原因也一并说出来。代码如下:def minAreaRect(image_path):
# 读入图片3通道 [[[255,255,255],[255,255,255]],[[255,255,255],[255,255,255]]]
image = cv2.imread(image_path)
# 转为灰度单通道 [[255 255],[255 255]]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 黑白颠倒
gray = cv2.bitwise_not(gray)
# 二值化
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# %% 把大于0的点的行列找出来
ys, xs = np.where(thresh > 0)
# 组成坐标[[306 37][306 38][307 38]],里面都是非零的像素
coords = np.column_stack([xs,ys])
# 获取最小矩形的信息 返回值(中心点,长宽,角度)
rect = cv2.minAreaRect(coords)
angle = rect[-1] # 最后一个参数是角度
print(rect,angle) # ((26.8, 23.0), (320.2, 393.9), 63.4)
# %% 通过换算,获取四个顶点的坐标
box = np.int0(cv2.boxPoints(rect))
print(box) # [[15 181][367 5][510 292][158 468]]
# 画框,弹窗展示
cv2.drawContours(image, [box], 0, (0, 0, 255), 2)
cv2.imshow("output", image)
cv2.waitKey(0)
return angle
前面读入图片、灰度、黑白颠倒、二值化,与boundingRect处理一样。有区别的地方就是为了能够找到最小面积,minAreaRect需要你给它提供所有非空白的坐标。它通过自己的算法,计算这些坐标,可以画出一个非平行于XY轴,刚刚包裹这些坐标点的矩形区域。minAreaRect的返回值需要解读一下((248.26095581054688, 237.67669677734375), (278.31488037109375, 342.6839904785156), 53.530765533447266),它分为三个部分(中心点坐标x、y,长宽h、w,角度a)。我们先看角度a,如果角度a是正数。不想记这些也没有关系,想搞到文本框的位置有一个方法叫cv2.boxPoints(rect),它可以从minAreaRect的返回值,直接转换出来四个顶点的坐标[[15 181][367 5][510 292][158 468]]。至于如何旋转图片……你先记下就好,能用就行,因为这不是重点,没有必要为所有事情耗费精力,人生总要留点遗憾才能去奋力弥补。# 传入图片数据数组和需要旋转的角度
def rotate_bound(image, angle):
#获取宽高
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
# 提取旋转矩阵 sin cos
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# 计算图像的新边界尺寸
nW = int((h * sin) + (w * cos))
nH = h
# 调整旋转矩阵
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
return cv2.warpAffine(image, M, (nW, nH),flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
求角度,再旋转图片# 调用求角度
angle = minAreaRect('img2_0_rotate.jpg')
# 旋转图片,查看效果
image = rotate_bound(cv2.imread('img2_0_rotate.jpg'), 90-angle)
cv2.imshow("output", image)
cv2.waitKey(0)
我带着程序去找HR姐姐,推开门,看到她刚把外套挂在衣架上,在她转身的那一刻,我看到她穿的不多,很低,很火。不行!我是正人君子,非礼勿视。我说,程序改好了,你再试试吧。说完我就红着脸摔门而出。不一会,她又叫我过去,说程序好像有点问题。我再去时,发现她居然把外套穿上了,明明不是很冷,她为什么要穿上外套!她跟我描述了一下现象。她说你看,处理出来的图片依然不是她想要的,她就是想要摆正了的文字图片。我一看,这个异常情况太异常了,首先一个斜的矩形文本区域,这个区域内的文本又是斜的。这玩意,你怎么画框它也转不正啊。HR姐姐娇羞羞地问:大工程师,这个有难度吗?“难度,哈哈哈,不存在的!我先回去了,抽个空就给你搞定!”,我直挺挺地走出房间,关上门的瞬间泄了气,这玩意怎么整啊?四、HoughLinesP 霍夫线变换这时候不能再想什么框和区域了,一切框都是不起作用的。更不能想天气热不热的问题,这些都只会扰乱思绪。我最终还是找到一种方法,把这种变态的倾斜文本给旋转正了。我采用的是一种叫霍夫变换的方法。霍夫变换不仅能识别直线,也能够识别任何形状,常见的有圆形、椭圆形。我不贪心,我这里只用它识别直线。# 读入图片3通道 [[[255,255,255],[255,255,255]],[[255,255,255],[255,255,255]]]
image = cv2.imread(image_path)
# 转为灰度单通道 [[255 255],[255 255]]
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 处理边缘
edges = cv2.Canny(gray, 500, 200)
# 求得所有直线的集合
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=30, maxLineGap=200)
print(lines) # [[[185 153 369 337]] [[128 172 355 362]]]
图像的灰度处理也和之前一样,主要目的是要让数据即简洁又有效。这里多了一个cv2.Canny(gray, 500, 200)的边缘处理,处理的效果如下。这样做的目的依然是进一步让数据即简洁又有效。说一下Canny(gray, 500, 200)的3个参数。第1个参数是灰度图像的数据,因为它只能处理灰度图像。第2个参数是大阈值,用于设置刻画边缘的力度,数值大边缘越粗犷,大到一定程度,边缘就断断续续的连不成块了。第3个参数是小阈值,用于修补断开的边缘,数值决定修补的精细程度。通过边缘处理,我们就得到图像的轮廓,这时候是不影响原图结构的。虽然2个点确定一条直线,但是一条直线也可以有3个点、4个点、5个点,如果条件合适,是可以从点定出直线的。cv2.HoughLinesP就在轮廓的基础上根据条件,在图片上辗转反侧地去画线,看看能画出多少直线。肯定很多条,但并不是都符合条件,根据参数它能找到符合条件的所有线段如下所示。我们解读它的参数 cv2.HoughLinesP(edges, 1, np.pi/180, threshold=30, maxLineGap=200)。第1个参数edges是边缘的像素数据。第2个参数是搜索直线时,每次移动几个像素。第3个参数是搜索直线时,每次旋转多少角度。第4个参数threshold最小是多少个点相交可以算一条直线。第5个参数maxLineGap被看做一条直线的两点间最大像素距离,太远了当成一个直线没有意义。经过这些筛选,直线们就出来了。有了直线,我们就可以计算直线的角度了,这里用到一个反三角函数公式。# 计算一条直线的角度
def calculateAngle(x1,y1,x2,y2):
x1 = float(x1)
x2 = float(x2)
y1 = float(y1)
y2 = float(y2)
if x2 - x1 == 0:
result=90 # 直线是竖直的
elif y2 - y1 == 0:
result=0 # 直线是水平的
else:
# 计算斜率
k = -(y2 - y1) / (x2 - x1)
# 求反正切,再将得到的弧度转换为度
result = np.arctan(k) * 57.29577
return result
随后,我们计算所有直线的角度。然后选出频次最高的角度,这个角度基本代表整体的倾斜角度。# 存储所有线段的倾斜角度
angles = []
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(image, (x1, y1), (x2, y2), (0,0,255))
angle = calculateAngle(x1, y1, x2, y2)
angles.append(round(angle))
# 找到最多出现的一个
mostAngle = Counter(angles).most_common(1)[0][0]
print("mostAngle:",mostAngle)
最后,我们调用方法,展示出来旋转后的效果。mostAngle = houghImg("img3_0_cut.jpg")
# 旋转图片,查看效果
image = rotate_bound(cv2.imread('img3_0_cut.jpg'), mostAngle)
cv2.imshow("output", image)
cv2.waitKey(0)
这样,这种文档我们也可以矫正了。我连忙去找HR姐姐,把这个好消息告诉了她。她很高兴,并且说自己最近也在研究程序,还问我要源码。于是,我就把项目的github地址 github.com/hlwgy/doc_c… 给了她。那天,我们两个笑的都很开心。
GG
NLP实战:自动生成原创宋词
1. 背景我有两个爱好,一个是传统文化,另一个是高新技术。传统文化,我喜欢唐诗宋词、笔墨丹青,高新技术我则从事前沿的IT编程,喜欢研究人工智能。我很想让这两者联系起来,这一老一新,不知道会碰撞出什么火花。2. 成果通过试验,利用循环神经网络结合文本生成,我最终练成神功:提供一个开头,他就会自动生成一篇宋词。而且,这篇新词绝对是原创。开头生成细雨细雨仙桂春。明月此,梦断在愁何。等闲帘寒,归。正在栖鸦啼来。清风清风到破向,貌成眠无风。人在梦断杜鹃风韵。门外插人莫造。怯霜晨。高楼高楼灯火,九街风月。今夜楼外步辇,行时笺散学空。但洗。俯为人间五色。海风海风落今夜,何处凤楼偏好。奇妙。残月破。将心青山上,落分离。今夜今夜谁和泪倚阑干。薰风却足轻。似泠愁绪。似清波似玉人。羞见。对于诗词稍有研究的我,对于上面“高楼”一词生成的文本,比较满意。高楼灯火,九街风月。今夜楼外步辇,行时笺散学空。但洗。俯为人间五色。高楼处在高处,后面的文本也体现了“高”的特色,“高楼望街”是一番意境,“高楼望夜”又是另一番意境,最后出了一个“俯看五色”,一个“俯”字,也是体现了居高临下,整篇文本无不围绕“高”的主题。实乃绝妙!下面就来剖析下,宋词生成是如何实现的。3. 实现方式3.1 数据的准备我找到了一个宋词数据集,是一个csv格式的文件,里面有2万首宋词。文档的第一列是词牌名,第二列是作者,第三列是正文。其中正文,已经做好了分词处理。想要了解分词,可以查看NLP知识点:中文分词。3.2 数据的读入首先导入整个项目涉及到的包。import csv
import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy as np
from tensorflow.python.keras.engine.sequential import Sequential
from tensorflow.keras import layers
from tensorflow.keras.optimizers import Adam
下面是加载数据集文件中数据的方法。def load_data(num = 1000):
# 读取csv文件。表头:0 题目| 1 作者| 2 内容
csv_reader = csv.reader(open("./ci.csv",encoding="gbk"))
# 以一首词为单位存储
ci_list = []
for row in csv_reader:
# 取每一行,找到词内容那一列
ci_list.append(row[2])
# 超过最大数量退出循环,用多少取多少
if len(ci_list) > num:break
return ci_list
想要详细了解如何加载csv数据集,可以查看NLP知识点:CSV格式的读取。然后进行数据序列化。def get_train_data():
# 加载数据作为语料库["春花 秋月","一江 春水 向东 流"]
corpus = load_data()
# 定义分词器
tokenizer = Tokenizer()
# 分词器适配文本,将语料库拆分词语并加索引{"春花":1,"秋月":2,"一江":3}
tokenizer.fit_on_texts(corpus)
# 定义输入序列
input_sequences = []
# 从语料库取出每一条
for line in corpus:
# 序列成数字 "一江 春水 向东 流" ->[3,4,5,6]
token_list = tokenizer.texts_to_sequences([line])[0]
# 截取字符[3,4,5,6]变成[3,4],[3,4,5],[3,4,5,6]
for i in range(1, len(token_list)):
n_gram_sequence = token_list[:i+1]
input_sequences.append(n_gram_sequence)
# 找到语料库中最大长度的一项
max_sequence_len = max([len(x) for x in input_sequences])
# 填充序列每一项到最大长度,采用前面补0的方式
input_sequences = np.array(pad_sequences(input_sequences, maxlen=max_sequence_len, padding='pre'))
return tokenizer, input_sequences, max_sequence_len
关于为什么要把文本进行序列化,以及如何序列化,可以前往知识点Tokenizer、texts_to_sequences、pad_sequences查看详细说明。这里要重点说明一下,因为要做文本预测的训练,需要从上面的词语推断出下面的词语,所以这里做了一些加工。比如“看山 不是 山 , 看山 又是 山”这一句,它给转化成了多句:看山 不是
看山 不是 山
看山 不是 山 ,
看山 不是 山 , 看山
看山 不是 山 , 看山 又是
看山 不是 山 , 看山 又是 山
这么做的目的就是告诉神经网络,如果前面是“看山”,后面跟一个词语是“不是”。当前面变成“看山 不是 山 , 看山”时,这时“看山”后面就变成“又是”了。“看山”后面并不是固定的,而是根据它前面一串词语综合判断而决定的。将一句话,切成多句话,这是一个特殊处理的地方,就是下面代码做的事情:for i in range(1, len(token_list)):
n_gram_sequence = token_list[:i+1]
input_sequences.append(n_gram_sequence)
3.3 构建模型要训练数据,我们首先得有一个神经网络模型,下面是构建了一个网络模型序列。def create_model(vocab_size, embedding_dim, max_length):
# 构建序列模型
model = Sequential()
# 添加嵌入层
model.add(layers.Embedding(vocab_size, embedding_dim, input_length = max_length))
# 添加长短时记忆层
model.add(layers.Bidirectional(layers.LSTM(512)))
# 添加softmax分类
model.add(layers.Dense(vocab_size, activation='softmax'))
# adam优化器
adam = Adam(lr=0.01)
# 配置训练参数
model.compile(loss='categorical_crossentropy',optimizer=adam, metrics=['accuracy'])
return model
关于模型、层、激活函数的知识点,有专门解释:神经网络模型的序列和层、激活函数。3.4 进行训练训练的代码如下:# 分词器,输入序列,最大序列长度
tokenizer, input_sequences, max_sequence_len = get_train_data()
# 得出有多少个词,然后+1,1是统一长度用的填充词
total_words = len(tokenizer.word_index) + 1
# 从语料库序列中拆分出输入和输出,输入是前面几个词,输出是最后一个词
xs = input_sequences[:,:-1]
labels = input_sequences[:,-1]
# 结果转为独热编码
ys = tf.keras.utils.to_categorical(labels, num_classes=total_words)
# 创建模型
model = create_model(total_words, 256, max_sequence_len-1)
# 进行训练
model.fit(xs, ys, epochs= 15, verbose=1)
# 保存训练的模型
model_json = model.to_json()
with open('./save/model.json', 'w') as file:
file.write(model_json)
# 保存训练的权重
model.save_weights('./save/model.h5')
假设我们得到了训练序列input_sequences是:[0, 0, 1, 2]
[0, 0, 3, 4]
[0, 3, 4, 5]
[3, 4, 5, 6]
对应文字就是:[0, 0, 春花, 秋月]
[0, 0, 一江, 春水]
[0, 一江, 春水, 向东]
[一江, 春水, 向东, 流]
对于训练,一般都是成对的。一个输入,一个输出。机器将学习从输入推断出输出的诀窍。在这个例子中,因为是从上一个词推断出下一个词,所以输入和输出都要从上面的语料库中来取。下面这段代码就是从input_sequences取出了输入和输出:xs = input_sequences[:,:-1]
labels = input_sequences[:,-1]输入 xs输出 labels[0, 0, 春花][秋月][0, 0, 一江][春水][0, 一江, 春水][向东][一江, 春水, 向东][流 ]因为模型里面激活函数使用了activation='softmax',所以这个输出要通过tf.keras.utils.to_categorical转化成了独热编码。如果对独热编码有疑问,可以查看《知识点:独热编码》。此时,需要强调几个概念:文本序列的最大长度max_sequence_len就是[一江, 春水, 向东, 流]的长度,此处值为4。主要作用是定义一个固定的训练长度,长度不足时补0,超出时裁剪。为什么要这么做,可以点击此处了解。输入序列的长度input_length就是[0, 一江, 春水]的长度,固定为3,是从max_sequence_len截取出来的,最后一个词不要。主要作用是作为输入。关于上面的模型数据的保存,有兴趣的可以看知识点:模型保存为json和h5格式。3.5 进行预测训练完成之后,我们就可以享受胜利果实,开始进行预测了。预测代码如下:def predict(seed_text, next_words = 20):
# 分词器,输入序列,最大序列长度
tokenizer, input_sequences, max_sequence_len = get_train_data()
# 读取训练的模型结果
with open('./save/model.json', 'r') as file:
model_json_from = file.read()
model = tf.keras.models.model_from_json(model_json_from)
model.load_weights('./save/model.h5')
# 假如要预测后面next_words=20个词,那么需要循环20词,每次预测一个
for _ in range(next_words):
# 将这个词序列化 如传来“高楼”,则从词库中找高楼的索引为50,序列成[50]
token_list = tokenizer.texts_to_sequences([seed_text])[0]
# 填充序列每一项到最大长度,采用前面补0的方式[0,0……50]
token_list = pad_sequences([token_list], maxlen= max_sequence_len-1, padding='pre')
# 预测下一个词,预测出来的是索引
predicted = model.predict_classes(token_list, verbose = 0)
# 定义一个输出存储输出的数值
output_word = ''
# 找到预测的索引是哪一个词,比如55是“灯火”
for word, index in tokenizer.word_index.items():
if index == predicted:
output_word = word
break
# 输入+输出,作为下一次预测:高楼 灯火
seed_text = seed_text + " " + output_word
print(seed_text)
# 替换空格返回
return seed_text.replace(' ' , '')
# 预测数据
print(predict('细雨',next_words = 22))
# 细雨仙桂春。明月此,梦断在愁何。等闲帘寒,归。正在栖鸦啼来。
关于上面的模型数据的读取,有兴趣的可以看知识点:读取json、h5文件恢复模型。预测需要给一个开头的词语,并且指定后面需要预测多少个词语。首先,根据开始的词语,通过model.predict_classes(token_list)预测出下一个词语,接着开头词语连同预测词语两方再作为输入,继续预测下一个词语。如此类推,像贪吃蛇一样,从一个开头词语慢慢地引出一个长句子。句子中每个词语是有语义上的前后关系的。这就是宋词生成器的实现逻辑,希望对你有所帮助。
GG
NLP实战:300行代码实现基于GRU的自动对春联
1. 前言没错,我一直探索用高新技术来激活传统文化,用传统文化来滋养高新技术。我写了一个基于TensorFlow中GRU网络来自动对春联的程序。Input是人工输入的上联,Output是机器自动给出的下联。Input: <start> 神 州 万 里 春 光 美 <end> [2, 61, 27, 26, 43, 4, 20, 78, 3]
Output:<start> 祖 国 两 制 好 事 兴 <end> [2, 138, 11, 120, 428, 73, 64, 46, 3]
Input: <start> 爆 竹 迎 新 春 <end> [2, 167, 108, 23, 9, 4, 3]
Output: <start> 瑞 雪 兆 丰 年 <end> [2, 92, 90, 290, 30, 8, 3]
Input: <start> 金 牛 送 寒 去 <end> [2, 63, 137, 183, 302, 101, 3]
Output: <start> 玉 鼠 喜 春 来 <end> [2, 126, 312, 17, 4, 26, 3]
Input: <start> 锦 绣 花 似 锦 <end> [2, 68, 117, 8, 185, 68, 3]
Output: <start> 缤 纷 春 如 风 <end> [2, 1651, 744, 4, 140, 7, 3]
Input: <start> 春 风 送 暖 山 河 好 <end> [2, 4, 5, 183, 60, 7, 71, 45, 3]
Output: <start> 瑞 雪 迎 春 世 纪 新 <end> [2, 92, 90, 27, 4, 36, 99, 5, 3]
Input: <start> 百 花 争 艳 春 风 得 意 <end> [2, 48, 8, 164, 76, 4, 5, 197, 50, 3]
Output: <start> 万 马 奔 腾 喜 气 福 多 <end> [2, 6, 28, 167, 58, 17, 33, 15, 113, 3]
2. 数据的准备人工智能的背后是大量数据的训练,关于数据来源,我还有一篇文章《没啥才艺,30行代码写了个春联数据爬虫》,主要讲述如何从网络上爬取春联数据。我们就有了春联数据,就可以开始进行训练了。2.1 导入所需要的包不管是哪种开发语言,首先是导入包,导入的每一个都是有用的。# tensorflow主包
import tensorflow as tf
# 分词器,将文字变为数字
from tensorflow.keras.preprocessing.text import Tokenizer
# 把序列填充,让数据长度相等,不足的补0
from tensorflow.keras.preprocessing.sequence import pad_sequences
# 维度数组与矩阵运算的数学函数库
import numpy as np
# 神经网络的序列
from tensorflow.python.keras.engine.sequential import Sequential
# 将训练集按照一定比例拆分为训练和测试数据
from sklearn.model_selection import train_test_split
# 神经网络的层
from tensorflow.keras import layers
# 神经网络的优化器
from tensorflow.keras.optimizers import Adam
# 系统文件一些操作
import os
# 时间
import time
2.2 读取要训练的数据首先要把我们准备的数据进行预处理。你得从文件中读出文本吧,读完了还得分出上下联单独存储吧,因为上联是输入,下联是输出,都要告诉机器的。机器呢,它又只认识数字,不认识文字。所以,你还得把它们转化成数字(这一步叫序列化)。下面一段代码,做的事情是从.txt中读取文本,然后将文本拆分保存到上联、下联两个数组中。# 给一段文本加上开始和结束标记,主要是告诉机器什么时候开始,什么时候结束
def preprocess_sentence(w):
w = w.strip()
w = '<start> ' + w + ' <end>'
return w
# 加载一定条数的数据
def load_data(num = 10000):
# 保存上联(input)和下联(target)的数组
inp_lang = []
targ_lang = []
# 指定文件位置(同级目录data下的data.txt文件)
file = open('F://all.txt','rb')
line = file.readline()
# 读取一行文本,如果此行存在
while line:
# 读出内容:【春 来 眼 际 , 喜 上 眉 梢】拆分上下联
wstr = str(line, encoding = "utf-8")
wstrs = wstr.split(',')
if len(wstrs) > 1:
inp_lang.append(preprocess_sentence(wstrs[0]))
targ_lang.append(preprocess_sentence(wstrs[1]))
# 接着再读下一行
line = file.readline()
# 如果超过预设最大数量,则退出循环
if len(inp_lang) > num:break
file.close()
return inp_lang, targ_lang
特别说明一下,文本内容样式是:春 来 眼 际 , 喜 上 眉 梢
春 光 普 照 , 福 气 长 临
春 和 景 明 , 物 阜 年 丰
每个字之间都有一个空格,主要目的是中文分词。上下联之间使用,区分。调用一下,打印出两个数组如下:inp_lang, targ_lang = load_data()
print("inp_lang=>","\n ",inp_lang,"inp_lang=>","\n ", targ_lang)
'''
inp_lang => ['<start> 春 来 眼 际 <end>', '<start> 春 光 普 照 <end>']
targ_lang=> ['<start> 喜 上 眉 梢 <end>', '<start> 福 气 长 临 <end>']
'''
2.3 将文本进行序列化我们获得了上面文字版的标准结构。但是,机器不认识,需要将他们变成数字版的。下面我们要做的基本上就是要将“床前明月光”变为“65498”。# 分词器,将文本转化为序列:上天言好事->65234
def tokenize(lang):
# 创建分词器,默认以空格分词
lang_tokenizer = Tokenizer(oov_token='<OOV>', filters='',split=' ')
# 分词器设置处理训练的文本。 {'<end>': 2,'<start>': 1,'下': 54,'不': 5}
lang_tokenizer.fit_on_texts(lang)
# 分词器序列化文本为数字。[[18, 19],[1, 18, 19,20]]
tensor = lang_tokenizer.texts_to_sequences(lang)
max_sequence_len = max([len(x) for x in tensor])
# 预处理文本,补齐长度,统一格式 [[0, 0, 18, 19],[1, 18, 19, 20]]
tensor = pad_sequences(tensor, maxlen=max_sequence_len)
# 将序列化后的数字和包含段落信息的分词器返回
return tensor, lang_tokenizer
input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
target_tensor, targ_lang_tokenizer = tokenize(targ_lang)
返回值里面,第一tensor是文字转变的数字,第二个lang_tokenizer是吃透了这段文本的分词器,里面包含了这段文本的信息,比如总共多少个字,这些字都是什么,每个字的编号是多少,相当于一个“数据管家”。里面涉及到了一些知识点:Tokenizer、texts_to_sequences、pad_sequences。我都给你准备好了,你不点一下吗?到了这里,春联里的上联和下联的序列化数据,我们就都有了。上联是:[[1,3,5,7,9]]。下联是:[[2,4,6,8,0]]。2.4 做更适合框架的数据看似到这一步就可以了,但是为了更好地融入机器学习框架的体系,还需要对数据进一步加工和处理。# 包装成TF需要的格式,采用8:2切分训练集和验证集
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)
BUFFER_SIZE = len(input_tensor_train) # 训练集的大小
BATCH_SIZE = 64 # 批次大小,一次取多少条数据
# 一轮训练,需要几步取完所有数据
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
# 将数据集顺序打乱
dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
# 生成训练批次,N批64条的数据drop_remainder=True最后一个批次不够的舍弃,去除余数。
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
train_test_split会按照一定的比例,将数据切分为训练数据和验证数据。即便如此,数据量可能还是巨大的,比如500万条。一次吃起来太费劲,所以就有了一个批次batch的概念,它表示每次取多少条数据BATCH_SIZE进行处理。所有数据,按照一批一批地取,全部扫个遍,我们称为一个周期epoch。每取一次数据进行处理,称为走了一步step。这些都是这个行业的黑话(shuyu)。3. 模型的构建模型是个啥?这里是有关神经网络模型的序列和层、激活函数的介绍。看看最好,不看也行,我再简单一说。模型就好比工厂里流水线的机器,塞进去原料,吐出来成品。就如同一个小孩,你教给他认识苹果、桔子、香蕉、梨这些都是水果,他自己找特点总结规律,最终开悟了。后面,你给他一个猕猴桃和馒头,他能清楚地知道馒头和水果不是一类东西。具体他怎么判断的,是基于他大脑里那个固定的模型推演的,大脑的结构是固定的,但是思维方式的变化的。对于人工智能来说也类似,我们要构建的模型就是大脑的结构。要实现自动对对联的功能,我们也得构建一个模型,让数据按照这种结构去自己组织和思考,最终让它具备思维。3.1 GRU神经网络我们听说过很多种神经网络,他们各有特点,就像京东、天猫、拼多多的差别一样,我们也需要从众多网络中选取一个来解决问题。本例子中,我们选择了GRU神经网络。很遗憾,百度百科上都没有GRU的词条。我们想学点人工智能真的很难,找不到靠谱的资料。我想这也是为什么我这么菜,还有人来看我博客的原因。GRU是RNN(Recurrent Neural Network-循环神经网络)的一种。RNN上个世纪就出现了,但是有硬伤。后来,在1997年就出现了LSTM(Long-Short Term Memory-长短期记忆人工神经网络),但是它也有很多弊端。于是,在2014年,就出现了GRU(Gate Recurrent Unit-门循环单元)来救场。目前来看,用着还行。说这些的目的,主要是想说明你要想了解GRU,其实得先了解RNN。它是处理一段序列的,不同于图像识别处理单张图片,它更像是处理一段视频。那是后话,现在可以先了解一下我讲的GRU。下面是GRU的示意图。能看懂最好,看不懂也不强求,它可能影响你的研究,但是绝对不影响你的使用。我还是忍不住想解释一下。我们看到这个单元有两个输入:h0、x1,输出了下一个h1。这里面x1是序列里某一时段的输入,h0是上一个序列传来的状态,经过一番运算,得出下一个状态h1。拿对联举例子,比如“春光普照”这一句。序列x1h0h11春前面没东西后面的,你注意“春”2光上面好像说到“春”了上家是“春”,我这里是“光”3普上面有提到“春光”“光”无所谓,把“春”记好就行,我出“普”4照“春”是一个季节,“普”是全的意思……它就是这么朴实无华,同一个结构,一遍遍地去循环,但是每次都得到了新的信息保存在h里遗传下去,于是就拥有了记忆。字——,机器是不认识的,即便是变成了数字,它也是理解不了语义的,他需要转为多维向量来解释这一切,这一步叫编码。为什么要编码,可以去了解下嵌入Embedding的概念。编好码之后,对联数据就这样交给神经网络,训练上几次,它就学到里面的“道”、“规则”、“猫腻”。它就会特别注意,字与字之间是如何联系的,一句话之中,前言和后语是如何对应的。其实都是多维向量之间的参数计算。训练时,神经网络计算出了向量参数,也就是评委团。当我们想要让它预测的时候,可以将向量转化为字符,这个过程叫做解码。所以,训练和预测的流程就是:编码—观察计算总结—解码对照答案。3.2 编码器首先我们将序列进行编码,也就是把输入进行升维。有了前面的介绍,相信下面这30行代码你可以轻松掌握了。embedding_dim = 256 # 嵌入层的维度
units = 1024 # 神经元数量
# 输入集字库的数量,+1是因为有长度不够时,为了凑数的补0
vocab_inp_size = len(inp_lang_tokenizer.word_index)+1
# 输出集字库的数量,+1是因为有0
vocab_tar_size = len(targ_lang_tokenizer.word_index)+1
# 编码器,把数据转化为向量
class Encoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
super(Encoder, self).__init__()
self.batch_sz = batch_sz # 每次批次大小
self.enc_units = enc_units # 神经元数量
# 嵌入层(字符数量,维度数)
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
# 门控循环单元 GRU
self.gru = tf.keras.layers.GRU(self.enc_units, # 神经元数量
return_sequences=True, # 返回序列
return_state=True, # 返回状态
recurrent_initializer='glorot_uniform') # 选择初始化器,固定值
def call(self, x, hidden):
x = self.embedding(x)
output, state = self.gru(x, initial_state = hidden)
return output, state
def initialize_hidden_state(self):
return tf.zeros((self.batch_sz, self.enc_units))
encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)
基本思路就是:每个字拆分出上百个维度,然后和上千个神经元进行运算,通过自己猜测的结果和标准输出进行比较,最终得出最适合的参数值。3.3 注意力机器如果只有编码和解码,流程就像下面这样,是可以满足需求的。但是,这也有问题。当预测Y1时,评委是c,预测Y2时评委也是c,也就是说某时段的X对Y的影响是相同的。但是,实际上一个序列中前后的内容是有关联的,比如这样一句话:我没有否定我不是不想你。如果只有一个固定评委,句子越长,携带的信息就会被稀释的越严重。所以,除了全局状态之外,也需要关注局部的状态,因此需要引入注意力机器。引入注意力机制之后,“我没有否定我不是不想你”中的否定词语将被注意或者忽略。把“没有”、“否定”、“不是”、“不”去掉,这句话就变成了“我想你”。其实,注意力机制就是一个权重配置。
# 注意力机器
class BahdanauAttention(tf.keras.layers.Layer):
def __init__(self, units):
super(BahdanauAttention, self).__init__()
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
def call(self, query, values):
# 隐藏层的形状 == (批大小,隐藏层大小)
# hidden_with_time_axis 的形状 == (批大小,1,隐藏层大小)
# 这样做是为了执行加法以计算分数
hidden_with_time_axis = tf.expand_dims(query, 1)
# 分数的形状 == (批大小,最大长度,1)
# 我们在最后一个轴上得到 1, 因为我们把分数应用于 self.V
# 在应用 self.V 之前,张量的形状是(批大小,最大长度,单位)
score = self.V(tf.nn.tanh(self.W1(values) + self.W2(hidden_with_time_axis)))
# 注意力权重 (attention_weights) 的形状 == (批大小,最大长度,1)
attention_weights = tf.nn.softmax(score, axis=1)
# 上下文向量 (context_vector) 求和之后的形状 == (批大小,隐藏层大小)
context_vector = attention_weights * values
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
上面输出一个向量和它的权重。3.4 解码器下面是解码器的代码,它和编码相反。最终是降维,将多维向量再复原到语料库中。# 解码器
class Decoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
super(Decoder, self).__init__()
self.batch_sz = batch_sz
self.dec_units = dec_units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
self.fc = tf.keras.layers.Dense(vocab_size)
# 用于注意力
self.attention = BahdanauAttention(self.dec_units)
def call(self, x, hidden, enc_output):
# 编码器输出 (enc_output) 的形状 == (批大小,最大长度,隐藏层大小)
context_vector, attention_weights = self.attention(hidden, enc_output)
# x 在通过嵌入层后的形状 == (批大小,1,嵌入维度)
x = self.embedding(x)
# x 在拼接 (concatenation) 后的形状 == (批大小,1,嵌入维度 + 隐藏层大小)
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
# 将合并后的向量传送到 GRU
output, state = self.gru(x)
# 输出的形状 == (批大小 * 1,隐藏层大小)
output = tf.reshape(output, (-1, output.shape[2]))
# 输出的形状 == (批大小,vocab)
x = self.fc(output)
return x, state, attention_weights
decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)
这样,编码器,解码器,注意力机器,都有了,下面就可以进行数据训练了。4. 训练数据上面,格式化的训练数据准备好了。模型也准备好了,下面就该训练了。训练就是把数据,按照模型预设的层次(网络结构),按照一定的幅度(损失函数)进行探索。4.1 损失函数下面是损失函数的设置,同时也设置了训练结果文件的保存位置。optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
# 保存训练数据的位置,此处设置了绝对路径
checkpoint_dir = 'F://juejin_chunlian/training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(optimizer=optimizer,encoder=encoder,decoder=decoder)
def loss_function(real, pred):
mask = tf.math.logical_not(tf.math.equal(real, 0))
loss_ = loss_object(real, pred)
mask = tf.cast(mask, dtype=loss_.dtype)
loss_ *= mask
return tf.reduce_mean(loss_)
4.2 训练一步下面这个函数,就是执行一步训练的。@tf.function
def train_step(inp, targ, enc_hidden):
loss = 0
with tf.GradientTape() as tape:
enc_output, enc_hidden = encoder(inp, enc_hidden)
dec_hidden = enc_hidden
dec_input = tf.expand_dims([targ_lang_tokenizer.word_index['<start>']] * BATCH_SIZE, 1)
# 强制 - 将目标词作为下一个输入
for t in range(1, targ.shape[1]):
# 将编码器输出 (enc_output) 传送至解码器
predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)
loss += loss_function(targ[:, t], predictions)
# 使用强制
dec_input = tf.expand_dims(targ[:, t], 1)
batch_loss = (loss / int(targ.shape[1]))
variables = encoder.trainable_variables + decoder.trainable_variables
gradients = tape.gradient(loss, variables)
optimizer.apply_gradients(zip(gradients, variables))
return batch_loss
将上联输入序列、下联输出序列、编码隐藏层传入,它最终返回本次训练的损失情况。损失越小,说明越好,和预期越接近。4.3 多轮训练上面是训练一步。我们知道,所有数据扫个遍叫一个epoch,一个epoch需要好多步才能完成。但是,我们还需要训练多个epoch。EPOCHS = 2000
for epoch in range(EPOCHS):
start = time.time()
enc_hidden = encoder.initialize_hidden_state()
total_loss = 0
for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
batch_loss = train_step(inp, targ, enc_hidden)
total_loss += batch_loss
if batch % 10 == 0:
print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1, batch,batch_loss.numpy()))
# 每10个周期(epoch),保存(检查点)一次模型
if (epoch + 1) % 10 == 0:
print('save model')
checkpoint.save(file_prefix = checkpoint_prefix)
print('Epoch {} Loss {:.4f}'.format(epoch + 1,total_loss / steps_per_epoch))
print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
就这样训练吧。后面你就静静地看着日志打印,等着它训练完成。这个过程很慢,具体看你的机器情况。我机器并不好,训练一轮大约30分钟。开始没有耐心,急于验证效果,训练100轮就停止了,试了下它对出来的下联是什么样子的,结果连字数一致都没法保证,更别提意义相对,效果那叫一个差。我减少春联条数到200条,增大训练轮数到1000次,效果也是很差,我一度认为是算法有问题。后来,耐下心来,大数据量下的千轮级别的大出血训练了好几天,效果就好了不少。看来,机器学习的两个基础真的很重要:大数据量、多训练次数。而好的算法,是后面的事情。5. 验证效果最后是验证效果。def max_length(tensor):
return max(len(t) for t in tensor)
# 计算目标张量的最大长度 (max_length)。如果训练集确定,其实这个可以写个固定的。
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)
def evaluate(sentence):
sentence = preprocess_sentence(sentence)
inputs = [inp_lang_tokenizer.word_index[i] for i in sentence.split(' ')]
print("inputs:",inputs)
inputs = pad_sequences([inputs], maxlen=max_length_inp)
inputs = tf.convert_to_tensor(inputs)
result = ''
hidden = [tf.zeros((1, units))]
enc_out, enc_hidden = encoder(inputs, hidden)
dec_hidden = enc_hidden
dec_input = tf.expand_dims([targ_lang_tokenizer.word_index['<start>']], 0)
outputs = []
for t in range(max_length_targ):
predictions, dec_hidden, attention_weights = decoder(dec_input,dec_hidden, enc_out)
predicted_id = tf.argmax(predictions[0]).numpy()
outputs.append(predicted_id)
result += targ_lang_tokenizer.index_word[predicted_id] + ' '
if targ_lang_tokenizer.index_word[predicted_id] == '<end>':
print("outputs:",outputs)
return result, sentence
# 预测的 ID 被输送回模型
dec_input = tf.expand_dims([predicted_id], 0)
return result, sentence
# 恢复检查点目录 (checkpoint_dir) 中最新的检查点
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
s = '百 花 争 艳 春 风 得 意'
result, sentence = evaluate(s)
print('Input: %s' % (sentence))
print('Predicted: {}'.format(result))
最后结果就是:Input: <start> 百 花 争 艳 春 风 得 意 <end> [2, 48, 8, 164, 76, 4, 5, 197, 50, 3]
Predicted: <start> 万 马 奔 腾 喜 气 福 多 <end> [2, 6, 28, 167, 58, 17, 33, 15, 113, 3]
6. 其他带上注释才300行代码,去了注释估计也就200出头。但是,它却实现了一个完整的对春联的基本功能。有一些知识点没有讲的太细,比如损失函数、优化器这些,我打算以后专项再讲。其实,这个例子正统是tensorflow官网用于语言翻译的(原文链接),比如西班牙语翻译英语。我此处是尝试用于对春联,但是看效果也可以。它也可以用于文言文翻译白话文,原理都是一样的,它支持输入输出的长度不一样。
GG
NLP知识点:文本数据的预处理
1. 为啥要有预处理?NLP(Natural Language Processing)指的是自然语言处理,就是研究计算机理解人类语言的一项技术。要研究语言处理,那么首先得有语言文本。之前讲过利用Tokenizer分词器对固定格式的文本进行序列化处理。在序列化之前,如何把这些文本按照一定的格式处理好,也是一项基础的工作,这一步叫数据的预处理。2. 文本存储的几种载体一般直接可读的文本数据会存储在这么几种文件里:数据库:sqlite、mysql……表格文件:csv、excel……纯文本文件:txt、json……下面我们就挨个来讲一下,如何读写这些文件。2.1 数据库数据库存储文本信息,有很多优势:支持大数据量存储查询高效支持复杂的关联关系拿sqlite数据库举例子,看一下如何进行数据的读取。下面有一个数据库文件,大小为8KB。数据库里面有一张名称叫"ci"的表,数据结构和内容如下:表里面存储了25条数据,是25篇宋词,每条数据包含序号(value)、词牌名(rhythmic)、作者(author)、内容(content)。假设,我们要使用每首词的内容作为训练集,那么我们该如何组织数据呢?# 导入sqlite的支持
import sqlite3
# 保存每首词的内容
str_array = []
# 指定文件位置(同级目录data下的data.db文件)建立连接
conn = sqlite3.connect('./data/data.db')
# 执行查询语句,只获取author, content两个字段,获得游标(内含结果)
cursor = conn.execute("SELECT author, content from ci;")
# 循环结果
for row in cursor:
# 获取结果(0 author, 1 content)中索引为1的数据
ci = row[1]
# 加入内容列表
str_array.append(ci)
# 关闭操作
cursor.close()
conn.close()
# 打印结果
print(str_array)
最终打印结果如下:['出砒霜,价钱可。\r\n赢得拨灰兼弄火。\r\n畅杀我。', '丞相有才裨造化,圣皇宽诏养疏顽。\r\n赢取十年闲。', '归。\r\n十万人家儿样啼。\r\n公归去,何日是来时。', '归。\r\n数得宣麻拜相时。\r\n秋前後,公衮更莱衣。', '百尺长藤垂到地,千株乔木密参天。\r\n只在郡城边。', '巍峨万丈与天高。\r\n物轻人意重,千里送鹅毛。\r\n\r\n' ……]
其中\r\n是回车换行。我们看看巍峨万丈与天高。\r\n物轻人意重,千里送鹅毛。\r\n\r\n在文本框的展示,能够帮助你更好地理解。延伸知识:sqlite3的写入数据的写入很简单,和数据读取类似。也是先建立连接,然后执行sql语句,这里多一个commit提交,最后断开连接。下面举例说明,连续插入2条数据。# 导入sqlite的支持
import sqlite3
# %% 数据表数据的写入
conn = sqlite3.connect('./data/data.db')
for t in[(9998,"名称1","作者1","正文1"),(9999,"名称2","作者2","正文2")]:
conn.execute("insert into ci values (?,?,?,?)", t)
conn.commit()
conn.close()
2.2 表格文档相比于数据库,表格类文档(csv、excel)也是一种很好的文本存储方式。它双击就能打开,可以直接操作内容,也能利用自带的工具做一些数据处理。下面有一个csv文件,里面有很多行,每一行是一首宋词,前三列分别是:词牌名、作者、内容。假设,我们要使用每首词的内容作为训练集,那么我们该如何组织数据呢?import csv
# 建立存储内容的数组
str_array = []
# 构建阅读器,指定文件位置(同级目录data下的data.csv文件),指定编码格式
csv_reader = csv.reader(open("./data/data.csv",encoding="gbk"))
# 循环每一行
for row in csv_reader:
# 取出索引为2的列(第3列),存入数组
str_array.append(row[2])
# 打印数据
print(str_array)
最终打印结果如下:['出砒霜,价钱可。\r\n赢得拨灰兼弄火。\r\n畅杀我。', '丞相有才裨造化,圣皇宽诏养疏顽。\r\n赢取十年闲。', '归。\r\n十万人家儿样啼。\r\n公归去,何日是来时。', '归。\r\n数得宣麻拜相时。\r\n秋前後,公衮更莱衣。', '百尺长藤垂到地,千株乔木密参天。\r\n只在郡城边。', '巍峨万丈与天高。\r\n物轻人意重,千里送鹅毛。\r\n\r\n' ……]
这样,这个数组数据就可以使用了。延伸知识:csv的写入数据的写入和数据读取类似。先构建一个写入器 ,写入数据,最后需要关闭打开的文件。下面举例说明,新建一个csv文件,然后插入1条数据。import csv
# 以写入的方式打开(新建)一个文件,指定编码
f_csv = open('./data/data2.csv','w',encoding='gbk', newline='')
# 获取这个文件的写入器
csv_writer = csv.writer(f_csv)
# 写入一行数据
csv_writer.writerow(['第一列','第二列','第三列'])
# 关闭文件
f_csv.close()
代码执行后,会在同级的data目录下新建一个data2.csv文件,然后写入一行3列的数据。2.3 文本文档文本文档(txt)是最轻量级的一种文本存储方式。它不像数据库或者表格文件那样有关联关系,它只能罗列一段段文本,它也无法承载太多的数据,一般上万行文本就会导致它读取困难。但是,它也是有优势的。那就是——使用方便。打开文件往里面输入字符就可以了。因为没有行列条数的概念,一般文本文档要存储数据集,都是以特殊字符作为区分,例如回车换行符,一行就是一条数据。下面有一段文本,我们看看如何读取它。import os
# 保存每行文本内容的数组
str_array = []
# 指定文件位置(同级目录data下的data.txt文件)
f_read = open('./data/data.txt','rb')
# 读取一行文件
line = f_read.readline()
# 如果此行存在
while line:
# 读出内容
wstr = str(line, encoding = "utf-8-sig")
# 添加到数组
str_array.append(wstr)
# 接着再读下一行
line = f_read.readline()
# 关闭文件
f_read.close()
# 打印数组
print(str_array)
最终打印结果如下:['巍峨万丈与天高。物轻人意重,千里送鹅毛。\r\n', '远来犹自忆梁陈。江南无好物,聊赠一枝春。\r\n', '用心勤苦是新诗。吟安一个字,拈断数茎髭。\r\n', '扪窗摸户入房来。笙歌归院落,灯火下楼台。\r\n', '酥某露出白皑皑。遥知不是雪,为有暗香来。\r\n', '称觞喜对二阳临。况当弦月上,一醉祝千春。\r\n']
延伸知识:txt的写入数据的写入和数据读取类似。先打开一个文件 ,写入数据,最后需要关闭打开的文件。下面举例说明,新建一个txt文件,然后写入文本。import os
# 以写入的方式打开(新建)data2.txt的文本
f_write = open('./data/data2.txt','w')
f_write.write('写入文本第一行\n第二行')
f_write.close()
3. 组合拳:清洗数据并存为json文件假设我们要训练一套关于宋词的数据,数据源就是下面数据库里的这张表的数据。看似这些数据井井有条,其实并不是那么完美。此时你有几个诉求:去冗余:去掉头尾多余的数据,去掉重复的数据。做筛选:只想要《临仙江》这种句式的数据。换存储:因为数据量不大,想以json文本格式存储清洗后的数据。分析:去掉文本的头尾空格和换行,可以使用strip()方法。去除重复数据,可通过代码逻辑实现,将遇到的句子保存起来,下一个句子到已保存列表里面查找,能查到说明重复,查不到说明第一次见。《临仙江》的格式为:{[7个汉字]。<换行回车>[5个汉字],[5个汉字]。},可以通过正则匹配筛选出,正则表达式为:^[\u4e00-\u9fa5]{7}。\r\n[\u4e00-\u9fa5]{5},[\u4e00-\u9fa5]{5}。$。从数据库读取数据,筛选到合适的文本,组成json字符串写入文本即可。代码如下:import sqlite3
import json
import re
# 存储内容的数组
str_array = []
# 已经遇到过的内容
keys = {""}
# 筛选 {7。5,5。}格式内容的正则表达式
pattern = re.compile(r'^[\u4e00-\u9fa5]{7}。\r\n[\u4e00-\u9fa5]{5},[\u4e00-\u9fa5]{5}。$')
# 连接数据库
conn = sqlite3.connect('./data/data.db')
# 执行查询,只获取内容字段,获得游标结果
cursor = conn.execute("SELECT content from ci;")
# 循环结果
for row in cursor:
# 获取索引为0的列
ci = row[0]
# 裁剪头尾
ci = ci.strip()
# 匹配格式
m = pattern.match(ci)
# 没有匹配到
if m == None:
print('\n没有匹配到:',ci)
else: # 匹配到
print('\n匹配成功:',ci)
# 是否出现过
if ci in keys:
# 出现过,是重复的,不处理
print('\n已存在->',ci)
else:
# 没有出现过,加入出现列表,加入内容列表
keys.add(ci)
str_array.append(ci)
# 关闭游标和链接
cursor.close()
conn.close()
# 将内容列表转为json
j_str = json.dumps(str_array, indent=2, ensure_ascii=False)
# 打开(新建)文本
f_write = open('./data/data2.json','w')
# 写入文本
f_write.write(j_str)
# 关闭文件
f_write.close()
生成的data2.json内容如下:
GG
AI实战课:利用TensorFlow预测你能否月薪过万
序月薪过万这个词很有意思。有的人觉得很难做到,有的人觉得很容易做到。正所谓:会者不难,难者不会。那么,月薪过万究竟和什么有关系呢?学历?年龄?性别?我这里有一份大约2万人的数据。这类数据不难找,专门做数据研究的人都知道,好多官方的机构(国外居多)都有公开的数据集,供民众研究和学习使用。就像下面这样,各行各业的都有。那么,看我手里的这份数据,如果让我来分析,我肯定是这个思路,首先是否月薪过万,和学历有关系,博士肯定都月薪过万。但是,从数据来看,确实存在很多月薪不过万的博士。继续观察发现,这些博士虽然学历高,但是多数是个体户,由此可见干个体的很难月薪过万。但是看下面数据,很多个体户也能月薪过万。我又发现上面结果中出生地是城市的居多,那是不是从城市出生的,从小就受到了城市化的影响。所以,城市出生的干个体的更容易月薪过万呢?我这么研究,肯定会陷入无限分裂之中,这只是8个字段,如果是那种预测癌症上百个字段的,人工肯定是无法完成的。那我们不如把他交给人工智能来处理,让AI去学习研究和推断。一、读入数据以下代码全部基于python 3.8 + tensorflow 2.3,运行如有报错,请注意查看版本是否对应。首先,第一步是导入相关的包,看导入的模块就知道,这是一个近似HelloWorld级别的程序。所以大家不用恐惧,所有代码真的不会超过100行(超过了,你来评论区喷我)。import tensorflow as tf
from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split
import pandas as pd
import os
除了TensorFlow外,注意安装pandas和sklearn。把我们的test.csv数据集放到代码的同级目录下,然后调用下面代码就可以把数据读入到内存了。csv_file = 'test.csv'
dataframe = pd.read_csv(csv_file)
dataframe
如果你也是用的vscode,并且也用jupyter调试,运行上面的代码可以输出如下表格。这里我要做一个特殊说明:我把中文的字符都改成了字母,因为程序确实很难更好地支持中文(编码和字节占用问题)。1.1 命名之争我没有说把中文改成英文,而是改成了字母,因为有些翻译用的是汉语拼音(先别喷)。在中英文的命名方面,我不认为纯英文就是高大上,定义一个拼音变量就是low。我的方法论就是:谁能做到既简洁又明确,我就用谁。比如上面的单位性质一栏分为:国企,私企,个体户。我们来看一下英文和拼音的对照。名称英文拼音国企state enterpriseguoqi私企private enterprisesiqi个体户individual businessgeti首先看长度,拼音要明显短得多(你是否也讨厌代码里一半字符都是变量的命名,官方代码这样是因为他们想不到更好的方法,缩写会影响可读性),就算英文缩写也无法做到这么短。其次看表意,在单位性质一栏,如果我说guoqi大家肯定想到的是私企国企里的国企,而非“过期”、“国戚”之类的,所以表意也明确。那为什么不选拼音呢?其实,对于计算机来说,最好处理的是数字。我们喜欢的代码喜欢的计算机喜欢的男man1女woman0但是,这样也会有一个问题,如果只有男、女我们还记得住,如果是职业,有几十种之多,来一个13,请问这是什么职业,你总不能拿出字典来查吧,所以这里面免不了各种转化。二、组织数据集数据读入后,就需要组装成TensorFlow框架需要的格式了。# 所有数据,20%化为测试集,80%是训练集
train, test = train_test_split(dataframe, test_size=0.2)
# 训练集里面,再分20%是验证集,80%是训练集
train, val = train_test_split(train, test_size=0.2)
# 将数据组合成(输入,输入)2项
def df_to_dataset(dataframe, shuffle=True, batch_size=32):
dataframe = dataframe.copy()
labels = dataframe.pop('result')
ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
if shuffle:
ds = ds.shuffle(buffer_size=len(dataframe))
ds = ds.batch(batch_size)
return ds
# 每32组数据为一个批次,将3类数据集都做处理
batch_size = 32
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)
这里面需要讲的,也是大家感兴趣的,那就是输入和输入到底是怎么组装的。输入是8个字段(年龄、单位性质、学历、婚姻状况、职业、性别、一周工作小时数、出生地),输出为是否月薪过万(是或者否)。关键代码就是把读入的数据,先调用labels = dataframe.pop('result')把结果列分割出来存到labels标签里面。然后,把剩余的其他字段通过dict(dataframe)进行字典化。我们来跟踪一下这两句核心代码,看看从数据层面它发生了什么,来,上jupyter。相信不用我多说什么了,使用jupyter调试就是这么强大,每一行的代码你都能看到它执行了什么。最终就是把数据组成了字典(key是字符串,value是对象)的输入+数字结果(1或0)的输出。三、构建模型的神经网络我们再来看一下我们的数据集。输入项总共有8列,我们也已经把它搞成了字典。但是,如何把它们交给神经网络的处理层,这是一门学问,需要看我们的设计和构思。我们首先要定义一个特征列的数组,然后把我们需要关注的列加进去,然后构建一个特征层,把这个特征层放入神经网络序列中,然后它们才能运转。feature_columns = [] # 存放特征列
feature_columns.append(col_a) # A列的特征
feature_columns.append(col_b) # B列的特征
……
# 构建特征层
feature_layer = tf.keras.layers.DenseFeatures(feature_columns)
model = tf.keras.Sequential([feature_layer,……]) # 模型
model.compile(……) # 配置
model.fit(……) # 开始训练
3.1 数值列比如,每周工作小时数hours这一列,我就想看看具体的数值对结果的影响,那么我们就把它定义成数值列。# 将hours设置为数值列
hours = feature_column.numeric_column("hours")
feature_columns.append(hours)
3.2 分桶列但是,有时候我们并不关注具体的数值,比如年龄,数据中从17岁到90岁都有。我们想了解某个年龄段对是否月薪过万的影响,这时候就适合用分桶列。age = feature_column.numeric_column("age")
age_buckets = feature_column.bucketized_column(age, boundaries=[20,25,30,35,40,45,60,70])
feature_columns.append(age_buckets)
其中boundaries参数中的值[20,25,30,35,40,45,60,70]就是把数据中的年龄分成多个桶。20以下20到2525到30……70以上李子涵16岁、刘梓含19岁肖雨轩24岁王大锤28岁……丁文元89岁(虚岁)只有在不同桶里才有差别,比如子涵和梓含虽然差3岁,但是我们让神经网络认为他们是一样的。其实这种分类很有用,因为20岁和30岁对于收入可能会有很大差别,但是70岁和90岁可能不会有太明显的差别。3.3 分类列即便是我们已经把私企转化为了siqi。但是,计算机还是无法更好地计算。他们真的只喜欢数值。所以,需要把字母再进一步转化,转为one_hot也就是独热形式。unit = feature_column.categorical_column_with_vocabulary_list('unit', ['guoqi', 'siqi', 'geti'])
unit_one_hot = feature_column.indicator_column(unit)
feature_columns.append(unit_one_hot)
上面代码其实就是做如下处理。名称guoqisiqigeti索引012独热表示[1, 0, 0][0, 1, 0][0, 0, 1]根据上面说的,我们想看看这8项输入对结果都有什么影响,最终构建特征层是这样的:feature_columns = [] # 特征列
# 年龄
age = feature_column.numeric_column("age")
age_buckets = feature_column.bucketized_column(age, boundaries=[20,25,30,35,40,45,60,70])
feature_columns.append(age_buckets)
# 单位性质
unit = feature_column.categorical_column_with_vocabulary_list('unit', ['guoqi', 'siqi', 'geti'])
unit_one_hot = feature_column.indicator_column(unit)
feature_columns.append(unit_one_hot)
# 学历
xueli = feature_column.categorical_column_with_vocabulary_list('xueli', ['gaozhong', 'zhuanke', 'benke', 'shuoshi', 'boshi'])
xueli_one_hot = feature_column.indicator_column(xueli)
feature_columns.append(xueli_one_hot)
# 婚姻
hunyin = feature_column.categorical_column_with_vocabulary_list('hunyin', ['weihun', 'yihun', 'lihun', 'sang_ou'])
hunyin_one_hot = feature_column.indicator_column(hunyin)
feature_columns.append(hunyin_one_hot)
# 职业
zhiye = feature_column.categorical_column_with_vocabulary_list('zhiye', ['guanli', 'jiaoxue', 'jishu', 'nongmin', 'sale', 'siji', 'weixiu', 'wenyuan', 'wuye'])
zhiye_one_hot = feature_column.indicator_column(zhiye)
feature_columns.append(zhiye_one_hot)
# 性别
sex = feature_column.categorical_column_with_vocabulary_list('sex', ['man', 'woman'])
sex_one_hot = feature_column.indicator_column(sex)
feature_columns.append(sex_one_hot)
# 一周工作时长
feature_columns.append(feature_column.numeric_column("hours"))
# 出生地
address = feature_column.categorical_column_with_vocabulary_list('address', ['village', 'city'])
address_one_hot = feature_column.indicator_column(address)
feature_columns.append(address_one_hot)
# 特征层
feature_layer = tf.keras.layers.DenseFeatures(feature_columns)
四、开始训练其实,最难的(数据准备和处理)我们都做完了。训练反而是最简单的,只需要构建好模型,然后配置一下,训练就可以了。# 构建模型
model = tf.keras.Sequential([
feature_layer,
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
# 配置
model.compile(optimizer='adam',loss='binary_crossentropy',metrics=['accuracy'], run_eagerly=True)
# 存放训练结果
model.load_weights('model.ckpt')
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath='model.ckpt', save_best_only=True)
# 进行训练
model.fit(train_ds,validation_data=val_ds,epochs=100, callbacks=[cp_callback])
我们选择使用relu激活函数,第一层64个神经元,第二层32个,最后一层1个输出(和训练数据的标签维度对应),输出采用的是sigmoid激活函数,它将输出一个0到1之间的数,我们就当它是月薪过万可能性的百分比吧。训练完毕之后,同级目录下应该有model.ckpt系列文件,那就是训练的结果。因为时间有限,我只训练了100轮。最后,我试了一下测试集的效果。loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
# 120/120 [==============================] - 9s 76ms/step
# loss: 0.4170 - accuracy: 0.7953
预测的准确率已经接近80%了。也就是说当它遇到一组从来没有见过的数据,它作出预测之后,和标准答案一对比,准确率是80%。五、验证效果我拿我的数据试一下。sample = {
'age': 31,
'unit': 'siqi',
'xueli': 'zhuanke',
'hunyin': 'yihun',
'zhiye': 'jishu',
'sex': 'man',
'hours': 42,
'address': 'village'
}
input_dict = {name: tf.convert_to_tensor([value]) for name, value in sample.items()}
predictions = model.predict(input_dict)
prob = tf.nn.sigmoid(predictions[0])
print("%.1f ok." % (100 * prob)) # 60.2 ok.
结果显示我有60%可能性月薪过万。我再改改参数试试。修改内容结果变化说明原始数据60.2%31岁已婚在私企不加班的专科IT农村男年龄设为5063.9%年龄变大更有可能涨薪出生地改为城市60.8%IT行业和出生在哪里关系不大婚姻改为未婚52.2%程序员不结婚不容易月薪过万工作单位改为国企59.9%去国企有降低工资的风险从大数据看,我还得好好学习,等着年龄增长了,起码秃顶了,才能挣到更多的钱。结注意:本例子并不是最佳实践,因为如此少量的字段和数据量,有更简单和有效的算法来实现,比如决策树或者随机森林。本文所述方法适合做为更高级和复杂数据的起点,此处只是从教学的角度来讲述如何对样本数据进行结构化处理。
GG
代码生成OCR训练集,老板:没有数据?你new一个
夕阳很强,甚至有些刺眼,它穿透玻璃照射到键盘上来。这是个普通的塑料键盘,缝隙里落满了灰尘、毛屑,在强光的照射下,更加清晰可见,犹如显微镜下的视野。工作这么多年,我早已明白,键盘只是工具,影响技术水平的只有老板提出的需求。IDE上的光标在那里闪动,它始终没有移动过一格,但也不曾休息过,就像我现在的思路一样。我的脑海中不断地重复着上午的场景:老板说:你不是说现在OCR技术很成熟了吗?那我们就自己搞啊! 我说:框架是很成熟了。但是,我们没有数据啊…… 老板问:要什么数据? 我回答:你要想让机器认识“1”,你起码得拿500张“1”的照片来训练它。 老板问:那要训练“2”呢? 我回答:500张“2”的照片,而且不能重复,重复的算1张。你想想,常用汉字就3000多个,我们没有素材啊!要不要去网上买点…… 老板陷入了沉思,突然眼镜一闪:哎,你让程序员new一个出来,你们连老婆都能new出来。 我连忙解释:那是对象。 我刚想说,手里没人,就一个实习生。 老板的电话突然响了,他捂着电话跟我说:我要出差了,3天,我回来时,你一定要把老婆……不是,把数据集给new出来!第一天:画黑框,输个字一大早,我提着豆浆,走进办公室。我的办公室不大,里面只有两个工位,一个我,一个实习生小王。但是,门口的标牌却赫然写着“产业园软件研发中心”几个字,老板说以后要扩到200百人的技术团队。小王现在读大四,这一年在这里实习,岗位是研发工程师。小王工作很认真,每天来的比我还早,好好培养应该是个好苗子。“请问,这里是财务部不?”,门口探出个脑袋,一个大爷问。“不是!”。“不是就不是,你那么大声音干什么!你这个房间很像财务部啊,一个小屋2个人,你是会计,他是出纳”。我走向小王,小王正在看掘金博客,里面有好多大神:TF男孩,春哥,林三心……。小王叫我老大,因为我是“产业园软件研发中心”的负责人,我负责他,他对我负责。跟你安排一个任务,很简单,用python先画一个32*32像素的黑色背景,然后在上面写上白色的字,你去写吧。小王很快就写好了。
from PIL import Image
from PIL import ImageDraw
# 画出一个32*32的黑色框
img = Image.new("RGB", (32, 32), "black")
# 在黑框里写上字
draw = ImageDraw.Draw(img)
draw.text((0,0), "2", (255, 255, 255))
# 保存画好的图片
img.save("2.png")
小王上午就来找我汇报工作。我试过了在黑色背景图上写上字母、数字、符号,都是可以的。我连连点头,称赞小王很棒。小王问,接下来要做什么。我说,明天再告诉你。第二天:先加载字体,再绘制字符第二天,小王问我,老大,今天什么任务啊,我昨天等了一下午。我说,运行你昨天的代码,输出个汉字试试。小王执行了下面的代码:draw.text((0,0), "汉", (255, 255, 255)),结果报错了:AttributeError: 'ImageFont' object has no attribute 'getmask2'。小王愣在一旁,我却微微一笑:你没有加载支持汉字的字体,就直接绘制汉字是不行的。今天的任务就是:你百度解决汉字的绘制。小王直到下午才来找我汇报工作。他不但展示了效果,还跟我汇报了代码。from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
img = Image.new("RGB", (320, 320), "black")
draw = ImageDraw.Draw(img)
# 加载一种字体, 320是字体的大小,和黑框一样大
font = ImageFont.truetype("chinese_fonts/fangzheng_heiti.TTF", 320)
# 将字体作为参数传入
draw.text((0,0), "汉", (255, 255, 255),font)
img.save("汉.png")
跟昨天的相比,区别就是调用 ImageFont.truetype("字体文件路径", 字体大小)加载了字体文件,然后调用draw.text(……,font)的时候,把字体font传入,这样字体大小也可以控制了。我连连点头,称赞小王很棒。并继续说,汉字的可以了,你再试试数字和符号。小王又调用draw.text(",")、raw.text("2")draw了个“,”和“2”。“什么感觉?看完这些图片,你什么感觉?”,我问小王,声音有些严厉。“没什么感觉啊,这……这不挺好的!”,小王回答道。我提高了音量,敲着屏幕:“不居中啊,大哥,逗号那么明显,你看不出来吗?”。刚刚还沉浸在骄傲中的小王有些疑惑,但是他依然很镇定:这个好弄,draw.text((x,y),……)我改下x和y坐标就行了。“今天能改完吗?”,我问他。“肯定能改完!调个坐标就完事了”,小王很自信的样子。“好”,我告诉小王:“你要记得一件事,不要针对某一个字符调坐标,多调几个,不管是出1,2,3,4,还是甲乙丙丁,都要居中,记住了吗?”。下午,我回家时,小王说要加个班。凌晨2点,我在家上厕所时,远程查看了一下公司的网络流量数据,不断有关于“python”、“字体居中”的搜索。第三天:字体居中,添加椒盐第三天一早,我在家吃过饭,又从楼下买了包子和豆浆。一进办公室,我发现小王趴在办公桌上不动了。我心里就是一惊,别再是怎么着了吧。昨天的任务对他来说可真是够难的,我连忙晃动他:小王,醒醒,醒醒,小王!小王慢慢地睁开眼,打了个哈欠,伸了个懒腰:天亮了吗?小王发现我在旁边,才反应过来:哦,这是在公司啊,我的“居中”功能还没有实现呢!老大,为什么我怎么调都有问题,x=20,y=30,对这个字符可以,换别字符的就不行了呢?如何才能写出通用的代码啊?那得加多少if和else if判断啊?我说,你先把早饭吃了,完了我告诉你个知识点,你马上就能完成任务了。其实,字体制作时,有很多规则。图中红框表示的区域,代表这是字体的范围,就算内部是空白也是人家的领地。里面,还有一个字符区域,是具体显示的内容,比如逗号,就靠下,为了便于标记,字符在字体内有一个偏移量offset属性,表示它相对于字体区域的偏移情况。所以,你要让一个字在背景里居中,他的坐标绝不是你肉眼看到的,而且是千变万化的,需要你结合字体的宽高以及偏移量来计算。小王很兴奋,有规则就好办了。很快,他就写好了代码。from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
width,height = 32,32 # 因为宽高多处使用,定义成变量
font_size = 32
char = "好" # 要绘制的字符
img = Image.new("RGB", (width, height), "black")
draw = ImageDraw.Draw(img)
# 加载一种字体, 32是字体的大小,和黑框一样大
font = ImageFont.truetype("chinese_fonts/fangzheng_fangsong.ttf", font_size)
# 获取字体的宽高
font_width, font_height = draw.textsize(char, font)
offset_x, offset_y = font.getoffset(char)
# 计算字体绘制的x,y坐标,主要是让文字画在图标中心
x = (width - font_width - offset_x) // 2
y = (height - font_height - offset_y) // 2
# 将字体作为参数传入
draw.text((x,y), char, (255,255, 255),font)
img.save("好.png")小王试了试,不管是数字、字母、符号还是汉字,确实都可以居中了。其实,关键点就是通过draw.textsize(char, font)获取了字体的宽高,通过font.getoffset(char)获取了偏移量的信息。如果要将字体在一个背景中居中,其实就是背景的长度减去字体长度再减去偏移量长度,这是字符和背景的缝隙,缝隙除以2,那就是把缝隙分到两边了,它就居中了。今天,小王虽然没有睡觉,但是他却很开心,我告诉他帮公司解决了一个大难题,让他早早地回去休息了。今天晚上,老板应该出差回来了。我拿出3天前就写好的代码,跑了起来。
from __future__ import print_function
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import os
import shutil
import time
import cv2
# 要生成的文本
label_dict = {0: '你', 1: '好', 2: '掘', 3: '金', 4: ':', 5: '1', 6: '+', 7: '2', 8: ',', 9: 'g', 10: 'o', 11: '!'}
# 文本对应的文件夹,给每一个分类建一个文件
for value,char in label_dict.items():
train_images_dir = "dataset"+"/"+str(value)
if os.path.isdir(train_images_dir):
shutil.rmtree(train_images_dir)
os.makedirs(train_images_dir)
def makeImage(label_dict, font_path, width=32, height=32, rotate = 0, salt = 22):
# 从字典中取出键值对
for value,char in label_dict.items():
# 创建一个黑色背景的图片
img = Image.new("RGB", (width, height), "black")
draw = ImageDraw.Draw(img)
# 加载一种字体,字体大小是图片宽度的90%
font = ImageFont.truetype(font_path, int(width*0.9))
# 获取字体的宽高
font_width, font_height = draw.textsize(char, font)
offset_x, offset_y = font.getoffset(char)
# 计算字体绘制的x,y坐标,主要是让文字画在图标中心
x = (width - font_width - offset_x) // 2
y = (height - font_height - offset_y) // 2
# 绘制图片,在那里画,画啥,什么颜色,什么字体
draw.text((x,y), char, (255, 255, 255), font)
# 设置图片倾斜角度
if rotate != 0:
img = img.rotate(rotate)
# 将数据转为np格式
np_img = np.asarray(img.getdata(), dtype='uint8')
# 降维,3通道转为1通道,并组成矩阵
np_img = np_img[:, 0].reshape((height, width))
for i in range(salt): #添加噪声
temp_x = np.random.randint(0,np_img.shape[0])
temp_y = np.random.randint(0,np_img.shape[1])
np_img[temp_x][temp_y] = 255
# 命名文件保存,命名规则:dataset/编号/img-编号_r-选择角度_时间戳.png
time_value = int(round(time.time() * 1000))
img_path = "dataset/{}/{}_{}.png".format(value, time_value, rotate)
cv2.imwrite(img_path, np_img)
# 存放字体的路径
font_dir = "./chinese_fonts"
for font_name in os.listdir(font_dir):
# 把每种字体都取出来,每种字体都生成一批图片
path_font_file = os.path.join(font_dir, font_name)
# 倾斜角度从-5到5度,每个角度都生成一批图片
for k in range(-5, 5, 1):
# 每个字符都生成图片
makeImage(label_dict, path_font_file, rotate = k, salt = 5-k)
这段代码,不但生成了文字,而且还添加了干扰项,比如对图片进行适度地旋转,比如给图片添加随机的噪点,我们叫椒盐(胡椒是黑色的,盐是白色的,表示黑白噪点)。因为,当送给我们识别的文档,也会有不清晰的情况,所以我们就要按照有干扰来训练,这样出来的效果才是更贴近真实情况的。其实,生成字符集的代码很简单,我和老板争论的那天下午,我就写好了。让我一直纠结的是,这次要不要让小王来写。让小王写,我需要多耗费几倍的精力,因为跟他说的功夫,我都能写完了。想起,小王已经是公司来的第10位实习生了,前几位都稍微学有所长就走了。最后,我还是选择了培养小王。尽人事,听天命。现在看来,虽然只有3天,小王的进步已经很大了。第四天:交差了我把字符集交给了老板,说是小王开发的。老板很开心,说要给小王涨200块钱的工资。我正在犹豫要不要告诉小王。小王找到了我,他有点不好意思。其实,我也想到了。小王说:老大,我找到了一份新工作,对方很认可我的能力,尤其对于我可以自动生成字符集,他们也很需要,工资给我翻了一番……所以……“好啊,祝福你!打算什么时候走?”,我的内心没有一丝波澜。小王急迫地说:今天下午……可以吗?可以!望着小王离去的工位,我从抽屉里拿出了一份策划案,那是培养小王完成整个OCR识别项目的剧本。我随便翻开一页,这页的知识点是:为什么图片的训练数据集多采用黑色背景,白色字体呢?我笑了笑:那是因为黑色色值是0,白色是255,计算机对于0是忽略的,更关注白色的255就好。 如果是白底黑字,那么需要计算机去关注0,这会让它很痛苦。后面还有如何训练,如何调优,如何部署等等等等。也罢,他有个好的去处,我也算是交差了。交差了,都交差喽。
GG
CNN基础识别-想为女儿批作业(一):生成文字图片
一、亮出效果最近在线教育行业遭遇一点小波折,一些搜题、智能批改类的功能要下线。退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介: 作对了,能打对号;做错了,能打叉号;没做的,能补上答案。醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。二、实现步骤基本思路其实,搞定两点就成,第一是能识别数字,第二是能切分数字。首先得能认识5是5,这是前提条件,其次是能找到5、6、7、8这些数字区域的位置。前者是图像识别,后者是图像切割。对于图像识别,一般的套路是下面这样的(CNN卷积神经网络):既然思路能走得通,那么咱们先搞图像识别。要自己搞图像识别,得准备数据->训练数据并保存模型->使用训练模型预测结果。2.1 准备数据对于男友,找一个油嘴滑舌的花花公子,不如找一个闷葫芦IT男,亲手把他培养成你期望的样子。咱们不用什么官方的mnist数据集,因为那是官方的,不是你的,你想要添加+-×÷它也没有。有些通用的数据集,虽然很强大,很方便,但是一旦放到你的场景中,效果一点也不如你的愿。只有训练自己手里的数据,然后自己用起来才顺手。更重要的是,我们享受创造的过程。假设,我们只给口算做识别,那么我们需要的图片数据有如下几类:索引:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
字符:0 1 2 3 4 5 6 7 8 9 = + - × ÷
如果能识别这些,基本上能满足整数的加减乘除运算了。好了,图片哪里来?!是啊,图片哪里来?吓得我差点从梦里醒来,500万都规划好该怎么花了,居然双色球还没有选号!梦里,一个老者跟我说,图片要自己生成。我问他如何生成,他呵呵一笑,消失在迷雾中……仔细一想,其实也不难,打字我们总会吧,生成数字无非就是用代码把字写在图片上。字之所以能展示,主要是因为有字体的支撑。如果你用的是windows系统,那么打开C:\Windows\FontsC:\Windows\FontsC:\Windows\Fonts这个文件夹,你会发现好多字体。我们写代码调用这些字体,然后把它打印到一张图片上,是不是就有数据了。而且这些数据完全是由我们控制的,想多就多,想少就少,想数字、字母、汉字、符号都可以,今天你搞出来数字识别,也就相当于你同时拥有了所有识别!想想还有点小激动呢!看看,这就是打工和创业的区别。你用别人的数据相当于打工,你是不用操心,但是他给你什么你才有什么。自己造数据就相当于创业,虽然前期辛苦,你可以完全自己把握节奏,需要就加上,没用就去掉。2.1.1 准备字体建一个fonts文件夹,从字体库里拷一部分字体放进来,我这里是拷贝了13种字体文件。好的,准备工作做好了,肯定很累吧,休息休息休息,一会儿再搞!2.1.2 生成图片代码如下,可以直接运行。from __future__ import print_function
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import os
import shutil
import time
#%% 要生成的文本
label_dict = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'}
# 文本对应的文件夹,给每一个分类建一个文件
for value,char in label_dict.items():
train_images_dir = "dataset"+"/"+str(value)
if os.path.isdir(train_images_dir):
shutil.rmtree(train_images_dir)
os.makedirs(train_images_dir)
# %% 生成图片
def makeImage(label_dict, font_path, width=24, height=24, rotate = 0):
# 从字典中取出键值对
for value,char in label_dict.items():
# 创建一个黑色背景的图片,大小是24*24
img = Image.new("RGB", (width, height), "black")
draw = ImageDraw.Draw(img)
# 加载一种字体,字体大小是图片宽度的90%
font = ImageFont.truetype(font_path, int(width*0.9))
# 获取字体的宽高
font_width, font_height = draw.textsize(char, font)
# 计算字体绘制的x,y坐标,主要是让文字画在图标中心
x = (width - font_width-font.getoffset(char)[0]) / 2
y = (height - font_height-font.getoffset(char)[1]) / 2
# 绘制图片,在那里画,画啥,什么颜色,什么字体
draw.text((x,y), char, (255, 255, 255), font)
# 设置图片倾斜角度
img = img.rotate(rotate)
# 命名文件保存,命名规则:dataset/编号/img-编号_r-选择角度_时间戳.png
time_value = int(round(time.time() * 1000))
img_path = "dataset/{}/img-{}_r-{}_{}.png".format(value,value,rotate,time_value)
img.save(img_path)
# %% 存放字体的路径
font_dir = "./fonts"
for font_name in os.listdir(font_dir):
# 把每种字体都取出来,每种字体都生成一批图片
path_font_file = os.path.join(font_dir, font_name)
# 倾斜角度从-10到10度,每个角度都生成一批图片
for k in range(-10, 10, 1):
# 每个字符都生成图片
makeImage(label_dict, path_font_file, rotate = k)
上面纯代码不到30行,相信大家应该能看懂!核心代码就是画文字。draw.text((x,y), char, (255, 255, 255), font)
翻译一下就是:使用某字体在黑底图片的(x,y)位置写白色的char符号。核心逻辑就是三层循环。如果代码你运行的没有问题,最终会生成如下结果:好了,数据准备好了。总共15个文件夹,每个文件夹下对应的各种字体各种倾斜角的字符图片3900个(字符15类×字体13种×角度20个),图片的大小是24×24像素。有了数据,我们就可以再进行下一步了,下一步是训练和使用数据。
GG
公交快到站了,我赶紧写了个图像样本采集器
小团队搞算法,需要多面手,等同当个厨子要从买菜到炒菜,一直到端给顾客,还要劝顾客多吃,以便好收拾盘子。据说,我的前辈曾是大厂的大牛,但是来了之后没出成绩,反而先要求配两个助手。后来,经过很长时间,也没有什么成绩,大牛说是助手不行,要换更高级的助手。我听到后,我说,我不是大牛,不用助手,我喜欢从琐碎的事情中寻找规律,并开发出工具来提高效率。图像处理,有三驾马车:硬件、样本、算法。其中,硬件舍得花钱买就行,算法基本上对成熟的模型进行微调即可。而这个样本,却需要实打实地进行收集和标注。比如做语义分割,你觉得,哇,真了不起,计算机那么智能,怎么就把各类物体区分出来了。你看下面这张图,传入一张小猫的自拍照,计算机就像是能读懂图像的语义一样,轻松地把小猫、草地、森林、天空,分割了出来。其实,它之所以智能,是因为前期喂了它很多数据。这就和养孩子一样,她叫你一声“爸爸”,你感觉好神奇,其实你仔细想想,你喂了她多少奶粉,你叫了她多少句“爸爸”。在计算机能识别物体之前,是用了成千上万张标注好的图片进行了训练。这些图片是一个点一个点地标记了哪些像素是猫,哪些像素是草地。而在自动驾驶领域,标注更为变态,你需要标上你看到的一切,细致到行人、非机动车、机动车、建筑物、绿植、电线、路牌、不可跨越的障碍物(如护栏)、可跨越的障碍物(如马路牙子)等等。所以,你看到现在很多人工智能的企业招聘一种职业,叫图像标注员(可兼职,一天200)。其实,这活不好干。市面上有很多图像样本处理工具,有做框选的labelimg,也有做标注的labelme,甚至还有人说用Photoshop。是的,我就用过PS,是在要识别一些目标上,比如行人。需要的素材就是包含行人的图片5000张,以及不包含行人的图片1000张,这些图片要求宽高尺寸都一样。首先,用PS打开下面的图片,然后使用裁剪工具,框出固定大小或者比例的行人,然后保存。以此类推。这样效率很低。做了50张,我就累了。框图不累,但是输入文件名点击保存按钮等各种操作很累。于是,我就写了下面这个工具。它会加载文件夹下的素材图片。你只需要在图片上拉一个框出来,它就会保存这个框对应的图片。而且,起初保存的是1开头的文件名,点击鼠标右键后,保存的是0开头的文件名。这样,就可以做到先框选行人,然后再框选背景,大大地提高了效率。以下是实现的代码,100行代码,相信结合注释,你能轻松读懂。#%% 导入相关包
import os
import tkinter as tk
from PIL import Image
from PIL import ImageTk
import time
# 鼠标左键按下事件
def left_mouse_down(event):
global left_mouse_down_x, left_mouse_down_y
# 记录按下的坐标,赋值给全局变量
left_mouse_down_x = event.x
left_mouse_down_y = event.y
# 鼠标左键抬起事件
def left_mouse_up(event):
# 记录抬起时的坐标,鼠标左键抬起时x,y坐标
left_mouse_up_x = event.x
left_mouse_up_y = event.y
# 通过抬起的点减去按下的点,比划矩形,计算出宽和高
width = left_mouse_up_x - left_mouse_down_x
height = left_mouse_up_y - left_mouse_down_y
# 如果宽高太小,有可能是点击了一下,或者想放弃这次操作
if width < 20 or height < 20:
print("size is to small,不要了")
return
# 如果宽大于高,让高依照比例自动计算:谁幅度大听谁的
if width > height:
_height = int(width*h_scale/w_scale)
# 强行定义鼠标抬起的位置
left_mouse_up_y = left_mouse_down_y + _height
else: # 宽不大于高,一样操作
_width = int(height*w_scale/h_scale)
left_mouse_up_x = left_mouse_down_x + _width
# 保存文件
f_name = "out/"+str(crop_pos)+"_"+str(int(time.time()))+".png"
corp_image = image.crop((left_mouse_down_x, left_mouse_down_y, left_mouse_up_x, left_mouse_up_y))
corp_image.save(f_name)
# 鼠标左键按下并移动
def moving_mouse(event):
global sole_rectangle # 绘制的矩形
# 鼠标按下的x,y
global left_mouse_down_x, left_mouse_down_y
moving_mouse_x = event.x
moving_mouse_y = event.y
# 通过移动的点减去起始按下的点,比划矩形,计算出宽和高
width = moving_mouse_x - left_mouse_down_x
height = moving_mouse_y - left_mouse_down_y
if width > height:
# 如果宽大于高,让高依照比例自动计算:谁幅度大听谁的
_height = int(width*h_scale/w_scale)
# 强行定义鼠标移动的位置
moving_mouse_y = left_mouse_down_y + _height
else:
_width = int(height*w_scale/h_scale)
moving_mouse_x = left_mouse_down_x + _width
# 如果原来画过矩形,删除前一个矩形,绘制出新的
if sole_rectangle is not None:
canvas.delete(sole_rectangle)
sole_rectangle = canvas.create_rectangle(left_mouse_down_x, left_mouse_down_y, moving_mouse_x,moving_mouse_y, outline='red')
# 鼠标右键按下
def right_mouse_down(event):
pass
# 鼠标右键抬起
def right_mouse_up(event):
global crop_pos
crop_pos = 0
#%% 执行代码
if __name__ == '__main__':
# 鼠标左键按下时x,y坐标
left_mouse_down_x, left_mouse_down_y = 0, 0
sole_rectangle = None # 画出的矩形
w_scale = 1 # 画出的宽高比例
h_scale = 3
target = "img"
all_files=os.listdir(target)
for f_name in all_files:
crop_pos = 1
img_path = target+"/"+f_name
win = tk.Tk()
frame = tk.Frame()
frame.pack()
button = tk.Button(frame, text = "下一张", command=win.destroy)
button.pack()
image = Image.open(img_path)
image_x, image_y = image.size
img = ImageTk.PhotoImage(image)
canvas = tk.Canvas(frame, width=image_x, height=image_y, bg='white')
i = canvas.create_image(0, 0, anchor='nw', image=img)
canvas.pack()
canvas.bind('<Button-1>', left_mouse_down) # 鼠标左键按下
canvas.bind('<ButtonRelease-1>', left_mouse_up) # 鼠标左键释放
canvas.bind('<Button-3>', right_mouse_down) # 鼠标右键按下
canvas.bind('<ButtonRelease-3>', right_mouse_up) # 鼠标右键释放
canvas.bind('<B1-Motion>', moving_mouse) # 鼠标左键按下并移动
win.mainloop()
其逻辑有几点:鼠标按下时记录起点(x1,y1)坐标,鼠标抬起时记录终点(x2,y2)坐标,对图片中起点和终点两个点形成的矩形进行裁剪保存。文件名添加前缀crop_pos的数值,当点击右键时,把crop_pos的值改为0。这样就保存了0开头的文件名。定义图像的宽高比,当鼠标拖动时,依照比例强行画出符合宽高比的矩形框,类似于PS中按着Shift+Alt等比例缩放。还有一个小彩蛋,那就是当你下手选择图像之后,发现此位置无法裁出符合要求的图,想反悔的时候,把框缩小,此时会放弃这张图,不会保存。
GG
没啥才艺,30行代码写了个春联数据爬虫
1. 缘起没错,我一直探索用高新技术来激活传统文化,用传统文化来滋养高新技术。这不,我打算写一个基于TensorFlow的自动对春联的程序。经过尝试,目前已经完成了。Input是人工输入的上联,Output是机器自动给出的下联。Input: <start> 神 州 万 里 春 光 美 <end> [2, 61, 27, 26, 43, 4, 20, 78, 3]
Output:<start> 祖 国 两 制 好 事 兴 <end> [2, 138, 11, 120, 428, 73, 64, 46, 3]
Input: <start> 爆 竹 迎 新 春 <end> [2, 167, 108, 23, 9, 4, 3]
Output: <start> 瑞 雪 兆 丰 年 <end> [2, 92, 90, 290, 30, 8, 3]
Input: <start> 金 牛 送 寒 去 <end> [2, 63, 137, 183, 302, 101, 3]
Output: <start> 玉 鼠 喜 春 来 <end> [2, 126, 312, 17, 4, 26, 3]
Input: <start> 锦 绣 花 似 锦 <end> [2, 68, 117, 8, 185, 68, 3]
Output: <start> 缤 纷 春 如 风 <end> [2, 1651, 744, 4, 140, 7, 3]
Input: <start> 春 风 送 暖 山 河 好 <end> [2, 4, 5, 183, 60, 7, 71, 45, 3]
Output: <start> 瑞 雪 迎 春 世 纪 新 <end> [2, 92, 90, 27, 4, 36, 99, 5, 3]
Input: <start> 百 花 争 艳 春 风 得 意 <end> [2, 48, 8, 164, 76, 4, 5, 197, 50, 3]
Output: <start> 万 马 奔 腾 喜 气 福 多 <end> [2, 6, 28, 167, 58, 17, 33, 15, 113, 3]
2. 实现人工智能的背后是大量数据的训练,而仅仅这些训练数据就让人很为难:找不到啊。网络上有一个开源的对对联项目,里面有一个70万条的对联数据集,项目地址如下:GitHub - wb14123/couplet-dataset: Dataset for couplets. 70万条对联数据库。但是,我并不满意,因为我要的是春联,不是对联。对联虽然包含了春联,但是春联是带有民俗气息的,里面充满了喜庆祥和的味道。于是我在网络上找到了一个春联网站 www.duiduilian.com/chunlian/ ,里面的内容质量不错。于是,我就写了个爬虫程序去采集数据。2.1 分析元素打开网址,浏览器按F12分析网页。分析可见,我们关注的主体内容都在<div class="content_zw"></div>之间,而且一幅对联用<p></p>包裹,上下联用,分割。这是极其标准的数据爬取的素材案例,或许这就是为了教学而设计的。如果我们要获取春联内容的话,只需要通过网址加载下来html代码,然后取出正文部分,然后通过<p>标签分组,每副春联通过逗号“,”分上下联,最后存入文件就行了。开干。2.2 取出正文首先加载网址,取出我们关注的正文。import requests
import re
# 模拟浏览器发送http请求
response = requests.get(url)
# 设置编码方式
response.encoding='gbk'
# 获取整个网页html
html = response.text
# 根据标签分析,从html中获取正文
allText = re.findall(r'<div class="content_zw">.*?</div>', html, re.S)[0]
print(url+"\n allText:"+allText)
其中值得一讲的就是re是正则表达式的支持库,上面的re.findall就是从html文本中找到所有形状类似于<div class="content_zw">乱七八糟什么内容都行</div>的内容。因为可能会找到多个,但是此处场景只有一个,所以取第一个[0]就是我们想要的。这样,我们就拿到了如下内容:<div class="content_zw">
<p>春来眼际,喜上眉梢</p>
<p>春光普照,福气长临</p>
<p>春和景明,物阜年丰</p>
<p>春降大地,福满人间</p>
<p>春明花艳,民富国强</p>
……
</div>
2.3 拆出对联从目标html文本中,选出对联。text = "<div class='content_zw'><p>春来眼际,喜上眉梢</p><p>春光普照,福气长临</p></div>"
couplets = re.findall(r'<p>(.*?)</p>',text)
print(couplets) # ['春来眼际,喜上眉梢', '春光普照,福气长临']
for couplet in couplets:
cs = couplet.split(",")
print("上联:",cs[0], ",下联:",cs[1])
# 上联: 春来眼际 ,下联: 喜上眉梢 上联: 春光普照 ,下联: 福气长临
这里面依然用到了re.findall。这个方法是爬虫程序中很常用的方法。所谓爬取数据,其实就是拿到全量文本,然后撕下来你感兴趣的一段文本。如何来撕,就靠re配合一系列规则来实现。re.findall(r'<p>(.*?)</p>',text)指的是从text中,选取出<p>乱七八糟什么内容都行</p>形状的括号内的内容。这里值得一说的是,它只要()里面的内容。举个例子:text = "<p>春来眼际,喜上眉梢</p>"
text_r1 = re.findall(r'<p>(.*?)</p>',text)[0]
print(text_r1) # 春来眼际,喜上眉梢
text_r2 = re.findall(r'<p>.*?</p>',text)[0]
print(text_r2) # <p>春来眼际,喜上眉梢</p>
看上面的两个列子,就可以理解带不带括号的区别了。获取到了couplets其实是一个数组['春来眼际,喜上眉梢', '春光普照,福气长临']。然后,再循环这个数组,通过split(",")拆分出上下联。这样你就有了上下联的春联,然后你就可以为所欲为了。2.4 还有分页上面只是说了一个url页面。但是,实际上,有好多并列的页面。第一个入口页面我们可以手动输进去,但是其他页面你得自动一些了吧。下面是分页部分的html代码分析:我们F12调试可以看到,分页部分和春联内容一样,也在div.content_zw内部,它的整体标签是<div id="pages"></div>包裹的。<div id="pages">
<a class="a1" href="/chunlian/4zi.html">上一页</a>
<span>1</span>
<a href="/chunlian/4zi_2.html">2</a>
<a href="/chunlian/4zi_3.html">3</a>
……
<a href="/chunlian/4zi_8.html">8</a>
<a class="a1" href="/chunlian/4zi_2.html">下一页</a>
</div>
其中a标签里面的href就是相对路径的url连接,我们尝试取一下。# 获取分页相关的网页html
pages = re.findall(r'<div id="pages">.*?</div>', allText, re.S)[0]
page_list = re.findall(r'href="/chunlian/(.*?)">(.*?)<',pages)
page_urls = []
for page in page_list:
if page[1] != '下一页':
page_url="https://www.duiduilian.com/chunlian/%s" % page[0]
page_urls.append(page_url)
# page_urls 就是所有链接
相信通过之前的说明,这里大多数代码你已经能看明白了。这和获取p标签里的对联很像,区别就是这里面有2个()。我们打印一下匹配出来的page_list:page_list: [('4zi.html', '上一页'), ('4zi_2.html', '2'), ('4zi_3.html', '3')
, ('4zi_4.html', '4'), ('4zi_5.html', '5'), ('4zi_6.html', '6')
, ('4zi_7.html', '7'), ('4zi_8.html', '8'), ('4zi_2.html', '下一页')]
原来,re.findall(r'href="/chunlian/(.*?)">(.*?)<',pages)意思就是,要取2处地方,分别是……/chunlian/(这个位置1)">(这个位置2)<……。除了“下一个”按钮之外,其他的<a>链接正好就是1~8页的完整地址,这样我们就全获取到了。2.5 全部代码好了,分页也搞定了,那所有链接就有了,每一个链接如何撕下来春联句子也就有了,下面是全部代码。import requests
import re
def getContent(url):
response = requests.get(url)
response.encoding='gbk'
html = response.text
allText = re.findall(r'<div class="content_zw">.*?</div>', html, re.S)[0]
return allText
def getPageUrl(allText):
pages = re.findall(r'<div id="pages">.*?</div>', allText, re.S)[0]
page_list = re.findall(r'href="/chunlian/(.*?)">(.*?)<',pages)
page_urls = []
for page in page_list:
if page[1] != '下一页':
page_url="https://www.duiduilian.com/chunlian/%s" % page[0]
print("page_url:",page_url)
page_urls.append(page_url)
return page_urls
def do(url, file_name):
c_text = getContent(url)
pages = getPageUrl(c_text)
f = open(file_name,'w')
for page_url in pages:
page_text = getContent(page_url)
page_couplets = re.findall(r'<p>(.*?)</p>',page_text)
str = '\n'.join(page_couplets)+'\n'
f.write(str)
f.close()
url = 'https://www.duiduilian.com/chunlian/4zi.html'
file_name = 'blog4.txt'
do(url, file_name)
我故意省掉了注释,因为我想说,其实这个功能只有30行代码。最终,它把获取到的数据存到名字为blog4.txt文件中了。这是4字的春联,还有5字的,6字的,7字的,可以如法炮制。3. 理想和现实看完上面的内容,你可以去吃饭了,因为你已经掌握了温室大棚里的生存技能。吃完饭后,我告诉你,上面30行代码的实现,其实是理想情况,现实是不可能是这样的。实际上还有很多异常情况。比如,下面这个,春联正文的<p>标签里面有我们想要的,也有我们不想要的,都拿过来肯定用不了。再看下面这个,春联正文里就没有<p>标签,那你想办法吧。再看下面这个,当分页过多时,出现了省略号,有些页码的链接就不全了,有些数据就取不到了。最后,再看下面这个,分页是列表形状的,你还用原来的方法就无法适配了。是吧,教程和实战还是有区别的。教程,越干净越好,要用最短的距离来讲述一个知识点,干扰项越少越好。实战,越真实越好,要用最全面的考虑来设计一项功能,异常项越多越好。我这篇文章就是讲一些片面知识点的教程,主要目的是做科普工作。不足之处,还希望大家海涵。
GG
ChatGPT火了,我连夜详解AIGC原理,并实战生成动漫头像
一、AIGC:人工智能的新时代AIGC可能会是人工智能的下一个时代。尽管很多人还不知道AIGC是什么。当还有大批人宣扬所谓人工智能、元宇宙都是概念,并且捂紧了口袋里的两百块钱的时候,人工智能行业发生了几件小事。首先,由人工智能生成的一幅油画作品《太空歌剧院》,获得了艺术博览会的冠军。有人感觉这有什么?各种比赛多了去了,不就是获个奖吗?可是这次不一样,这是一幅油画作品。在此之前,好的油画只能由人工绘制。但是现在人工智能也可以绘制了,而且还拿了冠军。很多人类艺术家仰天长叹:“祖师爷啊,我这代人,在目睹艺术死亡!”上一次艺术家们发出这样的感慨,还是1839年,那时照相机问世了。随后,ChatGPT横空出世。它真正做到了和人类“对答如流”。它也可以做数学题、创作诗歌、写小说,甚至也能写代码、改bug。再说一个震惊的报道:由ChatGPT生成的论文,拿下了全班的最高分。导师找到学生,说他上交的论文,段落简洁、举例恰当、论据严谨,甚至引经据典,古今中外,无所不通,教授不敢相信。学生瑟瑟发抖,他说,这是AI生成的,我只是想应付一下作业。另外,美国89%的大学生都在用ChatGPT做作业。以色列总统在周三发表了一个演讲,内容也是由人工智能写的。现在全球都在讨论,这类人工智能技术,看似是带来了巨大的商业价值,实则可能会给人类带来严重的打击。这项技术就是AIGC(AI-Generated Content),翻译成中文就是:人工智能生成内容。二、AIGC实战:智能生成动漫头像其实,利用人工智能生成内容资源,很早就有了。记得有一年的双十一购物节,上万商家的广告图就是人工智能生成的。只是现在的数据、算法、硬件,这三个条件跟上了,这才让它大放异彩,全民可用。下面,我就以人工智能生成动漫头像为例,采用TensorFlow框架,从头到尾给大家讲一下AIGC的全过程。从原理到实现都很详细,自己搭建,不调API,最后还带项目源码的那种。2.1 自动生成的意义那位问了,自动生成内容有什么好处?我的天啊,省事省力省钱呐!下图是一个游戏中的海洋怪物。这便是人工智能生成的。这个大型游戏叫《无人深空(No Man's Sky)》。号称有1840亿颗不同的星球,每个星球都有形态各异的怪物。这游戏玩着得多爽啊?简直就是视觉震撼呐。这些怪物要是人工来做,得招聘多少团队,得花费多少时间?用人工智能生成的话,你可以像去网吧一样,跟老板说:嗨,多开几台机子!当然,下面我要做的,没有上面那样地绚丽,甚至很原始。但是过程类似,原理一致。效果就是AI生成动漫头像:2.2 自动生成的原理AIGC的原理,用中国古话可以一语概括,那就是:读书破万卷,下笔如有神。以生成猫咪的照片来举例子,基本上AIGC的套路是下面这样的:首先,程序会设计两个角色。一个叫生成器,一个叫鉴别器。为了便于理解,我们称呼生成器为艺术家,称鉴别器为评论家。艺术家负责生产内容,也就是画猫。不要觉得拥有艺术家头衔就很了不起,他可能和你一样,画不好。但是,就算乱画,也得画。于是,他就画啊画啊画。评论家呢,相比艺术家就负责一些了。他首先调研了大量猫的照片。他知道了猫的特点,有俩眼睛,有斑纹,有胡须。这些特征,他门儿清。下面有意思的就来了。艺术家这时还啥也不懂,随便画一笔,然后交给评论家,说画好了。评论家拿旁光一看,瞬间就给否了。还给出一些意见,比如连轮廓都没有。艺术家一听,你要轮廓那我就画个轮廓。他加了个轮廓,又交了上去。评论家正眼一看,又给否了。不过,他还是给出一些意见,比如没有胡须。就这样,这俩人经过成千上万次的友好磋商(评论家幸好是机器,不然心态崩了)。到后来,艺术家再拿来画作,评论家会看好久,甚至拿出之前的照片挨个对照。最后他甚至还想诈一下艺术家,说你这是假的,艺术家说这次是真的。这时,评论家说好吧,我确实找不出问题了,我看也是真的。至此,剧终。搞一个造假的,再搞一个验假的。然后训练。随着训练加深,生成器在生成逼真图像方面逐渐变强,而鉴别器在辨别真伪上逐渐变强。当鉴别器无法区分真实图片和伪造图片时,训练过程达到平衡。上面这一套操作叫“生成对抗网络(Generative Adversarial Networks)”,简称叫GAN。我感觉,这套流程有点损,叫“干”没毛病。2.3 数据准备鉴别器是需要学习资料学习的。因此,我准备了20000张这样的动漫头像。这些数据来自公开数据集Anime-Face-Dataset。数据文件不大,274MB。你很容易就可以下载下来。这里面有60000多张图片。我用我的电脑训练了一下。200分钟过去了,一个epoch(把这些数据走一遍)都还没有结束。那……稍微有效果得半个月之后了。乡亲们,我这里是AI小作坊,干不了大的。于是乎,我就取了20000张图片,并且将尺寸缩小到56×56像素,再并且将彩色改为黑白。这样一来,效率马上就提高了。2分钟就可以训练一圈。如此,我训练500圈也就是不到一天的时间。这是可以承受的。上面处理图片的代码:import cv2
# 存放源图片的文件夹
dir_path = "anime"
all_files=os.listdir(dir_path)
# 循环里面的每一个文件
for j,res_f_name in enumerate(all_files):
res_f_path = dir_path+"/"+res_f_name
# 读入单通道
img1 = cv2.imread(res_f_path, 0)
# 重新定义尺寸为56
img2=cv2.resize(img1,(56,56),interpolation=cv2.INTER_NEAREST)
# 转存到face文件夹下
cv2.imwrite("face/"+res_f_name, img2)
# 超过20000退出循环
if j > 20000: break
相信加上注释后,还是通俗易懂的。文件准备好了。尽管维度降了,但看起来,这个辨识度还过得去。下一步要转为TensorFlow格式化的数据集。from PIL import Image
import pathlib
import numpy as np
# 将图片文件转为数组
dir_path = "face"
data_dir = pathlib.Path(dir_path)
imgs = list(data_dir.glob('*.jpg'))
img_arr = []
for img in imgs:
img = Image.open(str(img))
img_arr.append(np.array(img))
train_images = np.array(img_arr)
nums = train_images.shape[0]
train_images = train_images.reshape(nums, 56, 56, 1).astype('float32')
# 归一化
train_images = (train_images - 127.5) / 127.5
# 转为tensor格式
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(nums).batch(256)
我很想说一下数据形态的变化过程。因为这和后续的神经网络结构有关联。首先,我们的图片是56×56像素,单通道。所以,图片的数据数组img_arr的形状是(20000, 56, 56)。也就是说有20000组56×56的数组。这里面的数是int型的,取值为0到255,表示从纯黑到纯白。((20000, 56, 56),
array([[ 0, 0, 0, 0, 0, …… 0],
[ 18, 18, 126, 136, 175, …… 0],
[ 0, 0, 253, 253, 0, …… 0]], dtype=uint8))
然后用reshape做一个升维,并且用astype('float32')做一个浮点转化。升维的目的,是把每一个像素点单独提出来。因为每一个像素点都需要作为学习和判断的依据。浮点转化则是为了提高精确度。到这一步train_images的形状变为(20000, 56, 56, 1)。((20000, 56, 56, 1),
array([[ [0.], [0.], [0.], [0.], [0.], …… [0.]],
[ [18.], [18.], [126.], [136.], [175.], …… [0.]],
[ [0.], [0.], [253.], [253.], [0.], …… [0.]]], dtype=float32))
接着,进行一个神奇的操作。执行了(train_images-127.5)/127.5这一步。这一步是什么作用呢?我们知道,色值最大是255,那么他的一半就是127.5。可以看出来,上一步操作就是把数据的区间格式化到[-1,1]之间。如果你足够敏感的话,或许已经猜到。这是要使用tanh,也就是双曲正切作为激活函数。这个函数的输出范围也是在-1到1之间。也就是说,经过一系列计算,它最终会输出-1到1之间的数值。这个数值我们反向转化回去,也就是乘以127.5然后加上127.5,那就是AI生成像素的色值。2.4 生成器首先我们来建立一个生成器。用于生成动漫头像的图片。def make_generator_model():
model = tf.keras.Sequential()
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(160,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((7, 7, 256)))
assert model.output_shape == (None, 7, 7, 256)
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
assert model.output_shape == (None, 7, 7, 128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
……
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
assert model.output_shape == (None, 56, 56, 1)
return model
# 生成一个试试
generator = make_generator_model()
noise = tf.random.normal([1, 160])
generated_image = generator(noise, training=False)
因为我最终会放出全部源码,所以这个地方省略了几层相似的神经网络。从结构上看,输入层是大小为160的一维噪点数据。然后通过Conv2DTranspose实现上采样,一层传递一层,生成变化的图像。最终到输出层,通过tanh激活函数,输出56×56组数据。这将会是我们要的像素点。如果输出一下,生成器生成的图片。是下面这个样子。这没错,一开始生成的图像,就是随机的像素噪点。它只有一个确定项,那就是56×56像素的尺寸。这就可以了。它已经通过复杂的神经网络,生成图片了。这个生成器有脑细胞,但刚出生,啥也不懂。这就像是艺术家第一步能绘制线条了。如果想要画好猫,那就得找评论家多去沟通。2.5 鉴别器我们来建立一个鉴别器。用于判断一张动漫头像是不是真的。def make_discriminator_model():
model = tf.keras.Sequential()
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[56, 56, 1]))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Flatten())
model.add(layers.Dense(1))
return model
# 鉴别上一个生成的噪点图片generated_image试试
discriminator = make_discriminator_model()
decision = discriminator(generated_image)
我们来看一下这个模型。它的输入形状是(56, 56, 1)。也就是前期准备的数据集的形状。它的输出形状是(1),表示鉴别的结果。中间是两层卷积,用于把输入向输出聚拢。采用的是LeakyReLU激活函数。我们把生成器生成的那个噪点图,鉴别一下,看看啥效果。tf.Tensor([[0.00207942]], shape=(1, 1), dtype=float32)
看这个输出结果,数值极小,表示可能性极低。我们只是建立了一个空的模型。并没有训练。它这时就判断出了不是动漫头像。倒不是因为它智能,而是它看啥都是假的。它现在也是个小白。下面就该训练训练了。2.6 训练数据开练!GAN!cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)
……
@tf.function
def train_step(images):
noise = tf.random.normal([BATCH_SIZE, noise_dim])
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
for epoch in range(500):
for image_batch in dataset:
train_step(image_batch)
同样,我还是只放出了部分关键代码。不然影响你的阅读。最后我会开源这个项目,不要着急。我们来分析原理,一定要反复看,精彩和烧脑程度堪比《三国演义》。我连图片都不敢加,怕打断你的思绪。首先看损失函数。算法训练的一个途径,就是让损失函数的值越变越小。损失函数表示差距,预测的差距和实际差距缩小,表示预测变准。先看一下生成器的损失函数。位置在代码中的generator_loss部分。它返回两个数据之间的差距。第一个数是造假的结果fake_output,这个结果是鉴别器给的。另一个数据是标准的成功结果。随着训练的进行,算法框架会让这个函数的值往小了变。那其实就是让生成器预测出来的数据,同鉴别器判断出来的结果,两者之间的差距变得越来越小。这一番操作,也就是让框架留意,如果整体趋势是生成器欺骗鉴别器的能力增强,那就加分。再看鉴别器的损失函数。也就是代码中的discriminator_loss函数。它这里稍微复杂一些。我们看到它的值是real_loss加fake_loss,是两项损失值的总和。real_loss是real_output和标准答案的差距。fake_loss是fake_output和标准答案的差距。那这两个值又是怎么来的呢?得去train_step函数里看。real_output是鉴别器对训练数据的判断。fake_loss是鉴别器对生成器造假结果的判断。看到这里,我感叹人工智能的心机之重。它什么都要。随着大量学习资料的循环,它告诉人工智能框架,它要锻炼自己对现有学习材料鉴别的能力。如果自己猜对了学习资料,也就是那20000张动漫头像。请提醒我,我要调整自己的见识,修改内部参数。代码中定义的training=True,意思就是可随着训练自动调节参数。同时,伴着它学习现有资料的过程中,它还要实践。它还要去判断生成器是不是造假了。它也告诉框架,我要以我现在学到的鉴别能力,去判断那小子造的图假不假。因为人工智能要想办法让损失函数变小。因此得让fake_loss的值变小,才能保证discriminator_loss整体变小。于是,框架又去找生成器。告诉它,鉴别器又学习了一批新知识,现在人家识别造假的能力增强了。不过,我可以偷偷地告诉你,它学了这个还有那个。这么一来,生成器造假的本领,也增强了。如此循环往复。框架相当于一个“挑唆者”。一边让鉴别器提高鉴别能力,一边也告诉生成器如何实现更高级的造假。最终,世间所有的知识,两方全部都学到了。鉴别器再也没有新的知识可以学习。生成器的造假,鉴别器全部认可,也不需要再有新的造假方案。所有防伪知识全透明。这时AIGC就成功了。2.7 自动生成我对20000张动漫图片训练了500轮。每一轮都打印一个九宫格的大头贴。最终我们可以看到这500轮的演变效果。这张图大约25秒,只播放一遍(如果放完了,拖出来再看),需要耐心看。从动态图看,整体趋势是往画面更清晰的方向发展的。动图比较快,我放上一张静态图。这完全是由人工智能生成的图片。生成的代码很简单。# 加载训练模型
if os.path.exists(checkpoint_dir+"/checkpoint"):
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
# 生成噪点作为输入
test_input = tf.random.normal([1, 160])
# 交给生成器批量生成
predictions = generator(test_input, training=False)
# 取出一张结果
img_arr = predictions[0][:, :, 0]
# 将结果复原成图片像素色值数据
img_arr = img_arr* 127.5 + 127.5这是20000张图,500轮训练的效果。如果是百万张图片,几千轮训练呢?完全仿真很简单。三、我们对AIGC该有的态度AIGC的火爆出圈,引起全球的强烈讨论。很多地方甚至打算立法,禁止学生使用它做作业。虽然我说了这么多。可能直到现在,依然有人觉得这是噱头:我的工作这么高级,是有灵魂的工作,人工智能写文章能比我通顺?它还写代码?它懂逻辑吗?国外有一个IT老哥叫David Gewirtz。他从1982年开始就写代码,干了40多年,也在苹果公司待过。他以为用ChatGPT写代码不会有啥惊喜。直到出现结果,却吓了他一大跳。他的需求是给它老婆写一个网站的插件,用于挑选顾客,并滚动顾客的名字展示。这个需要几天完成的工作,ChatGPT很快就完成了。而且代码纯粹简洁,极其规范。它还告诉你该操作哪个文件,该如何部署。现阶段的人工智能,可能没有自己的思考,但是它有自己的计算。你会写文章,因为你读过300多本书,并且记住了里面20%的内容。这些让你引以为傲。但是人工智能,它读过人类历史上出现过的所有文献,只要硬盘够,它全部都能记住。而且它还不停对这些内容做分析、加工、整理:这里和这里有关联,这里和那里都是在介绍橙子的营养成分。它通过计算,让一切知识发生互联互通。当有人向人工智能表示人类的担忧时,人工智能也给出了自己的回答。我比较赞同它的观点。抱有其他观点的人,主要担心有了人工智能,人类就会变得不动脑子了。时间长就废了。我觉得,这些都是工具。相机出来的时候,也是被画家抵制,因为成像太简单了。现在想想,太简单有问题吗?没有!同样的还有计算器之于算盘,打字之于手写。甚至TensorFlow 2.0出来时,也被1.0的用户抵制。他们说开发太简单了,这让开发者根本接触不到底层。殊不知,1.0出来的时候,那些写汇编语言的开发者也想,他们堕落了,居然不操作寄存器。其实,我感觉这些担心是多余的。每个时代都有会属于自己时代的产物。就像现在我们不用毛笔写字了,但是我们的祖先也没有敲过键盘呀!可能下一个时代的人,连键盘也不敲了。
GG
知识点:TensorFlow训练模型的保存和恢复
1. 为什么要保存模型数据?人生重要的是积累,20岁到了什么程度,在此基础上30岁又达到什么境界,如此积累,不断进步。你有没有想过,你花半天时间背诵了一页《三字经》,吃了个午饭后,全忘了。于是,你加大投入,一天一夜背会了整篇《三字经》,结果睡了一觉后又全忘了。是的,这肯定很痛苦。同样,对于神经网络而言也一样。刚刚耗费了200个小时,认识了30万张狗狗的图片,并计算出了他们的特征,能够轻松分辨出哈士奇和狼,结果计算机一断电,它又空白了。这肯定不行。因此,训练的结果要及时保存,保存的结果可以随时恢复。当再次训练时,可以在上次成果的基础上继续累加。就像一个人一样,研究学问到80岁,已经是满腹经纶了。2. 训练结果的保存和恢复2.1 新HelloWorld:fashion_mnist我们拿人工智能的新HelloWorld来举例子。原来人工智能的入门例子是mnist手写数据集。后来改成fashion_mnist这个数据集了。这个数据集,主要是10个品类的时尚装饰。标签类0T恤/上衣1裤子2套头衫3连衣裙4外套5凉鞋6衬衫7运动鞋8包9短靴通过训练它们,让神经网络认识他们,从而遇到一张图可以说出是衬衫还是裤子。我们的重点主要是讲保存训练数据,所以训练相关的代码一笔带过。from os import times
from re import T
import tensorflow as tf
from tensorflow import keras
import numpy as np
import cv2
# 分类名称
class_names = ["T恤/上衣","裤子","套头衫","连衣裙","外套","凉鞋","衬衫","运动鞋","包","短靴"]
# 读取数据集-构建网络模型-进行训练
fashion_mnist = keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
train_images = train_images/255.0
test_images = test_images/255.0
# 构建模型
model = keras.Sequential([
keras.layers.Flatten(input_shape=(28,28)),
keras.layers.Dense(128, activation='relu'),
keras.layers.Dropout(0.2),
keras.layers.Dense(10, activation='softmax')
])
# 配置训练
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
# 开始训练
model.fit(train_images, train_labels, epochs=50)
运行后是这样的效果:Epoch 1/10
1875/1875 [======] - 1s 602us/step - loss: 0.5273 - accuracy: 0.8111
Epoch 2/10
1875/1875 [======] - 1s 594us/step - loss: 0.3994 - accuracy: 0.8554
Epoch 3/10
1875/1875 [======] - 1s 590us/step - loss: 0.3673 - accuracy: 0.8662
……
上面就训练完数据了。2.2 内存中的数据如果运行了上面的代码,那么其实两个关键数据(网络模型结构、训练的权重)就已经在内存中了,主要存在model中,因为刚刚就是训练的它,程序还没有关闭,它还是活的。这时候,可以找一个验证数据试一试,这个验证数据是程序从没有见过的,比如说下面的img_2.png这个裤子。通过代码识别一下:img =cv2.imread('./img/img_2.png',0)
img = img/255.0
img = np.expand_dims(img, 0)
p = model.predict(img)
print(p)
print(class_names[np.argmax(p[0])])
结果如下:[[6.5590651e-11 1.0000000e+00 2.8622449e-13 6.2133689e-09 2.7508923e-10
2.4884808e-19 7.4704088e-12 2.6084349e-25 3.8184945e-13 4.6570902e-23]]
裤子
上面输出了10个分类的可能性,其中第2个分类的可能性为100%,第2个分类索引为1。class_names = ["T恤/上衣","裤子","套头衫","连衣裙","外套","凉鞋","衬衫","运动鞋","包","短靴"]所以它是个裤子。保存在内存中,很不稳定,关闭程序就丢失了。如果想要使用,必须重新进行训练。2.3 模型保存为json、h5文件一个训练完成的神经网络,包含结构和权重两个部分。举个不恰当的例子,把下面这张图比喻成训练好的神经网络。其中的架子部分是每一层神经网络的结构。那么神经网络的权重,就表示里面存放的花盆等物品。如果想从另一个地方、另一个时间还原上面的这个角落,就需要记录两个东西:一个是容纳物品的框架,另一个是被摆放的物品。对应到神经网络就是结构和权重。神经网络的结构,可以通过json文件来存储。神经网络的权重,可以通过h5文件存储。保存起来非常简单,代码如下:# 保存训练的模型
model_json = model.to_json()
with open('./save/model.json', 'w') as file:
file.write(model_json)
# 保存训练的权重
model.save_weights('./save/model.h5')
当训练完成之后,所有的信息都保存在了model中,但此时只在内存里。通过model.to_json()以及model.save_weights可以把信息提取成数据持久化到文件里。2.4 读取json、h5文件恢复模型想使用的时候,也很简单,直接加载并使用就可以:# 读取训练的模型结果
with open('./save/model.json', 'r') as file:
model_json_from = file.read()
new_model = keras.models.model_from_json(model_json_from)
new_model.load_weights('./save/model.h5')
# 进行预测
img =cv2.imread('./img/img_2.png',0)
img = img/255.0
img = np.expand_dims(img, 0)
p = model.predict(img)
print(p)
print(class_names[np.argmax(p[0])])
输出的结果,也是:裤子。同上面2.2章节中预测不同的是,你不用先执行训练的代码了。这时,你可以新建一个文件单纯只做识别的任务,因为训练好的信息已经存储到json和h5里了,你读取出来使用就可以了。2.5 恢复模型继续训练如果某次你训练了2天数据,还没有结束,但是你要提着鸡蛋去走亲戚,因为没有人看护服务器,你需要先暂停。正好你看过上面的教程,先把模型保存成json和h5了。去到亲戚家,住了5天,临走前亲戚送你一些小鸡仔,并且又给你了一些训练数据。你回到家,需要继续训练。这时候怎么办?我们看一下。还是fashion_mnist的训练,我们训练了20轮,训练结果是这样的:Epoch 1/20
1875/1875 [======] - 1s 579us/step - loss: 0.5342 - accuracy: 0.8096
……
Epoch 20/20
1875/1875 [======] - 1s 560us/step - loss: 0.2347 - accuracy: 0.9102
训练集的正确率从0.80到0.91。假设我们中断了训练,并且依照2.3保存了模型。后来,我们又想继续训练。# 读取训练的模型结果
with open('./save/model.json', 'r') as file:
model_json_from = file.read()
new_model = keras.models.model_from_json(model_json_from)
new_model.load_weights('./save/model.h5')
# 训练模型
new_model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
new_model.fit(train_images, train_labels, epochs=10)
训练结果是这样的:Epoch 1/10
1875/1875 [======] - 1s 604us/step - loss: 0.2313 - accuracy: 0.9128
Epoch 2/10
1875/1875 [======] - 1s 602us/step - loss: 0.2275 - accuracy: 0.9136
……
我们看到,本次训练第1轮准确率就是0.91。这和上次训练结束时的准确率是对应的。这说明本次是在一定准确率的基础上继续训练的。2.6 保存最优权重上面讲了如何保存训练结果,那是保存最后训练的结果。悄悄问一句,最后一次训练就是最好的结果吗?未必!如果说最低点就是最好的训练结果,那么在某个区间内,最低点可能会被误判。所以,你保存的只是最后结果,并不是最优的结果。那么如何保存最优结果呢?filePath = 'best_weights.h5'
checkpoint = tf.keras.callbacks.ModelCheckpoint(filepath=filePath, monitor='val_accuracy', verbose=1, save_best_only=True, mode = 'max',varbose=1)
callback_list = [checkpoint]
model.fit(train_images, train_labels, validation_split=0.2 ,epochs = 30, batch_size = 64, verbose = 0, callbacks = callback_list)
打印结果如下:Epoch 00001: val_accuracy improved from -inf to 0.89317, saving model to best_weights.h5
Epoch 00002: val_accuracy improved from 0.89317 to 0.89617, saving model to best_weights.h5
Epoch 00003: val_accuracy did not improve from 0.89617
通过以上例子可以看出,当准确率有提高时,会把文件保存起来。即便是后面有最新的训练结果,但是准确率并没有提高,是不会保存的。如果你不想只保存最好的,也想了解它是如何一次改善的,可以调用如下代码:filePath = 'weights-improvement-{epoch:02d}-{val_accuracy:2f}.h5'
checkpoint = tf.keras.callbacks.ModelCheckpoint(filepath=filePath, monitor='val_accuracy', verbose=1, save_best_only=True, mode = 'max',varbose=1)
callback_list = [checkpoint]
model.fit(train_images, train_labels, validation_split=0.2 ,epochs = 30, batch_size = 64, verbose = 0, callbacks = callback_list)
打印结果如下:Epoch 00001: val_accuracy improved from -inf to 0.89417, saving model to weights-improvement-01-0.894167.h5
Epoch 00002: val_accuracy improved from 0.89417 to 0.89475, saving model to weights-improvement-02-0.894750.h5
Epoch 00003: val_accuracy did not improve from 0.89475
它会把准确率升高的每次都记录下来。有了上面的方法,训练数据就有了记忆。可以随时停,随时续,随时用。想想还有点小激动呢!
GG
知识点:详解激活函数
1. 激活函数的身影每当复制代码的时候,经常发现一个叫激活函数的东西,看下面的代码:model = Sequential([
Flatten(input_shape=(28,28)),
Dense(128, activation='relu'),
Dense(10, activation='softmax')
])
其中类似activation='relu'形状的,就是激活函数。那么问题来了,为什么要有它?他能干啥?2. 为什么需要激活函数?2.1 神经元的加权求和我们知道,人工智能的神经网络模仿的就是人类的神经元。看,它有好多触角,每个触角都能接收到不同的信息,他们对信息也都有自己的判断,最终汇总到一起,然后继续往下传递,最终到大脑,你做出个决策:快跑!单看某一个神经元,他的输入输出是这样的:Y = X1×W1+X2×W2+X3×W3。到神经网络里面,只不过变成多维矩阵加权而已。其实,这种工作方式是就是加权求和。加权求和这种方式,是一个线性模型,它有一个局限,那就是无论多少层叠加,他都是一个线性模型。线性模型解决不了非线性问题。2.2 谁给解释下什么叫线性多少篇文章,里面都说激活函数是为了去线性化。就这么一个“线性”,让多少小白折戟沉沙,从此再也不看人工智能。那么这个线性到底指什么?线性就是不拐弯,愣头青,没数。y = kx + b,随着变量变大,结果也变大。有多大变大多,有多小变多小,正负无穷。但是,实际应用中,我们要的结果并不是这样。举几个例子:数字识别场景,不管输入多少张图片,我们要的输出就是10个分类:0~9。电影评价情感分析,不管输入多少句评价,我们要的输出就是好评的概率。股票预测,不管是牛市还是熊市,不会是涨跌到无限大,只会逼近某一个值。现实生活中,很少有线性的事物,一个人再有钱,也是有数额的。都说头发无数根,其实也是有数量的。所以,神经网络要应用到生活中,就必须去线性。因此,就需要在神经网络的层上再套一个激活函数。可以来TensorFlow游乐场来试验下面的例子。一条线可以轻松地把两种样本分离开来。如果是下面的这种样本,一条直线是解决不了的。如果在加权求和上再包一层激活函数:y=function(kx+b),变成函数套函数,这样就不是线性模型了,可以很轻易地区分。把输入按照某种特定的规则映射到输出,这就是激活函数的作用。下面这个高尔顿板,相当于在自由下落外面加了一层激活函数,就很形象地说明了它所起的作用。如果换一种激活函数,输入都是一样,但是输出的分布却是另一种情况。所以,不同的激活函数会有不同的效果,下面我们看看都有哪些激活函数。3. 常见的激活函数3.1 Sigmoid S型函数sigmoid函数也叫Logistic函数,也是生物学中常见的一个函数,称为S型生长曲线。记得生物老师讲过,当环境差时,生物种群增长缓慢。随着环境越来越好,生物种群快速增长。当增长到一定数量后,受到相互竞争的压力,种群数量趋于稳定。因为它不是无限增长,另外它会将全部输入映射到0~1的区间内,所以在信息科学中,Sigmoid函数常被用作神经网络的激活函数,可以用来做二分法。比如:做文本情感分析《NLP基础-中文语义理解:豆瓣影评情感分析》时,最后一层就用了sigmoid作为激活函数。model = Sequential([
Embedding(vocab_size, embedding_dim, input_length= max_length),
GlobalAveragePooling1D(),
Dense(64, activation='relu'),
Dense(1, activation='sigmoid')
])
当输入一段文本后,最终输出1个数值,数值在0~1之间。0代表差评, 1代表好评。 "很好,演技不错", 0.91799414 ===>好评
"要是好就奇怪了", 0.19483969 ===>差评
"一星给字幕", 0.0028086603 ===>差评
"演技好,演技好,很差", 0.17192301 ===>差评
"演技好,演技好,演技好,演技好,很差" 0.8373259 ===>好评
3.2 tanh 双曲正切函数tanh是双曲正切函数,下面是它的公式和图像。它的形状和sigmoid有点像,只不过输出区间变成了-1到1。相比sigmoid,它很好地解决了以0为中心的问题。3.3 ReLU 线性整流函数ReLU称为线性整流函数(Rectified Linear Unit),又称修正线性单元。相比于sigmoid和tanh,线性整流函数有很多优势:更接近仿生学:大脑同一时间大概只有1%~4%的神经元处于活跃状态。使用relu函数可以实现对神经元活跃度的控制。计算过程更简单:没有其他函数的指数运算。同时,因为神经元的活跃度分散,使得计算成本下降。如果你构建神经网络时,不知道该用哪个激活函数,那就用ReLU。3.4 softmax 归一化指数函数softmax被称为归一化指数函数,它是在sigmoid二分类上进行的多分类推广,目的是将多分类的结果以概率的形式展现出来。softmax在业内一直有一个争议点:它算不算激活函数?从我这里看,它算式一个激活函数。Softmax主要解决多类别分类问题,解决只有唯一正确答案的问题,它以概率形式输出多个分类的可能性,输出是互斥的,所有输出的概率和接近1。比如手写数字识别,最后一层就是用的softmax:Dense(10, activation='softmax'),输出0~9这10个分类的概率。[[2.3691061e-11 1.0000000e+00 5.5736946e-14 1.7459076e-10 1.8988343e-13
8.0071365e-31 1.2010425e-14 0.0000000e+00 6.0655720e-20 1.8470960e-27]]
然后调用np.argmax(p)就可以得出概率列表中最大概率的索引是1。以上就是关于激活函数的知识点。
GG
NLP基础-中文语义理解:豆瓣影评情感分析
1. NLPNLP(Natural Language Processing)是指自然语言处理,他的目的是让计算机可以听懂人话。下面是我将2万条豆瓣影评训练之后,随意输入一段新影评交给神经网络,最终AI推断出的结果。 "很好,演技不错", 0.91799414 ===>好评
"要是好就奇怪了", 0.19483969 ===>差评
"一星给字幕", 0.0028086603 ===>差评
"演技好,演技好,很差", 0.17192301 ===>差评
"演技好,演技好,演技好,演技好,很差" 0.8373259 ===>好评
看完本篇文章,即可获得上述技能。2. 读取数据首先我们要找到待训练的数据集,我这里是一个csv文件,里面有从豆瓣上获取的影视评论50000条。他的格式是如下这样的:名称评分评论分类电影名1分到5分评论内容1 好评,0 差评部分数据是这样的:代码是这样的:# 导入包
import csv
import jieba
# 读取csv文件
csv_reader = csv.reader(open("datasets/douban_comments.csv"))
# 存储句子和标签
sentences = []
labels = []
# 循环读出每一行进行处理
i = 1
for row in csv_reader:
# 评论内容用结巴分词以空格分词
comments = jieba.cut(row[2])
comment = " ".join(comments)
sentences.append(comment)
# 存入标签,1好评,0差评
labels.append(int(row[3]))
i = i + 1
if i > 20000: break # 先取前2万条试验,取全部就注释
# 取出训练数据条数,分隔开测试数据条数
training_size = 16000
# 0到16000是训练数据
training_sentences = sentences[0:training_size]
training_labels = labels[0:training_size]
# 16000以后是测试数据
testing_sentences = sentences[training_size:]
testing_labels = labels[training_size:]
这里面做了几项工作:文件逐行读入,选取评论和标签字段。评论内容进行分词后存储。将数据切分为训练和测试两组。2.1 中文分词重点说一下分词。分词是中文特有的,英文不存在。下面是一个英文句子。This is an English sentence.请问这个句子,有几个词?有6个,因为每个词之间有空格,计算机可以轻易识别处理。ThisisanEnglishsentence.123456下面是一个中文句子。欢迎访问我的掘金博客。请问这个句子,有几个词?恐怕你得读几遍,然后结合生活阅历,才能分出来,而且还带着各类纠结。今天研究的重点不是分词,所以我们一笔带过,采用第三方的结巴分词实现。安装方法代码对 Python 2/3 均兼容全自动安装:easy_install jieba 或者 pip install jieba / pip3 install jieba半自动安装:先下载 pypi.python.org/pypi/jieba/ ,解压后运行 python setup.py install手动安装:下载代码文件将 jieba 目录放置于当前目录或者 site-packages 目录通过 import jieba 来引用引入之后,调用jieba.cut("欢迎访问我的掘金博客。")就可以分词了。import jieba
words = jieba.cut("欢迎访问我的掘金博客。")
sentence = " ".join(words)
print(sentence) # 欢迎 访问 我 的 掘金 博客 。
为什么要有分词?因为词语是语言的最小单位,理解了词语才能理解语言,才知道说了啥。对于中文来说,同一个的词语在不同语境下,分词方法不一样。关注下面的“北京大学”:import jieba
sentence = " ".join(jieba.cut("欢迎来北京大学餐厅"))
print(sentence) # 欢迎 来 北京大学 餐厅
sentence2 = " ".join(jieba.cut("欢迎来北京大学生志愿者中心"))
print(sentence2) # 欢迎 来 北京 大学生 志愿者 中心
所以,中文的自然语言处理难就难在分词。至此,我们的产物是如下格式:sentences = ['我 喜欢 你','我 不 喜欢 他',……]
labels = [0,1,……]
3. 文本序列化文本,其实计算机是无法直接认识文本的,它只认识0和1。你之所以能看到这些文字、图片,是因为经过了多次转化。就拿字母A来说,我们用65表示,转为二进制是0100 0001。二进制十进制缩写/字符解释0100 000165A大写字母A0100 001066B大写字母B0100 001167C大写字母C0100 010068D大写字母D0100 010169E大写字母E当你看到A、B、C时,其实到了计算机那里是0100 0001、0100 0010、0100 0011,它喜欢数字。Tips:这就是为什么当你比较字母大小是发现 A<B ,其实本质上是65<66。那么,我们的准备好的文本也需要转换为数字,这样更便于计算。3.1 fit_on_texts 分类有一个类叫Tokenizer,它是分词器,用于给文本分类和序列化。这里的分词器和上面我们说的中文分词不同,因为编程语言是老外发明的,人家不用特意分词,他起名叫分词器,就是给词语分类。from tensorflow.keras.preprocessing.text import Tokenizer
sentences = ['我 喜欢 你','我 不 喜欢 他']
# 定义分词器
tokenizer = Tokenizer()
# 分词器处理文本,
tokenizer.fit_on_texts(sentences)
print(tokenizer.word_index) # {'我': 1, '喜欢': 2, '你': 3, '不': 4, '他': 5}
上面做的就是找文本里有几类词语,并编上号。看输出结果知道:2句话最终抽出5种不同的词语,编号1~5。3.2 texts_to_sequences 文本变序列文本里所有的词语都有了编号,那么就可以用数字表示文本了。# 文本转化为数字序列
sequences = tokenizer.texts_to_sequences(sentences)
print(sequences) # [[1, 2, 3], [1, 4, 2, 5]]
这样,计算机渐渐露出了笑容。3.3 pad_sequences 填充序列虽然给它提供了数字,但这不是标准的,有长有短,计算机就是流水线,只吃统一标准的数据。pad_sequences 会把序列处理成统一的长度,默认选择里面最长的一条,不够的补0。from tensorflow.keras.preprocessing.sequence import pad_sequences
# padding='post' 后边填充, padding='pre'前面填充
padded = pad_sequences(sequences, padding='post')
print(padded) # [[1 2 3] [1 4 2 5]] -> [[1 2 3 0] [1 4 2 5]]
这样,长度都是一样了,计算机露出了开心的笑容。少了可以补充,但是如果太长怎么办呢?太长可以裁剪。# truncating='post' 裁剪后边, truncating='pre'裁剪前面
padded = pad_sequences(sequences, maxlen = 3,truncating='pre')
print(padded) # [[1, 2, 3], [1, 4, 2, 5]] -> [[1 2 3] [4 2 5]]
至此,我们的产物是这样的格式:sentences = [[1 2 3 0] [1 4 2 5]]
labels = [0,1,……]
4. 构建模型所谓模型,就是流水线设备。我们先来看一下流水线是什么感觉。看完了吧,流水线的作用就是进来固定格式的原料,经过一层一层的处理,最终出去固定格式的成品。模型也是这样,定义一层层的“设备”,配置好流程中的各项“指标”,等待上线生产。# 构建模型,定义各个层
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length= max_length),
tf.keras.layers.GlobalAveragePooling1D(),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
# 配置训练方法 loss=损失函数 optimizer=优化器 metrics=["准确率”]
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
4.1 Sequential 序列你可以理解为整条流水线,里面包含各类设备(层)。4.2 Embedding 嵌入层嵌入层,从字面意思我们就可以感受到这个层的气势。嵌入,就是插了很多个维度。一个词语用多个维度来表示。下面说维度。二维的是这样的(长,宽): 三维是这样的(长,宽,高):100维是什么样的,你能想象出来吗?除非物理学家,否则三维以上很难用空间来描述。但是,数据是很好体现的。性别,职位,年龄,身高,肤色,这一下就是5维了,1000维是不是也能找到。对于一个词,也是可以嵌入很多维度的。有了维度上的数值,我们就可以理解词语的轻重程度,可以计算词语间的关系。如果我们给颜色设置R、B、G 3个维度:颜色RGB红色25500绿色02550蓝色00255黄色2552550白色255255255黑色000下面见证一下奇迹,懂色彩学的都知道,红色和绿色掺在一起是什么颜色?来,跟我一起读:红色+绿色=黄色。到数字上就是:[255,0,0]+[0,255,0] = [255,255,0]这样,颜色的明暗程度,颜色间的关系,计算机就可以通过计算得出了。只要标记的合理,其实计算机能够算出:国王+女性=女王、精彩=-糟糕,开心>微笑。那你说,计算机是不是理解词语意思了,它不像你是感性理解,它全是数值计算。嵌入层就是给词语标记合理的维度。我们看一下嵌入层的定义:Embedding(vocab_size, embedding_dim, input_length)vocab_size:字典大小。有多少类词语。embedding_dim:本层的输出大小。一个词用多少维表示。input_length:输入数据的维数。一句话有多少个词语,一般是max_length(训练集的最大长度)。4.3 GlobalAveragePooling1D 全局平均池化为一维主要就是降维。我们最终只要一维的一个结果,就是好评或者差评,但是现在维度太多,需要降维。4.4 Dense这个也是降维,Dense(64, activation='relu')降到Dense(1, activation='sigmoid'),最终输出一个结果,就像前面流水线输入面粉、水、肉、菜等多种原材料,最终出来的是包子。4.5 activation 激活函数activation是激活函数,它的主要作用是提供网络的非线性建模能力。所谓线性问题就是可以用一条线能解决的问题。可以来TensorFlow游乐场来试验。如果是采用线性的思维,神经网络很快就能区分开这两种样本。但如果是下面的这种样本,画一条直线是解决不了的。如果是用relu激活函数,就可以很轻易区分。这就是激活函数的作用。常用的有如下几个,下面有它的函数和图形。我们用到了relu和sigmoid。relu:线性整流函数(Rectified Linear Unit),最常用的激活函数。sigmoid:也叫Logistic函数,它可以将一个实数映射到(0,1)的区间。Dense(1, activation='sigmoid')最后一个Dense我们就采用了sigmoid,因为我们的数据集中0是差评,1是好评,我们期望模型的输出结果数值也在0到1之间,这样我们就可以判断是更接近好评还是差评了。4. 训练模型4.1 fit 训练训练模型就相当于启动了流水线机器,传入训练数据和验证数据,调用fit方法就可以训练了。model.fit(training_padded, training_labels, epochs=num_epochs,
validation_data=(testing_padded, testing_labels), verbose=2)
# 保存训练集结果
model.save_weights('checkpoint/checkpoint')
启动后,日志打印是这样的:Epoch 1/10 500/500 - 61s - loss: 0.6088 - accuracy: 0.6648 - val_loss: 0.5582 - val_accuracy: 0.7275
Epoch 2/10 500/500 - 60s - loss: 0.4156 - accuracy: 0.8130 - val_loss: 0.5656 - val_accuracy: 0.7222
Epoch 3/10 500/500 - 60s - loss: 0.2820 - accuracy: 0.8823 - val_loss: 0.6518 - val_accuracy: 0.7057
经过训练,神经网络会根据输入和输出自动调节参数,包括确定词语的具体维度,以及维度的数值取多少。这个过程变为黑盒了,这也是人工智能和传统程序设计不同的地方。最后,调用save_weights可以把结果保存下来。5. 自动分析结果5.1 predict 预测sentences = [
"很好,演技不错",
"要是好就奇怪了",
"一星给字幕",
"演技好,演技好,很差",
"演技好,演技好,演技好,演技好,很差"
]
# 分词处理
v_len = len(sentences)
for i in range(v_len):
sentences[i] = " ".join(jieba.cut(sentences[i]) )
# 序列化
sequences = tokenizer.texts_to_sequences(sentences)
# 填充为标准长度
padded = pad_sequences(sequences, maxlen= max_length, padding='post', truncating='post')
# 预测
predicts = model.predict(np.array(padded))
# 打印结果
for i in range(len(sentences)):
print(sentences[i], predicts[i][0],'===>好评' if predicts[i][0] > 0.5 else '===>差评')
model.predict()会返回预测值,这不是个分类值,是个回归值(也可以做到分类值,比如输出1或者0,但是我们更想观察0.51和0.49有啥区别)。我们假设0.5是分界值,以上是好评,以下是差评。最终打印出结果:很好,演技不错 0.93863165 ===>好评
要是好就奇怪了 0.32386222 ===>差评
一星给字幕 0.0030411482 ===>差评
演技好,演技好,很差 0.21595979 ===>差评
演技好,演技好,演技好,演技好,很差 0.71479297 ===>好评
本文阅读对象为初级人员,为了便于理解,特意省略了部分细节,展现的知识点较为浅薄,旨在介绍流程和原理,仅做入门用。
GG
CNN基础识别-想为女儿批作业(二):卷积的使用
一、亮出效果最近在线教育行业遭遇一点小波折,一些搜题、智能批改类的功能要下线。退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介: 作对了,能打对号;做错了,能打叉号;没做的,能补上答案。醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。二、实现步骤今天主要讲如何训练和使用数据。往期回顾2.1 准备数据 2.1.1 准备字体2.1.2 生成图片2.2 训练数据2.2.1 构建模型你先看代码,外行感觉好深奥,内行偷偷地笑。# %% 导入必要的包
import tensorflow as tf
import numpy as np
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
import pathlib
import cv2
# %% 构建模型
def create_model():
model = Sequential([
layers.experimental.preprocessing.Rescaling(1./255, input_shape=(24, 24, 1)),
layers.Conv2D(24,3,activation='relu'),
layers.MaxPooling2D((2,2)),
layers.Conv2D(64,3, activation='relu'),
layers.MaxPooling2D((2,2)),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(15)]
)
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
return model
这个模型的序列是下面这样的,作用是输入一个图片数据,经过各个层揉搓,最终预测出这个图片属于哪个分类。这么多层都是干什么的,有什么用?和衣服一样,肯定是有用的,内衣、衬衣、毛衣、棉衣各有各的用处。2.2.2 卷积层 Conv2D各个职能部门的调查员,搜集和整理某单位区域内的特定数据。 我们输入的是一个图像,它是由像素组成的,这就是Rescaling(1./255,inputshape=(24,24,1))Rescaling(1./255, input_shape=(24, 24, 1))Rescaling(1./255,inputshape=(24,24,1))中,input_shape输入形状是24*24像素1个通道(彩色是RGB 3个通道)的图像。卷积层代码中的定义是Conv2D(24,3),意思是用3*3像素的卷积核,去提取24个特征。我把图转到地图上来,你就能理解了。以我大济南的市中区为例子卷积的作用就相当于从地图的某级单位区域中收集多组特定信息。比如以小区为单位去提取住宅数量、车位数量、学校数量、人口数、年收入、学历、年龄等等24个维度的信息。小区相当于卷积核。提取完成之后是这样的。卷积之后,我们从市中区得到N个小区的数据。卷积是可以进行多次的。比如在小区卷积之后,我们还可在小区的基础上再来一次卷积,在卷积就是街道了。通过再次以街道为单位卷积小区,我们就从市中区得到了N个街道的数据。这就是卷积的作用。通过一次次卷积,就把一张大图,通过特定的方法卷起来,最终留下来的是固定几组有目的数据,以此方便后续的评选决策。这是评选一个区的数据,要是评选济南市,甚至山东省,也是这么卷积。这和现实生活中评选文明城市、经济强省也是一个道理。2.2.3 池化层 MaxPooling2D说白了就是四舍五入。计算机的计算能力是强大的,比你我快,但也不是不用考虑成本。我们当然希望它越快越好,如果一个方法能省一半的时间,我们肯定愿意用这种方法。池化层干的就是这个事情。池化的代码定义是这样的MaxPooling2D((2,2))MaxPooling2D((2,2))MaxPooling2D((2,2)),这里是最大值池化。其中(2,2)是池化层的大小,其实就是在2*2的区域内,我们认为这一片可以合成一个单位。再以地图举个例子,比如下面的16个格子里的数据,是16个街道的学校数量。为了进一步提高计算效率,少计算一些数据,我们用2*2的池化层进行池化。池化的方格是4个街道合成1个,新单位学校数量取成员中学校数量最大(也有取最小,取平均多种池化)的那一个。池化之后,16个格子就变为了4个格子,从而减少了数据。这就是池化层的作用。2.2.4 全连接层 Dense弱水三千,只取一瓢。在这里,它其实是一个分类器。我们构建它时,代码是这样的Dense(15)Dense(15)Dense(15)。它所做的事情,不管你前面是怎么样,有多少维度,到我这里我要强行转化为固定的通道。比如识别字母a~z,我有500个神经元参与判断,但是最终输出结果就是26个通道(a,b,c,……,y,z)。我们这里总共有15类字符,所以是15个通道。给定一个输入后,输出为每个分类的概率。注意:上面都是二维的输入,比如24×24,但是全连接层是一维的,所以代码中使用了layers.Flatten()将二维数据拉平为一维数据([[11,12],[21,22]]->[11,12,21,22])。对于总体的模型,调用model.summary()打印序列的网络结构如下:_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
rescaling_2 (Rescaling) (None, 24, 24, 1) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 22, 22, 24) 240
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 11, 11, 24) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 9, 9, 64) 13888
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64) 0
_________________________________________________________________
flatten_2 (Flatten) (None, 1024) 0
_________________________________________________________________
dense_4 (Dense) (None, 128) 131200
_________________________________________________________________
dense_5 (Dense) (None, 15) 1935
=================================================================
Total params: 147,263
Trainable params: 147,263
Non-trainable params: 0
_________________________________________________________________
我们看到conv2d_5 (Conv2D) (None, 9, 9, 64) 经过2*2的池化之后变为max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64)。(None, 4, 4, 64) 再经过FlattenFlattenFlatten拉成一维之后变为(None, 1024) ,经过全连接变为(None, 128)再一次全连接变为(None, 15),15就是我们的最终分类。这一切都是我们设计的。model.compilemodel.compilemodel.compile就是配置模型的几个参数,这个现阶段记住就可以。2.2.5 训练数据执行就完了。# 统计文件夹下的所有图片数量
data_dir = pathlib.Path('dataset')
# 从文件夹下读取图片,生成数据集
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir, # 从哪个文件获取数据
color_mode="grayscale", # 获取数据的颜色为灰度
image_size=(24, 24), # 图片的大小尺寸
batch_size=32 # 多少个图片为一个批次
)
# 数据集的分类,对应dataset文件夹下有多少图片分类
class_names = train_ds.class_names
# 保存数据集分类
np.save("class_name.npy", class_names)
# 数据集缓存处理
AUTOTUNE = tf.data.experimental.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
# 创建模型
model = create_model()
# 训练模型,epochs=10,所有数据集训练10遍
model.fit(train_ds,epochs=10)
# 保存训练后的权重
model.save_weights('checkpoint/char_checkpoint')
执行之后会输出如下信息:Found 3900 files belonging to 15 classes.
Epoch 1/10 122/122 [=========] - 2s 19ms/step - loss: 0.5795 - accuracy: 0.8615
Epoch 2/10 122/122 [=========] - 2s 18ms/step - loss: 0.0100 - accuracy: 0.9992
Epoch 3/10 122/122 [=========] - 2s 19ms/step - loss: 0.0027 - accuracy: 1.0000
Epoch 4/10 122/122 [=========] - 2s 19ms/step - loss: 0.0013 - accuracy: 1.0000
Epoch 5/10 122/122 [=========] - 2s 20ms/step - loss: 8.4216e-04 - accuracy: 1.0000
Epoch 6/10 122/122 [=========] - 2s 18ms/step - loss: 5.5273e-04 - accuracy: 1.0000
Epoch 7/10 122/122 [=========] - 3s 21ms/step - loss: 4.0966e-04 - accuracy: 1.0000
Epoch 8/10 122/122 [=========] - 2s 20ms/step - loss: 3.0308e-04 - accuracy: 1.0000
Epoch 9/10 122/122 [=========] - 3s 23ms/step - loss: 2.3446e-04 - accuracy: 1.0000
Epoch 10/10 122/122 [=========] - 3s 21ms/step - loss: 1.8971e-04 - accuracy: 1.0000
我们看到,第3遍时候,准确率达到100%了。最后结束的时候,我们发现文件夹checkpoint下多了几个文件:char_checkpoint.data-00000-of-00001
char_checkpoint.index
checkpoint
上面那几个文件是训练结果,训练保存之后就不用动了。后面可以直接用这些数据进行预测。2.3 预测数据终于到了享受成果的时候了。# 设置待识别的图片
img1=cv2.imread('img1.png',0)
img2=cv2.imread('img2.png',0)
imgs = np.array([img1,img2])
# 构建模型
model = create_model()
# 加载前期训练好的权重
model.load_weights('checkpoint/char_checkpoint')
# 读出图片分类
class_name = np.load('class_name.npy')
# 预测图片,获取预测值
predicts = model.predict(imgs)
results = [] # 保存结果的数组
for predict in predicts: #遍历每一个预测结果
index = np.argmax(predict) # 寻找最大值
result = class_name[index] # 取出字符
results.append(result)
print(results)
我们找两张图片img1.png,img2.png,一张是数字6,一张是数字8,两张图放到代码同级目录下,验证一下识别效果如何。图片要通过cv2.imread('img1.png',0) 转化为二维数组结构,0参数是灰度图片。经过处理后,图片转成的数组是如下所示(24,24)的结构:我们要同时验证两张图,所以把两张图再组成imgs放到一起,imgs的结构是(2,24,24)。下面是构建模型,然后加载权重。通过调用predicts = model.predict(imgs) 将imgs传递给模型进行预测得出predicts。predicts的结构是(2,15),数值如下面所示:[[ 16.134243 -12.10675 -1.1994154 -27.766754 -43.4324 -9.633694 -12.214878 1.6287893 2.562174 3.2222707 13.834648 28.254173 -6.102874 16.76582 7.2586184] [ 5.022571 -8.762314 -6.7466817 -23.494259 -30.170597 2.4392672 -14.676962 5.8255725 8.855118 -2.0998626 6.820853 7.6578817 1.5132296 24.4664 2.4192357]] 意思是有2个预测结果,每一个图片的预测结果有15种可能。然后根据 index = np.argmax(predict) 找出最大可能的索引。根据索引找到字符的数值结果是['6', '8']。下面是数据在内存中的监控:可见,我们的预测是准确的。下面,我们将要把图片中数字切割出来,进行识别了。
GG
NLP知识点:Tokenizer分词器
1. 为什么要有分词器?NLP(Natural Language Processing)指的是自然语言处理,就是研究计算机理解人类语言的一项技术。计算机无论如何都无法理解人类语言,它只会计算,不过就是通过计算,它让你感觉它理解了人类语言。举个例子:单=1,双=2,计算机面临“单”和“双”的时候,它所理解的就是2倍关系。再举一个例子:赞美=1,诋毁=0, 当计算机遇到0.5的时候,它知道这是“毁誉参半”。再再举一个例子:女王={1,1},女人={1,0},国王={0,1},它能明白“女人”+“国王”=“女王”。你看,它面临文字的时候,都是要通过数字去理解的。所以,如何把文本转成数字,这是NLP中最基础的一步。很幸运,TensorFlow框架中,提供了一个很好用的类Tokenizer, 它就是为此而生的。2. 分词工具 Tokenizer假设我们有一批文本,我们想投到分词器中,让他变为数字。文本格式如下:corpus = ["I love cat" , "I love dog" , "I love you too"]
2.1 构建分词器想要对象,首先要先谈一个。想要使用分词器,首先得构建一个。from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer()
2.2 适配文本 fit_on_texts可以调用分词器的fit_on_texts方法来适配文本。tokenizer.fit_on_texts(corpus)
经过tokenizer吃了文本数据并适配之后,tokenizer已经从小白变为鸿儒了,它对这些文本可以说是了如指掌。["I love cat" , "I love dog" , "I love you too"]tokenizer.document_count记录了它处理过几段文本,此时值是3,表示处理了3段。tokenizer.word_index将所有单词上了户口,每一个单词都指定了一个身份证编号,此时值是{'cat': 3, 'dog': 4, 'i': 1, 'love': 2, 'too': 6, 'you': 5}。比如cat编号是3,3就代表cat。tokenizer.index_word和word_index相对,数字在前{1: 'i', 2: 'love', 3: 'cat', 4: 'dog', 5: 'you', 6: 'too'},编号为1的是i,i用1表示。tokenizer.word_docs则统计的是每一个词出现的次数,此时值是{'cat': 1, 'dog': 1, 'i': 3, 'love': 3, 'too': 1, 'you': 1}。比如“i”出现了3次。延伸知识:大小写和标点符号"I love cat"、"i love Cat"、"I love cat!",经过fit_on_texts后,结果是一样的。这说明它的处理是忽略英文字母大小写和英文标点符号的。2.3 文本序列化 texts_to_sequences虽然上面对文本进行了适配,但也只是对词语做了编号和统计,文本并没有全部变为数字。此时,可以调用分词器的texts_to_sequences方法来将文本序列化为数字。input_sequences = tokenizer.texts_to_sequences(corpus)
["I love cat" , "I love dog" , "I love you too"]通过序列化后, 文本列表变为数字列表 [[1, 2, 3], [1, 2, 4], [1, 2, 5, 6]]。延伸知识:超出语料库 OOV文本之所以能被序列化,其基础就是每一个词汇都有一个编号。123456ilovecatdogyoutooI love you -> 1 2 5当我们遇到没有见过的词汇时,比如 I do not love cat。 这时候该怎么办?而这种情况出现的概率还是挺高的。举个例子,你做了一个电影情感分析,用20000条电影的好评和差评训练了一个神经网络模型。当你想试验效果的时候,这时候你随便输入一条评论文本,这条新评论文本里面的词汇很有可能之前没出现过,这时候也是要能序列化并给出预测结果的。我们先来看会发生什么?corpus = ["I love cat","I love dog","I love you too"]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(corpus)
# tokenizer.index_word: {1: 'i', 2: 'love', 3: 'cat', 4: 'dog', 5: 'you', 6: 'too'}
input_sequences = tokenizer.texts_to_sequences(["I do not love cat"])
# input_sequences: [[1, 2, 3]]
从结果看,它给忽略了。"I do not love cat"和"I love cat"最终结果一样,只因为"do"和"not"没有备案。但是,有时候我们并不想忽略它,这时候该怎么办?很简单,只需要构建Tokenizer的时候传入一个参数oov_token='<OOV>'。OOV是什么意思?在自然语言文本处理的时候,我们通常会有一个字词库(vocabulary),它来源于训练数据集。当然,这个词库是有限的。当以后你有新的数据集时,这个数据集中有一些词并不在你现有的vocabulary里,我们就说这些词汇是out-of-vocabulary,简称OOV。因此只要是通过Tokenizer(oov_token='<OOV>')方式构建的,分词器会给预留一个编号,专门用于标记超纲的词汇。我们再来看看会发生什么?corpus = ["I love cat","I love dog","I love you too"]
tokenizer = Tokenizer(oov_token='<OOV>')
tokenizer.fit_on_texts(corpus)
# tokenizer.index_word: {1:'<OOV>',2:'i',3:'love',4:'cat',5:'dog',6:'you',7:'too'}
input_sequences = tokenizer.texts_to_sequences(["I do not love cat"])
# input_sequences: [[2, 1, 1, 3, 4]]
从结果可见,给超纲词一个编号1,最终"I do not love cat"序列化为2, 1, 1, 3, 4。2.4 序列填充 pad_sequences["I love cat" , "I love dog" , "I love you too"]已经通过序列化变为 [[1, 2, 3], [1, 2, 4], [1, 2, 5, 6]]。文本变数字这一步,看似大功告成了,实际上还差一步。你看下面这张图,是两捆铅笔,你觉得不管是收纳还是运输,哪一个更方便处理呢?从生活的常识来看,肯定是B序列更方便处理。因为它有统一的长度,可以不用考虑差异性,50个或者50000个,只是单纯的倍数关系。是的,计算机也和你一样,[[1, 2, 3], [1, 2, 4], [1, 2, 5, 6]]这些数字也是有长有短的,它也希望你能统一成一个长度。TensorFlow早已经考虑到了,它提供了一个pad_sequences方法,专门干这个事情。from tensorflow.keras.preprocessing.sequence import pad_sequences
sequences = [[1, 2, 3], [1, 2, 4], [1, 2, 5, 6]]
sequences = pad_sequences(sequences)
# sequences:[[0, 1, 2, 3],[0, 1, 2, 4],[1, 2, 5, 6]]
将序列数据传进去,它会以序列中最长的那一条为标准长度,其他短的数据会在前面补0,这样就让序列长度统一了。统一长度的序列,就是NLP要的素材。划重点:我们会在各种场合看到这样一句代码vocab_size = len(tokenizer.word_index) + 1,词汇总量=文本中所有词汇+1,这个1其实就是用来填充的0,数据集中没有这个词,其目的就是凑长度用的。要注意<OOV>这个是真实存在的一个词汇,它表示超纲的词汇。延伸知识:更多的定制填充数据的填充处理,存在不同的场景。上面说了前面补0, 其实有时候也希望后面补0。sequences=[[1,2,3],[1,2,4],[1,2,5,6]]
sequences=pad_sequences(sequences, padding="post")
# sequences:[[1, 2, 3, 0],[1, 2, 4, 0],[1, 2, 5, 6]]
参数padding是填充类型,padding="post"是后面补0。前面补0是padding="pre",pre是默认的。还有一种场景,就是裁剪成固定的长度,这类也很常用。举个例子,看下面几个序列:[[2,3],[1,2],[3,2,1],[1,2,5,6,7,8,9,9,9,1]]上面有4组数据,他们的长度分别是2,2,3,10。这时候如果给序列进行填充,所有数据都会填充到10的长度。[2, 3, 0, 0, 0, 0, 0, 0, 0, 0][1, 2, 0, 0, 0, 0, 0, 0, 0, 0][3, 2, 1, 0, 0, 0, 0, 0, 0, 0][1, 2, 5, 6, 7, 8, 9, 9, 9, 1]其实,这是没有必要的。因为不能为了个别数据,导致整体数据产生过多冗余。因此,填充需要进行裁剪。我们更愿意填充成长度为5的序列,这样可以兼顾各方利益。代码如下:sequences=[[2,3],[1,2],[3,2,1],[1,2,5,6,7,8,9,9,9,1]]
sequences=pad_sequences(sequences, maxlen = 5, padding='post', truncating='post')
# [[2,3,0,0,0],[1,2,0,0,0],[3,2,1,0,0],[1,2,5,6,7]]
新增了2个参数,一个是maxlen = 5,是序列允许的最大长度。另一个参数是truncating='post',表示采用从后部截断(前部是pre)的方式。这段代码意思是,不管来了什么序列,我都要搞成长度为5的数据,不够的后面补0,超过的后面扔掉。其实这种方式是实战中最常用到的。因为训练数据的输入序列格式我们可以控制,我们也用它训练了一个模型。但是当预测时,输入格式是千奇百怪的。比如我们用了一个100长度的数据训练了一个模型。当用这个模型做预测时,用户输入了一个10000长度的数据,这时模型就识别不了了。所以,不管用户输了10000长度,还是1个长度,都要转成训练时的长度。3. 中文分词英文有空格区分出来各个词汇。I love cat.里面有3个词语:I、love、cat。但是,中文却没有一个标志来区分词汇。我喜欢猫。里面有几个词语?这就很尴尬了。做NLP是必须要以词汇为基本单位的。中文的词汇拆分很庞大,一般采用第三方的服务。举例采用结巴分词实现词汇拆分。3.1 jieba的安装和使用方法代码对 Python 2/3 均兼容全自动安装:easy_install jieba 或者 pip install jieba / pip3 install jieba半自动安装:先下载 pypi.python.org/pypi/jieba/ ,解压后运行 python setup.py install手动安装:下载代码文件将 jieba 目录放置于当前目录或者 site-packages 目录通过 import jieba 来引用关注下面的“北京大学”:import jieba
sentence = " ".join(jieba.cut("欢迎来北京大学餐厅"))
print(sentence) # 欢迎 来 北京大学 餐厅
sentence2 = " ".join(jieba.cut("欢迎来北京大学生志愿者中心"))
print(sentence2) # 欢迎 来 北京 大学生 志愿者 中心
中文的自然语言处理首先要将词汇拆分出来,这是唯一区别。
GG
RNN文本生成-想为女朋友写诗(二):验证训练结果
一、亮出效果世界上美好的事物很多,当我们想要表达时,总是感觉文化底蕴不够。看到大海时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!看到鸟巢时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!看到美女时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!是的,没有文化底蕴就是这样。但是,你生在这个数字时代,中华五千年的文化底蕴,你触手可及!这篇教程就是让人工智能学习大量的诗句,找到作诗的规律,然后你给他几个关键字,他给你一首诗。看效果:输入的关键词输出的诗句大海,凉风大海阔苍苍,至月空听音。筒动有歌声,凉风起萧索。建筑,鸟巢建筑鼓钟催,鸟巢穿梧岸。深语在高荷,栖鸟游何处。美女美女步寒泉,归期便不住。日夕登高看,吟轩见有情。我,爱,美,女我意本悠悠,爱菊花相应。美花酒恐春,女娥踏新妇。老,板,英,明老锁索愁春,板阁知吾事。英闽问旧游,明主佳期晚。二、实现步骤上一篇《RNN文本生成-想为女朋友写诗(一)》,我们讲了如何训练数据。这一篇,我们试一下,如何使用训练好的数据。2.1 恢复模型你当初是如何训练的,现在就要恢复当时那个现场。当时有几层楼,有多少人开会,他们都坐在哪里,现在也是一样,只不过是讨论的话题变了。from numpy.core.records import array
import tensorflow as tf
import numpy as np
import os
import time
import random
# 读取字典
vocab = np.load('vocab.npy')
# 创建从非重复字符到索引的映射
char2idx = {u:i for i, u in enumerate(vocab)}
# 创建从数字到字符的映射
idx2char = np.array(vocab)
# 词集的长度,也就是字典的大小
vocab_size = len(vocab)
# 嵌入的维度,也就是生成的embedding的维数
embedding_dim = 256
# RNN 的单元数量
rnn_units = 1024
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
batch_input_shape=[batch_size, None]),
tf.keras.layers.GRU(rnn_units,
return_sequences=True,
stateful=True,
recurrent_initializer='glorot_uniform'),
tf.keras.layers.Dense(vocab_size)])
return model
# 读取保存的训练结果
checkpoint_dir = './training_checkpoints'
tf.train.latest_checkpoint(checkpoint_dir)
model = build_model(vocab_size, embedding_dim,
rnn_units, batch_size=1)
# 当初只保存了权重,现在只加载权重
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
# 从历史结果构建起一个model
model.build(tf.TensorShape([1, None]))
最终得到的是一个model,里面包含了输入结构、神经元结构、输出格式等信息,最重要的是它也加载了这些权重,这些权重一开始是随机的,但是经过前期训练,都变成了能够预测结果的有效值,如果调用model.summary()model.summary()model.summary()打印一下,是如下的结构:_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (1, None, 256) 1377280
_________________________________________________________________
gru_1 (GRU) (1, None, 1024) 3938304
_________________________________________________________________
dense_1 (Dense) (1, None, 5380) 5514500
=================================================================
Total params: 10,830,084
Trainable params: 10,830,084
Non-trainable params: 0
_________________________________________________________________
关于模型这个模型的序列是三部分,作用是输入文字,经过各个层蹂躏,最终预测出下一个字可能会是什么2.1.1 嵌入层 Embedding给文字赋予情感文字是有情感关系的。比如我们看到落叶,就会悲伤。看到夏雨荷,梦里喊出了皇上。但是,机器他不知道,他只知道10101010。计算机它真的好惨!为了解决这个问题,就需要把文字标记成数字,让他通过计算来理解相互之间的关系。来看颜色是如何用数字来表示的颜色数值红色[255,0,0]绿色[0,255,0]蓝色[0,0,255]黄色[255,255,0]白色[255,255,255]下面见证一下奇迹,懂色彩学的都知道,红色和绿色掺在一起是什么颜色?来,跟我一起读:红色+绿色=黄色。到数字上就是:[255,0,0]+[0,255,0] = [255,255,0]这很难吗?好像也不难,只要数字标的好,板凳有脚也能跑。到了文字上也一样,嵌入层干的就是这个。上面的颜色是用3个维度来表示,而嵌入层有更多个维度,我们代码中嵌入层设置了256个维度。每一个字词都有256个维度对它进行表示。比如“IT男”和“IT从业者”这两个词的表示。有了这些,当我们问计算IT男和IT从业者之间的差异时,他会计算出来最大的差别体现在是否是男性。你看,这样计算机就理解了词与词之间的关系了。这也就是嵌入层做出的贡献。当我们输入文本时,由于经过前面的训练,这些文本里面是带着这些维度的。有了词语维度,神经网络才能有条件为预测做出判断。2.1.2 门控循环单元 GRU女朋友总是记住这些年惹他生气的事情,细节记得一清二楚填空题:我坐在马路牙子上正抽烟,远处,一个男人掏出烟,熟练的抽出一根叼进嘴里,摸摸了上衣兜,又拍了拍裤兜,他摇了摇头,向我走来,他说:兄弟,_____。请问空格处填什么?来,跟我一起喊出来:借个火。为什么?有没有问过自己为什么能答出来。如果这道题是:他摇了摇头,向我走来,他说:兄弟,_____。你还能答对吗?你能明确知道该怎么回答吗?这又是为什么?是因为记忆,文本的前后是有关系的,我们叫上下文,代码里你见过叫context。你首先是能理解文本,另外你也有记忆,所以你能预测出下面该出现什么。我们上面解决了机器的理解问题,但是它也需要像我们一样拥有记忆。这个GRU层,就是处理记忆的。它处理的比较高级,只保存和结果有关的记忆,对于无关的词汇,它统统忽视。经过它训练和处理之后,估计题目变成这样:填空题:我抽烟,男人掏出烟,叼进嘴里,他摇了摇头,他说:兄弟,_____。 答案:借个火。有了记忆,神经网络才有底气和实力为预测做出判断。2.1.3 全连接层 Dense弱水三千,只取一瓢。在这里,它其实是一个分类器。我们构建它时,代码是这样的Dense(5380)Dense(5380)Dense(5380)。他在序列中网络层的结构是这样的:dense_1 (Dense) (1, None, 5380) 5514500
它所做的事情,不管你前面是怎么样,到我这里我要强行转化为固定的通道。比如数字识别0~9,我有500个神经元参与判断,但是最终输出结果就是10个通道(0,1,2,3,4,5,6,7,8,9)。识别字母,就是26个通道。我们这里训练的文本,7万多个句子,总共有5380类字符,所以是5380个通道。给定一个输入后,输出为每个字的概率。有了分类层,神经网络才能有方法把预测的结果输出下来。2.2 预测数据有了上面的解释,我相信,下面的代码你一定能看明白了。下面是根据一个字预测下一个字的示例。start_string = "大"
# 将起始字符串转换为数字
input_eval = [char2idx[s] for s in start_string]
print(input_eval) # [1808]
# 训练模型结构一般是多套输入多套输出,要升维
input_eval = tf.expand_dims(input_eval, 0)
print(input_eval) # Tensor([[1808]])
# 获得预测结果,结果是多维的
predictions = model(input_eval)
print(predictions)
'''
输出的是预测结果,总共输入'明'一个字,输出分别对应的下一个字的概率,总共有5380个字
shape=(1, 1, 5380)
tf.Tensor(
[[[ -3.3992984 2.3124864 -2.7357426 ... -10.154563 ]]])
'''
# 预测结果,删除批次的维度[[xx]]变为[xx]
predictions1 = tf.squeeze(predictions, 0)
# 用分类分布预测模型返回的字符,从5380个字中根据概率找出num_samples个字
predicted_ids = tf.random.categorical(predictions1, num_samples=1).numpy()
print(idx2char[predicted_ids]) # [['名']]
下面是生成藏头诗的示例。# 根据一段文本,预测下一段文本
def generate_text(model, start_string, num_generate=6):
# 将起始字符串转换为数字(向量化)
input_eval = [char2idx[s] for s in start_string]
# 上面结果是[2,3,4,5]
# 训练模型结构一般是多套输入多套输出,要升维
input_eval = tf.expand_dims(input_eval, 0)
# 上结果变为[[2,3,4,5]]
# 空字符串用于存储结果
text_generated = []
model.reset_states()
for i in range(num_generate):
# 获得预测结果,结果是多维的
predictions = model(input_eval)
# 预测结果,删除批次的维度[[xx,xx]]变为[xx,xx]
predictions = tf.squeeze(predictions, 0)
# 用分类分布预测模型返回的字符
predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
# 把预测字符和前面的隐藏状态一起传递给模型作为下一个输入
input_eval = tf.expand_dims([predicted_id], 0)
# 将预测的字符存起来
text_generated.append(idx2char[predicted_id])
# 最终返回结果
return start_string+''.join(text_generated)
#%%
s = "掘金不止"
array_keys = list(s)
all_string = ""
for word in array_keys:
all_string = all_string +" "+ word
next_len = 5-len(word)
print("input:",all_string)
all_string = generate_text(model, start_string=all_string, num_generate = next_len)
print("out:",all_string)
print("最终输出:"+all_string)
# %%
'''
input: 掘
out: 掘隼曳骏迟
input: 掘隼曳骏迟 金
out: 掘隼曳骏迟 金马徒自举
input: 掘隼曳骏迟 金马徒自举 不
out: 掘隼曳骏迟 金马徒自举 不言巧言何
input: 掘隼曳骏迟 金马徒自举 不言巧言何 止
out: 掘隼曳骏迟 金马徒自举 不言巧言何 止足知必趣
最终输出: 掘隼曳骏迟 金马徒自举 不言巧言何 止足知必趣
'''
GG
简述神经网络原理:为什么AI就出现了智慧?
白盒与黑盒白盒与黑盒,同白猫、黑猫不是一回事。白盒也叫透明盒。就是说它的逻辑是透明的,你可以了解得一清二楚。它也可能会极其复杂,但是每一个步骤,都不会超出你的预期。即便是几十个齿轮相互关联,修表的工匠也能理出其中的前因后果。这很像大家普遍认知中那个死板的指令代码。还有一种盒子叫黑盒,它与白盒正好相反。你不知黑盒是怎么运作的。但是,你可以使用它。你们之间有输入和输出关系。你说一句,它回一句。你付个钱,它给你展示效果。究竟它是怎么想的,它遇到卖西瓜的会买几个包子,这些我们不知道。就像我们问机器人问题,它每次回答都有些差别。人工智能的程序就是黑盒,它相比于传统的白盒代码,它执行的具体逻辑,我们是不知道的。甚至连它的作者,写代码的程序员也不知道。此时,有一个新问题就产生了:既然程序员都不知道它是怎么判断的,那这代码是怎么写的?又是怎么让它工作的呢?神经网络人工智能技术大多是采用神经网络实现的。“神经网络”这个概念,最早是由冯·诺依曼在1948年提出来的。冯·诺依曼研究人脑结构与计算机存储的区别,提出了用存储计算模拟人类的神经元,从而实现计算机“自动思考”。但是,当时白盒的指令程序,发展很好,很挣钱。于是,老冯就放弃了神经网络。再往后,科学家们也一直研究神经网络。但总是热一阵儿,冷一阵儿。反正也研究不明白,机器也不快,同时也不挣钱。直到近几年,它突然火了。那么,神经网络的原理到底如何呢?它既然模仿的是人类神经元,那我们不妨先了解神经元是什么东西。通俗了说,神经元就是好多个爪爪最终汇成了一个爪。这有点像八爪鱼。八爪鱼摸啤酒瓶,摸半天,几个爪爪获得数据,传给大脑。八爪鱼结合尺寸、形状和材质,得出结论,原来是摸了个啤酒瓶。摸麻将也是,有时候一个爪儿一绕,小吸盘一靠,这是一个八筒。这种结构有一个作用,那就是通过多个输入,最终产生一个输出。这就像你去相亲,通过对方的相貌、经济、性格等多个输入,最终你会产生他是否值得交往,这样一个结果输出。当这些小八爪多了之后,一个八爪的头又作为顶级的小触手,相互勾连,层层传递,就形成了我们的神经网络。计算机所模拟的,就是这类结构。这并不难理解,可以搞成一个数学模型。训练和学习我是从2018年开始了解神经网络的。我的女儿是2020年出生的。我感觉人工智能的训练和学习过程,很像一个小婴儿的成长。于是,我就一直观察女儿学习和认知的过程。我拿了张卡片,教她认识小动物。我告诉她,这是一张狗。她记下了,哦,长这模样的就叫狗。后来,我带她出门,在小区遇到遛狗的美女。女儿指着草丛中撒尿的小狗喊道:狗狗!有点奇怪。我陷入了思考,我教过她认识卡片上的狗。但是草丛里的那条狗,她是第一次见,她也能分辨得出来。这说明,她肯定是找到了狗身上的某些特征。具体是哪些特征呢?她可能是胡乱猜的,比如舌头或者鼻子。是的,人工智能的神经网络,一开始的机制也是乱猜。就比如说,有一堆零件,里面有合格品也有次品。我们想让人工智能去学会区分。因此,我们得先提供了一批样品,作为训练数据。这里面有合格的,也有不合格的。然后把它们交给程序去学习。算法程序怎么去学习呢?其实就是计算,从已知的输入去计算结果的输出。假设我们知道这些零件的误差尺寸,也知道它们是否合格。然后,程序员就建立一个数学模型,告诉它需要关注什么地方。比如写一个公式:结果=参数1×误差尺寸+参数2。也就是让它找到一条线,这条线是合格品和次品的分界线。这里有一个上帝视角的问题。因为我标好了,而且还做成了图像,所以你一眼就知道线应该在哪里。但是,计算机面对的是一组组混乱的数据。因此,它首先就无脑地随便画一条线。然后说这就是那条分界线。结果,把训练数据带进去一算,不对,这条线猜错了。然后,它再随机画一条线,说这条线就是分界线,然后再算。因为它的试错机会很多,一秒可以运算上万次,所以最终它总能找对答案。在寻找答案的过程中,不断调整的就是公式中的“参数1”和“参数2”。我们看整个过程,程序员并没有写关于逻辑判断的语句。他们只是搭建了几层神经网络,然后指定了一个“结果=参数1×误差尺寸+参数2”的公式。其中起关键作用的“参数1”和“参数2”,程序员也不知道具体的数值是多少。他通过框架和模型,先随机胡乱猜测一个,然后通过大量样本数据进行矫正。猜不对就再换一个。最终会找到适合多数训练数据的参数。这个找的参数的过程,依靠的就是算法,一般都有成熟的开源框架,大家都可以使用。到这里,不知我是否已经讲得明白了,人工智能通过神经网络,开局是个脑残,但是借助于高频次的计算,再利用数据的喂养进行不断矫正,最终实现了自发的智慧。这一点也是和传统的指令程序,差别最大的地方。实现质的飞越后来,我又给女儿看了一张照片。我问她,这个图片是什么?她回答说:是狗!我说不对,这个叫“狮子”!她仔细看了看,点了点头,记下了这个模样的动物叫狮子。我猜测她的神经元内应该是又建立了很多新的突触,就像是人工智能的神经网络又更新了参数。其实,从这里我们也看到。只有见得多,才会认识的多。不管是人,还是人工智能。所以,缺少数据的喂养,就像是婴儿缺少教育,如果到八岁还只见过狗,没见过猫,那发展多少年智力都高不了。这也是很多人工智能客服不智能的一个重要原因。可以想象,那些对话机器人在训练时,也是给计算机喂数据,一问一答。问题是“你吃了吗?”,答案是“吃过了!”。算法模型很鸡贼,只给问题,让计算机自己随机出答案。计算机就胡乱输出,给出成千上万甚至上亿条答案,这里面有“天气不错”、“电影好看”,当然也包括“吃过了!”。这就是有网友评论说,给猴子一台电脑,他能从键盘上只字不差地打出一部《莎士比亚全集》。前提是要有足够长的时间。这个概率比你期期中彩票还要低。但这在计算机的超级计算能力下,一些概率问题,可以靠增加次数解决。最后,模型给出答案,它说“吃过了!”是标准答案。并让计算机记下来,当你猜到是“吃过了!”时,你内部的随机值是怎样的,发生了什么。你要存储下来,下次还有用。计算机的算力是很强大的,记录这种随机值,就分配了百万亿个参数。它会从不同的维度去记录,甚至包括当时猜到“吃过了!”时,我的GPU温度是多少度。尽管这项数据和问题没有关系,但是架不住企业有设备,存就完了,万一有用呢!而数据也是海量的,全球的对话数据它都有,人类历史上出现过的对话,都会作为训练数据。你不要认为你问的问题很刁钻,放眼全球,像你一样的人,还有很多。最后,人工智能看遍了世间所有能找到的资料。它有的是算力,因此它的计算速度非常快。很早之前它就叫“电脑”,现在更了不起,叫“超级电脑”。它能计算,因此会把数据进行拆分和关联。比如关于森林的知识,它可以从文本中拆分出“树木”、“气候”、“温度”等词汇。关于家具的知识,它可以拆分出“钉子”、“材料”、“风格”等元素。当我们问“多买家具会发生什么时?”。它可能会回答“可能会造成森林减少,进而影响气候”。为什么这么回答?人类的资料中并没有这类训练数据啊?但是,它脑子快,脑容量大。它从家具关联到了木材,从木材关联到了森林,从森林关联到了气候……具体它会回答什么?又是谁和谁进行的关联?这一切,人类已经无法通过逻辑去判断了。因为就算是万亿个if、else分支,我们也理不清楚了。更何况实际的神经网络结构要更复杂。而我们尚且连自己的大脑都还没有研究明白。以上举的例子比较落后,仅仅为了从理论层面,去解释计算机出现了非人类可知的结果。
GG
知识点:TensorFlow的序列模型和各种层
1. 为什么要有神经网络模型?编程源于生活而高于生活。生活中,有一只狗叫旺财。编程中也有类和对象的概念:wangcai = Dog()。之所以这么搞,就是为了符合人类的思维习惯,便于人们理解和编程。深度学习中,有一个神经网络模型的概念。它接收输入,经过一系列操作,最终产出输出。比如建立了一个手写识别的模型,输入手写的图片或者轨迹,它可以输出识别后的数字是“6”。你可以理解它是一套流水线设备。那么,流水线上是怎么样的呢?首先有输入,就是把原材料放进去。然后按照次序,经过多道工序,层层处理。每一层处理都有自己的独特的工作。最后是输出,生成最终产物。2. 模型序列中那些常用的层一个神经网络模型和流水线很类似,也有一套多层的处理设备,能将输入转化为输出。2.1 Sequential 序列通过序列Sequential构建模型有如下两种方式:# 方式一:一站式
model = tf.keras.models.Sequential([
keras.layers.Flatten(),
keras.layers.Dense(),
keras.layers.Dropout(),
keras.layers.Dense()
])
# 方式二:分步式
model = tf.keras.models.Sequential()
model.add(layers.Embedding())
model.add(layers.Bidirectional())
model.add(layers.Dense())
两种方式的共同点都是通过Sequential构建模型,一个模型都会设置多个层。我们拿手写数字识别的序列举个例子:model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28,28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dense(10, activation='softmax')
])
model.summary()
这个序列里面依次包含了三个层,每个层是有顺序的,这也是为什么叫序列的原因。可以调用model.summary()打印出来结构:Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten (Flatten) (None, 784) 0
_________________________________________________________________
dense (Dense) (None, 128) 100480
_________________________________________________________________
dense_1 (Dense) (None, 10) 1290
_________________________________________________________________
第一层输入28x28的二维数据,输出784大小的一维数据。第二层将第一层的输出作为输入,输出128大小的一维数据。第三层将第二层的输出作为输入,输出10大小的一维数据。其实28x28的二维数据就是图片像素值的宽高矩阵。其转化的流程如下图所示:第一层转为784大小的一维数组,经过第二层变为128大小,到第三层时转化成10的大小,这10个代表0~9个数字的分类,其中数字“6”的分类占比最高,所以识别结果就是“6”。那么,这些个层都有哪些,他们都起什么作用呢?看代码都是tf.keras.layers开头的,说明他们是一类,下面就说几个常见的层。2.2 Dense 全连接层Dense层又称全连接层,是神经网络中最常见的一个层,它既能当输入层,又能当输出层,还能当中间层。Dense的中文翻译是:稠密的,笨拙的。看它上面的结构图就很稠密,密密麻麻。说到笨拙,其实它确实也不聪明,它就是固定的计算,逻辑上等价于一个函数:out=Activation(Wx+bias)。W:m*n的矩阵,权重,结构是提前定义的,但是数值是训练出来的。x:n维向量,是输入。Activation:激活函数,为线性变换再套一层变换,解决非线性问题。bias:偏置项,也是训练出来的。out:最终输出m维向量。最终变化就是一个线性变化加一个非线性变化产生输出。从效果看是:输入的n维数据,最后输出m维数据。就好比手写数字识别中,输入是28x28的二维向量,经过层层变化,最终输出10个分类。全连接层的定义举例:Dense(10, activation='softmax')该层有10个节点的输出,也是下一个节点的输入。激活函数使用softmax。2.3 Conv2D 卷积层卷积层一般用于图像处理,用于寻找图像的局部特征。假如,我们要识别一张图片是不是老鹰,我们依靠两点进行判断:鹰嘴、鹰爪。如果有这两个特点,我们就说这是老鹰。要判断是否包含这两类特征,我们就需要有一个小方块在图片上移动,这个小方块是带着有色眼镜的,会专门寻找鹰爪的那个勾,遇到带勾的区域就特别兴奋地标记为1,遇不到就没有感觉地标记为0。这个小方块就是卷积核,卷积核在整幅图像上挨个标记的操作就叫卷积操作。上图就是模拟了一个卷积操作。用一个3x3的卷积核,在5x5的图像上标记,每次移动两个格子(术语:步长为2),最后生成了最右边2x2的图像。如果我们定义卷积值大于600就是鹰爪的话,那么原图经过扫描之后,发现有2个鹰爪。卷积可以重复卷,可以卷完了再卷,很恐怖。卷积的定义举例:Conv2D(64, (3,3), activation='relu')卷积层的卷积核大小为3x3。有64类卷积方块去进行扫描。激活函数使用relu。2.4 Flatten 压平层这个层最容易理解。就是把多维压平成一维。一般情况下,它作为从卷积层到全连接层的过渡。因为图片一般是二维的,但是分类是一维的,需要中间某个时机进行转化。……
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10))
……
上面的例子就是图片经过卷积多次之后,经过Flatten拉平,然后加两个全连接层最终达到图片变为分类的效果。2.5 MaxPooling2D 池化层池化层,可以简单理解成压缩。对于人工智能,一旦训练起来可能是几天甚至几个月。如果能节省时间,那就太好了,可以快速试错和验证新想法。那么,如何节省时间呢?除了砸钱增加设备配置之外,降低数据计算量也是一个有效的办法。我们看下面这张图,左边和右边虽然大小不一样,但是他俩的特征却是一样的,是同一类图像。这时候,计算尺寸小的图片就比较合算了,能减少成倍的计算量。所以,我们看很多图片数据集,都是非常小且模糊,精简到你勉强认识。但是,实际上我们生活中的图片却非常大。因此,将高清图在保留特征的情况下尽量压缩,以减少数据复杂度,这就是池化层的作用。池化有很多种方式,比如最大池化,也就是在一定区域内只要最大值。其他还有平均池化,就是取平均值。池化层的定义很简单,和卷积层很像,需要定义池化矩阵。例如MaxPooling2D((2,2))就是定义了一个2x2的池化矩阵。看下面一个卷积后池化的例子。model = models.Sequential()
model.add(layers.Conv2D(32,(3,3), activation='relu', input_shape=(32,32,1)))
model.add(layers.MaxPooling2D((2,2)))
model.summary()
通过打印结果发现,32x32的图片,经过3x3的卷积核以每次移动1个单位处理后,变成30x30的图片,再通过2x2的池化,让30x30变成了15x15。conv2d (Conv2D) (None, 30, 30, 32) 320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 15, 15, 32) 0
_________________________________________________________________
上面就是神经网络序列中经常出现的几种层。
GG
opencv基础:文档透视(扭曲)矫正
上回说到公司HR姐姐了解我会图像矫正,提供很多倾斜图片让我处理,我都一一克服了。后来了解到,她是在替他男朋友找源码。得知真相的我眼泪掉了下来,从此再也不相信漂亮女生的言语。一、缘起二、boundingRect 边界矩形三、minAreaRect 最小面积矩形四、HoughLinesP 霍夫线变换五、故事继续……就在我决定和她绝交的时候。HR姐姐发来信息,说她在宾馆609房间等我。六、再续前缘我还是决定去和她见面,探一探虚实,丰富一下人生阅历,即便以后写小说也会有素材。不过,即便不去,我也已经基本猜到了。她男朋友以及他的研发团队,在宾馆里封闭开发。这次叫我还是让我写代码的。我到了宾馆,敲门进入了609房间。见到了她,果然,有很多人,很显眼的是,一个穿黑色皮衣的中年男人坐在椅子上,而她就站在旁边。她看到我,有点不好意思,说到,又要麻烦你了。这次是我又遇到了一个问题,你看这样的图片如何矫正。七、warpPerspective 透视变换我看到这张图,微微一笑。我心想:当初为了给你矫正文档,我研究了大量的资料,这种可不是角度的倾斜,这是透视扭曲。我先看了看那个皮衣男,那是她的男朋友吧,年纪不小了,哼哼,这点知识居然都没有掌握,而且也没有我长得帅。我问:这是你扫描文件的时候,纸张下面垫了个玻璃瓶子吧?只有这种情况才能扫描成这样!她说,你怎么看出来的。我说,这叫透视扭曲。比如下面这张图。怎么才能达到你那种效果,只有通过立体感变换才可以,说白了就是远近角度的扭曲,不能直视它,要斜视,哪个角度看着难受哪个角度看。你看着屏幕上的这张图,跑到天花板上看,或者趴到地板上看。要么你不动,让文件动,就像下面这样,让文件一部分接近你或者远离你,这就是从视觉上的扭曲,绘画上叫“远大近小”。能扭曲,就能矫正。知道怎么扭曲的,倒放的步骤就能矫正。import numpy as np
import cv2
# 读入图片
img = cv2.imread('img4_0_perspective.jpg')
img_size = (img.shape[1], img.shape[0])
# 确定需要矫正的区域,左上,左下,右下,右上
src = np.float32([[82,90],[82,368],[433,338],[433,124]])
# 确定需要矫正成的形状,和上面一一对应
dst = np.float32([[82,90],[82,368],[433,368],[433,90]])
# 获取矫正矩阵,也就步骤
M = cv2.getPerspectiveTransform(src, dst)
# 进行矫正,把img
img = cv2.warpPerspective(img, M, img_size)
# 展示校正后的图形
cv2.imshow('output', img)
cv2.waitKey(0)
矫正后的图像如下:实现起来主要就是两步。第一通过cv2.getPerspectiveTransform获取矫正的转换钥匙(矩阵)。传入图像扭曲部分4个点的坐标,然传入矫正到4个点的坐标,坐标顺序没有要求,但是前后要对应(不然会翻车),就可以计算出转换钥匙。第二步,调用cv2.warpPerspective(img, M, img_size)进行矫正。第一个参数是要矫正的图像数据,第二个参数是转换的钥匙(矩阵),第三个参数是图像的大小。默认矫正后空白填充0值,也就是黑色。当然我们通过cv2.warpPerspective(img, M, img_size, borderValue=(255,255,255))可以设置成白色。注意下图虚线处就是矫正前和校正后四个点的坐标。八、得寸进尺 findContours、approxPolyDP皮衣男看到结果后,微微点头,向右看去。HR姐姐在皮衣男左边,右边是一个秃顶男。秃顶男收到信号后,问我:传入参数调用方法执行即可,这也不难,敢问矫正前后的四个点,从何获取?你能自动识别吗?呜呼呀!这里居然还有懂行的人,实际应用中,哪有人给你手工找坐标点,肯定也得是自动计算。看来简单应付是不行了。我微微一笑,这有何难!我飞快地用右手敲击键盘,用红绿两种颜色画出了扭曲框和矫正框。“怎么样!这效果还满意吧!”,我语气中故意将问号全部换成了叹号。说吧,我就要走。“壮士留步!”,秃顶男将我拦下:“可否留下代码,解读一下!”“没有这个必要吧,你我萍水相逢,何况我还有公务在身,不便久留”,说完要走。皮衣男瞅了瞅HR姐姐,HR姐姐把我拦了下来:“大郎……不是,大工程师,来都来了,不在乎这一时半会儿了,说说吧,就当帮我了!”“帮你,凭什么帮你?!”,我心里想,但是没有说出口,我被潜意识控制,说出一个字:好!img = cv2.imread('img4_0_perspective.jpg')
# 转为灰度单通道 [[255 255],[255 255]]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化图像
ret,img_b=cv2.threshold(gray,200,255,cv2.THRESH_BINARY_INV)
# 图像出来内核大小,相当于PS的画笔粗细
kernel=np.ones((5,5),np.uint8)
# 图像膨胀
img_dilate=cv2.dilate(img_b,kernel,iterations=8)
# 图像腐蚀
img_erode=cv2.erode(img_dilate,kernel,iterations=3)
# 寻找轮廓
contours, hierarchy = cv2.findContours(img_erode,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓
#cv2.drawContours(img,contours,-1,(255,0,255),1)
# 一般会找到多个轮廓,这里因为我们处理成只有一个大轮廓
contour = contours[0]
# 每个轮廓进行多边形拟合
approx = cv2.approxPolyDP(contour, 150, True)
# 绘制拟合结果,这里返回的点的顺序是:左上,左下,右下,右上
cv2.polylines(img, [approx], True, (0, 255, 0), 2)
# 寻找最小面积矩形
rect = cv2.minAreaRect(contour)
# 转化为四个点,这里四个点顺序是:左上,右上,右下,左下
box = np.int0(cv2.boxPoints(rect))
# 绘制矩形结果
cv2.drawContours(img, [box], 0, (0, 66, 255), 2)
img_size = (img.shape[1], img.shape[0])
# 同一成一个顺序:左上,左下,右下,右上
src = np.float32(approx)
dst = np.float32([box[0],box[3],box[2],box[1]])
# 获取透视变换矩阵,进行转换
M = cv2.getPerspectiveTransform(src, dst)
img = cv2.warpPerspective(img, M, img_size, borderValue=(255,255,255))
cv2.imshow('output', img)
cv2.waitKey(0)
代码在此,告辞!“壮士留步,代码都留了,何不解释一下!”,秃顶男说。我没有理他,转身要走。HR姐姐又把我留下了。“这张图,我们如何识别出轮廓。”灰度、二值化都是常规操作了,目的是转为黑白单通道,简化计算。cv2.dilate(img_b,kernel,iterations=8)。img_b是需要膨胀的图片,kernel是膨胀的内核,上面我们定义了5*5像素大小,iterations是膨胀次数。膨胀之后这样效果。这样看,他们就连为一体的,但是太胖了,我们还要收缩,收缩用腐蚀cv2.erode(img_dilate,kernel,iterations=3)。为什么要膨胀完了再收缩?膨胀是为了融为一体,为了一体化必然会扩大,收缩是为了接近原始大小。这样,基本的图形轮廓就出来了,我们利用cv2.findContours找到轮廓的点。# 寻找轮廓
contours, hierarchy = cv2.findContours(img_erode,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓
cv2.drawContours(img,contours,-1,(255,0,255),1)
contours是图形中能找到的所有闭合的轮廓,如果是下面的图,那就是2个轮廓。数据是离散的点的集合,样式如下面这样[ [[[ 84, 101]], [[ 84, 103]],[[ 83, 104]]] , [[[ 84, 101]], [[ 84, 103]],[[ 83, 104]]] ],每个轮廓由多个点构成。轮廓之间有可能存在嵌套关系,比如甜甜圈那样,hierarchy就是说明这些之前的嵌套关系,本例子不涉及,此处不做详细说明。我们的图,经过8次膨胀,已经合成一体是一个轮廓。所以,我们可以先按照contour = contours[0]处理这一个轮廓,轮廓的数据也是点的集合。cv2.approxPolyDP(contour, 150, True)做一个多边形拟合。approxPolyDP能够根据传入的多个点,得出一个包裹所有点的多边形,返回多边形的顶点。approxPolyDP(curve,epsilon,closed)第一个参数curve是数据点。第二个参数epsilon是多边形的精度,数值越小越像曲线。第三个参数closed是指多边形是否闭合的。# 每个轮廓进行多边形拟合
approx = cv2.approxPolyDP(contour, 150, True)
# 绘制拟合结果,这里返回的点的顺序是:左上,左下,右下,右上
cv2.polylines(img, [approx], True, (0, 255, 0), 2)
通过approxPolyDP并绘制之后,结果如下:我们可以获得它的4个顶点,顺序是:左上,左下,右下,右上。这4个顶点就是矫正前的点。我们可以利用minAreaRect获得最小面积矩形,作为期望被矫正后的轮廓。# 寻找最小面积矩形
rect = cv2.minAreaRect(contour)
# 转化为四个点,这里四个点顺序是:左上,右上,右下,左下
box = np.int0(cv2.boxPoints(rect))
# 绘制矩形结果
cv2.drawContours(img, [box], 0, (0, 66, 255), 2)
矫正后的4个点,这4个点的顺序是:左上,右上,右下,左下。有了前后的4个点,我们再去调用矫正方法,就可以做到自动矫正了。九、故事反转“好好好!”,皮衣男拍着巴掌站了起来:“你讲的很通俗,我居然都听懂了,是个人才!”。说完,他就走了。紧着着,其他人也都跟着走了,留下我一个人在房间里。我还在发愣,这是什么情况,搞什么鬼?我正在思考之际,HR姐姐急忙返回来,跟我说:你发达了!说完她就急忙走了。
GG
知识点:one-hot独热编码
1. one-hot 独热独热,是机器学习中初学者经常听到的一个词。从字面意义看,独表示唯独,一家独大,独占鳌头,独热表示只有1个热,其他都是凉的。事实也是如此。我们来看一个独热编码的例子:[0, 1, 0, 0, 0]可以看到,上面只有一个1,其他都是凉凉的0,这就是独热。假设,我们有5种状态:金、木、水、火、土。我们给这5个状态留了5个空,它们都有专门的位置。数字位置编号金0木1水2火3土4自从有了这个规则以后,但凡是老金出现,都是以这种状态示人:[1, 0, 0, 0, 0],我有5个兄弟,我表示第一个。老土出现那就是:[0, 0, 0, 0, 1]。如果两个一起出现,是这样式的:[[1, 0, 0, 0, 0],[0, 0, 0, 0, 1]].这就是独热的表示。2. 为什么要用独热?为什么会用这种奇怪的方式表示呢?老金直接是0,老土直接是4不就完了?其实这么做是有目的的。独热是为了体现公平。每次出现都携带者团队成员的数量,避免了招摇撞骗,说房子是自己的,其实自己只占几份之一。另外,每个成员只能是1,只是用来标记是不是你,无法夸大你的比重,比如展示成[99, 0, 0, 0, 0],这样就不是独热的标准。通过独热的编码标准,使得计算时,公平公正。大家的值都是1,避免了你是1我是2,出现谁大谁小从而干扰计算。如果,你的基础学科比较好的话,其实我上面说那么多,都是下面的通俗化:大部分算法是基于向量空间中的度量来进行计算的,为了使非偏序关系的变量取值不具备偏序性,而且到圆点是等距的。使用one-hot编码,将离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理。3. 独热的局限虽然,独热编码有优势。但是,它也是有局限性的。独热适合表示少量的,无关联的数据。首先说它不适合大量的数据。如果总共有5条数据,那其中一条是这么表示[1, 0, 0, 0, 0],如果是10条,这么表示[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]。如果是5000条,那就是1个1,4999个0。这种情况,术语上叫过于稀疏,反而不利于计算。另外,也是因为独热很公正公平,所以导致成员间没有个人关系。有时候,尤其当自然语言处理时,我们很希望能表示出每个词语间的相关性。比如,我们要表示心情好坏程度,有如下几种心情:悲伤、郁闷、无聊、微笑、大笑、爆笑。那么,我们假设以0为中心点,负面的情绪定义为负数,正面的情绪定义为正数。上面几种状态可以这么表示:心情表示悲伤-3郁闷-2无聊-1微笑1大笑2爆笑3这样,我们就可以了解他们之间的关系了:爆笑(3)程度要大于微笑(1)。大笑(2)和郁闷(-2)是完全相反的状态。这种带成员关系的数据,就不合适用独热来表示了。4. 独热的应用场景一般的分类问题,比如手写数字识别,OCR识别,花朵种类识别。TensorFlow提供了一个将数值转换为独热编码的方法:import tensorflow as tf
labels = [1,2,3]
ys = tf.keras.utils.to_categorical(labels, 5)
print(ys)
最终输出的结果:[[0. 1. 0. 0. 0.]
[0. 0. 1. 0. 0.]
[0. 0. 0. 1. 0.]]
此方法很有用,因为一般训练集标记的不是独热编码,比如手写识别结果存储都是具体的:3、6、2,我们可以使用to_categorical来转化为独热编码。
GG
老张让我用TensorFlow识别语音命令:前进、停止、左转、右转
一、老张的需求我有朋友叫老张,他是做传统单片机的。他经常搞一些简单的硬件发明,比如他家窗帘的打开和关闭,就是他亲自设计的电路板控制的。他布线很乱,从电视柜的插座直接扯电线到阳台的窗户,电线就像蜘蛛网一样纵横交错。老张的媳妇是个强迫症、完美主义者,没法忍受乱扯的电线。但是,她又害怕影响自己的丈夫成为大发明家。于是,她就顺着电线绑上绿萝,这样就把电线隐藏起来了,丝毫不影响房间的美观。上周,老张邀请我去他家里,参观这个电动窗帘。老张很激动,赶紧拿来凳子,站在凳子上,用拖把杆去戳一个红色的按钮。他激动地差点滑下来。我问他,你为什么要把开关放那么偏僻。老张说,是为了避免3岁的儿子频繁地按开关。老张站在凳子上,像跳天鹅湖一样,垫着脚尖努力去戳按钮,给我演示窗帘的开和关。我赶紧夸他的窗帘非常棒。我不知道,就这种情况,如果他摔下来,从法律上讲我有没有连带责任。我连忙转移注意力:哎,你家的绿萝长得挺好。我不自觉地摸了一下叶子,感觉手指头麻了一下。我去!老张你家绿萝带刺!老张说,不是带刺,可能是带电。我并没有太惊奇,因为我认识老张10多年了。我们先是高中3年同学,后来4年大学同学,后来又2年同事。在老张这里,什么奇怪的事情都可能发生。我还记得,高中时,他晚上抱着半个西瓜插着勺子,去厕所蹲坑,上下同步进行。他丝毫没有尴尬的意思,并称之为豁达。我回忆起往事,痛苦不堪。今天被绿萝电了,也会成为往事。我起身准备要走。老张说,我搞了好多年嵌入式板子,你知道为什么一直没有起色?我说,什么?你对电路板还能起色?!老张说,不。我意思是说,我搞嵌入式工作这么多年,一直是平平无奇。主要原因,我感觉就是没有结合高新技术,比如人工智能。而你,现在就在搞人工智能。我说,我能让你起色吗?老张说,是的。我研究的巡逻小车,都是靠无线电控制的,我一按,就发送个电波。你帮我搞一个语音控制的。我一喊:跑哇!它就往前走。我一喊:站住!它就停止。我一喊:往左,往右!它就转弯。我说,这个不难。但是,这有用吗?老张说,是的,这很起色。据我所知,我们车间里,还没有人能想出来我这个想法。而我,马上就能做出来了。我说,可以。你这个不复杂。但是,我也有个要求。那就是,我把你这个事情,写到博客里,也让网友了解一下,可以吗?老张说:没问题!二、我的研究人工智能有三大常用领域,视觉、文字和语音。前两者,我写过很多。这次,开始对语音领域下手。以下代码,环境要求 TensorFlow 2.6(2021年10之后版) + python 3.9。2.1 语音的解析我们所看到的,听到的,都是数据。体现到计算机,就是数字。比如,我们看到下面这张像素图,是4*4的像素点。图上有两个紫色的点。你看上去,是这样。其实,如果是黑白单通道,数据是这样:[[255, 255, 255, 255],
[255, 131, 255, 255],
[255, 131, 255, 255],
[255, 255, 255, 255]]
如果是多通道,也就是彩色的,数据是下面这样:[[[255, 255,255],[255, 255, 255],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[198, 102, 145],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[198, 102, 145],[255, 255, 255],[255, 255, 255]],
[[255, 255, 255],[255, 255, 255],[255, 255, 255],[255, 255, 255]]]
我们看到,空白都是255。只是那2个紫色的格子有变化。彩色值是[198, 102, 145],单色值是131。可以说,一切皆数据。语音是被我们耳朵听到的。但是,实际上,它也是数据。你要不信,我们来解析一个音频文件。# 根据文件路径,解码音频文件
import tensorflow as tf
audio_binary = tf.io.read_file("datasets\\go\\1000.wav")
audio, rate = tf.audio.decode_wav(contents=audio_binary)
使用tf.audio.decode_wav可以读取和解码音频文件,返回音频文件的数据audio和采样率rate。其中,解析的数据audio打印如下:<tf.Tensor: shape=(11146, 1), dtype=float32, numpy=
array([[-0.00238037],
[-0.0038147 ],
[-0.00335693],
...,
[-0.00875854],
[-0.00198364],
[-0.00613403]], dtype=float32)>
上面的数据,形式是[[x]]。这表示,这段音频是单声道(类比黑白照片)。x是声道里面某一时刻具体的数值。其实它是一个波形,我们可以把它画出来。import matplotlib.pyplot as plt
plt.plot(audio)
plt.show()这个波的大小,就是推动你耳朵鼓膜的力度上面的图是11146个采样点的形状。下面,我们打印10个点的形状。这10个点就好比是推了你耳朵10下。import matplotlib.pyplot as plt
plt.plot(audio[0:10])
plt.show()至此,我们可以看出,音频实际上就是几组带有序列的数字。要识别音频,就得首先分析音频数据的特征。2.2 音频的频谱每个个体都有自己的组成成分,他们是独一无二的。就像你一样。但是,多个个体之间,也有相似之处。就像我们都是程序员。于是,我们可以用一种叫“谱”的东西来描述一个事物。比如,辣子鸡的菜谱。正是菜谱描述了放多少辣椒,用哪个部位的鸡肉,切成什么形状。这才让我们看到成品时,大喊一声:辣子鸡,而非糖醋鱼。声音也有“谱”,一般用频谱描述。声音是振动发生的,这个振动的频率是有谱的。把一段声音分析出来包含哪些固定频率,就像是把一道菜分析出来由辣椒、鸡肉、豆瓣酱组成。再通过分析食材,最终我们判断出来是什么菜品。声音也是一样,一段声波可以分析出来它的频率组成。如果想要详细了解“频谱”的知识,我有一篇万字长文详解《终于,掘金有人讲傅里叶变换了》。看完需要半个小时。我上面说的,谷歌公司早就知道了。因此,他们在TensorFlow框架中,早就内置了获取音频频谱的函数。它采用的是短时傅里叶变换stft。waveform = tf.squeeze(audio, axis=-1)
spectrogram = tf.signal.stft(waveform, frame_length=255, frame_step=128)
我们上面通过tf.audio.decode_wav解析了音频文件,它返回的数据格式是[[-0.00238037][-0.0038147 ]]这种形式。你可能好奇,它为什么不是[-0.00238037, -0.0038147 ]这种形式,非要外面再套一层。回忆一下,我们的紫色像素的例子,一个像素点表示为[[198, 102, 145]],这表示RGB三个色值通道描述一个彩色像素。其实,这里也一样,是兼容了多声道的情况。但是,我们只要一个通道就好。所以需要通过tf.squeeze(audio, axis=-1)对数据进行降一个维度,把[[-0.00238037][-0.0038147 ]]变为[-0.00238037, -0.0038147 ]。这,才是一个纯粹的波形。嗯,这样才能交给傅里叶先生进行分析。tf.signal.stft里面的参数,是指取小样的规则。就是从总波形里面,每隔多久取多少小样本进行分析。分析之后,我们也是可以像绘制波形一样,把分析的频谱结果绘制出来的。看不懂上面的图没有关系,这很正常,非常正常,极其正常。因为,我即便用了一万多字,50多张图,专门做了详细的解释。但是依然,有20%左右的读者还是不明白。不过,此时,你需要明白,一段声音的特性是可以通过科学的方法抽取出来的。这,就够了。把特性抽取出来之后,我们就交给人工智能框架去训练了。2.3 音频数据的预处理上面,我们已经成功地获取到一段音频的重要灵魂:频谱。下面,就该交给神经网络模型去训练了。在正式交给模型之前,其实还有一些预处理工作要做。比如,给它切一切毛边,叠一叠,整理成同一个形状。正如计算机只能识别0和1,很多框架也是只能接收固定的结构化数据。举个简单的例子,你在训练古诗的时候,有五言的和七言的。比如:“床前明月光”和“一顿不吃饿得慌”两句。那么,最终都需要处理成一样的长短。要么前面加0,要么后边加0,要么把长的裁短。总之,必须一样长度才行。床前明月光〇〇
一顿不吃饿得慌
蜀道难〇〇〇〇
那么,我们的音频数据如何处理呢?我们的音波数据经过短时傅里叶变换之后,格式是这样的:<tf.Tensor: shape=(86, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
[[3.96062881e-01],
[2.01166332e-01],
[2.09505502e-02],
...,
[1.43915415e-04]]], dtype=float32)>
这是因为我们11146长度的音频,经过tf.signal.stft的frame_step=128分割之后,可以分成86份。所以我们看到shape=(86, 129, 1)。那么,如果音频的长度变化,那么这个结构也会变。这样不好。因此,我们首先要把音频的长度规范一下。因为采样率是16000,也就是1秒钟记录16000次音频数据。那么,我们不妨就拿1秒音频,也就是16000个长度,为一个标准单位。过长的,我们就裁剪掉后面的。过短的,我们就在后面补上0。我说的这一系列操作,反映到代码上,就是下面这样:waveform = tf.squeeze(audio, axis=-1)
input_len = 16000
waveform = waveform[:input_len]
zero_padding = tf.zeros([16000] - tf.shape(waveform),dtype=tf.float32)
waveform = tf.cast(waveform, dtype=tf.float32)
equal_length = tf.concat([waveform, zero_padding], 0)
spectrogram = tf.signal.stft(equal_length, frame_length=255, frame_step=128)
spectrogram = tf.abs(spectrogram)
spectrogram = spectrogram[..., tf.newaxis]
这时候,再来看看我们的频谱数据结构:<tf.Tensor: shape=(124, 129, 1), dtype=float32, numpy=
array([[[4.62073803e-01],
...,
[2.92062759e-05]],
...
[0.00000000e+00],
...,
[0.00000000e+00]]], dtype=float32)>
现在,不管你输入任何长短的音频,最终它的频谱都是shape=(124, 129, 1)。从图上我们也可以看出,不足的就算后面补0,也得凑成个16000长度。下面,真的要开始构建神经网络了。2.4 构建模型和训练依照老张的要求……我现在不想提他,因为我的手指被绿萝电的还有点发麻。依照要求……他要四种命令,分别是:前进、停止、左转、右转。那么,我就搞了四种音频,分别放在对应的文件夹下面。从文件夹读取数据、将输入输出结对、按照比例分出数据集和验证集,以及把datasets划分为batch……这些操作,在TensorFlow中已经很成熟了。而且,随着版本的更新,越来越成熟。体现在代码上,就是字数越来越少。此处我就不说了,我会把完整代码上传到github,供诸君参考。下面,我重点说一下,本例子中,实现语音分类,它的神经网络的结构,以及模型训练的配置。import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import models
model = models.Sequential([
layers.Input(shape= (124, 129, 1)),
layers.Resizing(32, 32),
layers.Normalization(),
layers.Conv2D(32, 3, activation='relu'),
layers.Conv2D(64, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Dropout(0.25),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dropout(0.5),
layers.Dense(4),
])
model.compile(
optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
其实,我感觉人工智能应用层面的开发,预处理和后处理比较难。中间的模型基本上都是有固定招式的。第1层layers.Input(shape= (124, 129, 1))叫输入层,是训练样本的数据结构。就是我们上一节凑成16000之后,求频谱得出的(124, 129)这个结构。最后一层layers.Dense(4),是输出层。我们搞了“走”,“停”,“左”,“右”4个文件夹分类,最终结果是4类,所以是4。头尾基本固定后,这个序列Sequential就意味着:吃音频文件,然后排出它是4个分类中的哪一种。那么中间我们就可以自己操作了。Normalization是归一化。Conv2D是做卷积。MaxPooling2D是做池化。Dropout(0.25)是随机砍掉一定比例(此处是25%)的神经网络,以保证其健壮性。快结束时,通过Flatten()将多维数据拉平为一维数据。后面给个激活函数,收缩神经元个数,准备降落。最后,对接到Dense(4)。这就实现了,将前面16000个音频采样点,经过一系列转化后,最终输出为某个分类。最后,进行训练和保存模型。model = create_model()
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath='model/model.ckpt',
save_weights_only=True,
save_best_only=True)
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=50,
callbacks=[cp_callback]
)
filepath='model/model.ckpt'表示训练完成后,存储的路径。save_weights_only=True只存储权重数据。save_best_only=True意思是只存储最好的训练的结果。调用训练很简单,调用model.fit,传入训练集、验证集、训练轮数、以及训练回调就可以啦。2.5 加载模型并预测上一节中,我们指定了模型的保存路径,调用model.fit后会将结果保存在对应的路径下。这就是我们最终要的产物:我们可以加载这些文件,这样就让我们的程序具备了多年功力。可以对外来音频文件做预测。model = create_model()
if os.path.exists('model/model.ckpt.index'):
model.load_weights('model/model.ckpt')
labels = ['go', 'left', 'right', 'stop']
# 音频文件转码
audio = get_audio_data('mysound.wav')
audios = np.array([audio])
predictions = model(audios)
index = np.argmax(predictions[0])
print(labels[index])
上面代码中,先加载了历史模型。然后,将我录制的一个mysound.wav文件进行预处理,方式就是前面说的凑成16000,然后通过短时傅里叶解析成(124, 129)结构的频谱数据。这也是我们训练时的模样。最后,把它输入到模型。出于惯性,它会顺势输出这是'go'分类的语音指令。尽管这个模型,从来没有见过我这段动听的嗓音。但是它也能识别出来,我发出了一个包含'go'声音特性的声音。以上,就是利用TensorFlow框架,实现声音分类的全过程。音频分类项目开源地址:github.com/hlwgy/sound再次提醒大家:要求TensorFlow 2.6(2021年10之后版) + python 3.9。因为,里面用了很多新特性。旧版本是跑不通的,具体体现在TensorFlow各种找不到层。三、我们的合作我带着成果去找老张。老张沉默了一会儿,不说话。我说,老张啊,你就说吧。你不说话,我心里没底,不知道会发生啥。老张说,兄弟啊,其实语音小车这个项目,没啥创意。我昨天才知道,我们车间老王,三年前,自己一个人,就做出来过了。说完,老张又沉默了。我安慰他说,没关系的。这个不行,你就再换一个呗。老张猛然抬起头,眼睛中闪着光,他说:兄弟,宇宙飞船相关的软件,你搞得定吗?!火星车也行。我不紧不忙地关闭服务,并把电脑收进包里。我穿上鞋,然后拿上包。打开门,回头跟老张说了一句:兄弟,三个月内,我们先不联系了吧。
GG
从HR的考核,详解AI的:准确率、精确率、召回率
一、从绩效考核说起绩效考核,是很多人力资源从业者(HR)的噩梦。这在IT行业尤为突出。我见过很多IT公司的HR,因为思考如何对程序员做考核,掉光了头发。直到他们回到劳动密集型企业,又重新长出了浓密的黑发。很多HR在对程序员们做绩效考核宣讲的时候,都会面临舌战群狮、虎、豹、大象,最后无言以对,自残而退。其实,他们纠结的问题基本上就是:质与量,到底该如何衡量和判断。我觉得对程序员,就要用程序员的方式去考核。只有这样,才能做到无懈可击,令其信服。下面我就来讲讲,程序员是如何给一套程序做“绩效考核”的。他们给程序做考核时,也是有指标的。而这个指标,他们自己也很难找出漏洞,也会让他们左右为难。但是最终却有解决方案。二、程序员的指标假设程序员写了一个程序,要从一堆货物里面,识别出合格品和次品。这个程序,做个不恰当的类比,就像是HR搞了一套考核指标,要从众多员工当中,筛选出优秀员工与普通员工。首先,我们无法一眼就看出合格品和次品。就像员工脸上没有贴着“优秀”一样。所以,我们看到的样本基本是:打眼一瞧,都一样。但是,它们又是有细微差别的,比如轮廓、颜色、大小、透明度等。我们正是靠这些多维度的特征,依照考核规则,来评判出是否合格。这就好比我们有一副神奇的眼镜,戴上一看,样本是否合格,它们就显现出了原形。当然,这是最终理想的状态。实际上,这个眼镜需要不断实施,反馈,调整。于是,程序员就用右手,写了一个人工智能的程序。随后,又训练了一套模型,相当于制度的试运行。最终,这套程序可以把合格的样本,用蓝色的小圈,框出来。我们从上帝视角看,红色的点代表合格,灰色代表不合格。其实,程序是不知道底牌的。那我们看到,A圈识别的很对,圈中的样本代表合格,它都识别对了。B圈差点儿意思,将一些次品也识别成合格了。C圈整个完蛋,黑白颠倒,识别的所有合格,实际上全是次品(这种考核完全是逼优秀的人离职的节奏)。2.1 T-F-P-N这里面引入一套概念,因为后面要用到公式进行计算。假如我们的程序识别出了一些合格的样本。那么,基本上分为这么两种情况。第一,它识别对了。我们用T也就是true表示它成功了。这里面其实还分为两种情况:1、把实际上合格的,判定成了合格。这个判定的合格我们叫P,也就是positive。那么这类情况就是TP;2、判定为合格之外的那些样本,实际上它也真的不合格(Negative)。那这类也算是评对了,我们把这类叫做TN。第二,它识别错了。我们用F也就是false表示它出错了。这里面其实也分为两种情况:1、把实际上不合格的,判定成了合格。那么这类情况就是FP;2、判定为合格之外的那些样本,实际上也有合格的,但是你没有识别出来,你认为它Negative,我们把这类叫做FN。HR看到上面的TP、TN、FP、FN四种情况,骂骂咧咧地关了页面,走了。只剩下由程序员转行的HR,还在继续看。2.2 准确率 Accuracy程序员引入的第一个评判标准叫准确率。准确率表示我猜对的占比。比如,我在研发部评选优秀员工。所有的100名员工中,我评出来90个优秀,实际上这90个确实优秀。那么我这套考核的准确率就是100%。准确率的公式是:Accuracy=(TP+TN)/(TP+TN+FP+FN),表示预测正确的占总数的比重。准确率这种指标,看起来无懈可击,非常完美。但是,程序员并不这么认为。程序员觉得漏洞很大,很容易钻空子,而且极其不公平。举个例子,假如有一套识别程序或者评价制度,它看谁都像是好人。而且,它又凑巧都遇到了好人。那么,局部来看,它的准确率指标,非常棒。但是,换一个角度,它居然很糟糕。就像是我开发了一套识别癌症病人的程序,这个程序根本不思考,给谁检测都说没有。那么,你说准确率高不高,非常高。但是,这真没什么用。患癌症的人数,100人中可能只有1例,它的作用就是要找出这1例,而不是那些健康的99例。它虽然相对准确,但是绝对不精确。因此,程序员又提出了第二个指标:精确率。2.3 精确率 Precision精确率,是用来保证不出错的。比如审判,如果你没有足够的证据证明其有罪,那么就是无罪。不要求你处理多少案件,但凡处理一个,都要保证准确。这,就是高精确率。精确率的公式就是:Precision=TP/(TP+FP),表示预测正确的占预测数量的比重。比如,总共有1000个样本,我预测了其中100个样本。这100个当中,99个预测对了,1个样本预测错了,那么精确率就是99%。它和准确率的区别就是,它要对每一个样本都负责,一个都不能错。如果说高准确率,保证了集体的利益。那么,高精确率,保证了个体的利益。但是,程序员发现,精确率虽然公平公正,但是漏洞更大。举个例子,我预测了3个样本,这3个样本全都正确。带入公式Precision=TP/(TP+FP)计算就是:3/(3+0)=100%。这里面,我们看到一个小技巧。预测几个不重要,关键是要让FP等于0。一旦FP=0,精确率就恒为100%。也就是说,高精确率虽然强调,要把每一件经手的事情干好,但是也有一个逻辑漏洞:干100件事和干1件事并不重要,不出错才重要。这可能会导致,少干才会少出错,而且拣着容易的活干也会少出错。于是,为了解决上面的问题,程序员又想出一个指标:召回率。2.4 召回率 Recall这次我们先看召回率的公式:Recall=TP/(TP+FN)。因为有了上面的经验,这次我们先找它的漏洞,只要让FN=0,这个召回率就会一直是100%。那么,我们再回忆一下,FN代表什么?“我们用F也就是false表示它出错了。判定为合格之外的那些样本,实际上也有合格的,但是你认为它不合格,判定了Negative,这类叫做FN。”如何让FN等于零,那就是扩大检测范围。因为公式是Recall=TP/(TP+FN),里面并没有涉及FP和TN。因此,只要扩大检测范围,哪怕存在误判的滥竽充数者FP,就会让FN=0,不会影响100%的召回率。所以,召回率的精髓可以用一句话概括:“宁可错杀一千,绝不放走一个”。这样就解决了那些少干活、拣活干的情况。因为,只有保证所有活都干了,才能提高召回率。三、综合权衡现在我们来总结一下,程序员使用的这些指标。准确率提倡尽量把所有事情都干好,精确率要求尽量别在某一件事上出问题,召回率鼓励尽量别让任何一件小事掉在地上。这可能就是你老板一直要求的:要多,要快,还要好,还得省。尽管程序员很抵触这类要求,但是他们仍然会对自己的程序提这样的要求。下面可能是我们期望的最终状态,每一个都被100%准确地识别出来。而且,换一批样本,结果依然不受影响。但是,根据我多年的活生生的生活经验,又便宜,质量又好,花5块钱买了,随时可以卖5万的商品,不好找。这一点,程序员也知道。因此,需要一个权衡。权衡的目的就是即不麻烦,又省成本,最后结果还大体满意。权衡和都要,最大的一个区别就是:权衡允许妥协。有人说,权衡是中国几千年文化中的一个玄学问题。你把结果引向这里,属于说了一堆儿,但是又啥也没说。估计是想甩锅了。并不是,程序员这里就不存在玄学问题(除了在我这里可以运行之外)。遇到玄学问题,你找他们讨论,必定会得出一个确定的结论,即便这个答案是“我也不知道”。他们从不会说上半个小时,而没有明确的态度。对于权衡,程序员也是有一个公式的。这个公式HR就不用懂了,程序员可以去了解一下。大体意思就是,召回率高(干的多),必然会导致精确率低(出错多)。反之,干得少,出错也少。因此引入一个参数β用于调节决策结果。如果β的取值大于1,则表示强调召回率。适合创业阶段,鼓励多干,先野蛮生长。如果β的取值小于1,则表示强调精确率。适合守业阶段,鼓励干精,要稳打稳扎。如果β的取值为1,则表示两者都强调。但是,你不能要求都是100%,也不能容忍都是50%。这么看,程序员还是把决定权交给了使用者,他们只负责提供多种解决方案。其实,如果大家有什么问题,可以问问身边的程序员。没准儿,他们会有解决方案。最起码,你多了一个看问题的角度。
GG
终于,有人讲傅里叶变换了
一、傅里叶变换:著名学科劝退师傅里叶变换,一度被推上了玄学的位置。这位说,了解了它,能改变你,认识世界的方式。那个说,我喜欢信号系统,但是看了2周的傅里叶变换,放弃了。也有很多人,写了科普教程,有零基础教程、保证看懂教程、掐死教程。但是,当他们亮出数学公式的那一刻,很多人陷入沉思:我……是从哪里开始,就已经看不懂了……这些现象,激发了我强烈的表演欲望,我要登上舞台,为大家表演。我的表演,如果你看不懂,请掐死我。二、写博客:傅里叶变换进行时我正写博客,没有骗你,真的在写,有图为证(是的,用vscode写博客第一人)。此时正是深夜,我屋内的电脑,发出“嗡嗡”的散热声。同时,窗外的马路,飞驰而过一辆渣土车,发出“嘀~”一声刺耳的鸣笛。我,是如何,听到,声音的。物体振动发出的声音,在空气中传播,进入我的耳朵,推动我的耳膜,声音大就推的强,声音小就推的弱,经过一系列处理,我就听到了电脑和汽车。仔细想一下,有点奇怪,我居然根据耳膜被推动的情况,分析出了声音里,包含电脑散热风扇和汽车喇叭两种物体。其实除此之外,我还听到了楼上放电视、窗外的虫子叫、电动车报警器响了……这个声音发出和接收的场景,就如同你用手指在桌子上有规律、有轻重地敲击。我相信,你能听出这番敲击是打快板,那番敲击是非洲鼓。但是,你很难通过敲桌子的节奏,听出来:你的同事在敲键盘,同时路上响起了救护车的声音。先给你3分钟的时间思考一下,我去上个厕所先。人类几万年的历史,不知道是否有人想过这个问题,拿根棍子捅你的耳膜,你就能分辨出十几样不同的发声物体,这到底该怎么来解释呢?其实,这——就是傅里叶变换(我能让你猜着,这突然地转场?)。如下图所示:推动你耳膜的力度就是左侧的波形,你的大脑经过傅里叶变换,分析出了十几种不同的发声物体。好了,现在拿出傅里叶变换的定义:傅里叶变换,表示能将满足一定条件的某个函数表示成三角函数(正弦和/或余弦函数)或者它们的积分的线性组合。定义看不懂没关系,只要能看懂前面说的,分辨声音的那个例就行。关于什么是傅里叶,此处打个标记叫[A]。如果你看不懂,就说从[A]开始就糊涂了,来掐我时,好让我死得明白。但是,有一点谈资我们要知道,这个理论是法国人傅里叶在1807年提出来的。那时,嘉庆皇帝刚抄了和珅的家没过几年。三、描述一个波:都有独特的气质你糊弄我?你拿人这种有灵性的生物,来举数学的例子,数学公式能解决道德问题吗? 再说,声音混在一起,就如同染料混在一个缸里,你还能分出来?我就不信了!我感觉有人跟我对话,充满了质疑和不屑。那好,我换一个例子,收音机大家都知道吧。全国几百家广播台,各家都往外发送无线电波。我们并非生活在多个平行宇宙。因此,这些电波没有VIP通道,肯定都是混在一起在空中传播的。就像下面这样:那……收音机,可以收听指定电台的节目。这个是机器,它的收音设备,没有你的耳朵那么复杂,也没有神经元,这一点儿灵性也没有,但它是可以分解指定波段的。好了,继续看我表演,说一下声音的两个属性:频率、振幅。3.1 声音的频率声音是由振动产生的,振动一般都是……就……就像这样:一个点围绕着某个相对固定的中心,周期性地移动。比如,一个弹簧吊着一个小球,上下振动。如果,一边记录时间,一边记录位置,其实它的形状就是弦曲线(我保证这个词,已是本文最专业的术语),也可以叫波。有些物体振动的快,1秒钟反复来回1000次。有些物体振动的慢,1秒钟才往返1个周期。这个单位时间内振动一个周期的次数,我们就叫频率。因为材质不同,不同的物体振动的频率是不一样,波形也不一样。下面是物理课上,敲击音叉振动的声波。下面是用嘴吹试管的声波。只要我们听到的声音不一样,基本上它们声波的频率也是不一样的。就算都是口琴,低音区是这样的:高音区是这样的:我们发现,高音区比较密集,相同单位内振动的次数更多,也就是频率大。反之,低音区频率小,比较稀疏。你听到的每种不同类型声音,都有自己固定的频率,就像每个地方菜的口味一样,是有差别的。此处打个标记叫[B],如果你看不懂,就说从[B]开始,就乱了,来告诉我。3.2 声音的振幅声音的振幅就是声音的大小。大小和频率没有任何关系。频率是某个时间内,推了耳膜多少次。振幅是这次推动,花了多少力气。还是拿音叉举例子,声音的振幅只影响音量,波形、频率都不变。你敲出原子弹爆炸的音量,它也是那个形状。声音大时,音叉的波形:声音小时,音叉的波形:这很好理解,不管我小声说话,还是大声说话,你都能听出是我。但是,我大声说话,你会震得耳朵疼(耳膜遭受一记重拳)。再来点儿谈资吧。我们人类能听到的频率范围是20Hz(1秒钟振动20个周期)到20000Hz。超过20000Hz为超声波,低于20Hz为次声波。我们常说,蝙蝠发出的超声波,不是因为它声音太小或太大,导致我们听不见。其实是因为它振动得太快,蝙蝠已经说了5000字了,但是我们只收到一个“的”。这是没有意义的。同样,我们人类能发出的声音频率也是有范围的,范围在250Hz-4000Hz之间,这是由我们人类声带的肉质(额……不知道为啥突然出来这个词)决定的。振幅和频率的区别,此处打个标记叫[C]。如果你了解了,频率和振幅这两个波的属性,后面就好说了。四、说傅里叶变换吧:我不抗拒了!傅里叶发现,这个世界上的人,对事物的理解不透彻,描述的也不科学。世人都以时间为轴线,来描述事物。比如,描述一段1分钟的录音:第1秒说了什么,第2秒说了什么……。世人摇摇头,叹了口气:傅里叶,你想咋着?4.1 时域傅里叶说,以时间作为参考的描述,叫时域分析。比如下面这图,这是个时域图。我们看到这1秒内,它很忙,来来回回,从0到1秒跑着,从-1跑到1,反复了5次。所以,这段波的频率是5,最大振幅是1。整图描述的是,每一个时刻的信号值:如果这段波,不是1秒,而是1个小时,那么它就是3600个这样的图拼在一起。时域的概念,打个标记叫[D]。不懂的话,就看看表,再看看路上的汽车,刚才在你左边,1分钟后到了你的右边。2分钟后,它还在原地等红绿灯,每个时间都有对应的位置。这也是我们普通人认识世界的方式。4.2 频域还是那段波,而如果用频域来描述,那就是这样的:横坐标表示频率,纵坐标表示振幅。上面这个图表示:这里面有一段波,频率为5,振幅为1。傅里叶告诉世人,我这个频域,可以抵得过时域的千年万年。两者说的是同一件事,只是分析的角度和描述手段不一样。呵呵哈……哈呵呵,傅里叶冷笑着离开了,他连笑,都带着波形。频域,打个标记叫[E]。4.3 波的合成和分解世界上的波,不会是上面说的那么单纯,实际上是很复杂的。波,根据固定的频率振动,原本很单纯。只是,大家都在同一个空间内振动,相互之间发生了抵消和促进,相互融合,进行叠加,导致它很复杂。波的叠加,打个标记叫[F]。不明白的话,赶紧评论区输入[F],让我知道,我会加强细节描述。既然能叠加,那么能拆解吗?其实,是可以的。不只是听觉,人类的味觉也做到了,你吃一口菜,可以尝出来里面有糖,有醋,有辣椒。它的理论基础就是,味觉具有一定的标准,辣就是辣,混到哪里都是辣。波也一样,大家都是振动,震动就是弦曲线,只是频率不一样。我们可以通过转啊转,各种组合,你护拢我,我护拢你,总能拼起来。规律的波,并非一定得是“U”形,下面这几种情况,也都是规律的波形。甚至,下面这个形状,也是规律的波形。可以拆解为若干组波的叠加。二维空间里没有规律,就去从三维空间里面找。即便实在无法分解,我们也可以认为它的周期无限长,我有规律,只是本次循环还没到头呢?着啥急!波的分解,不用明白具体是怎么分的。知道能分就行,打个标记叫[G]。4.4 时域拆解频域一段复杂的波,可以分解成,多段,规律的单纯波的集合。然后,对这些规律的波从频域进行描述,就有了整段波的谱线图。下图是一个综述。f指的是频率维度。这张图,很清晰地说明了波形、时域和频域的关系。很多教程,开局就拿出这张图,可解万难。但是,我说了这么久,才敢亮出来。此图打个标记叫[H],还不明白来掐我。4.5 频域复原时域根据频域,也能复原成时域(此处用理想波,便于理解概念,还有相位等问题,不敢具体展开了)。上面图里绿色的频率和振幅,可以描述一个波。如果把谱线上描述的波,依次画出来,然后做叠加。这样就复原了时域的波形。这个过程就像是泡发木耳。波的复原,打个标记叫[I],有疑问请评论(别的平台复制本文可能不回复,但是掘金TF男孩一定给你回复,且回复的比你问的还要多)。五、傅里叶变换有什么用?傅里叶变换,在生物领域,你已经享受到它的益处了,你能分辨出一段声音里不同的声源,一口食物里不同的味道。除此之外,我们可以拿时域转频域,对频域的特征进行二次加工,然后再恢复成时域,以此来做文章。比如那些演唱会上调节声卡的人,如果你高音不足,可以给你升上去,低音太高,可以给你降下来。举个例子,比如变声软件。把一段音频,分离出男声和女声,将男声改为女声的频率,然后还原回去,实现男声变女声。再比如,声音压缩。将一些低频波形,合成一条。如下图蓝色线条,可以替代其下方的其他多条波形,而又不会影响人们的听觉判断,以此实现了数据的压缩。除了在音频和信号的应用之外,在视觉上也有广泛应用,比如轮廓提取、美颜磨皮等等。甚至,我还有一个想法,研发一款不会颠簸的汽车:应用部分,如需讨论,打个标记叫[J]。六、代码实践如果,你已掌握了上面说的傅里叶变换。那么,下面,我们就要实践一下了。实践的目的在于应用,表示我们已经可以利用代码,进行傅里叶变换了。这可以加深理解,并储备知识。以下代码,采用python语言演示。6.1 生成波形傅里叶变换,是对波形做变换。因此,要变换我们首先得有一段波形。比如这一段音频,这是单词“stop”的声音波形(3个小高峰,s-tɒ-p):信息的读取和绘制都很简单:from scipy.io import wavfile
import matplotlib.pyplot as plt
# 读取音频数据
fs, audio_wave = wavfile.read("stop.wav")
# 采样率 fs, 音频数据 audio_wave
plt.plot(audio_wave) # 绘制音频数据
plt.show()
这样看的不直观,我们选取开头的160个点,plt.plot(audio_wave[0:160])来绘制这一小段,这样看就是线条的波形了:从总图看,开头那部分是一马平川。但是,当我们放大这部分细节时,我们看到的却是满眼的崎岖和坎坷。我们看到上面的wavfile.read函数返回两个值,一个叫采样率,一个叫音频数据。声音的读取和波形绘制,此处打个标记叫[K]。6.1.1 采样率采样率就是采样的频率,描述多长时间记录一次数据。还记得打点计时器吗?那个在纸上打点的频率,就叫采样率。比如,1秒钟打60个点,采样率就是60。60的采样率,能记录上信息吗?这得看对方信号有多快。如果对方太慢,会导致一个地方频繁打点,其实打一个点和打100个点没有区别,这很浪费。如果对方太快,可能会记录不全,甚至它跑了好多周期了,我们一个点都采不上。蝙蝠的超声波就是频率太快了,我们的耳膜采样不上。下图是采样率为1000和100时,记录的图:可见,信号不复杂时,采样率高和超高,没有什么变化,都能记录全了。区别只是,用1万个点来描述,还是用100个点来描述。下图是采样率为20时,绘制的同一段信号的图:采样率一旦比信号低(小于信号最高频的2倍)了,有些信息的细节就记录不上了。对于录音机,我们说,它是否能把声音的细节记录上,主要看它的采样率高不高。关于采样率,此处打个标记叫[L],你是否get全了呢?为什么要了解这些概念?因为我们要制造声波数据,需要先了解它。为什么要造数据(这自问自答,我也是不要脸了)?因为我们造的数据,是我们已知的参数,便于我们和求出的结果做对照。要造数据,还需要了解弦函数sin(θ)。就是横轴上一个点,对应纵轴上一个值,把它们画出来,就是一个波:6.1.2 绘制波形现在,我们来搞一系列的横纵坐标点。import numpy as np
#采样率,单位时间内采集多少次样本(1秒内记录1000次,采样率1kHz)
Fs = 1000
#采样周期,频率的倒数,相邻两点的时间间隔,1秒打10个点,采样周期就是0.1秒
T = 1/Fs
#信号长度,信号有多少个点
L = 1200
# 时间轴X轴的集合,这批信号每次采集的时间点
t = np.arange(L)*T
# 幅度值Y轴的集合,1200个点对应的Y轴的值
S = np.sin(2*np.pi*t)
上面代码,t是横轴坐标,S是纵轴坐标。我们先来看1秒内发生的故事,因为我们设置的是1秒一个周期,其他时间段都是循环的。我们的采样率是1000,也就是1秒记录1000个点。1秒内,t的值是[0.000,0.001,0.002,……,1.0],把一个2π(完整周期)分成了1000份。对这1000个点求函数的值,也就是S = [0,…0.5,…1,…0,…-0.5,…-1,…,0]。如果用代码画出来的话。import matplotlib.pyplot as plt
plt.plot(t[:1000], S[:1000])
plt.show()
数据就是下图所示:上面的例子中,1秒进行了一个周期,频率是1。如果,我们要一个频率为5的波形,那又该怎么整呢?很简单,让S = np.sin(2*np.pi*t*5),里面乘以5,再画出来,就是下图这样:如果想要一个频率为8的波形呢?乘以8就可以了!关于波形数据的生成和绘制,此处打个标记叫[M]。如果大脑过载的话,要及时上报啊。下面,我们准备了一组复合波数据,作为测试样本。这批样本,其实就是一段频率为5的波,叠加一段频率为20的波。其中有点插曲(故意设计的),这个频率为20的波,他的振幅为0.5(频率5的振幅是1),波形是这样的。两段波相加,混合起来是这样的:S_5 = np.sin(2*np.pi*5*t)
S_20 = 0.5*np.sin(2*np.pi*20*t)
S_25 = S_5 + S_20好了,这个就是我们自己造的测试数据。下面,我们来做傅里叶变换,看看能不能分析出来它的组成。关于波的叠加,此处打个标记叫[N]。6.2 傅里叶变换 FFT今天,我们是用傅里叶变换,不是写傅里叶变换。所以过程会很简单,简单到就3个字母fft。python库scipy.fftpack中有fft,它就是专门处理傅里叶变换的。from scipy.fftpack import fft
# 完成傅里叶变换
Y = fft(S_25)
# 把结果画出来
p2 = np.abs(Y)
p1 = np.abs(Y)[:int(L/2)]
# Fs、L是之前生成波形时的采样率和信号长度
f = np.arange(int(L/2))*Fs/L
plt.plot(f,2*p1/L)
plt.show()
采用傅里叶变换,对测试波形进行分析,得出了这个谱线。从图上我们看出,整段波里,存在2个频率的波形,一个频率为5,振幅为1;另一个频率为20,振幅是0.5(刚才就是按照这个规则造的假数据)。这说明,分析的很对。此时,我们可以玩耍的就多了。比如增大高频的频率,或者把低频的删除,然后再还原为波形,就做到了对声音的增强、提取和调整。关于谱线,能看懂就行,此处打个标记叫[O],看不懂来掐我。等会……这里面存在一个问题,那就是:这个分析结果,把时间维度给忽略了。然而,时间是一个很重要的参考。上面演示的是S20和S5叠加的情况。如果要是S20追加S5呢?意思就是有先后顺序,就像这样:S_20_5 = np.append(S_20, S_5)这段波形,先是一段小声(振幅小)的高频(频率大),然后是一段大声(振幅大)的低频(频率小)。我分析啊,应该先是女生小声嘀咕,然后男生大声呵斥。这个时间顺序里面,是包含着故事的。波形分析里没有时间维度,就相当于食品说明里,只有组成成分,没有含量占比。这,没法判断它的口味和营养。于是,短时傅里叶变换就出现了。6.3 短时傅里叶变换 STFT本文的毕业考核,就是最终能看懂下面这张图。不着急,我慢慢说。为了加上时间(序列)维度,才有了短时傅里叶。其实,它的本质还是傅里叶,只是它从时域信号中,依次选取一段样本,进行傅里叶分析。分析完了,再合并起来,这样就有了时间维度了。短时傅里叶变换的调用依然很简单,比傅里叶变换多一个字母,它是stft。传入的参数是要分析的波形数据,以及采样频率,返回值是:时间t、频率f、振幅Z。from scipy.signal import stft
# 短时傅里叶变换
f, t, Z = stft(S_20_5, fs=1000)
# 把结果画出来
z = np.abs(Z)
plt.figure(figsize=(10,10))
plt.xlabel("author:handroid@126.com Time(s)")
plt.ylabel("frequency")
plt.ylim(ymin=0, ymax= 30)
plt.title("from https://juejin.cn/user/615370768790158")
c = plt.pcolormesh(t, f, z, cmap ='Greens', vmin = 0, vmax = 1)
plt.colorbar(c)
由短时傅里叶变换得出数据生成的这个图,叫三维频谱图。相比于谱线图,它加入了时间维度。其中,横坐标是时间维度,这个时间可以不和波形图的时间对应。它和上面的fs采样点参数有关。纵坐标,则表示波形的频率。它虽然是一幅2维图,但可以表示3维的数据。他的第三维度就是色块的色值,表示振幅。色值越亮,振幅越大。如何来分析这个图:从纵坐标入手:这段信号,包含2段频率,一段频率是20,另一段频率是5。其中20频率段振幅较小,5频率段的振幅较大。从横坐标入手:1.2s之前,只有高频信号;1.2s之后,高频消失,出现了低频信号,低频音量较大。从振幅入手:声音较大(色值较重)的主要出现在后半段,且是低频信号发出,推测是男生发火了。关于三维频谱图的解读,此处打个标记叫[P],看不懂就告诉我。好了,试试毕业考试吧,看下面这幅频谱图。这是一段音乐,上面两个波形是左右声道,下面火红的颜色是频谱,右侧坐标是音调(频率),比如C调,G5调等。这段音乐,哪段音域最活跃呢?这个歌手是女高音,还是男低音?七、保命条款朋友们,我讲的很初级,真的,一点都不谦虚。没讲相位,没讲函数的每一个参数,能省就省。这只是为了科普,让大家知道有这么一个东西,不但要知道,最好能吸收进去一些知识。学到的知识才是自己的。本文的目标读者,就是那些连sin和cos都还给初中数学老师的朋友们。他们真的找不到通俗的资料去学习,但是他们又有强烈的求知欲,尽管很多医生……不是,很多研究生,大呼他们已经尽力了。我不会再往深里讲了,因为本身我也不太懂。但是,我相信本文也是能帮到一些人的,尤其本文上传了将近50张图,很多动态图。如果有错误,欢迎大家指正,我会做修改,同时这也是我学习的机会。
GG
人工智能图像实战课:夏天照片归类器
前言我喜欢用简单的技术,去解决生活中的实际问题,以此来达到技术推广和普及的目的:你看,就是这个原理,挺简单吧!你也可以做一做,我们一起学习,共同成长!本文,我将以手机中带有夏天元素照片的自动归类为例,来普及人工智能在图片分类中的应用和实践。一、发现问题一个夏天,你会拍多少张照片?你有没有想过,去整理一下相册,去总结一下,这个夏天你都关注过什么?西瓜、粽子、海滩、烧烤、蝉,还有女票……我们来详细看看:唉呀,太乱了,懒得搞,要是有人能帮我整理就好了!算了,他自己都不愿整理,还能帮我?哎,计算机能自己整理吗?!你得敢想一些,这个问题,计算机真的会自己整理,这就是人工智能当中的图片分类,而且已经相当成熟了。二、分析问题你知道吗?人工智能在修炼成智能之前,都是“智障”。它像一个刚出生的孩子一样,什么都需要去教:这个是西瓜,那个是风扇……和孩子不同,人工智能成长的周期比较短,如果数据足够多,他可能分分钟超越你。下面是一张肺部光学影像图,一般的医生看,他可能说有毛病,因为病理上看是阳性,找几个老医生戴上老花镜在聚光灯前研究五分钟,老医生拍着年轻人的肩膀:仔细看,这是个假阳性!年轻医生仔细一看,哦,确实是!这种假阳性,人工智能1秒钟能看600张,而且还张张准确。当然,这是有前提的,前提就是你需要像教孩子一样,花费大量精力去训练它,改正它,验证它,最终它才能成为比我们还要快速、准确的人工智能。我们这个分类夏天照片的问题也一样,我们首先要告诉计算机,什么样的照片属于什么类别,然后训练它,最后它就可以自己进行分类了。三、解决问题经过上面的分析,我们明确了要做三件事:给现有的夏天照片分类将标记好的照片样本交给计算机训练让计算机自己分类夏天的照片3.1 整理训练样本我大体翻阅了一下照片,发现基本上只有10类,分别是:空调、烧烤、海滩、蝉、赛龙舟、电风扇、雪糕、泳衣、西瓜、粽子。这么多分类,你要怎样去将他们整理出来,对照片一张张地重命名吗?不!我们用技术手段,去写一个快速标记器。上图所示,其实它有好多功能:加载文件夹下的所有图片,然后一张张展示出来每张图片上展示分类,可以鼠标点击,也可以键盘输入序号,进行标记标记完成后,将此图片文件名添加前缀,然后自动切换下一张代码如下所示(python代码):import tkinter as tk
from tkinter.messagebox import showinfo
import os
from PIL import Image, ImageTk
dir_path = "dataset" # 文件夹路径
prefix = "marked" # 文本标记过后的前缀,防止重复标记
top = tk.Tk()
top.title("图片标记器")
width = 640
height = 480
top.geometry(f'{width}x{height}')
# 标记当前图片,并切换到下一个图片
def next_img(type):
global index
o_name = img_file_names[index]
o_path = dir_path+"/"+o_name
n_name = prefix+"_"+type+"_"+o_name
n_path = dir_path+"/"+n_name
# 重命名文件 abc.png-->marked_1_abc.png
os.rename(o_path, n_path)
# 如果是最后一个,给出提示,关闭程序
if index+1 >= len(imgs):
showinfo(title = '提示',message='已经是最后一个')
top.destroy()
return
# 索引加1,更换图片
index = index + 1
label_img.configure(image=imgs[index])
imgs = [] # 所有图片
img_file_names = [] # 所有图片的名称
all_files=os.listdir(dir_path)
for f_name in all_files: # 遍历所有图片
if f_name.startswith(prefix) or not f_name.endswith(".jpg"):
continue # 如果已经标注过了,下一个
img_path = dir_path+"/"+f_name
img = Image.open(img_path) # 打开图片
photo = ImageTk.PhotoImage(img) # 用PIL模块的PhotoImage打开
# 把图片和名称存储起来
imgs.append(photo)
img_file_names.append(f_name)
# 先展示第一个
index = 0
label_img = tk.Label(top, image=imgs[index])
label_img.place(x=20, y=20)
class_names = ['空调', '烧烤', '海滩', '蝉', '赛龙舟', '电风扇', '雪糕', '泳衣', '西瓜', '粽子']
# 按键的回调
def callback(event):
c = event.char # 获取键盘输入
if c in ["0","1","2","3","4","5","6","7","8","9","x"]:
# 如果输入的是上面的数字,就给这幅图分类改名字
next_img(str(c))
# 绑定键盘事件
frame = tk.Frame(top, width = 20, height = 20)
frame.bind("<Key>", callback)
frame.focus_set()
frame.pack()
# 摆放数字
b_x = 160 # 基础X位置
b_y = -10 # 基础Y位置
d_l = 75 # 间距
button = tk.Button(top, text="1 "+class_names[1], command=lambda: next_img("1"))
button.place(x=b_x, y=b_y+1*d_l)
button = tk.Button(top, text="2 "+class_names[2], command=lambda: next_img("2"))
button.place(x=b_x, y=b_y+2*d_l)
button = tk.Button(top, text="3 "+class_names[3], command=lambda: next_img("3"))
button.place(x=b_x, y=b_y+3*d_l)
button = tk.Button(top, text="4 "+class_names[4], command=lambda: next_img("4"))
button.place(x=b_x, y=b_y+4*d_l)
button = tk.Button(top, text="5 "+class_names[5], command=lambda: next_img("5"))
button.place(x=b_x, y=b_y+5*d_l)
button = tk.Button(top, text="6 "+class_names[6], command=lambda: next_img("6"))
button.place(x=b_x+d_l, y=b_y+1*d_l)
button = tk.Button(top, text="7 "+class_names[7], command=lambda: next_img("7"))
button.place(x=b_x+d_l, y=b_y+2*d_l)
button = tk.Button(top, text="8 "+class_names[8], command=lambda: next_img("8"))
button.place(x=b_x+d_l, y=b_y+3*d_l)
button = tk.Button(top, text="9 "+class_names[9], command=lambda: next_img("9"))
button.place(x=b_x+d_l, y=b_y+4*d_l)
button = tk.Button(top, text="0 "+class_names[0], command=lambda: next_img("0"))
button.place(x=b_x+d_l, y=b_y+5*d_l)
top.mainloop()
通过这个工具,基本上就做到了浏览一遍就标注完成了,标注后的效果如下图所示,同类的在一起。3.2 训练数据人工智能框架,我使用TensorFlow 2.3版本。选择TensorFlow,因为我是它的婴幼儿推广大使。选择2.3版本,因为它可以直接从分类好的文件夹读取样本,自动分类,相比之前方便太多了。我们将上面标注好的图片放到对应的文件夹里,都在dataset下,像下面这样:然后就可以写代码了。首先,导入相关的包:import tensorflow as tf
import numpy as np
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
import pathlib
import cv2
import os
from matplotlib import pyplot as plt
import shutil
然后,准备要训练的数据集:# 统计文件夹下的所有图片数量
data_dir = pathlib.Path('dataset')
batch_size = 64
img_width = 256 # 图片宽度
img_height = 256 # 图片高度
# 从文件夹下读取图片,生成数据集
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset='training',
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size
)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size
)
# 数据集缓存处理
AUTOTUNE = tf.data.experimental.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
然后,构建模型:def create_model(img_height=256, img_width=256, num_classes=10):
# 构建序列中的层,输入是图片,输出是num_classes,也就是10分类
model = Sequential([
layers.experimental.preprocessing.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
layers.Conv2D(16, 2, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Dropout(0.2),
layers.Conv2D(32, 2, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Conv2D(64, 2, padding='same', activation='relu'),
layers.MaxPooling2D(),
layers.Dropout(0.2),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(num_classes)]
)
# 配置优化器和损失函数
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
return model
看上面的模型,参数中有图片的宽度和高度,我们默认都是256像素。也就是说,不管你的照片是什么尺寸,都将会被转化为256*256像素进行处理。下面,就可以训练数据了: # 创建模型
model = create_model(img_height,img_width,10)
# 训练结果存放的位置,没有就重新创建,有就在此基础上继续训练
checkpoint_save_path = 'models/checkpoint'
if os.path.exists(checkpoint_save_path + '.index'):
model.load_weights(checkpoint_save_path)
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
save_weights_only=True,
save_best_only=True)
# 训练模型
model.fit(train_ds,validation_data=val_ds,epochs=100, callbacks=[cp_callback])
这段程序运行之后,会在控制台打印训练过程和进度,等不打印了,就表示训练好了。同时,同级目录models下,也生成了对应的训练结果文件。3.3 使用数据训练完成之后,我们就要开始享用成功果实了。我们在同级目录下放一个res文件夹,里面是我们需要整理的图片,此时还是乱的。然后,我们构建模型,加载训练结果:class_names = ['airconditioner', 'barbecue', 'beach', 'cicada', 'dragonboat', 'electricfan', 'icecream', 'swimwear', 'watermelon', 'zongzi']
img_width = 256
img_height = 256
num_classes = len(class_names)
model = create_model(img_height, img_width, num_classes)
model.load_weights('models/checkpoint')
再然后,我们读入res文件下的图片,然后把它们交给model.predict进行识别:output_folder = "res"
all_files=os.listdir(output_folder)
imgs = []
for f_name in all_files:
if not f_name.endswith(".jpg"):
continue
img_path = output_folder+"/"+f_name
try:
im = cv2.imread(img_path)
im = cv2.cvtColor(im,cv2.COLOR_BGR2RGB)
im = cv2.resize(im,(img_width,img_height))
imgs.append(im)
except Exception as e:
print(f_name, str(e))
tf_imgs = tf.convert_to_tensor(imgs,dtype=tf.int64)
# 利用模型预测分类
predicts = model.predict(tf_imgs)
for (i,predict) in enumerate(predicts):
index = np.argmax(predict)
result_label = class_names[int(index)]
print(all_files[i],"is", result_label)
old_file_path = output_folder+"/"+ all_files[i]
new_file_dir = output_folder+"/"+ result_label
if not os.path.exists(new_file_dir):
os.makedirs(new_file_dir)
#移动文件到指定文件夹
shutil.move(old_file_path, new_file_dir)
运行之后,它会将某一个文件移入到识别的分类文件夹中,就像下面这样:以后,再有类似的夏天图片,你就可以让程序替你整理了。当然,如果你有新的分类,比如:遮阳帽,也记得增加这个分类的训练,否则计算机是无法识别的哦。四、总结问题图片标记器只是一个辅助手段,是额外的工具。如果只看用人工智能进行图片分类的话,从头到尾用几十行代码就可以完全搞定了。他复杂吗?以前复杂,现在不复杂。他难吗?以前很难,现在不难。人工智能的发展,实际上远远超过你我的想象和认知,我们需要去了解它,应用它。对于我,我更愿意去传播它。我正是发现了人工智能技术变得如此简单,才将首页介绍改为:TF男孩,婴幼儿学习TensorFlow框架的推广大使。我很希望中国的人工智能可以从娃娃抓起。用科技改变生活,是每个技术人的追求和使命。
GG
RNN文本生成-想为女朋友写诗(一):训练文本
一、亮出效果世界上美好的事物很多,当我们想要表达时,总是感觉文化底蕴不够。看到大海时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!看到鸟巢时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!看到美女时,心情舒畅,顿时感觉激情澎湃,想了很久,说了句:真大啊!是的,没有文化底蕴就是这样。但是,你生在这个数字时代,中华五千年的文化底蕴,你触手可及!这篇教程就是让人工智能学习大量的诗句,找到作诗的规律,然后你给他几个关键字,他给你一首诗。看效果:输入的关键词输出的诗句大海,凉风大海阔苍苍,至月空听音。筒动有歌声,凉风起萧索。建筑,鸟巢建筑鼓钟催,鸟巢穿梧岸。深语在高荷,栖鸟游何处。美女美女步寒泉,归期便不住。日夕登高看,吟轩见有情。我,爱,美,女我意本悠悠,爱菊花相应。美花酒恐春,女娥踏新妇。老,板,英,明老锁索愁春,板阁知吾事。英闽问旧游,明主佳期晚。二、实现步骤基本流程看上面的图,我们可以了解到,基本上就是两步:训练和使用。打铁还得要铁呢,我们训练数据,首先得有数据,我这里的数据是这样的:床前明月光 疑是地上霜 举头望明月 低头思故乡 渌水明秋月 南湖采白蘋 荷花娇欲语 ……这只是一小部分,总共大约70000句左右,都存在一个txt文档里。训练总共分为3步:准备数据、构建模型、训练并保存。2.1 准备数据爱情不是你想买,想买就能买。这句话揭示了世间道理是相通的。因为,训练不是你想训,想训就能训。你得把数据整理成人工智能框架要求的格式,他们很笨,喜欢数字,而且TensorFlow就认Tensor(不要被英文吓到,就是一串数字外面套一层人家的装甲壳子)。1.数据第一次处理2.数据第二次处理2.1.1 读取文件内容import tensorflow as tf
import numpy as np
import os
import time
# 从硬盘或者网络连接读取文件存到的.keras\datasets下,这里是把数据集poetry.txt放到了C盘根目录下
path_to_file = tf.keras.utils.get_file("poetry.txt","file:///C:/poetry.txt")
# 读取文本内容
text = open(path_to_file, 'rb').read().decode(encoding='gbk')
# 打印出来
print(text) # 凭楼望北吟 诗为儒者禅 此格的惟仙 古雅如周颂 清和甚舜弦 冰生听瀑句 香发早梅篇……
2.1.2 初步整理数据主要作用:把文本数字化。# 列举文本中的非重复字符即字库
# 所有文本整理后,就是这么多不重复的字 ['龙', '龚', '龛', '龟'……]
vocab = sorted(set(text))
# 把这个字库保存到文件,以后使用直接拿,不用再去计算
np.save('vocab.npy',vocab)
# 创建从非重复字符到索引的映射
# 一个字典 {'龙': 1, '龚': 2, '龛': 3, '龟': 4……},根据字能到数
char2idx = {u:i for i, u in enumerate(vocab)}
# 创建从索引到非重复字符的映射
idx2char = np.array(vocab) # 一个数组 ['龙' ... '龚' '龛' '龟'],根据数能找到字
# 将训练文件内容转换为索引的数据
# 全部文本转换为数字 [1020 4914 3146 ... 4731 2945 0]
text_as_int = np.array([char2idx[c] for c in text])
2.1.3 数据往Tensor上靠主要作用:把数字切成多个块。# 处理一句段文本,拆分为输入和输出两段
def split_input_target(chunk):
input_text = chunk[:-1] # 尾部去一个字
target_text = chunk[1:] # 头部去一个字
return input_text, target_text # 入:大江东去,出:大江东,江东去
# 创建训练样本,将转化为数字的诗句外面套一层壳子,原来是[x]
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
# 所有样本中,每24个字作为一组
sequences = char_dataset.batch(24, drop_remainder=True) # 数据当前状态:((24,x))
# 将每24个字作为一组所有样本,掐头去尾转为输入,输出结对
dataset = sequences.map(split_input_target) # 数据当前状态:((23,x), (23,x))
# 将众多输入输出对打散,并64个为一组
BATCH_SIZE = 64
# 数据当前状态:((64, 23), (64, 23))
dataset = dataset.shuffle(10000).batch(BATCH_SIZE, drop_remainder=True)
# 获取一批训练的输入,输出
train_batch, train_labels = next(iter(dataset))
以上的代码处理,他究竟做了什么操作?看下面给出解释!下面是原始文本凭楼望北吟 诗为儒者禅 此格的惟仙 古雅如周颂 清和甚舜弦 冰生听瀑句 香发早梅篇 想得吟成夜 文星照楚天 牛得自由骑 春风细雨飞 水涵天影阔 山拔地形高 贾客停非久 渔翁转几遭 飒然风起处 又是鼓波涛 堂开星斗边 大谏采薇还 禽隐石中树 月生池上山 凉风吹咏思 幽语隔禅关 莫拟归城计 终妨此地闲 远庵枯叶满 群鹿亦相随 顶骨生新发 庭松长旧枝 禅高太白月 行出祖师碑 乱后潜来此 南人总不知 路自中峰上 盘回出薜萝 到江吴地尽 隔岸越山多 古木丛青霭 遥天浸白波 下方城郭近第一刀,将它24个字符为1组切成如下(空格也算一个字符):凭楼望北吟 诗为儒者禅 此格的惟仙 古雅如周颂清和甚舜弦 冰生听瀑句 香发早梅篇 想得吟成夜文星照楚天 牛得自由骑 春风细雨飞 青山青草里第二刀,将24个字符掐头去尾形成输入输出对:凭楼望北吟 诗为儒者禅 此格的惟仙 古雅如周颂::楼望北吟 诗为儒者禅 此格的惟仙 古雅如周颂清和甚舜弦 冰生听瀑句 香发早梅篇 想得吟成夜::和甚舜弦 冰生听瀑句 香发早梅篇 想得吟成夜文星照楚天 牛得自由骑 春风细雨飞 青山青草里::星照楚天 牛得自由骑 春风细雨飞 青山青草里第三刀,将64个输入输出对作为一个批次,产生N个批次:凭……颂::楼……颂 | 清……生::香……篇甚……弦::生……瀑 | 早……篇::成……得做这些是为了什么?就是化整为零。70000句古诗系统一下消化不了。拆分成一首首,打包成一册册。就跟存入仓库一样,随便调取,一箱也行,一包也行,主要是这个目的。2.2 构建模型关于模型,说来话长,长的我都没啥说的。这样吧,你先复制代码,看注释。想详细了解模型结构,点击这里《RNN文本生成-想为女朋友写诗(二)》。# 构建一个模型的方法
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
batch_input_shape=[batch_size, None]),
tf.keras.layers.GRU(rnn_units,
return_sequences=True,
stateful=True,
recurrent_initializer='glorot_uniform'),
tf.keras.layers.Dense(vocab_size)])
return model
# 词集的长度,也就是字典的大小
vocab_size = len(vocab)
# 嵌入的维度,也就是生成的embedding的维数
embedding_dim = 256
# RNN 的单元数量
rnn_units = 1024
# 整一个模型
model = build_model(
vocab_size = len(vocab),
embedding_dim=embedding_dim,
rnn_units=rnn_units,
batch_size=BATCH_SIZE)
# 损失函数
def loss(labels, logits):
return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)
# 配置优化器和损失函数
model.compile(optimizer='adam', loss=loss)
2.3 训练训练很简单,就跟喊“开火”、“发射”一样。# 训练结果保存的目录
checkpoint_dir = './training_checkpoints'
# 文件名 ckpt_训练轮数
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
# 训练的回调
checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_prefix, save_weights_only=True)
# 进行训练
history = model.fit(dataset, epochs=20, callbacks=[checkpoint_callback])
开启了训练之后是这样的。Epoch 1/20
4/565 [..............................] - ETA: 21:35 - loss: 6.7695
tips:因为epochs=20,所以我们要训练20轮。但是你知道4/565是怎么来的吗?我们的文本总共有867852个字符,24个字符一组,64组一个批次,867852/24/64=565。也就是说,一次训练一个批次,一轮下来需要565个批次。训练完成之后会在同级目录training_checkpoints下生成文件:checkpoint
ckpt_1.data-00000-of-00001
ckpt_1.index
……
ckpt_20.data-00000-of-00001
ckpt_20.index
保存好这些,这都是辛苦训练来的,你要像工资一样珍惜它,因为后边会有用。
GG
兔年了,利用AI风格化实现剪纸兔、年画兔、烟花兔
一、图像风格化简介说起图像风格化(image stylization),你可能会感觉到陌生。尽管这项技术,已经深入你的生活很久了。专业名词,有时候,沟通起来不方便。记得高中时,我看见同桌带了一个书包。很特别。我就问他这是什么材料的。他说是PP的。我摇了摇头。他又说,就是聚丙烯。当时我很自卑,他说了两遍我依然不懂,感觉我知识太贫乏了。即便如此,我还是虚伪地点了点头。多少年之后。我才了解到,原来聚丙烯的袋子从我出生时,我就见过了,就是下图这样的:从那一刻起,我发誓,对于专业名词,我要做到尽量不提。但是不提,同行又以为我不专业。因此,我现在就是,说完了通俗的,再总结专业的。我会说编织袋或者蛇皮袋,文雅一点可以称为:聚丙烯可延展包装容器。而对于图像风格化,其实就类似你的照片加梵高的画作合成梵高风格的你。又或者你的照片直接转为动漫头像。风格化需要有两个参数。一个叫 content 原内容,另一个叫 style 风格参照。两者经过模型,可以将原内容变为参照的风格。举个例子。如果 content 是一只兔子,style 是上面的聚丙烯编织袋,那么两者融合会发生什么呢?那肯定是一个带有编织袋风格的……兔子!上面的风格融合,多少有点下里巴人。我再来一个阳春白雪的。让兔子和康定斯基的抽象画做一次融合。看着还不错,虽然没有抽象感,但是起码风格是有的。大家想象一下,在兔年来临之际,如果兔子和年画、剪纸、烟花这类春节元素融合起来,会是怎样的效果呢?从技术上(不调用API接口)又该如何实现呢?下面,跟随我的镜头,我们来一探究竟(我已经探完了,不然不能有上面的图)。二、技术实现讲解首先说啊,咱们不调用网络API。其次,我们是基于开源项目。调用第三方API,会实时依附于服务提供商。一般来说,它处于自主产品鄙视链的底层。我了解一些大牛,尤其是领导,声称实现了很多高级功能。结果一深究,是调用了别家的能力。这类人,把购买接口的年租费用,称为“研发投入”。把忘记续费,归咎于“服务器故障”。那么,鄙视链再上升一层。就是拿国外的开源项目,自己部署服务用。尽管这种行为依然不露脸。但是,这在国内已经算很棒的了。因为他们会把部署好的服务,再卖给上面的大牛领导,然后还鄙视他只能调API。今天,我要使用的,就是从开源项目本地部署这条路。因此,你学会了也不要骄傲,这并没有什么自主的知识产权。学不会也不用自卑,你还可以试试调用API。2.1 TensorFlow Hub库可以说我对 TensorFlow 很熟,而且是它的铁杆粉丝。铁到我的昵称“TF男孩”的TF指的就是 TensorFlow 。TensorFlow 已经很简单和人性化了。简单到几十行代码,就可以实现数字识别的全流程(我都不好意写这个教程)。但是,它依然不满足于此。代码调用已经够简单了。但是对于训练的样本数据、设备性能这些条件,仍然是限制普通人去涉足的门槛。于是,TensorFlow 就推出了一个 TensorFlow Hub 来解决上面的问题。你可以利用它训练好的模型和权重,自己再做微调,以此适配成自己的成果。这节省了大量的人力和物力的投入。Hub 是轮毂的意思。这让我们很容易就联想到“重复造轮子”这个话题。但是,它又很明确,不是轮子,是轮毂。这说明,它把最硬的部件做好了,你只需要往上放轮胎就行。到这里,我开始感觉,虽然我很讨厌有些人说一句话,又是带中文,又是带英文的。但是,这个 Hub ,很难翻译,还是叫 TensorFlow Hub 更为贴切。2.2 加载image-stylization模型如果你打算使用 hub 预训练好的风格化模型,自己不做任何改动的话,效果就像下面这样。这是我搞的一个梵高《星空》风格的兔子:而代码其实很简单,也就是下面几行:# 导入hub库
import tensorflow_hub as hub
# 加载训练好的风格化模型
hub_model = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')
# 将content和style传入
stylized_image = hub_model(content_image, style_image)[0]
# 获取风格化后的图片并打印出来
tensor_to_image(stylized_image)
这,看起来很简单。似乎人工智能的工作很容易干。事实,并非如此。我建议大家都来学习人工智能,利用成熟的代码或工具,解决生活中遇到的问题。但是,我不建议你着急转行到人工智能的工作岗位中来。因为在学习过程中,你会发现,相比于其他语言,人工智能具有更多的限制和基础学科要求。因此,作为使用体验者和制作开发者,会是两个不同的心境。随着下面的讲解,上面的问题我们会逐个碰到。首先,上面的 hub.load('https://tfhub.dev/……') 你就加载不下来。而这个地址,正是图像风格化的模型文件。咔,晴天霹雳啊,刚起头就是挫折。其实TensorFlow 是谷歌的开源项目。因此他们很多项目的资源是共享的。你可以替换 tfhub.dev 为 storage.googleapis.com/tfhub-modules 。并且在末尾加上后缀 .tar.gz 。下载完成之后,解压文件,然后指定加载路径。其实这一步操作,也是框架的操作。它也是先下载到本地某处,然后从本地加载。比如,我将 .tar.gz 解压到同级目录下。然后调用 hub.load('image-stylization-v1-256_2') 即可完成 hub 的加载。这就是我说的限制。相比较而言,Java 或者 Php 这类情况也会有,但是频率没有这么高。下面,我们继续。还会有其他惊喜。2.3 输入图片转为tensor格式hub_model = hub.load(……) 是加载模型。我们是加载了图像风格化的模型。赋值的名称随便起就行,上面我起的名是 hub_model 。之所以说这句话,是因为我发现有些人感觉改个名字,代码就会运行不起来。其实,变一变,更有利于理解代码。而项目运行不起来,向上帝祈祷不起作用,是需要看报错信息的。如果完全不更改 hub 预置模型的话,再一行代码就完工了。这行代码就是 stylized_images = hub_model(content_image, style_image) 。这行代码是把内容图片 content_image 和风格参照图片 style_image 传给加载好的模型 hub_model 。模型就会输出风格化后的结果图片stylized_images。哇哦,瞬间感觉自己能卖API了。但是,这个图片参数的格式,却并没有那么简单。前面说了,TensorFlow Hub 是 TensorFlow 的轮毂,不是轮子,更不是自动驾驶。它的参数和返回值,都是一个 flow 流的形式。TensorFlow 中的 flow 是什么?这很像《道德经》里的“道”是什么一样。它们只能在自己的语言体系里能说清楚。但是在这里,你只需要知道调用一个 tf.constant(……) ,或者其他 tf 开头的函数,就可把一个字符、数组或者结构体,包装成为 tensor flow 的格式。那么下面,我们就要把图片文件包装成这个格式。先放代码:import tensorflow as tf
# 根据路径加载图片,并缩小至512像素,转为tensor
max_dim = 512
img = tf.io.read_file(path_to_img)
img = tf.image.decode_image(img, channels=3)
img = tf.image.convert_image_dtype(img, tf.float32)
shape = tf.cast(tf.shape(img)[:-1], tf.float32)
long_dim = max(shape)
scale = max_dim / long_dim
new_shape = tf.cast(shape * scale, tf.int32)
img = tf.image.resize(img, new_shape)
img = img[tf.newaxis, :]
tf_img = tf.constant(img)首先,模型在训练和预测时,是有固定尺寸的。比如,宽高统一是512像素。然后,对于用户的输入,我们是不能限制的。比如,用户输入一个高度为863像素的图,这时我们不能让用户裁剪好了再上传。应该是用户上传后,我们来处理。最后,要搞成tensorflow需要的格式。上面的代码片段,把这三条都搞定了。read_file 从路径读入文件。然后通过 decode_image 将文件解析成数组。这时,如果打印img,具体如下:shape=(434, 650, 3), dtype=uint8
array([[[219, 238, 245],
...,
[219, 238, 245]],
[[219, 238, 245],
...,
[219, 238, 245]]])
shape=(434, 650, 3) 说明这是一个三维数组,看数据这是一张650×434像素且具有RGB三个通道的图片。其中的array是具体像素的数值,在某个颜色通道内,255表示纯白,0表示纯黑。接着 convert_image_dtype(img, tf.float32) 把img转成了float形式。此时,img的信息为:shape=(434, 650, 3), dtype=float32
array([[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]])
为啥要把int转为float呢?初学者往往会有这样的疑问。因为他们发现,只要是计算,就要求搞成float类型。就算明明是9个分类,也不能用1、2、3、4来表示,也要转为一堆的小数点。今天这个图片的像素,也是如此,255个色值多好辨认,为什么非要转为看不懂的小数呢?别拦着我,我今天非要要解释一下。这并不是算法没事找事,假装高级。其实,这是为了更好地对应到很多基础学科的知识。比如,我在《详解激活函数》中讲过很多激活函数。激活函数决定算法如何做决策,可以说是算法的指导思想。你看几个就知道了。不管是sigmoid还是tanh,它的值都是以0或者1为边界的。也就是说你的模型做数字识别的时候,计算的结果并不是1、2、3、4,而是0到1之间的小数。最后,算法根据概率得出属于哪个分类。哎,你看概率的表示也是0到1之间的数。除此之外,计算机的二进制也是0或者1。芯片的计算需要精度,整数类型不如小数精确。各种原因,导致还是浮点型的小数更适合算法的计算。甚至,人工智能的体系中,还具有float64类型,也就是64位的小数。变为小数之后,后面就是将图片数组做缩放。根据数据的shape,找到最长的边。然后缩放到512像素以内。这就到了 resize(img, new_shape) 这行代码。到这一步时,img的数据如下:shape=(341, 512, 3), dtype=float32
array([[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]])
原来的 (434, 650, 3) 图片被重新定义成了 (341, 512, 3) 。依然是3通道的色彩,但是长宽尺寸经过计算,最大已经不超过512像素了。为什么做缩放?除了模型要求,还要防止用户有可能上传一张1亿像素的图片,这时你的服务器就冒烟了。(434, 650, 3) 代表的是一张图。但是纵观所有算法模型,不管是 model.fit(train_ds) 训练阶段,还是 model.predict(tf_imgs) 预测阶段。就没有处理单张图片的代码逻辑,全都是批量处理。它不能处理单张图片的结构,你别说它不人性化,不用跟他杠。兄弟,模型要的只是一个数组结构,它并不关心里面图片的数量。一张图片可以是 ["a.png"] 这种形式。说到这里,我又忍不住想谈谈关于接口设计的话题了。我给业务方提供了一个算法接口能力,就是查询一张图上存在的特定目标信息。我也是返回多个结果的结构。尽管样本中只有一个目标。业务方非要返回一个。从长远来讲,谁也不敢保证以后场景中只有一个目标。我必须要如实返回,有一个返回一个,有两个返回两个,你可以只取第一个。但是,结构肯定是要支持多个的。从成本和风险权衡的角度,从列表中取一条数据的成本,要远小于程序出错或者失灵的风险。但是业务方比较坚持返回一个就行。后来,他们让我把图片的base64返回值带上 data:image/jpeg;base64, 以便于前端直接展示。那一刻,我就明白了,跟他们较这个真,是我冲动了。而对于 TensorFlow 的要求,你必须要包装成批量的形式。我认为这很规范。这句代码 img = img[tf.newaxis, :] 就是将维度上升一层。可以将 1 变为 [1] ,也可以将 [[1],[2]] 变为 [[[1],[2]]] 。此时再打印img,它已经变为了如下结构:shape=(1, 341, 512, 3), dtype=float32
array([[[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]]])
shape=(1, 341, 512, 3) 表示有1张512×341的彩图。那么,这个结构它也可以承载100张这样的图,那时就是shape=(100, 341, 512, 3)。这就做到了,以不变应万变。最后一步的 tf_img = tf.constant(img) ,作用是通过 tf.constant 把图片数据,包装成 TensorFlow 需要的格式。这个格式,就可以传给hub_model去处理了。经过 stylized_images = hub_model(tf_img_content, tf_img_style) 这行代码的处理。它会将处理结果放到 stylized_images 中。你马上就可以看到融合结果了。不过,好像也没有那么简单。这个结果的呈现,实际上是图片到tensor格式的逆向过程。我们下面就来处理它。2.4 tensor格式结果转为图片上一步经过 hub_model 转化,我们获取到了 stylized_images 。这是我们辛苦那么久的产物。你是否会好奇 stylized_images 到底是怎样的结构。我们来打印一下:[<tf.Tensor: shape=(1, 320, 512, 3), dtype=float32, numpy=
array([[[[0.31562978, 0.47748038, 0.7790847 ],
...,
[0.7430198 , 0.733053 , 0.6921962 ]],
[[0.76158 , 0.6912774 , 0.5468565 ],
...,
[0.69527835, 0.70888966, 0.6492392 ]]]], dtype=float32)>]
厉害了,它是一个 shape=(1, 320, 512, 3) 形状的 tf.Tensor 的数组。不要和我说这些,我要把它转为图片看结果。来,先上代码:import numpy as np
import PIL.Image
tensor = stylized_images[0]
tensor = tensor*255
tensor_arr = np.array(tensor, dtype=np.uint8)
img_arr = tensor_arr[0]
img = PIL.Image.fromarray(img_arr)
img.save(n_path)
相信有了上面图片转 tensor 的过程,这个反着转化的过程,你很容易就能理解。第1步:取结果中的第一个 stylized_images[0] ,那是 shape=(1, 320, 512, 3) 。第2步:小数转为255色值的整数数组 tensor*255、 np.array(tensor, dtype=np.uint8) 。第3步:取出 shape=(1, 320, 512, 3) 中的那个1,也就是512×320的那张图。第4步:通过 fromarray(img_arr) 加载图片的数组数据,保存为图片文件。我敢保证,后面的事情,你只管享受就好了。源码在这里 github.com/hlwgy/image… 。你可以亲自运行试验下效果。不过,多数人还是会选择看完文章再试。三、一切皆可兔图的效果春节就要到了,新的一年是兔年(抱歉,我好像说过了)。下面,我就把小兔子画面和一些春节元素,做一个风格融合。3.1 年画兔当然,我只说我这个年龄段的春节场景。年画,过年是必须贴的,在我老家(倒装句暴露了家乡)。而且年画种类很丰富。有这样的:还有这样的:它们的制作工艺不同,作用不同,贴的位置也大不相同。我最喜欢贴的是门神。老家的门是木头门。搞一盆浆糊,拿扫帚往门上抹。然后把年画一放,就粘上了。纸的质量不是很好,浆糊又是湿的,浆糊融合着彩纸还会把染料扩散开来。估计现在的孩子很少再见到了。我们看一下,可爱的小兔子遇到门神年画,会发生怎样的反应:你们知道年画是怎么制作的吗?在没有印刷机的年代,年画的制作完全靠手工。需要先雕刻模子,其作用类似于印章。有用木头雕刻的模具,印出来的就是木板年画。好了,雕刻完了。最终的模子是这样的。模板里放上不同的染料,然后印在纸上,年画就出来了。如果兔子遇到这种木板模具,会是什么风格呢?我有点好奇,我们看一下:我想,这个图,再结合3D打印机,是不是就不用工匠雕刻了。3.2 剪纸兔剪纸,也是过春节的一项民俗。我老家有一种特殊的剪纸的工艺,叫“挂门笺”。当地叫“门吊子”,意思就是吊在门下的旗子。其实这个习俗来源于南宋。那时候过年,大户人家都挂丝绸旗帜,以示喜庆。但是普通百姓买不起啊,就改成了彩纸。跟年画比,这个工艺现在依然活跃,农村大集还有卖的。如果小兔子遇到剪纸,会是什么风格呢?揭晓一下效果:3.3 烟花兔说起烟花,就不是哪个年龄段的专利了。现如今,即便是小孩子,也很喜欢看烟花。如果兔子遇到烟花,会产生什么样的融合呢?放图揭晓答案:确实很美丽。四、无限遐想最后,我仍然意犹未尽。我尝试自己画了个小兔子,和掘金的吉祥物们做了一个融合:这……我感觉比较失落。但是,转念一想,其实作为抽象画也可以,反正大多数人都看不懂。我又融合了一张,裱起来,打上落款,好像也过得去。上面的例子,我们是 hub.load 了 https://tfhub.dev/……image-stylization-v1-256/2 这个模型。其实,你可以试试 https://tfhub.dev/……image-stylization-v1-256/1 这个模型,它是一个带有发光效果的模型。嗨,技术人,不管你是前端还是后端,如果春节没事干,想跨界、想突破,试试这个人工智能的项目吧。搞个小程序,给亲友用一下,也挺好的。
GG
NLP深度学习精讲
系统介绍NLP(自然语言处理)领域的核心知识点。涵盖了Tokenizer分词器、文本数据预处理、TensorFlow模型保存与恢复、序列模型和各种层的应用、one-hot独热编码、激活函数等关键概念。
GG
AI实战课:CV图像
CV图像识别例子,拳拳到肉,刀刀见血,没有理论,只有功能。
GG
AI实战课:NLP自然语言
NLP例子,拳拳到肉,刀刀见血,没有理论,只有功能