1. 程式人生 > 實用技巧 >【神經網路】LSTM在Pytorch中的使用

【神經網路】LSTM在Pytorch中的使用

先附上張玉騰大佬的內容,我覺得說的非常明白,原文閱讀連結我放在下面,方面大家檢視。

LSTM的輸入與輸出:

  1. output儲存了最後一層,每個time step的輸出h,如果是雙向LSTM,每個time step的輸出h = [h正向, h逆向] (同一個time step的正向和逆向的h連線起來)。
  2. h_n儲存了每一層,最後一個time step的輸出h,如果是雙向LSTM,單獨儲存前向和後向的最後一個time step的輸出h。
  3. c_n與h_n一致,只是它儲存的是c的值。

1.output是一個三維的張量,第一維表示序列長度,第二維表示一批的樣本數(batch),第三維是 hidden_size(隱藏層大小) * num_directions ,num_directions根據是“否為雙向”取值為1或2。因此,我們可以知道,output第三個維度的尺寸根據是否為雙向而變化,如果不是雙向,第三個維度等於我們定義的隱藏層大小;如果是雙向的,第三個維度的大小等於2倍的隱藏層大小。為什麼使用2倍的隱藏層大小?因為它把每個time step的前向和後向的輸出連線起來了。
這裡引入一個問題為什麼LSTM鼓勵我們第一維不是batch,這與我們常規輸入想悖,可以閱讀 https://www.cnblogs.com/yuqinyuqin/p/14100967.html 這篇文章,醍醐灌頂

2.h_n是一個三維的張量,第一維是num_layers*num_directions,num_layers是我們定義的神經網路的層數,num_directions在上面介紹過,取值為1或2,表示是否為雙向LSTM。第二維表示一批的樣本數量(batch)。第三維表示隱藏層的大小。第一個維度是h_n難理解的地方。首先我們定義當前的LSTM為單向LSTM,則第一維的大小是num_layers,該維度表示第n層最後一個time step的輸出。如果是雙向LSTM,則第一維的大小是2 * num_layers,此時,該維度依舊錶示每一層最後一個time step的輸出,同時前向和後向的運算時最後一個time step的輸出用了一個該維度。

舉個例子,我們定義一個num_layers=3的雙向LSTM,h_n第一個維度的大小就等於 6 (2*3),h_n[0]表示第一層前向傳播最後一個time
step的輸出,h_n[1]表示第一層後向傳播最後一個time step的輸出,h_n[2]表示第二層前向傳播最後一個time step的輸出,h_n[3]表示第二層後向傳播最後一個time step的輸出,h_n[4]和h_n[5]分別表示第三層前向和後向傳播時最後一個time step的輸出。

3.c_n與h_n的結構一樣,就不重複贅述了

使用jupyter notebook程式碼驗證下,發現確實如此:

雙向改為單向再驗證一次:

下面在介紹我今天論文閱讀的LSTM程式碼前,複習一個tensor.data操作:

如果我們想要修改tensor的數值,但是又不希望被autograd記錄(即不會影響反向傳播),那麼我麼可以對tensor.data進行操作。

 1 x = torch.ones(1,requires_grad=True)
 2 print(x.data) # 還是一個tensor
 3 print(x.data.requires_grad) # 但是已經是獨立於計算圖之外
 4 y = 2 * x
 5 x.data *= 100 # 只改變了值,不會記錄在計算圖,所以不會影響梯度傳播
 6 y.backward()
 7 print(x) # 更改data的值也會影響tensor的值
 8 print(x.grad)
 9 #tensor([1.])
10 #False
11 #tensor([100.], requires_grad=True)
12 #tensor([2.])

模型程式碼如下,主要任務是實現關係分類,LSTM是Encode部分中的一個元件:

 1 class LSTM(nn.Module):
 2     def __init__(self, config):
 3         super(LSTM, self).__init__()
 4         self.config = config
 5         #ori_model = model_pattern(config = self)  接受傳遞來的引數
 6         word_vec_size = config.data_word_vec.shape[0]   #glove處理的詞向量
 7         self.word_emb = nn.Embedding(word_vec_size, config.data_word_vec.shape[1])
 8         #生成一個形狀與 config.data_word_vec相同的Embedding,值是瞎J8生成的
 9         self.word_emb.weight.data.copy_(torch.from_numpy(config.data_word_vec))
10         # 將glove處理過的單詞的權重拷貝入word_emb中
11         self.word_emb.weight.requires_grad = False
12         #預訓練向量表中單詞對應的權重不進行權重更新
13 
14         self.coref_embed = nn.Embedding(config.max_length, config.coref_size, padding_idx=0)
15         self.ner_emb = nn.Embedding(7, config.entity_type_size, padding_idx=0)
16         '''句子長度不夠的句子進行填充,比如用值0進行填充,當用nn.Embedding()'''
17         '''進行詞向量嵌入時,對應的索引為0的向量將變為全為0的向量。這樣就減少了填充值對模型訓練的影響'''
18         '''把padding_idx設定為填充的值,如padding_idx=0,訓練過程中索引為0的將始終設定為0,不進行引數更新'''
19         input_size = config.data_word_vec.shape[1] + config.coref_size + config.entity_type_size #+ char_hidden
20         hidden_size = 128
21         # EncoderLSTM初始化需要的引數(self, input_size, num_units, nlayers, concat, bidir, dropout, return_last)
22         self.rnn = EncoderLSTM(input_size, hidden_size, 1, True, False, 1 - config.keep_prob, False)
23         self.linear_re = nn.Linear(hidden_size, hidden_size)  # *4 for 2layer
24         self.bili = torch.nn.Bilinear(hidden_size+config.dis_size, hidden_size+config.dis_size, config.relation_num)
25         self.dis_embed = nn.Embedding(20, config.dis_size, padding_idx=10)
26 
27     #model(context_idxs, context_pos, context_ner, context_char_idxs, input_lengths, h_mapping, t_mapping, relation_mask, dis_h_2_t, dis_t_2_h)
28     def forward(self, context_idxs, pos, context_ner, context_char_idxs, context_lens, h_mapping, t_mapping,relation_mask, dis_h_2_t, dis_t_2_h):
29         #self.word_emb(context_idxs).shape = [40,512,config.data_word_vec.shape[1]]
30         #self.coref_embed(pos) = [40,512,config.coref_size]
31         #self.coref_embed(pos) = [40,512,config.coref_size]
32         sent = torch.cat([self.word_emb(context_idxs) , self.coref_embed(pos), self.coref_embed(pos)], dim=-1)
33         context_output = self.rnn(sent, context_lens)
34         #context_lens含有一個batch中句子多少個單詞[510,456,389,...]已按句子長度順序排好   [40,512,hidden_size=128]
35         context_output = torch.relu(self.linear_re(context_output))
36         start_re_output = torch.matmul(h_mapping, context_output)  #[40,1800,512]*[40,512,128]->[40,1800,128]
37         end_re_output = torch.matmul(t_mapping, context_output)
38 
39         s_rep = torch.cat([start_re_output, self.dis_embed(dis_h_2_t)], dim=-1)
40         t_rep = torch.cat([end_re_output, self.dis_embed(dis_t_2_h)], dim=-1)
41         # self.dis_embed(dis_h_2_t).shape = [40,1800,20]  拼接過後s_rep.shape = [40,1800,128+20]
42         predict_re = self.bili(s_rep, t_rep)   #predict_re.shape = [40,1800,97]
43         return predict_re

執行過程中真正用到LSTM的實際上是EncoderLSTM模組:

 1 class LockedDropout(nn.Module):
 2     def __init__(self, dropout):
 3         super().__init__()
 4         self.dropout = dropout
 5 
 6     def forward(self, x):
 7         dropout = self.dropout
 8         if not self.training:
 9             return x
10         m = x.data.new(x.size(0), 1, x.size(2)).bernoulli_(1 - dropout)   #有(1-0.dropout)的機率某些元素置為1
11         mask = Variable(m.div_(1 - dropout), requires_grad=False)
12         mask = mask.expand_as(x)
13         return mask * x
14 
15 class EncoderLSTM(nn.Module):
16     #(input_size, hidden_size, 1, True, False, 1 - config.keep_prob, False)
17     def __init__(self, input_size, num_units, nlayers, concat, bidir, dropout, return_last):
18         super().__init__()
19         self.rnns = []
20         for i in range(nlayers):
21             if i == 0:
22                 input_size_ = input_size
23                 output_size_ = num_units
24             else:
25                 input_size_ = num_units if not bidir else num_units * 2
26                 output_size_ = num_units
27             self.rnns.append(nn.LSTM(input_size_, output_size_, 1, bidirectional=bidir, batch_first=True))
28         self.rnns = nn.ModuleList(self.rnns)
29         self.init_hidden = nn.ParameterList([nn.Parameter(torch.Tensor(2 if bidir else 1, 1, num_units).zero_()) for _ in range(nlayers)])
30         self.init_c = nn.ParameterList([nn.Parameter(torch.Tensor(2 if bidir else 1, 1, num_units).zero_()) for _ in range(nlayers)])
31         self.dropout = LockedDropout(dropout)
32         self.concat = concat
33         self.nlayers = nlayers
34         self.return_last = return_last
35         # self.reset_parameters()
36 
37     def reset_parameters(self):
38         for rnn in self.rnns:
39             for name, p in rnn.named_parameters():
40                 if 'weight' in name:
41                     p.data.normal_(std=0.1)
42                 else:
43                     p.data.zero_()
44 
45     def get_init(self, bsz, i):
46         return self.init_hidden[i].expand(-1, bsz, -1).contiguous(), self.init_c[i].expand(-1, bsz, -1).contiguous()
47 
48     def forward(self, input, input_lengths=None):
49         bsz, slen = input.size(0), input.size(1)
50         output = input
51         outputs = []
52         #獲取輸入batch的所有資料的長度
53         if input_lengths is not None:
54             lens = input_lengths.data.cpu().numpy()
55 
56         for i in range(self.nlayers):
57             hidden, c = self.get_init(bsz, i)
58             output = self.dropout(output)  #輸入input進行dropout
59             if input_lengths is not None:
60                 output = rnn.pack_padded_sequence(output, lens, batch_first=True)
61                 #pack_padded_sequence 是先補齊到相同長度 再壓緊,詳見下方學習連結,batch_first = True只對input與output起作用
62             output, hidden = self.rnns[i](output, (hidden, c))
63             if input_lengths is not None:
64                 output, _ = rnn.pad_packed_sequence(output, batch_first=True)
65                 #反過來,對壓緊後的序列,進行擴充補齊操作。
66                 if output.size(1) < slen: # used for parallel
67                     padding = Variable(output.data.new(1, 1, 1).zero_())
68                     #output.data.new(1, 1, 1)意義:繼承output維度的新Tensor,shape為(1,1,1)
69                     output = torch.cat([output, padding.expand(output.size(0), slen-output.size(1), output.size(2))], dim=1)
70                     #從資料長度那維度(第二維)padding,把pack,pad過程損失的output.shape還原出來
71             if self.return_last:
72                 outputs.append(hidden.permute(1, 0, 2).contiguous().view(bsz, -1))
73                 #shape:[seq_len,bsz,count_dim]->[bsz,seq_len,count_dim]->[b,seq_len*count_dim]
74                 #另外如果這裡沒有contiguous(),View無法工作,因為permute操作將重新定義下標與元素的對應關係
75                 #內部資料的佈局方式和從頭開始建立一個這樣的常規的tensor的佈局方式不一樣了
76             else:
77                 outputs.append(output)
78         if self.concat:
79             return torch.cat(outputs, dim=2)  #需要拼接,將每一層得到的output的最後一維hidden進行拼接
80         return outputs[-1]   #返回最後一層的output   input.shape = [40,512,100+20+20]  output.shape = [40,512,hidden_size=128]

參考:

關於nn.embedding的中padding_idx的含義:https://blog.csdn.net/weixin_40426830/article/details/108870956

pytorch中的nn.Bilinear的計算原理詳解 :https://blog.csdn.net/nihate/article/details/90480459

梯度:torch的.data方法:https://tangshusen.me/Dive-into-DL-PyTorch/#/chapter02_prerequisite/2.3_autograd

PyTorch中LSTM的輸出格式:https://zhuanlan.zhihu.com/p/39191116

Pytorch—tensor.expand_as()函式示例:https://blog.csdn.net/wenqiwenqi123/article/details/101306839

torch.nn.utils.rnn.pack_padded_sequence解讀:https://www.cnblogs.com/yuqinyuqin/p/14100967.html

torch.matmul()用法介紹:https://blog.csdn.net/qsmx666/article/details/105783610

Pytorch中的contiguous理解:https://blog.csdn.net/gdymind/article/details/82662502?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai