赞
踩
我们生活的世界充满了形形色色的序列数据,只要是有顺序的数据统统都可以看作是序列数据,比如文字是字符的序列,音乐是音符组成的序列,股价数据也是序列,连DNA序列也属于序列数据。循环神经网络RNN天生就具有处理序列数据的能力。我们在上一篇文章中介绍了循环神经网络,这次我们来看一个具体的序列生成问题:
假设一个简单的序列生成问题,01序列生成。一个序列数据由0和1组成,并且每个0之间是连续的,每个1之间也是连续的,并且0和1的个数相等,比如:
000111
00001111,...,等等
这种序列被称为“上下文无关文法”。如果现在的数据是0000,那下一位其实不好判断,可能是0,也可能是1;如果现在的数据是00001,那下一位一定是1;如果现在数据是00001111,那由于0和1相等,接下来应该结束这个序列。
我们首先尝试生成这种数据:
- class MyDataset(Dataset):
- def __init__(self, samples = 2000, max_size=10, transform=None):
- max_size=10
- self.data = []
- for m in range(samples):
- probability = 1.0*np.array([10,6,4,3,1,1,1,1,1,1])
- probability = probability[:max_size]
- # 生成概率值
- probability = probability/np.sum(probability)
- # 生成训练样本
- # 对于每一个生成的字符串,随机选择一个n,n被选择的权重记录在probability中
- n = np.random.choice(range(1, max_size+1), p=probability)
- inputs = [0]*n+[1]*n # 生成仅包含0和1的序列
- inputs.insert(0,3) # 序列开始处插入3,作为标志位
- inputs.append(2) # 序列结束处插入2,作为标志位
- self.data.append(inputs)
-
- def __len__(self):
- return len(self.data)
-
- def __getitem__(self, idx):
- return self.data[idx]
这里,我们首先默认生成2000个样本,每个样本的最大长度为10个数字,probability代表的是生成10个概率值,我们可以把这10个概率值输出来看看:
- array([0.34482759, 0.20689655, 0.13793103, 0.10344828, 0.03448276,
- 0.03448276, 0.03448276, 0.03448276, 0.03448276, 0.03448276])
可以看到第一个概率最大,接近0.35,之后依次减少,最后六个概率都仅仅是第一个概率的十分之一,0.035左右。np.random.choice(array, prob),是指根据prob指明的概率值输出array中的值,我们可以把生成的n打印出来:
- for i in range(10):
- n = np.random.choice(range(1, max_size+1), p=probability)
- print(n, end=' ') # 以空格分隔
- # 这其实就是说,从[1,2,3,4,5,6,7,8,9,10]中,按概率probability随机抽取数字
- # 输出:
- # 5 3 1 1 1 1 1 1 6 7
可以多尝试几次,总之,1出现的概率最大,1和2出现的次数都很多,符合我们的直观印象。目的就是为了生成长度为n的随机的01字符串(n个0加上0个1)。同时在字符串的前端加上3,表示字符串起始位置,在字符串的末尾加上2,表示字符串的结束位置,如3000011112,30001112,30000001111112。
我们把生成的样本库用pytorch的dataset和dataloader包装一下,并取出来观察一下:
- if __name__ == '__main__':
- # 定义训练数据集
- trainDataset = MyDataset()
- trainDataloader = DataLoader(dataset=trainDataset, batch_size=1, shuffle=True)
- # 查看数据集内容
- for i,seq in enumerate(trainDataloader):
- print(seq)
- # 输出
- [tensor([3]), tensor([0]), tensor([1]), tensor([2])]
- [tensor([3]), tensor([0]), tensor([0]), tensor([0]), tensor([0]), tensor([1]), tensor([1]), tensor([1]), tensor([1]), tensor([2])]
- [tensor([3]), tensor([0]), tensor([0]), tensor([1]), tensor([1]), tensor([2])]
- [tensor([3]), tensor([0]), tensor([0]), tensor([0]), tensor([1]), tensor([1]), tensor([1]), tensor([2])]
- [tensor([3]), tensor([0]), tensor([1]), tensor([2])]
- [tensor([3]), tensor([0]), tensor([1]), tensor([2])]
可以看到,生成了3开头,2结尾的01序列,只不过都转成了Tensor类型。
下面,我们来实现一下自定义的RNN模型:
- class MyRnn(nn.Module):
- def __init__(self, input_size, hidden_size, output_size, num_layers = 1):
- # 定义
- super(MyRnn, self).__init__()
- self.hidden_size = hidden_size
- self.num_layers = num_layers
- # 一个embedding层
- self.embedding = nn.Embedding(input_size, hidden_size)
- # PyTorch的RNN层,batch_first标识可以让输入的张量的第一个维度表示batch指标
- self.rnn = nn.RNN(hidden_size, hidden_size, num_layers, batch_first = True)
- # 输出的全连接层
- self.fc = nn.Linear(hidden_size, output_size)
- # 最后的LogSoftmax层
- self.softmax = nn.LogSoftmax(dim=1)
-
- def forward(self, input):
- # 运算过程
- # 先进行embedding层的计算
- # 它可以把一个数值先转化为one-hot向量,再把这个向量转化为一个hidden_size维的向量
- # input的尺寸为:batch_size, num_step, data_dim
- x = self.embedding(input)
- # 从输入层到隐含层的计算
- # x的尺寸为:batch_size, num_step, hidden_size
- h0 = torch.zeros(self.num_layers, 1, self.hidden_size)
- output, hidden = self.rnn(x, h0)
- # 从输出output中取出最后一个时间步的数值,注意output包含了所有时间步的结果
- # output尺寸为:batch_size, num_step, hidden_size
- output = output[:,-1,:]
- # output尺寸为:batch_size, hidden_size
- # 把前面的结果输入给最后一层全连接网络
- output = self.fc(output)
- # output尺寸为:batch_size, output_size
- # softmax函数
- output = self.softmax(output)
- return output, hidden
这里,有个地方需要注意,和我上次的循环神经网络简介中介绍的RNN模型不同,多了一个embedding层。这里要特别提一下,embedding层是RNN,特别是自然语言处理(NLP)等工作中特别重要的一个步骤。embedding就是做一个查表,把输入数据映射到一个新的表空间的,本质上其实就是进行了一次降维或升维操作,有点类似卷积神经网络的1x1卷积的作用,下面我们来具体看看embedding操作到底是干了什么。
- import torch
- import torch.nn as nn
-
- embedding = nn.Embedding(10, 3) # an Embedding module containing 10 tensors of size 3
- input = torch.LongTensor([[1,2,4,5],[4,3,2,9]]) # a batch of 2 samples of 4 indices each
- e = embedding(input)
- print(e)
- # 输出
- tensor([[[-1.1222, 0.5364, -1.5284],
- [-0.4762, 0.7091, -0.0703],
- [-1.1492, 0.2443, 0.4336],
- [ 1.8044, 0.5492, 1.1109]],
-
- [[-1.1492, 0.2443, 0.4336],
- [-0.4599, -0.3097, 0.2294],
- [-0.4762, 0.7091, -0.0703],
- [ 0.6926, 1.7407, 1.5432]]], grad_fn=<EmbeddingBackward0>)
是不是很奇怪?我们的输入仅仅是一个2x4的二维张量,怎么变成了2x4x3的三维张量了,并且这些值是哪儿来的呢?别急,我们一步步来看。
- print(input.shape)
- print(e.shape)
- print(embedding.weight)
- print(embedding.weight.shape)
- # 输出
- torch.Size([2, 4])
- torch.Size([2, 4, 3])
- Parameter containing:
- tensor([[ 1.5728, 0.1152, 2.1069],
- [-1.1222, 0.5364, -1.5284],
- [-0.4762, 0.7091, -0.0703],
- [-0.4599, -0.3097, 0.2294],
- [-1.1492, 0.2443, 0.4336],
- [ 1.8044, 0.5492, 1.1109],
- [-1.3228, 0.2966, 0.5020],
- [-0.4501, -0.1242, -0.2341],
- [ 0.0743, 0.8168, -1.2459],
- [ 0.6926, 1.7407, 1.5432]], requires_grad=True)
- torch.Size([10, 3])
可以看到,我们输入的大小确实是(2,4),经过embedding的输出也确实是(2,4,3)了,我们可以看到,embedding的大小是(10,3),其实简单来说就是,embedding生成了一张10行3列的表,表中的数据是embedding自动计算出来的权重,input里面的值可以看成是索引,[[1,2,4,5],[4,3,2,9]],我们看到第一个数字1,那我们从embedding表中取到第1行,[-1.1222, 0.5364, -1.5284],依次类推,得到最后的结果,如下图所示:
等于说input里面的值变成了embedding权重表里面的索引值,根据这个索引值去查询embedding表的权重,组成的结果就是把输入进行embedding后的结果。
下面我们再来详细分析一下我们的循环神经网络模型MyRnn里面的运算过程,只要真正理解了这个过程,那就明白了模型里面的运作原理了。
首先,我有一个输入,假设是tensor[1]
- input = torch.LongTensor([1]).unsqueeze(0) # 扩展一个维度,作为batch_size
- print(input.shape)
- print(input)
- # 输出
- torch.Size([1, 1])
- tensor([[1]])
下面,我们进行embedding操作,我们的模型假设input_size=4,hidden_size=2,output_size=3,因为输入有0,1,2,3四种可能,输出有0,1,2三种可能。
- # input_size=4,hidden_size=2
- embedding = nn.Embedding(4, 2)
- print(embedding.weight)
- print(embedding.weight.shape)
- x = embedding(input) # embedding操作
- print(x)
- print(x.shape)
- # 输出
- Parameter containing:
- tensor([[ 0.1870, 0.5711],
- [ 0.2994, -0.4649],
- [-0.3611, -0.1150],
- [-1.0868, -0.1419]], requires_grad=True)
- torch.Size([4, 2])
- tensor([[[ 0.2994, -0.4649]]], grad_fn=<EmbeddingBackward0>)
- torch.Size([1, 1, 2])
可以看到,做了embedding操作后,我们的输入从[[1]]变成了[[[ 0.2994, -0.4649]]],接下来,我们把这个结果和初始化的隐藏层hidden一起送入RNN。
- h = torch.zeros(1, x.size(0), 2) # 隐藏层参数layer_size,batch_size,hidden_size
- print(h)
- print(h.shape)
- rnn = nn.RNN(2, 2, batch_first=True) # RNN模型,因为经过了embedding,input的shape变成了和hidden一样的shape
隐藏层的三个参数分别为隐藏层大小layer_size,这里取1,batch_size这里就是x第一个维度,也就是1,hidden_size,设置隐藏层的大小为2。
- out, _ = rnn(x, h) # RNN结果
- print(out.shape)
- print(out)
- print(out[:,-1,:]) # 取出结果
- # 输出
- torch.Size([1, 1, 2])
- tensor([[[-0.2823, -0.5443]]], grad_fn=<TransposeBackward1>)
- tensor([[-0.2823, -0.5443]], grad_fn=<SliceBackward0>)
可以看到我们现在取出的out结果的shape从[1,1,2]变成了[1,2],最后我们做一个全连接运算,把分类结果输出:
- fc = nn.Linear(2, 3) # 全连接层
- out = fc(out[:, -1, :]) # 分类结果
- print(out)
- # 输出
- tensor([[0.7543, 0.5506, 0.8528]], grad_fn=<AddmmBackward0>)
可以看到得到了三个值的分类结果,根据这个值再去获得概率最高的结果,就是最终的输出结果。好了,这就是我们整个模型内部的运行过程。下面给出完整的训练和测试代码:
训练代码:
- def train(net, dataloader, epochs=50):
- criterion = torch.nn.NLLLoss()
- optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
- for epoch in range(epochs):
- train_loss = 0.
- for i, seq in enumerate(dataloader):
- loss = 0
- hidden = net.initHidden()
- for k in range(len(seq)-1):
- x = torch.LongTensor([seq[k]]).unsqueeze(0) # 增加一个batch_size的维度
- y = torch.LongTensor([seq[k+1]])
- #print(x)
- #print(y)
- output, hidden = net(x, hidden)
- #output, _ = net(x)
- loss += criterion(output, y)
- loss = 1.0*loss/len(seq) # 计算每个字符的损失值
- optimizer.zero_grad()
- loss.backward()
- optimizer.step()
- train_loss += loss
- if i>0 and i%500==0:
- print('第{}轮,第{}个,训练Loss:{:.2f}'.format(epoch, i, train_loss.data.numpy()/i))
- torch.save(net, 'checkpoints/RNN.pt')
验证代码:
- def val(net, dataloader):
- valid_loss = 0
- errors = 0
- show_out = ''
- net = torch.load('checkpoints/RNN.pt')
- net.eval()
- with torch.no_grad():
- for i, seq in enumerate(dataloader):
- # 对valid_set中的每一个字符串进行循环
- loss = 0
- outstring = ''
- targets = ''
- diff = 0
- hidden = net.initHidden() # 初始化隐含神经元
- for t in range(len(seq) - 1):
- # 对每一个字符进行循环
- x = torch.LongTensor([seq[t]]).unsqueeze(0)
- # x尺寸:batch_size = 1, time_steps = 1, data_dimension = 1
- y = torch.LongTensor([seq[t + 1]])
- # y尺寸:batch_size = 1, data_dimension = 1
- output, hidden = net(x, hidden)
- # output尺寸:batch_size, output_size = 3
- # hidden尺寸:layer_size =1, batch_size=1, hidden_size
- mm = torch.max(output, 1)[1][0] # 将概率最大的元素作为输出
- outstring += str(mm.data.numpy()) # 合成预测的字符串
- targets += str(y.data.numpy()[0]) # 合成目标字符串
- #loss += criterion(output, y) # 计算损失函数
- diff += 1 - mm.eq(y).data.numpy()[0] # 计算模型输出字符串与目标字符串之间存在差异的字符数量
- #loss = 1.0 * loss / len(seq)
- #valid_loss += loss # 累积损失函数值
- errors += diff # 计算累积错误数
- show_out = outstring + '\n' + targets
- # 打印结果
- print(output[0][2].data.numpy())
- print(show_out)
测试代码:
- def test():
- net = torch.load('checkpoints/RNN.pt')
- net.eval()
- for n in range(20):
- inputs = [0]*n + [1]*n
- inputs.insert(0,3)
- inputs.append(2)
- outstring = ''
- targets = ''
- diff = 0
- hiddens = []
- hidden = net.initHidden()
- for t in range(len(inputs)-1):
- x = Variable(torch.LongTensor([inputs[t]]).unsqueeze(0))
- y = Variable(torch.LongTensor([inputs[t+1]]))
- output,hidden = net(x, hidden)
- hiddens.append(hidden.data.numpy()[0][0])
- mm = torch.max(output, 1)[1][0]
- outstring += str(mm.data.numpy())
- targets += str(y.data.numpy()[0])
- diff += 1-mm.eq(y).data.numpy()[0]
- # 打印每一个生成的字符串和目标字符串
- print("n======",n)
- print(outstring)
- print(targets)
- print('Diff:{}'.format(diff))
最终输出:
- 第49轮,第500个,训练Loss:0.24
- 第49轮,第1000个,训练Loss:0.24
- 第49轮,第1500个,训练Loss:0.24
- -0.030197786
- 0100112
- 0001112
- n====== 0
- 0
- 2
- Diff:1
- n====== 1
- 012
- 012
- Diff:0
- n====== 2
- 01012
- 00112
- Diff:2
- n====== 3
- 0100112
- 0001112
- Diff:2
- n====== 4
- 010001112
- 000011112
- Diff:2
- n====== 5
- 01000011112
- 00000111112
- Diff:2
- n====== 6
- 0100000111112
- 0000001111112
- Diff:2
- n====== 7
- 010000001111112
- 000000011111112
- Diff:2
- n====== 8
- 01000000011111112
- 00000000111111112
- Diff:2
- n====== 9
- 0100000000111111112
- 0000000001111111112
- Diff:2
- n====== 10
- 010000000001111111112
- 000000000011111111112
- Diff:2
- n====== 11
- 01000000000011111111112
- 00000000000111111111112
- Diff:2
- n====== 12
- 0100000000000111111111112
- 0000000000001111111111112
- Diff:2
- n====== 13
- 010000000000001111111111112
- 000000000000011111111111112
- Diff:2
- n====== 14
- 01000000000000011111111111112
- 00000000000000111111111111112
- Diff:2
- n====== 15
- 0100000000000000111111111111112
- 0000000000000001111111111111112
- Diff:2
- n====== 16
- 010000000000000001111111111111112
- 000000000000000011111111111111112
- Diff:2
- n====== 17
- 01000000000000000011111111111111112
- 00000000000000000111111111111111112
- Diff:2
- n====== 18
- 0100000000000000000111111111111111112
- 0000000000000000001111111111111111112
- Diff:2
- n====== 19
- 010000000000000000001111111111111111112
- 000000000000000000011111111111111111112
- Diff:2
可以看到,模型预测还是比较准确的,就是第二个数字预测错误,以及由0变1的那个位置会预测错误。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。