循环神经网络---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

 

posted @ 2021-10-27 11:28  luyizhou  阅读(2343)  评论(0编辑  收藏  举报