命名实体识别 订阅
命名实体识别(Named Entity Recognition,简称NER),又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等。 展开全文
命名实体识别(Named Entity Recognition,简称NER),又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等。
信息
别    称
专名识别
简    称
NER
中文名
命名实体识别
外文名
Named Entity Recognition
命名实体识别作用
命名实体识别是信息提取、问答系统、句法分析、机器翻译、面向Semantic Web的元数据标注等应用领域的重要基础工具,在自然语言处理技术走向实用化的过程中占有重要地位。一般来说,命名实体识别的任务就是识别出待处理文本中三大类(实体类、时间类和数字类)、七小类(人名、机构名、地名、时间、日期、货币和百分比)命名实体。
收起全文
精华内容
下载资源
问答
  • 命名实体识别

    2021-03-11 12:09:44
    命名实体识别(Named Entity Recognition,NER) 任务是识别文本中的人名、地名等专有名称,和有意义的时间、日期等数量短语等,并加以归类. 命名实体识别是信息抽取的核心任务, 分为两个子任务 判别实体边界 判别...

    概念

    命名实体识别(Named Entity Recognition,NER)
    任务是识别文本中的人名、地名等专有名称,和有意义的时间、日期等数量短语等,并加以归类.
    命名实体识别是信息抽取的核心任务, 分为两个子任务

    • 判别实体边界
    • 判别实体类型

    内容分类

    实体类 日期类 数值类
    人名 日期 货币
    地名 时间 百分比
    机构名

    (注意不包括普通名词, 比如飞机, 公司等泛指名词)

    指标

    (同检索的衡量指标)
    采用Precision / Recall / F-value加以衡量
    准确率 Precision = TP/(TP+FP)
    召回率 Recall = TP/(TP+FN)

    对数据集的切分
    • P/N:Positive or Negative,表示算法对样本的判断
    • T/F:True or False,表示算法判断的正确与否
    四种简写的含义:
    • TP:True Positive,样本为正例,且被判定为正,即真正
    • FN:False Negative,样本为正例,但错误地被判定为负,即假负
    • FP:False Positive,样本为负例,但错误地被判定为正,即假正
    • TN:True Negative,样本为负例,且被判定为负,即真负

    F值(F-measure),即准确率与召回率的加权调和平均数, 准确率 P, 召回率 R
    F=1α1P+(1α)1R=(β2+1)PRβ2P+RF = \frac{1}{\alpha \frac{1}{P} + (1 - \alpha) \frac{1}{R}} = \frac{(\beta^2 + 1)PR}{\beta^2P + R}

    α\alphaβ\beta是设定的参数, 通常取α\alpha = 0.5, β\beta = 1

    方法

    • 词典
    • 规则
    • 统计

    常用工具

    中文

    • NLPIR-ICTCLAS:http://ictclas.nlpir.org/nlpir/
    • HanLP:http://hanlp.linrunsoft.com/
    • NLTK:http://www.nltk.org/

    英文

    • Stanford NER https://nlp.stanford.edu/software/CRF-NER.shtml
    • MALLET http://mallet.cs.umass.edu/
    展开全文
  • 基于深度学习的命名实体识别与关系抽取

    万次阅读 多人点赞 2019-07-18 22:12:50
    基于深度学习的命名实体识别与关系抽取 摘要:构建知识图谱包含四个主要的步骤:数据获取、知识抽取、知识融合和知识加工。其中最主要的步骤是知识抽取。知识抽取包括三个要素:命名实体识别(NER)、实体关系抽取...

    基于深度学习的命名实体识别与关系抽取

    作者:王嘉宁 QQ:851019059 Email:lygwjn@126.com 个人网站:http://www.wjn1996.cn
    【备注:此博文初次编辑为2018年11月23日,最新编辑为2019年10月24日】
    在这里插入图片描述
    夏栀的博客——王嘉宁的个人网站 正式上线,欢迎访问关注:http://www.wjn1996.cn


    摘要:构建知识图谱包含四个主要的步骤:数据获取、知识抽取、知识融合和知识加工。其中最主要的步骤是知识抽取。知识抽取包括三个要素:命名实体识别(NER)实体关系抽取(RE)属性抽取。其中属性抽取可以使用python爬虫爬取百度百科、维基百科等网站,操作较为简单,因此命名实体识别(NER)和实体关系抽取(RE)是知识抽取中非常重要的部分,同时其作为自然语言处理(NLP)中最遇到的问题一直以来是科研的研究方向之一。
      本文将以深度学习的角度,对命名实体识别和关系抽取进行分析,在阅读本文之前,读者需要了解深度神经网络的基本原理、知识图谱的基本内容以及关于循环神经网络的模型。可参考本人编写的博文:(1)基于深度学习的知识图谱综述;(2)深度神经网络
      本文的主要结构如下,首先引入知识抽取的相关概念;其次对词向量(word2vec)做分析;然后详细讲解循环神经网络(RNN)、长短期记忆神经网络(LSTM)、门控神经单元模型(GRU);了解基于文本的卷积神经网络模型(Text-CNN);讲解隐马尔可夫模型(HMM)与条件随机场等图概率模型(CRF);详细分析如何使用这些模型实现命名实体识别与关系抽取,详细分析端到端模型(End-to-end/Joint);介绍注意力机制(Attention)及其NLP的应用;随后介绍知识抽取的应用与挑战,最后给出TensorFlow源码、推荐阅读以及总结。本文基本总结了整个基于深度学习的NER与RC的实现过程以及相关技术,篇幅会很长,请耐心阅读

    • 一、相关概念
    • 二、序列模型数据
    • 三、循环神经网络
    • 四、循环神经网络的缺陷
    • 五、长短期记忆神经网络(LSTM)与门控神经单元(GRU)
    • 六、长期依赖模型的优化
    • 七、概率图模型(PGM)
    • 八、运用Bi-LSTM和CRF实现命名实体识别
    • 九、卷积神经网络
    • 十、基于文本的卷积神经网络(Text-CNN)的关系抽取
    • 十一、基于依存关系模型的关系抽取
    • 十二、基于远程监督的关系抽取
    • 十三、注意力机制(Attention)
    • 十四、基于注意力机制的命名实体识别与关系抽取
    • 十五、三元组的存储——图形数据库
    • 十六、Tensorflow实现命名实体识别与关系抽取
    • 十七、推荐阅读书籍
    • 十八、项目实例1(面向智慧农业的知识图谱及其应用系统 · 上海 · 华东师范大学数据科学与工程学院)
    • 十九、项目实例2(博主的本科毕业设计&计算教育——智学AI·基于深度学习的学科知识图谱)
    • 二十、总结

    一、相关概念

      在传统的自然语言处理中,命名实体识别与关系抽取是两个独立的任务,命名实体识别任务是在一个句子中找出具有可描述意义的实体,而关系抽取则是对两个实体的关系进行抽取。命名实体识别是关系抽取的前提,关系抽取是建立在实体识别之后。

    1.1 实体与关系

      实体是指具有可描述意义的单词或短语,通常可以是人名、地名、组织机构名、产品名称,或者在某个领域内具有一定含义的内容,比如医学领域内疾病、药物、生物体名称,或者法律学涉及到的专有词汇等。实体是构建知识图谱的主要成员。
      关系是指不同实体之间的相互的联系。实体与实体之间并不是相互独立的,往往存在一定的关联。例如“马云”和“阿里巴巴”分别属于实体中的人名和机构名,而它们是具有一定关系的。
      在命名实体识别和关系抽取之后,需要对所产生的数据进行整合,三元组是能够描述整合后的最好方式。三元组是指(实体1,关系,实体2)组成的元组,在关系抽取任务中,对任意两个实体1和实体2进行关系抽取时,若两者具有关系,则它们可以构建成三元组。例如一句话“马云创办了阿里巴巴”,可以构建的三元组为(“马云”,“创办”,“阿里巴巴”)。

    1.2 标注问题

      监督学习中有三种问题,分别是分类问题、回归问题和标注问题。分类问题是指通过学习的模型预测新样本在有限类集合中对应的类别;回归问题是指通过学习的模型拟合训练样本,使得新样本可以预测出一个数值;标注问题则是根据输入的序列数据对其用预先设置的标签进行依次标注。
      本文的思想便是序列标注,通过输入的序列数据,选择相应的模型对样本进行训练,完成对样本的标注任务。
      常用的标注任务包括命名实体识别、词性标注、句法分析、分词、机器翻译等,解决序列标注问题用到的深度学习模型为循环神经网络。

    二、序列模型数据

      在深度神经网络一文对深度神经网络的分析中已经指出,传统的BP神经网络只能处理长度固定,样本之间相互独立的数据,而对于处理命名实体识别、关系抽取,以及词性标注、情感分类、语音识别、机器翻译等其他自然语言处理的问题中,文本类的数据均以句子为主,而一个句子是由多个单词组成,不同的句子长度不一致,因此对于模型来说,大多数是以单词为输入,而单个词往往没有特定的意义,只有多个词组合在一起才具有一定的含义。例如对于“马云”一词,单个词“马”可能表达的是动物,“云”一词可能表示的是天上飘得云彩,也可以表示“云计算”的云,而“马云”却表示人名。所以这一类数据之间是有关联的。我们对句子级别的数据称为序列模型数据
      对于文本类的序列模型数据,通常是不能直接作为模型的输入数据的,需要进行预处理。

    2.1 one-hot向量

      将句子中的单词转换为数字的一种方法是采用one-hot向量。例如训练集中有3000个不重复的单词,根据其在词汇表中的排序,可以依次为其编号,例如“a”编号为0,“book”的编号为359,“water”的编号2441。因此这3000个单词都有唯一的编号。
      为了能通过向量形式表达,one-hot向量是指除了下标为该单词编号所对应的的值为1以外其他都为0。例如一个集合只有一句话“马云在杭州创办了阿里巴巴”,其中只有11个不重复的词,分别编号0-10,则“马”字的one-hot向量为[1,0,0,0,0,0,0,0,0,0,0][1,0,0,0,0,0,0,0,0,0,0],“阿”字的one-hot向量是[0,0,0,0,0,0,0,0,1,0,0][0,0,0,0,0,0,0,0,1,0,0]
      one-hot向量能够很清楚得为每一个词进行“数值化表示”,将人理解的内容转换为计算机可以理解的内容。
      Ps:有关onehot向量的详解:OneHot编码知识点数据预处理:独热编码(One-Hot Encoding)

    2.2 词嵌入向量(word embeddings)

      one-hot向量虽然能够简单的表示一个词,但是却存在三个问题:
      (1)one-hot向量是稀疏向量,并不能存储相应的信息;
      (2)当语料库中包含的词汇很多时(上百万上千万),一个one-hot向量的维度将会很大,容易造成内存不足;
      (3)序列模型的数据需要能够体现出词语词之间的关联性,单纯的one-hot向量不能体现出关联性。例如对于词汇“good”和“well”都表示不错的意思,再某些程度上具有相似关联,而one-hot只是简单的编号,并未体现这层相似性。
    因此,为了解决one-hot带来的问题,引入词向量概念。
      词向量有许多种表达方式,传统的方法是统计该词附近的词出现的次数。基于深度学习的词向量则有word embeddings,其是通过谷歌提出的word2vec方法训练而来。
      word2vec方法是将高维度的one-hot向量进行降维,通常维度设置为128或者300,其通过神经网络模型进行训练。基于神经网络的词向量训练有两种模型,分别是CBOW和Skip-Gram模型,如下图

    图1
      (1)CBOW模型是将一个词所在的上下文中的词作为输入,而那个词本身作为输出。通常设置一个窗口,不断地在句子上滑动,每次滑动便以窗口中心的词作为输出,其他词作为输入。基于大量的语句进行模型训练,通过神经网络的梯度下降法进行调参。最终神经网络的权重矩阵即为所有词汇的word embeddings。
      (2)Skip-Gram模型与CBOW相反,其随机选择窗口内的一个词的one-hot向量作为输入,来预测其他所有词可能出现的概率,训练后的神经网络的权重矩阵即为所有词汇的word embeddings。
      word2vec对训练好的神经网络,需要通过遍历所有词汇表抽取出所有词汇的word embedding,常用的优化模型是哈弗曼树(Hierarchical Softmax)和负采样(Negative Sampling)。
      Ps:关于word2vec训练详细解读可参考:如果看了此文还不懂 Word2Vec,那是我太笨Word Embedding与Word2Vec;关于word2vec的模型优化可参考基于Hierarchical Softmax的模型概述基于Negative Sampling的模型概述

    三、循环神经网络

      循环神经网络是BP神经网络的一种改进,其可以完成对序列数据的训练。根据前面讲解内容,循环神经网络需要能够记住每个词之间的关联性,因为每个词可能会受到之前的词的影响。

    3.1 循环神经网络的结构

      循环神经网络与BP神经网络不同之处在于其隐含层神经元之间具有相互连接。循环神经网络是基于时间概念的模型,因此对于横向的连接每一个神经元代表一个时间点。模型如图所示:

    在这里插入图片描述
      对于时刻 tt 的输入为 xtx_t,其中 xt={xt1,xt2,...}x_t=\{x_{t1},x_{t2},...\} 即为一个word embedding,输入到中心圆圈(隐层状态神经元)的箭头表示一个神经网络,权重矩阵为 UU,偏向为 bb,对于时间 t1t-1 时刻,中心的圆圈已经存在一个值 st1s_{t-1} ,该圆圈与tt 时刻的圆圈的箭头也表示一个神经网络,其权重矩阵为 WW,偏向也为 bb 。循环神经网络的关键即为某一时刻 tt 的隐层状态神经元的值不仅取决于当前的输入,也取决于前一时刻的隐层状态,即为:
    st=f(Wst1+Uxt+b)s_{t}=f(Ws_{t-1}+Ux_t+b)

    其中f()f(·)表示激活函数。
      对于网络的输出部分,循环神经网络输出个数与输入数量一致,时刻 tt 的输出为:
    y^t=ot=softmax(Vst+c)\hat y_t = o_t=softmax(Vs_t+c)

      由于对于序列模型来说,数组的长度是不一致的,即循环神经网络的输入神经元长度是不确定的,因此各个神经网络采用了共享参数 WUVbcW,U,V,b和c
      循环神经网络的剖面图如下图,该图能够比较直观的认识循环神经网络的空间结构(图片为转载,可忽略里面的参数):

    在这里插入图片描述

    3.2 循环神经网络的训练

      循环神经网络的训练与传统的BP神经网络训练方法一样,分为前向传播和反向传播,前向传播的公式为:
    st=f(Wst1+Uxt+b)s_{t}=f(Ws_{t-1}+Ux_t+b)

    y^t=ot=softmax(Vst+c)\hat y_t = o_t=softmax(Vs_t+c)

    假设损失函数为 LL。反向传播采用BPTT(基于时间的反向梯度下降)算法计算各个参数的梯度。输出层的神经网络梯度下降与BP神经网络的一样。而对于隐含层状态神经元部分,由于其在前向传播过程中的值来自于两个方向,同时又流向另外两个方向,因此在反向传播过程中,梯度将来自当前时刻的输出和下一时刻的状态,同时梯度流向该时刻的输入和上一时刻的状态,如图(Et=otE_t=o_t):

    在这里插入图片描述
    因此,假设时间长度为 nn ,对于某一时刻 t(0<t<n)t(0<t<n),隐层状态神经元的梯度将是不断累加的。下面给出循环神经网络的参数 WUVbcW,U,V,b,c 的梯度:
    Lc=t(otc)TLot=tLot\frac{\partial L}{\partial c}=\sum_t (\frac{\partial o_t}{\partial c})^T\frac{\partial L}{\partial o_t}=\sum_t \frac{\partial L}{\partial o_t}

    Lb=t(stbt)TLst=tdiag(1(st)2)Lst\frac{\partial L}{\partial b}=\sum_t (\frac{\partial s_t}{\partial b_t})^T\frac{\partial L}{\partial s_t}=\sum_t diag(1-(s_t)^2)\frac{\partial L}{\partial s_t}

    LV=ti(Loti)TotiV=tLotstT\frac{\partial L}{\partial V}=\sum_t \sum_i (\frac{\partial L}{\partial o_{ti}})^T\frac{\partial o_{ti}}{\partial V}=\sum_t \frac{\partial L}{\partial o_t}s_t^T

    LW=t(Lsti)TstiWt=tdiag(1(st)2)Lstst1T\frac{\partial L}{\partial W}=\sum_t (\frac{\partial L}{\partial s_{ti}})^T\frac{\partial s_{ti}}{\partial W_t}=\sum_t diag(1-(s_t)^2)\frac{\partial L}{\partial s_t}s_{t-1}^T

    LU=t(Lsti)TstiUt=tdiag(1(st)2)LstxtT\frac{\partial L}{\partial U}=\sum_t (\frac{\partial L}{\partial s_{ti}})^T\frac{\partial s_{ti}}{\partial U_t}=\sum_t diag(1-(s_t)^2)\frac{\partial L}{\partial s_t}x_{t}^T

      由于循环神经网络的共享参数机制,使得网络可以用很少的参数来构建一个大的模型,同时减少了计算梯度的复杂度。
      Ps:关于BPTT算法详细推导,可参考博文:基于时间的反向传播算法BPTT(Backpropagation through time),书籍:人民邮电出版社的《深度学习》233页。

    3.3 其他类型的循环神经网络

      上述的循环神经网络结构是标准的模型,对于不同的应用会产生不同的模型。在基于循环神经网络的情感分类问题中,模型的输入是长度不一的句子,而模型的输出往往只有一个(是积极的还是消极的评论),因此这属于多对一模型,如图:

    在这里插入图片描述

    对于音乐的生成问题,输入的是一段声音,希望模型输出对于的音符,这种模型为一对多,如图:

    在这里插入图片描述

    对于基于循环神经网络的机器翻译问题,输入的是不定长的待翻译句子,而输出的也是不定长的新句子,即模型的输入与输出长度不一致,这一类属于多对多模型,通常也叫做编码解码器,如图:

    在这里插入图片描述

    3.4 双向循环神经网络

      在命名实体识别问题中,输入的数据是一个句子,往往一个句子中的词不仅受到前面词的影响,同时也可能受到后面的词的影响,例如句子“王小明是班级上学习最好的同学”中之所以可以识别出人名实体“王小明”,是因为其后的关键词“同学”,而该实体前面没有词可以提供识别的依据。普通的循环神经网络仅考虑了前一时刻的影响,却未考虑后一时刻的影响,因此需要引入了双向循环神经网络模型。
      双向循环神经网络的结构如下:

    在这里插入图片描述

      其由输入层、前向隐含层、后向隐含层和输出层组成。输入层输入序列数据,前向隐含层的状态流向是顺着时间,后向隐含层的状态流向是逆着时间的,因此前向隐含层可以记住当前时刻之前的信息,而后向隐含层可以记住当前时刻未来的信息。通过输出层将前向、后向隐含层的向量进行拼接或者求和。
      双向循环神经网络的前向传播式子为:
    ht=fF(UFxt+WFht1+bF)\overrightarrow{h_t}=f_F(U_Fx_t+W_F\overrightarrow{h_{t-1}}+b_F)

    ht=fB(UBxt+WBht1+bB)\overleftarrow{h_t}=f_B(U_Bx_t+W_B\overleftarrow{h_{t-1}}+b_B)

    ht=[ht,ht]ht=12(ht+ht)h_t=[\overleftarrow{h_t},\overrightarrow{h_t}] 或 h_t=\frac{1}{2}(\overleftarrow{h_t}+\overrightarrow{h_t})

    y^t=ot=softmax(Vht+c)\hat y_t=o_t=softmax(Vh_{t}+c)

      双向循环神经网络的梯度下降法仍然使用BPTT算法,对于前向和后向隐含层,梯度的流向是互不干扰的,因此可以用普通的循环神经网络梯度下降的方法对前向和后向分别计算。

    3.5 深度循环神经网络

      循环神经网络的结构还是属于浅层模型,可以观察其结构图,只有两层网络组成。为了提高模型的深度,一种深度循环神经网络被提出,其结构如图所示:

    深度循环神经网络

    可知每一层便是一个循环神经网络,而下一层的循环神经网络的输出作为上一层的输入,依次进行迭代而成。
      深度循环神经网络的应用范围较少,因为对于单层的模型已经可以实现对序列模型的编码,而深层次模型会造成一定的过拟合和梯度问题,因此很少被应用。

    四、循环神经网络的缺陷

      在命名实体识别任务中,通过训练循环神经网络模型,实体识别的精度往往可以达到60%-70%,同时以其共享参数的机制,构建一个较大的循环神经网络不需要像BP网络那样需要庞大的参数,因此模型的训练效率也大大提高。
      但是循环神经网络有两个致命的缺陷,在实验中经常遇到:
      (1)容易造成梯度爆炸或梯度消失问题。当序列长度为100时,第1个输入的梯度在传递100层之后,可能会导致指数上升或者指数下降。
      (2)对于较长的序列,模型无法能够长期记住当前的状态。例如对于句子“这本书,名字叫平凡的世界,白色封皮,有一个书签夹在里面,旁边放着一支笔,…,是我的”,若要提取出有效的信息“这本书是我的”,需要长久的记住“这本书”,直到遇到“是我的”为止,而对于省略号完全可以无限的长。因此普通的循环神经网络无法记住这么久。
      因此为了解决这两个问题,需要对循环神经网络做出改进,使得其能够长期记住某一状态。

    五、长短期记忆神经网络(LSTM)与门控神经单元(GRU)

      为了解决循环神经网络的缺陷,引入门控概念。通过设置门结构来选择性的决定是否记忆或遗忘。

    5.1 长短期记忆神经网络(LSTM)

      长短期记忆神经网络(LSTM)巧妙的运用的门控概念,实现了可以长期记忆一个状态。LSTM的模型结构与循环神经网络结构是一样的,如图:

    在这里插入图片描述

    不同的是隐含层的部分不是简单的求和。隐含层的部分又称为LSTM单元,如图:

    LSTM单元

    其主要由三个门控组成,分别是遗忘门、输入门和输出门,中间的Cell称为记忆细胞,用来存储当前的记忆状态。
    (1)遗忘门:遗忘门的作用是决定记忆细胞中丢弃什么信息。采用的是sigmod激活函数,其数据来源于当前的输入、上一时刻的隐层状态以及上一时刻的记忆细胞,前向传播的公式为:
    ft=σ(Wxfxt+Whfht1+Wcfct1+bf)f_t=\sigma(W_{xf}x_t+W_{hf}h_{t-1}+W_{cf}c_{t-1}+b_f)

    ftf_t 值取值为0或1,0表示完全丢弃,1表示完全保留。
    (2)输入门:输入门决定了需要新增的内容。采用的是sigmod激活函数,前向传播公式为:
    it=σ(Wxixt+Whiht1+Wcict1+bi)i_t=\sigma(W_{xi}x_t+W_{hi}h_{t-1}+W_{ci}c_{t-1}+b_i)

    iti_t 的值为0或1,0表示不添加当前的内容,1表示新增当前的内容,而待新增的内容取决于当前时刻的输入以及上一时刻的隐含状态(这与普通的循环神经网络是一致的),待新增内容记为 ztz_t
    zt=tanh(Wxcxt+Whcht1+bc)z_t=tanh(W_{xc}x_t+W_{hc}h_{t-1}+b_c)

      Ps:遗忘门与输入门的计算公式中输入的数据都是一模一样的,区别两者功能的便是相应的权重矩阵和偏向。
    (3)记忆细胞:记忆细胞内存储着已经记住的内容,当确定当前时刻是否保留过去的记忆(即 ftf_t 的取值)和是否记住新的内容(即 iti_t 的取值),于是更新记忆细胞,公式为:
    ct=ftct1+itztc_t=f_tc_{t-1}+i_tz_t

      更新细胞的公式可以这么理解:ct1c_{t-1} 表示 t1t-1 时刻LSTM模型记住的内容,当在 tt 时刻时将面临两个问题,是否继续记住之前(t1t-1 时刻)的内容?是否需要记住当前新的内容?因此将有四种情况:
      (1)当 ft=0f_t=0it=0i_t=0 时,ct=0c_t=0,即忘记过去全部内容且不记住新的内容;
      (2)当 ft=0f_t=0it=1i_t=1 时,ct=ztc_t=z_t ,即忘记过去全部内容,但记住新的内容;
      (3)当 ft=1f_t=1it=0i_t=0 时,ct=ct1c_t=c_{t-1},即保留之前的内容,对新的内容不予理睬;
      (4)当 ft=1f_t=1it=1i_t=1 时,ct=ct1+ztc_t=c_{t-1}+z_t,即既保留之前的内容,又记住新的内容。
      Ps:而在实际的实验中,因为sigmod函数并不是二值的(即其是在0-1之间的一个值),因此对于 ftf_titi_t 实际上是分别决定了保留记忆过去内容和选择记住新的内容的多少,例如 ft=1f_t=1 则表示保留全部过去内容,ft=0.5f_t=0.5 则表示忘记过去的一半内容,或表示为淡化过去的记忆。

    (4)输出门:输出门则是决定输出什么内容,即对于当前时刻 tt ,若 ot=0o_t=0 则表示不输出,若 ot=1o_t=1 则表示输出:
    ot=tanh(Wxoxt+Whoht1+Wcoct+bo)o_t=tanh(W_{xo}x_t+W_{ho}h_{t-1}+W_{co}c_t+b_o)

    待输出的内容则是:
    ht=ottanh(ct)h_t=o_ttanh(c_t)

    其中 tanh(ct)tanh(c_t) 是对当前时刻记忆细胞内记住的内容进行处理使其值范围在-1至1之间。
      因此总结LSTM模型的原理:在第 tt 时刻时,首先判断是否保留过去的记忆内容,其次判断是否需要新增内容,更新记忆细胞之后再判断是否需要将当前时刻的内容输出。
      LSTM的梯度下降法仍然使用BPTT算法(因为每个门其实就是一个神经网络),梯度下降法与循环神经网络的思路和原理是一样的,此处不再推导。

    5.2 双向长短期记忆神经网络(Bi-LSTM)

      与循环神经网络一样,单向的模型不能够记住未来时刻的内容,因此采用双向模型,双向模型如图所示:

    在这里插入图片描述

    Bi-LSTM的结构与Bi-RNN模型结构一样。同一时刻在隐含层设置两个记忆单元(LSTM unit),分别按照顺时间(前向)和逆时间(后向)顺序进行记忆,最后将该时刻两个方向的输出进行拼接,即:
    ht=[ht,ht]h_t=[\overleftarrow{h_t},\overrightarrow{h_t}]

      在诸多学术论文中,对命名实体识别最常用的便是Bi-LSTM模型,该模型实体识别的精度通常高达80%。

    5.3 门控神经单元(GRU)

      门控神经单元(GRU)是LSTM的一个变种,其简化了LSTM结构,其只有重置门和更新门两个门结构,GRU单元结构如图所示:

    GRU单元

      (1)重置门:决定是否重置当前的记忆, rtr_t 的取值为0或1,0表示重置,1表示不重置:
    rt=σ(Wr[ht1,xt]+br)r_t=σ(Wr[h_{t−1},x_t]+b_r)

      (2)更新门:决定是否更新当前的心内容,待更新的内容为:
    h^t=tanh(Wh[rtht1,xt]+bh)\hat h_t=tanh⁡(W_h[r_th_{t−1},x_t]+b_h)

    更新门为 ztz_t ,取值为0或1:
    zt=σ(Wz[ht1,xt]+bz)z_t=σ(Wz[h_{t−1},x_t]+b_z)

    于是有:
    ht=(1zt)ht1+zth^th_t=(1−z_t)h_{t−1}+z_t\hat h_t

    可知当 zt=0z_t=0ht=ht1h_t=h_{t-1} ,即保留过去的内容,即不更新;当 zt=1z_t=1ht=h^th_t=\hat h_t 即更新当前时刻的内容。
      (3)模型输出:
    yt=σ(Woht+by)y_t=σ(W_oh_t+b_y)

      GRU相比LSTM只有两个门结构,运算方面更加快,同时也能实现长期记忆。例如对于句子“The cat,which already ate …,was full.”,需要模型能够正确表示为“was”而不是“were”,当处于单词“cat”时刻时,设置重置门为1,更新门为1,此时模型 hth_t 为“cat”(单数),然后经过“which”、“already”…,均设置重置门和更新门为0,此时 hth_t 的值始终是 “cat”(单数),直到遇到“was”为止。
      GRU的训练仍然为BPTT算法实现梯度下降法调参。
      Ps:GRU详解参考:GRU神经网络门控循环单元(GRU)的基本概念与原理

    六、长期依赖模型的优化

      在模型的训练过程中,容易出现梯度爆炸或梯度衰减的问题,而对于LSTM这种高维度非线性模型,容易造成不同大小梯度之间的骤降,即参数在很小的变化范围内代价函数的梯度呈现指数级别的“爆炸”。假设对于代价函数 J(W,b)J(W,b) 的梯度图如图所示:

    在这里插入图片描述

    对于含有陡峭悬崖的梯度模型,需要进行梯度截断(Gradient Clipping)。其中红色的点为最优值的位置,如果使用梯度截断法(右图),则可以使梯度在接近悬崖时降低步伐(学习率衰减),如果不使用梯度截断(左图),则可能由于过大的学习率使当前的参数被“抛出”曲面。
      截断梯度法有两种策略,一种是在参数更新前,逐元素地截断小批量产生的参数梯度;另一种策略是在参数更新前截断梯度 gg 的范数 g||g||gg 表示待更新的参数,例如权重矩阵 WW 和偏向 bb)。设 vv 是范数的上界,则梯度截断的参数更新表示为
    g:=gvgg:=\frac{gv}{||g||}

    其中 g>v||g||>v
      梯度截断法很好的解决了梯度爆炸问题,但对于梯度衰减问题则无济于事。对于LSTM模型,梯度衰减容易导致记忆内容的丢失,因此为了能够捕获长期依赖,需要优化模型的结构:
    (1)修改门结构;
    (2)修改损失函数的正则项:引导信息流的正则化

    七、概率图模型(PGM)

      在基于深度学习的命名实体识别中,使用RNN、LSTM或GRU模型是对序列数据的一种编码(encoding),虽然RNN、LSTM和GRU的输出数据也表示对实体标注的预测,但往往会出现错误。例如通常在命名实体识别中使用“BIES”表示实体词中每个单词的相对位置,其中“B”表示位于实体词的第一个位置,“I”表示位于实体词的中间位置,“E”表示位于实体词的最后一个位置,“S”表示该实体词只有一个单词(例如实体“华东师范大学”的标注序列为“BIIIIE”)。而对于RNN、LSTM的输出只会单纯的输出其是否是实体,而并未考虑相对位置,即模型可能对“华东师范大学”实体输出“BBBBBB”,显然这是个错误的。因此需要引入解码器(decoding)。
      常用的解码器可以是LSTM进行解码,在部分论文中也使用LSTM进行解码,但常用的解码器是基于概率图模型的隐马尔可夫模型和条件随机场模型。

    7.1 概率图模型概念

      概率图模型是通过图结构直观的表现出各个随机变量之间的依赖关系。图结构如图所示:

    图结构

      图 G(V,E)G(V,E) 中有 nn 个结点 V={v1,v2,...,vn}V=\{v_1,v_2,...,v_n\} 分别表示各个随机变量, X={Xv1,Xv2,...Xvn}X=\{X_{v_1},X_{v_2},...X_{v_n}\} ;边 E={e1,e2,...,em}E=\{e_1,e_2,...,e_m\} 则表示其相连的两个结点表示的随机变量的依赖关系。概率图模型分为两种:有向概率图模型和无向概率图模型。有向概率图包括贝叶斯网络,隐马尔可夫模型,无向图包括条件随机场等。

    7.2 贝叶斯网络

      贝叶斯网络又称信念网络或因果网络,其属于有向无环图,例如对于结点 v1v1v2v2 分别表示随机变量 X1X_1X2X_2 ,则随机变量 X2X_2 的概率为 p(X2X1)p(X_2|X_1) ,即变量 X1X_1X2X_2 的因。对于没有边相连的结点表示的随机变量,则两者是相互独立的。
     &emsp在概率论中,贝叶斯定理表示的条件概率与各个变量之间的关系,即:
    p(AB)=p(A)p(BA)p(B)p(A|B)=\frac{p(A)p(B|A)}{p(B)}

      假设图 G(V,E)G(V,E) 中有5个结点 V={v1,v2,v3v4v5}V=\{v_1,v_2,v_3,v_4,v_5\} 分别表示随机变量 X={Xv1,Xv2,Xv3,Xv4,Xv5}X=\{X_{v_1},X_{v_2},X_{v_3},X_{v_4},X_{v_5}\} ,有向边如图所示:

    在这里插入图片描述

    因此可知变量 X1X_1 是所有变量的因,各个变量的条件概率为:
    p(X1)=p1p(X_1)=p_1

    p(X1,X2)=p(X2X1)p(X1)=p21p1p(X_1,X_2)=p(X_2|X_1)p(X_1)=p_{21}·p_1

    p(X1,X3)=p(X3X1)p(X1)=p31p1p(X_1,X_3)=p(X_3|X_1)p(X_1)=p_{31}·p_1

    p(X1,X2,X3,X4)=p(X1,X3,X4)p(X1,X2,X4)p(X_1,X_2,X_3,X_4)=p(X_1,X_3,X_4)p(X_1,X_2,X_4)

    =p(X4X1,X3)p(X1,X3)p(X4X1,X2)p(X1,X2)=p413p31p412p21p12=p(X_4|X_1,X_3)p(X_1,X_3)·p(X_4|X_1,X_2)p(X_1,X_2)=p_{413}p_{31}·p_{412}p_{21}·p{_1}{^2}

    p(X1,X3,X5)=p(x5x1,x3)p(x1,x3)=p513p31p1p(X_1,X_3,X_5)=p(x_5|x_1,x_3)p(x_1,x_3)=p_{513}p_{31}p_1

    因此有:
    p(X1,X2,X3,X4,X5)=p(X1,X2,X3,X4)p(X1,X3,X5)=p513p413p412p21p312p12p(X_1,X_2,X_3,X_4,X_5)=p(X_1,X_2,X_3,X_4)·p(X_1,X_3,X_5)=p_{513}p_{413}p_{412}p_{21}p{_{31}}{^2}p{_{1}}{^2}

    7.2 隐马尔可夫模型(HMM)

      隐马尔可夫模型是一种特殊的贝叶斯网络,各个随机变量之间的依赖关系并不像图结构那样错综复杂,而是单纯的一条链式结构,因此称为隐马尔可夫链,如图所示:

    隐马尔可夫链

      S={S0,S1,...,Sn}S=\{S_0,S_1,...,S_n\} 表示隐含状态序列,其中 StS_t 表示第 tt 时刻的某一状态。隐马尔可夫模型的假设指出,tt 时刻状态仅受 t1t-1 时刻影响,与其他无关(即 p(StS0,S1,..,St1)=p(StSt1)p(S_t|S_0,S_1,..,S_{t-1})=p(S_t|S_{t-1})),因此 t1t-1 时刻与 tt 时刻之间存在一个有向边,表示状态的转移,用矩阵 AA 表示。每一个时刻的状态都将对应一个输出,且该时刻的输出仅与当前时刻的隐含状态有关,输出的值即为观测值,输出矩阵为 BB
      隐马尔可夫模型所表达的含义即多个随机变量含有隐含状态(内因)以及它们对应的外在表现(观测序列)。隐马尔可夫模型解决的问题主要包括评估问题、解码问题和学习问题。
      在命名实体识别中,主要应用的是解码问题。关于解码问题主要指根据已知的观测序列,推测最有可能的隐状态序列。即已知 O={O1,O2,...,On}O=\{O_1,O_2,...,O_n\}来推测 SS,通过初始化隐状态的转移概率矩阵以及各个观测值为某个状态的概率,可构建起若干条状态路径,每一条路径对应一个评分值,因此通过选择最大评分值对应的路径即为预测的隐含状态序列。在解决若干条路径的最值问题,可以参考最短路径算法或者动态规划算法来解决。
      Ps:隐马尔可夫模型的原理以及通过例子来推导解码过程可参考:HMM超详细讲解+代码隐马尔可夫模型

    7.3 条件随机场模型(CRF)

      条件随机场(CRF)是给定一组输入序列条件下另一组输出序列的条件概率分布模型,在自然语言处理中得到了广泛应用。对于命名实体识别这一类的序列标注问题,通常采用的是线性条件随机场(linear-CRF),模型如图所示:

    线性条件随机场

      线性条件随机场仍满足隐马尔可夫模型(HMM),在HMM的基础上引入特征函数 。特征函数分别为sls_ltkt_k
      (1)sl(yi,x,i)(l=1,2,...L)s_l(y_i,x,i) (l=1,2,...L)表示当前的状态特征,其只与当前状态有关,其中 LL 是定义在该节点的节点特征函数的总个数, xx 表示观测序列, ii 是当前节点在序列的位置。
      (2)tk(yi1,yi,x,i)t_k(y_{i-1},y_i,x,i) 表示当前时刻的转移特征,即由 k1k-1 时刻转移至 kk 时刻的特征函数,因此当前的状态特征与之前时刻的状态有关。
    linear-CRF的参数化形式如下:

    P(yx)=1Z(x)exp(i,kλktk(yi1,yi,x,i)+i,lμlsl(yi,x,i))P(y|x) = \frac{1}{Z(x)}exp\Big(\sum\limits_{i,k}\lambda_kt_k(y_{i-1},y_i, x,i) +\sum\limits_{i,l}\mu_ls_l(y_i, x,i)\Big)

    其中 λk\lambda_kμl\mu_l 为权重系数, Z(x)Z(x) 为归一化因子。
      线性条件随机场还有其他表示形式,包括参数化形式(即上面的表达式),也有矩阵形式,具体可参考李航的《统计学习方法》194-199页。
      条件随机场解决的问题也为三个:评估问题、解码问题和学习问题。在命名实体识别、词性标注等序列标注问题上,普遍运用CRF实现解码,并通过Viterbi算法求得最大概率的序列。

    7.4 概率图模型解决命名实体识别

      通过运用HMM和CRF模型实现命名实体识别,需要首先初始化相应的参数,例如对于HMM模型,需要初始化转移状态矩阵 AA、观测序列与状态的关联(混淆矩阵) BB ,以及初始化状态概率(即第一个时刻为某一状态的概率);对于CRF模型,需要初始化两个特征函数以及其对应的系数。其次通过观测序列和状态序列对这些参数进行训练,训练即属于三个问题中的学习问题。最后通过已学习的模型,通过训练集样本进行解码测试
      现如今非常常用的模型是Bi-LSTM+CRF,即应用Bi-LSTM实现对序列(一个句子)进行编码(encoding),使得该编码保存了整个语句的前后关系,其次将Bi-LSTM的输出通过CRF进行解码,已获取最为可能的序列标注。

    八、运用Bi-LSTM和CRF实现命名实体识别

      前面讲解了双向长短期记忆神经网络以及条件随机场概率图模型,本节将运用Bi-LSTM与CRF来实现命名实体抽取。

    8.1 数据获取与处理

      数据集可以采用自定义标注的数据,但这一类数据往往会存在很多缺陷,因此,在实验中通常使用公开训练集。
      常用的公开训练集有ACE04,ACE05,可以用来完成词性标注(命名实体识别便属于一种词性标注问题)。训练集中包含成千上万个完整的句子,主要以英文句子为主。对于词性标注问题,还将对于一个标注序列。数据通常是以JSON格式保存,在读取数据时需要进行JSON解析。
      获取数据后,该数据不能直接作为计算机的输入,需要转化为词向量。词向量可以用自己的语料库使用神经网络(CBOW或Skip-Gram模型)进行训练。实验常用谷歌训练好的词向量,其包含了上千万个语料库,相比自己训练的更加完善。
      下面以一句话“马云在杭州创办了阿里巴巴”为例,分析Bi-LSTM+CRF实现命名实体识别的训练与预测过程。实体给定范围的JSON表示为:
    {
      ‘o’:0,
      ‘B-PER’:1,
      ‘I-PER’:2,
      ‘B-LOC’:3,
      ‘I-LOC’:4,
      ‘B-ORG’:5,
      ‘I-ORG’:6
    }
    该句子每个字对于的语料库(假设共3000字)中的编号假设为:
    {
      ‘马’ : 1683,
      ‘云’ : 2633,
      ‘在’ : 2706,
      ‘杭’ : 941,
      ‘州’ : 2830,
      ‘创’ : 550,
      ‘办’ : 236,
      ‘了’ : 1436,
      ‘阿’ : 1,
      ‘里’ : 1557,
      ‘巴’ : 213,
    }
    因此该句子的one-hot向量应该为:{1683,2633,2706,941,2830,550,236,1436,1,1557,213}。其次将该one-hot向量与词嵌入矩阵word embeddings相乘,得到该句子每个字对于的词向量,因此该句子将得到一个句子向量,用 xx 表示,假设word embedding对每个词的维度为300(通常实验都设定为300),则 xx 的长度也为300。
      Ps:通常在训练数据集时,假设一个数据集中有1000个句子,通常采用的是mini-batch法进行训练,即将1000个句子分为若干组(假设分为10组),则每组将平均随机分到batch_size个句子(即每组100个句子),其次将这一组内的句子进行合并。因为每个句子长度不一致,所以取最长的句子为矩阵的列数,其他句子多余的部分则填充0。

    8.2 LSTM单元编码

      获取该句子的向量后,便将其放入LSTM的的输入层(论文中也多称为input layer或者embedding layer),每个输入神经元对应一个字的词向量,正向传播则从第一个字“马”开始,随着时间推移一直到“巴”。
      每个时刻 tt 对于的字 xtx_t 通过前向传播和后向传播并拼接得到 hth_t,其次得到 y^t\hat y_t,该值即为当前时刻 tt 对应的7个标签中每个标签预测的概率。例如对于“马”字,y^t=[0.031,0.305,0.219,0.015,0.129,0.133,0.168]\hat y_t=[0.031,0.305,0.219,0.015,0.129,0.133,0.168] ,最大的值为0.305,对应于下标1,即标签“B-PER”。

    8.3 CRF解码

      在CRF中要解决的问题之一是解码问题,对于 y^t\hat y_t 的结果不一定完全符合输出规则,因此需要将其按照输出规则进行解码。输出规则则体现在CRF中的超参数和参数。例如对于 t=5t=5 时刻,字为“州”,对应的 y^t=[0.085,0.113,0.153,0.220,0.207,0.108,0.114]\hat y_t=[0.085,0.113,0.153,0.220,0.207,0.108,0.114],可知最大的值对应下标表示的标签为“B-LOC”,虽然成功的预测了其属于地区这一类实体,但很显然应该是“I-LOC”。因此将该输出概率向量做下列计算:
    P(yty^t)=1Z(x)exp(λktk(y^t1,yt,x,t)+μlsl(yt,x,t))P(y_t|\hat y_t) = \frac{1}{Z(x)}exp\Big(\lambda_kt_k(\hat y_{t-1},y_t, x,t) +\mu_ls_l(y_t, x,t)\Big)

    然后对其他词按照该式子进行计算,通过维特比算法求出最大值,即对应的序列中,“州”字的概率向量可能变为:y^t=[0.085,0.113,0.153,0.207,0.220,0.108,0.114]\hat y_t=[0.085,0.113,0.153,0.207,0.220,0.108,0.114]
      应用Bi-LSTM和CRF模型的命名实体识别在论文《Bidirectional LSTM-CRF Models for Sequence Tagging》中被提出,可参考该论文,点击一键下载

    九、卷积神经网络

      卷积神经网络是神经网络的另一个演化体,其通常用于图像处理、视频处理中,在自然语言处理范围内,常被用来进行文本挖掘、情感分类中。因此基于文本类的卷积神经网络也广泛的应用在关系抽取任务中。

    9.1 卷积运算

      卷积神经网络主要通过卷积运算来实现对多维数据的处理,例如对一副图像数据,其像素为6x6,通过设计一个卷积核(或称过滤器)filter来对该图形数据进行扫描,卷积核可以实现对数据的过滤,例如下面例子中的卷积核可以过滤出图像中的垂直边缘,也称为垂直边缘检测器。
      例如假设矩阵:
    [301274158931272513013178421628245239] \begin{bmatrix} 3 & 0 & 1 & 2 & 7 & 4 \\ 1 & 5 & 8 & 9 & 3 & 1 \\ 2 & 7 & 2 & 5 & 1 & 3 \\ 0 & 1 & 3 & 1 & 7 & 8 \\ 4 & 2 & 1 & 6 & 2 & 8 \\ 2 & 4 & 5 & 2 & 3 & 9 \\ \end{bmatrix}

    表示一个原始图像类数据,选择卷积核(垂直边缘检测器):
    [101101101] \begin{bmatrix} 1 & 0 & -1 \\ 1 & 0 & -1 \\ 1 & 0 & -1 \\ \end{bmatrix}
    然后从原始图像左上方开始,一次向右、向下进行扫描,扫描的窗口为该卷积核,每次扫描时,被扫描的9个数字分别与卷积核对应的数字做乘积(element-wise),并求这9个数字的和。例如扫描的第一个窗口应该为:
    31+11+21+00+50+70+1(1)+8(1)+2(1)=53*1+1*1+2*1+0*0+5*0+7*0+1*(-1)+8*(-1)+2*(-1)=-5

    则最后生成新的矩阵:
    [540810223024732316] \begin{bmatrix} -5 & -4 & 0 & 8 \\ 10 & -2 & 2 & 3 \\ 0 & -2 & -4 & -7 \\ -3 & -2 & -3 & -16 \\ \end{bmatrix}

      以上的事例称为矩阵的卷积运算。在卷积神经网络中,卷积运算包括如下几个参数:
    (1)数据维度:即对当前需要做卷积运算的矩阵的维度,通常为三维矩阵,维度为 mndm*n*d
    (2)卷积核维度:即卷积核的维度,记为 ffdf*f*d ,当d为1时,为二维卷积核,通常对二维矩阵进行卷积运算,当 d>1d>1 时为三维卷积核,对图像类型数据进行卷积运算。
    (3)卷积步长stride:即卷积核所在窗口在输入数据上每次滑动的步数。上面的事例中明显步长为1。
    (4)数据填充padding:在卷积操作中可以发现新生产的矩阵维度比原始矩阵维度变小,在一些卷积神经网络运算中,为了保证数据维度不变,设置padding=1,对原始矩阵扩充0。这种方式可以使得边缘和角落的元素可以被多次卷积运算,也可以保证新生成的矩阵维度不变。可以计算出每个维度应该向外扩充 f1f-1d1d-1

    9.2 卷积神经网络的结构

      卷积神经网络的基本结构如图所示:

    卷积神经网络结构

    其主要有输入层、卷积层、池化层、全相连接层和输出层组成,其中卷积层与池化层通常组合在一起,并在一个模型中循环多次出现。
    (1)输入层:输入层主要是将数据输入到模型中,数据通常可以是图像数据(像素宽*像素高*三原色(3)),也可以是经过处理后的多维数组矩阵。
    (2)卷积层:卷积层主要是对当前的输入数据(矩阵)进行卷积运算,9.1节简单介绍了卷积运算,下面对卷积运算进行符号化表示:
      设当前的输入数据为图像 G(GRmnd)G(G\in\mathbb R^{m*n*d}) ,其中 mm 表示 GG 的宽, nn 表示 GG 的高, dd 表示 GG 的层数。
      设卷积神经网络有2次卷积和池化操作(如上图),两次卷积操作分别有 c(s)(s=1,2)c^{(s)}(s=1,2) 个卷积核 C(s,t)(t[1,c(s)])C^{(s,t)}(t\in\mathbb [1,c^{(s)}])(每个卷积核各不相同,不同的卷积核可以对原始图像数据进行不同方面的特征提取),每个卷积核的维度设为 fsfsdf_s*f_s*d (卷积核维度一般取奇数个,使得卷积核可以以正中间的元素位置中心对称,卷积核的层数需要与上一轮输出的数据层数一致),因此对于第 ss次中的第 tt 个卷积核的卷积操作即为:
    Gpq(s,t)=k=1di=pp+fsj=qq+fsCijk(s,t)Ppq(s1,t)G_{pq}^{(s,t)}=\sum_{k=1}^{d}\sum_{i=p}^{p+f_s}\sum_{j=q}^{q+f_s}C^{(s,t)}_{ijk}*P_{pq}^{(s-1,t)}

    其中 pqpq 表示卷积后的矩阵的第p行第q列, Ppq(s1,t)P_{pq}^{(s-1,t)} 表示第 s1s-1 次卷积池化操作后的池化层值,P(0,1)P^{(0,1)} 即为原始图像数据 GG
      卷积运算后,将生成 c(s)c^{(s)} 个维度为 m+2paddingfsstriden+2paddingfsstride\lfloor \frac{m+2*padding-f_s}{stride}*\frac{n+2*padding-f_s}{stride} \rfloor 的新矩阵
      Ps:公式是三层循环求和,在程序设计中可以使用矩阵的对应位置求积。
    (3)池化层(polling):池化层作用是为了降低卷积运算后产生的数据维度,池化操作包括最大池化(max-polling)和平均池化(avg-polling)。
      池化操作是一种特殊的卷积,其并不像卷积操作一样逐个相乘,对于最大池化,是取当前所在窗口所在的数据中最大的数据;对于平均池化则是取当前窗口所有值的平均值。池化层的窗口维度一般为2*2,窗口的滑动步长stride=2。例如对于9.1节中经过卷积操作的矩阵的池化操作后应为:
    [540810223024732316]maxpolling[10803] \begin{bmatrix} -5 & -4 & 0 & 8 \\ 10 & -2 & 2 & 3 \\ 0 & -2 & -4 & -7 \\ -3 & -2 & -3 & -16 \\ \end{bmatrix} \stackrel{max-polling}{\longrightarrow} \begin{bmatrix} 10 & 8 \\ 0 & -3 \\ \end{bmatrix}

    [540810223024732316]avgpolling[0.253.251.757.5] \begin{bmatrix} -5 & -4 & 0 & 8 \\ 10 & -2 & 2 & 3 \\ 0 & -2 & -4 & -7 \\ -3 & -2 & -3 & -16 \\ \end{bmatrix} \stackrel{avg-polling}{\longrightarrow} \begin{bmatrix} -0.25 & 3.25 \\ -1.75 & -7.5 \\ \end{bmatrix}

      池化层可以通过提取出相对重要的特征来减少数据的维度(即减少数据量),即减少了后期的运算,同时可以防止过拟合。第 ss 次卷积池化操作中,对第 tt 个卷积核过滤生成的矩阵进行最大或平均池化操作的符号表示如下:
    Pij(s,t)=max(Gij(s,t),Gij+1(s,t),Gi+1j(s,t)Gi+1j+1(s,t))P_{ij}^{(s,t)}=max(G_{ij}^{(s,t)},G_{ij+1}^{(s,t)},G_{i+1j}^{(s,t)},G_{i+1j+1}^{(s,t)})

    Pij(s,t)=avg(Gij(s,t),Gij+1(s,t),Gi+1j(s,t)Gi+1j+1(s,t))P_{ij}^{(s,t)}=avg(G_{ij}^{(s,t)},G_{ij+1}^{(s,t)},G_{i+1j}^{(s,t)},G_{i+1j+1}^{(s,t)})

      池化操作后矩阵的数量不变,仅仅是维度变小了。
    (4)全相连层:在多次卷积和池化运算后,将生成若干个小矩阵,通过concatenate操作,将其转换为一维度的向量。例如有16个10*10的矩阵,其可以拼接成一个长度为400的向量。全相连层是通过构建一个多层的BP神经网络结构,通过每一层的权重矩阵和偏向的前向传播,将其不断的降维。全相连层的输入神经元个数即为拼接形成向量的个数,隐含层个数及每层的神经元数量可自定义,输出层的神经元个数通常为待分类的个数。
    (5)softmax层:与传统的神经网络和循环神经网络类似,最终的全相连层的输出是每个类别的概率值,因此需要对其进行softmax操作,则最大值所对应的的类记为卷积神经网络的预测结果。

    9.3 卷积神经网络的训练

      卷积神经网络的训练与BP神经网络一样,选择损失函数 LL 刻画模型的误差程度,选择最优化模型(梯度下降法)进行最小化损失函数。
      卷积神经网络的超参数包括卷积池化的次数 ss、每次卷积池化操作中卷积核的个数 c(s)c^{(s)}和其维度 fsf_s 、卷积操作中是否填充(padding取值0或1)、卷积操作中的步长stride、池化层的窗口大小(一般取2)及池化类型(一般取max-polling)、全相连的层数及隐含层神经元个数、激活函数和学习率 α\alpha 等。参数包括卷积层的每个卷积核的值、全相连层中权重矩阵和偏向。卷积神经网络即通过梯度下降法不断对参数进行调整,以最小化损失函数。
      卷积神经网络的训练过程分为前向传播和反向传播,前向传播时需要初始化相关的参数和超参数,反向传播则使用梯度下降法调参。梯度下降的优化中也采用mini-batch法、采用Adam梯度下降策略试图加速训练,添加正则项防止过拟合。

    十、基于文本的卷积神经网络(Text-CNN)的关系抽取

      卷积神经网络已经成功地广泛应用与图像识别领域,而对于文本类数据近期通过论文形式被提出。在2014年,纽约大学的Yoon Kim发表的《Convolutional Neural Networks for Sentence Classification》一文开辟了CNN的另一个可应用的领域——自然语言处理。
      Yoon提出的Text-CNN与第九节的卷积神经网络有一定区别,但设计思想是一样的,都是通过设计一个窗口并与数据进行计算。

    10.1 数据处理

      基于文本的Text-CNN的输入数据是预训练的词向量word embeddings,词向量可参考本文的第2节内容。对于一句话中每个单词均为 kk 维的词向量,因此对于长度为 nn 的一句话则可用维度为 nkn*k 的矩阵 xx 表示,如图所示:


    concatenate

    10.2 Text-CNN的结构

    (1)卷积层: 对某一句话对应的预训练的词向量矩阵维度为 nkn*k ,设计一个过滤器窗口 WW ,其维度为 hkh*k,其中 kk 即为词向量的长度,hh 表示窗口所含的单词个数(Yoon 在实验中设置了 hh 的取值为2、3、4)。其次不断地滑动该窗口,每次滑动一个位置时,完成如下的计算:
    ci=f(Wxi:i+h1+b)c_i=f(W·x_{i:i+h-1}+b)

    其中 ff 为非线性激活函数,x_{i:i+h-1}表示该句子中第 iii+h1i+h-1 的单词组成的词向量矩阵,W·x_{i:i+h-1}表示两个矩阵的对应位乘积,cic_i 表示当前窗口位置的取值。
      因此对于长度为 nn 的句子,维度为 hkh*k 的过滤器窗口将可以产生 nh+1n-h+1 个值组成的集合:
    c={c1,c2,...cnh+1}c=\{c_1,c_2,...c_{n-h+1}\}

    (2)最大池化层:为了能够提取出其中最大的特征,Yoon对其进行max-over-time操作,即取出集合 cc 中的最大值 c^=maxc\hat c=max{c}。另外可以分析得到max-over-time操作还可以解决每句话长度不一致的问题。

      Text-CNN的结构如图所示:

    在这里插入图片描述

    (3)全相连层:对于 mm 个过滤器窗口,将产生 mm 个值组成的向量 z=[c^1,c^2,...,c^m]z=[\hat c_1,\hat c_2,...,\hat c_m],Text-CNN通过设置一个全相连层,将该向量映射为长度为 ll 的向量, ll 即为待预测的类的个数,设置softmax激活函数即可转换为各个类的概率值。

    10.3 Text-CNN的训练

      Text-CNN的前向传播即为上图所示流程。反向传播采用梯度下降法
    (1)正则化防止过拟合
      对于 mm 个过滤器窗口产生的向量 z=[c^1,c^2,...,c^m]z=[\hat c_1,\hat c_2,...,\hat c_m] ,则输出值为 y=wz+by=w·z+b,为了防止过拟合,采用 l2l_2 dropout权重衰减法,表达式为:
    y=w(zr)+by=w·(z\circ r)+b

    其中 \circ 表示对应位置相乘, rr 表示以概率 pp 产生只含有0或1元素的矩阵, p=0.5p=0.5 则表示可能矩阵 rr 中有一半的元素为1 。
      在测试环境,则为了限制 l2l_2 范式的权重矩阵,设置 w2=s||w||_2=s ,当w2>s||w||_2>s 时进行梯度下降的调整。
      Ps:l2l_2正则化权重衰减可参考:正则化方法:L1和L2 regularization、数据集扩增、dropout
    (2)超参数设置:Yoon设置了一系列超参数如下:

    超参数 取值
    window(hh值) 3/4/5
    dropout(pp值) 0.5
    l2l_2ss值) 3
    mini-batch 50

    10.4 Text-CNN应用于关系抽取

      Text-CNN在句子分类方面有不错的效果,而对于关系抽取问题,可以将其视为句子分类任务。
      假设长度为 nn 具有 mm 个实体的句子 x={x1,x2,...,xn}x=\{x_1,x_2,...,x_n\} ,其中实体分别为 e(i)={xt1(i),xt2(i),...,xtki(i)}e^{(i)}=\{x_{t_1}^{(i)},x_{t_2}^{(i)},...,x_{t_{k_i}}^{(i)}\},其中i,tki[2,m]i,t_{k_i}\in\mathbb[2,m]kik_i 表示第 ii 个实体的长度。该句子内所有实体将组成一个集合 Ex={e(1),e(2),...,e(m)}E_x=\{e^{(1)},e^{(2)},...,e^{(m)}\} 。任意取集合 ExE_x 中的两个实体作为一个组合 (e(a),e(b))(e^{(a)},e^{(b)}),其中 (a<b)(a<b) ,并考察其是否具有关系,关系表示为 ra,br_{a,b} ,其取值为 {0,1,2,...,r}\{0,1,2,...,r\}rr 表示关系类标个数,即预设的试题关系的种类,每一个关系对应一个整数,0则表示没有关系。因此对于一个包含 mm 个实体 ExE_x 的句子 xx 将产生 h=Cm2=m(m1)2h=C_m^2=\frac{m(m-1)}{2} 个关系组合 Rx={r1,2,r1,3,...,rm1,m}R_x=\{r_{1,2},r_{1,3},...,r_{m-1,m}\}
      是将一句话 xx 中不同组合的实体及其之间的所有单词组成一个新子句,并将其对应的word embeddings作为输入数据。子句 xi,j={e(i),xtki+1(i),...,xtkj1(i),e(j)}x_{i,j}=\{e^{(i)},x_{t_{k_i}+1}^{(i)},...,x_{t_{k_j}-1}^{(i)},e^{(j)}\} ,其对应的关系类标为 ri,jr_{i,j} ,因此对于 xx 将有 hh 个子句组成的样本集 X={(xi,j,ri,j)i<j,i,j[1,h]}X=\{(x_{i,j},r_{i,j})|i<j,i,j\in\mathbb[1,h]\}
      对于该样本,由于每个子句的长度不一致,因此需要进行填充0方式使各个子句长度一致,然后将其喂给Text-CNN。Text-CNN采用mini-batch法进行梯度下降,其训练过程可参考10.3节内容。
      Ps:在诸多论文中都有表示,两个实体是否具有关系与其相对距x离有关系,而一般来说两者的距离超过25(即两个实体之间有超过25个单词),则两个实体具有关系的概率将会趋近于0,因此认为实体在一个句子内会产生关联,而不在同一个句子内认为不具有关联性,当然这种假设也存在一定的问题,但在实验中影响不大。
      

    十一、基于依存关系模型的关系抽取

      卷积神经网络可以很好的对实体与实体关系进行分类,而在自然语言处理中,通常会对句子进行句法分析,通过不同语言的语法规则建立起模型——依存关系。基于依存关系的关系抽取是该模型的一个应用方向,其主要是通过句法分析实现,而不是通过深度模型自动挖掘。本文虽然主要是以深度学习角度分析,但传统的“浅层态”模型也需要了解,以方便将其与深度模型进行整合。

    11.1依存句法分析

      基于依存关系模型的关系抽取也叫做开放式实体关系抽取,解决关系抽取的思路是对一个句子的词性进行预处理,例如对于一句话“马云在杭州创办了阿里巴巴”。不同于之前所讲的深度模型,词性分析则是对该句话中每一个词进行预先标注,例如“马云”、“杭州”和“阿里巴巴”被标记为名词,“在”和“了”被标记为介词,“创办”被标记为动词。所谓的依存关系则体现在不同词性的词之间存在着依存关系路径。“在”通常后面跟着地名,也就是名词,“创办”动词前通常为名词,而“在…创办了”便是一个依存关系。因此依存关系即为不同词性的词之间的关系结构,下标列出了关于中文的依存标注:

    在这里插入图片描述

    例如对于下面一句话,句子开头设置一个“ROOT”作为开始,句子结束则为句号“。”,依存关系可以表示为下图:

    在这里插入图片描述

      在传统的实体识别中,是通过基于规则的词性分析实现的,最简单的是正则表达式匹配,其次是使用NLP词性标注工具。通常认为名词是实体,因此实体可以通过词性标注实现抽取。因为词性对每一个词进行了标注,自然根据语法规则可以构建起上图所示的依存关系。每一个词根据语法规则构建起一条关系路径。所有路径的最终起始点即为句子的核心(HED)。

    11.2 依存句法分析实现关系抽取

      对于关系抽取问题,基于依存关系的关系抽取模型中,关系词并非是预先设置的类别,而是存在于当前的句子中。例如“马云在杭州创办了阿里巴巴”,预定义的关系可能是“创始人”,而“创始人”一词在句子中不存在,但是句中存在一个与其相似的词“创办”。因此在句法分析中,能够提取出核心词“创办”,该词前面有一个名词“杭州”,而“杭州”前面有一个介词“在”,因此“在杭州”是一个介宾短语,依存路径被标记为POB,所以“杭州”不是“创办”的主语,自然是“马云”。“创办”一词后面是助词“了”可以省略,再往后则是名称“阿里巴巴”,因此“创办阿里巴巴”为动宾关系VOB,与上面的“探索机制”一样。因此可分析得到语义为“马云创办阿里巴巴”,核心词“创办”即为关系,“马云”和阿里巴巴则是两个实体。
      因此基于依存关系的关系抽取算法步骤如下:
      (1)获取句子 xx
      (2)对句子 xx 进行词性标注;
      (3)构建依存关系路径,并依据依存标注表对路径进行标注;
      (4)提取核心词;
      (5)构建起动宾结构,以核心词为关系寻找主语和宾语作为两个实体。
    可以发现,基于依存关系不仅可以抽取关系,也可以提取出其对应的实体。因此包括基于深度学习模型在内,一种端到端的联合实体识别与关系抽取被提出。

    十二、基于远程监督的关系抽取

    参考博主写的博文:基于监督学习和远程监督的神经关系抽取

    十三、注意力机制(Attention)

    基于注意力机制的模型目前有许多,本文暂提供两种论文解读,详情博主两篇论文解读:
    1.论文解读:Attention Is All You Need
    2.论文解读:Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification

    十四、基于注意力机制的命名实体识别与关系抽取

    应用注意力机制实现命名实体识别和关系抽取(也包括其他自然语言处理任务)成为常用之计,由于篇幅过大,本章节决定另开辟博文讲述。

    十五、知识的存储——图形数据库

      在通过一系列算法实现对非结构化数据进行命名实体识别和关系抽取之后,按照知识图谱中知识抽取的步骤将其存储在数据库中。通常使用的数据库为关系数据库,即满足第一和第二范式的“表格型”存储模型,但关系型数据库不能够很直接的处理具有关系类别的数据,即若表达不同数据之间的关系需要构建外键,并通过外连接方式进行关联,这对于百万级别数据来说,对查询速度以及后期的数据更新与维护都是不利的,因此需要一个能够体现不同实体关系的数据库。因此引入图数据库概念。

    15.1 图数据库

      图数据库包含的元素主要由结点、属性、关系,如图:
    关系图
    “马云”和“阿里巴巴”为实体,对于图数据库中的结点,“马云” 与“阿里巴巴”二者实体之间有一个定向关系“创办”,对于于数据库中的关系,每个实体结点都有各自的属性,即图数据库中的属性,因此构建图数据库的关键三要素即为实体1,实体2和关系,即为三元组。
      Ps:常用的图数据库有 Neo4j,NoSQL,Titan,OrientDB和 ArangoDB。关于图数据库的概念可参考:图数据库,数据库中的“黑科技”初识图数据与图数据库

    15.2 Neo4j的简介

      Neo4j是由java实现的开源NOSQL图数据库,数据库分为关系型和非关系型两种类型。其中非关系型又分为Graph(图形),Document(文档),Cloumn Family(列式),以及Key-Value Store(KV),这四种类型数据库分别使用不同的数据结构进行存储。因此它们所适用的场景也不尽相同。(引自Neo4j简介

    15.3 简单的CQL语句

      在手动编辑Neo4j数据库时,需要编写CQL语句实现对数据库的操作。Java在实现对Neo4j时也需要编写CQL语句,因此在知识抽取后的保存工作,需要对CQL语句有所了解,下面是简单的CQL语句:
    1、创建
    (1)创建单个标签到结点

    CREATE(结点名称:标签名)
    

    (2)创建多个标签到结点:

    CREATE(结点名称:标签名1:标签名2:....)
    

    (3)创建结点和属性:

    CREATE(结点名称:标签名{属性1名称:属性值1,属性2名称:属性值2,....})
    

    (4)MERGE创建结点和属性(检测是否存在结点和属性一模一样的,若存在则不创建,不存在则创建)

    MERGE(结点名称:标签名{属性1名称:属性值1,属性2名称:属性值2,....})
    

    2、检索:
    (1)检索一个结点

    MATCH(结点名称:标签名) RETURN 结点名称
    

    (2)检索一个结点的某个或多个属性

    MATCH(结点名称:标签名) RETURN 结点名称.属性名称1,结点名称.属性名称2,...
    

    (3)使用WHERE关键字搜索

    MATCH(结点名称:标签名) WHERE 标签名.属性名 关系运算符(=>OR等) 数值 RETURN 结点名称.属性名称1,结点名称.属性名称2,...
    

    Ps:其中WHERE 标签名.属性名 IS NOT NULL可以过滤掉不存在该属性值的结点
    Ps:WHERE 标签名.属性名 IN [100,102] 表示筛选出该属性值介于100和102之间
    (4)使用ORDER BY进行排序

    MATCH(结点名称:标签名) RETURN 结点名称.属性名1,... ORDER BY 结点名称.属性名1
    

    (DESC表示逆序,ASC表示正序)
    (5)使用UNION进行联合查询(要求具有相同的列名称和数据类型,过滤掉重复数据)

    MATCH(结点名称1.标签名)
    RETURN 结点名称1.属性名1 as 属性名s1,结点名称1.属性名2 as 属性名s2,..
    UNION
    MATCH(结点名称2.标签名)
    RETURN 结点名称2.属性名1 as 属性名s1,结点名称2.属性名2 as 属性名s2,..
    

    Ps:若使用UNION ALL,则不过滤重复行,其他与UNION一致
    (6)LIMIT限制返回记录个数

    MATCH(结点名称:标签名称)
    RETURN 结点名称.属性名1,...
    LIMIT 5(限制最多返回5条数据)
    

    (7)SKIP跳过前面的记录显示后面的

    MATCH(结点名称:标签名称)
    RETURN 结点名称.属性名1,...
    SKIP 5(跳过前5条数据显示第6个及以后)
    

    3、接点与关系
    (1)添加新结点并为关系创建标签:

    CREATE(结点1名称:标签名1)-[关系名称:关系标签名]->(结点2名称:标签名2)
    

    (2)为现有的结点添加关系:

    MATCH(结点1名称:标签名1),(结点2名称:标签名2)
    WHERE 条件
    CREATE(结点1名称)-[关系名称:关系标签名]->(结点2名称)
    

    (3)为现有结点添加/修改属性

    MATCH(结点名称:标签名)
    SET 结点名称.新属性名 = 值
    RETURN 结点名称
    

    4、删除
    (1)删除结点:

    MATCH(结点名称:标签名) DELETE 结点名称
    

    (2)删除结点及关系

    MATCH(结点1名称:标签名1)-[关系名称]->(结点2名称:标签名2)
    DELETE 结点1名称,结点2名称,关系名称
    

    (3)删除结点的属性

    MATCH(结点名称:标签名)
    REMOVE 结点名称.属性名,...
    RETURN 结点名称
    

    (4)删除结点的某个标签

    MATCH(结点名称:标签名1)
    REMOVE 结点名称:标签名2
    

    十六、Tensorflow实现命名实体识别与关系抽取

      本文以及详细的分析了实现命名实体识别与关系抽取的各种模型,本节将提供两个项目程序,一个是基于Bi-LSTM和CRF的命名实体识别,另一个是基于Bi-LSTM-LSTM和Text-CNN的端到端模型的实体与关系的联合抽取。

    16.1 基于Bi-LSTM和CRF的命名实体识别

    (1)main函数,主要解决控制台参数获取、数据获取以及调用模型:

    import tensorflow as tf
    import numpy as np
    import os, argparse, time, random
    from model import BiLSTM_CRF
    from utils import str2bool, get_logger, get_entity
    from data import read_corpus, read_dictionary, tag2label, random_embedding
    
    ## Tensorflow的Session运行环境
    os.environ['CUDA_VISIBLE_DEVICES'] = '0'
    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'  # default: 0
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    config.gpu_options.per_process_gpu_memory_fraction = 0.2  # need ~700MB GPU memory
    
    ## 控制台输入的超参数
    parser = argparse.ArgumentParser(description='BiLSTM-CRF for Chinese NER task')
    parser.add_argument('--train_data', type=str, default='data_path', help='train data source')
    parser.add_argument('--test_data', type=str, default='data_path', help='test data source')
    parser.add_argument('--batch_size', type=int, default=64, help='#sample of each minibatch')
    parser.add_argument('--epoch', type=int, default=10, help='#epoch of training')
    parser.add_argument('--hidden_dim', type=int, default=300, help='#dim of hidden state')
    parser.add_argument('--optimizer', type=str, default='Adam', help='Adam/Adadelta/Adagrad/RMSProp/Momentum/SGD')
    parser.add_argument('--CRF', type=str2bool, default=True, help='use CRF at the top layer. if False, use Softmax')
    parser.add_argument('--lr', type=float, default=0.001, help='learning rate')
    parser.add_argument('--clip', type=float, default=5.0, help='gradient clipping')
    parser.add_argument('--dropout', type=float, default=0.5, help='dropout keep_prob')
    parser.add_argument('--update_embedding', type=str2bool, default=True, help='update embedding during training')
    parser.add_argument('--pretrain_embedding', type=str, default='random', help='use pretrained char embedding or init it randomly')
    parser.add_argument('--embedding_dim', type=int, default=300, help='random init char embedding_dim')
    parser.add_argument('--shuffle', type=str2bool, default=True, help='shuffle training data before each epoch')
    parser.add_argument('--mode', type=str, default='demo', help='train/test/demo')
    parser.add_argument('--demo_model', type=str, default='1521112368', help='model for test and demo')
    args = parser.parse_args()
    
    ## 获取数据(word embeddings)
    #word2id:为每一个不重复的字进行编号,其中UNK为最后一位
    word2id = read_dictionary(os.path.join('.', args.train_data, 'word2id.pkl'))
    print("\n========word2id=========\n",word2id)
    if args.pretrain_embedding == 'random':
        #随机生成词嵌入矩阵(一共3905个字,默认取300个特征,维度为3905*300)
        embeddings = random_embedding(word2id, args.embedding_dim)
    else:
        embedding_path = 'pretrain_embedding.npy'
        embeddings = np.array(np.load(embedding_path), dtype='float32')
    print("\n=========embeddings==========\n",embeddings,"\ndim(embeddings)=",embeddings.shape)
    
    ## read corpus and get training data获取
    if args.mode != 'demo':
        train_path = os.path.join('.', args.train_data, 'train_data')
        test_path = os.path.join('.', args.test_data, 'test_data')
        train_data = read_corpus(train_path)#读取训练集
        test_data = read_corpus(test_path); test_size = len(test_data)#读取测试集
        #print("\n==========train_data================\n",train_data)
        #print("\n==========test_data================\n",test_data)
    
    
    ## paths setting创建相应文件夹目录
    paths = {}
    # 时间戳就是一个时间点,一般就是为了在同步更新的情况下提高效率之用。
    #就比如一个文件,如果他没有被更改,那么他的时间戳就不会改变,那么就没有必要写回,以提高效率,
    #如果不论有没有被更改都重新写回的话,很显然效率会有所下降。
    timestamp = str(int(time.time())) if args.mode == 'train' else args.demo_model
    #输出路径output_path路径设置为data_path_save下的具体时间名字为文件名
    output_path = os.path.join('.', args.train_data+"_save", timestamp)
    if not os.path.exists(output_path): os.makedirs(output_path)
    
    summary_path = os.path.join(output_path, "summaries")
    paths['summary_path'] = summary_path
    if not os.path.exists(summary_path): os.makedirs(summary_path)
    model_path = os.path.join(output_path, "checkpoints/")
    if not os.path.exists(model_path): os.makedirs(model_path)
    ckpt_prefix = os.path.join(model_path, "model")
    paths['model_path'] = ckpt_prefix
    result_path = os.path.join(output_path, "results")
    paths['result_path'] = result_path
    if not os.path.exists(result_path): os.makedirs(result_path)
    log_path = os.path.join(result_path, "log.txt")
    paths['log_path'] = log_path
    get_logger(log_path).info(str(args))
    
    ## 调用模型进行训练
    if args.mode == 'train':
        #创建对象model
        model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
        #创建结点,
        model.build_graph()
    
        ## hyperparameters-tuning, split train/dev
        # dev_data = train_data[:5000]; dev_size = len(dev_data)
        # train_data = train_data[5000:]; train_size = len(train_data)
        # print("train data: {0}\ndev data: {1}".format(train_size, dev_size))
        # model.train(train=train_data, dev=dev_data)
    
        ## train model on the whole training data
        print("train data: {}".format(len(train_data)))
        model.train(train=train_data, dev=test_data)  # use test_data as the dev_data to see overfitting phenomena
    
    ## 调用模型进行测试
    elif args.mode == 'test':
        ckpt_file = tf.train.latest_checkpoint(model_path)
        print(ckpt_file)
        paths['model_path'] = ckpt_file
        model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
        model.build_graph()
        print("test data: {}".format(test_size))
        model.test(test_data)
    
    ## 根据训练并测试好的模型进行应用
    elif args.mode == 'demo':
        ckpt_file = tf.train.latest_checkpoint(model_path)
        print(ckpt_file)
        paths['model_path'] = ckpt_file
        model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
        model.build_graph()
        saver = tf.train.Saver()
        with tf.Session(config=config) as sess:
            saver.restore(sess, ckpt_file)
            while(1):
                print('Please input your sentence:')
                demo_sent = input()
                if demo_sent == '' or demo_sent.isspace():
                    print('bye!')
                    break
                else:
                    demo_sent = list(demo_sent.strip())
                    demo_data = [(demo_sent, ['O'] * len(demo_sent))]
                    tag = model.demo_one(sess, demo_data)
                    PER, LOC, ORG = get_entity(tag, demo_sent)
                    print('PER: {}\nLOC: {}\nORG: {}'.format(PER, LOC, ORG))
    

    (2)数据处理module,处理数据,包括对数据进行编号,词向量获取以及标注信息的处理等:

    import sys, pickle, os, random
    import numpy as np
    
    ## tags, BIO
    tag2label = {"O": 0,
                 "B-PER": 1, "I-PER": 2,
                 "B-LOC": 3, "I-LOC": 4,
                 "B-ORG": 5, "I-ORG": 6
                 }
    
    #输入train_data文件的路径,读取训练集的语料,输出train_data
    def read_corpus(corpus_path):
        """
        read corpus and return the list of samples
        :param corpus_path:
        :return: data
        """
        data = []
        with open(corpus_path, encoding='utf-8') as fr:
            lines = fr.readlines()
        sent_, tag_ = [], []
        for line in lines:
            if line != '\n':
                [char, label] = line.strip().split()
                sent_.append(char)
                tag_.append(label)
            else:
                data.append((sent_, tag_))
                sent_, tag_ = [], []
        return data
    
    #生成word2id序列化文件
    def vocab_build(vocab_path, corpus_path, min_count):
        """
    	#建立词汇表
        :param vocab_path:
        :param corpus_path:
        :param min_count:
        :return:
        """
        #读取数据(训练集或测试集)
        #data格式:[(字,标签),...]
        data = read_corpus(corpus_path)
        word2id = {}
        for sent_, tag_ in data:
            for word in sent_:
                if word.isdigit():
                    word = '<NUM>'
                elif ('\u0041' <= word <='\u005a') or ('\u0061' <= word <='\u007a'):
                    word = '<ENG>'
                if word not in word2id:
                    word2id[word] = [len(word2id)+1, 1]
                else:
                    word2id[word][1] += 1
        low_freq_words = []
        for word, [word_id, word_freq] in word2id.items():
            if word_freq < min_count and word != '<NUM>' and word != '<ENG>':
                low_freq_words.append(word)
        for word in low_freq_words:
            del word2id[word]
        new_id = 1
        for word in word2id.keys():
            word2id[word] = new_id
            new_id += 1
        word2id['<UNK>'] = new_id
        word2id['<PAD>'] = 0
        print(len(word2id))
        #将任意对象进行序列化保存
        with open(vocab_path, 'wb') as fw:
            pickle.dump(word2id, fw)
    
    #将句子中每一个字转换为id编号,例如['我','爱','中','国'] ==> ['453','7','3204','550']
    def sentence2id(sent, word2id):
        """
        :param sent:源句子
        :param word2id:对应的转换表
        :return:
        """
        sentence_id = []
        for word in sent:
            if word.isdigit():
                word = '<NUM>'
            elif ('\u0041' <= word <= '\u005a') or ('\u0061' <= word <= '\u007a'):
                word = '<ENG>'
            if word not in word2id:
                word = '<UNK>'
            sentence_id.append(word2id[word])
        return sentence_id
    
    #读取word2id文件
    def read_dictionary(vocab_path):
        """
        :param vocab_path:
        :return:
        """
        vocab_path = os.path.join(vocab_path)
        #反序列化
        with open(vocab_path, 'rb') as fr:
            word2id = pickle.load(fr)
        print('vocab_size:', len(word2id))
        return word2id
    
    #随机嵌入
    def random_embedding(vocab, embedding_dim):
        """
        :param vocab:
        :param embedding_dim:
        :return:
        """
        embedding_mat = np.random.uniform(-0.25, 0.25, (len(vocab), embedding_dim))
        embedding_mat = np.float32(embedding_mat)
        return embedding_mat
    
    def pad_sequences(sequences, pad_mark=0):
        """
        :param sequences:
        :param pad_mark:
        :return:
        """
        max_len = max(map(lambda x : len(x), sequences))
        seq_list, seq_len_list = [], []
        for seq in sequences:
            seq = list(seq)
            seq_ = seq[:max_len] + [pad_mark] * max(max_len - len(seq), 0)
            seq_list.append(seq_)
            seq_len_list.append(min(len(seq), max_len))
        return seq_list, seq_len_list
    
    def batch_yield(data, batch_size, vocab, tag2label, shuffle=False):
        """
        :param data:
        :param batch_size:
        :param vocab:
        :param tag2label:
        :param shuffle:随机对列表data进行排序
        :return:
        """
        #如果参数shuffle为true,则对data列表进行随机排序
        if shuffle:
            random.shuffle(data)
        seqs, labels = [], []
        for (sent_, tag_) in data:
        	#将句子转换为编号组成的数字序列
            sent_ = sentence2id(sent_, vocab)
            #将标签序列转换为数字序列
            label_ = [tag2label[tag] for tag in tag_]
            #一个句子就是一个样本,当句子数量等于预设的一批训练集数量,便输出该样本
            if len(seqs) == batch_size:
                yield seqs, labels
                seqs, labels = [], []
            seqs.append(sent_)
            labels.append(label_)
    
        if len(seqs) != 0:
            yield seqs, labels
    

    (3)二进制读写

    import os
    def conlleval(label_predict, label_path, metric_path):
        """
        :param label_predict:
        :param label_path:
        :param metric_path:
        :return:
        """
        eval_perl = "./conlleval_rev.pl"
        with open(label_path, "w") as fw:
            line = []
            for sent_result in label_predict:
                for char, tag, tag_ in sent_result:
                    tag = '0' if tag == 'O' else tag
                    char = char.encode("utf-8")
                    line.append("{} {} {}\n".format(char, tag, tag_))
                line.append("\n")
            fw.writelines(line)
        os.system("perl {} < {} > {}".format(eval_perl, label_path, metric_path))
        with open(metric_path) as fr:
            metrics = [line.strip() for line in fr]
        return metrics
    

    (4)字符串处理,对数据类标转换,对已生成的标注序列进行实体提取:

    import logging, sys, argparse
    
    #将字符串转换为布尔型
    def str2bool(v):
        # copy from StackOverflow
        if v.lower() in ('yes', 'true', 't', 'y', '1'):
            return True
        elif v.lower() in ('no', 'false', 'f', 'n', '0'):
            return False
        else:
            raise argparse.ArgumentTypeError('Boolean value expected.')
    
    #获得实体(将标签序列找出相应的标签组合并返回对应的文字)
    def get_entity(tag_seq, char_seq):
        PER = get_PER_entity(tag_seq, char_seq)
        LOC = get_LOC_entity(tag_seq, char_seq)
        ORG = get_ORG_entity(tag_seq, char_seq)
        return PER, LOC, ORG
    
    def get_PER_entity(tag_seq, char_seq):
        length = len(char_seq)
        PER = []
        for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
            if tag == 'B-PER':
                if 'per' in locals().keys():
                    PER.append(per)
                    del per
                per = char
                if i+1 == length:
                    PER.append(per)
            if tag == 'I-PER':
                per += char
                if i+1 == length:
                    PER.append(per)
            if tag not in ['I-PER', 'B-PER']:
                if 'per' in locals().keys():
                    PER.append(per)
                    del per
                continue
        return PER
    
    def get_LOC_entity(tag_seq, char_seq):
        length = len(char_seq)
        LOC = []
        for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
            if tag == 'B-LOC':
                if 'loc' in locals().keys():
                    LOC.append(loc)
                    del loc
                loc = char
                if i+1 == length:
                    LOC.append(loc)
            if tag == 'I-LOC':
                loc += char
                if i+1 == length:
                    LOC.append(loc)
            if tag not in ['I-LOC', 'B-LOC']:
                if 'loc' in locals().keys():
                    LOC.append(loc)
                    del loc
                continue
        return LOC
    
    def get_ORG_entity(tag_seq, char_seq):
        length = len(char_seq)
        ORG = []
        for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
            if tag == 'B-ORG':
                if 'org' in locals().keys():
                    ORG.append(org)
                    del org
                org = char
                if i+1 == length:
                    ORG.append(org)
            if tag == 'I-ORG':
                org += char
                if i+1 == length:
                    ORG.append(org)
            if tag not in ['I-ORG', 'B-ORG']:
                if 'org' in locals().keys():
                    ORG.append(org)
                    del org
                continue
        return ORG
    
    def get_logger(filename):
        logger = logging.getLogger('logger')
        logger.setLevel(logging.DEBUG)
        logging.basicConfig(format='%(message)s', level=logging.DEBUG)
        handler = logging.FileHandler(filename)
        handler.setLevel(logging.DEBUG)
        handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s: %(message)s'))
        logging.getLogger().addHandler(handler)
        return logger
    

    (5)Bi-LSTM+CRF模型:

    import numpy as np
    import os, time, sys
    import tensorflow as tf
    from tensorflow.contrib.rnn import LSTMCell
    from tensorflow.contrib.crf import crf_log_likelihood
    from tensorflow.contrib.crf import viterbi_decode
    from data import pad_sequences, batch_yield
    from utils import get_logger
    from eval import conlleval
    
    #batch_size:批大小,一次训练的样本数量
    #epoch:使用全部训练样本训练的次数
    #hidden_dim:隐藏维度
    #embeddinds:词嵌入矩阵(字数量*特征数量)
    class BiLSTM_CRF(object):
        def __init__(self, args, embeddings, tag2label, vocab, paths, config):
            self.batch_size = args.batch_size
            self.epoch_num = args.epoch
            self.hidden_dim = args.hidden_dim
            self.embeddings = embeddings
            self.CRF = args.CRF
            self.update_embedding = args.update_embedding
            self.dropout_keep_prob = args.dropout
            self.optimizer = args.optimizer
            self.lr = args.lr
            self.clip_grad = args.clip
            self.tag2label = tag2label
            self.num_tags = len(tag2label)
            self.vocab = vocab
            self.shuffle = args.shuffle
            self.model_path = paths['model_path']
            self.summary_path = paths['summary_path']
            self.logger = get_logger(paths['log_path'])
            self.result_path = paths['result_path']
            self.config = config
    
        #创建结点
        def build_graph(self):
            self.add_placeholders()
            self.lookup_layer_op()
            self.biLSTM_layer_op()
            self.softmax_pred_op()
            self.loss_op()
            self.trainstep_op()
            self.init_op()
    
        #placeholder相当于定义了一个位置,这个位置中的数据在程序运行时再指定
        def add_placeholders(self):
            self.word_ids = tf.placeholder(tf.int32, shape=[None, None], name="word_ids")
            self.labels = tf.placeholder(tf.int32, shape=[None, None], name="labels")
            self.sequence_lengths = tf.placeholder(tf.int32, shape=[None], name="sequence_lengths")
            self.dropout_pl = tf.placeholder(dtype=tf.float32, shape=[], name="dropout")
            self.lr_pl = tf.placeholder(dtype=tf.float32, shape=[], name="lr")
        
        def lookup_layer_op(self):
            #新建变量
            with tf.variable_scope("words"):
                _word_embeddings = tf.Variable(self.embeddings,
                                               dtype=tf.float32,
                                               trainable=self.update_embedding,
                                               name="_word_embeddings")
                #寻找_word_embeddings矩阵中分别为words_ids中元素作为下标的值
                #提取出该句子每个字对应的向量并组合起来
                word_embeddings = tf.nn.embedding_lookup(params=_word_embeddings,
                                                         ids=self.word_ids,
                                                         name="word_embeddings")
            #dropout函数是为了防止在训练中过拟合的操作,将训练输出按一定规则进行变换
            self.word_embeddings =  tf.nn.dropout(word_embeddings, self.dropout_pl)
            print("========word_embeddings=============\n",word_embeddings)
    
        #双向LSTM网络层输出
        def biLSTM_layer_op(self):
            with tf.variable_scope("bi-lstm"):
                cell_fw = LSTMCell(self.hidden_dim)#前向cell
                cell_bw = LSTMCell(self.hidden_dim)#反向cell
                #(output_fw_seq, output_bw_seq)是一个包含前向cell输出tensor和后向cell输出tensor组成的元组,_为包含了前向和后向最后的隐藏状态的组成的元组
                (output_fw_seq, output_bw_seq), _ = tf.nn.bidirectional_dynamic_rnn(
                    cell_fw=cell_fw,
                    cell_bw=cell_bw,
                    inputs=self.word_embeddings,
                    sequence_length=self.sequence_lengths,
                    dtype=tf.float32)
                output = tf.concat([output_fw_seq, output_bw_seq], axis=-1)
                output = tf.nn.dropout(output, self.dropout_pl)
            with tf.variable_scope("proj"):
                #权值矩阵
                W = tf.get_variable(name="W",
                                    shape=[2 * self.hidden_dim, self.num_tags],
                                    initializer=tf.contrib.layers.xavier_initializer(),
                                    dtype=tf.float32)
                #阈值向量
                b = tf.get_variable(name="b",
                                    shape=[self.num_tags],
                                    initializer=tf.zeros_initializer(),
                                    dtype=tf.float32)
                s = tf.shape(output)
                output = tf.reshape(output, [-1, 2*self.hidden_dim])
                #tf.matmul矩阵乘法
                pred = tf.matmul(output, W) + b
                self.logits = tf.reshape(pred, [-1, s[1], self.num_tags])
    
        #损失函数
        def loss_op(self):
            #使用CRF的最大似然估计
            if self.CRF:
                #crf_log_likelihood作为损失函数
                #inputs:unary potentials,就是每个标签的预测概率值
                #tag_indices,这个就是真实的标签序列了
                #sequence_lengths,一个样本真实的序列长度,为了对齐长度会做些padding,但是可以把真实的长度放到这个参数里
                #transition_params,转移概率,可以没有,没有的话这个函数也会算出来
                #输出:log_likelihood:标量;transition_params,转移概率,如果输入没输,它就自己算个给返回
                #self.logits为双向LSTM的输出
                log_likelihood, self.transition_params = crf_log_likelihood(inputs=self.logits,
                                                                       tag_indices=self.labels,
                                                                       sequence_lengths=self.sequence_lengths)
                #tf.reduce_mean默认对log_likelihood所有元素求平均
                self.loss = -tf.reduce_mean(log_likelihood)
            else:
                #交差信息熵
                losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits,
                                                                        labels=self.labels)
                mask = tf.sequence_mask(self.sequence_lengths)
                losses = tf.boolean_mask(losses, mask)
                self.loss = tf.reduce_mean(losses)
            tf.summary.scalar("loss", self.loss)
    
        def softmax_pred_op(self):
            if not self.CRF:
                self.labels_softmax_ = tf.argmax(self.logits, axis=-1)
                self.labels_softmax_ = tf.cast(self.labels_softmax_, tf.int32)
    
        def trainstep_op(self):
            with tf.variable_scope("train_step"):
                #global_step:Optional Variable to increment by one after the variables have been updated
                self.global_step = tf.Variable(0, name="global_step", trainable=False)
                #选择优化算法
                if self.optimizer == 'Adam':
                    optim = tf.train.AdamOptimizer(learning_rate=self.lr_pl)
                elif self.optimizer == 'Adadelta':
                    optim = tf.train.AdadeltaOptimizer(learning_rate=self.lr_pl)
                elif self.optimizer == 'Adagrad':
                    optim = tf.train.AdagradOptimizer(learning_rate=self.lr_pl)
                elif self.optimizer == 'RMSProp':
                    optim = tf.train.RMSPropOptimizer(learning_rate=self.lr_pl)
                elif self.optimizer == 'Momentum':
                    optim = tf.train.MomentumOptimizer(learning_rate=self.lr_pl, momentum=0.9)
                elif self.optimizer == 'SGD':
                    optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
                else:
                    optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
                #根据优化算法模型计算损失函数梯度,返回梯度和变量列表
                grads_and_vars = optim.compute_gradients(self.loss)
                #tf.clip_by_value(A, min, max)指将列表A中元素压缩在min和max之间,大于max或小于min的值改成max和min
                #梯度修剪
                grads_and_vars_clip = [[tf.clip_by_value(g, -self.clip_grad, self.clip_grad), v] for g, v in grads_and_vars]
                #grads_and_vars_clip: compute_gradients()函数返回的(gradient, variable)对的列表并修剪后的
                #global_step:Optional Variable to increment by one after the variables have been updated
                self.train_op = optim.apply_gradients(grads_and_vars_clip, global_step=self.global_step)
    
        def init_op(self):
            self.init_op = tf.global_variables_initializer()
        #显示训练过程中的信息
        def add_summary(self, sess):
            """
            :param sess:
            :return:
            """
            self.merged = tf.summary.merge_all()
            #指定一个文件用来保存图。
            self.file_writer = tf.summary.FileWriter(self.summary_path, sess.graph)
    
        def train(self, train, dev):
            """
            :param train:
            :param dev:
            :return:
            """
            #创建保存模型对象
            saver = tf.train.Saver(tf.global_variables())
            with tf.Session(config=self.config) as sess:
                sess.run(self.init_op)
                self.add_summary(sess)       
                #循环训练epoch_num次
                for epoch in range(self.epoch_num):
                    self.run_one_epoch(sess, train, dev, self.tag2label, epoch, saver)
    
        def test(self, test):
            saver = tf.train.Saver()
            with tf.Session(config=self.config) as sess:
                self.logger.info('=========== testing ===========')
                saver.restore(sess, self.model_path)
                label_list, seq_len_list = self.dev_one_epoch(sess, test)
                self.evaluate(label_list, seq_len_list, test)
    
        #demo
        def demo_one(self, sess, sent):
            """
            :param sess:
            :param sent: 
            :return:
            """
            label_list = []
            #随机将句子分批次,并遍历这些批次,对每一批数据进行预测
            for seqs, labels in batch_yield(sent, self.batch_size, self.vocab, self.tag2label, shuffle=False):
                #预测该批样本,并返回相应的标签数字序列
                label_list_, _ = self.predict_one_batch(sess, seqs)
                label_list.extend(label_list_)
            label2tag = {}
            for tag, label in self.tag2label.items():
                label2tag[label] = tag if label != 0 else label
            #根据标签对照表将数字序列转换为文字标签序列
            tag = [label2tag[label] for label in label_list[0]]
            print('===mode.demo_one:','label_list=',label_list,',label2tag=',label2tag,',tag=',tag)
            return tag
    
        #训练一次
        def run_one_epoch(self, sess, train, dev, tag2label, epoch, saver):
            """
            :param sess:
            :param train:训练集
            :param dev:验证集
            :param tag2label:标签转换字典
            :param epoch:当前训练的轮数
            :param saver:保存的模型
            :return:
            """
            #训练批次数
            num_batches = (len(train) + self.batch_size - 1) // self.batch_size 
            start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            #随机为每一批分配数据
            batches = batch_yield(train, self.batch_size, self.vocab, self.tag2label, shuffle=self.shuffle)
            #训练每一批训练集
            for step, (seqs, labels) in enumerate(batches):
                sys.stdout.write(' processing: {} batch / {} batches.'.format(step + 1, num_batches) + '\r')
                step_num = epoch * num_batches + step + 1
                feed_dict, _ = self.get_feed_dict(seqs, labels, self.lr, self.dropout_keep_prob)
                _, loss_train, summary, step_num_ = sess.run([self.train_op, self.loss, self.merged, self.global_step],
                                                             feed_dict=feed_dict)
                if step + 1 == 1 or (step + 1) % 300 == 0 or step + 1 == num_batches:
                    self.logger.info(
                        '{} epoch {}, step {}, loss: {:.4}, global_step: {}'.format(start_time, epoch + 1, step + 1,
                                                                                    loss_train, step_num))
                self.file_writer.add_summary(summary, step_num)
                #保存模型
                if step + 1 == num_batches:
                    #保存模型数据
                    #第一个参数sess,这个就不用说了。第二个参数设定保存的路径和名字,第三个参数将训练的次数作为后缀加入到模型名字中。
                    saver.save(sess, self.model_path, global_step=step_num)
            self.logger.info('===========validation / test===========')
            label_list_dev, seq_len_list_dev = self.dev_one_epoch(sess, dev)
            #模型评估
            self.evaluate(label_list_dev, seq_len_list_dev, dev, epoch)
    
        def get_feed_dict(self, seqs, labels=None, lr=None, dropout=None):
            """
            :param seqs:句子数字序列
            :param labels:对应句子的标签数字序列
            :param lr:
            :param dropout:
            :return: feed_dict
            """
            #获取填充0后的句子序列样本(使得每个句子填充0后长度一致),同时记录每个句子实际长度
            #例如:s = [['3','1','6','34','66','8'],['3','1','34','66','8'],['3','1','6','34']]
            #返回为[['3', '1', '6', '34', '66', '8'], ['3', '1', '34', '66', '8', 0], ['3', '1', '6', '34', 0, 0]],长度为 [6, 5, 4]
            word_ids, seq_len_list = pad_sequences(seqs, pad_mark=0)
            feed_dict = {self.word_ids: word_ids,
                         self.sequence_lengths: seq_len_list}
            if labels is not None:
                #为标签序列填充0并返回每个句子标签的实际长度
                labels_, _ = pad_sequences(labels, pad_mark=0)
                feed_dict[self.labels] = labels_
            if lr is not None:
                feed_dict[self.lr_pl] = lr
            if dropout is not None:
                feed_dict[self.dropout_pl] = dropout
            return feed_dict, seq_len_list
    
        def dev_one_epoch(self, sess, dev):
            """
            :param sess:
            :param dev:
            :return:
            """
            label_list, seq_len_list = [], []
            for seqs, labels in batch_yield(dev, self.batch_size, self.vocab, self.tag2label, shuffle=False):
                label_list_, seq_len_list_ = self.predict_one_batch(sess, seqs)
                label_list.extend(label_list_)
                seq_len_list.extend(seq_len_list_)
            return label_list, seq_len_list
    
        #预测一批数据集
        def predict_one_batch(self, sess, seqs):
            """
            :param sess:
            :param seqs:
            :return: label_list
                     seq_len_list
            """
            #将样本进行整理(填充0方式使得每句话长度一样,并返回每句话实际长度)
            feed_dict, seq_len_list = self.get_feed_dict(seqs, dropout=1.0)
            #若使用CRF
            if self.CRF:
                logits, transition_params = sess.run([self.logits, self.transition_params],
                                                     feed_dict=feed_dict)
                label_list = []
                for logit, seq_len in zip(logits, seq_len_list):
                    viterbi_seq, _ = viterbi_decode(logit[:seq_len], transition_params)
                    label_list.append(viterbi_seq)
                return label_list, seq_len_list
            else:
                label_list = sess.run(self.labels_softmax_, feed_dict=feed_dict)
                return label_list, seq_len_list
    
        def evaluate(self, label_list, seq_len_list, data, epoch=None):
            """
            :param label_list:
            :param seq_len_list:
            :param data:
            :param epoch:
            :return:
            """
            label2tag = {}
            for tag, label in self.tag2label.items():
                label2tag[label] = tag if label != 0 else label
            model_predict = []
            for label_, (sent, tag) in zip(label_list, data):
                tag_ = [label2tag[label__] for label__ in label_]
                sent_res = []
                if  len(label_) != len(sent):
                    print(sent)
                    print(len(label_))
                    print(tag)
                for i in range(len(sent)):
                    sent_res.append([sent[i], tag[i], tag_[i]])
                model_predict.append(sent_res)
            epoch_num = str(epoch+1) if epoch != None else 'test'
            label_path = os.path.join(self.result_path, 'label_' + epoch_num)
            metric_path = os.path.join(self.result_path, 'result_metric_' + epoch_num)
            for _ in conlleval(model_predict, label_path, metric_path):
                self.logger.info(_)
    

    十七、推荐阅读书籍

      非常能够感谢你能够通读整片文章,本人将针对基于深度学习的命名实体识别与关系抽取为主题,推荐学习文献或书籍。

    17.1 推荐的书籍

    (1)《统计学习方法》(李航)
      这本书是主要针对自然语言处理领域编写的统计学习方法。统计学习方法又叫做统计机器学习。这本书很细致的讲解了几大典型的机器学习算法,同时针对每一种算法的模型进行了相关推导。另外书中还细讲了图概率模型、隐马尔可夫模型和条件随机场,这对于理解命名实体识别非常有利。
    在这里插入图片描述
    (2)《机器学习》(周志华)
      南京大学人工智能实验室的主任周志华在机器学习和大数据挖掘领域内是国内外知名的“大佬”,他出版的《机器学习》西瓜书已经成为IT领域内的畅销书。本人阅读了该书,认为该书对数学理论要求非常高,因此这本书需要在对基本的机器学习知识有所了解之后才适合读。因此较为合理的应先阅读《统计学习方法》后再阅读《机器学习》。
    在这里插入图片描述
    (3)《深度学习》([美] 伊恩·古德费洛)
      《深度学习》是美国数学家伊恩·古德费洛所编写,由人民邮电出版社出版。该书主要以深度学习为主,再简单介绍了机器学习的知识后,对深度学习进行了多角度的分析,并介绍了多层感知机BP神经网络、循环神经网络、卷积神经网络、计算图等内容,是深度学习领域内非常权威的书籍。因此在学习完基本的机器学习内容之后,学习深度学习是必要的。
    在这里插入图片描述
    (4)另外推荐几本数学类书籍,对数学方面薄弱的或者没有学习的读者提供学习的方向:
    《微积分》、《线性代数·同济版》、《矩阵论·华科版》、《最优化方法及其应用》、《最优控制》等。

    十八、项目实例1

      项目名称:面向智慧农业的知识图谱及其应用系统
      研究课题:上海市《农业信息服务平台及农业大数据综合利用研究》之《上海农业农村大数据共享服务平台建设和应用》
      项目团队:华东师范大学数据科学与工程学院
      项目开源:https://github.com/qq547276542/Agriculture_KnowledgeGraph
      运行界面:项目部署可参考GitHub上的README。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    十九、项目实例2:智学AI·基于深度学习的学科知识图谱

    本项目为博主的本科毕业设计,网站网址为:https://www.wjn1996.cn/zxai/index。项目截图如下:
    管理系统:
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    学生平台
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    二十、总结

      本文章(专栏)讲述了基于深度学习的命名实体识别与关系抽取,细致的介绍了相关概念、涉及的模型以及通过公式推导和例子来对模型进行分析,最后给出源码和项目实例。
      本人在研究知识图谱过程中,对知识图谱的关键步骤——知识抽取花了大量的时间,研究了深度神经网络模型、循环神经网络模型,长期记忆模型,在了解相关模型后认识到了编码解码问题,而神经网络充当着编码,因此解决解码问题就用到了概率图模型(当然LSTM也可以解码)。
      命名实体识别问题可以被认为是序列标注问题,关系抽求可以被认为是监督分类问题。在2017年几篇端到端模型的文章纷纷呈现,打破了传统的pipeline模型,在实验效果上非常出色。几种端到端模型包括Bi-LSTM+LSTM+Text-CNN,Bi-LSTM+LSTM,Bi-LSTM+Independency-Tree等,这些模型目的是在命名实体抽取的过程中一并将关系挖掘出来,因此在模型的运行效率上有很大的改善。
      除了端到端模型,一种基于注意力机制的模型映入眼帘,在几篇学术论文中声称其模型比端到端模型更优,在实验中也得到了体现。基于注意力机制模型还需要进行深入的研究。
      关于基于深度学习的知识抽取,本文便到此结束,今后会不断根据学术论文的情况进行更新或添加。


      博主同文:基于深度学习的命名实体识别与关系抽取;更多文章可点击阅读:夏栀的博客

    展开全文
  • 这个命名实体识别数据集是本人亲自整理的最终大型的数据集,供各位学习投票模型的亲朋好友们使用,本人亲自测试,效果非常好!!!!!!
  • NER(中文实体命名识别)光健字: 中文命名实体识别 NER BILSTM CRF IDCNN BERT摘要:对中文命名实体识别一直处于知道却未曾真正实践过的状态,此次主要是想了解和实践一些主流的中文命名实体识别的神经网络算法。...

    NER(中文实体命名识别)

    光健字: 中文命名实体识别 NER BILSTM CRF IDCNN BERT

    摘要:对中文命名实体识别一直处于知道却未曾真正实践过的状态,此次主要是想了解和实践一些主流的中文命名实体识别的神经网络算法。通过对网上博客的阅读了解,中文命名实体识别比较主流的方法是BILSTM+CRF、IDCNN+CRF、BERT+BILSTM+CRF这几种神经网络算法,这个demo也用Keras实现了这几个算法,并且采用几个比较通用的数据集进行训练测试。这个demo是以了解和学习为目的的,所以未得出任何结论

    注意:由于算力和时间的问题,对神经网络的参数未进行太多调试,所以模型现在的参数并不是最佳参数

    主要库的版本

    本项目是基于keras(Using TensorFlow backend)以下是主要库的版本

    python = 3.6.8

    keras == 2.2.4

    keras_contrib == 0.0.2

    keras_bert == 0.80.0

    tensorflow == 1.14.0

    项目目录结构

    data 数据目录

    具体请查看数据目录文件夹下的README文件

    DataProcess 数据处理文件夹

    具体请查看数据处理文件夹下的README文件

    Pubilc 公共工具

    path 定义文件(文件夹)的路径

    utils 工具

    创建log

    keras的callback调类

    Model 模型(总共定义了5个模型,具体结构请查看Model文件夹下的README文件)

    BERT+BILST+CRF

    BILSTM+Attention+CRF

    BILSTM+CRF

    IDCNN+CRF(1)

    IDCNN+CRF(2)

    log 记录数据

    运行项目

    注意:需要用到bert网络的需要提前下载BERT预训练模型解压到data文件夹下

    直接在IDE里运行项目

    直接运行 train.py文件

    命令行

    python train.py

    运行结果

    运行GPU: GeForceRTX2080Ti(GPU显存 10.0G, 算力7.5)

    训练周期为15个周期,提前停止条件:2个周期验证集准确率没有提升。

    BERT采用batch_size=32 因为值为64的时候所使用GPU内存不够

    以下数据基于MSRA数据集,以8:2的拆分(训练集:测试集)。测试结果

    模型

    准确率

    F1

    召回率

    IDCNN_CRF

    0.988

    0.860

    0.871

    IDCNN_CRF_2

    0.990

    0.872

    0.897

    BILSTM_Attention_CRF

    0.987

    0.850

    0.848

    BILSTMCRF

    0.989

    0.870

    0.863

    BERT_BILSTM_CRF

    0.996

    0.954

    0.950

    很显然BERT+BILIST+CRF的组合效果会好很多

    提示:log文件夹里有每个训练周期记录的数据

    展开全文
  • Future丨Intelligence丨Nutrition丨Energy让阅读成为习惯,让灵魂拥有温度点击关注PKUFineLab一起进步命名实体识别研究进展概述命名实体识别任务旨在从自然语言文本中识别具有特定意义的实体,如人名、地名、组织名...
    65e3d30a010e6c7311b55ac480058870.png

    Future丨Intelligence丨Nutrition丨Energy

    让阅读成为习惯,让灵魂拥有温度

    点击关注PKUFineLab 

    514c4dbbd17c7050561c0fa18c8f0202.gif

    一起进步

    命名实体识别研究进展概述

    命名实体识别任务旨在从自然语言文本中识别具有特定意义的实体,如人名、地名、组织名等。在自然语言处理(Natural Language Processing, NLP)中,命名实体识别通常是自然语言理解的第一步,它的结果被用于许多后续的NLP任务,如实体链接(Entity linking)、关系抽取(Relation extraction)和句法分析(Syntactic parsing)等,因此,命名实体识别任务的效果好坏就至关重要了。举例来讲,如果命名实体识别任务从“南京市长江大桥于1968年12月29日建成通车”这句话中错误地识别出职务名——“南京市长”和人名——“江大桥”,而不是正确地识别出地名——“南京市”和桥名——“长江大桥”,则后续基于错误识别结果的NLP任务也无法得到期望的结果。

    命名实体识别任务往往被看作语言序列标注(Linguistic sequence labeling)任务。传统的序列标注任务多使用线性模型,如隐马尔可夫模型(Hidden Markov Models, HMM)和条件随机场(Conditional Random Fields, CRF),并且依赖专家知识(Task-specific knowledge)和人工提取特征(Hand-crafted features)。近年来随着机器算力的增长和深度学习算法的发展,命名实体识别任务利用分布式表示学习方法和非线性的神经网络构建了端到端的模型,从而在无专家知识和人工提取特征的基础上实现了更好的效果。进一步的,针对命名实体识别现阶段面临的各类问题,近期的研究工作相应地对模型做了精细化的调整,也提出了一些新颖的想法。下文将首先介绍现阶段命名实体识别的基本套路,接着在此基础上介绍一些新的、有意思的探索,最后是个人的一点体会。

         基本套路

    现阶段主流的命名实体识别的基本套路可以总结为“三板斧”:单词的字符级表示、(双向)LSTM编码和CRF解码。代表性工作是2016年发表在ACL的工作[1]。

    首先,在对句子中的单词进行向量表示的时候,除了常用的词嵌入向量(word embedding),文章还利用CNN来学习每个单词字符级别的特征,如图1所示。

    8b96bf7ba72c4d983f035f79c415cbc0.png

    图1 用于提取单词的字符级表示的CNN

    将单词“playing”的各字符嵌入向量作为CNN的输入,经过卷积和池化后,得到单词“playing”的字符级表示。通过这种方式,文章认为可以学习到单词的词法特征,如单词的前缀、后缀等特征。

    接着,文章使用双向LSTM作为编码层,学习每个单词在句子中上下文的特征,如图2所示。

    fde567463f07a06e4537a4124e58ccb6.png

    图2 双向LSTM和CRF组成的模型框架

    将每个单词的词嵌入向量和字符级表示向量拼接之后作为编码层的输入,通过双向LSTM为每个单词得到两个(前向和后向)隐藏层向量表示,将这两个隐藏层向量拼接后作为解码层——CRF的输入。

    最后,文章选用CRF作为解码层的原因是,CRF会考虑相邻label间的关系,而不是独立地将每个单词解码成label,例如,在词性标注任务中,“形容词”后更可能跟一个“名词”而不是“动词”,或者是在利用BIOES(Begin, Inside, Other, End & Single)标注模式的实体识别任务中,“I-ORG”不可能紧跟在“I-PER”后。从而能够为一个给定的文本序列解码出一个最好的label链。

          新的探索

    随着深度学习的发展,命名实体识别任务也有了一些新的探索,包括解决标注数据规模和模型参数量不匹配的问题、文本中命名实体嵌套的问题、中文分词对实体识别的影响问题和模型的并行化问题等等,本章节主要就前两个问题及相关工作做进一步叙述。

    标注数据不足

    随着模型的愈发精细复杂,需要训练的参数日益庞大,但其训练所需的人工标注数据却因为标注成本的问题难以得到相应地增长。为解决这个问题,一个常用的方法是使用远程监督的方法来得到大量的远程监督标注数据,但由于用于远程监督的知识库规模有限,大量的实体存在于文本中而未出现在知识库中,导致在远程监督时,将这些未出现在知识库中的实体标注为非实体,从而产生大量的假负例,这即是远程监督标注数据带来的有限覆盖问题(Limited Coverage)。除此之外,另外一种思路是通过优化模型,限制参数量,从而使得模型能够在较小的标注数据集上也能够完成训练。

    813cc9f772ab26309cfc8e89bbdd2484.gif

    有限覆盖问题

    为了缓解有限覆盖问题对模型训练的影响,Shang J等人在2018年的工作中提出了两种方式。首先是利用最先进的短语挖掘方法在文本中挖掘出“高质量短语”,然后在利用知识库远程监督标注时,将文本中未出现在知识库中的“高质量短语”标注为“Unknow”。这样标注数据除了实体类别和非实体以外,还多了“Unknown”类别。随着标签的扩展,上一章中提到的基本套路中的CRF解码层就需要做相应改变,如图3所示。

    860506f2af662173be67cbf9f39f2493.png

    图3 扩展的CRF解码层

    基于BIOES序列标注模式进行实体标注,句子中每个单词的标签是“位置-实体类别”的形式,于是,每个单词有5*实体类别数量个可能的标签(图3中的每一列)。在传统的数据标注上,CRF解码层是从图中所有的路径中找出最大可能性的“一条”路径进行解码标注,即每列找出一个最可能的标签。而增加了“Unknow”类别后,文章认为“Unknown”类别即意味着其单词可能是任意的标签(图3中“prostaglandin”和“synthesis”对应的两列),则CRF解码层需要从图中所有路径中找出最大可能性的“一簇”路径进行解码标注。

    进一步的为了缓解有限覆盖问题带了的负面影响,文章提出了第二种方法——“Tie or Break”序列标注模式。不同于传统的标准的BIO2标注模式,“Tie or Break”不再直接对文本中的单词进行标注,而是对文本中相邻单词的关系进行标注。其规则如下,

    1. 相邻的两个单词出现在同一个(知识库)实体中,这两个单词间的关系标注为“Tie”;

    2. 相邻的两个单词中但凡有一个出现在Unknown类型的“高质量短语”中,这两个单词间的关系标注为“Unknown”;

    3. 其它情况,标注为“Break”。

    这种标注模式有两点好处,其一,当文本中的短语在远程监督时被知识库中的实体部分匹配或者错误匹配时,这些短语内单词间的Tie关系是正确的不受影响的;其二,在远程监督标注时,文本中的单词极容易被知识库中unigram entity即由单独单词组成的实体错误地标注成假正例。而使用“Tie or Break”序列标注模式,无论一个单词时真正的unigram entity或者是假正例,它与两边的单词间的关系都是“Break”,由此减少了假正例对模型的影响。为了匹配这种新的标注模式,文章提出了一种新的命名实体识别模型——AutoNER,如图4所示。

    6308bf7b8e4ca7ddf622353cd1e40175.png

    图4 基于“Tie or Break”标注模式的AutoNER

    自下而上,我们依然能看到单词的字符级别表示和双向LSTM编码,虽然与上一章提到的具体方法有所差异,但其思想基本是一致的,我们重点关注在LSTM编码层之后的解码层。在这个模型中,文章没有继续使用CRF而是分为两步去做解码:第一步,使用双向LSTM表示出单词间关系的隐藏层向量表示后,然后使用一个二分类器区分“Break”和“Tie”,不对“Unknown”做判断;第二步,将第一步预测出的两个相邻“Break”之间的短语视为一个待预测的候选实体,用单词的隐藏层向量综合表示该短语,然后输入到一个多分类器中。

    813cc9f772ab26309cfc8e89bbdd2484.gif

    领域敏感特征

    为了使得模型能在较小的标注数据集上完成训练,2018年Liu L等人发表在AAAI上的工作[3]提出了一种可行的思路。文章提出了一种LM-LSTM-CRF模型,通过模型的名称,我们就可以看出在该模型的编码层和解码层应当和第一章提到的主流方法相同,文章的主要贡献在单词的字符级表示,准确来讲,文章的主要贡献在对字符级别的特征的挑选。文章认为,单词字符级的特征虽然在已有的工作中被证明有效,但其是在大规模预料上经过预训练得到,包含了大量领域无关的特征,这些领域无关的特征需要更多的数据、更大的模型来训练更长的时间。而本文利用多任务学习提取共享特征的思路,以及语言模型(Neural Language Model)可以学习领域敏感特征的特性,使用语言模型从文本的大量特征中只提取领域相关的特征用于序列标注模型的训练,屏蔽掉了大量的任务无关特征,从而使得序列标注模型可以用少量的参数达到较好的效果,具体模型如图5所示。

    36e41a82e3a3841b6ec7f0d96e632723.png

    图5 LM-LSTM-CRF模型结构

    图中使用了双向LSTM提取字符级别的特征,而隐藏层向量却是用于两个任务:序列标注(主要任务)和语言模型(辅助任务)。其中,序列标注任务不再赘述,语言模型就是利用已知序列预测下一个单词是什么,在该模型中,利用单词的前一个隐藏层输出来表示该单词,如前向LSTM中用“Pierre”前一个字符的隐藏层向量来表示“Pierre”,然后进行Softmax分类操作。并且由于这两个任务是不相关的(相关的任务如实体识别和关系抽取),所以文章中在字符级别的LSTM后特意使用了highway层,将隐层向量映射到两个不同的向量空间,各自完成不同的任务。

    命名实体嵌套

    实体嵌套是指在一句文本中出现的实体,存在某个较短实体完全包含在另外一个较长实体内部的情况,如“南京市长”中地名“南京”就嵌套在职务名“南京市长”中。而传统的命名实体识别任务关注的都是平坦实体(Flat entities),即文本中的实体之间不交叉、不嵌套。为了解决嵌套命名实体识别的问题,近年来的研究做了不同的尝试,本文就我了解的几篇工作作简要的介绍,希望能对大家有所启发。

    首先,第一篇是Ju M等人于2018年发表在NAACL上的工作[4],该工作提出一种层模型,其思路其实非常简单,就是对文本一遍一遍的识别,直到识别不出实体为止。具体就是将前面提到的LSTM+CRF看作一个平坦命名实体识别层(flat NER layer),通过动态的堆叠flat NER layer,自内而外的一层一层的识别嵌套实体(先识别inner entities,再识别outer entities),直到flat NER layer识别不出实体,停止堆叠,模型如图6所示。

    4715dca534629c1384c662ea687102a3.png

    图6 层模型

    自下而上,在第一个flat NER layer内,文章把每个token经过双向LSTM后对应的隐藏层表示输入到CRF中,用于识别最内层实体(most inner entities),然后将构成每个最内层实体的单词的隐藏层向量相加并平均来表示最内层实体,然后将其看作一个新的“单词”,替换组成最内层实体的所有单词,然后和其他被识别为Other的单词组成新的序列输入到下一个flat NER layer中,重复以上内容,直到某个flat NER layer将其输入的token都标记为Other后结束堆叠。作者认为通过这样的方式不但可以将嵌套的实体识别出来,还能考虑到inner entities和其outer entities之间的关系。

    接着,第二篇是Sohrab M G等人于2018年发表在EMNLP上的工作[5],该工作的主要内容集中在解码层,单词的字符级表示和双向LSTM编码层与前面的工作大同小异。如图7所示,在解码层,文章不再使用CRF,而是使用一个可变长的滑动窗口在编码层输出的隐藏层向量序列上滑动,枚举出所有可能的片段作为候选实体,然后利用隐藏层向量表示这些候选实体,最后分别通过Softmax进行分类。虽然试验结果不错,但我认为这种穷举的方法比较适合于序列长度较短的情况,因为一方面它忽略了标签间的相关性,另一方面当序列长度较长的情况下,穷举法会得到大量的负样本,使得正负样本不均衡,影响模型效果。

    7fc81bc295a6f9c6031bf24c5c0ede72.png

    图7 枚举模型

    最后,同样是一篇来自2018年EMNLP的工作[6],不同于前面两篇工作对平坦命名实体识别主流方法的修修改改,本文的核心思想是将文本进行一定的转化之后,实现对嵌套实体的平坦访问(顺序访问)。

    首先,如图8所示,作者将嵌套实体表示成一棵句法成分树,文本中的多个嵌套实体便被表示成一个森林。其中,树的叶子节点是文本中的token,而非叶子节点便是实体类别,这样实体的嵌套关系便被表示成了树中非叶子节点的父子关系。

    bc8b732e851160c87eaac53d50be8b9d.png

    图8 用句法成分树标注的嵌套实体

    同时自底向上构建句法成分树的过程中,便实现了嵌套实体平坦访问,即构建树的过程中,用非叶子节点的构建动作对应了相应的实体类别,而这些动作是顺序的,不存在嵌套关系。而森林的构建过程是由多个树的顺序构建过程组成,其被放入一个栈中。如图9所示,

    85258407cd9260ea30032d2ce447a27f.png

    图9 句法成分森林的构建过程

    构建动作的执行是由系统的当前状态决定的,系统的当前状态包含栈的状态,文本序列的剩余部分,构建动作的历史。这样,以系统状态作为输入,当前状态应执行的动作为输出,即可完成模型的训练。

    具体而言,文章首先定义了三类动作:SHIFT,REDUCE-X和UNARY-X,其中X是指实体类别,属于一个预定义的类别集合。SHIFT操作是指将文本序列中的当前单词入栈;REDUCE-X是给栈顶的两个元素出栈,并给他们设置一个共同父节点X,然后将这个树元素入栈;UNARY-X则是将栈顶元素出栈,为其设置一个父节点X,然后将这个新的树元素入栈。定义好动作之后即得到了模型的输出(label),而其输入——系统的状态,则分三部分表示:

    1. 栈的向量化表示使用了stack LSTM模型,在传统的LSTM上加入了栈顶指针,用栈顶指针所指向的unit的隐藏层输出来表示栈的状态;

    2. 文本序列的剩余部分则通过一个后向的LSTM建模,使用顺序第一个单词的隐藏层输出表示文本序列的剩余部分的状态;

    3. 历史构建动作则通过一个前向的LSTM进行学习,最近一次动作的隐藏层输出表示历史动作的状态。

    将这三种状态的向量表示拼接起来表示系统状态,作为Softmax的输入。

        总结

    科研有时候是使用新技术解决旧问题,而同时,新技术的使用也会带来新的问题,这一点在命名实体识别领域让我感触尤为深刻。一方面,得益于深度学习的发展,命名实体识别模型实现了更好的效果,对各种细节问题也有了精细化的建模;而另一方面,深度学习技术也带来了一些问题,比如标注数据同参数规模不匹配的问题,精细模型并行化程度不高的问题等。

    针对命名实体识别领域的研究现状,本文首先介绍了该领域的主要任务和主流方法,并在此基础上,就近年来的最新进展挑选了两个有意思的问题和相关探索进行了介绍。此外还有中文分词对命名实体识别的影响,以及模型并行话程度不高等问题,由于篇幅所限,这里未作介绍,感兴趣的话可以查看[7][8]两篇工作。

    fdaa28ddfc456b7b6d783fa93fa71aee.png

    参考文献

    [1] Ma X, Hovy E H. End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF[J]. meeting of the association for computational linguistics, 2016: 1064-1074.

    [2] Shang J, Liu L, Ren X, et al. Learning Named Entity Tagger using Domain-Specific Dictionary[J]. arXiv preprint arXiv:1809.03599, 2018.

    [3] Liu L, Shang J, Ren X, et al. Empower sequence labeling with task-aware neural language model[C]//Thirty-Second AAAI Conference on Artificial Intelligence. 2018.

    [4] Ju M, Miwa M, Ananiadou S. A neural layered model for nested named entity recognition[C]//Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long Papers). 2018, 1: 1446-1459.

    [5] Sohrab M G, Miwa M. Deep Exhaustive Model for Nested Named Entity Recognition[C]//Proceedings of the 2018 Conference on Empirical Methods in Natural Language Processing. 2018: 2843-2849.

    [6] Wang B, Lu W, Wang Y, et al. A neural transition-based model for nested mention recognition[J]. arXiv preprint arXiv:1810.01808, 2018.

    [7] Zhang Y, Yang J. Chinese NER Using Lattice LSTM[J]. meeting of the association for computational linguistics, 2018: 1554-1564.

    [8] Strubell E, Verga P, Belanger D, et al. Fast and Accurate Entity Recognition with Iterated Dilated Convolutions[J]. empirical methods in natural language processing, 2017: 2670-2680.

    4880832dd69eb10cb26ca9370d37c543.pngf5715a5d679d237c29f5b11ae8fcb35c.png

    作者简介及往期文章

    张程博

    博士研究生二年级、软件工程专业

    研究方向:日志增强技术,知识图谱构建及应用

    作者往期文章汇总:

    1. 浅谈程序语言处理

    2. 语义网:这二十年的’老古董’究竟是什么

    3. 表示学习之跨语言表示学习

    4880832dd69eb10cb26ca9370d37c543.pngf5715a5d679d237c29f5b11ae8fcb35c.png18426465a8047ce4ac6c88cdd2b163c6.png

    扫 描 关 注 

    PKUFineLab

    转载请联系:

    colordown@pku.edu.cn

    Where there is a will there is a way.

    展开全文
  • 基于BERT预训练的中文命名实体识别TensorFlow实现

    万次阅读 多人点赞 2019-01-03 11:58:25
    BERT-BiLSMT-CRF-NER Tensorflow solution of ...使用谷歌的BERT模型在BLSTM-CRF模型上进行预训练用于中文命名实体识别的Tensorflow代码’ 代码已经托管到GitHub 代码传送门 大家可以去clone 下来亲自体验一下! g...
  • NER(中文实体命名识别)光健字: 中文命名实体识别 NER BILSTM CRF IDCNN BERT摘要:对中文命名实体识别一直处于知道却未曾真正实践过的状态,此次主要是想了解和实践一些主流的中文命名实体识别的神经网络算法。...
  • NER(中文实体命名识别)光健字: 中文命名实体识别 NER BILSTM CRF IDCNN BERT摘要:对中文命名实体识别一直处于知道却未曾真正实践过的状态,此次主要是想了解和实践一些主流的中文命名实体识别的神经网络算法。...
  • 转载自 | 哈工大讯飞联合实验室本期导读:嵌套命名实体是一种特殊形式的命名实体,由于其复杂的层次化结构,传统的基于序列标注的命名实体模型不能很好地解决嵌套命名实体识别的任务。因此,自然语言处理领域的研究...
  • 作者 | Walker【磐创AI导读】:本文主要介绍自然语言处理中的经典问题——命名实体识别的两种方法。目录一.什么是命名实体识别二.基于NLTK的命名实体识别三.基于Stanford的NER四.总结一 、什么是命名实体识别?...
  • 什么是命名实体识别二.基于NLTK的命名实体识别三.基于Stanford的NER四.总结一 、什么是命名实体识别命名实体识别(Named Entity Recognition,简称NER),又称作“专名识别”,是指识别文本中具有特定意义的实体...
  • 为此,笔者想基于之前更新的命名实体识别的文章,再写一写最近看到的一些NER算法。笔者在这里就不对命名实体识别等基础知识进行赘述了,我们扣1直接开车。阿力阿哩哩:命名实体识别NER论文调研​zhuanlan.zhihu....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,257
精华内容 1,702
关键字:

命名实体识别