pytorch中BiLSTM模型构建及序列标注

前言

为什么要用LSTM
因为简单的RNN很容易就发生梯度消失和梯度爆炸,其中主要的原因是RNN中求导,引起的链式法则,对时间上的追溯,很容易发生系数矩阵的累乘,矩阵元素大于1,那么就会发生梯度爆炸;矩阵元素小于1,就会发生梯度消失。
LSTM通过门的控制,可以有效的防止梯度消失,但是依旧可能出现梯度爆炸的问题,所以训练LSTM会加入梯度裁剪(Gradient Clipping)。在Pytorch中梯度裁剪可以使用

import torch.nn as nn
nn.utils.clip_grad_norm(filter(lambda p:p.requires_grad,model.parameters()),max_norm=max_norm)

关于梯度消失和梯度爆炸的具体原因分析可以移步
http://www.cs.toronto.edu/~rgrosse/courses/csc321_2017/readings/L15%20Exploding%20and%20Vanishing%20Gradients.pdf

一、建立模型

RNN层

对于最简单的RNN,我们可以使用两种方式来调用:torch.nn.RNNCell(),它只接受序列中的单步输入,必须显式的传入隐藏状态。torch.nn.RNN()可以接受一个序列的输入,默认会传入一个全0的隐藏状态,也可以自己申明隐藏状态传入。输入大小是三维tensor[seq_len,batch_size,input_dim]
如:

# 构造RNN网络,x的维度5,隐层的维度10,网络的层数2
rnn_seq = nn.RNN(5, 10,2)  
# 构造一个输入序列,句长为 6,batch 是 3, 每个单词使用长度是 5的向量表示
x = torch.randn(6, 3, 5)
#out,ht = rnn_seq(x,h0) 
out,ht = rnn_seq(x) #h0可以指定或者不指定

则out:(6,3,10), ht:(2,3,10),out的输出维度[seq_len,batch_size,output_dim],ht的维度[num_layers * num_directions, batch, hidden_size],如果是单向单层的RNN那么一个句子只有一个hidden。
out[-1]和ht[-1]相等,隐藏单元就是输出的最后一个单元,可以想象,每个的输出其实就是那个时间步的隐藏单元。

LSTM层

torch.nn包下实现了LSTM函数,实现LSTM层。多个LSTMcell组合起来是LSTM。
LSTM自动实现了前向传播,不需要自己对序列进行迭代。
LSTM的用到的参数如下:创建LSTM指定如下参数,至少指定前三个参数

input_size:输入特征维数
hidden_size:隐层状态的维数
num_layers:RNN层的个数
bias:隐层状态是否带bias,默认为true
batch_first:是否输入输出的第一维为batch_size,因为pytorch中batch_size维度默认是第二维度,故此选项可以将 batch_size放在第一维度。如input是(4,1,5),中间的1是batch_size,指定batch_first=True后就是(1,4,5)
dropout:是否在除最后一个RNN层外的RNN层后面加dropout层
bidirectional:是否是双向RNN,默认为false,若为true,则num_directions=2,否则为1

LSTM的输入为:LSTM(input,(h0,c0))
其中,指定batch_first=True​后,input就是(batch_size,seq_len,input_size)​,(h0,c0)是初始的隐藏层,因为每个LSTM单元其实需要两个隐藏层的。
记hidden=(h0,c0),其中,h0的维度是(num_layers*num_directions, batch_size, hidden_size),c0维度同h0。注意,即使batch_first=True,这里h0的维度依然是batch_size在第二维度。
LSTM的输出为:out,(hn,cn)
其中,out是每一个时间步的最后一个隐藏层h的输出,假如有5个时间步(即seq_len=5),则有5个对应的输出,out的维度是(batch_size,seq_len,hidden_size)
而hidden=(hn,cn),他自己实现了时间步的迭代,每次迭代需要使用上一步的输出和hidden层,最后一步hidden=(hn,cn)记录了最后一各时间步的隐藏层输出,有几层对应几个输出,如果这个是RNN-encoder,则hn,cn就是中间的编码向量。hn的维度是(num_layers*num_directions,batch_size,hidden_size),cn同。
如:

# 输入维度 50,隐层100维,两层
lstm_seq = nn.LSTM(50, 100, num_layers=2)
# 输入序列seq= 10,batch =3,输入维度=50
lstm_input = torch.randn(10, 3, 50)
out, (h, c) = lstm_seq(lstm_input) # 使用默认的全 0 隐藏状态

LSTM的输出多了一个memory单元。out:(10 * 3 * 100),(h,c):都是(2 * 3 * 100)。out[-1,:,:]和h[-1,:,:]相等。

实现LSTM模型
import torch
# 实现一个num_layers层的LSTM-RNN
class RNN(torch.nn.Module):
    def __init__(self,input_size, hidden_size, num_layers):
        super(RNN,self).__init__()
        self.input_size = input_size
        self.hidden_size=hidden_size
        self.num_layers=num_layers
        self.lstm = torch.nn.LSTM(input_size=input_size,hidden_size=hidden_size,num_layers=num_layers,batch_first=True)
    
    def forward(self,input):
        # input应该为(batch_size,seq_len,input_szie)
        self.hidden = self.initHidden(input.size(0))
        out,self.hidden = lstm(input,self.hidden)
        return out,self.hidden
    
    def initHidden(self,batch_size):
        if self.lstm.bidirectional:
            return (torch.rand(self.num_layers*2,batch_size,self.hidden_size),torch.rand(self.num_layers*2,batch_size,self.hidden_size))
        else:
            return (torch.rand(self.num_layers,batch_size,self.hidden_size),torch.rand(self.num_layers,batch_size,self.hidden_size))

input_size = 12
hidden_size = 10
num_layers = 3
batch_size = 2
model = RNN(input_size,hidden_size,num_layers)
# input (seq_len, batch, input_size) 包含特征的输入序列,如果设置了batch_first,则batch为第一维
input = torch.rand(2,4,12)    #生成一个用随机数初始化的矩阵
model(input)
pytorch中如何处理变长padding

训练数据 sequence 长度是变化的,难以采用 mini-batch 训练,这时应该怎么办,难道只能一个 sequence 一个 sequence 地训练吗?针对这一问题,PyTorch 给出了三个函数:

torch.nn.utils.rnn.pad_sequence(train_x, batch_first=True)
torch.nn.utils.rnn.pack_padded_sequence(input, lengths, batch_first=True)
torch.nn.utils.rnn.pad_packed_sequence()

第一个函数用于给 mini-batch 中的数据加 padding,让 mini-batch 中所有 sequence 的长度等于该 mini-batch 中最长的那个 sequence 的长度。
第二、三个函数,用于提高效率,避免 LSTM 前向传播时,把加入在训练数据中的 padding 考虑进去。因此第二、三个函数理论上可以不用,但为了提高效率最好还是用。

pad_sequence:把长度小于最大长度的 sequences 用 0 填充,并且把 list 中所有的元素拼成一个 tensor。这样做的主要目的是为了让 DataLoader 可以返回 batch,因为 batch 是一个高维的 tensor,其中每个元素的数据必须长度相同。
pack_padded_sequence:RNN网络读取数据的顺序是依次读取mini-batch 中所有 sequence 中相同下标的数据。而该 mini-batch 中的 0 是没有意义的 padding,只是为了用来让它和最长的数据对齐而已,显然这种做法浪费了大量计算资源。因此,我们将用到 pack_padded_sequence 。即,不光要 padd,还要 pack。

参数说明:
input 是上一步加过 padding 的数据
lengths 是各个 sequence 的实际长度
batch_first是数据各个 dimension 按照 [batch_size, sequence_length, data_dim]顺序排列。

pad_packed_sequence:函数将一个 填充过的变长序列压紧。输入的形状可以是(T×B×* )。T是最长序列长度,B是batch size,代表任意维度(可以是0)。如果batch_first=True的话,那么相应的 input size 就是 (B×T×)。
Variable中保存的序列,应该按序列长度的长短排序,长的在前,短的在后(特别注意需要进行排序)。即input[:,0]代表的是最长的序列,input[:, B-1]保存的是最短的序列。

参数说明:
input (Variable) – 变长序列 被填充后的 batch
lengths (list[int]) – Variable 中 每个序列的长度。(知道了每个序列的长度,才能知道每个序列处理到多长停止)
batch_first (bool, optional) – 如果是True,input的形状应该是BTsize。
返回值:
一个PackedSequence 对象。

embed_input_x_packed = pack_padded_sequence(embed_input_x, sentence_lens, batch_first=True)
encoder_outputs_packed, (h_last, c_last) = self.lstm(embed_input_x_packed)

此时,返回的h_last和c_last就是剔除padding字符后的hidden state和cell state,都是Variable类型的。代表的意思如下(各个句子的表示,lstm只会作用到它实际长度的句子,而不是通过无用的padding字符)。
但是返回的output是PackedSequence类型的,可以使用:

encoder_outputs, _ = pad_packed_sequence(encoder_outputs_packed, batch_first=True)

将encoderoutputs再转换为Variable类型,得到的_代表各个句子的长度。

二、用LSTM来进行序列标注

准备数据:

training_data为[(data,tag),…]的形式。
对word进行索引。

def prepare_sequence(seq, to_ix):
    idxs = [to_ix[w] for w in seq]
    return torch.tensor(idxs, dtype=torch.long)

training_data = [
    ("The dog ate the apple".split(), ["DET", "NN", "V", "DET", "NN"]),
    ("Everybody read that book".split(), ["NN", "V", "DET", "NN"])
]
word_to_ix = {}
for sent, tags in training_data:
    for word in sent:
        if word not in word_to_ix:
            word_to_ix[word] = len(word_to_ix)
print(word_to_ix)
tag_to_ix = {"DET": 0, "NN": 1, "V": 2}

# 实际中通常使用更大的维度如32维, 64维.
# 这里我们使用小的维度, 为了方便查看训练过程中权重的变化.
EMBEDDING_DIM = 6
HIDDEN_DIM = 6
创建模型:
class LSTMTagger(nn.Module):

    def __init__(self, embedding_dim, hidden_dim, vocab_size, tagset_size):
        super(LSTMTagger, self).__init__()
        self.hidden_dim = hidden_dim

        self.word_embeddings = nn.Embedding(vocab_size, embedding_dim)

        # LSTM以word_embeddings作为输入, 输出维度为 hidden_dim 的隐藏状态值
        self.lstm = nn.LSTM(embedding_dim, hidden_dim)

        # 线性层将隐藏状态空间映射到标注空间
        self.hidden2tag = nn.Linear(hidden_dim, tagset_size)
        self.hidden = self.init_hidden()

    def init_hidden(self):
        # 一开始并没有隐藏状态所以我们要先初始化一个
        # 关于维度为什么这么设计请参考Pytoch相关文档
        # 各个维度的含义是 (num_layers*num_directions, batch_size, hidden_dim)
        return (torch.zeros(1, 1, self.hidden_dim),
                torch.zeros(1, 1, self.hidden_dim))

    def forward(self, sentence):
        embeds = self.word_embeddings(sentence)
        lstm_out, self.hidden = self.lstm(
            embeds.view(len(sentence), 1, -1), self.hidden)
        tag_space = self.hidden2tag(lstm_out.view(len(sentence), -1))
        tag_scores = F.log_softmax(tag_space, dim=1)
        return tag_scores

其他损失函数:
损失函数NLLLoss() 的输入是一个对数概率向量和一个目标标签. 它不会为我们计算对数概率,适合最后一层是log_softmax()的网络。
损失函数 CrossEntropyLoss() 与 NLLLoss() 类似, 唯一的不同是它为我们去做 softmax.可以理解为:
CrossEntropyLoss()=log_softmax() + NLLLoss()

训练模型:
model = LSTMTagger(EMBEDDING_DIM, HIDDEN_DIM, len(word_to_ix), len(tag_to_ix))
loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# 查看训练前的分数
# 注意: 输出的 i,j 元素的值表示单词 i 的 j 标签的得分
# 这里我们不需要训练不需要求导所以使用torch.no_grad()
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    tag_scores = model(inputs)
    print(tag_scores)

for epoch in range(300):  # 实际情况下你不会训练300个周期, 此例中我们只是随便设了一个值
    for sentence, tags in training_data:
        # 第一步: 请记住Pytorch会累加梯度.
        # 我们需要在训练每个实例前清空梯度
        model.zero_grad()

        # 此外还需要清空 LSTM 的隐状态,
        # 将其从上个实例的历史中分离出来.
        model.hidden = model.init_hidden()

        # 准备网络输入, 将其变为词索引的 Tensor 类型数据
        sentence_in = prepare_sequence(sentence, word_to_ix)
        targets = prepare_sequence(tags, tag_to_ix)

        # 第三步: 前向传播.
        tag_scores = model(sentence_in)

        # 第四步: 计算损失和梯度值, 通过调用 optimizer.step() 来更新梯度
        loss = loss_function(tag_scores, targets)
        loss.backward()
        optimizer.step()

# 查看训练后的得分
with torch.no_grad():
    inputs = prepare_sequence(training_data[0][0], word_to_ix)
    tag_scores = model(inputs)

    # 句子是 "the dog ate the apple", i,j 表示对于单词 i, 标签 j 的得分.
    # 我们采用得分最高的标签作为预测的标签. 从下面的输出我们可以看到, 预测得
    # 到的结果是0 1 2 0 1. 因为 索引是从0开始的, 因此第一个值0表示第一行的
    # 最大值, 第二个值1表示第二行的最大值, 以此类推. 所以最后的结果是 DET
    # NOUN VERB DET NOUN, 整个序列都是正确的!
    print(tag_scores)

输出:

tensor([[-1.1389, -1.2024, -0.9693],
        [-1.1065, -1.2200, -0.9834],
        [-1.1286, -1.2093, -0.9726],
        [-1.1190, -1.1960, -0.9916],
        [-1.0137, -1.2642, -1.0366]])
tensor([[-0.0858, -2.9355, -3.5374],
        [-5.2313, -0.0234, -4.0314],
        [-3.9098, -4.1279, -0.0368],
        [-0.0187, -4.7809, -4.5960],
        [-5.8170, -0.0183, -4.1879]])
扩展:使用字符级特征来增强LSTM词性标注器

在词性标注中, 每个词都有一个词嵌入, 作为序列模型的输入. 接下来让我们使用每个的单词的 字符级别的表达来增强词嵌入. 我们期望这个操作对结果能有显著提升, 因为像词缀这样的字符级 信息对于词性有很大的影响. 比如说, 像包含词缀 -ly 的单词基本上都是被标注为副词.

具体操作如下. 用 c w c_w​ 的字符级表达, 同之前一样, 我们使用 x w x_w​ 来表示词嵌入. 序列模型的输入就变成了 x w x_w​ c w c_w​ 的拼接. 因此, 如果 的维度 x w x_w​ 是5, 的维度 c w c_w​ 是3, 那么我们的 LSTM 网络的输入维度大小就是8.

为了得到字符级别的表达, 将单词的每个字符输入一个 LSTM 网络, 而 c w c_w 则为这个 LSTM 网络最后的隐状态. 一些提示:

新模型中需要两个 LSTM, 一个跟之前一样, 用来输出词性标注的得分, 另外一个新增加的用来 获取每个单词的字符级别表达.
为了在字符级别上运行序列模型, 你需要用嵌入的字符来作为字符 LSTM 的输入。

扩展:使用mask来求变长序列的loss

对于变长数据,如果采用mini_batch进行训练,那么会采用padding的形式在短数据后面补零,如果不想在计算loss的时候考虑这些零,可以用torch.masked_select把矩阵/tensor中的零mask掉。

torch.masked_select(input, mask, out=None) → Tensor
参数:
input (Tensor) – 输入张量
mask (ByteTensor) – 掩码张量,包含了二元索引值
out (Tensor, optional) – 目标张量

根据掩码张量mask中的二元值,取输入张量中的指定项( mask为一个 ByteTensor),将取值返回到一个新的1D张量,张量 mask须跟input张量有相同数量的元素数目,但形状或维度不需要相同。
注意: 返回的张量不与原始张量共享内存空间。

参考网址
【pytorch】pytorch-LSTM
[PyTorch] rnn,lstm,gru中输入输出维度
SEQUENCE MODELS AND LONG-SHORT TERM MEMORY NETWORKS
PyTorch 1.0 中文官方教程:序列模型和LSTM网络(上文翻译,详细有用)
PyTorch 实现序列模型和基于LSTM的循环神经网络(类似上文)
在Pytorch下搭建BiLSTM(Reproducible/Deterministic)
PyTorch 训练 RNN 时,序列长度不固定怎么办?(详细有用)
pytorch中如何处理RNN输入变长序列padding(详细有用)
torch.masked_select 掩码

发布了143 篇原创文章 · 获赞 161 · 访问量 29万+

猜你喜欢

转载自blog.csdn.net/vivian_ll/article/details/93894151