看这篇文章之前你应该已经对lstm有所了解。
这里只讲pytorch的lstm的输出,其实所有rnn网络都一样。
单向lstm
1层lstm单元
这里假设输入的batch_size为8,
句子长度为10,
词向量维度为128,
lstm的隐藏层维度为50,
只有1层lstm单元。
batch_size = 8 #batch为8
seq_len = 10 #句子长度为10
embedding_size = 128 #词向量维度为128
x = torch.rand((batch_size,seq_len,embedding_size))
input_size = embedding_size #对于lstm的输入,每个词的维度就是词向量维度
hidden_size = 50
num_layers=1
lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=False)
output,(h,c) = lstm(x)
print('output.shape = ',output.shape)
print('h.shape = ',h.shape)
输出为:
output.shape = torch.Size([8, 10, 50])
h.shape = torch.Size([1, 8, 50])
可以这么理解,这里的output是包含的是每个单词的信息(每个句子10个单词,每个单词的信息维度都是50)。
需要注意的是,lstm在传播的时候因为考虑了时序信息,也就是说这里output中每个单词的信息都是在考虑了之前所有单词信息(也就是上文信息)之后的产生的信息。
简单画个图就是这样了(对于单个句子)
print(torch.all(output[:,-1,:] == h[0]).item())
输出为:
True
这里就是说明隐藏层的信息h就是每个句子最后一个单词信息。 根据我们刚刚说过的,output中每个单词的信息都是在考虑了之前所有单词信息(也就是上文信息)之后的产生的信息。 所以 `h[0]= output[:,-1,:] (h是三维,取h[0]就是二维了)`就是蕴含了整个句子信息的结果。
2层lstm单元
代码与上面相同,只是lstm单元变为2层。
batch_size = 8
seq_len = 10
embedding_size = 128
x = torch.rand((batch_size,seq_len,embedding_size))
input_size = embedding_size #对于输入,每个词的维度就是词向量维度
hidden_size = 50
num_layers=2
lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=False)
output,(h,c) = lstm(x)
print('output.shape = ',output.shape)
print('h.shape = ',h.shape)
输出为:
output.shape = torch.Size([8, 10, 50])
h.shape = torch.Size([2, 8, 50])
print(torch.all(output[:,-1,:] == h[0]).item())
print(torch.all(output[:,-1,:] == h[1]).item())
输出为:
False
True
简单画个图就是这样(对于单个句子)
可以看到输出层output其实和单层的lstm没区别,只是两层lstm可以用更多的全连接层来使得信息更加准确。
而h[0]和h[1]只是两个隐藏层,它们都蕴含了整个句子的信息,只是学习到的信息会有所不同。
双向lstm
1层lstm单元
这里假设输入的batch_size为8,
句子长度为10,
词向量维度为128,
lstm的隐藏层维度为50,
只有1层lstm单元。
batch_size = 8
seq_len = 10
embedding_size = 128
x = torch.rand((batch_size,seq_len,embedding_size))
input_size = embedding_size #对于输入,每个词的维度就是词向量维度
hidden_size = 50
num_layers=1
lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
output,(h,c) = lstm(x)
print('output.shape = ',output.shape)
print('h.shape = ',h.shape)
输出为:
output.shape = torch.Size([8, 10, 100])
h.shape = torch.Size([2, 8, 50])
简单画个图就是这样了(对于单个句子)
print(torch.all(output[:,-1,:hidden_size]==h[0]).item())
print(torch.all(output[:,0,hidden_size:]==h[1]).item())
输出为:
True
True
可以看到,output就是每个单词在正向lstm中的信息和反向lstm中的信息concatenate得到的。
h[0]就是正向lstm得到的整个句子的信息。
h[1]就是反向lstm得到的整个句子的信息。
再提一嘴,这里正向lstm和反向lstm的参数是各自更新的,它们不是权值共享的,可以看下面的代码。
for i in range(len(lstm.all_weights[0])):
if torch.any(lstm.all_weights[0][i]!=lstm.all_weights[1][i]):
print(False)
False
False
False
False
2层lstm单元
2层的双向lstm和之前的同理,只是多了2个正向隐藏层和反向隐藏层。
batch_size = 8
seq_len = 10
embedding_size = 128
x = torch.rand((batch_size,seq_len,embedding_size))
input_size = embedding_size #对于输入,每个词的维度就是词向量维度
hidden_size = 50
num_layers=2
lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
output,(h,c) = lstm(x)
print('output.shape = ',output.shape)
print('h.shape = ',h.shape)
输出为:
output.shape = torch.Size([8, 10, 100])
h.shape = torch.Size([4, 8, 50])
print(torch.all(output[:,-1,:hidden_size]==h[2]).item())
print(torch.all(output[:,0,hidden_size:]==h[3]).item())
输出为:
True
True
画图就是
对于多层的lstm,其实就是一层一层的处理,后一层的输入为前一层的输出,比如这里的2层lstm,其实就是首先对于画框的部分进行单层lstm的处理,得到输出向量,然后作为第二层lstm的输入继续计算。
通过这个代码:
for i in range(len(lstm.all_weights)):
for w in lstm.all_weights[i][:2]:
print(w.shape)
torch.Size([200, 128])
torch.Size([200, 50])
torch.Size([200, 128])
torch.Size([200, 50])
torch.Size([200, 100])
torch.Size([200, 50])
torch.Size([200, 100])
torch.Size([200, 50])
我们可以看到torch.Size([200, 100])
,这其实就是第一层的输出(维度50)cat之后作为第二层的输入而导致的权重矩阵。(200是4*hidden_size,表示的(W_hi|W_hf|W_hg|W_ho)
4个权重矩阵)
什么时候用输出层output信息
以双向lstm为例,
我们知道了output其实是每个单词的信息,只不过这些单词因为双向lstm的原因所以考虑了上下文所有的信息(如果是单向lstm,就是只考虑了上文信息)。
所以我们如果要做命名实体识别这样对一个句子中每个单词的词性进行分类的问题,就需要使用输出层output信息。
什么时候用隐藏层信息
以双向lstm为例,
我们知道了隐藏层其实是整个句子的信息,双向lstm同时得到了正向读句子时的信息结果和反向读句子时的信息结果。
所以如果我们要做语句的情感分类这样对整个句子的信息进行分类的结果,那我们只需要将这两个信息结果进行concatenate然后送入到线性层中去分类就好了。一般都是用h[-2](正向传播得到最深的那一个隐藏层结果),h[-1](反向传播得到最深的那一个隐藏层结果)。
后话
其实所有的RNN网络都是这样,比如Transformer和Bert的encoder部分就是多个encoder叠加在一起,说白了就是对一个句子的隐藏层编码再编码,那我们其实只需要最后一层的隐藏层就够了。
不过一般的做法都是将第一个隐藏层的结果和最后一个隐藏层的结果进行求和或者平均池化的操作后得到的结果来作为整个句子的嵌入向量。这样就是借助残差网络的思想,可以避免信息在过深的网络中传播时导致的信息丢失问题。