提高版:做动态决策和Bi-LSTM CRF
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
动态和静态深度学习工具包
--------------------------------------------
Pytorch是一个动态神经网络工具包。动态工具包的另一个例子是' Dynet
核心差异如下:
*在静态工具包中,一旦定义一个计算图,编译它,然后将实例流到它。
*在动态工具包中,为每个实例定义一个计算图。它从不编译,而是动态执行
没有丰富的经验,很难理解其中的差别。一个例子是假设我们想要构建一个深层的成分解析器。假设我们的模型大致包括以下步骤:
*我们建立自底向上的树
*标记根节点(句子中的单词)
*然后,使用神经网络和单词的嵌入来找到组成成分的组合。每当您形成一个新的组件时,使用某种技术来获得该组件的嵌入。在这种情况下,我们的网络架构将完全依赖于输入语句。在“the green cat scratch the wall”这句话中,在模型的某个点上,我们想要组合跨度(也就是说,一个NP成分跨越单词1到单词3,在本例中是“the green cat”)。
然而,另一个句子可能是“the big fat cat scratch the wall”。在这句话中,我们想要在某个时刻形成成分:我们想要形成的成分将取决于实例。如果我们像在静态工具包中那样只编译一次计算图,那么编写这个逻辑将异常困难或不可能。然而,在动态工具包中,并不只有一个预定义的计算图。每个实例都可以有一个新的计算图,所以这个问题就没有了。
动态工具包还具有易于调试和代码更接近宿主语言的优点(我的意思是Pytorch和Dynet看起来更像实际的Python代码,而不是Keras或Theano)。
Bi-LSTM 条件随机场的讨论
-------------------------------------------
在本节中,我们将看到一个用于命名实体识别的Bi-LSTM条件随机场的完整而复杂的示例。上面的LSTM标记器对于词性标记来说已经足够了,但是像CRF这样的序列模型对于NER上的强大性能是非常必要的。
假设您熟悉CRF。虽然这个名字听起来很吓人,在LSTM提供了这些特性后,模型都是CRF。不过,这是一个高级模型,比本教程中任何较早的模型都要复杂得多。如果你想跳过,没关系。看看你是否准备好了,看看你是否可以:
-在第i步为标记k写出viterbi变量的递归式。
-修改上面的递归式来计算正向变量。
-再次修改上面的递归式,以计算log-space中的正向变量(提示:log-sum-exp)
如果您能够做到这三件事,您应该能够理解下面的代码。还记得CRF计算条件概率吗?
y是标记序列,x是单词的输入序列。
然后我们计算
分数是通过定义一些对数势来确定的
为了使配分函数易于处理,势必须只考虑局部特征。
在双lstm CRF中,我们定义了两种势:发射势和跃迁势。单词 的排放潜力来自于Bi-LSTM在timestep: 时的隐藏状态。转换分数存储在矩阵中
,其中:是标记集。在我的实现中,是标记转换到标记的分数。所以:
在第二个表达式中,我们认为标签被分配了唯一的非负索引。
如果上面的讨论过于简短,您可以查看迈克尔·柯林斯在CRFs上写的“
实现注意事项
--------------------
下面的示例在日志空间中实现了计算配分函数的正向算法,以及解码的维特比算法。反向传播将为我们自动计算梯度。我们不需要手工做任何事情。
实现没有优化。如果您了解发生了什么,您可能很快就会看到,在正向算法中迭代下一个标记可能可以在一个大型操作中完成。我想让代码更具可读性。如果您想进行相关的更改,您可以将此标记符用于实际任务。
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
torch.manual_seed(1)
#####################################################################
# Helper functions to make the code more readable.
def argmax(vec):
# return the argmax as a python int
_, idx = torch.max(vec, 1)
return idx.item()
def prepare_sequence(seq, to_ix):
idxs = [to_ix[w] for w in seq]
return torch.tensor(idxs, dtype=torch.long)
# 以一种数值稳定的方式为正向算法计算log-sum-exp
def log_sum_exp(vec):
max_score = vec[0, argmax(vec)]
max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
return max_score + \
torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))
#####################################################################
# Create model
class BiLSTM_CRF(nn.Module):
def __init__(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):
super(BiLSTM_CRF, self).__init__()
self.embedding_dim = embedding_dim
self.hidden_dim = hidden_dim
self.vocab_size = vocab_size
self.tag_to_ix = tag_to_ix
self.tagset_size = len(tag_to_ix)
self.word_embeds = nn.Embedding(vocab_size, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2,
num_layers=1, bidirectional=True)
# 将LSTM的输出映射到标记空间。
self.hidden2tag = nn.Linear(hidden_dim, self.tagset_size)
# 过渡参数矩阵。条目i,j是从* j转换到* i *的得分。
self.transitions = nn.Parameter(
torch.randn(self.tagset_size, self.tagset_size))
# 这两个语句执行了一个约束,即我们永远不会转移到开始标记,也永远不会转移到停止标记
self.transitions.data[tag_to_ix[START_TAG], :] = -10000
self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000
self.hidden = self.init_hidden()
def init_hidden(self):
return (torch.randn(2, 1, self.hidden_dim // 2),
torch.randn(2, 1, self.hidden_dim // 2))
def _forward_alg(self, feats):
# 用正向算法计算配分函数
init_alphas = torch.full((1, self.tagset_size), -10000.)
# START_TAG拥有所有的分数。
init_alphas[0][self.tag_to_ix[START_TAG]] = 0.
# 在变量中进行包装,这样我们就可以得到自动的backprop
forward_var = init_alphas
# 遍历整个句子
for feat in feats:
alphas_t = [] # The forward tensors at this timestep
for next_tag in range(self.tagset_size):
# 广播发射分数:无论之前的标签是什么,它都是相同的
emit_score = feat[next_tag].view(
1, -1).expand(1, self.tagset_size)
# 第i个条目的trans_score是从i转换到next_tag的得分
trans_score = self.transitions[next_tag].view(1, -1)
# next_tag_var的第i个条目是我们执行log-sum-exp之前的
# edge (i -> next_tag)的值
next_tag_var = forward_var + trans_score + emit_score
# 这个标签的前向变量是所有分数的log-sum-exp。
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
def _get_lstm_features(self, sentence):
self.hidden = self.init_hidden()
embeds = self.word_embeds(sentence).view(len(sentence), 1, -1)
lstm_out, self.hidden = self.lstm(embeds, self.hidden)
lstm_out = lstm_out.view(len(sentence), self.hidden_dim)
lstm_feats = self.hidden2tag(lstm_out)
return lstm_feats
def _score_sentence(self, feats, tags):
# 给出所提供的标记序列的得分
score = torch.zeros(1)
tags = torch.cat([torch.tensor([self.tag_to_ix[START_TAG]], dtype=torch.long), tags])
for i, feat in enumerate(feats):
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
def _viterbi_decode(self, feats):
backpointers = []
# 在日志空间中初始化维特比变量
init_vvars = torch.full((1, self.tagset_size), -10000.)
init_vvars[0][self.tag_to_ix[START_TAG]] = 0
# 第i步的forward_var保存第i-1步的viterbi变量
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]保存上一步标记i的viterbi变量,
# 加上从标记i转换到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))
# 现在添加发射分数,并将forward_var分配给我们刚刚计算的viterbi变量集
forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)
backpointers.append(bptrs_t)
# 过渡到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]
# 遵循反向指针来解码最佳路径。
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)
# 去掉开始标记(我们不想把它返回给调用者)
start = best_path.pop()
assert start == self.tag_to_ix[START_TAG] # Sanity check
best_path.reverse()
return path_score, best_path
def neg_log_likelihood(self, sentence, tags):
feats = self._get_lstm_features(sentence)
forward_score = self._forward_alg(feats)
gold_score = self._score_sentence(feats, tags)
return forward_score - gold_score
def forward(self, sentence): # dont confuse this with _forward_alg above.
# 从BiLSTM获得排放分数
lstm_feats = self._get_lstm_features(sentence)
# 根据这些特性找到最佳路径。
score, tag_seq = self._viterbi_decode(lstm_feats)
return score, tag_seq
#####################################################################
# Run training
START_TAG = ""
STOP_TAG = ""
EMBEDDING_DIM = 5
HIDDEN_DIM = 4
# 编造一些训练数据
training_data = [(
"the wall street journal reported today that apple corporation made money".split(),
"B I I I O O O B I O O".split()
), (
"georgia tech is a university in georgia".split(),
"B I O O O O B".split()
)]
word_to_ix = {}
for sentence, tags in training_data:
for word in sentence:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
tag_to_ix = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}
model = BiLSTM_CRF(len(word_to_ix), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM)
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)
# 训练前检查预测
with torch.no_grad():
precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)
precheck_tags = torch.tensor([tag_to_ix[t] for t in training_data[0][1]], dtype=torch.long)
print(model(precheck_sent))
>>>(tensor(2.6907), [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1])
# 确保加载了LSTM部分前面的prepare_sequence
for epoch in range(300): # 再说一次,通常你不会做300个epoch,这是toy数据
for sentence, tags in training_data:
# 第1步.请记住Pytorch积累梯度。我们需要在每个实例之前清除它们
model.zero_grad()
# 步骤2.让我们的输入为网络做好准备,也就是说,把它们转换成单词索引的张量。
sentence_in = prepare_sequence(sentence, word_to_ix)
targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)
# 步骤3. 正向传播
loss = model.neg_log_likelihood(sentence_in, targets)
# 步骤4. 计算损失, 梯度, 通过optimizer.step()更新参数
loss.backward()
optimizer.step()
# 训练后检查预测
with torch.no_grad():
precheck_sent = prepare_sequence(training_data[0][0], word_to_ix)
print(model(precheck_sent))
>>>(tensor(20.4906), [0, 1, 1, 1, 2, 2, 2, 0, 1, 2, 2])
# We got it!
######################################################################
#练习:用于区分标签的新损失函数
#在进行解码时,我们实际上没有必要创建一个计算图,因为我们不会从维特比路径值反向传播。
#既然我们已经 有了它,试着训练标记器,其中损失函数是维特比路径得分与黄金标准路径得分之间的差值。
#应该清楚的是,这个函数值是非负和0的,当预测的标记序列是正确的标记序列时。
#这本质上是结构化感知器。这个修改应该很短,因为已经实现了Viterbi和score\_sentence。
#这是计算图形形状的一个例子*取决于训练实例*。虽然我还没有尝试在静态工具包中实现这一点,
#但我认为这是可能的,但要简单得多。
#收集一些真实的数据并进行比较!