放牧代码和思想
专注自然语言处理、机器学习算法
    This thing called love. Know I would've. Thrown it all away. Wouldn't hesitate.

基于神经网络的高性能依存句法分析器

目录

依存句法分析器综述

谈起依存句法分析,主流的统计句法分析一般分为两大流派——生成式和判决式。

生成式句法分析

生成式就是生成一系列句法树,从里面挑选出概率最大的那一棵作为输出。在具体实现的时候,可以选择最大熵等模型去计算单条依存边的概率,利用最大生成树算法来挑选最佳句法树,比如《最大熵依存句法分析器的实现》。

其优点是效果好,但开销大。训练的时候常常要用一份巨大的特征模板,得到的模型中含有大量复杂的特征函数。在解码的时候,这些特征函数的储存和运算成本很高。由于是全局最优,所以可以取得较高的准确率,还可以很方便地处理非投射的句法树。不过也由于搜索的全局性和特征函数的复杂度,模型常常会过拟合,在训练集和测试集上的准确率差别很大。

判决式句法分析

判决式一般是基于动作(或称转移)和一个分类器实现的,仿照人类从左到右的阅读顺序,判决式句法分析器不断地读入单词,根据该单词和已构建的句法子树等信息建立分类模型,分类模型输出当前状态下的最佳动作,然后判决式分析器根据最佳动作“拼装”句法树。

动作体系简介

一般有两大体系,分别处理投射和非投射两种语言现象。

投射

刘群老师在计算语言学讲义中说,大多数语言,包括汉语和英语,满足投射性。

投射.png

所谓投射性是指:如果词p依存于词q,那么p和q之间的任意词r就不能依存到p和q所构成的跨度之外(用白话说,就是任意构成依存的两个单词构成一个笼子,把它们之间的所有单词囚禁在这个笼子里,只可内部交配,不可与外族通婚)。比如:

神经网络依存句法分析2.png

再比如:

处理投射现象的动作体系有Arc-eager和Arc-standard (Nirve, 2004),后者就是本依存句法分析器采用的动作体系,后面会详细阐述。

非投射

非投射就没有上述限制了,这会导致依存边有交叉,怎么都理不顺:

神经网络依存句法分析3.png

事实上,不同于刘群老师的观点,Saarbrücken说道,25% or more of the sentences in some languages are non-projective。对于汉语,王跃龙还专门写了《汉语交叉依存类非投射性现象.pdf》进行分门别类地详细研究,比如:

神经网络依存句法分析4.png

处理非投射的动作体系有Arc standard + swap (Nirve, ACL 2009)。

本Parser无法处理非投射现象,也就是说,即使一个句子本身是非投射的,但分析出来的依存句法树依然是投射的。

Arc-standard详解

在arc-standard system中,一次分析任务神经网络依存句法分析5.png由一个栈s(题外话,有些人包括维基认为栈也叫堆栈,我强烈反对,堆是heap,栈是stack,heap和stack才混称堆栈),一个队列b,一系列依存弧A构成。如果定义一个句子为单词构成的序列神经网络依存句法分析6.png,那么——

栈s是用来储存系统已经处理过的句法子树的根节点的,初始状态下神经网络依存句法分析7.png。另外,定义从栈顶数起的第i个元素为si。那么栈顶元素就是s1,s1的下一个元素就是s2:

神经网络依存句法分析10.png

在一些中文论文中习惯使用焦点词这个表述,我觉得该表述更形象,如果我们将栈横着放,亦即让先入栈的元素在左边,后入栈的元素在右边:

神经网络依存句法分析9.png

则称s2为左焦点词,s1为右焦点词。接下来的动作都是围绕着这两个焦点词展开的。

队列

在《A Fast and Accurate Dependency Parser using Neural Networks.pdf》中,使用的是buffer这个单词,与其翻译为缓冲区,我认为这个概念更像一个顺序队列,而不是随机访问的缓冲区。队列模拟的是人眼的阅读顺序,人眼从左到右阅读,系统也从左往右读入单词,未读入的单词就构成一个队列,队列的出口在左边。初始状态下队列就是整个句子,且顺序不变:b=神经网络依存句法分析8.png

依存弧

一条依存弧(原文dependency arcs,我认为翻译成变换也许更好)有两个信息:动作类型+依存关系名称l。l视依存句法语料库中使用了哪些依存关系label而定,在arc-standard系统中,一共有如下三种动作:

LEFT-ARC(l):添加一条s1 -> s2的依存边,名称为l,并且将s2从栈中删除。前提条件:神经网络依存句法分析11.png。亦即建立右焦点词依存于左焦点词的依存关系,例如:

神经网络依存句法分析15.png

RIGHT-ARC(l)添加一条s2 -> s1的依存边,名称为l,并且将s1从栈中删除。前提条件:神经网络依存句法分析11.png。亦即建立左焦点词依存于右焦点词的依存关系,例如:

神经网络依存句法分析17.png

SHIFT:将b1出队,压入栈。亦即不建立依存关系,只转移句法分析的焦点,即新的左焦点词是原来的右焦点词,依此类推。例如:

神经网络依存句法分析21.png

可见当存在神经网络依存句法分析22.png种依存关系时,系统一共有神经网络依存句法分析23.png种变换。

基于神经网络的句法分析器

神经网络模型

初等神经网络背景知识请参考《反向传播神经网络极简入门》,这并不是本文的重点。

反向传播神经网络极简入门》中介绍的是二分类模型,然而arc-standard转移系统中,一共有神经网络依存句法分析23.png种变换,这时候就得将输出层由一个节点换成多个节点。

或者由一个节点换成一个softmax层(这也是Chen and Manning (2014)中的标准做法)。单一输出节点和softmax层的区别如图:

神经网络依存句法分析25.pngVS神经网络依存句法分析24.png

假设训练集由m个样本组成:神经网络依存句法分析26.png,每个样本的特征维度都是n+1,其中神经网络依存句法分析27.png对应截距项。

在二分类神经网络模型中,对隐藏层的输出做了一次逻辑斯蒂回归

神经网络依存句法分析28.png

这个假设函数对应y=0的概率,用1减掉这个得到y=1的概率,二分类就是这么简单。

而在多分类神经网络模型中,做的是softmax回归:

神经网络依存句法分析29.png

其中,输出是一个k维的向量,k是所有类别的数量。神经网络依存句法分析30.png是模型的参数,在实现的时候,一般用一个神经网络依存句法分析31.png的矩阵储存:

神经网络依存句法分析32.png

神经网络依存句法分析34.png则是用于概率分布的归一化。

不过,在LTP的实现中,并没有使用softmax层,而是直接用了隐藏层的输出:

神经网络依存句法分析33.png

这使得该神经网络模型的输出不再是概率,而是表示概率大小的一个“分值”,所以代码中的方法名为:

/**
 * 给每个类别打分
 * @param attributes 属性
 * @param retval 返回各个类别的得分
 */
void score(final List<Integer> attributes,
           List<Double> retval)

特征提取

不同于传统句法分析器的人工编写特征模板的作坊式风格,Chen and Manning将一些零散的原始特征直接作为输入传入神经网络模型中。至于它们是如何组合的,不再由人工编写的特征模板决定,而是由神经网络模型的隐藏层自动提取。

如上图所示,Chen and Manning使用了词、词性、依存关系名称等作为了原始特征(这里还有他们的独创做法,且听下文分解)。

具体是哪些词和词性以及依存关系呢?对转移系统的当前状态(亦即上下文)来讲,有如下值得注意的词语:

/**
 * 上下文
 * @author hankcs
 */
public class Context
{
    int S0, S1, S2, N0, N1, N2;             // 栈和队列的第i个元素
    int S0L, S0R, S0L2, S0R2, S0LL, S0RR;   // 该元素最左右的子节点、倒数第2个最左右的子节点
    int S1L, S1R, S1L2, S1R2, S1LL, S1RR;
}

NeuralNetworkParser#get_basic_features这个方法将它们的词和词性作为原始特征添加到了待分析的实例中。

词向量、词性向量、依存向量

词向量就是众所周知的word2vec训练出的向量,英文术语是embedding,我觉得直译成“词嵌入”有些过硬,还是通俗的词向量比较容易理解。

比较先进的句法分析器已经开始用词向量代替词本身来作为特征了,但Chen and Manning还有更绝的,他们把词性和依存关系也向量化了(To our best knowledge, this is the first attempt to introduce POS tag and arc label embeddings instead of discrete representations)。Chen and Manning说,既然词和词之间有相似的关系,那么词性、依存相互之间都应该有相似的关系。这些有相似度的向量导入模型后,对性能有所提升。

这些向量被存在一个矩阵中:

/**
 * 向量化(词向量、词性向量、依存关系向量)
 */
Matrix E;

加载完模型后确定向量的维度

embedding_size = E.rows();

以后每个特征(词、词性、依存关系)都直接去这个矩阵里取出一列作为列向量就完成了到向量的转化:

E.col(aid)

不过,在此之前,还有一个性能提升的措施。

词聚类

词聚类也是一种特征的抽象化,相似的词在句法树中的功能也应该是相似的。LTP的文档中指出利用了Guo et. al, (2015)中提出的词聚类特征,然而我并没有找到这篇论文。如果你有,恳请告诉我。

接下来我将通过代码反推词聚类大概的作用。

首先,聚类的第一个问题就是聚成几个类。在代码中,一共聚类了3次,每次的类的数目都不同,从少到多依次是:

/**
 * 将词映射到词聚类中的某个类
 */
Map<Integer, Integer> form_to_cluster4;
Map<Integer, Integer> form_to_cluster6;
Map<Integer, Integer> form_to_cluster;

另外,还保留了每个聚类中每个类别的String->ID映射:

/**
 * 少量类目数的聚类
 */
Alphabet cluster4_types_alphabet;
/**
 * 中等类目数的聚类
 */
Alphabet cluster6_types_alphabet;
/**
 * 大量类目数的聚类
 */
Alphabet cluster_types_alphabet;

接下来就会将词映射到类的ID:

/**
 * 获取词聚类特征
 * @param data 输入数据
 * @param cluster4
 * @param cluster6
 * @param cluster
 */
void get_cluster_from_dependency(final Dependency data,
                                 List<Integer> cluster4,
                                 List<Integer> cluster6,
                                 List<Integer> cluster)
{
    if (use_cluster)
    {
        int L = data.forms.size();
        for (int i = 0; i < L; ++i)
        {
            int form = data.forms.get(i);
            cluster4.add(i == 0 ?
                                 cluster4_types_alphabet.idOf(SpecialOption.ROOT) : form_to_cluster4.get(form));
            cluster6.add(i == 0 ?
                                 cluster6_types_alphabet.idOf(SpecialOption.ROOT) : form_to_cluster6.get(form));
            cluster.add(i == 0 ?
                                cluster_types_alphabet.idOf(SpecialOption.ROOT) : form_to_cluster.get(form));
        }
    }
}

上文的特征提取中的词,其实都指的是词的聚类的ID,这就是上文提到的性能提升的措施。

距离和配价特征

Zhang and Nivre (2011)在中指出,两个焦点词的距离和配价(Valency,指的是一个词有几个子节点)都有助于分类决策。在实现中,分别对应:

/**
 * 获取距离特征
 * @param ctx 当前特征
 * @param features 输出特征
 */
void get_distance_features(final Context ctx,
                           List<Integer> features)

/**
 * 获取(S0和S1的)配价特征
 * @param ctx 上下文
 * @param nr_left_children 左孩子数量列表
 * @param nr_right_children 右孩子数量列表
 * @param features 输出特征
 */
void get_valency_features(final Context ctx,
                          final List<Integer> nr_left_children,
                          final List<Integer> nr_right_children,
                          List<Integer> features)

立方激活函数

如同反向传播神经网络极简入门》中介绍的一样,常用的激活函数有tanh和Sigmoid这两种。不过在本句法分析器中,使用的是立方激活函数:

神经网络依存句法分析35.png

神经网络依存句法分析38.png

这个函数有什么好处呢?它可以将输入特征充分地组合起来,因为特征来自词、词性、依存这三个大类,假设分别为神经网络依存句法分析36.png,那么

神经网络依存句法分析37.png

立方展开后将ijk遍历相乘,组合在一起了。据Chen and Manning试验,该立方激活函数在性能上优于传统的激活函数:

神经网络依存句法分析39.png


在代码中,对应:

Matrix output = W2.times(new Matrix (hidden_layer.cube())); // 立方激活函数

知识共享许可协议 知识共享署名-非商业性使用-相同方式共享码农场 » 基于神经网络的高性能依存句法分析器

评论 26

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #9

    请问这个神经网路模型的训练代码在哪里。我找了好久没有找到。

    微微小冷3年前 (2021-08-30)回复
  2. #8

    图片显示不了

    hipanda5年前 (2019-04-24)回复
    • 切换到外网IP就可以了 应该是我的网络的问题

      hipanda5年前 (2019-04-24)回复
  3. #7

    您好 我看代码 好像没有神经网络的训练部分吧?请问训练是如何完成的?

    karis7年前 (2017-07-18)回复
    • 麻烦问一下,最后你找到训练部分了吗?

      leilei6年前 (2017-12-04)回复
  4. #6

    博主,请问
    # 依存句法分析
    print(HanLP.parseDependency("徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标。"))

    Traceback (most recent call last):
    File "C:/Users/admin/PycharmProjects/text1/test6_jufa1.py", line 32, in <module>
    print(HanLP.parseDependency("徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标。"))
    jpype._jexception.ExceptionInInitializerErrorPyRaisable: java.lang.ExceptionInInitializerError

    依存句法为什么会报这样的错误?

    Law7年前 (2016-12-30)回复
  5. #5

    博主您好,我想试下的s-LSTM方法,请问能否给下语料呢?多谢!

    Deep NLP8年前 (2016-04-27)回复
  6. #4

    请参考文末Reference

    hankcs8年前 (2016-01-03)回复
  7. #3

    您好,想在NLP中使用神经网络文本特征的输入格式应该怎么定义?

    柳若先8年前 (2015-12-30)回复
    • 请详细描述问题。

      hankcs8年前 (2015-12-30)回复
      • 想用DBN 从中文文本挖掘信息,有标注好的语料,如何把这些标注好的语料作为特征放到DBN中训练

        柳若先8年前 (2015-12-30)回复
        • 请参考对方的文档。

          hankcs8年前 (2015-12-31)回复
  8. #2

    博主好,最近在用你的库,其中“再说”跟“再讲”的词性一个是C,一个是V,这很奇怪,像这种情况,我们该怎么把“再说”的词性改过来?

    Sam8年前 (2015-12-22)回复
    • 请参考https://github.com/hankcs/HanLP#修改方法

      hankcs8年前 (2015-12-22)回复
      • “修改了后者之后必须同步删除前者的缓存”,删除缓存是什么意思啊?有一个文件还是有这样的接口可调用?

        Sam8年前 (2015-12-22)回复
        • 请通读文档至少一次!

          hankcs8年前 (2015-12-23)回复
    • 譬如这样的句子:在说什么&在讲什么,再说一遍&再讲一遍,句子结构、词性应该是一样的,为什么分析的结果会不一样?

      Sam8年前 (2015-12-22)回复
      • 词不一样,数据的稀疏性。

        hankcs8年前 (2015-12-22)回复
  9. #1

    博主您好,看过您的文章受益匪浅。我使用LTP时发现,LTP的“语义依存分析”似乎比“依存句法分析”更加适合应用于语义理解,请问您有“语义依存分析”的源码或者知道实现方法吗

    Terry8年前 (2015-12-01)回复
    • 你好,这是学术界新提出来的方法,最近还有一场赛事。在论文都没有公开发表的时候,没人知道详情。

      hankcs8年前 (2015-12-01)回复
      • 嗯,我会继续关注之,谢了

        Terry8年前 (2015-12-01)回复
      • 大神我也在关注这个,有最新消息分享一下,还有我发现你能找到好多论文的材料,请问你平时都是去哪里找的,阁下有空的话回答下我的问题。

        岩枭6年前 (2018-07-16)回复

我的作品

HanLP自然语言处理包《自然语言处理入门》