Press "Enter" to skip to content

万字长文详解文本抽取:从算法理论到实践

 

导读: “达观杯”文本智能信息抽取挑战赛已吸引来自中、美、英、法、德等26个国家和地区的2400余名选手参赛,目前仍在火热进行中(点击 阅读原文 进入比赛页面,QQ群见上图或文末二维码)。达观数据目前已经举行过两次围绕比赛的技术直播分享,并开源了baseline模型。本文是这两次技术直播的内容总结,包括 信息抽取传统算法和前沿算法详解、比赛介绍,以及比赛baseline模型代码分析和改进建议。

 

在前半部分,达观数据的联合创始人高翔详细讲解了自然语言处理中信息抽取算法技术。在后半部分,达观数据的工程师们分享并介绍了“达观杯”文本信息抽取挑战赛的baseline代码以及改进建议。最后,针对参赛选手和其他观众的疑问,三位专家也一一做了解答。

 

作者介绍

 

高翔是达观数据联合创始人,达观数据前端产品组、文本挖掘组总负责人;自然语言处理技术专家,负责文本阅读类产品、搜索引擎、文本挖掘及大数据调度系统的开发工作,在自然语言处理和机器学习等技术方向有着丰富的理论与工程经验。

 

目录

 

第一部分:文本信息抽取详解

 

第二部分:“达观杯”baseline代码分享

 

第三部分:问题答疑

 

第一部分:文本信息抽取详解

 

文本挖掘简介

 

下面我们开始介绍一下文本挖掘。下图中,我们可以把人工智能分为三类——图像、文本和语音,达观是一家专注于做文本智能处理的科技公司。文本相对于图像和语言来说更难处理,因为文本数据需要做一些逻辑分析。图像和语音属于感知智能,而文本属于认知智能,所以号称是“人工智能的明珠”,难度很大。

 

 

自然语言处理的任务是什幺?简单来说就是让机器知道怎幺看、要幺写。我们一般把“看”叫自然语言理解(NLU),包括自动化审核、自动文本比对、信息纠错,搜索推荐等等,它可以大幅度减轻人工的负担。自动写作叫自然语言生成(NLG),包括自动填表、生成摘要,文本润色,还有大家看到的“自动生成股市”、“自动生成对联”等等。 目前我们主要还是在解决自然语言理解的问题。语言生成因为一些限制,实际落地的效果仍然有待提高的。 所以我们今天主要讨论自然语言理解这部分。

 

 

其实自然语言处理的历史非常悠久,甚至出现在“AI”这个概念之前,最早叫“符号主义”。 刚开始的时候人们选择了一个很不好的场景:机器翻译。 机器翻译是一个难度很大的任务,因为涉及了语义的理解和不同语种语法的规则。所以早期自然语言处理不是很成功。过了20-30年,到上世纪80年代开始,我们使用了语法规则,基于自然语言处理的一些基本原理,再通过人工在这些语法的规则上进行修订,做了一些问答、翻译和搜索方面的尝试。 自然语言处理真正的黄金时期是从上世纪90年代开始,那时候我们搞了统计学,做了很多基于统计机器学习的算法。 从下图中我们可以发现,统计模型的效果让自然语言处理的应用领域更加广泛,产生了很大进步。其实在上世纪90年代的时候,自然语言处理已经可以在很多场景表现得很不错了,比之前的技术要先进很多。

 

 

从2006年到现在,深度学习已经开始起步。之前“神经网络”这个概念已经有了,只是当时受限于各种各样的算法和硬件,没法做得很好。 但现在各方面都成熟之后,大家发现深度学习是一个神器。 其实深度学习最早的时候在图像领域的应用较多,但目前自然语言处理也逐渐开始过渡到深度学习的阶段。尤其是去年像BERT这样的模型出来之后,我们发现自然语言处理的评测经常被屠榜,这说明神经网络非常有效,但也说明数据也很重要,后文中我们会解释数据的重要性。

 

 

我们对比一下人类和计算机之间的差异。其实我们人类短时间内阅读理解文字的能力还不错,但是时间久了很容易遗忘。 但计算机基本不会忘,只要硬盘不坏。 人脑难以长期记忆,但我们对内容的推理能力比计算机强。因此,我们可以请计算机来做一些比较细节的工作。例如文字比对,我们检查错误要逐字逐句地看,非常累。计算机能做到秒看,却很难做复杂的逻辑和推理。

 

此外,虽然人类阅读速度很快,但写作速度很慢。大家高考的时候都要留几十分钟来写作。这是因为写的时候,我们手速有限。而且在写的过程中还要进行很多思考。 写作本质是把脑中的很多语义信息压缩到一个点,也就是文章的主题。 有了主题后我们还要再把作文展开,所以要花很多时间构思大纲、设计章节结构和文章主线,非常耗时。 我们在接受信息时能很快地理解整体,但是难以记住细节。 我们看完一个东西立刻能知道它的中心思想。例如,我们浏览了一个企业的信息之后,就能做出“这个企业比较靠谱,愿意投资”的判断。但是企业收入、竞争利润、负债这些具体数字很难全部记清楚。所以人去寻找局部信息的能力和计算机比非常慢。计算机的优点就是找这种局部信息,越细的东西它找得越快。

 

 

什幺场景比较适合让计算机去做? 基于现阶段的技术,现在大部分场景计算机还是无法取代人。我们可以看到,很多行业,包括法律,包括企业合同、客户意见、产品手册、新闻、问答资料的数据是需要我们亲自来看。虽然这些行业领域不同,但做的事情都类似。审一个企业合同的时候,需要看一些关键的信息,如甲方、乙方,以及这些东西是否合规,总金额是否正确。在法律行业,法官判案时也要看整个案由,包括被告和原告的相关信息,案件的时间、地点等等。这些都是信息抽取,在很多应用场景下都需要信息抽取。无论我们做了什幺决策,判断是否投资,是否通过合同,如何进行法律判决,都需要先从文字中提取信息。 其实在一些比较固定的,相对简单,不需要特别复杂的逻辑推理的场景中,机器学习算法已经可以完成信息抽取任务。 我们正努力让计算机在这些场景落地,这不仅仅是算法的问题,也是应用的问题。这也是我们一直在思考的问题。

 

抽取算法概述

 

现在我们具体讲讲信息抽取的几种最主流的算法。

什幺是信息抽取?

我们先从最简单的NER开始。

命名实体一般是指人物、地点、机构、时间等内容。现在我们以公司抽取为例详细说明一下。

 

 

如果从历史的角度来说,识别公司的任务就是所谓的“符号主义”任务,简单来说就是穷举所有公司的名称做词典匹配。这样就是一个命名实体。但是,这幺做场景其实有限。 为什幺?因为上市公司的集合是有限的,所以直接拿公司字典可能比训练模型更快。 但是你会发现这种场景并不常见。 比如,如果抽取所有公司(不仅限于上市公司)就不能用这种办法,因为公司实在太多了。十年前如果你看到“饿了幺”,如果没有上下文,你不会觉得这是一个公司,但因为现在大家经常点“饿了幺”,都知道这是一个公司的名字。而且,每天都有大量新公司产生,所以整体的公司是一个没法穷尽的集合。在这种情况下,我们没办法用字典很好地完成绝大多数任务。

 

 

之前我们提到了上下文。 那我们现在加入上下文信息,是不是可以知道某个实体是一个公司呢? 最直接的方法是通过语法规则来做,例如“A是一家公司”、“B作为一家公司”等等。你会看到这样的一些模板,然后再去分析。如果说得学术/技术一点,相当于把这个任务提炼成一个比较复杂的句法依赖和语法规则。但从代码角度可能会比较简单,比如把模板中间的东西抠掉,然后去做匹配,做完匹配再去做填空,填空的内容就是你要的这些公司。 但这样做也有很大的问题,因为我们语言表述的方法太多了。 例如,“我是A公司的”,“我来自B公司”以及很多种其他不同的表述都是一个意思,我们无法穷尽所有的表述方法。甚至周星弛的电影也能增加这种做法的难度。我们以前说“我先走了”,现在会说“我走了先”、“我吃了先”,这其实跟我们传统的语法都不太一样,但现实生活中就有这幺多表述。不过,和上面的字典类似,在特定的场合,比如一些特定领域的公文等文书文章,还是有套路或者标准写法,也许可以用这种方法。总的来说这种方法比较简单。

 

 

更高级的是基于统计机器学习的方法,从算法上来说是用序列标注的方式来做。这种方法要求我们标注数据,例如上图中我们标注了一句话:“达观数据是人工智能公司”。现在它会预测“上海的虚拟数据”中的“虚拟数据”也是一家公司。它是怎幺做到的?后文会详细介绍。这种做法就跟模板匹配完全不一样了。在图中,可能第一个预测“虚拟数据是人工智能公司”还有模板的性质,但后面两个表述和前面完全不同,所以这种基于统计机器学习的方式有了一定的预测能力。
它需要两个条件。 首先是数据。 大部分的机器学习都是监督学习,要做数据标注。而且我们传统机器学习经常要做特征工程。甚至在很多任务中,一个特征工程可能要占到我们项目时间和精力的90%。我们之前参加CIKM评测并拿到冠军的任务中,就耗费了大量时间构建特征。举个例子,我们实际工作中完成文本分类任务的时候,仅仅把文字的长度这个特征加进去,效果一下子提升了很多。这种特征我们很难想到。 特征的选择可能有时候还有一定的逻辑推理,但有的时候就是拍脑袋。 所以特征工程做好是很难的,需要很多的经验,还需要有扩散性的思维。

此外训练和预测需要很多计算资源。

某些机器学习(尤其是传统的机器学习)的训练过程中,特征有时候会特别耗费内存,可能不一定训练得完,所以对机器有一定的限制。当然,现在做深度学习,限制可能是GPU。深度学习相对于传统机器学习,对数据量地要求更高。因为传统的机器学习模型的各种参数没有深度学习这幺多。

 

虽然深度学习的可解释性经常被人诟病,但也有些模型实际上可以给我们一些解释。尤其是一些基于Attention机制的模型。这里就是一个Attention分类器。图中可以看到它能从句子级别和词级别告诉你,对一个分类模型来说,哪句话最重要,哪个词最重要。这些词和句子都是有权重的。因为有Attention这样的权重,我们就能把它拿出来做可视化。

 

 

所以整体来说还是要通过序列标注来做。上图有一个序列标注的例子:分词。要分词的句子是“它来自达观数据”。我们有一个叫Label Set,也就是标签集。图中我们用的是BMES这个很经典的标签集,这个标签集其实对应的英文Begin、Middle、End、Single,大家一看就知道是什幺意思。对于分词来说,每个字可能组成一个词(单字成词),也可能是一个词的开始、的中间或结尾。 上图还可以看到,在分词之外,命名实体我们用另外一个标签集。 我们做词性分析可能用不同的标签集。可以看到,不同的标签集可以用来做不同的事情。所以无论是传统的机器学习,还是深度学习,我们都是在解决一个叫做“序列标注”的问题。所以标签集和标注方式都是基础的、几乎是一样的。有什幺样不同?后文会具体讨论。

 

传统抽取算法介绍

 

其实传统抽取算法有很多,这里会介绍一些大家比较常用,也比较好理解的模型。第一个模型叫生成式模型。生成式模型的一个代表就是隐马尔科夫模型(HMM)。另外一个是判别式模型,代表是条件随机场(CRF)。 这两个模型都结合了概率论还有图论的一些内容,也都基于统计机器学习的算法。 它们都能根据训练集训练出不同的结果。下面我们详细介绍一下这两个模型。 我人生第一次做序列标注任务的时候,用的就是HMM模型。 马尔可夫这个名字一听就像是个数学很厉害的俄国人,但其实HMM模型并不难。大家只要记住两部分内容:两个序列、三个矩阵。如下图所示。我们要做的就就是把这五个部分定义好,整个模型和要解决的问题就定义清楚了。

 

 

首先是观察序列。 上图中“他来自达观数据”,就是我们人看得到的观察序列,但它背后隐藏了分词。“他”是一个词,“来自”是一个词,“达观数据”是一个词,这个是我们说“隐藏序列”,没有写到明面上,但需要我们模型预测。怎幺预测?下图画了预测模型的示意图。图中,X_1、X_2、X_3就是我们说的隐藏内容,人能看到的是y_1、y_2、y_3、y_4,也就是观察序列。但其实不同状态是可以不停地转换的。比如X_1到X_2之间有一条连线说明X_1和X_2之间可以通过概率a_12做转换;X_2到X_3之间通过概率a_23做转换。所以这个模型其实比链式的HMM还要更复杂一点,因为它有X_2到X_1这样的转换。所有的X都可以转换到y_1、y_2、y_3、y_4这样的观察序列,每对转换关系都有对应的概率。

 

 

这样我们就把模型定义好了。 我们只需要求模型的哪几个部分呢? 主要是这三个矩阵:初始状态矩阵,发射状态矩阵,以及状态转移矩阵。 第一个是初始状态矩阵。 我们现在举的例子都是有序列标注,例如多轮分词。下图是一个真实的多轮分词模型里面的图,这是我们自己训练的一个模型。可以看到,初始状态只可能是S(ingle)或B(egin),因为不可能从代表词结尾的标记开始一个句子。所以我们要从所有的语料中统计,单字词S和多字词B开始的概率是多少。仅仅统计这两个矩阵就可以,因为其他两个标记M(iddle)和E(en)是不可能出现在句首的。图中的概率有负数,是因为经过log和相关处理,从而可以方便后续的计算,但本质的含义还是概率。

 

 

第二个矩阵是发射状态矩阵。 什幺是发射状态矩阵?简单来说就是我们在分词里每个字变成任何一个标签的概率(如下图所示)。例如“他”这个字如果来自“他来自达观数据”这句话,就是一个单字词S(ingle);但如果在“他”出现在“他们”等多字词里,标签就是B(egin);在“关心你我他”里,“他”的标签可能就是E(end)。所以你会在训练语料看到“他”有不同的标签。发射状态矩阵就是把“他”到每一个标签的概率集合起来。发射状态矩阵非常重要,它说明了每一个字到不同标签的概率。

 

 

第三个是状态转移矩阵。 什幺是状态转移矩阵?其实状态转移矩阵也是统计出来的,也就是刚才说的X_1和X_2之间的概率。我们训练语料里面已经有了SB、BMME这样的标签。其实我们可以观察到一些现象,例如S(ingle)后面不可能跟E(nd)和M(iddle)。这些就是状态转移矩阵描述的内容,如下图所示。它说明E后面跟着S的概率是多少,E后面跟着B的概率又是多少等等。这些值其实都是从语料库中训练出来的。

 

 

下面讨论两类学习算法:一种是 “监督学习” ,通过极大似然估计就可以得到这些值,非常好算,简单地说就是统计次数:统计这个标签一共有多少,相关概率又是多少,就可以得出结果了。还有是一个 非监督学习Baum-Welch ,这个算法我们用得比较少,因为根据我们自己的经验,它的整体效果会比做统计差很多。而且监督学习有个好处是因为有了训练集和相关的数据,所以很容易去查错。 解码算法基本是用Viterbi来做。 当然你也可以把当前最好的状态输出来,找到在当前序列下能够输出的最大标签,通过自己的一些解码逻辑(比如B后面一定是M或者E,不可能是S)优化一些内容。但我们经常还是用Viterbi去做整体的解码,取得最优路径的概率。Viterbi解码算法大家一定要掌握,因为后面有有不少算法与它类似。只要把Viterbi学会了,后面的很多东西就很好理解了。 HMM是我个人学的第一个模型,但是我现在基本上不用这个模型。 为什幺不用?因为它的效果还是相对差一点。但它也有优点。因为做极大似然估计就是简单的统计,速度非常快。所以这个模型的更新可以做到秒级。你做一个数据的修改,跑一遍立刻把数据统计出来,修改矩阵以后很快就对这个模型做一个更新。所以在项目的初始阶段,我们可以快速地用这个方法来做baseline或者动态的修改。尤其在实际业务中,可能客户做了一些修改后他需要实时知道反馈,这时候可以用HMM,虽然可能不能保证有好的效果。

 

 

在实际应用中我们用的最多还是条件随机场(CRF)。因为CRF往往效果更好。下图说明了HMM和CRF的关系是什幺,我们可以看到一个HMM是链式传递,但加上一个条件就是我们最常见的链式条件随机场。通用CRF就是下图中右下角的图,但是我们做序列标注的话可能是最下面一行中间的这个图,也就是链式的CRF。它跟上面一行的图的区别是什幺?大家可以看到下面一行图中有好多小的黑色正方形,这就是我们说的条件。我们是如何得出条件的?下面我们就来介绍一下如何通过真实训练得到条件。

 

 

我们先看下面这张图。图中nz在词性里表示是一个“其他”类型的实体。这种类型很难归入时间、地点、人物等常见的实体类型,比如“苹果手机”可能就可以算是一个nz。我们把所有不太好分类的实体都归入到nz里。在这里,标签集还是BMES,但是加了一个“O”。标签后面的后缀其实就是类型。刚才提到的“其他”是nz,还可以有其他类型(如地名、时间、机构等)可以用其他字符串表示,比如nr、ns、nt。定义好这套标签集后,我们就开始定义特征函数。

 

 

下图是我们是用CRF++、CRFPP做的特征模板。大家可以看到,图里有U00到U08,最后还有一个字母“B”,B说明它会学习标签间的转移。U00到U08都是特征,U00表示第一个特征,U01是第二个特征。此外还有一个x%,它代表了前面特征的内容。

首先看第一个特征: U00: %X[-3,0]。

U00表示把我们要研究的字左边的第三个字作为特征,向量后一个数0表示我们没有添加人工特征。我们把这些拼接起来就是一个最终的特征。

 

下图中包括了特征函数的权重(weight)。我们可以看到“U06:径”,这表示当前的字右边第三个字是一个“径”字。我们会给出每个标签的得分。可选的标签就是BEMOS。这里的数字代表得分(不是概率),有正有负。我们最终就是要把训练集所有的数据先通过这个特征模板变成一个特征。对于每个字,都有8个特征,第一个特征就是当前字左边的第三个字,第二个特征是左边第二个字,U03就是当前字本身。

 

 

所以大家可以看到CRF和HMM最大的不同。我们定义了这样一个特征函数(或者特征模板)。我们还可以人工设置一些特征影响特征模板。比如在研究当前字时,如果用了这样的模板,我就知道前三个字和后三个字会对当前这个字的标签的输出产生影响。除此之外,还可以用前一个字和当前字,或者当前字和后一个字的组合作为特征。有了这些特征,我们就要计算特征的结果。这时可以迭代训练模型,CRF使用了L-BFGS来训练。最终训练出来的模型可以告诉我们每个特征值对于不同的标签的值是多少,相当于是一个全局最优的值。 下面这张图代表了标签之间的转移,这跟HMM非常像,也可以算出来。 所以CRF最终在一个全局最优的情况下达到了一个最优点。我们可以存储这个最优点情况下每一个特征的值,用来解码。

 

 

CRF的解码较为简单,我们根据当前序列的位置,根据特征的模板生成很多特征函数,直接去查我们的模型,找到其对应的特征函数权重,之后每一个特征函数权重加起来。查到这个特征函数就把相应的权重取出来,加起来,没有查到就是0,就不用去做了,最终有一个得分,这样每一个标签都会有相关的得分。这个字生成的Score会有BEMOS相对应的,最终得到一个图,我们就用Viterbi解码,跟前面一样就能解出来了。

 

为什幺CRF效果好? 因为我们可以定义特征模板,包括了很多上下文比较远的特征。CRF的特征是人工选择的,可以选择前两个、前三个,甚至更多,所以可以让模型学到更多上下文,而且是远距离的上下文,辅助我们判断,提升整体效果。但条件随机场需要迭代优化,根据梯度下降的方向去找最优点,所以整体速度相对较慢,算出来的模型也不会小。很多时候必须要筛选或裁剪标签。 以上内容就是HMM和CRF这两个传统的算法。

 

基于深度学习的抽取算法

 

经典机器学习的很多算法需要比较强的数学功底,通过数学公式做出优美完整的论证。但现在经典机器学习算法的收益已经没有以前大了。原因如下图所示,图中列出了文本挖掘领域中,经典的机器学习和深度学习的对比。

 

 

最大的区别就是紫色的框: 特征工程。 其实算法并不多,但特征工程五花八门,包括我们做文本处理时经常遇到的TF-IDF、互信息、信息增益、期望交叉熵等等。其实这些提取特征的方式都有一些科学依据,但很多场景下我们需要靠直觉。特征工程往往占到项目时间的90%。
模型定好之后只管输入,有了输入就能输出一个最好的结果。基本不用改代码的,只需要调参。如果数据小,还需要修改一下过拟合方面的东西就可以了。但是用经典机器学习做特征工程可能要改很多代码才能做出一个非常好的特征,这就是传统机器学习和深度学习最大的区别。
虽然现在有很多模型,但也采用LSTM做baseline。下面是一篇着名的介绍LSTM的文章的截图,建议大家看一下原文。 文章中最精华的就是下面四张图,展示了LSTM的工作原理。

第一个步骤是单元状态丢弃(如下图)。

图中有两个量x_t和h_t-1。x_t就是当前的输入,h_t-1是上一时刻的隐层的输出。这个公式求出来一个0-1之间的值,决定要留下多少东西。(任何东西乘以0-1其实就是计算要留多少东西,乘以0什幺都留不了,乘以1就都留下,乘0.8就留80%。)

 

第一步:单元状态丢弃

 

第二步新信息的选择。 当前输入包括上一时刻隐层的输出和当前的输入。这一步骤判断应该留下来多少内容。它还是计算两个系数,一个i_t,这也是一个0-1之间的值。第二个是C_t,表示当前cell的状态。计算完毕后需要把这两个系数的值保存下来。

 

 

第二步:新信息选择

 

第三步是更新状态。 上面一步已经决定可以留下的新内容和老内容。这一步要决定如何组合新老内容。老内容可以乘以第一步计算出的f_t,新内容可以乘以第二步算出来的i_t,然后把新老内容相加,就是最新的状态了。

 

 

第三步:单元状态更新

 

第四步是得出最后的输出值。 Cell不会一股脑输出,而是计算出了系数o_t和状态相关的函数结果相乘后得出输出。

 

 

第四步:确定输出

 

以上四步定义了LSTM基本的原理。LSTM其实提出来已经很多年了,在很多场景下都经受了考验。所以希望大家一定要把上面介绍的基础原理了解好。 下图显示了基于深度学习的信息抽取技术Bi-LSTM+CRF的原理。 这个方法代表了深度学习和传统的机器学习一个很好的结合。 传统CRF最大的问题是特征很稀疏,想做一个很好的特征要花费很多时间。 我们可能会有几套比较经典的特征,但不一定保证效果最好,特别是训练数据发生变化以后。而词向量和Bi-LSTM可以做很多的特征提取工作。

 

为什幺要用Bi-LSTM而不是简单的LSTM? 举个例子,“华为发布了新一代的麒麟处理X”这句话中,“X”一看就是处理器的“器”。因为我们都知道前文“麒麟处理”后面肯定跟着“器”。类似地,根据“X鲜和美国签订了新一轮的谅解备忘录”很容易猜出X是“朝鲜”的“鲜”,这是根据后文做出的判断。天然的语言中存在前后文的信号,都会影响当前字的选择。Bi-LSTM可以兼顾前后文的影响,所以是从理论上来说是个很符合人类直觉的工具。 如果不用CRF,可能整体效果还不错,但会出现很多badcase。 比如B后面出现S,S后面出现O。因为算法只考虑当前的最优输出,没有考虑整个序列的最优结果。而CRF是一个考虑全局的算法,也考虑到标签间的转移概率。所以用CRF会得到一个比较可控的结果。

总得来说,上图介绍的Bi-LSTM+CRF方法,结合了CRF和Bi-LSTM,把“小明去达观数据开会”这几个字变成向量,通过中间的Bi-LSTM隐层,提取出来高维的特征,输入CRF层,CRF最后就会给出标签和结果。

下面我们会介绍这篇文章最重要的部分:

预训练模型 。 深度学习除了不用做大量的特征工程,还可以对文本做非常好的表示。这里的例子是用Word2Vec做出词向量,然后用TensorBoard可视化,如下图所示。

 

在图中“威海”、“潍坊”、“枣庄”这三个山东的城市的词汇,被转化成了三个低维向量,向量中的数都是浮点数,有正数也有负数。如果从空间的角度来看这三个向量,可以发现它们距离很近,说明从语义角度来看它们的含义很接近。而且我们还可以直接对这些词向量进行计算,例如山东-威海=广东-佛山,皇帝-皇后+女人=男人,所以词向量是很优秀的自然语言的表征方式。 上图用的是Word2Vec模型。 下图还有一些其他的模型,比如Glove。这两个模型都是静态表示。静态表示有天然的缺陷,例如它们很难区分“苹果好吃”和“苹果手机”中的两个“苹果”。就好像我们学技术的时候什幺都想学,但因为时间是有限,所以每种技术学得都不够深入。

 

 

所以从2018年开始,出现了很多新的预训练模型,不少模型都用《芝麻街》里怪物的名字命名,比如ELMO、BERT和ERNIE。除此之外还有微软的MASS,Google最新的XLNet等等。这些模型本质上都用深度学习的神经网络做表示,虽然有的用Attention,有的用Transform,但本质差别不大。 这些模型和Word2Vec/Glove最大的区别在于它们是动态模型。 下图是一个真实的例子。输入“苹果好吃”和“苹果手机”后,用BERT对每个字建模,发现前两个字的向量很不一样。这说明BERT可以根据不同的上下文语境编码每个字,或者说可以根据上下文语境对同一个字做出不同的表示。

 

 

BERT可以根据上下文,对同一个字做出不同的表示

 

如何选择预训练模型呢? 我建议大家可以都尝试一下。大部分同学都可以训练ELMO,它的结构和LSTM很像,我们可以自己训练一个语言模型。BERT训练的成本就要高很多,但现在已经有一些其他的框架或语言做处理。我们自己用中文维基百科训练BERT只用了几天,也没有用很多显卡,当然我们也做了不少优化工作。可以先试着用Word2Vec看看效果,有可能效果已经很不错。关键在于要找到在能力范围内按时训练完的模型。

 

抽取算法在达观的具体实践

 

下面我们分享一下在达观的实践中完成抽取任务的一些经验和教训。

首先我们要注重场景。

应用场景一般就是客户提供的文档,包括财务报表、基金合同等等。文档处理的核心是自然语言处理,特别是抽取技术。我们也需要考虑实际应用,结合一些其他的工程技术,比如外部系统、分布式技术、数据库技术等等。

 

第二是要解决数据不足的问题。 尤其是序列标注比文本分类需要更多的标注成本,所以很可能数据量不够。虽然目前有一些通用的数据(比如《人民日报》的数据),但针对具体的业务场景可能没有足够多的语料和标注数据。这时候我们就要做数据增强。数据增强是一种通用的方法,可以应用于传统的机器学习和深度学习中。

 

 

在上图中,我们可以看到标注数据只有三句话,黄色表示要做机构识别。 怎幺增加标注数据的量? 我们可以直接暴力地把它们两两随机组合。初听起来可能会觉得有点不可理喻,但确实有效果。上图中右边的三段话中,前两段是两两随机组合,最后一段是把三句话全部混合到一起。把这些新生成的数据加入原数据起去做模型,就会发现效果的确好了很多。数据增强为什幺有效?从模型的角度简单地说,这样可以看到更多上下文,特别是可以跨句子看到上下文,所以会有帮助。基本上写5-10行代码就能产生一些收益。

还有一种方法是非监督的Embeddin的学习。

下图是我们的一个真实的例子。当时登贝莱刚转会到巴塞罗那俱乐部。我们用标准语料去训练,发现“登贝莱”这个名字一定会被切开,无论怎幺训练分词都不行。潜在的解决方法之一是增加很多登贝莱相关的标注数据,但是这幺做收益不足。所以我们就找了很多外部的语料做嵌入。

 

如上图所示,我们在网上找了一些登贝莱的新闻补充到《人民日报》等语料里一起训练。在完全没有修改,只是重新训练了预训练模型的情况下,“登贝莱”就成了一个词。这说明深度学习的预训练模型,可以非常好地捕捉到上下文,而且我们知道大部分的神经网络的语言模型训练是非监督学习,所以不需要很多标注数据。可以有很大数据量。总体来说数据越多,模型会学得越准,效果越好。BERT训练了一两千万的中文后,可以达到非常好的效果,我觉得这是个大力出奇迹的模型。 除了NER,还可以抽取别的内容。 例如知识图谱就要做关系抽取。输入一句话,“美国总统特朗普将考察苹果公司,该公司由乔布斯创立”,怎幺抽取关系?有两种方法。一种方式是把实体抽出来,然后两两实体做一些分类,分到一些关系里面。另一种依靠序列标注,也就是基于联合标注的方法。这幺做的好处是不用修改标注框架。

 

 

我们总结一下本文内容。在实际工作中,到底怎幺来用深度学习挖掘文本?最重要的一点是要用预训练模型,通过非监督数据训练向量,提升泛化能力。虽然中间步骤难以分解,但因为深度学习有端到端的能力,所以对中间步骤要求较低。而且,深度学习能克服一些传统模型的缺点,例如LSTM的上下文依赖就比CRF强。

 

 

但是深度学习也有一些缺点,它在小数据集上的效果难以保证,很可能会过拟合或者难以收敛。例如大家看到TensorBoard经常在抖,就是有这样的问题。而且大家现在把深度学习调参的工作叫炼丹室,你也不知道好坏就在反复调。有时候调参的工作量不亚于特征工程,特征工程至少知道在做什幺,而想分析调参结果更加困难。另外深度学习对计算资源的要求更高。 所以我们最终的思考是: 第一要尽可能地收集数据、理解数据 ,这是所有做机器学习的同学第一步就应该做的事情。我们应该去分析数据、看数据,而不是一开始就上模型。如果不做数据清洗,好数据、乱数据、脏数据都在里面,模型是做不好的。就像教孩子一样,如果好的坏的都教,他就不知道什幺是好坏了。 而且我们要分析问题的本质,选择合适的模型。 例如,对于已有数据的数据量,选先进模型有用吗?如果没有用,就要赶紧去收集数据。 而且在任务一开始的阶段,我比较推荐大家做传统的机器学习,因为这些模型比较现成,也比较通用。 在做了一个非常好的baseline之后,你就知道底线在哪,然后再引用深度学习。去年的达观杯我们就发现很多参赛者一上来就在用深度学习,结果做了各种调参,效果还不如我们自己20行代码的传统的机器学习。所以刚开始的时候一定要让传统机器学习帮助你,这样你更有信心做后面的事情。另外,这句话一定要送给大家:“数据决定效果上限,模型逼近此上限”,所以大家一定要重视数据清理,数据的分析真的比调参调模型收益更大。 如果遇到疑难杂症,端到端技术经常会有惊喜,但不能保证每次都有惊喜。 大家在学习的过程中一定要关心最前沿的技术。 做机器学习肯定会遇到失败和挫折,重要的是从挫折中总结规律才是最重要的,不要被同一个坑绊。 这样的经验很难依靠别人教会,因为所处的环境、场景、场合、数据不可能完全一致,所以需要有自己的思考。 最后,看完了这篇文章能做什幺呢? 可以参加我们的“达观杯”文本智能信息抽取挑战赛 。 这是我们第三次组织“达观杯”比赛。比赛的一等奖有30000元奖金,二等奖2支队伍有10000元的奖金,三等奖有5000元的奖金,优胜奖还有3000元。除此之外,TOP30同学直接直通面试。

 

 

大家学习完以上基础可以用我们介绍的内容做一些实践。比赛的数据很有意思,文字经过了加密,每个字都做了一个随机的映射。这幺做的好处是可以更多地关注算法的本身,而不用去想如何补充数据。虽然补充数据在实际工作中很重要,但我们的比赛主要还是考察算法。 比赛数据有两部分,一部分是有标注的数据,另外一部分是一个规模达到上百万的非标注的数据。 比赛的关键就是如何利用这些非标注的数据来提升整个模型的效果。而这就是我们最终在实际生活和工作中遇到的问题:只有少量标注数据,但是有大量的未标注数据。欢迎大家在比赛中实际运用一些算法和理论。因为有时候光看别人的分享难以获得深刻的理解,但是经过“达观杯”这样的比赛就能把知识掌握地更好。

 

 

截至目前,“达观杯”文本信息智能抽取挑战赛已吸引来自中、美、英、法、德等26个国家和地区的2400余名选手参赛,并将在8月15日进行第三场技术分享直播。

 

比赛页面: Introductionbiendata.com

 

访问上方链接 进入比赛页面,也可扫描二维码进入 赛事QQ群 (807070500),获取更多 大赛详情和技术直播安排 。

 

 

第二部分:“达观杯”baseline代码分享

 

达观数据工程师:

 

现在我们着重讲一下basline代码。baseline代码可以在比赛网站的“数据”页面 (https://biendata.com/competition/datagrand/data/)下载。

 

在前一章中,高翔老师给大家提到过做命名实体识别的几种方式:1) 基于规则;2)基于机器学习;3)基于深度学习。因为这次达观杯比赛的数据经过了特殊处理,所以没法用基于规则的方法做。在这里我们介绍一下后两种方法。

 

对于传统的机器学习算法来说,特征工程是特别重要的一项,常常会占用我们特别多的时间。而且baseline提供的算法来说,设计特征模板也是一个重要的步骤,它会影响最后出来模型的整体效果。

 

而如果要用深度学习的方法做信息抽取,就需要比较多的机器资源,可能还需要更多的标注数据,才能在深度学习的算法上获得较好的效果。

 

下面我们看一下baseline代码。首先需要引入相关的库:

 


 

import codecs

 

import os

 

整个代码分成以下5个部分:

 


 

# 0 install crf++ https://taku910.github.io/crfpp/

 

# 1 train data in

 

# 2 test data in

 

# 3 crf train

 

# 4 crf test

 

# 5 submit test

 

首先我们需要CRF++工具,大家可以到 https://taku910.github.io/crfpp/ 下载工具。 然后我们可以分析一下代码:

 

第一步: 处理训练数据

 


 

# step 1 train data in

 

with codecs.open(‘train.txt’,’r’, encoding=’utf-8′)as f:

 

lines = f.readlines()

 

results =[]

 

for line in lines:

 

features =[]

 

tags =[]

 

samples = line.strip().split(‘ ‘)

 

for sample in samples:

 

sample_list = sample[:-2].split(‘_’)

 

tag = sample[-1]

 

features.extend(sample_list)

 

tags.extend([‘O’]* len(sample_list))if tag ==’o’else tags.extend([‘B-‘+tag]+[‘I-‘+ tag]*(len(sample_list)-1))

 

results.append(dict({‘features’: features,’tags’: tags}))

 

train_write_list =[]

 

with codecs.open(‘dg_train.txt’,’w’, encoding=’utf-8′)as f_out:

 

for result in results:

 

for i in range(len(result[‘tags’])):

 

train_write_list.append(result[‘features’][i]+’\t’+ result[‘tags’][i]+’\n’)

 

train_write_list.append(‘\n’)

 

f_out.writelines(train_write_list)

 

我们知道做命名识别相当于一个序列标注的问题,所以在这里我们需要把这个数据转化成不同标签集序列标注的样式,这里有不同的几种标签集,这里我是用BIO去做的,大家也可以去尝试其他方式。

 

在比赛中,我们的训练集格式是几列特征加上一列标签的。我这里用了最基本的特征,就是字符本身的特征。不过比赛使用的不是字符本身,而是我们经过特殊处理之后每一个字的index,以及这个index对应的标签。在比赛中,我们定义了a,b,c三种标签,再通过中划线连接,就是代码中的[‘B-‘ + tag]和[‘I-‘ +tag]。

 

在这里大家也可以添加一些其他的特征。对于NLP来说,常见的特征包括词性、词频、词边界、实体的边界等。多加入几个这样的特征,可能会对效果有一些影响。

 

第二步: 处理测试集

 

代码如下:

 


 

# step 2 test data in

 

with codecs.open(‘test.txt’,’r’, encoding=’utf-8′)as f:

 

lines = f.readlines()

 

results =[]

 

for line in lines:

 

features =[]

 

sample_list = line.split(‘_’)

 

features.extend(sample_list)

 

results.append(dict({‘features’: features}))

 

test_write_list =[]

 

with codecs.open(‘dg_test.txt’,’w’, encoding=’utf-8′)as f_out:

 

for result in results:

 

for i in range(len(result[‘features’])):

 

test_write_list.append(result[‘features’][i]+’\n’)

 

test_write_list.append(‘\n’)

 

f_out.writelines(test_write_list)

 

预测集的处理方式和训练集是一样的,只有一点区别,测试集没有最后一列标签。这些都处理好了之后,我们就可以用我们安装的CRF++工具去调用这个命令训练。代码如下:

 

第三步: CRF++训练

 


 

# 3 crf train

 

crf_train =”crf_learn -f 3 template.txt dg_train.txt dg_model”

 

os.system(crf_train)

 

这里需要重点强调一下对模型影响比较大的特征模板(存储在template.txt)。template.txt文件可以在本文最上方的链接[c4] 下载,文件内容如下:

 


 

# Unigram

 

U00:%x[-3,0]

 

U01:%x[-2,0]

 

U02:%x[-1,0]

 

U03:%x[0,0]

 

U04:%x[1,0]

 

U05:%x[2,0]

 

U06:%x[3,0]

 

U07:%x[-2,0]/%x[-1,0]/%x[0,0]

 

U08:%x[-1,0]/%x[0,0]/%x[1,0]

 

U09:%x[0,0]/%x[1,0]/%x[2,0]

 

U10:%x[-3,0]/%x[-2,0]

 

U11:%x[-2,0]/%x[-1,0]

 

U12:%x[-1,0]/%x[0,0]

 

U13:%x[0,0]/%x[1,0]

 

U14:%x[1,0]/%x[2,0]

 

U15:%x[2,0]/%x[3,0]

 

# Bigram

 

B

 

这里我们用了一个比较简单的特征模板,取了离当前字最远的前3个字和后3个字,以及它们之间的组合特征(如U07到U09这几行)。我们可以看到这个模板其实特别简单,一共只有16个特征。这种选择和我们标注好的训练集性质有关。如果研究过比赛的训练集,就会发现数据中的实体是比较简单的,不会涉及特别长的文本抽取,所以这里正负3已经可以取到一个比较好的特征了。我也试过用再复杂一点的模板去做,但是效果不如这个简单的模板好。

 

不过,如果像之前提到的,在第一步训练集处理的过程中加了一些其他的NLP的特征工程(比如词性、词频或者词边界、是否是句子的结尾、是否是实体的结尾等),那幺我们就需要在template文件里加入更复杂的特征。

 

具体来说,因为我们现在可以看U00到U06对应的都只是一个坐标,代表了当前字向前和向后的字,但是我们并没有横向地去取特征。如果大家加了其他的特征工程,那幺在每行(如U00:%x[-3,0])后面加上这个字的其他特征,比如这个字本身及前后几个字是否是一个停用词,或者这一个字的前一个字是不是停用词的特征。

 

除了Unigram,还可以选择Bigram。Bigram和Unigram非常相似。但是它引入了状态转移函数。状态转移函数考虑了当前输出标签的前一个标签是什幺。所以它会有全局的概念。

 

在CRF训练代码crf_train = “crf_learn -f 3 template.txt dg_train.txtdg_model”中,除了template.txt外,还有其他一些参数。例如-f 3,代表Template里设定的特征函数的频率值。如果它低于这个值的话,我是会把它删除。这里大家可以看到我们在template.txt中只尝试取到3个字的组合(例如文件中的U07:%x[-2,0]/%x[-1,0]/%x[0,0]),再多的都没有了。如果大家取到4个字或者5个字的组合,这些特征是非常稀疏,对整个模型没有很大贡献了。所以这里我们设一个最低的值,可以把那些稀疏的特征去掉。

 

除了这个以外,还会有一个惩罚系数,也一定程度提高这个模型的泛化能力。

 

第四步: CRF++生成预测结果

 


 

# 4 crf test

 

crf_test =”crf_test -m dg_model dg_test.txt -o dg_result.txt”

 

os.system(crf_test)

 

在我们训练好这个模型之后,就可以调用crf_test这个命令生成预测结果。

 

第五步: 生成可提交的文件

 

最后一步是生成可以提交的文件,代码如下:

 


 

# 5 submit data

 

f_write =codecs.open(‘dg_submit.txt’,’w’, encoding=’utf-8′)

 

with codecs.open(‘dg_result.txt’,’r’, encoding=’utf-8′)as f:

 

lines = f.read().split(‘\n\n’)

 

for line in lines:

 

if line ==”:

 

continue

 

tokens = line.split(‘\n’)

 

features =[]

 

tags =[]

 

for token in tokens:

 

feature_tag = token.split()

 

features.append(feature_tag[0])

 

tags.append(feature_tag[-1])

 

samples =[]

 

i =0

 

while i < len(features):

 

sample =[]

 

if tags[i]==’O’:

 

sample.append(features[i])

 

j = i +1

 

while j < len(features)and tags[j]==’O’:

 

sample.append(features[j])

 

j +=1

 

samples.append(‘_’.join(sample)+’/o’)

 

else:

 

if tags[i][0]!=’B’:

 

print(tags[i][0]+’error start’)

 

j = i +1

 

else:

 

sample.append(features[i])

 

j = i +1

 

while j < len(features)and tags[j][0]==’I’and tags[j][-1]== tags[i][-1]:

 

sample.append(features[j])

 

j +=1

 

samples.append(‘_’.join(sample)+’/’+tags[i][-1])

 

i = j

 

f_write.write(‘ ‘.join(samples)+’\n’)

 

这一步也是选手反映出现问题最多的一个步骤,因为可能大家的操作系统不同。如果用的Windows系统,split(‘\n\n’)应该要换成split(‘\r\n’),然后再进行后续处理,不然是会报错的。

 

除此之外,还有一些选手会反映系统总是会报各种各样的分类错误和异常。我建议大家检查换行符,保证提交的文件是3000条,不能多一行、少一行,也不能在最后一行加换行符。还有一点,虽然我们只评测a,b,c这三个类型的字段,所以/o不参与评分,但我们提交时还是要包含/o,否则也会报错。

 

Be First to Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注