NER系列:CRF条件随机场原理简介,深入理解CRF源码实现

摘要:CRF条件随机场序列标注命名实体识别

内容摘要

  • NER任务简介
  • NER中引入CRF的目的
  • CRF中的学习参数
  • CRF的损失函数
  • CRF PyTorch源码解析

NER任务简介

命名实体识别(Named Entity Recognition,NER),是指识别文本中有特定意义的实体边界类别,所指的实体包括人名、地名、机构名,或者某种具有特定业务含义的词组等,NER的目标是识别出这些特定词组在文本中的位置,并且给该位置下的词组标记出正确的实体业务含义类别。
NER属于序列标注任务,即将输入文本分词后转化为序列,对序列的每一个元素位置进行分类预测,预测该元素所属的类别。一般的NER使用BIO标注作为学习目标,其中B作为实体的词首,I作为实体词中或者结尾,剩余的非命名实体元素词全部置为O,对于多种实体类别使用任意字符串加在B,I后面进行表示,例如以下句子包含一个需要识别的人名(PER)和机构组织名(ORG),用BIO标记如下:

>>> "小王在前进小学上小学"
>>> ['B-PER', 'I-PER', 'O', 'B-ORG', 'I-ORG', 'I-ORG', 'I-ORG', 'O', 'O', 'O']

在BIO标注之后将各标记类型转化为数值类型,就可使用任意网络和Softmax交叉熵进行损失迭代,即训练一个网络来表征句子,从而捕捉输入的句子和目标标签的关系


NER中引入CRF的目的

从前文来看只需要引入一个网络对句子进行表征,再结合Softmax交叉熵学习对应的实体类别即可解决NER问题,如下图每个位置的字词单独预测所属的实体类别

一般网络只能逐位拟合字词到实体类别的关系

这种方式忽略了实体类别内部前后元素之间的关联信息,不同于一般分类模型输出的是一个值,NER输出的是一组值形成的序列,NER中加入CRF的目的是为了学习前后两个连续标注结果的关系,将这种y值的前后迁移关系也加入网络一起进行训练,使得整体网络不仅能够拟合字词和实体类别的关系,也能适配实体类别前后的排列规律。

带有学习标签上下文关联的网络

由此引出本文介绍的条件随机场(Conditinal random field, CRF),它用来刻画序列y中上下文的关联,这种仅和y内部元素有关,和x无关,而一般的分类网络例如Bert,它仅对句子x到标签的y进行逐位拟合,难以学习到y的上下文关联,一般采用Bert+CRF两者联合训练作为NER任务的Baseline。


CRF中的学习参数

CRF只关心相邻两个类别标签之间的关系,目的是刻画类别A后面紧接着是类别B的可能性大小,我们期望模型能够学习到B-PER后面大概率紧随着I-PER,而B-PER后面不可能是I-ORG,这种模式可以使用得分矩阵来表达

B-PER I-PER B-ORG I-ORG
B-PER 0.2 5 -1 -1
I-PER 0.8 4 0.3 -2
B-ORG -0.5 -2 0.1 7
I-ORG 0.6 -3 0.3 3

表格中横轴表示前词类别情况,纵轴表示后词的类别情况,每个交叉格子中的值代表转移得分,即从前词类别后面紧随后词类别的转移得分,转移得分越大这种类别前后搭配的可能性越大,越符合人类的语言习惯,CRF的学习参数就是这个转移矩阵,以BIO标注为例,如果要做PER,ORG这两类的实体识别,则一共有B-PER,I-PER,B-ORG,I-ORG,O这5类标签,转移矩阵是一个5×5的方阵。
转移矩阵能够刻画前后两个词的情况,而原始句子中的第一个词没有前词,最后一个词没有紧随的词,因此在句子的开头和结尾分别加上一个<start>,<end>的占位符,<start>到词1类别形成一个开始转移矩阵,用来学习该类别作为词首的合理性,词n类别到<end>形成一个结束转移矩阵,用来学习该类别作为词尾的合理性。还是以PER,ORG这两类实体任务为例,开始转移矩阵为长度是5的向量,结束转移矩阵也是长度为5的向量,令一共存在k个BIO类别,CRF层一共需要学习1个转移得分矩阵2个转移得分向量,包含k×k+2k个模型参数。

CRF需要学习的转移得分

CRF的损失函数

前文提到Bert+CRF作为NER的一般范式,Bert学习每个词在该位置上对应的实体类别的可能性,也叫发射得分矩阵,发射得分矩阵再输入给CRF融入转移得分矩阵一起联合训练,模型采用CRF层的损失来训练Bert层的发射得分矩阵与CRF层的转移得分矩阵。
CRF层的损失聚焦实体类别的完整路径,期望n那条唯一正确的标记路径在所有可能的路径下得分最高,占比最大,我们称之为归一化路径得分。设所有类别的排列组合会生成n条路径y_n,其中正确的标记路径为y_true,则

目标路径占比所有路径得分

其中S是一条路径的最终得分,它等于该路径各位置下发射矩阵和转移矩阵得分的累加,其中T表示转移矩阵,E表示发射矩阵。

单条路径得分

P(y|x)是一个0~1之间的数,越大代表标记正确的类别路径在所有路径中得分越高,则模型识别实体越准,因此引入负的对数似然构成CRF层的损失函数使其最小化

负的对数似然损失

该公式中第二项Syn是所有路径的得分总和,它采用递归的方式计算,即逐位记录下每个位置以各个标签作为终点的得分,再以此得分作为下一个位置的发射得分,在下文源码解析中对该部分做详细介绍。

想比于Softmax交叉熵损失关注的是每个位置的分类对错,CRF的损失关注的是整个序列的合理性,它是由每个位置分类对错和分类上下文对错综合考量而来的。令句子长度为n,实体类别数为k,前者将序列标注看成是n个k分类问题,后者将序列标注看成是1个k*n分类问题。


PyTorch源码解析

采用torchcrf实现的CRF做源码解析,它采用PyTorch框架封装了CRF的损失和维特比求解的过程,本文只涉及CRF的损失部分。可以通过pip很方便的安装torchcrf

pip install torchcrf
1.定义CRF参数

作者在构造器中定义了CRF的三个转移矩阵作为模型的学习参数

class CRF(nn.Module):
 def __init__(self, num_tags: int, batch_first: bool = False) -> None:
        if num_tags <= 0:
            raise ValueError(f'invalid number of tags: {num_tags}')
        super().__init__()
        # TODO {"O": 0, "B-PER": 1, "I-PER": 2, "B-ORG": 3, "I-ORG": 4, "B-LOC": 5, "I-LOC": 6}
        self.num_tags = num_tags
        self.batch_first = batch_first  # False
        # TODO 增加start到下一个的转移矩阵 
        self.start_transitions = nn.Parameter(torch.empty(num_tags))
        # TODO 增加最后一个字到end的转移矩阵 
        self.end_transitions = nn.Parameter(torch.empty(num_tags))
        # TODO 除start和end之外的各tag之间的转移矩阵
        self.transitions = nn.Parameter(torch.empty(num_tags, num_tags))
        # 三大转移矩阵初始化,全部-0.1~0.1均匀分布
        self.reset_parameters()

其中start_transitions,end_transitions,transitions是转移矩阵,令类别数量为k,则start_transitions,end_transitions是k维向量,transitions是k×k的矩阵。

2.计算正确路径的得分

在初始化模块,torchcrf会自己维护转移矩阵,而在forward前向传播部分,需要额外输入前层网络输出的发射矩阵,根据发射矩阵emissions,转移矩阵,正确的标签tags计算出正确路径的得分,numerator形成了P(y|x)的分子

numerator = self._compute_score(emissions, tags, mask)

计算过程和注解如下

        # TODO 金标准第一个元素,调用对应的转移矩阵
        # TODO 拿到从开始到第一个字的转移概率
        # TODO [batch_size] 计算每条样本第一个位置的得分(start => token的转移得分)
        score = self.start_transitions[tags[0]]
        # emissions: (seq_length, batch_size, num_tags)
        # TODO 该批次下所有第一个字的发射概率 => [batch_size, ]
        # TODO torch.arange(batch_size)是横坐标,tags[0]是纵坐标,通过横纵坐标把该批次下每个样本和金标准符合的位置的值拿出来作为发射概率
        score += emissions[0, torch.arange(batch_size), tags[0]]

        # TODO 下一个位置的值只和上一个位置的值的转移矩阵和该位置的发射矩阵有关
        for i in range(1, seq_length):
            # shape: (batch_size,)
            # TODO 字和字之间的转移矩阵登场,这个转移矩阵只和label有关,tags[i - 1], tags[i]指定了横纵坐标
            # TODO mask和转移概率对应位置相乘element-wise
            # TODO tags[i - 1]是前一个位置tag的全部横坐标,tags[i]是后一个位置tag的全部纵坐标,拿到对应的转移得分
            # TODO 和mask对应位置相乘,如果某条样本该位置值是0,则该条样本的路径已经结束,后续所有不计入计算得分
            score += self.transitions[tags[i - 1], tags[i]] * mask[i]
            # shape: (batch_size,)
            # TODO 对应的发射概率也和mask相乘,score的形状一直维护在(batch_size,),每条样本之后自己做+=
            score += emissions[i, torch.arange(batch_size), tags[i]] * mask[i]

        # shape: (batch_size,)
        # TODO 每条样本的真实长度-1,-1是因为索引从0开始否则越界
        seq_ends = mask.long().sum(dim=0) - 1
        # shape: (batch_size,)
        # TODO 拿到每个句子最后一个字所对应的金标准y
        last_tags = tags[seq_ends, torch.arange(batch_size)]
        # shape: (batch_size,)
        # TODO 加上最后的 =>end 的转移概率
        score += self.end_transitions[last_tags]

注意所有计算以一个batch为单位进行,一个batch下的所有样本并行计算,互不干扰,从句首开始分别调用对应的转移矩阵和发射矩阵,通过横纵的索引位置拿到对应的转移得分和发射得分,一直相加到句尾,最终形成一个batch size长度的得分向量,代表该批次下每条样本的正确路径的得分。

3.计算所有路径的得分

根据发射矩阵emissions,转移矩阵来计算所有路径得分,在计算所有路径得分的时候并不需要标签y,denominator形成了P(y|x)的分母

denominator = self._compute_normalizer(emissions, mask)

重点对以下三行做讲解

# TODO 笛卡尔积相加,broadcast_score为前分,dim=1, broadcast_emissions为后分,dim=2
next_score = broadcast_score + self.transitions + broadcast_emissions
# TODO 将所有结尾tag相同的得分相加
next_score = torch.logsumexp(next_score, dim=1)
# TODO 如果mask=1,则分数更新,否则分数不变
score = torch.where(mask[i].unsqueeze(1), next_score, score)

全部路径的得分和计算正确标签单个路径得分是一样的,一个批次下从句首开始逐位相加到句尾,其中broadcast_score为该位置之前的路径的整体在每个标签下的发射得分,broadcast_emissions为该位置的发射得分,而transitions构成了前后位置之间的转移得分,broadcast_score和broadcast_emissions形成笛卡尔积交叉的全部相加,相当于穷举了下一个位置的转移和发射的全部可能得分情况,累加到之前路径上,示意图如下

计算所有路径得分的累加过程

由于next_score是一个(batch_size, num_tags, num_tags)的矩阵,下一步需要对结果进行聚合,将所有结尾相同类别的路径做求和合并,形成新的每个类别为发射得分的情况在递归计算,作者采用logsumexp第1维进行聚合,我们知道第一维是前词,第二维是后词,对前词进行聚合相加代表后词的类别不动,所有前词不同的情况相加,示意图如红色线条所示

对后词类别相同的路径做聚合

至于为什么是logsumexp而不是sum,举个简单的例子,路径a有a1,a2两种可能,路径b有b1,b2两种可能,从P(y|x)原始公式出发则全部路径得分计算如下

全部路径得分计算1

他等价于使用logsumexp先对a和b进行聚合,log和e可以约掉

全部路径得分计算2

聚合后得到新的位置的综合发射得分,给到下一个位置使用,而需要注意mask=1,则分数更新,否则分数不变,mask=0代表该批次下这个句子已经结束,计算句子得分不需要再增长。
这个递归过程一直持续到该批次下所有样本达到句尾,最终得到了每条样本以各个分类标签为结尾的得分,若标签分类数有k种,则输出(batch_size, k)的二维矩阵,紧接着我们将各标签结尾的情况对应的得分再做一次汇总即可得到最终所有路径的得分,_compute_normalizer函数最终的返回如下

return torch.logsumexp(score, dim=1)
4.计算归一化正确路径的得分

在求得分子和分母之后,作者直接套用公式,经过化简即可得到最终的归一化得分

numerator = self._compute_score(emissions, tags, mask)
denominator = self._compute_normalizer(emissions, mask)
# TODO llh => Log likelihood, 两个log相减,指数相除、
llh = numerator - denominator

此处llh指的是对数似然,最终forward前向传播返回CRF层的损失函数。

正确路径归一化得分的对数似然

因此在计算损失时需要对返回取负号。到此torchcrf中关于CRF的损失计算部分介绍完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容