NER系列:CRF条件随机场原理简介,深入理解CRF源码实现
ztj100 2024-11-14 19:24 18 浏览 0 评论
关键词:CRF,条件随机场,序列标注,命名实体识别
内容摘要
- NER任务简介
- NER中引入CRF的目的
- CRF中的学习参数
- CRF的损失函数
- CRF PyTorch源码解析
NER任务简介
命名实体识别(Named Entity Recognition,NER),是指识别文本中有特定意义的实体的边界和类别,所指的有意义实体包括人名、地名、机构名,或者某种具有特定业务含义的词组等,NER的目标是识别出这些特定词组在文本中的位置,并且给该位置下的词组所属的业务类别。
NER属于序列标注任务,即将输入文本看作分词序列,对序列的每一个元素位置进行分类预测,预测每个元素所属的类别。一般的采用BIO标注作为学习目标,其中B作为实体的词首,I作为实体词中或者结尾,剩余的非命名实体元素全部置为O,在BIO后面可以加入任意业务分类,例如以下句子包含需要识别的人名(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>到词首类别形成一个开始转移向量,用来学习该类别作为词首的合理性,词尾类别到<end>形成一个结束转移向量,用来学习该类别作为词尾的合理性。令一共存在k个BIO类别,CRF层一共需要学习1个转移得分矩阵,和2个转移得分向量,包含k×k+2k个模型参数。
CRF的损失函数
前文提到Bert+CRF作为NER的一般范式,Bert学习每个词在该位置上对应的实体类别的可能性,也叫发射得分矩阵,发射得分矩阵再输入给CRF融入转移得分矩阵一起联合训练,模型采用CRF层的损失来训练迭代发射得分矩阵与转移得分矩阵。
CRF层的损失聚焦实体类别的完整路径,期望那条唯一正确的标记路径在所有可能的路径下得分最高,且占比越大越好最大,我们称之为归一化路径得分。设所有类别的排列组合会生成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):
# 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]
# TODO 对应的发射概率也和mask相乘,score的形状一直维护在(batch_size,),每条样本之后自己做+=
score += emissions[i, torch.arange(batch_size), tags[i]] * mask[i]
# TODO 每条样本的真实长度-1,-1是因为索引从0开始否则越界
seq_ends = mask.long().sum(dim=0) - 1
# TODO 拿到每个句子最后一个字所对应的金标准y
last_tags = tags[seq_ends, torch.arange(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维进行聚合,我们知道第1维是前词,第2维是后词,对前词进行聚合相加代表后词的类别不动,所有前词不同的情况相加,示意图如红色线条所示
至于为什么是logsumexp而不是sum,举个简单的例子,路径a有a1,a2两种可能,路径b有b1,b2两种可能,从P(y|x)原始公式出发则全部路径得分计算如下
它等价于使用logsumexp先对a和b进行聚合,log和e可以约掉
聚合后得到新的位置的综合发射得分,给到下一个位置使用,而需要注意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的损失计算部分介绍完毕。
相关推荐
- 从IDEA开始,迈进GO语言之门(idea got)
-
前言笔者在学习GO语言编程的时候,GO语言在国内还没有像JAVA/Php/Python那样普及,绕了不少的弯路,要开始入门学习一门编程语言,最好就先从选择一个好的编程语言的开发环境开始,有了这个开发环...
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于SpringBoot...
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目描述贸易管理进销存spring...
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
-
本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍SpringBoot+V...
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
-
帮粉丝找了一个基于SpringBoot的天猫商城项目,快速部署运行,所用技术:MySQL,Druid,Log4j2,Maven,Echarts,Bootstrap...免费给大家分享出来前台演示...
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
-
今天为大家带来的是基于SpringBoot+JPA+Thymeleaf框架的手机商城管理系统,商城系统分为前台和后台、前台用的是Bootstrap框架后台用的是SpringBoot+JPA都是现在主...
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
-
又是一年一度的“金九银十”秋招大热门,为助力广大程序员朋友“面试造火箭”,小编今天给大家分享的便是这份马士兵内部的面试神技——1658页《Java面试突击核心讲》!...
- SpringBoot数据库操作的应用(springboot与数据库交互)
-
1.JDBC+HikariDataSource...
- SpringBoot 整合 Flink 实时同步 MySQL
-
1、需求在Flink发布SpringBoot打包的jar包能够实时同步MySQL表,做到原表进行新增、修改、删除的时候目标表都能对应同步。...
- SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享
-
后端技术栈基于SpringBoot+Mybatis+Shiro+mysql+redis构建的智慧云智能教育平台基于数据驱动视图的理念封装element-ui,即使没有vue的使...
- Springboot+Mysql舞蹈课程在线预约系统源码附带视频运行教程
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的Springboot+Mysql舞蹈课程在线预约系统,系统项目源代码在【猿来入此】获取!https://www.yuan...
- SpringBoot+Mysql在线众筹系统源码+讲解视频+开发文档(参考论文
-
今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的在线众筹管理系统,主要实现了普通用户在线参与众筹基本操作流程的全部功能,系统分普通用户、超级管理员等角色,除基础脚手架外...
- Docker一键部署 SpringBoot 应用的方法,贼快贼好用
-
这两天发现个Gradle插件,支持一键打包、推送Docker镜像。今天我们来讲讲这个插件,希望对大家有所帮助!GradleDockerPlugin简介...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 从IDEA开始,迈进GO语言之门(idea got)
- 基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql
- 基于springboot的个人服装管理系统java网上商城jsp源代码mysql
- 基于springboot的美食网站Java食品销售jsp源代码Mysql
- 贸易管理进销存springboot云管货管账分析java jsp源代码mysql
- SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql
- 目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗
- SpringBoot+Mysql实现的手机商城附带源码演示导入视频
- 全网首发!马士兵内部共享—1658页《Java面试突击核心讲》
- SpringBoot数据库操作的应用(springboot与数据库交互)
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)