命名实体识别(英语:Named Entity Recognition),简称NER,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等,以及时间、数量、货币、比例数值等文字。目前在NER上表现较好的模型都是基于深度学习或者是统计学习的方法的,这些方法共同的特点都是需要大量的数据来进行学习,本文使用的数据集是2018ACL论文中新浪财经收集的简历数据。
数据集链接:https://github.com/jiesutd/LatticeLSTM
标注集采用BIOES(B表示实体开头,E表示实体结尾,I表示在实体内部,O表示非实体,S表示单个实体),句子之间用一个空行隔开。
对于命名实体识别其他方法举例
常用的模型以及涉及到的主要代码
1、隐马尔可夫模型(HMM)
隐马尔可夫模型描述由一个隐藏的马尔科夫链随机生成不可观测的状态随机序列,再由各个状态生成一个观测而产生观测随机序列的过程(李航 统计学习方法)。隐马尔可夫模型由初始状态分布,状态转移概率矩阵以及观测概率矩阵所确定。上面的定义太过学术看不懂没关系,我们只需要知道,NER本质上可以看成是一种序列标注问题(预测每个字的BIOES标记),在使用HMM解决NER这种序列标注问题的时候,我们所能观测到的是字组成的序列(观测序列),观测不到的是每个字对应的标注(状态序列)。对应的,HMM的三个要素可以解释为,初始状态分布就是每一个标注作为句子第一个字的标注的概率,状态转移概率矩阵就是由某一个标注转移到下一个标注的概率(设状态转移矩阵为 ,那么若前一个词的标注为 ,则下一个词的标注为 的概率为 ),观测概率矩阵就是指在某个标注下,生成某个词的概率。根据HMM的三个要素,我们可以定义如下的HMM模型:
class HMM(object):
def __init__(self, N, M):
"""Args:
N: 状态数,这里对应存在的标注的种类
M: 观测数,这里对应有多少不同的字
"""
self.N = N
self.M = M
# 状态转移概率矩阵 A[i][j]表示从i状态转移到j状态的概率
self.A = torch.zeros(N, N)
# 观测概率矩阵, B[i][j]表示i状态下生成j观测的概率
self.B = torch.zeros(N, M)
# 初始状态概率 Pi[i]表示初始时刻为状态i的概率
self.Pi = torch.zeros(N)
有了模型定义,接下来的问题就是训练模型了。HMM模型的训练过程对应隐马尔可夫模型的学习问题(李航 统计学习方法),实际上就是根据训练数据根据最大似然的方法估计模型的三个要素,即上文提到的初始状态分布、状态转移概率矩阵以及观测概率矩阵。举个例子帮助理解,在估计初始状态分布的时候,假如某个标记在数据集中作为句子第一个字的标记的次数为k,句子的总数为N,那么该标记作为句子第一个字的概率可以近似估计为k/N
,很简单对吧,使用这种方法,我们近似估计HMM的三个要素,代码如下(出现过的函数将用省略号代替):
class HMM(object):
def __init__(self, N, M):
....
def train(self, word_lists, tag_lists, word2id, tag2id):
"""HMM的训练,即根据训练语料对模型参数进行估计,
因为我们有观测序列以及其对应的状态序列,所以我们
可以使用极大似然估计的方法来估计隐马尔可夫模型的参数
参数:
word_lists: 列表,其中每个元素由字组成的列表,如 ['担','任','科','员']
tag_lists: 列表,其中每个元素是由对应的标注组成的列表,如 ['O','O','B-TITLE', 'E-TITLE']
word2id: 将字映射为ID
tag2id: 字典,将标注映射为ID
"""
assert len(tag_lists) == len(word_lists)
# 估计转移概率矩阵
for tag_list in tag_lists:
seq_len = len(tag_list)
for i in range(seq_len - 1):
current_tagid = tag2id[tag_list[i]]
next_tagid = tag2id[tag_list[i+1]]
self.A[current_tagid][next_tagid] += 1
# 一个重要的问题:如果某元素没有出现过,该位置为0,这在后续的计算中是不允许的
# 解决方法:我们将等于0的概率加上很小的数
self.A[self.A == 0.] = 1e-10
self.A = self.A / self.A.sum(dim=1, keepdim=True)
# 估计观测概率矩阵
for tag_list, word_list in zip(tag_lists, word_lists):
assert len(tag_list) == len(word_list)
for tag, word in zip(tag_list, word_list):
tag_id = tag2id[tag]
word_id = word2id[word]
self.B[tag_id][word_id] += 1
self.B[self.B == 0.] = 1e-10
self.B = self.B / self.B.sum(dim=1, keepdim=True)
# 估计初始状态概率
for tag_list in tag_lists:
init_tagid = tag2id[tag_list[0]]
self.Pi[init_tagid] += 1
self.Pi[self.Pi == 0.] = 1e-10
self.Pi = self.Pi / self.Pi.sum()
模型训练完毕之后,要利用训练好的模型进行解码,就是对给定的模型未见过的句子,求句子中的每个字对应的标注,针对这个解码问题,我们使用的是维特比(viterbi)算法。关于该算法的数学推导,可以查阅一下李航统计学习方法。
HMM存在两个缺陷:1)观察值之间严格独立,观测到的句子中每个字相互独立
2)状态转移过程中当前状态只与前一状态有关,没有关注到后一时刻的状态
HMM代码实现的主要模型部分如下:
import torch
class HMM(object):
def __init__(self, N, M):
"""Args: