『深度应用』NLP命名实体识别(NER)开源实战教程

『深度应用』NLP命名实体识别(NER)开源实战教程近几年来,基于神经网络的深度学习方法在计算机视觉、语音识别等领域取得了巨大成功,另外在自然语言处理领域也取得了不少进展。在 NLP 的关键性基础任务—命名实体识别(Named Entity Recognition,NER)的研究中,深度学习也获得了不错的效果。 0. 概念讲解 …

开源地址:github.com/xiaosongshi…

欢迎大家关注小宋公众号**《极简 AI》**带你学深度学习:

基于深度学习的理论学习与应用开发技术分享,笔者会经常分享深度学习干货内容,大家在学习或者应用深度学习时,遇到什么问题也可以与我在上面交流知无不答。

近几年来,基于神经网络的深度学习方法在计算机视觉、语音识别等领域取得了巨大成功,另外在自然语言处理领域也取得了不少进展。在 NLP 的关键性基础任务—命名实体识别(Named Entity Recognition,NER)的研究中,深度学习也获得了不错的效果。

目录

0. 概念讲解

0.1 NER 简介

0.2 深度学习方法在 NER 中的应用

2. 编程实战

2.1 概述

2.2 数据预处理

2.3 模型搭建

2.4 模型训练

2.5 模型应用

3. 总结 & 待续

-1. 参考

0. 概念讲解

0.1 NER 简介

NER 又称作专名识别,是自然语言处理中的一项基础任务,应用范围非常广泛。命名实体一般指的是文本中具有特定意义或者指代性强的实体,通常包括人名、地名、组织机构名、日期时间、专有名词等。**NER 系统就是从非结构化的输入文本中抽取出上述实体,并且可以按照业务需求识别出更多类别的实体,**比如产品名称、型号、价格等。因此实体这个概念可以很广,只要是业务需要的特殊文本片段都可以称为实体。

『深度应用』NLP命名实体识别(NER)开源实战教程

学术上 NER 所涉及的命名实体一般包括 3 大类(实体类,时间类,数字类)和 7 小类(人名、地名、组织机构名、时间、日期、货币、百分比)。

实际应用中,NER 模型通常只要识别出人名、地名、组织机构名、日期时间即可,一些系统还会给出专有名词结果(比如缩写、会议名、产品名等)。货币、百分比等数字类实体可通过正则搞定。另外,在一些应用场景下会给出特定领域内的实体,如书名、歌曲名、期刊名等。

NER 是 NLP 中一项基础性关键任务。**从自然语言处理的流程来看,NER 可以看作词法分析中未登录词识别的一种,是未登录词中数量最多、识别难度最大、对分词效果影响最大问题。**同时 NER 也是关系抽取、事件抽取、知识图谱、机器翻译、问答系统等诸多 NLP 任务的基础。

NER 当前并不算是一个大热的研究课题,因为学术界部分学者认为这是一个已经解决的问题。当然也有学者认为这个问题还没有得到很好地解决,原因主要有:命名实体识别只是在有限的文本类型(主要是新闻语料中)和实体类别(主要是人名、地名、组织机构名)中取得了不错的效果;与其他信息检索领域相比,实体命名评测预料较小,容易产生过拟合;命名实体识别更侧重高召回率,但在信息检索领域,高准确率更重要;通用的识别多种类型的命名实体的系统性能很差。

总结一下就是从语句中提取出关键名词

0.2 深度学习方法在 NER 中的应用

NER 一直是 NLP 领域中的研究热点,从早期基于词典和规则的方法,到传统机器学习的方法,到近年来基于深度学习的方法,NER 研究进展的大概趋势大致如下图所示。

『深度应用』NLP命名实体识别(NER)开源实战教程图 1:NER 发展趋势

在基于机器学习的方法中,NER 被当作序列标注问题。利用大规模语料来学习出标注模型,从而对句子的各个位置进行标注。**NER 任务中的常用模型包括生成式模型 HMM、判别式模型 CRF 等。**条件随机场(ConditionalRandom Field,CRF)是 NER 目前的主流模型。它的目标函数不仅考虑输入的状态特征函数,而且还包含了标签转移特征函数。在训练时可以使用 SGD 学习模型参数。在已知模型时,给输入序列求预测输出序列即求使目标函数最大化的最优序列,是一个动态规划问题,可以使用 Viterbi 算法解码来得到最优标签序列。CRF 的优点在于其为一个位置进行标注的过程中可以利用丰富的内部及上下文特征信息。『深度应用』NLP命名实体识别(NER)开源实战教程图 2:一种线性链条件随机场

近年来,随着硬件计算能力的发展以及词的分布式表示(word embedding)的提出,神经网络可以有效处理许多 NLP 任务。这类方法对于序列标注任务(如 CWS、POS、NER)的处理方式是类似的:将 token 从离散 one-hot 表示映射到低维空间中成为稠密的 embedding,随后将句子的 embedding 序列输入到 RNN 中,用神经网络自动提取特征,Softmax 来预测每个 token 的标签。

这种方法使得模型的训练成为一个端到端的过程,而非传统的 pipeline,不依赖于特征工程,是一种数据驱动的方法,但网络种类繁多、对参数设置依赖大,模型可解释性差。此外,这种方法的一个缺点是对每个 token 打标签的过程是独立的进行,不能直接利用上文已经预测的标签(只能靠隐含状态传递上文信息),进而导致预测出的标签序列可能是无效的,例如标签 I-PER 后面是不可能紧跟着 B-PER 的,但 Softmax 不会利用到这个信息。

学界提出了 DL-CRF 模型做序列标注。在神经网络的输出层接入 CRF 层 (重点是利用标签转移概率) 来做句子级别的标签预测,使得标注过程不再是对各个 token 独立分类。

0.2.1 BiLSTM-CRF(RNN base)

LongShort Term Memory 网络一般叫做 LSTM,是 RNN 的一种特殊类型,可以学习长距离依赖信息。LSTM 由 Hochreiter &Schmidhuber (1997) 提出,并在近期被 Alex Graves 进行了改良和推广。在很多问题上,LSTM 都取得了相当巨大的成功,并得到了广泛的使用。LSTM 通过巧妙的设计来解决长距离依赖问题。
所有 RNN 都具有一种重复神经网络单元的链式形式。在标准的 RNN 中,这个重复的单元只有一个非常简单的结构,例如一个 tanh 层。

『深度应用』NLP命名实体识别(NER)开源实战教程图 3:传统 RNN 结构

LSTM 同样是这样的结构,但是重复的单元拥有一个不同的结构。不同于普通 RNN 单元,这里是有四个,以一种非常特殊的方式进行交互。

『深度应用』NLP命名实体识别(NER)开源实战教程图 4:LSTM 结构

LSTM 通过三个门结构(输入门,遗忘门,输出门),选择性地遗忘部分历史信息,加入部分当前输入信息,最终整合到当前状态并产生输出状态。

『深度应用』NLP命名实体识别(NER)开源实战教程图 5:LSTM 各个门控结构

应用于 NER 中的 biLSTM-CRF 模型主要由 Embedding 层(主要有词向量,字向量以及一些额外特征),双向 LSTM 层,以及最后的 CRF 层构成。**实验结果表明 biLSTM-CRF 已经达到或者超过了基于丰富特征的 CRF 模型,成为目前基于深度学习的 NER 方法中的最主流模型。**在特征方面,该模型继承了深度学习方法的优势,无需特征工程,使用词向量以及字符向量就可以达到很好的效果,如果有高质量的词典特征,能够进一步获得提高。

『深度应用』NLP命名实体识别(NER)开源实战教程图 6:biLSTM-CRF 结构示意图

0.2.2 IDCNN-CRF(CNN base)

对于序列标注来讲,普通 CNN 有一个不足,就是卷积之后,末层神经元可能只是得到了原始输入数据中一小块的信息。**而对 NER 来讲,整个输入句子中每个字都有可能对当前位置的标注产生影响,即所谓的长距离依赖问题。**为了覆盖到全部的输入信息就需要加入更多的卷积层,导致层数越来越深,参数越来越多。而为了防止过拟合又要加入更多的 Dropout 之类的正则化,带来更多的超参数,整个模型变得庞大且难以训练。因为 CNN 这样的劣势,对于大部分序列标注问题人们还是选择 biLSTM 之类的网络结构,尽可能利用网络的记忆力记住全句的信息来对当前字做标注。

但这又带来另外一个问题,biLSTM 本质是一个序列模型,在对 GPU 并行计算的利用上不如 CNN 那么强大。如何能够像 CNN 那样给 GPU 提供一个火力全开的战场,而又像 LSTM 这样用简单的结构记住尽可能多的输入信息呢?

Fisher Yu and Vladlen Koltun 2015 提出了 dilated CNN 模型,意思是 “膨胀的”CNN。其想法并不复杂:正常 CNN 的 filter,都是作用在输入矩阵一片连续的区域上,不断 sliding 做卷积。dilated CNN 为这个 filter 增加了一个 dilation width,作用在输入矩阵的时候,会 skip 所有 dilation width 中间的输入数据;而 filter 本身的大小保持不变,这样 filter 获取到了更广阔的输入矩阵上的数据,看上去就像是“膨胀” 了一般。

具体使用时,dilated width 会随着层数的增加而指数增加。这样随着层数的增加,参数数量是线性增加的,而 receptive field 却是指数增加的,可以很快覆盖到全部的输入数据。『深度应用』NLP命名实体识别(NER)开源实战教程图 7:idcnn 示意图

图 7 中可见感受域是以指数速率扩大的。原始感受域是位于中心点的 1×1 区域:

(a)图中经由原始感受域按步长为 1 向外扩散,得到 8 个 1×1 的区域构成新的感受域,大小为 3×3;

(b)图中经过步长为 2 的扩散,上一步 3×3 的感受域扩展为为 7×7;

(c)图中经步长为 4 的扩散,原 7×7 的感受域扩大为 15×15 的感受域。每一层的参数数量是相互独立的。感受域呈指数扩大,但参数数量呈线性增加。

对应在文本上,输入是一个一维的向量,每个元素是一个 character embedding:

『深度应用』NLP命名实体识别(NER)开源实战教程图 8:一个最大膨胀步长为 4 的 idcnn 块

IDCNN 对输入句子的每一个字生成一个 logits,这里就和 biLSTM 模型输出 logits 完全一样,加入 CRF 层,用 Viterbi 算法解码出标注结果。

CNN base 方法利用空洞卷积 + 多层的方式实现提取整句的功能,同时也能实现并行计算加速(相较于 RNN,CNN 与 RNN 速度对比区别可以参考我之间博文,CNN RNN 并行理解)。

在 biLSTM 或者 IDCNN 这样的网络模型末端接上 CRF 层是序列标注的一个很常见的方法。biLSTM 或者 IDCNN 计算出的是每个词的各标签概率,而 CRF 层引入序列的转移概率,最终计算出 loss 反馈回网络。

现在就剩一个问题了:什么是 CRF 层?为什么要用?

0.2.3 CRF 层讲解

接下来,简明介绍一下该模型。
示意图如下所示:

  • 首先,句子 xxx 中的每个单词表达成一个向量,该向量包含了上述的 word embedding 和 character embedding,其中 character embedding 随机初始化,word embedding 通常采用预训练模型初始化。所有的 embeddings 将在训练过程中进行微调。
  • 其次,BiLSTM-CRF 模型的的输入是上述的 embeddings,输出是该句子 xxx 中每个单词的预测标签。

『深度应用』NLP命名实体识别(NER)开源实战教程

尽管,我们讲的是 CRF 层,不必了解 BiLSTM 层的细节,但是为了便于了解 CRF 层,我们必须知道 BiLSTM 层输出的意义。

『深度应用』NLP命名实体识别(NER)开源实战教程

从上图可以看出,BiLSTM 层的输出是每个标签的得分,如单词 w0w_0w0​,BiLSTM 的输出为 1.5(B-Person),0.9(I-Person),0.1(B-Organization), 0.08 (I-Organization) and 0.05 (O),这些得分就是 CRF 层的输入。
将 BiLSTM 层预测的得分喂进 CRF 层,具有最高得分的标签序列将是模型预测的最好结果。

如果没有 CRF 层将如何?

根据上文,能够发现,如果没有 CRF 层,即我们用下图所示训练 BiLSTM 命名实体识别模型:

『深度应用』NLP命名实体识别(NER)开源实战教程

因为 BiLSTM 针对每个单词的输出是标签得分,对于每个单词,我们可以选择最高得分的标签作为预测结果。
例如,对于 w0w_0w0​,“B-Person”得分最高(1.5),因此我们可以选择 “B-Person” 最为其预测标签;同样的,w1w_1w1​的标签为”I-Person”,w2w_2w2​的为 “O”, w3w_3w3​的标签为 “B-Organization”,w4w_4w4​的标签为 “O”。
按照上述方法,对于 xxx 虽然我们得到了正确的标签,但是大多数情况下是不能获得正确标签的,例如下图的例子:

『深度应用』NLP命名实体识别(NER)开源实战教程

显然,输出标签 “I-Organization I-Person” 和 “B-Organization I-Person” 是不对的。

CRF 能够从训练数据中学习到约束条件

CRF 层可以对最终的约束标签添加一些约束条件,从而保证预测标签的有效性。而这些约束条件是 CRF 层自动从训练数据中学到。
约束可能是:

  • 一句话中第一个单词的标签应该是 “B-“ or “O”,而不能是 “I-“;
  • “B-label1 I-label2 I-label3 I-…”中,label1, label2, label3 … 应该是相同的命名实体标签。如 “B-Person I-Person” 是有效的,而“B-Person I-Organization” 是无效的;
  • “O I-label” 是无效的。一个命名实体的第一个标签应该以 “B-“ 开头,而不能以 “I-“开头,换句话说, 应该是“O B-label” 这种模式;

有了这些约束条件,无效的预测标签序列将急剧减少。

CRF 层就是加了约束使得输出更加符合要求,同时也增加算法成本,有些类似束搜索的功能,下面我们看一看 CRF 层具体如何工作的。

逐帧 softmax #

CRF 主要用于序列标注问题,可以简单理解为是给序列中的每一帧都进行分类,既然是分类,很自然想到将这个序列用 CNN 或者 RNN 进行编码后,接一个全连接层用 softmax 激活,如下图所示

『深度应用』NLP命名实体识别(NER)开源实战教程

逐帧 softmax 并没有直接考虑输出的上下文关联

条件随机场 #

然而,当我们设计标签时,比如用 s、b、m、e 的 4 个标签来做字标注法的分词,目标输出序列本身会带有一些上下文关联,比如 s 后面就不能接 m 和 e,等等。逐标签 softmax 并没有考虑这种输出层面的上下文关联,所以它意味着把这些关联放到了编码层面,希望模型能自己学到这些内容,但有时候会 “强模型所难”。

而 CRF 则更直接一点,它将输出层面的关联分离了出来,这使得模型在学习上更为 “从容”:

『深度应用』NLP命名实体识别(NER)开源实战教程

CRF 在输出端显式地考虑了上下文关联

数学 #

当然,如果仅仅是引入输出的关联,还不仅仅是 CRF 的全部,CRF 的真正精巧的地方,是它以路径为单位,考虑的是路径的概率

模型概要 #

假如一个输入有 nn 帧,每一帧的标签有 kk 种可能性,那么理论上就有 knkn 中不同的输出。我们可以将它用如下的网络图进行简单的可视化。在下图中,每个点代表一个标签的可能性,点之间的连线表示标签之间的关联,而每一种标注结果,都对应着图上的一条完整的路径。

『深度应用』NLP命名实体识别(NER)开源实战教程

4tag 分词模型中输出网络图

而在序列标注任务中,我们的正确答案是一般是唯一的。比如 “今天天气不错”,如果对应的分词结果是 “今天 / 天气 / 不 / 错”,那么目标输出序列就是 bebess,除此之外别的路径都不符合要求。换言之,在序列标注任务中,我们的研究的基本单位应该是路径,我们要做的事情,是从 knkn 条路径选出正确的一条,那就意味着,如果将它视为一个分类问题,那么将是 knkn 类中选一类的分类问题!

总结一下:CRF 作用可以优化输出实体之间的关联

2. 编程实战

2.1 概述

  • 该实战项目参考博文
  • 该项目使用了 conll2003_v2 数据集,其中标注的命名实体共计九类:
['O', 'B-LOC', 'B-PER', 'B-ORG', 'I-PER', 'I-ORG', 'B-MISC', 'I-LOC', 'I-MISC']

实现了将输入识别为命名实体的模型,如下所示:

['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

2.2 数据预处理

数据下载并解压,以供训练, 地址 files.deeppavlov.ai/deeppavlov_…

下载解压后可以看到三个文件:test.txt,train.txt,valid.txt

打开后可以看到,数据格式如下:我们只需要每行开头和最后一个数据,他们分别是文本信息和命名实体。

数据读取与预处理

我们需要将数据进行处理,使之成为网络能接收的形式。

读取数据,并测试输出

def read(self, data_path):
        data_parts = ['train', 'valid', 'test']
for data_part in tqdm(data_parts):
            file_path = data_path + data_part + extension
            dataset[data_part] = self.read_file(str(file_path))
def read_file(self, file_path):
        fileobj = open(file_path, 'r', encoding='utf-8')
            content = content.strip('\n')
if content == '-DOCSTART- -X- -X- O':
                    samples.append((tokens, tags))
                contents = content.split(' ')
                tokens.append(contents[0])
                tags.append(contents[-1])
if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    data1 = ds_rd.read("./conll2003_v2/")
for sample in data1['train'][:2]:
for token, tag in zip(*sample):
            print('%s\t%s' % (token, tag))

输出结果

(['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'], ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O'])
(['Peter', 'Blackburn'], ['B-PER', 'I-PER'])

可以看出数据已经经过整理,每一句保存为了两个 list,一个是单词 list,另一个是标注 list

但是这里还有两个问题:1. 网络无法处理单词级别数据,我们需要准换成数值表示 2. 每个句子长度不同,无法统一训练,需要归一化

对于问题 1. 我们可以通过转换为字典的方式来数值化。

    w_all_dict,n_all_dict = {},{}
for token, tag in zip(*sample):
if token not in w_all_dict.keys():
if tag not in n_all_dict.keys():
    sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)
    sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)
    w_keys = [x for x,_ in sort_w_list[:15999]]
    n_keys = [ x for x,_ in sort_n_list]
    w_dict = { x:i for i,x in enumerate(w_keys) }
    n_dict = { x:i for i,x in enumerate(n_keys) }
if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    data1 = ds_rd.read("./conll2003_v2/")
    w_dict,n_dict = get_dicts(data1["train"])
    print(len(w_dict),n_dict)

测试输出结果

8000 {'O': 0, 'B-LOC': 1, 'B-PER': 2, 'B-ORG': 3, 'I-PER': 4, 'I-ORG': 5, 'B-MISC': 6, 'I-LOC': 7, 'I-MISC': 8}

我们保留前 15999 个常用的单词,新增了一个 “UNK” 代表未知单词。

下面我们就要将利用这些字典把单词给替换为数值

def w2num(datas,w_dict,n_dict):
        num_w_list,num_n_list = [],[]
for token, tag in zip(*sample):
if token not in w_dict.keys():
            num_w_list.append(w_dict[token])
            num_n_list.append(n_dict[tag])        ret_datas.append((num_w_list,num_n_list,len(num_n_list)))
if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    w_dict,n_dict = get_dicts(dataset["train"])
    data_num["train"] = w2num(dataset["train"],w_dict,n_dict)
    print(data_num["train"][:4])
    print(dataset["train"][:4])

测试输出结果,已经实现了数值化的要求。为了方便统计句子长度,每个元祖最后一位保存为了句子长度。

[([957, 11983, 233, 762, 6, 4147, 209, 6182, 1], [3, 0, 6, 0, 0, 0, 6, 0, 0], 9), ([732, 2068], [2, 4], 2), ([1379, 134], [1, 0], 2), ([18, 226, 455, 13, 12, 66, 35, 8127, 24, 233, 4148, 6, 2476, 6, 11984, 209, 6182, 407, 3542, 2069, 499, 1789, 1920, 651, 287, 39, 8128, 6, 1921, 1], [0, 3, 5, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 30)]
[(['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'], ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']), (['Peter', 'Blackburn'], ['B-PER', 'I-PER']), (['BRUSSELS', '1996-08-22'], ['B-LOC', 'O']), (['The', 'European', 'Commission', 'said', 'on', 'Thursday', 'it', 'disagreed', 'with', 'German', 'advice', 'to', 'consumers', 'to', 'shun', 'British', 'lamb', 'until', 'scientists', 'determine', 'whether', 'mad', 'cow', 'disease', 'can', 'be', 'transmitted', 'to', 'sheep', '.'], ['O', 'B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'O', 'O', 'B-MISC', 'O', 'O', 'O', 'O', 'O', 'B-MISC', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]

我们输出句子长度的统计,发现最大值 113,最小值为 1,为了方便统一训练,我们归一化长度为 80

data_num["train"] = w2num(dataset["train"],w_dict,n_dict)
w_lens = [data[-1] for data in  data_num["train"]]
print(max(w_lens),min(w_lens))

句子长度归一化操作,这里采用 padding 为 0,就是当做 “UNK” 与“O”来用,其实也可以使用 Mask 方法等

def len_norm(data_num,lens=80):
for sample1 in list(data_num):
            sample[0] = sample[0][:lens]
            sample[1] = sample[1][:lens]
        ret_datas.append(sample[:2])
if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    w_dict,n_dict = get_dicts(dataset["train"])
    data_num["train"] = w2num(dataset["train"],w_dict,n_dict)
    data_norm["train"] = len_norm(data_num["train"])
    print(data_norm["train"][:4])

测试输出结果为

[[[957, 11983, 233, 762, 6, 4147, 209, 6182, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [3, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[732, 2068, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[1379, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[18, 226, 455, 13, 12, 66, 35, 8127, 24, 233, 4148, 6, 2476, 6, 11984, 209, 6182, 407, 3542, 2069, 499, 1789, 1920, 651, 287, 39, 8128, 6, 1921, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 3, 5, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]]

2.3 模型搭建

模型搭建采用了 BiRNN 方法,具体的说是 BiLSTM,为了方便讲解,采用的是 RNN+Softmax 方式,没有用 CRF,后面有时间我会更新一个 CRF 的版本。网络结构如下:

『深度应用』NLP命名实体识别(NER)开源实战教程

模型搭建代码

from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.optimizers  import *
def build_model(num_classes=9):
    model.add(Embedding(16000, 256, input_length=80))    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(num_classes, activation='softmax'))

输出模型结构

Layer (type) Output Shape Param =================================================================
embedding (Embedding)        (None, 80, 256)           4096000
_________________________________________________________________
bidirectional (Bidirectional (None, 80, 256)           394240
_________________________________________________________________
bidirectional_1 (Bidirection (None, 80, 256) 394240 _________________________________________________________________
dense (Dense)                (None, 80, 128)           32896
_________________________________________________________________
dense_1 (Dense) (None, 80, 9) 1161 ================================================================= Trainable params: 4,918,537 _________________________________________________________________ 

2.4 模型训练

if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    w_dict,n_dict = get_dicts(dataset["train"])
    data_num["train"] = w2num(dataset["train"],w_dict,n_dict)
    data_norm["train"] = len_norm(data_num["train"])    model.compile(loss="sparse_categorical_crossentropy",optimizer=opt)
    train_data = np.array(data_norm["train"])
    train_x = train_data[:,0,:]
    train_y = train_data[:,1,:]       model.fit(x=train_x,y=train_y,epochs=10,batch_size=200,verbose=1,validation_split=0.1)

训练 10 个 epoch,MX150GPU 耗时五分钟,可以发现 train_loss 与 val_loss 都在下降

12636/12636 [==============================] - 68s 5ms/sample - loss: 0.3199 - val_loss: 0.1359
12636/12636 [==============================] - 58s 5ms/sample - loss: 0.1274 - val_loss: 0.1201
12636/12636 [==============================] - 63s 5ms/sample - loss: 0.1099 - val_loss: 0.0957
12636/12636 [==============================] - 58s 5ms/sample - loss: 0.0681 - val_loss: 0.0601
12636/12636 [==============================] - 63s 5ms/sample - loss: 0.0372 - val_loss: 0.0498

2.5 模型应用

最终训练 10 个 epoch

if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    w_dict,n_dict = get_dicts(dataset["train"])
    data_num["train"] = w2num(dataset["train"],w_dict,n_dict)
    data_norm["train"] = len_norm(data_num["train"])    model.compile(loss="sparse_categorical_crossentropy",optimizer=opt)
    train_data = np.array(data_norm["train"])
    train_x = train_data[:,0,:]
    train_y = train_data[:,1,:]        model.fit(x=train_x,y=train_y,epochs=10,batch_size=200,verbose=1,validation_split=0.1)
        model.load_weights("model.h5")
        pre_y = model.predict(train_x[:4])
        pre_y = np.argmax(pre_y,axis=-1)
for i in range(0,len(train_y[0:4])):
            print("label "+str(i),train_y[i])
            print("pred "+str(i),pre_y[i])

测试输出结果,可以发现,预测前四个训练集数据达到不错的效果。

label 0 [3 0 6 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0pred 0 [3 0 6 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0label 1 [2 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0pred 1 [2 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0label 2 [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0pred 2 [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0label 3 [0 3 5 0 0 0 0 0 0 6 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0pred 3 [0 3 5 0 0 0 0 0 0 6 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

3. 总结 & 待续

为了简化,本文只用了 RNN+Softmax 方法进行了训练集测试,可以改进地方还有很多,例如加入 CRF,使用 Mask 方法,三个数据集都用到等,后面有时间就会进行更新。也欢迎大家一起交流,共同改进。

-1. 参考

  1. www.jiqizhixin.com/articles/20…
  2. blog.csdn.net/suan2014/ar…
  3. spaces.ac.cn/archives/55…
  4. blog.csdn.net/chinateleco…

今天的文章『深度应用』NLP命名实体识别(NER)开源实战教程分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15108.html

(0)
编程小号编程小号

相关推荐

发表回复

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