我们生活的世界充满了形形色色的序列数据,只要是有顺序的数据统统都可以看作是序列数据,比如文字是字符的序列,音乐是音符组成的序列,股价数据也是序列,连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的那个位置会预测错误。