dann的alpha torch_pytorch BiLSTM+CRF代码详解
一. BILSTM + CRF介绍
1.介绍
基于神经网络的方法在自然语言处理领域中表现出了显著的应用价值,在命名实体识别任务中被广泛采用。 如果你对Bi-LSTM和CRF的概念感到陌生,记住它们分别是命名实体识别模型中的两种核心组件。
1.1开始之前
我们假定我们的数据集中分为两个实体类别——人名和地名,在其对应于我们的训练数据集中的有五个类别标记:
B-Person, I- Person,B-Organization,I-Organization
w1,w2
w1,w2
1.2BiLSTM-CRF模型
以下将给出模型的结构:
第一,在句子x中每个单元都代表了由字符或单词组成的向量表示。其中字符嵌入采用随机初始化的方式进行设置而单词(词)嵌入则是通过数据训练获得的具体数值表示。这些所有的表示形式在训练过程中都会被优化至最佳状态以实现模型的任务目标
第二, 这些字或词被编码为BiLSTM-CRF模型的一部分, 输出的是句子x中每个单元对应的预测结果.
Bi-LSTM结构图
通常无需深入了解BiLSTM的工作原理;然而,在理解CRF运行机制时会有所帮助。
图2.Bi-LSTM标签预测原理图
如下图所示,在该BiLSTM层中各标签对应的预测分数由其输出结果给出。具体而言,在单元w0的位置。
1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization) 0.05 (O).
这些分值将作为CRF的输入。
1.3 如果没有CRF层会怎样
您可能已经意识到,在无需CRF层的情况下(即不采用CRF层),我们依然能够训练出一个性能卓越的BiLSTM命名实体识别模型(如图所示)。
图3.去除CRF的BiLSTM命名实体识别模型
基于BiLSTM模型的输出结果是每个单元对应的所有标签的概率得分分布情况,在实际应用中我们需要根据这些概率值来确定每个单元对应的最优标签。具体来说,在处理某一个特定单位(比如w₀)时(或者考虑w₀的情况),我们可以通过选择得分最高的标签来确定该单元的最佳预测结果;例如,在w₀的情况下,“B-Person”显示出最高概率值为1.5;同样地,在分析后续如w₁、“I-Person”、“O”等单位时,“B-Organization”的概率值也达到了较高的水平
然而,在获得句子x中每个单元的真实标签方面,并非总是如此;即使我们能够获得这些真实标签也无法保证每次预测都能准确无误。例如,在图4所示的例子中其标注序列包括‘I-Organization I-Person’和‘B-Organization I-Person’这显然是不正确的。
然而,在获得句子x中每个单元的真实标签方面,并非总是如此;即使我们能够获得这些真实标签也无法保证每次预测都能准确无误。例如,在图4所示的例子中其标注序列包括'I-Organization I-Person'和'B-Organization I-Person'这显然是不正确的。
在这里插入图片描述
1.4 CRF层能从训练数据中获得约束性的规则
该模型通过CRF层对最终预测的标签施加约束条件以确保预测结果的有效性。在模型训练过程中,这些约束条件将由CRF层自动识别并纳入模型优化。
这些约束可以是:
I:句子中第一个词总是以标签“B-“ 或 “O”开始,而不是“I-”
II:标记" B - label1 I - label2 I - label3 I - … " 中 的 label1 、 label2 、 label3 应归为同一类别实体 。例如 , " B - Person I - Person " 是合法 的 标记序列 , 而 " B - Person I - Organization " 则是非法 标记序列 。
第三部分:在III部分中提到的 tag sequence「O I-label」是无效的形式。实体标记的第一个标记应为「B-」而不是「I-」。换句话说,正确的形式应为 tag 序列为「O B-tag」。
有了这些约束,标签序列预测中非法序列出现的概率将会大大降低。
二. 标签的score和损失函数的定义
Bi-LSTM层的输出向量空间维数对应于标签集合的数量。每个词wi会被模型预测其对应的标签的概率分布。在CRF模型中,我们假设存在一个转移矩阵A,则其元素Ai,j表示从状态i转移到状态j的概率。
对于输入序列 X 对应的输出tag序列 y,定义分数为
在这里插入图片描述
通过Softmax函数, 我们赋予每个正确的 tag 序列 y 一个概率值(其中 Y_X 代表所有可能的 tag 序列集合, 并包含那些理论上可能出现但实际未出现的情况)
在这里插入图片描述
因此,在模型训练过程中,我们寻求最大值的是条件概率p(y|X),即为通过采用对数似然方法来实现这一目标。
在这里插入图片描述
基于此,在此我们将其损失函数被定义为-log(p(y|X))时,则能够使得网络通过梯度下降法来进行学习。
loss function:
在这里插入图片描述
在对损失函数进行计算的时候,S(X,y)的计算很简单,而
在这里插入图片描述(记作logsumexp)的过程稍微繁琐一些,因为它需要对所有可能路径进行分数计算.这里采用了更为简便的方式:对于到达词wi+1的所有路径而言,在计算时先将到达词wi的logsumexp值进行计算.
在这里插入图片描述
由此可见,在任何情况下都采用这种方法不仅能够准确地先计算每一步的路径分数而且还能与直接计算全局分数保持一致的效果
三. 对于损失函数的详细解释
这篇文章对于理解十分有用
我 爱 中国人民
我 爱 中国人民
名词-虚词-名词
接下来我想讲的是这个公式:
在这里插入图片描述
这个项很容易被确认成立,通过手动计算可以验证其正确性,在代码实现中主要采用了这一项的理论基础.
def _forward_alg(self, feats):
Do the forward algorithm to compute the partition function
init_alphas = torch.full((1, self.tagset_size), -10000.)
START_TAG has all of the score.
init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
Wrap in a variable so that we will get automatic backprop
forward_var = init_alphas
Iterate through the sentence
for feat in feats:
alphas_t = [] # The forward tensors at this timestep
for next_tag in range(self.tagset_size):
broadcast the emission score: it is the same regardless of
the previous tag
emit_score = feat[next_tag].view(
1, -1).expand(1, self.tagset_size)
the ith entry of trans_score is the score of transitioning to
next_tag from i
trans_score = self.transitions[next_tag].view(1, -1)
The ith entry of next_tag_var is the value for the
edge (i -> next_tag) before we do log-sum-exp
next_tag_var = forward_var + trans_score + emit_score
The forward variable for this tag is log-sum-exp of all the
scores.
alphas_t.append(log_sum_exp(next_tag_var).view(1))
forward_var = torch.cat(alphas_t).view(1, -1)
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
alpha = log_sum_exp(terminal_var)
return alpha
我们看到有这么一段代码 :
next_tag_var = forward_var + trans_score + emit_score
我们主要就是来讲讲他。
我 爱 中华人民
我 爱 中华人民
{I} 爱 {中华} 人民
在这里插入图片描述
意思是说,在这个句子上进行所有可能标注的情况下计算每个标注对应的分数值。具体操作方法是将这些分数值按照指数次幂的方式累加起来之后再对总和进行一次对数值计算以获得最终结果。通常来说,在处理所有可能的标注情况时会遇到一定的复杂性特别是在实际应用中所涉及的情况可能会远超简单的长度为三的情况因此我们需要寻找一种更为简便高效的计算方法以保证整个系统的运行效率。也就是在我们的程序中采用这一算法其计算流程如下:首先遍历每一个可能存在的标注组合对于每一个组合分别计算出对应的分数值然后将这些分数值按照指数次幂的方式累加最后对总和进行一次自然对数值转换得到最终结果
我, 爱
我, 爱
中国人民
中国人民
我,爱,中国人民
接下来我们来验证一下是不是这样
首先我们假设词性一共只有两种 名词N 和 动词 V
那么【我,爱】得词性组合一共有四种 N + N,N + V, V + N, V + V
那么【爱】标注为N时得log_sum_exp 为
在这里插入图片描述
【爱】 标注为 V时的 log_sum_exp为
在这里插入图片描述
我们的forward列表里就是存在着这两个值,即:
在这里插入图片描述
中华人民
中华人民
中华人民
在这里插入图片描述
在这里插入图片描述
四. 代码块详细说明:
先说明两个重要的矩阵:
特征发射分数(emit score)是在句子经过嵌入层处理后,在经过LSTM层计算得到的一个矩阵(即该矩阵由LSTM层输出生成),其尺寸为11×5(其中11代表输入句子的长度单位数和5为标签数量)。该矩阵反映了通过LSTM处理后的每个单词与各个标签之间的得分为
self.transitions代表转移矩阵,
其维度为15×15,
transitions[i][j]代表从label j转移到label i的概率。
transition[i]维度为15×15,
代表每个label转移到label i的概率。
即整个转移矩阵是一个概率矩阵。
1. def log_sum_exp(vec)
compute log sum exp in numerically stable way for the forward algorithm
def log_sum_exp(vec): #vec是1*5, type是Variable
max_score = vec[0, argmax(vec)]
max_score维度是1, max_score.view(1,-1)维度是1*1,
max_score.view(1, -1).expand(1, vec.size()[1])的维度是1*5
max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
里面先做减法,减去最大值可以避免e的指数次,计算机上溢
return max_score + \
torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
当我们遇到return函数的结果时可能会有这样的疑问:为什么会首先减去max_score?其实在这种情况下这是一种常见的做法:由于直接进行指数运算可能导致计算结果溢出。具体来说,在模型训练初期阶段会进行这样的处理:将每个得分都减去最大的那个分数(即max_score),然后对处理后的得分进行log_sum_exp操作以避免数值下溢或上溢的问题。完成这一操作后,在得到最终的概率分布之前需要将被减去的最大分数(max_score)重新加回去以恢复正确的概率值。
其实就等同于:
return torch.log(torch.sum(torch.exp(vec)))
2. def neg_log_likelihood(self, sentence, tags)
如果你认真阅读全部代码后, 你会认识到neg_log_likelihood()这个函数被定义为loss function.
loss = model.neg_log_likelihood(sentence_in, targets)
我们来分析一下neg_log_likelihood()函数代码:
def neg_log_likelihood(self, sentence, tags):
feats: 11*5 经过了LSTM+Linear矩阵后的输出,之后作为CRF的输入。
feats = self._get_lstm_features(sentence)
forward_score = self._forward_alg(feats)
gold_score = self._score_sentence(feats, tags)
return forward_score - gold_score
您在此处可能会产生疑问:为何forward_score - gold_score可以被用作loss?
这里,我们回顾一下我们在上文中说明的loss function函数公式:
在这里插入图片描述
你就会发现forward_score和gold_score分别表示上述等式右边的两项。
3. def _forward_alg(self, feats):
通过对前一阶段函数的深入研究和分析可以得出结论:当前所讨论的函数实际上被设计用于计算forward_score这一重要指标。即损失函数等式右侧的第一个组成部分是forward_score相关的计算模块。
在这里插入图片描述
预测序列的得分
只是根据随机的transitions,前向传播算出的一个score
基于动态规划的方法被采用。然而由于采用了随机转移矩阵进行计算得到的结果超过20
def _forward_alg(self, feats):
do the forward algorithm to compute the partition function
init_alphas = torch.full((1, self.tagset_size), -10000.) # 1*5 而且全是-10000
START_TAG has all of the score
因为start tag是4,所以tensor([[-10000., -10000., -10000., 0., -10000.]]),
将start的值为零,表示开始进行网络的传播,
init_alphas[0][self.tag_to_ix[START_TAG]] = 0
warp in a variable so that we will get automatic backprop
forward_var = init_alphas # 初始状态的forward_var,随着step t变化
iterate through the sentence
会迭代feats的行数次
for feat in feats: #feat的维度是5 依次把每一行取出来~
alphas_t = [] # the forward tensors at this timestep
for next_tag in range(self.tagset_size): #next tag 就是简单 i,从0到len
broadcast the emission(发射) score:
it is the same regardless of the previous tag
维度是1*5 LSTM生成的矩阵是emit score
emit_score = feat[next_tag].view(
1, -1).expand(1, self.tagset_size)
the i_th entry of trans_score is the score of transitioning
to next_tag from i
trans_score = self.transitions[next_tag].view(1, -1) # 维度是1*5
The ith entry of next_tag_var is the value for the
edge (i -> next_tag) before we do log-sum-exp
第一次迭代时理解:
trans_score所有其他标签到B标签的概率
由LSTM经过隐层后再至输出层得到标签B的概率其score维度为1×5这五个得分完全相同
next_tag_var = forward_var + trans_score + emit_score
The forward variable for this tag is log-sum-exp of all the scores
alphas_t.append(log_sum_exp(next_tag_var).view(1))
此时的alphas t 是一个长度为5,例如:
[tensor(0.8259), tensor(2.1739), tensor(1.3526), tensor(-9999.7168), tensor(-0.7102)]
forward_var = torch.cat(alphas_t).view(1, -1) #到第(t-1)step时5个标签的各自分数
最后只将最后一个单词的forward var与转移 stop tag的概率相加
tensor([[ 21.1036, 18.8673, 20.7906, -9982.2734, -9980.3135]])
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
alpha = log_sum_exp(terminal_var) # alpha是一个0维的tensor
return alpha
4. def _score_sentence(self, feats, tags)
根据2号函数进行分析的结果表明,在该模型中所使用的损失函数...对应于黄金标准指标(gold_score)及其计算公式中的第二项
根据真实的标签算出的一个score,
这与上面的def _forward_alg(self, feats)共同之处在于:
两者都是用的随机转移矩阵算的score
不同之处在于,在这个函数中虽然计算出了一个最大可能性路径(即所有标签之间的转移概率的最大值),但实际上这个结果与真实各个标签之间的转移概率并不完全一致
例如:真实的标签配置为N、V、V的形式;然而由于transitions的随机性,在这种情况下;上述函数实际上会生成N、N、N这样的结果
两者的score出现了差异。随后的反向传播过程能够使transitions得以更新,并最终使转移矩阵逼近
#真实的“转移矩阵”
得到gold_seq tag的score 即根据真实的label 来计算一个score,
但是因为转移矩阵是随机生成的,故算出来的score不是最理想的值
def _score_sentence(self, feats, tags): #feats 11*5 tag 11 维
gives the score of a provied tag sequence
score = torch.zeros(1)
将START_TAG的标签3拼接到tag序列最前面,这样tag就是12个了
tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])
for i, feat in enumerate(feats):
self.transitions[tags[i + 1], tags[i]] 实际上代表了从标签i+1指向标签i的概率分布
feat[tags[i+1]], feat是step i 的输出结果,有5个值,
对应B, I, E, START_TAG, END_TAG, 取对应标签的值
transition【j,i】 就是从i ->j 的转移概率值
score = score + \
self.transitions[tags[i+1], tags[i]] + feat[tags[i + 1]]
score = score + self.transitions[self.tag_to_ix[STOP_TAG], tags[-1]]
return score
5. def _viterbi_decode(self, feats):
维特比解码, 实际上就是在预测的时候使用了, 输出得分与路径值
预测序列的得分
def _viterbi_decode(self, feats):
backpointers = []
initialize the viterbi variables in long space
init_vvars = torch.full((1, self.tagset_size), -10000.)
init_vvars[0][self.tag_to_ix[START_TAG]] = 0
forward_var at step i holds the viterbi variables for step i-1
forward_var = init_vvars
for feat in feats:
bptrs_t = [] # holds the backpointers for this step
viterbivars_t = [] # holds the viterbi variables for this step
for next_tag in range(self.tagset_size):
next-tag_var[i] holds the viterbi variable for tag i
at the previous step, plus the score of transitioning
from tag i to next_tag.
we don't include the emission scores here because the max
does not depend on them(we add them in below)
其他标签(B,I,E,Start,End)到标签next_tag的概率
next_tag_var = forward_var + self.transitions[next_tag]
best_tag_id = argmax(next_tag_var)
bptrs_t.append(best_tag_id)
viterbivars_t.append(next_tag_var[0][best_tag_id].view(1))
now add in the emssion scores, and assign forward_var to the set
of viterbi variables we just computed
从step0到step(i-1)时5个序列中每个序列的最大score
forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
backpointers.append(bptrs_t) # bptrs_t有5个元素
transition to STOP_TAG
其他标签到STOP_TAG的转移概率
terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
best_tag_id = argmax(terminal_var)
path_score = terminal_var[0][best_tag_id]
follow the back pointers to decode the best path
best_path = [best_tag_id]
for bptrs_t in reversed(backpointers):
best_tag_id = bptrs_t[best_tag_id]
best_path.append(best_tag_id)
pop off the start tag
we don't want to return that ti the caller
start = best_path.pop()
assert start == self.tag_to_ix[START_TAG] # Sanity check
best_path.reverse() # 把从后向前的路径正过来
return path_score, best_path
如果对于该函数还没有太理解,可以参考这篇博客:
总结
以上就是我结合了几篇比较不错的博客后的总结,欢迎大家提问。
