条件随机场(CRF)是自然语言处理中的基础模型, 广泛用于分词, 实体识别和词性标注等场景. 随着深度学习的普及, BILSTM+CRF, BERT+CRF, TRANSFORMER+CRF等模型, 逐步亮相, 并在这些标注场景, 效果有显著的提升.
随机过程:
设 T 是一无限实数集, 把依赖于参数t ∈ T 的一族(无限多个、相互有关)随机变量称为随机过程, 记为 X ( t ) , t ∈ T
随机场: 从平面(随机过程)到向量空间(随机场)
若T 是n 维空间的某个子集, 即t 是一个n 维向量, 此时随机过程又称为随机场. 常见随机场有: 马尔可夫随机场(MRF), 吉布斯随机场(GRF), 条件随机场(CRF)和高斯随机场.
马尔可夫随机场
马尔可夫随机场是建立在 CRF 之上的抽象。即具有马尔可夫性的随机场。由图 G = (V, E) 表示,其中顶点或节点表示随机变量,边表示这些变量之间的依赖关系。边是无向的,即不含有箭头。
马尔可夫性: P ( Y v ∣ Y w , w ≠ v ) = P ( Y v ∣ Y w , w ∼ v )
1.w∼v表示在图G = ( V , E ) 中与顶点v 有边连接的所有顶点w
2.w ≠ v表示顶点v 以外的所有顶点
3.Y v 与Y w为顶点v与w对应的随机变量
条件随机场:
设X 与Y 是随机变量, P ( Y ∣ X ) 是在给定X的条件下Y 的条件概率分布.
若随机变量Y 构成一个由无向图G = ( V , E ) G=(V, E)G=(V,E)表示的马尔可夫随机场, 即
P ( Y v ∣ X , Y w , w ≠ v ) = P ( Y v ∣ X , Y w , w ∼ v )
对任意顶点v 成立, 则称条件概率分布P ( Y ∣ X )为条件随机场.
只基于Y序列做预测, 太单调了, 所以额外给出一个观测序列X, 帮助你更好的做决策. 这就是从马尔可夫随机场变成条件随机场的过程. 条件随机场中, "条件"指的是给定观测序列X的情况, 求状态序列Y的概率, 即输出的是条件概率分布. 而"随机场"指的是状态序列Y构成的随机场.
隐马尔可夫算法(HMM)
1、定义
隐马尔可夫算法是对含有未知参数(隐状态)的马尔可夫链进行建模的生成模型,如下图所示:
在隐马尔科夫模型中,包含隐状态和观察状态,隐状态 对于观察者而言是不可见的,而观察状态 对于观察者而言是可见的。隐状态间存在转移概率,隐状态 到对应的观察状态 间存在输出概率。
2、假设
假设隐状态 的状态满足马尔可夫过程, 时刻的状态 的条件分布,仅与其前一个状态 相关,即:
假设观测序列中各个状态仅取决于它所对应的隐状态,即:
3、存在问题
在序列标注问题中,隐状态(标注)不仅和单个观测状态相关,还和观察序列的长度、上下文等信息相关。例如词性标注问题中,一个词被标注为动词还是名词,不仅与它本身以及它前一个词的标注有关,还依赖于上下文中的其他词。
词性标注
非常简单的,就是给一个句子中的每个单词注明词性。比如这句话:“Bob drank coffee at Starbucks”,注明每个单词的词性后是这样的:“Bob (名词) drank(动词) coffee(名词) at(介词) Starbucks(名词)”。
下面,就用条件随机场来解决这个问题。
以上面的话为例,有5个单词,我们将:(名词,动词,名词,介词,名词)作为一个标注序列,称为l,可选的标注序列有很多种,比如l还可以是这样:(名词,动词,动词,介词,名词),我们要在这么多的可选标注序列中,挑选出一个最靠谱的作为我们对这句话的标注。
怎么判断一个标注序列靠谱不靠谱呢?
就我们上面展示的两个标注序列来说,第二个显然不如第一个靠谱,因为它把第二、第三个单词都标注成了动词,动词后面接动词,这在一个句子中通常是说不通的。
假如我们给每一个标注序列打分,打分越高代表这个标注序列越靠谱,我们至少可以说,凡是标注中出现了动词后面还是动词的标注序列,要给它负分!!
上面所说的动词后面还是动词就是一个特征函数,我们可以定义一个特征函数集合,用这个特征函数集合来为一个标注序列打分,并据此选出最靠谱的标注序列。也就是说,每一个特征函数都可以用来为一个标注序列评分,把集合中所有特征函数对同一个标注序列的评分综合起来,就是这个标注序列最终的评分值。
定义CRF中的特征函数
现在,我们正式地定义一下什么是CRF中的特征函数,所谓特征函数,就是这样的函数,它接受四个参数:
句子s(就是我们要标注词性的句子)
i,用来表示句子s中第i个单词
l_i,表示要评分的标注序列给第i个单词标注的词性
l_i-1,表示要评分的标注序列给第i-1个单词标注的词性
它的输出值是0或者1,0表示要评分的标注序列不符合这个特征,1表示要评分的标注序列符合这个特征。
Note:这里,我们的特征函数仅仅依靠当前单词的标签和它前面的单词的标签对标注序列进行评判,这样建立的CRF也叫作线性链CRF,这是CRF中的一种简单情况。为简单起见,本文中我们仅考虑线性链CRF。
从特征函数到概率
定义好一组特征函数后,我们要给每个特征函数f_j赋予一个权重λ_j。现在,只要有一个句子s,有一个标注序列l,我们就可以利用前面定义的特征函数集来对l评分。
上式中有两个求和,外面的求和用来求每一个特征函数f_j评分值的加权和,里面的求和用来求句子中每个位置的单词的的特征值的和。
对这个分数进行指数化和标准化,我们就可以得到标注序列l的概率值p(l|s),如下所示:
这个时候 权重λ 就变为了一个可训练的参数矩阵了,可以采用类似于最大似然方法对其训练
几个特征函数的例子:
前面我们已经举过特征函数的例子,下面我们再看几个具体的例子。
当l_i是“副词”并且第i个单词以“ly”结尾时,我们就让f1 = 1,其他情况f1为0。不难想到,f1特征函数的权重λ1应当是正的。而且λ1越大,表示我们越倾向于采用那些把以“ly”结尾的单词标注为“副词”的标注序列
如果i=1,l_i=动词,并且句子s是以“?”结尾时,f2=1,其他情况f2=0。同样,λ2应当是正的,并且λ2越大,表示我们越倾向于采用那些把问句的第一个单词标注为“动词”的标注序列。
当l_i-1是介词,l_i是名词时,f3 = 1,其他情况f3=0。λ3也应当是正的,并且λ3越大,说明我们越认为介词后面应当跟一个名词。
如果l_i和l_i-1都是介词,那么f4等于1,其他情况f4=0。这里,我们应当可以想到λ4是负的,并且λ4的绝对值越大,表示我们越不认可介词后面还是介词的标注序列。
好了,一个条件随机场就这样建立起来了,让我们总结一下:
为了建一个条件随机场,我们首先要定义一个特征函数集,每个特征函数都以整个句子s,当前位置i,位置i和i-1的标签为输入。然后为每一个特征函数赋予一个权重,然后针对每一个标注序列l,对所有的特征函数加权求和,必要的话,可以把求和的值转化为一个概率值。
CRF与HMM的比较
对于词性标注问题,HMM模型也可以解决。HMM的思路是用生成办法,就是说,在已知要标注的句子s的情况下,去判断生成标注序列l的概率,如下所示:
这里:
p(l_i|l_i-1)是转移概率,比如,l_i-1是介词,l_i是名词,此时的p表示介词后面的词是名词的概率。
p(w_i|l_i)表示发射概率(emission probability),比如l_i是名词,w_i是单词“ball”,此时的p表示在是名词的状态下,是单词“ball”的概率。
那么,HMM和CRF怎么比较呢?
这里的马尔科夫假设于HMM模型的有点不一样,一方面是指数族分布,另一方面是加性;而HMM模型的是乘性,而且不服从指数族分布。
CRF比HMM要强大的多,它可以解决所有HMM能够解决的问题,并且还可以解决许多HMM解决不了的问题。事实上,我们可以对上面的HMM模型取对数,就变成下面这样:
我们把这个式子与CRF的式子进行比较:
不难发现,如果我们把第一个HMM式子中的log形式的概率看做是第二个CRF式子中的特征函数的权重的话,我们会发现,CRF和HMM具有相同的形式。
用一句话来说明HMM和CRF的关系就是这样:
每一个HMM模型都等价于某个CRF。
但是,CRF要比HMM更加强大,原因主要有两点:
CRF可以定义数量更多,种类更丰富的特征函数。HMM模型具有天然具有局部性,就是说,在HMM模型中,当前的单词只依赖于当前的标签,当前的标签只依赖于前一个标签。这样的局部性限制了HMM只能定义相应类型的特征函数,我们在上面也看到了。但是CRF却可以着眼于整个句子s定义更具有全局性的特征函数
CRF可以使用任意的权重将对数HMM模型看做CRF。
而且CRF中,每个特征函数的权重可以是任意值,没有限制。
总结
"""CRF条件随机场算法"""
import torch
import torch.nn as nn
class CRF(nn.Module):
"""
本CRF的基本思想是:
(1) 将编码序列映射到一个似然函数
(2) 计算对数似然项的分子
(3) 计算对数似然项的分母
最终输出的为loss值, 在序列标注任务中利用loss来进行backward.
"""
def __init__(self, num_tags: int) -> None:
super(CRF, self).__init__()
if num_tags <= 0:
raise ValueError(f"The number tags unreasonable.")
# 初始化转移矩阵
self.num_tags = num_tags
self.start_transitions = nn.Parameter(torch.empty(num_tags))
self.end_transitions = nn.Parameter(torch.empty(num_tags))
self.transitions = nn.Parameter(torch.empty(num_tags, num_tags))
self.init_transitions()
def init_transitions(self):
"""
初始化转移矩阵, 让其服从(-0.1, 0.1)的均匀分布
"""
nn.init.uniform_(self.start_transitions, -0.1, 0.1)
nn.init.uniform_(self.end_transitions, -0.1, 0.1)
nn.init.uniform_(self.transitions, -0.1, 0.1)
def forward(self, emission: torch.Tensor, tags: torch.LongTensor,
mask: torch.ByteTensor = None, reduction: str = "sum"):
"""
:param emission: 发射矩阵 shape:[batch_size, seq_len, num_tags]
:param tags: 标签 shape:[batch_size, seq_len]
:param mask: mask矩阵 shape:[batch_size, seq_len]
:param reduction: 输出结果的计算方式 default:(sum, mean, none, token_mean)
:return: 对数似然函数 shape:[batch_size,]
"""
# reduction的计算方式
if reduction not in ('none', 'sum', 'mean', 'token_mean'):
raise ValueError(f'invalid reduction: {reduction}')
# 判断是否自定义mask
if mask is None:
mask = torch.ones_like(tags, dtype=torch.int8)
# shape: [batch_size,]
numerator = self._compute_likelihood_numerator(emission, tags, mask)
# shape: [batch_size,]
denominator = self._compute_likelihood_denominator(emission, mask)
# shape: [batch_size,]
log_likelihood = numerator - denominator
if reduction == 'none':
return log_likelihood
if reduction == 'sum':
return log_likelihood.sum()
if reduction == 'mean':
return log_likelihood.mean()
assert reduction == 'token_mean'
return log_likelihood.sum() / mask.float().sum()
def _compute_likelihood_numerator(self, emission: torch.Tensor, tags: torch.LongTensor,
mask: torch.ByteTensor) -> torch.Tensor:
"""
:param emission: 发射矩阵 shape:[batch_size, seq_len, num_tags]
:param tags: 标签 shape:[batch_size, seq_len]
:param mask: mask矩阵 shape:[batch_size, seq_len]
:return: emission矩阵的得分 shape:[batch_size,]
"""
tags = tags.type(torch.long)
# 对输入的矩阵进行验证
assert emission.dim() == 3 and tags.dim() == 2, "The dimensions of emission and tags were wrong."
assert emission.shape[:2] == tags.shape, "The dimensions of emission and tags were wrong."
assert emission.size(2) == self.num_tags, "The dimensions of emission were wrong."
assert mask.shape == tags.shape, "The dimensions of mask and tags were wrong."
assert mask[0].all()
batch_size, seq_len = tags.shape
mask = mask.float()
# 利用开始转移矩阵和第0维的emission值计算scores
# 这里要注意tags的维度交换
# scores shape:[batch_size, ]
# 取start_transitions的第一个值
scores = self.start_transitions[tags.transpose(0, 1)[0]]
# 将上一步的scores与emission的第1维的第0号矩阵的第一维矩阵的第二维元素(不知道怎么描述, 看下面例子)
"""
emission :tensor([[[0.1271, 0.8776, 0.9388, 0.8530, 0.9732],
[0.4324, 0.7661, 0.0715, 0.7880, 0.2132],
[0.8027, 0.2874, 0.4673, 0.4805, 0.1791],
[0.8975, 0.1386, 0.6488, 0.9324, 0.9506],
[0.6614, 0.1671, 0.8437, 0.8570, 0.2643]]])
取的就是这个矩阵的 0.8776, 然后把0.8776与start_transitions的值相加, 得到下面的scores
"""
scores += emission.transpose(0, 1)[0, torch.arange(batch_size), tags.transpose(0, 1)[0]]
# emission除0维以外的其他维度的值与transitions的相应值相加
for i in range(1, seq_len):
# 将scores转移到下一个标签, 在下一个时间步添加
# shape : [batch_size,]
scores += self.transitions[tags.transpose(0, 1)[i - 1], tags.transpose(0, 1)[i]] * mask.transpose(0, 1)[i]
# shape: [batch_size,]
scores += emission.transpose(0, 1)[i, torch.arange(batch_size), tags.transpose(0, 1)[i]] * \
mask.transpose(0, 1)[i]
# shape: [batch_size,]
seq_ends = mask.transpose(0, 1).long().sum(dim=0) - 1
# shape: [batch_size,]
last_tags = tags.transpose(0, 1)[seq_ends, torch.arange(batch_size)]
# 加入结束转移矩阵计算结束最终的scores
# shape: [batch_size,]
scores += self.end_transitions[last_tags]
return scores
def _compute_likelihood_denominator(
self, emission: torch.Tensor, mask: torch.ByteTensor) -> torch.Tensor:
"""
:param emission: [batch_size, seq_len,num_tags]
:param mask: [batch_size, seq_len]
:return: [batch_size,]
"""
assert emission.dim() == 3 and mask.dim() == 2
assert emission.shape[:2] == mask.shape
assert emission.size(2) == self.num_tags
assert mask[0].all()
seq_length = emission.transpose(0, 1).size(0)
# 计算开始转移矩阵与发射矩阵的和, 对于每个batch中的第j列计算score
# shape: [batch_size, num_tags]
score = self.start_transitions + emission.transpose(0, 1)[0]
for i in range(1, seq_length):
# 计算每个可能成为下一个标签的得分
# shape: [batch_size, num_tags, 1]
broadcast_score = score.unsqueeze(2)
# 计算每个当前标签的emission得分
# shape: [batch_size, 1, num_tags]
broadcast_emissions = emission.transpose(0, 1)[i].unsqueeze(1)
# 计算每个sample的第i行j列的得分之和
# shape: [batch_size, num_tags, num_tags]
next_score = broadcast_score + self.transitions + broadcast_emissions
# 计算当前所有可能标签的得分之和, 利用logsumexp防止溢出
# shape: [batch_size, num_tags]
next_score = torch.logsumexp(next_score, dim=1)
# 如果当前时间步是正确的, 则将scores设置为下一个time step的值
# shape: [batch_size, num_tags]
score = torch.where(mask.transpose(0, 1)[i].unsqueeze(1).byte(), next_score, score)
# 加入结束转移矩阵
# shape: [batch_size, num_tags]
score += self.end_transitions
# 对所有可能的标签进行累加
# shape: [batch_size,]
return torch.logsumexp(score, dim=1)
基于sklearn_crfsuite实现NER
sklearn_crfsuite简介
sklearn-crfsuite是基于CRFsuite库的一款轻量级的CRF库。该库兼容sklearn的算法,因此可以结合sklearn库的算法设计实体识别系统。sklearn-crfsuite不仅提供了条件随机场的训练和预测方法还提供了评测方法。
Python安装:pip install sklearn-crfsuite
模型初始化
crf_model = sklearn_crfsuite.CRF(algorithm='lbfgs',c1=0.25,c2=0.018,max_iterations=100,
all_possible_transitions=True,verbose=True)
crf_model.fit(X_train, y_train)
训练模型
def train(self):
"""训练"""
self.initialize_model()
x, y = self.corpus.generator()
x_train, y_train = x[500:], y[500:]
x_test, y_test = x[:500], y[:500]
self.model.fit(x_train, y_train)
labels = list(self.model.classes_)
labels.remove('O')
y_predict = self.model.predict(x_test)
metrics.flat_f1_score(y_test, y_predict, average='weighted', labels=labels)
sorted_labels = sorted(labels, key=lambda name: (name[1:], name[0]))
print(metrics.flat_classification_report(y_test, y_predict, labels=sorted_labels, digits=3))
self.save_model()
这里的训练集和测试集均使用一个样本集,将数据集一分为二,前500条作为训练集,后500条作为测试集。通过训练集训练模型,再使用测试集预测模型得出y_predict与y_test对比。训练好模型后,将模型保存,测试时加载模型,例如对某句话进行实体识别测试。
(1)将语料全角字符统一转为半角;(在自然语言处理过程中,全角、半角的的不一致会导致信息抽取不一致,因此需要统一。)
全角字符unicode编码从65281~65374 (十六进制 0xFF01 ~ 0xFF5E)
半角字符unicode编码从33~126 (十六进制 0x21~ 0x7E)
(2)合并语料库分开标注的姓和名,例如:温/nr 家宝/nr;
(3)合并语料库中括号中的大粒度词,例如:[国家/n 环保局/n]nt;
(4)合并语料库分开标注的时间,例如:(/w 一九九七年/t 十二月/t 三十一日/t )/w。