DL | 序列标注模型-BiLSTM+CRF机理概述



1.序列标注模型简介

序列标注问题包括自然语言处理中的分词,词性标注,命名实体识别,关键词抽取,词义角色标注等等。

例如,命名实体识别(NER)的标注问题就是:对长度为N的输入序列,对其中的每个元素打上标签,得到长度也为N的label,例如人名、地点等标签。



2.BiLSTM+CRF模型流程

2.1 为什么用BiLSTM+CRF模型

CRF是非常经典的序列标注模型,深度学习发展起来之后,深度学习+CRF的模型得到广泛应用。其中的代表就是BiLSTM+CRF。双向LSTM能更好的捕捉序列中上下文的信息,提高标注的准确性。

2.2 一种典型结构

2.2.1 数据预处理

字符串在输入模型之前已经做了数值化处理:一般是根据字符串在字典中的编号,将字符串转化为一个整数数组。

例如 “中华人民共和国”,根据每个字符在字典中的位置,查到这7个字对应的整数值,得到它的数值化结果,例如是[1,2,3,4,5,6,7]

2.2.2 典型结构

还可以在下述结构中添加 dropout等层,此处略去。
在这里插入图片描述


3. 几个关键问题

3.1 BiLSTM的运行机制

一般的LSTM,假设隐层神经元数目为100个,将“中国人”的embedding按次序输入LSTM,一共3个time_step,得到3个向量,它们记为[L0,L1,L2],分别对应“中”、“国”、“人”。

把“中国人”的embedding按倒序输入LSTM,也得到3个time_step的3个向量,记为[R0,R1,R2]。

将同一个字符对应的前向和后向两个向量拼接起来,就得到BiLSTM层的输出,即为:“中”对应[L0,R2],“国”对应[L1,R1],"人"对应[L2,R0]。

更详细的解释请参考 详解BiLSTM及代码实现


3.2 CRF loss的计算方法

3.2.1 标记序列的score

对于一个输入序列,对于给定的一个标记序列label,它的得分定义为:

S=EmissionScore+TransitionScore
————————————————————————————————
在这里插入图片描述简言之,EmissionScore是BilSTM给这个标记的打分(BiLSTM的输出中,每个字符对应其标签的位置)的和。例如BiLSTM输出的维度为[4,6,12],那么每个字符对应12种标签各有一个score,emit_score是指真实标签对应的那个score。

————————————————————————————————
在这里插入图片描述简言之,TransitionScore就是该序列状态转移矩阵中对应的和(序列的i位置为A,i+1序列为B,这之间对应一个transition score,可以理解成概率)。

3.2.2 CRF loss计算公式

给定一个输入序列,它的标记序列可能有很多。模型的目的是:使真实的序列的score在所有可能的序列的score和的占比最高。
在这里插入图片描述

(1)直接求解 P_RealPath
给定BiLSTM的输出emit_score、CRF的Transition矩阵、一个标签序列,可以根据3.2.1的计算方法计算该序列的score,从而得出P_RealPathde=exp(score)。

(2)动态规划求解 P_1+P_2+…+P_N
这个计算的困难在于,所有可能的path可能太多了(所有可能的标记序列太多了)。
解决办法是可以用动态规划来求解。详细过程请参考 The total score of all the paths

3.2.3 代码实现

以下代码摘自 bilstm_crf.py

    def cal_loss(self, tags, mask, emit_score):
        """ Calculate CRF loss
        Args:
            tags (tensor): a batch of tags, shape (b, len)
            mask (tensor): mask for the tags, shape (b, len), values in PAD position is 0
            emit_score (tensor): emit matrix, shape (b, len, K)
        Returns:
            loss (tensor): loss of the batch, shape (b,)
        """
        batch_size, sent_len = tags.shape
        # calculate score for the tags
        score = torch.gather(emit_score, dim=2, index=tags.unsqueeze(dim=2)).squeeze(dim=2)  # shape: (b, len)
        score[:, 1:] += self.transition[tags[:, :-1], tags[:, 1:]]
        #### total_score为P_realPath
        total_score = (score * mask.type(torch.float)).sum(dim=1)  # shape: (b,)
        # calculate the scaling factor
        d = torch.unsqueeze(emit_score[:, 0], dim=1)  # shape: (b, 1, K)
        for i in range(1, sent_len):
            n_unfinished = mask[:, i].sum()
            d_uf = d[: n_unfinished]  # shape: (uf, 1, K)
            emit_and_transition = emit_score[: n_unfinished, i].unsqueeze(dim=1) + self.transition  # shape: (uf, K, K)
            log_sum = d_uf.transpose(1, 2) + emit_and_transition  # shape: (uf, K, K)
            max_v = log_sum.max(dim=1)[0].unsqueeze(dim=1)  # shape: (uf, 1, K)
            log_sum = log_sum - max_v  # shape: (uf, K, K)
            d_uf = max_v + torch.logsumexp(log_sum, dim=1).unsqueeze(dim=1)  # shape: (uf, 1, K)
            d = torch.cat((d_uf, d[n_unfinished:]), dim=0)
        d = d.squeeze(dim=1)  # shape: (b, K)
        max_d = d.max(dim=-1)[0]  # shape: (b,)
        #### 用动态规划求得 P_1+P_2+...+P_N
        d = max_d + torch.logsumexp(d - max_d.unsqueeze(dim=1), dim=1)  # shape: (b,)
        llk = total_score - d  # shape: (b,)
        loss = -llk  # shape: (b,)
        return loss

值得一提的是:上述代码中,出现了两次减去最大值,再计算 logsumexp。这是为了防止向上溢出,原理上理解时可以忽略。

3.2.4 得到loss之后,可以更新模型参数


3.3 新样本的infer过程

3.3.1 思路

训练好模型之后,对于一个新的输入,需要给它打标签,用的是基于动态规划的维特比算法。
详细过程请参考 Infer the labels for a new sentence

3.3.2 代码实现

以下代码摘自bilstm_crf.py

    def predict(self, sentences, sen_lengths):
        """
        Args:
            sentences (tensor): sentences, shape (b, len). Lengths are in decreasing order, len is the length
                                of the longest sentence
            sen_lengths (list): sentence lengths
        Returns:
            tags (list[list[str]]): predicted tags for the batch
        """
        batch_size = sentences.shape[0]
        mask = (sentences != self.sent_vocab[self.sent_vocab.PAD])  # shape: (b, len)
        sentences = sentences.transpose(0, 1)  # shape: (len, b)
        sentences = self.embedding(sentences)  # shape: (len, b, e)
        emit_score = self.encode(sentences, sen_lengths)  # shape: (b, len, K)
        tags = [[[i] for i in range(len(self.tag_vocab))]] * batch_size  # list, shape: (b, K, 1)
        d = torch.unsqueeze(emit_score[:, 0], dim=1)  # shape: (b, 1, K)
        for i in range(1, sen_lengths[0]):
            n_unfinished = mask[:, i].sum()
            d_uf = d[: n_unfinished]  # shape: (uf, 1, K)
            emit_and_transition = self.transition + emit_score[: n_unfinished, i].unsqueeze(dim=1)  # shape: (uf, K, K)
            new_d_uf = d_uf.transpose(1, 2) + emit_and_transition  # shape: (uf, K, K)
            d_uf, max_idx = torch.max(new_d_uf, dim=1)
            max_idx = max_idx.tolist()  # list, shape: (nf, K)
            tags[: n_unfinished] = [[tags[b][k] + [j] for j, k in enumerate(max_idx[b])] for b in range(n_unfinished)]
            d = torch.cat((torch.unsqueeze(d_uf, dim=1), d[n_unfinished:]), dim=0)  # shape: (b, 1, K)
        d = d.squeeze(dim=1)  # shape: (b, K)
        _, max_idx = torch.max(d, dim=1)  # shape: (b,)
        max_idx = max_idx.tolist()
        tags = [tags[b][k] for b, k in enumerate(max_idx)]
        return tags
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值