補充:
常見的激活函數: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、忘記門

2、輸入門


3、輸出門
參考:
二、LSTM api
torch.nn.LSTM(input_size,hidden_size,num_layers,batch_first,dropout,bidirectional)
參數列表:
input_size
:輸入數據的形狀,即embedding_dimhidden_size
:隱藏層神經元的數量,即每一層有多少個LSTM單元,即隱藏層節點的個數,這個和單層感知器的結構是類似的。這個維數值是自定義的,根據具體業務需要決定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 ]。batch_first
:默認值為False,輸入的數據需要[seq_len,batch,feature]
,如果為True,則為[batch,seq_len,feature]
dropout
: dropout的比例,默認值為0。dropout是一種訓練過程中讓部分參數隨機失活的一種方式,能夠提高訓練速度,同時能夠解決過擬合的問題。這里是在LSTM的最后一層,對每個輸出進行dropoutbidirectional
:是否使用雙向LSTM,默認是False。是否是雙向 RNN,默認為:false,若為 true,則:num_directions=2,否則為1。 我的理解是,LSTM 可以根據數據輸入從左向右推導結果。然后再用結果從右到左反推導,看原因和結果之間是否可逆。也就是原因和結果是一對一關系,還是多對一的關系。這僅僅是我膚淺的假設,有待證明。- 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_n:cn的維度同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)
參考:
https://www.cnblogs.com/luckyplj/p/13370072.html
https://blog.csdn.net/wangwangstone/article/details/90296461