BILSTM-CRF代码讲解

BILSTM-CRF代码

代码来源于命名实体识别(NER):BiLSTM-CRF原理介绍+Pytorch_Tutorial代码解析
第一部分:导包
1.torch.nn包中主要包含了用来搭建各个层的模块(Modules),比如全连接、二维卷积、池化等;torch.nn包中还包含了一系列有用的loss函数。
2.torch.optim包则主要包含了用来更新参数的优化算法,比如SGD、AdaGrad、RMSProp、 Adam等。
3.import torch.autograd as autograd这是求自动微分的

第二部分
用于设置随机初始化的种子,即上述的编号,编号固定,每次获取的随机数固定。

1.设置标签与BILSTM隐藏层
首先确定标签个数:SBIOE
BILSTM的隐藏特征个数4,单向为2。

START_TAG = ""
STOP_TAG = ""
EMBEDDING_DIM = 5  # 由于标签一共有B\I\O\START\STOP 5个,所以embedding_dim为5
HIDDEN_DIM = 4  # 这其实是BiLSTM的隐藏层的特征数量,因为是双向所以是2倍,单向为2

2.数据集
这里用两个句子来测试,每一个句子对应的词性都有正确的标签。

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()
)]

3.处理数据集中句子的词,不重复的将句子中的词拿出来并标号。
设置一个word_to_ix存储句子中每一个单词。
首先我们拿出每一个句子跟其对应的标签,用sentence与tag循环每一个句子,比如第0次抽出来的就是第一个句子"the wall …money",与他的标签"B I I …0"。
现在我们只存储出现过的单词(不管标签),下面代码解释:

word_to_ix = {}
for sentence, tags in training_data:
    for word in sentence:
        if word not in word_to_ix: #word_to_ix是否包含左边的内容
            word_to_ix[word] = len(word_to_ix)

if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)

这句就是说word_to ix没有word这句单词时候,就存储进去,并且给每一个新的word赋值,赋的值就是长度。

最终的结果为都在word_to_ix中:
word_to_ix: {‘the’: 0, ‘wall’: 1, ‘street’: 2, ‘journal’: 3, ‘reported’: 4, ‘today’: 5, ‘that’: 6, ‘apple’: 7, ‘corporation’: 8, ‘made’: 9, ‘money’: 10, ‘georgia’: 11, ‘tech’: 12, ‘is’: 13, ‘a’: 14, ‘university’: 15, ‘in’: 16}

4.将5个标签存到tag_to_ix的字典中。

tag_to_ix = {"B": 0, "I": 1, "O": 2, START_TAG: 3, STOP_TAG: 4}

5.几个实用的小函数

def prepare_sequence(seq, to_ix):
    idxs = [to_ix[w] for w in seq]  #  将句子”w1 w2 w3”(词序列)[w1,w2,w3..]中的词w转化为在词表中的索引值 to_ix [w] 
    return torch.tensor(idxs, dtype=torch.long)
def argmax(vec):
    # 得到最大的值的索引
    _, idx = torch.max(vec, 1)
    return idx.item()
def log_sum_exp(vec):
    max_score = vec[0, argmax(vec)]  # max_score的维度为1
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])  # 维度为1*5
    return max_score + torch.log(torch.sum(torch.exp(vec - max_score_broadcast)))

6.接下来,我们将我们的句子输入到BILSTM-CRF模型,

model = BiLSTM_CRF(len(word_to_ix), tag_to_ix, EMBEDDING_DIM, HIDDEN_DIM)

第三部分.重点讲解BILSTM-CRF模型
那么我们先看一下BILSTM_CRF模型:

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)
        # 转移矩阵,transitions[i][j]表示从label_j转移到label_i的概率,虽然是随机生成的但是后面会迭代更新
        self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))

        self.transitions.data[tag_to_ix[START_TAG], :] = -10000  # 从任何标签转移到START_TAG不可能
        self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000  # 从STOP_TAG转移到任何标签不可能

        self.hidden = self.init_hidden()  # 随机初始化LSTM的输入(h_0, c_0)

1.定义模型def_init_():

class BiLSTM_CRF(nn.Module):
def init(self, vocab_size, tag_to_ix, embedding_dim, hidden_dim):

描述class XX(nn.Module):def _init_的操作,可以看这个链接。
vocab_size就是len(word_to_ix)的长度,也就是所有的词(不重复)。将tag_to_ix也穿过来,embedding_dim就是标签数量,hidden_dim是bilstm层隐藏数量。

super(BiLSTM_CRF, self).init()

和自定义模型一样,这句话就是调用父类的构造函数。
(2)定义一些模型参数,下面解释一点自己不会的

self.word_embeds = nn.Embedding(vocab_size, embedding_dim) # 嵌入层

这里是nn.Embedding的理解
简单来说就是把词向量放入网络,词向量的输出应该是什么样子,输出是:(vocab_size,embedding_dim)。

self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, num_layers=1, bidirectional=True)

这是nn.LSTM的理解
输入的是词向量纬度embedding_dim=5,hidden_dim的纬度为4/2=2,循环神经网络的层数为1,bidirecton代表使用双向LSTM。

同理,PyTorch的nn.Linear()是用于设置网络中的全连接层的。

下面这行代码就是看转移概率的,也就是从一个tag->tag:
transition矩阵是随机生成的,他会进行更新迭代。

self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size))
self.transitions.data[tag_to_ix[START_TAG], :] = -10000 # 从任何标签转移到START_TAG不可能
self.transitions.data[:, tag_to_ix[STOP_TAG]] = -10000 # 从STOP_TAG转移到任何标签不可能

(1)定义def init_hidden()函数:

    def init_hidden(self):
        return (torch.randn(2, 1, self.hidden_dim // 2),
                torch.randn(2, 1, self.hidden_dim // 2))

这应该就是双层lstm隐藏层,因为是双向的,所以生成2个,每一个纬度为(1,self.hidden_dim//2),随机初始化隐藏层的输入,这个函数用self.hidden = self.init_hidden()之后:

    (tensor([[[ 1.1456, -0.0378]],[[ 0.9321,  0.5295]]]), 
    tensor([[[-1.5295,  0.2228]],[[-1.4066, -0.2268]]]))

(2)应该是计算总路径的分数

    def _forward_alg(self, feats):  #前向算法,feats是LSTM所有时间步的输出
        '''
        输入:发射矩阵(emission score),实际上就是LSTM的输出——sentence的每个word经BiLSTM后,对应于每个label的得分
        输出:所有可能路径得分之和/归一化因子/配分函数/Z(x)
        '''
        init_alphas = torch.full((1, self.tagset_size), -10000.)#初始化得分值全部为-10000,并置start_tag的值为0,这个思想类似于dijkstra计算最短路径的第一步
        init_alphas[0][self.tag_to_ix[START_TAG]] = 0. # start位置的alpha为0

        # 包装到一个变量里面以便自动反向传播
        forward_var = init_alphas
        for feat in feats:  # w_i
            alphas_t = []
            for next_tag in range(self.tagset_size):  # tag_j  next_tag有target_size个可能的取值
                # t时刻tag_i emission score(1个)的广播。需要将其与t-1时刻的5个previous_tags转移到该tag_i的transition scors相加
                emit_score = feat[next_tag].view(1, -1).expand(1, self.tagset_size)  # 1*5: 遍历标签集 发射分数emission_score[i][j]是词i被标注为词性j的概率 两层for循环里第一层为词序列中的第i个特征(词性),第二层为标签集中的第j种标签(词性)
                # t-1时刻的5个previous_tags到该tag_i的transition scors
                trans_score = self.transitions[next_tag].view(1, -1)  # 维度是1*5 转移分数transitions_socre[m1][m2]是序列中当前词的前一个词被标注为m1下,当前词被标注为m2的概率 . 即由m1转移到下一个状态为m2的概率

                next_tag_var = forward_var + trans_score + emit_score
                # 求和,实现w_(t-1)到w_t的推进
                alphas_t.append(log_sum_exp(next_tag_var).view(1))
            forward_var = torch.cat(alphas_t).view(1, -1)  # 1*5 forward_var size:[1][标签集大小]
            # 每个时间步标注为词性j j->[1-len(tags)] 的分数 每个时间步都更新 for循环遍历结束后 forward_var就是遍历词序列末尾时的最后一个分数

        # 最后将最后一个单词的forward var与转移 stop tag的概率相加
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]
        alpha = log_sum_exp(terminal_var)
        return alpha

这个函数,只是根据 随机的transitions ,前向传播算出的一个score,用到了动态规划的思想,但是因为用的是随机的转移矩阵,算出的值很大 score>20。这是计算总路径之和,这篇知乎介绍了方法,看懂这个方法才能读懂代码。BiLSTM-CRF中CRF计算细节
或者看这篇博客的代码解析:pytorch中biLSTM_CRF模型源码学习
或者BILSTM_CRF代码解析

分析代码,:

init_alphas = torch.full((1, self.tagset_size), -10000.)

torch.full就是说,我创建一个(1,self.tagset_size)的纬度的矩阵,每一个都是-10000:
这个意思可能是:初始化得分值全部为-10000,并置start_tag的值为0,这个思想类似于dijkstra计算最短路径的第一步。

for feat in feats:

遍历句子 feats是一个[句长][标签集大小]的矩阵,总时间步==句长,这个循环就是循环11个词。(第一句话有11个词),第一个词的5个标签概率如下图:

    tensor([-0.2095,  0.1737, -0.3876,  0.4378, -0.3475]

for next_tag in range(self.tagset_size):

再循环5个标签中的第一个标签,在上面链接的知乎可以知道,我们需要将1个标签概率扩成(1,5)纬度。计算

next_tag_var = forward_var + trans_score + emit_score

纬度如下:

 tensor([[-10000., -10000., -10000.,      0., -10000.]]) 
 trans_score: tensor([[-1.1811e-01, -1.4420e+00, -1.1108e+00, -1.1187e+00, -1.0000e+04]],grad_fn=)
 emit_score: tensor([[-0.2095, -0.2095, -0.2095, -0.2095, -0.2095]],grad_fn=)

alphas_t.append(log_sum_exp(next_tag_var).view(1))

将每一个Pmt(1,5)维(3个1,5维矩阵加起来的之和),经过log_sum_exp变成一个数字,逐步添加到alphas_t,最终aphps是(1,5)维。
将alphas_t是保存Pmt,(m是标签个数,t是第几个词),这个其实就到第t个词的路径分数。m个标签,他当前就有m个路径。

forward_var = torch.cat(alphas_t).view(1, -1)

这时候已经出标签循环了,第t个词所有m个标签概率已经循环结束,f
forward_var size:[1][标签集大小]每个时间步标注为词性j j->[1-len(tags)] 的分数 每个时间步都更新 for循环遍历结束后 forward_var就是遍历词序列末尾时的最后一个分数。
紧接着进入第二个词的循环,不断的用forward_var_size,当一个句子所有的词循环结束,跳出循环。

terminal_var = forward_var +self.transitions[self.tag_to_ix[STOP_TAG]]
alpha = log_sum_exp(terminal_var)

最后加上句子最后一个词转移到STOP_TAG的分数,得到所有可能路径的得分总和。

(3)其中得到lstm层得到feats:

def _get_lstm_features(self, sentence):
	self.hidden = self.init_hidden()#初始化状态向量
	embeds = self.word_embeds(sentence) #获取句向量:用相应的词向量替换每一个词
    embeds = embeds.unsqueeze(1)#在1的位置加上一个维数为1的维度
 	lstm_out, self.hidden = self.lstm(embeds, self.hidden)#lstm层 输入句向量和状态向量,输出一个输出向量和状态向量 
	lstm_out = lstm_out.view(len(sentence), self.hidden_dim)#reshape 输出向量扁平化为[句长,隐藏层维度]
	lstm_feats = self.hidden2tag(lstm_out) #全连接层 将扁平化的词向量映射到类别的编号
	return lstm_feats

可以看出,函数里经过了embedding,lstm,linear层,是根据LSTM算出的一个矩阵。这里是11x5的一个tensor,而这个11x5的tensor,就是发射矩阵!!!发射矩阵!!!发射矩阵!!!(emission matrix)
(4)计算标签序列在当前模型的得分

def _score_sentence(self, feats, tags):
        # Gives the score of a provided tag sequence
        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]]#加上句子最后一个词转移到STOP_TAG的转移分数
        return score

根据真实的标签算出的一个score,这与上面的def _forward_alg(self, feats)有什么不同的地方嘛?共同之处在于,两者都是用的随机的转移矩阵算的score,但是不同地方在于,上面那个函数算了一个最大可能路径,但是实际上可能不是真实的 各个标签转移的值。例如说,真实的标签 是 N V V,但是因为transitions是随机的,所以上面的函数得到的其实是N N N这样,两者之间的score就有了差距。而后来的反向传播,就能够更新transitions,使得转移矩阵逼近真实的“转移矩阵”。(个人理解)。
(5)维特比算法

    #解码,得到预测的序列,以及预测序列的得分
    def _viterbi_decode(self, feats):
        backpointers = []
 
        # Initialize the viterbi variables in log space
        init_vvars = torch.Tensor(1, self.tagset_size).fill_(-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 = autograd.Variable(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)
                next_tag_var = forward_var + self.transitions[next_tag] #其他标签(B,I,E,Start,End)到标签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 emission scores, and assign forward_var to the set
            # of viterbi variables we just computed
            forward_var = (torch.cat(viterbivars_t) + feat).view(1, -1)#从step0到step(i-1)时5个序列中每个序列的最大score
            backpointers.append(bptrs_t) #bptrs_t有5个元素
 
        # Transition to STOP_TAG
        terminal_var = forward_var + self.transitions[self.tag_to_ix[STOP_TAG]]#其他标签到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路径
            best_tag_id = bptrs_t[best_tag_id]
            best_path.append(best_tag_id)
        # Pop off the start tag (we dont want to return that to the caller)
        start = best_path.pop()
        assert start == self.tag_to_ix[START_TAG]  # Sanity check
        best_path.reverse()# 把从后向前的路径正过来
        return path_score, best_path

维特比解码,实际上就是在预测的时候使用了,输出得分与路径值。
(6)loss函数-非常重要!!!这就是利用了用"真实路径”-总路径的loss,从而迭代更新!应该就是下面这个公式:
BILSTM-CRF代码讲解_第1张图片

def neg_log_likelihood(self, sentence, tags):
    feats = self._get_lstm_features(sentence)#11*5 经过了LSTM+Linear矩阵后的输出,之后作为CRF的输入。
    forward_score = self._forward_alg(feats) #0维的一个得分,20.*来着
    gold_score = self._score_sentence(feats, tags)#tensor([ 4.5836])

    return forward_score - gold_score #这是两者之间的差值,后来直接根据这个差值,反向传播

(7)forward函数

    def forward(self, sentence):
        '''
        解码过程,维特比解码选择最大概率的标注路径
        '''
        lstm_feats = self._get_lstm_features(sentence)

        score, tag_seq = self._viterbi_decode(lstm_feats)
        return score, tag_seq

def forward(self, sentence):forward函数只是用来预测了,train的时候没用调用它。

第四部分.训练数据集
(1)梯度下降

optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=1e-4)

for epoch in range(300):
    for sentence, tags in training_data:
        model.zero_grad()

        # 输入
        sentence_in = prepare_sequence(sentence, word_to_ix)
        targets = torch.tensor([tag_to_ix[t] for t in tags], dtype=torch.long)

        # 获取loss
        loss = model.neg_log_likelihood(sentence_in, targets)

        # 反向传播
        loss.backward()
        optimizer.step()

你可能感兴趣的:(pytorch,自然语言处理,深度学习)