【神經網路】LSTM在Pytorch中的使用
先附上張玉騰大佬的內容,我覺得說的非常明白,原文閱讀連結我放在下面,方面大家檢視。
LSTM的輸入與輸出:
- output儲存了最後一層,每個time step的輸出h,如果是雙向LSTM,每個time step的輸出h = [h正向, h逆向] (同一個time step的正向和逆向的h連線起來)。
- h_n儲存了每一層,最後一個time step的輸出h,如果是雙向LSTM,單獨儲存前向和後向的最後一個time step的輸出h。
- 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