基于PyTorch的深度学习入门教程_nlp_advanced_tutorial

提高版:做动态决策和Bi-LSTM CRF
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

动态和静态深度学习工具包
--------------------------------------------

Pytorch是一个动态神经网络工具包。动态工具包的另一个例子是' Dynet  ' (我之所以提到这一点,是因为使用Pytorch和Dynet是类似的。如果您在Dynet中看到一个示例,它可能会帮助您在Pytorch中实现它)。相反的是“静态”工具包,包括Theano、Keras、TensorFlow等。
核心差异如下:

*在静态工具包中,一旦定义一个计算图,编译它,然后将实例流到它。
*在动态工具包中,为每个实例定义一个计算图。它从不编译,而是动态执行

没有丰富的经验,很难理解其中的差别。一个例子是假设我们想要构建一个深层的成分解析器。假设我们的模型大致包括以下步骤:

*我们建立自底向上的树
*标记根节点(句子中的单词)
*然后,使用神经网络和单词的嵌入来找到组成成分的组合。每当您形成一个新的组件时,使用某种技术来获得该组件的嵌入。在这种情况下,我们的网络架构将完全依赖于输入语句。在“the green cat scratch the wall”这句话中,在模型的某个点上,我们想要组合跨度(i,j,r) = (1, 3, \text{NP})(也就是说,一个NP成分跨越单词1到单词3,在本例中是“the green cat”)。

然而,另一个句子可能是“the big fat cat scratch the wall”。在这句话中,我们想要在某个时刻形成成分:(2, 4, NP)我们想要形成的成分将取决于实例。如果我们像在静态工具包中那样只编译一次计算图,那么编写这个逻辑将异常困难或不可能。然而,在动态工具包中,并不只有一个预定义的计算图。每个实例都可以有一个新的计算图,所以这个问题就没有了。

动态工具包还具有易于调试和代码更接近宿主语言的优点(我的意思是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是单词的输入序列。
然后我们计算

P(y|x) = \frac{\exp{(\text{Score}(x, y)})}{\sum_{y'} \exp{(\text{Score}(x, y')})}

分数是通过定义一些对数势来确定的
\log \psi_i(x,y)

\text{Score}(x,y) = \sum_i \log \psi_i(x,y)

为了使配分函数易于处理,势必须只考虑局部特征。

在双lstm CRF中,我们定义了两种势:发射势和跃迁势。单词 i 的排放潜力来自于Bi-LSTM在timestep: i 时的隐藏状态。转换分数存储在|T|x|T矩阵中
\textbf{P},其中:T是标记集。在我的实现中,\textbf{P}_{j,k}是标记k转换到标记j的分数。所以:

\text{Score}(x,y) = \sum_i \log \psi_\text{EMIT}(y_i \rightarrow x_i) + \log \psi_\text{TRANS}(y_{i-1} \rightarrow y_i)= \sum_i h_i[y_i] + \textbf{P}_{y_i, y_{i-1}}

在第二个表达式中,我们认为标签被分配了唯一的非负索引。

如果上面的讨论过于简短,您可以查看迈克尔·柯林斯在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。
#这是计算图形形状的一个例子*取决于训练实例*。虽然我还没有尝试在静态工具包中实现这一点,
#但我认为这是可能的,但要简单得多。

#收集一些真实的数据并进行比较!

 

你可能感兴趣的:(基于PyTorch的深度学习入门教程_nlp_advanced_tutorial)