一、GRU介紹
GRU是LSTM網絡的一種效果很好的變體,它較LSTM網絡的結構更加簡單,而且效果也很好,因此也是當前非常流形的一種網絡。GRU既然是LSTM的變體,因此也是可以解決RNN網絡中的長依賴問題。
GRU的參數較少,因此訓練速度更快,GRU能夠降低過擬合的風險。
在LSTM中引入了三個門函數:輸入門、遺忘門和輸出門來控制輸入值、記憶值和輸出值。而在GRU模型中只有兩個門:分別是更新門和重置門。具體結構如下圖所示:
圖中的zt和rt分別表示更新門和重置門。更新門用於控制前一時刻的狀態信息被帶入到當前狀態中的程度,更新門的值越大說明前一時刻的狀態信息帶入越多。重置門控制前一狀態有多少信息被寫入到當前的候選集 h~th~t 上,重置門越小,前一狀態的信息被寫入的越少。
二、GRU與LSTM的比較
(1). GRU相比於LSTM少了輸出門,其參數比LSTM少。
(2). GRU在復調音樂建模和語音信號建模等特定任務上的性能和LSTM差不多,在某些較小的數據集上,GRU相比於LSTM表現出更好的性能。
(3). LSTM比GRU嚴格來說更強,因為它可以很容易地進行無限計數,而GRU卻不能。這就是GRU不能學習簡單語言的原因,而這些語言是LSTM可以學習的。
(4). GRU網絡在首次大規模的神經網絡機器翻譯的結構變化分析中,性能始終不如LSTM。
三、GRU的API
rnn = nn.GRU(input_size, hidden_size, num_layers, bias, batch_first, dropout, bidirectional)
初始化:
input_size: input的特征維度
hidden_size: 隱藏層的寬度
num_layers: 單元的數量(層數),默認為1,如果為2以為着將兩個GRU堆疊在一起,當成一個GRU單元使用。
bias: True or False,是否使用bias項,默認使用
batch_first: Ture or False, 默認的輸入是三個維度的,即:(seq, batch, feature),第一個維度是時間序列,第二個維度是batch,第三個維度是特征。如果設置為True,則(batch, seq, feature)。即batch,時間序列,每個時間點特征。
dropout:設置隱藏層是否啟用dropout,默認為0
bidirectional:True or False, 默認為False,是否使用雙向的GRU,如果使用雙向的GRU,則自動將序列正序和反序各輸入一次。
調用輸入:
rnn(input, h_0)
輸出:
output, hn = rnn(input, h0)
形狀啥的和LSTM差不多,也有雙向
四、情感分類demo修改成GRU
完整代碼:

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 = 1 # 即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.gru = nn.GRU(input_size=self.embedding_dim, hidden_size=self.hidden_size, bidirectional=self.bidriectional) 192 193 self.fc = nn.Linear(self.hidden_size * self.num_directions, 20) 194 self.fc2 = nn.Linear(20, 2) 195 196 def init_hidden_state(self, batch_size): 197 """ 198 初始化 前一次的h_0(前一次的隱藏狀態)和c_0(前一次memory) 199 :param batch_size: 200 :return: 201 """ 202 h_0 = torch.rand(self.num_layer * self.num_directions, batch_size, self.hidden_size) 203 return h_0 204 205 def forward(self, input): 206 # 句子轉換成詞向量 207 x = self.embedding(input) 208 # 如果batch_first為False的話轉換一下seq_len和batch_size的位置 209 x = x.permute(1,0,2) # [seq_len, batch_size, embedding_num] 210 # 初始化前一次的h_0(前一次的隱藏狀態)和c_0(前一次memory) 211 h_0 = self.init_hidden_state(x.size(1)) # [num_layers * num_directions, batch, hidden_size] 212 output, h_n = self.gru(x, h_0) 213 214 # 只要最后一個lstm單元處理的結果,這里多去的hidden state 215 out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1) 216 out = self.fc(out) 217 out = F.relu(out) 218 out = self.fc2(out) 219 return F.log_softmax(out, dim=-1) 220 221 222 train_batch_size = 64 223 test_batch_size = 5000 224 225 def train(epoch, ws): 226 """ 227 訓練 228 :param epoch: 輪次 229 :param ws: 字典 230 :return: 231 """ 232 mode = 'train' 233 imdb_lstm_model = ImdbLstmModel(ws) 234 optimizer = optim.Adam(imdb_lstm_model.parameters()) 235 for i in range(epoch): 236 train_dataloader = get_dataloader(mode=mode, batch_size=train_batch_size, ws=ws) 237 for idx, (target, input, input_length) in enumerate(train_dataloader): 238 optimizer.zero_grad() 239 output = imdb_lstm_model(input) 240 loss = F.nll_loss(output, target) 241 loss.backward() 242 optimizer.step() 243 244 pred = torch.max(output, dim=-1, keepdim=False)[-1] 245 acc = pred.eq(target.data).numpy().mean() * 100. 246 print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}\t ACC: {:.6f}'.format(i, idx * len(input), len(train_dataloader.dataset), 247 100. * idx / len(train_dataloader), loss.item(), acc)) 248 torch.save(imdb_lstm_model.state_dict(), 'model/gru_model.pkl') 249 torch.save(optimizer.state_dict(), 'model/gru_optimizer.pkl') 250 251 252 def test(ws): 253 mode = 'test' 254 # 載入模型 255 lstm_model = ImdbLstmModel(ws) 256 lstm_model.load_state_dict(torch.load('model/lstm_model.pkl')) 257 optimizer = optim.Adam(lstm_model.parameters()) 258 optimizer.load_state_dict(torch.load('model/lstm_optimizer.pkl')) 259 lstm_model.eval() 260 test_dataloader = get_dataloader(mode=mode, batch_size=test_batch_size, ws=ws) 261 with torch.no_grad(): 262 for idx, (target, input, input_length) in enumerate(test_dataloader): 263 output = lstm_model(input) 264 test_loss = F.nll_loss(output, target, reduction='mean') 265 pred = torch.max(output, dim=-1, keepdim=False)[-1] 266 correct = pred.eq(target.data).sum() 267 acc = 100. * pred.eq(target.data).cpu().numpy().mean() 268 print('idx: {} Test set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(idx, test_loss, correct, target.size(0), acc)) 269 270 271 if __name__ == '__main__': 272 # 構建字典 273 ws = fit_save_word_sequence() 274 # 訓練 275 train(10, ws) 276 # 測試 277 # test(ws)
結果展示:
參考:
【重溫經典】GRU循環神經網絡 —— LSTM的輕量級版本,大白話講解