循環神經網絡---LSTM模型


補充:

常見的激活函數:https://blog.csdn.net/tyhj_sf/article/details/79932893

常見的損失函數:https://blog.csdn.net/github_38140310/article/details/85061849

一、LSTM原理

 

拆分理解:

如果不加門結構的話,細胞的狀態類似於輸送帶,細胞的狀態在整個鏈上運行,只有一些小的線性操作作用其上,信息很容易保持不變的流過整個鏈。

門(Gate)是一種可選地讓信息通過的方式。 它由一個Sigmoid神經網絡層和一個點乘法運算組成。簡單理解就是對數據進行一下運算,看結果情況對運算的信息是否進行處理。Sigmoid神經網絡層輸出0和1之間的數字,這個數字描述每個組件有多少信息可以通過, 0表示不通過任何信息,1表示全部通過。LSTM有三個門,用於保護和控制細胞的狀態。

1、忘記門

LSTM的第一步是決定我們要從細胞狀態中丟棄什么信息。 該決定由被稱為“忘記門”的Sigmoid層實現。它查看ht-1(前一個輸出)和xt(當前輸入),並為單元格狀態Ct-1(上一個狀態)中的每個數字輸出0和1之間的數字。1代表完全保留,而0代表徹底刪除。

2、輸入門

下一步是決定我們要在細胞狀態中存儲什么信息。 這部分分為兩步。 首先,稱為“輸入門層”的Sigmoid層決定了我們將更新哪些值。 接下來一個tanh層創建候選向量Ct,該向量將會被加到細胞的狀態中。 在下一步中,我們將結合這兩個向量來創建更新值。
現在是時候去更新上一個狀態值Ct−1了,將其更新為Ct。簽名的步驟以及決定了應該做什么,我們只需實際執行即可。
我們將上一個狀態值乘以ft,以此表達期待忘記的部分。之后我們將得到的值加上 it∗C̃ t。這個得到的是新的候選值, 按照我們決定更新每個狀態值的多少來衡量.
在語言模型的例子中,對應着實際刪除關於舊主題性別的信息,並添加新信息,正如在之前的步驟中描述的那樣。

 3、輸出門

最后,我們需要決定我們要輸出什么。 此輸出將基於我們的細胞狀態,但將是一個過濾版本。 首先,我們運行一個sigmoid層,它決定了我們要輸出的細胞狀態的哪些部分。 然后,我們將單元格狀態通過tanh(將值規范化到-1和1之間),並將其乘以Sigmoid門的輸出,至此我們只輸出了我們決定的那些部分。

參考:
https://www.jianshu.com/p/4b4701beba92
https://zhuanlan.zhihu.com/p/32085405
 

二、LSTM api

torch.nn.LSTM(input_size,hidden_size,num_layers,batch_first,dropout,bidirectional)

參數列表:

  1. input_size:輸入數據的形狀,即embedding_dim
  2. hidden_size:隱藏層神經元的數量,即每一層有多少個LSTM單元,即隱藏層節點的個數,這個和單層感知器的結構是類似的。這個維數值是自定義的,根據具體業務需要決定
  3. num_layer :即RNN的中LSTM單元的層數,lstm隱層的層數,默認為1,LSTM 堆疊的層數,默認值是1層,如果設置為2,第二個LSTM接收第一個LSTM的計算結果。也就是第一層輸入 [ X0 X1 X2 ... Xt],計算出 [ h0 h1 h2 ... ht ],第二層將 [ h0 h1 h2 ... ht ] 作為 [ X0 X1 X2 ... Xt] 輸入再次計算,輸出最后的 [ h0 h1 h2 ... ht ]。
  4. batch_first:默認值為False,輸入的數據需要[seq_len,batch,feature],如果為True,則為[batch,seq_len,feature]
  5. dropout:  dropout的比例,默認值為0。dropout是一種訓練過程中讓部分參數隨機失活的一種方式,能夠提高訓練速度,同時能夠解決過擬合的問題。這里是在LSTM的最后一層,對每個輸出進行dropout
  6. bidirectional:是否使用雙向LSTM,默認是False。是否是雙向 RNN,默認為:false,若為 true,則:num_directions=2,否則為1。 我的理解是,LSTM 可以根據數據輸入從左向右推導結果。然后再用結果從右到左反推導,看原因和結果之間是否可逆。也就是原因和結果是一對一關系,還是多對一的關系。這僅僅是我膚淺的假設,有待證明。
  7. bias: 隱層狀態是否帶bias,默認為true。bias是偏置值,或者偏移值。沒有偏置值就是以0為中軸,或以0為起點。偏置值的作用請參考單層感知器相關結構。

輸入:input, (h0, c0)

輸入數據格式:
input    (seq_len, batch, input_size)
h0        (num_layers * num_directions, batch, hidden_size)
c0        (num_layers * num_directions, batch, hidden_size)

輸出:output, (hn,cn)

output  (seq_len, batch, hidden_size * num_directions)
hn        (num_layers * num_directions, batch, hidden_size)
cn        (num_layers * num_directions, batch, hidden_size)

理解

input:

第一維度體現的是batch_size,也就是一次性喂給網絡多少條句子,或者股票數據中的,一次性喂給模型多少個時間單位的數據,具體到每個時刻,也就是一次性喂給特定時刻處理的單元的單詞數或者該時刻應該喂給的股票數據的條數。上圖中10表示一次性喂給模型10個句子。

第二維體現的是序列(sequence)結構,也就是序列的個數,用文章來說,就是每個句子的長度,因為是喂給網絡模型,一般都設定為確定的長度,也就是我們喂給LSTM神經元的每個句子的長度,當然,如果是其他的帶有帶有序列形式的數據,則表示一個明確分割單位長度。上圖中40表示10個句子的統一長度均為40個單詞。

例如是如果是股票數據內,這表示特定時間單位內,有多少條數據。這個參數也就是明確這個層中有多少個確定的單元來處理輸入的數據

第三維度體現的是輸入的元素(elements of input),也就是,每個具體的單詞用多少維向量來表示,或者股票數據中 每一個具體的時刻的采集多少具體的值,比如最低價,最高價,均價,5日均價,10均價,等等。上圖中100表示每個單詞的詞向量是100維的。

h0-hn

就是每個時刻中間神經元應該保存的這一時刻的根據輸入和上一課的時候的中間狀態值應該產生的本時刻的狀態值,這個數據單元是起的作用就是記錄這一時刻之前考慮到所有之前輸入的狀態值,形狀應該是和特定時刻的輸出一致

c0-cn

就是開關,決定每個神經元的隱藏狀態值是否會影響的下一時刻的神經元的處理,形狀應該和h0-hn一致。當然如果是雙向,和多隱藏層還應該考慮方向和隱藏層的層數。

 

三、問題

LSTM模型輸出的output是啥意思?

輸出的h_n是啥意思?

輸出的c_n是啥意思?

理解:

h_n:只返回最后一個時間步的隱藏層輸出,第i層會輸出h(i)nhn(i),所以第一維為num_layers * num_directions,第二維的維度為batch_size,第三位就是hh本身的維度大小,即hidden_size。

c_ncn的維度同hn

output:返回每個時間步的隱藏層輸出,所以第一維為seq_len,第二維的維度為batch_size,第三維就是hidden_size,雙向的話拼接起來就是2*hidden_size,所以就是num_directions * hidden_size。h_n和c_n我都理解分別是上圖中橫向箭頭中的下方箭頭和上方的箭頭,那output是干什么用的?output是每個時間t,LSTM最后一層的輸出特征h_t。

由於 h_n 和 output 都包含了最后一個時間步的隱藏層輸出,所以output[1,:,:]=hn[1,:,:]

【注】如果batch_first=True,則 output[:,1,:]=hn[1,:,:]

實驗一下:

使用文本情感分類的demo,雙向lstm,

batch_size設置為64,

batch_first=False

seq_len:500

hidden_size :64

num_directions:2

num_layers:2

 所以此時的output形狀為【500,64,64*2】,h_n形狀為【2*2,64,64】

反向的情況下:

output[0,:,-64:] == h_n[-1]

正向的情況下:

output[-1,:,:64] == h_n[-2]

 結果是:

參考:

https://www.cnblogs.com/zyb993963526/p/13786310.html

https://www.cnblogs.com/LiuXinyu12378/p/12322993.html

四、文本情感分類demo

使用的雙向LSTM模型

訓練結果:

 測試結果:

 完整代碼:

  1 import torch
  2 import torch.nn as nn
  3 import torch.nn.functional as F
  4 from torch import optim
  5 import os
  6 import re
  7 import pickle
  8 import numpy as np
  9 from torch.utils.data import Dataset, DataLoader
 10 from tqdm import tqdm
 11 
 12 
 13 dataset_path = r'C:\Users\ci21615\Downloads\aclImdb_v1\aclImdb'
 14 MAX_LEN = 500
 15 
 16 def tokenize(text):
 17     """
 18     分詞,處理原始文本
 19     :param text:
 20     :return:
 21     """
 22     fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>', '\?', '@'
 23         , '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '', '', ]
 24     text = re.sub("<.*?>", " ", text, flags=re.S)
 25     text = re.sub("|".join(fileters), " ", text, flags=re.S)
 26     return [i.strip() for i in text.split()]
 27 
 28 
 29 class ImdbDataset(Dataset):
 30     """
 31     准備數據集
 32     """
 33     def __init__(self, mode):
 34         super(ImdbDataset, self).__init__()
 35         if mode == 'train':
 36             text_path = [os.path.join(dataset_path, i) for i in ['train/neg', 'train/pos']]
 37         else:
 38             text_path = [os.path.join(dataset_path, i) for i in ['test/neg', 'test/pos']]
 39         self.total_file_path_list = []
 40         for i in text_path:
 41             self.total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
 42 
 43     def __getitem__(self, item):
 44         cur_path = self.total_file_path_list[item]
 45         cur_filename = os.path.basename(cur_path)
 46         # 獲取標簽
 47         label_temp = int(cur_filename.split('_')[-1].split('.')[0]) - 1
 48         label = 0 if label_temp < 4 else 1
 49         text = tokenize(open(cur_path, encoding='utf-8').read().strip())
 50         return label, text
 51 
 52     def __len__(self):
 53         return len(self.total_file_path_list)
 54 
 55 
 56 class Word2Sequence():
 57     UNK_TAG = 'UNK'
 58     PAD_TAG = 'PAD'
 59     UNK = 0
 60     PAD = 1
 61 
 62     def __init__(self):
 63         self.dict = {
 64             self.UNK_TAG: self.UNK,
 65             self.PAD_TAG: self.PAD
 66         }
 67         self.count = {} # 統計詞頻
 68 
 69     def fit(self, sentence):
 70         """
 71         把單個句子保存到dict中
 72         :return:
 73         """
 74         for word in sentence:
 75             self.count[word] = self.count.get(word, 0) + 1
 76 
 77     def build_vocab(self, min=5, max=None, max_feature=None):
 78         """
 79         生成詞典
 80         :param min: 最小出現的次數
 81         :param max: 最大次數
 82         :param max_feature: 一共保留多少個詞語
 83         :return:
 84         """
 85         # 刪除詞頻小於min的word
 86         if min is not None:
 87             self.count = {word:value for word,value in self.count.items() if value > min}
 88         # 刪除詞頻大於max的word
 89         if max is not None:
 90             self.count = {word:value for word,value in self.count.items() if value < max}
 91         # 限制保留的詞語數
 92         if max_feature is not None:
 93             temp = sorted(self.count.items(), key=lambda x:x[-1],reverse=True)[:max_feature]
 94             self.count = dict(temp)
 95         for word in self.count:
 96             self.dict[word] = len(self.dict)
 97         # 得到一個反轉的字典
 98         self.inverse_dict = dict(zip(self.dict.values(), self.dict.keys()))
 99 
100     def transform(self, sentence, max_len=None):
101         """
102         把句子轉化為序列
103         :param sentence: [word1, word2...]
104         :param max_len: 對句子進行填充或裁剪
105         :return:
106         """
107         if max_len is not None:
108             if max_len > len(sentence):
109                 sentence = sentence + [self.PAD_TAG] * (max_len - len(sentence)) # 填充
110             if max_len < len(sentence):
111                 sentence = sentence[:max_len] # 裁剪
112         return [self.dict.get(word, self.UNK) for word in sentence]
113 
114     def inverse_transform(self, indices):
115         """
116         把序列轉化為句子
117         :param indices: [1,2,3,4...]
118         :return:
119         """
120         return [self.inverse_dict.get(idx) for idx in indices]
121 
122     def __len__(self):
123         return len(self.dict)
124 
125 
126 def fit_save_word_sequence():
127     """
128     從數據集構建字典
129     :return:
130     """
131     ws = Word2Sequence()
132     train_path = [os.path.join(dataset_path, i) for i in ['train/neg', 'train/pos']]
133     total_file_path_list = []
134     for i in train_path:
135         total_file_path_list.extend([os.path.join(i, j) for j in os.listdir(i)])
136     for cur_path in tqdm(total_file_path_list, desc='fitting'):
137         sentence = open(cur_path, encoding='utf-8').read().strip()
138         res = tokenize(sentence)
139         ws.fit(res)
140     # 對wordSequesnce進行保存
141     ws.build_vocab(min=10)
142     # pickle.dump(ws, open('./lstm_model/ws.pkl', 'wb'))
143     return ws
144 
145 
146 def get_dataloader(mode='train', batch_size=20, ws=None):
147     """
148     獲取數據集,轉換成詞向量后的數據集
149     :param mode:
150     :return:
151     """
152     # 導入詞典
153     # ws = pickle.load(open('./model/ws.pkl', 'rb'))
154     # 自定義collate_fn函數
155     def collate_fn(batch):
156         """
157         batch是list,其中是一個一個元組,每個元組是dataset中__getitem__的結果
158         :param batch:
159         :return:
160         """
161         batch = list(zip(*batch))
162         labels = torch.LongTensor(batch[0])
163         texts = batch[1]
164         # 獲取每個文本的長度
165         lengths = [len(i) if len(i) < MAX_LEN else MAX_LEN for i in texts]
166         # 每一段文本句子都轉換成了n個單詞對應的數字組成的向量,即500個單詞數字組成的向量
167         temp = [ws.transform(i, MAX_LEN) for i in texts]
168         texts = torch.LongTensor(temp)
169         del batch
170         return labels, texts, lengths
171     dataset = ImdbDataset(mode)
172     dataloader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
173     return dataloader
174 
175 
176 class ImdbLstmModel(nn.Module):
177 
178     def __init__(self, ws):
179         super(ImdbLstmModel, self).__init__()
180         self.hidden_size = 64   # 隱藏層神經元的數量,即每一層有多少個LSTM單元
181         self.embedding_dim = 200    # 每個詞語使用多長的向量表示
182         self.num_layer = 2  # 即RNN的中LSTM單元的層數
183         self.bidriectional = True  # 是否使用雙向LSTM,默認是False,表示雙向LSTM,也就是序列從左往右算一次,從右往左又算一次,這樣就可以兩倍的輸出
184         self.num_directions = 2 if self.bidriectional else 1 # 是否雙向取值,雙向取值為2,單向取值為1
185         self.dropout = 0.5  # dropout的比例,默認值為0。dropout是一種訓練過程中讓部分參數隨機失活的一種方式,能夠提高訓練速度,同時能夠解決過擬合的問題。這里是在LSTM的最后一層,對每個輸出進行dropout
186         # 每個句子長度為500
187         # ws = pickle.load(open('./model/ws.pkl', 'rb'))
188         print(len(ws))
189         self.embedding = nn.Embedding(len(ws), self.embedding_dim)
190         self.lstm = nn.LSTM(self.embedding_dim,self.hidden_size,self.num_layer,bidirectional=self.bidriectional,dropout=self.dropout)
191         self.fc = nn.Linear(self.hidden_size * self.num_directions, 20)
192         self.fc2 = nn.Linear(20, 2)
193 
194     def init_hidden_state(self, batch_size):
195         """
196         初始化 前一次的h_0(前一次的隱藏狀態)和c_0(前一次memory)
197         :param batch_size:
198         :return:
199         """
200         h_0 = torch.rand(self.num_layer * self.num_directions, batch_size, self.hidden_size)
201         c_0 = torch.rand(self.num_layer * self.num_directions, batch_size, self.hidden_size)
202         return h_0, c_0
203 
204     def forward(self, input):
205         # 句子轉換成詞向量
206         x = self.embedding(input)
207         # 如果batch_first為False的話轉換一下seq_len和batch_size的位置
208         x = x.permute(1,0,2)    # [seq_len, batch_size, embedding_num]
209         # 初始化前一次的h_0(前一次的隱藏狀態)和c_0(前一次memory)
210         h_0, c_0 = self.init_hidden_state(x.size(1))    # [num_layers * num_directions, batch, hidden_size]
211         output, (h_n, c_n) = self.lstm(x, (h_0, c_0))
212 
213         # 獲取反向的最后一個output和反向的最后一層h_n
214         a_output_last = output[0,:,-64:]
215         a_h_n_last = h_n[-1]
216         print(a_output_last.eq(a_h_n_last))
217         # 獲取正向的最后一個output和正向的最后一層h_n
218         b_output_last = output[-1,:,:64]
219         b_h_n_last = h_n[-2]
220         print(b_output_last.eq(b_h_n_last))
221 
222         # 只要最后一個lstm單元處理的結果,這里多去的hidden state
223         out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
224         out = self.fc(out)
225         out = F.relu(out)
226         out = self.fc2(out)
227         return F.log_softmax(out, dim=-1)
228 
229         # output, (h_n, c_n) = self.lstm(x, (h_0, c_0))
230         # # g = output[-1,:,:]
231         # # f = h_n[-1,:,:]
232         # # a = h_n[-2,:,:]
233         # # b = h_n[-1,:,:]
234         # # out = torch.cat([a, b], dim=-1)
235         # o = output[-1,:,:]
236         # out = self.fc(o)
237         # out = F.relu(out)
238         # out = self.fc2(out)
239         # res = F.log_softmax(out, dim=-1)
240         # return res
241 
242 
243 train_batch_size = 64
244 test_batch_size = 5000
245 
246 def train(epoch, ws):
247     """
248     訓練
249     :param epoch: 輪次
250     :param ws: 字典
251     :return:
252     """
253     mode = 'train'
254     imdb_lstm_model = ImdbLstmModel(ws)
255     optimizer = optim.Adam(imdb_lstm_model.parameters())
256     for i in range(epoch):
257         train_dataloader = get_dataloader(mode=mode, batch_size=train_batch_size, ws=ws)
258         for idx, (target, input, input_length) in enumerate(train_dataloader):
259             optimizer.zero_grad()
260             output = imdb_lstm_model(input)
261             loss = F.nll_loss(output, target)
262             loss.backward()
263             optimizer.step()
264 
265             pred = torch.max(output, dim=-1, keepdim=False)[-1]
266             acc = pred.eq(target.data).numpy().mean() * 100.
267             print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t ACC: {:.6f}'.format(i, idx * len(input), len(train_dataloader.dataset),
268                                                                                      100. * idx / len(train_dataloader), loss.item(), acc))
269     torch.save(imdb_lstm_model.state_dict(), 'model/lstm_model.pkl')
270     torch.save(optimizer.state_dict(), 'model/lstm_optimizer.pkl')
271 
272 
273 def test(ws):
274     mode = 'test'
275     # 載入模型
276     lstm_model = ImdbLstmModel(ws)
277     lstm_model.load_state_dict(torch.load('model/lstm_model.pkl'))
278     optimizer = optim.Adam(lstm_model.parameters())
279     optimizer.load_state_dict(torch.load('model/lstm_optimizer.pkl'))
280     lstm_model.eval()
281     test_dataloader = get_dataloader(mode=mode, batch_size=test_batch_size, ws=ws)
282     with torch.no_grad():
283         for idx, (target, input, input_length) in enumerate(test_dataloader):
284             output = lstm_model(input)
285             test_loss = F.nll_loss(output, target, reduction='mean')
286             pred = torch.max(output, dim=-1, keepdim=False)[-1]
287             correct = pred.eq(target.data).sum()
288             acc = 100. * pred.eq(target.data).cpu().numpy().mean()
289             print('idx: {} Test set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(idx, test_loss, correct, target.size(0), acc))
290 
291 
292 if __name__ == '__main__':
293     # 構建字典
294     ws = fit_save_word_sequence()
295     # 訓練
296     train(10, ws)
297     # 測試
298     # test(ws)
View Code

 

參考:

https://www.cnblogs.com/luckyplj/p/13370072.html

https://blog.csdn.net/wangwangstone/article/details/90296461

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM