补充:torch.randn()函数返回一个张量,包含了从正态分布(均值为0,方差为1)中抽取的一组随机数。张量的形状由参数决定,参数个数任意。
例如:torch.randn(3,4,5)返回一个shape为[3,4,5]即的张量,张量的元素满足标准正态分布。
一、简要介绍
RNN适用了处理序列性的数据,并利用权重共享机制(即一个RNN网络使用多次)。序列性数据用一个例子来理解,比如我们想要预知某一天是否会下雨,我们采集了大量天数的气象信息,气象信息包括温度、气压和降雨,我们分四天为一组,前三天(用x1,x2,x3表示)就是一个序列,第四天作为已知结果,通过第四天的已知结果与模型的估计值进行比较求损失、求梯度来优化模型参数。使用序列模型的意义在于数据前后之间有一定的联系或影响,即x2的数据依赖于x1,x3的数据依赖于x2,x4的数据依赖于x3,即前三天气象信息与是否降雨对第四天有一定的影响(天气的变化是比较缓和的,很少出现断崖式的变化,所以可以根据前些天的天气预知后一天的天气)。注意,这里的x1,x2,x3是一个序列,而x1,x2,x3三个张量又各自包含三个特征值,分别表示温度、气压和天气。
除了天气、股市等信息外,自然语言也是具有序列关系的信息。
二、RNN神经网络
RNN cell本质上是一个也是线性层(Linear Layer,进行wx+b的线性变换)。区别在于RNN cell的网络是共享的,即使用多次。
上图中的左边部分是RNN模型的一般书写模式,右边部分是其展开模式。,这里的X1,X2,X3,X4就是之前提到过的序列。h0可以是初始化全为0的张量。先将X1和h0(h表示hidden,即隐层)输入RNN cell(RNN 神经网络),输出结果为h1,再将x2和h1(即RNN cell的两个输出是一样的,都是hi)输入相同的RNN cell输出结果h2,依次类推,最终输出结果h4。可以看出h2的结果融合了x1与x2的信息,h3的结果融合了x1、x2与x3的信息,h4的结果融合了x1、x2、x3与x4的信息。
RNN网络的每一个隐藏层的具体过程:
input_size表示输入的维度,输入Xt先做一个线性变换,h(t-1)是上一层隐层的输出,也对其做一个线性变换,再将 两个线性变换的结果相加,上图中的Wih为的shape为[hidden_size, input_size],使得WihXt+bih的结果为[hidden_size,1];Whh为的shape为[hidden_size, hidden_size]。最后使用激活函数tanh()使结果位于(-1,1)的区间内,ht为该隐层的输出。
实际上,两个线性运算可以写成一个线性运算:
RNN cell的公式:
三、代码实现
自定义一个RNN cell,只需输入参数input_size(即输入值的维度,即Xi有几个分量)和hidden_size(隐藏层的维度,即hi有几个分量)。
# 声明一个RNN cell
cell = torch.nn.RNNCell(input_size = input_size, hidden_size = hidden_size)
# 调用RNN cell,并把下列语句写下一个for循环里(实现序列多次调用RNN cell)
hidden = cell(input, hidden)
注意:input.shape为[batch, input_size],batch为批量的大小,即数据集的大小,即input.shape=batch.size*input_size的二维张量,hidden.shape为[batch, hidden_size]。输出的hidden.shape为[batch, hidden_size]。
实例:
(1)使用RNN cell
RNN的输入张量的shape为[batchSize, inputSize],输出张量(隐层)的维度为[batchSize, hiddenSize]。数据集(dataset)的shape定义为[seqLen,batchSize,inputSize],这里的seqLen即为序列长度。
import torch
batch_size = 1 # 批量大小
seq_len = 3
input_size = 4
hidden_size = 2
cell = torch.nn.RNNCell(input_size = input_size, hidden_size = hidden_size)
dataset = torch.randn(seq_len,batch_size,input_size)
hidden = torch.zeros(batch_size,hidden_size) # h0
# 对dataset进行遍历,每次取出序列中的一个元素,即先取x1,再去x2,最后取x3
for idx,input in enumerate(dataset):
print('=' * 20,idx,'='*20)
print('Input size:',input.shape)
hidden = cell(input, hidden) # cell中的hidden为上一次输出的隐层,等号左边的hidden为这一次的隐层。
print('outputs size',hidden.shape)
print(hidden)
(2)使用RNN
# num_layers表示隐层的层数(后面图解)
cell = torch.nn.RNN(input_size = input_size, hidden_size = hidden_size, num_layers= num_layers)
# 调用
out, hidden = cell(inputs,hidden) #input为输入的序列[x1,x2,x3,..,xN],input.shape为[seqLen, batch, input_size]
# cell中的hidden表示h0,out表示[h1,h1,...,hN),等号左边的hidden为hN。hidden.shape为[numLayers, batch, hidden_size]
# output.shape为[seqLen, batch, hiddden_size]
上图的numLayers即为3,表示三层隐藏层,同一种颜色的RNN cell为同一层(其实同一种颜色是同一个RNN cell)。
import torch
batch_size = 1 # 批量大小
seq_len = 3
input_size = 4
hidden_size = 2
num_layers = 1
cell = torch.nn.RNN(input_size = input_size, hidden_size = hidden_size,num_layers=num_layers)
inputs = torch.zeros(seq_len,batch_size,input_size)
hidden = torch.zeros(num_layers,batch_size,hidden_size)
out, hidden = cell(inputs,hidden)
print('Output size:', output.size)
print('Output:', output)
print('Hidden size:', hidden.size)
print('Hidden:', hidden)
四、实例
训练目标,将输入的“hello"经过RNN网络根据每一个隐藏层的结果输出为“ohlol”。
1.将单词转化为张量
使用独热(one-hot)编码:
对于hello的每一个字符都对应一个索引值,将hello转化为索引值得数组,再根据独热编码得方法转化为等长得向量,可以用一个一维含四个特征值得张量表示。注意每一次RNNcell输出的hi是一个含有四个元素的张量,通过softmax函数决定是输出结果是e,h,l,o中的哪一类:
import torch
input_size = 4
hidden_size = 4
batch_size = 1
#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]
# 便于查询
one_hot_lookup = [[1,0,0,0],
[0,1,0,0],
[0,0,1,0],
[0,0,0,1]]
x_one_hot = [one_hot_lookup[x] for x in x_data] # 根据x_data的索引序列生成每一位字符的独热编码(后面详解)
inputs = torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
labels = torch.LongTensor(y_data).view(-1,1) # torch.LongTensor()用于创建长整型的张量,参数是多维数组。
# torch.view()和torch.reshape()都是用来重塑tensor的shape的,view()只适用于满足连续性条件的tensor进行操作,而reshape同时还可以对不满足连续性条件的tensor进行操作。
# 定义模型
class Model(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size):
super(Model,self).__init__()
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.rnncell = torch.nn.RNNCell(input_size = self.input_size,
hidden_size=self.hidden_size)
def forward(self,input,hidden):
hidden = self.rnncell(input,hidden)
return hidden
# 生成初始的h0
def init_hidden(self):
return torch.zeros(self.batch_size,self.hidden_size)
net = Model(input_size,hidden_size,batch_size)
# 损失函数
criterion = torch.nn.CrossEntryLoss()
# 优化器
optimizer = torch.optim.Adam(net.parameters(),lr=0.1)
# 训练模型
for epoch in range(15):
loss = 0
optimizer.zero_grad()
hidden = net.init_hidden()
print("Predicted stringL:",end='')
for input,label in zip(inputs,labels):
hidden = net(input,hidden)
loss += criterion(hidden,label)
_,idx = hiddden.max(dim=1)
print(idx2char[idx.item()],end='')
loss.backward()
optimizer.step()
print(',Epoch[%d/15] loss=%.4f' % (epoch+1,loss.item()))
补充:(1)torch.Tensor()与torch.tensor()的区别
(2)使用for循环来生成多维数组
下图中x_data中的每一个x作为下标值找到对应one_hot_lookup中的元素,最后生成一个多维数组。
使用RNN的书写形式:
# 定义模型
class Model(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size,numlayers=1):
super(Model,self).__init__()
self.numlayers = num_layers
self.batch_size = batch_size
self.input_size = input_size
self.hidden_size = hidden_size
self.rnn = torch.nn.RNN(input_size = self.input_size,
hidden_size=self.hidden_size,
num_layers = num_layers)
def forward(self,input):
hidden = torch.zeros(self.num_layers,self.batch_size,slef.hidden_size)
out,_=self.rnn(input,hidden)
return out.view(-1,self.hidden_size) # out.shape为[seqLen*batchSize,hiddenSize]
# 生成初始的h0
def init_hidden(self):
return torch.zeros(self.batch_size,self.hidden_size)
net = Model(input_size,hidden_size,batch_size)
# 损失函数
criterion = torch.nn.CrossEntryLoss()
# 优化器
optimizer = torch.optim.Adam(net.parameters(),lr=0.1)
# 训练模型
for epoch in range(15):
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backwrd()
optimizer.step()
_,idx= outputs.max(dim=1)
idx = idx.data.numpy()
2.使用嵌入层代替独热编码(embedding)
独热编码有许多缺点,维度大、张量稀疏、是硬编码(不是通过提取特征得到的)。因此想到使用低维的(lower-dimension),稠密的(dense),学习到的张量(learn from data)。
embedding即把一个高位的、稀疏的样本映射到一个低维的、稠密的空间(即数据降维):
加入嵌入层后:
加入嵌入层的网络结构:
# 参数
num_class = 4
input_size = 4
hidden_size = 8
embedding_size = 10
num_layers = 2
batch_size = 1
seq_len = 5
#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]
inputs = torch.LongTensor(x_data)
labels = torch.LongTensor(y_data).view(-1,1)
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()
self.emb = torch.nn.Embedding(input_size,embedding_size) # 加入嵌入层
self.rnn = torch.nn.RNN(input_size =embedding_size,
hidden_size=self.hidden_size,
num_layers = num_layers,
batch_first=True)# batch_first=True设为true时后面输入的数据的张量的input.shape为[batchSize,seqLen,input_size],即把训练维度与样本序列进行交换,这里的input_size转变为embedding_size。
# output.shape为[batchSize,seqLen,hiddenSize]
self.fc = torch.nn.Linear(hidden_size,num_class) # 完成输出类别的变换
def forward(self,x):
hidden = torch.zeros(num_layers,x.size(0),slef.hidden_size)
x = self.emb(x)
x,_=self.rnn(x,hidden)
x =self.fc(x)
return x.view(-1,num_class) # out.shape为[seqLen*batchSize,hiddenSize]
# 生成初始的h0
def init_hidden(self):
return torch.zeros(self.batch_size,self.hidden_size)
net = Model()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(),lr=0.05)
for epoch in range(15):
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()
_,idx = outputs.max(dim=1)
idx = idx.data.numpy()
print('Predicted:',''.join([idx2char[x] for x in idx]},end='')
print(',Epoch[%d/15] loss=%.3f' % (epoch +1,loss.item())
对于嵌入层可以通过下面的例子来理解:
嵌入层可以增大张量的维度,是通过查询的方式。比如上图中的x_data是一个shape为[5]的张量,通过对每一个元素(1,0,2,2,3)来找到一个对应的张量(张量的元素个数由embedding_size决定,为10),这样x_data就转化为了inputs,其shape为[5,10]。
注意:同一个嵌入层对象多次输入一个张量对应唯一的一个输出结果,同一个张量输入不同的嵌入层对象结果不同。