语义解析Parser用户手册

导论

Phoenix parser是为简单健壮的自然语言接口应用程序,特别是口语应用的开发设计的解析器。因为自然语言经常是有语病的并且识别器也会有识别错误,所以parser具有鲁棒性在识别、语法和流畅性中修正错误是很有必要的。这类parser是为了提升解析这些类型的输入语料而设计的。

Parser 将每个输入(input)表达成一个或者几个语义上的frame。开发者需要定义一套frame并且提供将词句列入frame中槽位语法规则。

产品配置(不过多阐述)

  • ParserLib
  • Grammer
  • Scripts
  • Example
  • Server
  • Doc

操作理论

Parser将输入的词句映射到一个语义frame序列。一个frame是一套命名的槽位,这些槽位代表相关的一条条信息,图一展示了一个航班信息query的frame示例。每个槽位有一个相关的无关上下文的语法,这些语法指定词句匹配槽位的pattern,并且编译到递归转移网络(Recursive Transition Networks)中。图2展示了一个解析过的frame。当parser填充的时候,每个槽位包含一个语义解析树用于它所跨越的字串,语义解析树的跟就是槽位的名字。
Figure1 Example Frame
Figure2 Example Parsed Output

图3展示了整个解析的流程。在搜索算法中,与声学匹配很相似,生成了一个词图,语法的槽位与字串相匹配并且生成了一个槽位图。这些active frames集合定义了active槽位的集合。每个槽点指向相关递归转移网络的根。这些网络与自上而下递归转移网络图标解析算法的输入词序列相匹配。

Figure3 The paring process

解析的过程从输入的每个词开始从左到右尝试去匹配每一个槽位网络,如下:

for (each word of input)
    for (each active slot)
        match_net ( slot, word )

match_net函数是一个与指定位置开始的字串匹配RTN的递归函数,这个函数生成所有的从词语位置开始的,或许有一些不同的结束点的网络匹配。这些网络并不是为解析整条语句而设计的,仅仅是解析词条的序列。递归传递网络的槽位在匹配的过程中调用其他的网络。每次网络匹配就是一种尝试(整个网络,不仅仅是槽位)。所有匹配的网络都被添加到网络中,任何时候尝试一次网路匹配,图表是首先被核对来判断匹配是否在之前被尝试过。当一个槽位匹配成功了,就会被添加到槽位图中。每一个在槽位图中的槽位序列就是一条路径,路径的分数是由序列所解释的单词数。当匹配一个槽位时,词语是不会被跳过的,但是在已经匹配过的槽位就可以被跳过。在图的扩展过程中低得分的路径会被去除,就像声学搜索那样。去除的标准是:第一,解释词语的数量,第二,序列碎片化的程度。如果两种路径覆盖了相同的输入部分并且其中一个比另一个解释了更多的词语,那么少的那个就会被移除。如果两条路径解释了相同数量的词语,并且其中一条路径比另外一条用了更少的槽位,那么那个用的更多槽位的那条路径就会被移除。最终结果图代表了所有的序列发现有个分数等于最好的,槽位序列用图表示出来然后整合到frames中,这仅仅是通过将frame标签分配到槽位完成的。然后回到整合中,更少的碎片化解析是更好的,这意味着如果有两个解析各有5个槽位,其中一个用了两个frame,另外一个用了三个frame,那么解析用两个frame的那个会更好。结果是一个槽位图,每个槽位被一个或者多个frame标签所标注,每条路径穿过这个图,所有得分都相等,就是一个解析(parse)。这些机制自然地产生部分或者碎片化的解析。考虑到语法和输入,动态的编程搜索会尽可能地产生最完整碎片化最少的解析。
解析器(parser)不需要语句边界,并且可以处理很长的输入。它按照自然断点给输入语句进行分词并且在缓存里展示一些垃圾的收集。这使得它在必要时允许将整个报告解析为单独的表达,处理的速度和输入的长度是线性相关的。
如何健壮或者约束一个系统是取决于frame和语法是如何构建的。用一个槽位的frame会产生一个标准的CFG解析器,这样做虽然高效,但是对于无法预测输入是没有鲁棒性的。在另一方面,让每个内容词语有相互独立的槽位将会产生一个关键词解析器。介于这两者之间的某个地方,通常会有精确性和鲁棒性最好的结合。

编写语义语法和frame

解析器需要两个输入文件,frame文件和语法文件(.gra)。

frame文件

这个文件指定frame需要被解析器所调用,一个frame就代表程序的一些基本的类型的操作或者对象,frame中的槽位表示与这些操作或者对象的相关信息,每个槽位都有相关的一套语法规则,槽位的名字就是相应语法解析树的根。
frame的句法定义如下:

    #comment
    FRAME:<frame name>
    NETS: slot names
        [<slot name>]
        [<slot name>]
        [<slot name>]
    ;

    举一个酒店frame的例子,如下所示:

    #预定酒店房间
    FRAME:酒店
    NETS:
        [酒店请求]
        [酒店名]
        [入住时长]
        [酒店地址]
        [房间类型]
        [到达时间]
        [需求]
    ;

frame的终结标识是’;’,写在下一行的开始处。
不要忘记以分号结束frame,否则就不会被识别。
不要忘记以分号结束frame,否则就不会被识别。
不要忘记以分号结束frame,否则就不会被识别。(重要事情说三遍)

语法文件(.gra)

语法文件的名字结束的地方是以”.gra”扩展名结尾的,这些文件包含了语法规则。语法都是遵循无关上下文规则,它所指定的词语模式对应于标识(网络名称)。
对应标识的语法句法如下:

    #optional comment
    [标识名称]
     (<模式 a>)
     (<模式 b>)
    <宏1>
      (<复写宏1的规则>)
    <宏2>
      (<复写宏2的规则>)
    ;

如上所示,标识名称被方括号括起来,在这之后跟着一套复写的规则,每一行写一条,被圆括号括起来并在前面有空格标识。之后再跟着宏的复写,格式和之前的是一样的。
在模式(pattern)中的符号运用规则如下:
*小写的字符串是终端。
*大写的字符串是宏。
*被方括号[]括起来的是非终端(会调用其他的标识规则)。
*被尖括号括<>起来的是宏。
*常规表达
*item 表示0个或者1个item的副本
+表示1个或者多个副本
+*表示0个或者多个副本
#include<文件名> 在那个点读取文件

宏在相同的语法规则之后会复写指定的规则,这些会让复写的文本替代原有的宏,但是不会引起非终端的标识出现在parser中。宏允许一个更加简单的语法表达式,并且排除不想要的标识在parser中。

举一个例子解释标识的语法规则,[酒店需求]和[想要]:
[酒店需求]
    (*[想要]*{一间}<酒店>)
<酒店>
    (酒店)
    (房间)
    (公寓)
    (宾馆)
;
 [想要]
     (*[我]<想>)
[我]
    (我)
    (我们)
<想>
    (想要)
    (需要)
    (想去)
    (想)
;

同时再举一个英文的例子,[hotel_request]和[want]:

[hotel_request]
    (*[want] *a HOTEL)
HOTEL
    (hotel)
    (motel)
    (room)
    (accommodations)
    (place to stay)
;
[want]
    (*I WANT)
I
    (i)
    (we)
WANT
    (want)
    (need)
    (would like)
;

按照整个grammar文件来看,I would like a hotel room 就是:
[hotel_request]([want](i would like)a hotel room)

编译语法

语法是调用脚本编译来进行编译的,在轮流调用常规的compile_grammar的语法的字典中,图4展示了一个示例的编译脚本。其中语法文件可以是一份单独的文件,也可以是分布在多个文件中。如果语法是在多个文件中的话,编译脚本会将他们连接到一个单独的文件中,然后再传递到compile_grammar中。接下来是编译语法的脚本的例子,他在本地路径用了两个语法文件,Place.gra和Schedule.gra,并且它从本地库中加载了grammar文件date_time.gra,numbers.gra和next.gra。独立的grammar文件首先连接到EX.gra文件中,然后EX.gra文件会编译到EX.net中。编译的脚本在本地路径创建了一个base.dic文件,这是一个字典文件,一个包含许多被数字标注的词语的文本文件,文件中的每一行都有一个词语并且有数字将其标注。这些词语数字并不是连续的,它们是哈希编码之后得到的结果,这些数字将会在parser中用到。
Figure4 compile script

.net file - 编译后的语法(grammar)文件

    这些是ASCII文件表示的递归转移网络并且有如下的格式:
    第一行给到了一个已经编译过的网络编号 
    网络编号 Number of Nets = 378

    然后就是编译的网络,对每一个网络来说:
    第一行给到了:网络编号,网络中节点的编号,concept leaf 标志
    然后节点会排列,然后每一个后面都跟着一个arc。
    一个节点进入到了文件中就会有如下的格式:
    节点编号,节点之外的arc编号,结束标志位(final flag):0代表非终端节点 1代表终端节点
    对于每一个网络,从0开始,节点的编号是连续的。
    节点的arc遵循节点的条目。
    Arc条目有如下的格式:
    word_number net_number to_node
    arc 或许是:
    word arc, 并且word_number和net_number=0
    null arc,由word_number=0并且net_number=0
    call arc,由net_number>0(这种情况下忽略word_number)

    [航空公司] 27 5 0
    0 5 0
            0 0 2
            20294 0 3
            372 372 4
            344 344 2
            30 30 2
    1 0 1
    2 1 0
            72 72 1
    3 3 0
            18753 0 1
            18762 0 1
            16707 0 1
    4 3 0
            18753 0 1
            18762 0 1
            16707 0 1

数据结构

语法结构:

Phoenix parser可以使用多种语法,read_grammar这个方法会从一个.net文件读取一个编译过的语法,并且返回一个指向关联语法结构的指针。

    struct gram *read_grammar(dir, dict_file, grammar_file, frames_file);
    typedef struct gram
    {
            FrameDef    *frame_def;     /* pointers to frame definition structures */
            char    **frame_name;    /* pointers to frame names */
            int    num_frames;    /* pointers of frames read in */
            char   **labels;    /* pointers to name of nets */
            Gnode    **Nets;    /* pointers to heads of compiled nets */
            int    num_nets;    /* number of nets read in */
            char    **words;    /* pointers to strings for words */
            int    num_words;  /* number of words in lexicon */
            int    *node_counts;    /* number of nodes in each net */
            char    *leaf;    /* concept leaf flags */
            char    *sym_buf;    /* strings for words and names */      
    } Gram;

根据需要可以尽可能多的读取独立的语法,并且指针指向相应的gram储存结构。当parse的方法被调用,它将字串传到parse和指向gram结构体的指针,来实现parse方法:parse(word_string,gram)。

活跃的槽位组

除了控制语法在语法分析中使用外,当调用每个parse方法的时候,开发者同样有整套槽位的动态的控制。全局变量cur_nets(int *cur_nets)和num_active()是用来实现这个的。cur_nets指向这一套槽位(网络号码)用于解析和num_active指定数组中元素的数量。parser主要的匹配环路如下所示:

for( word_position=1; word_position < script_len; word_position++ ) {
    for( slot_number= 0; slot_number < num_active; slot_number++ ) {
        match_net( cur_nets[slot_number], word_position, gram)

如果cur_net是空的,parser就会创建一套语法frame的网络,并相应地指向cur_net和num_active。因此系统会在第一次调用parser()的时候初始化。为了在parser中使用所有槽位的子集,产生一组要用的网络号码,从cur_nets指向数组和数组中的数字元素。为了重置在语法中所有的槽位,只需要将cur_nets或者num_active设置为空。

图表

图表是一种数据结构,用来记录所有它们找到的网络匹配。 这样做是为了如果在语法中其他点调用的话不需要重复操作。在一些情况下,节省了相当大的计算量。这个图表是个三角形矩阵,它由单词的起始词和结束词索引,如图5所示。
Figure5

由于它是一个稀疏矩阵,所以图表实际上并不是作为一个矩阵来实现的,而是作为一个链表,以开始词作为主键,结尾词为小键。

parser数据结构

当最终parser结构创建后,它们被放入一个由变量解析器指向的缓冲区中。parser是一个SeqNodes序列,这些序列代表含有frame id的边缘。SeqNode结构包含了frame id,并且在图表中指向边缘。含有相同frame id都在同一个frame实体里面。节点按照顺序写进了缓冲区,在前一次解析的最后一个槽后,新解析的第一个插槽被写入。所有的话语都有相同数量的插槽,因为任何更分散的都将被去掉。每个解析器中的插槽数就是一个全局变量n_slots。在num_parses中寸有备用的解析器的数量。

打印parses

方法print_parses()是用来将parses写入文本缓存的。参数如下:

  • parse的数字会被打印出来(从0开始)
  • 指向文本缓存的指针会被写入
  • 提取的标志位(0=解析后的形式,1=提取后的形式)
  • 一个指向语法结构体的指针

提取的形式

解析器提供了一种更加直接的打印输出机制,它会打印token和字符串,这些都会被提取,提取输出的格式如下:

    <frame_name>:<Node_Name>.[<Node_Name>.[<Node_Name>.]][<value>]

举个例子:《猩球崛起3》在哪里上映?将会产生解析和提取的形式:

    电影信息:[电影信息]([电影](《猩球崛起3》)[在](在)[上映电影]([_影院名称](哪里))[上映](上映))------>
    电影信息:[上映电影].影院名称    电影信息:[电影].《猩球崛起3》

eg. Where is American Beauty playing?

    movie_info:[movie_info] ([Display_Movie]([_theatre_name](where))[is](is)[Movie](AMERICAN BEAUTY)[showing](playing))------>
    movie_info:[Display_Movie].theatre_name    movie_info:[Movie].AMERICAN BEAUTY

为了运用这种机制,开发者必须按照协定来编写语法。

终端前缀

提取的第二个特征输出就是终端前缀,这些网络名从一个下花心标志符“”开始。对于这些网络而言,提取的输出是这些网络的名字而不是字符串。在上述的例子中,[影院名称]或者是[_theatre_name]就是这样的。这是一种很方便的方法将值放置于一种规范的形式。举个例子,有很多种表达yes的方式,但是你需要将实际的yes传到数据库。如果你定义了一个[_yes]的网络,并且写出了所有表达yes的方式,而不是将会出现在提取结果中的原始的字符串,这同样适用于像Philly到Philadelphia的映射。举个例子,就像下面的规则:

    [Answer]
        ([_yes])
        ([_no])
    ;

    [_yes]
        (yes)
        (fine)
        (sure)
        (that's good)
        (sounds good *to *me)
    ;

    [_no]
        (no *way)
        (*i don't think so)
    ;

所以sounds good to me 会被解析成:

        [Answer]([_yes](sounds good to me))

并且会被提取:

        [Answer].yes

同理 I don’t think so 会被解析成

        [Answer]([_no](I don't think so))

提取出来:

        [Answer].no。

猜你喜欢

转载自blog.csdn.net/oscar6280868/article/details/78104867