GATE 中文自然语言处理系列之三:中文分句(基于 JAPE)
一、中文分句概述
中文分句也是自然语言处理的基础之一,一般来说,中文中每个句子都表达了一个完整的意
思。GATE 的 ANNIE 提供了一个 ANNIE Sentence Split 资源,可以用于划分英文句子,但
对中文的划分效果很不好(整篇文章分成了一个句子)。
GATE 提供了两种句子划分的方法:基于 JAPE 规则和基于正则表达式,分别对应于类
gate.creole.splitter. SentenceSplitter 和 gate.creole.splitter.RegexSentenceSplitter。本文先来
讲述如何基于 JAPE 规则进行中文句子划分。
二、JAPE 简介
JAPE 的全称是 a Java Annotation Patterns Engine,Java 标注模式引擎,JAPE 提供了
基于正规表达式的标注有限状态转换。JAPE 是 CPSL(Common Pattern Specification
Language1)的一个版本。我们通过 JAPE 语言可以编写 GATE 能够识别的规则,通过这
些规则来进行较准确的命名实体识别。
总的来看,每条规则由左侧和右侧两部分组成。每条规则的左侧部分(LHS: Left Hand Side)
是一个包含正则表达式操作符(比如*, ?, +)的标注模式。每条规则的右侧部分(RHS: Right
Hand Side)包含了标注集操作描述。与左侧部分(LHS)匹配上的标注集将会按照右侧的
操作执行。下面让我们具体来看看 JAPE 规则的一些细节内容。
2.1 模式匹配
每条规则的 LHS 部分都可以包含一些操作符,这些操作符的含义与正则表达式相同,如下:
|:或者
*:零次或者多次发生
?:零次或者一次发生
+:一次或者多次发生
操作符可以对包含在圆括号中的任何模式进行操作。每个需要标注的完整模式都应当被圆括
号所包含,并且有标签标注。标签之前有冒号,比如下面的例子里,
标签就是:loc
(
{Lookup.majorType == location}
)
:loc
规则的 LHS 可以有多种模式以及相应的标签。比如:
(
{Lookup.majorType == jobtitle}
):jobtitle
(
{TempPerson}
):person
嵌套的模式只要正确标注也是允许的, 比如:
(
(
{Lookup.majorType == jobtitle}
):jobtitle
{TempPerson}
):person
2.2 上下文
模式之前或之后的上下文也可以通过将其包含在一对圆括号中来指定。被标引的模式和不被
标引的上下文之间的区别很简单,就是标签的有无,比如,没有标签的模式就是上下文。下
面的规则将会标引这样的模式:在之前出现了 in 或者 by 的年份。注意,在相同的语法情况
下,上下文不能在多条规则中被重复用。比如,当规则失效时候它也跟着失效。
Rule: YearContext1
({Token.string == “in”}|
{Token.string == “by”}
)
(YEAR)
:date
2.3 宏
宏也可以被使用在规则的 LHS 中。这意味着,把信息在宏中表达,宏可以被规则调用。这
么做的原因很简单,就是为了避免在几条规则中重复相同的信息。宏还可以在其他的宏之中
被调用。宏在一段语法的多个阶段使用的时候,不需要重复声明。
下面的例子展示了一个在规则中使用的宏 TITLE。一般来讲,宏都用大写字母来写,当然这
并不是必须的。
当在一条规则中使用的时候,宏一定不能被大括号包含,只能用圆括号包含。
Macro: TITLE
(
{Title}
({Token.string == “.”})?
({Title})?
({Token.string == “.”})?
)
Rule: TitlePerson
(
(TITLE)
({Upper})+
):person
–>
…
2.4 简单模式动作
规则的 RHS 包含将要赋给模式的标注信息。关于模式的信息通过标签,被传递给规则的 LHS。
最终,特性和相应的值会被附加给标注集。
在如下的例子中,标签是“loc”,规则的 RHS 是跟在箭头后面的部分。标签被传递给规则的
RHS 部分,并且标注类型“Location”被赋给模式。标注集被赋予两种特性,”kind”和”rule”,
分别取值”city”和”GazeCity”。
Rule: GazCity
(
{Lookup.majorType == city}
)
:loc –>
:loc.Location = {kind=”city”, rule=‘‘GazCity’’}
上面的知识讲解了如何编写一条 JAPE 规则的要领,但是只有把规则应用在具体的中文语言
环境中,才可以起到提高命名实体识别准确率的作用。
当然,JAPE 的用法还有很多,包括可以嵌入 JAVA 代码、使用本体等,后续会继续来讲。
二、JAPE 与正则表达式的关系
从官方手册的描述“JAPE is a Java Annotation Patterns Engine. JAPE provides finite state
transduction over annotations based on regular expressions.”来看,GATE 是对于正则表
达式的封装,它基于正则表达式实现对标注的有限状态转换。
正则表达式只能用于描述字符串集合, GATE 弥补了这个缺点,实现了对于标注的处理。
但正则表达式的另外一个缺点 JAPE 无法避免,即正则表达式是数据项的简单线性序列,不
能描述图结构,而 GATE 的标注模型恰恰是图结构,因此导致 JAPE 匹配的过程为非决定
式的(例如,结果依赖于一些随机因素如数据在虚拟机中存储的地址等):当图中的结构在
匹配时需要规则匹配之外的能力来识别时,JAPE 会任意选择一个可选择的。然而,这并不
是坏消息,因为在许多有用的场景中,在 GATE(以及其他的语言处理系统)的标注图中存
储数据的数据可以被看为简单的序列,能够使用正则表达式来自动匹配。
三、基于 JAPE 实现中分分句
以下的 JAPE 脚本基于 ANNIE Sentence Split,并对其进行了修改,以使得能够满足中文分
句。我们将中文分句划分为三个部分:(1)寻找分词标点;(2)划分句子,增加标注;
(3)清理。
3.1 寻找分词标点
根据国家标准 GB/T 15834-1995《标点符号用法》,描述句子结束的词语包括句号、问号、
感叹号、省略号等,另外换行符也是句子结束的标注,因此寻找分词标点就包括两个规则,
代码如下:
Java |
copy code |?
01
02
03
04
/*
* Author:whuwy
* Based on GATE ANNIE's find.jape resource
05
*/
06
07 Phase: find
08 Input: Token SpaceToken
09 Options: control = appelt
10
11 Macro: FULLSTOP
12
(
13
14
15
16
17
18
)
{Token.string == "。"}|
{Token.string == "!"} |
{Token.string == "!"} |
{Token.string == "?"} |
{Token.string == "?"}
19
20
//we'll allow suspension points of three or six dots
21 Macro: THREEDOTS
22
(
23
({Token.string=="."})[6]|
({Token.string=="."})[3]
24
25
)
26
27 Macro: NEWLINE
28
(
29
30
31
32
33
34
)
{SpaceToken.string == "\n"} |
{SpaceToken.string=="\n\r"} |
({SpaceToken.string=="\n"}{SpaceToken.string=="\r"}) |
{SpaceToken.string=="\r\n"} |
({SpaceToken.string=="\r"}{SpaceToken.string=="\n"})
35
//normal sentence split
36 Rule: Split1
37
(
38
39
40
)
FULLSTOP |
THREEDOTS
:split
-->
:split.Split = {kind = "internal"}
41
42
43
44
45 Rule: CR
46
(
NEWLINE
({SpaceToken.kind == space})*
NEWLINE
({SpaceToken.kind == space})*
):cr
-->
:cr.Split = {kind = "external"}
47
48
49
50
51
52
53
54
可以注意到,find.jape 脚本依赖于两种类型标注的输入:Token 、SpaceToken,而 Token 、
SpaceToken 标注正是我们上文中文分词插件创建的标注,这验证了上文提到的“分词是中
文自然语言处理的基础”,同时也提示我们在 GATE 的工作流中,分句必须要在分词之前完
成。
3.2 划分句子,增加标注
句子划分的思路就是根据上一阶段中找到的分词标点,获取两段标点之间的所有 Token,找
到第一个 kind=word 的 Token,从这个 Token 的位置到标号位置即为一段 Sentence。为了
避免每次都重复从起始位置寻找,在每识别出一个句子后,在句子结束位置上增加
“temp-last-sentence-end”标注,这样下次就从这个标注以后来寻找新的句子。
代码如下:
Java |
copy code |?
01
02
/*
03
04
* Author:whuwy
* Based on GATE ANNIE's split.jape resource
05
*/
06
07 Phase:split
08 Input: Split TempNoSplitText
09 Options: control = first
10
11
//sentence that consumes a split
12 Rule: internalSplits
13
14
({Split.kind == "internal"}):isplit
-->
15
{
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Long endOffset = ((AnnotationSet)bindings.get("isplit")).
lastNode().getOffset();
//find the end offset of previous sentences
Long lastOffset = (Long)doc.getFeatures().get("temp-last-sentence-end");
if(lastOffset == null) lastOffset = new Long(0);
AnnotationSet tokens = inputAS.getContained(lastOffset,
endOffset);
if(tokens != null) tokens = tokens.get("Token");
if(tokens != null && tokens.size() > 0){
List
tokList = new ArrayList(tokens);
Collections.sort(tokList, new OffsetComparator());
for(Annotation token : tokList){
String tokenKind = (String)token.getFeatures().get("kind");
if("word".equals(tokenKind)){
Long startOffset = token.getStartNode().getOffset();
if(startOffset.compareTo(endOffset) < 0){
//create the new sentence