RNN API调用示例

RNN模板类定义及其对象调用

 
import torch
from torch import nn
from torch.nn import functional as F

seq_len       = 85
batch_size    = 32
embedding_dim = 256

hidden_size   = 512

# RNN每个单元将单词维度映射到新的维度,因此模板定义需要指定其映射关系
rnn=nn.RNN(input_size=256,hidden_size=hidden_size,num_layers=1,bidirectional=False)
x = torch.randn([seq_len,batch_size,embedding_dim])

# 每个单链RNN都有一个初始化参数
h0= torch.zeros([1,batch_size,hidden_size])

# ht是每一个时间步t的输出,hn是最后一个时间步的输出
output,hn = rnn(x,h0)
    

参数说明

 
RNN模板类有四个参数:
input_size: 特征个数,in_features,单词的embedding_dim
hidden_size:特征个数,即要将原特征变换到什么维度,out_features,RNN隐藏层向量维数
num_layers: RNN链的层数
bidirectional:每层的RNN链是单向一条RNN链,还是正反两条RNN链 

RNN对象调用的两个参数:
x:数据,[L,B,C] [序列长度,批次大小,特征个数] [序列,批次,特征]
h0: 每条RNN链开始端,隐藏层的初始化参数,通常是0矩阵(但个人认为一个包含当前序列语境的上下文向量更合适,此想法尚未验证)

参数记忆技巧

 
模板类定义
参数网络终极目的就是特征变换,所有必有 一个输入+一个输出 
这里对应的是input_size,hidden_size 
RNN还多出来两个,一个是层数,是一个方向 

模板对象参数
输入数据是必然的,特征是数据的特征,
RNN还多了一个参数,每条RNN单链的初始化参数矩阵,
它要与每个单链的首个单词进行信息融合

循环神经网络概述

循环神经网络主要哪三个

 
RNN 
LSTM 
GRU

循环神经网络的循环

 
一个单词一单词的处理
每个单元处理一个单词,其输入是 当前单词 + 上一个单元的输出 

所以RNN处理是的序列,并且无法并发,
因为它只能顺序地处理一个单词后,
才能处理下一个单词,
前后有依赖关系 

循环神经网络的网络

 
指全连接 
ℎ𝑡=tanh(x𝑡@𝑊𝑖+𝑏𝑖+(ℎ𝑡−1)@𝑊ℎ+𝑏ℎ)

xw+b 就是全连接,
对当前的单词及上一个单元的输出各做一个全连接,
然后相加融合,再加一个激活函数 

这里有个面试题,RNN的本质是什么?
是全连接 
不要怎么追究这一问一答的细节,因为它不是数学公式,面试官问时就这么答就行了

循环神经网络的问题

 
依赖问题:序列过长时信息丢失严重,
经实践实验,序列长度超过20个时,精度明显下降
LSTM比RNN好上一丢丢... 

循环神经网络优化

 
双向比单向效果好一些

output,hn 信息量不一样,根据业务场景合理使用,而不是千篇一律只用一个 

层数为2就差不多了,更的层数比如5效果未必好

循环神经网络参数

RNN参数

 
一指模板类的参数,二指数据转换对象的参数 

模板类参数:输入,输出,是否双向,层数 
数据转换对象参数:数据x,隐藏层hidden 

参数与输入输出的shape

 
import torch
from torch import nn
from torch.nn import functional as F
seq_len       = 85
batch_size    = 32
embedding_dim = 256
hidden_size   = 512

# RNN每个单元将单词维度映射到新的维度,因此模板定义需要指定其映射关系
rnn=nn.RNN(input_size=256,hidden_size=hidden_size,num_layers=1,bidirectional=False)
x = torch.randn([seq_len,batch_size,embedding_dim])

# 每一个单元处理,都有一个隐藏层
# 输出层output包含所有的隐藏层,[seq_len,batch_size,hidden_size]
h0= torch.zeros([1,batch_size,hidden_size])

# ht是每一个时间步t的输出,hn是最后一个时间步的输出
output,hn = rnn(x,h0)

hn.shape
torch.Size([1, 32, 512])

output.shape
torch.Size([87, 32, 512])

隐藏层为什么要有批次这个维度?

 
RNN某一时刻就处理一个单词,而
隐藏层就是一个单词的输出

单词的embedding_dim转为hidden_size 
从设计的目的来看,隐藏层不需要批次,
也跟多少个单词,即序列的维度没有关系
因为每个单词的向量维度提前固定了

但深度学习的计算都是以批次计算的
文本矩阵化后序列长度与批次都会固定下来
从第1个位置开始,一次处理一个批次的单词,
然后处理第2个位置上的单词,又是一个批次的数据,
...... 

这里设定只有一个单向RNN链,
ℎ𝑡=tanh(𝑥𝑡𝑊𝑖+𝑏𝑖+(ℎ𝑡−1)𝑊ℎ+𝑏ℎ)
xt是t时刻,即序列循环中第t个单词的批次数据,
xt.shape=[1,batch_size,embedding_dim]
xtWi全连接后的shape为[1,batch_size,hidden_size]
隐藏层转换后的结果是要与批次数据xtWi相加,
也就是进行批次计算,
于是就设定ht的shape为[1,batch_size,hidden_size]
从第一个初始化隐藏层h0至最后一个隐藏层ht都是如此

至于序列这个维度,从头到尾,跟隐藏的shape没关系,
即一句话有多少个单词,跟每个单词按多少维输出没关系,
对于单向RNN链,RNN每次计算的只有一个单词,
任一时刻都只有一个单词在再与隐藏层进行计算

对于多个RNN链,
同一时刻与隐藏层计算的单词个数是单向RNN的链数,
即一个链就是一个序列的for循环,
每个for循环同一时刻只有一个单词在与一个隐藏层计算

多组参数对应关系

 
import torch
from torch import nn
from torch.nn import functional as F
seq_len       = 85
batch_size    = 32
embedding_dim = 256
hidden_size   = 512
bidirectional=True
num_layers = 1
single_rnn_nums = num_layers

# RNN每个单元将单词维度映射到新的维度,因此模板定义需要指定其映射关系
rnn=nn.RNN(input_size=256,hidden_size=hidden_size,num_layers=num_layers,bidirectional=bidirectional)
x = torch.randn([seq_len,batch_size,embedding_dim])

# 每一个单元处理,都有一个隐藏层
# 输出层output包含所有的隐藏层,[seq_len,batch_size,hidden_size]
if bidirectional:
    single_rnn_nums = num_layers*2
    
h0 = torch.zeros([single_rnn_nums, batch_size, hidden_size])

# ht是每一个时间步t的输出,hn是最后一个时间步的输出
output,hn = rnn(x,h0)

hn.shape
torch.Size([2, 32, 512])

output.shape
torch.Size([85, 32, 1024])


seq_len       = 85
batch_size    = 32
embedding_dim = 256
hidden_size   = 512
bidirectional=True
num_layers = 2
single_rnn_nums = num_layers

hn.shape
torch.Size([4, 32, 512])

output.shape
torch.Size([85, 32, 1024])

隐藏层的第1维,就是dim=0这个维度,

 

    

双层,双向可以增加模型的表达能力

 
import torch
from torch import nn
from torch.nn import functional as F
seq_len       = 85
batch_size    = 32
embedding_dim = 512
hidden_size   = 512

# RNN每个单元将单词维度映射到新的维度,因此模板定义需要指定其映射关系
rnn=nn.RNN(input_size=512,hidden_size=hidden_size,num_layers=2,bidirectional=False)
x = torch.randn([seq_len,batch_size,embedding_dim])

out,hn = rnn(x)

out.shape   # torch.Size([85, 32, 512])
    

 

    

 

    

 


 

  

 


RNN内部结构

全连接RNNCell

 
一个RNNCell 循环单元,就是将一个单词从一个维度变换到另外一个维度
一个单元处理一个序列位置上的单词
相同的单元在循环,但每次的输入却是一个序列上不同位置的单词
做这个变换的,就是xw+b,即全连接,所以有人认为RNN的本质就是全连接

序列依赖与单向RNN

 
从第1个单词到最后一个单词形成的这条链,这里给它起一个名称,叫一个单向RNN
每个单向RNN链条都需要一个初始化的隐藏层,
同一时刻,这个隐藏层计算的对象是单个的RNNCell,
它与输入的单词向量按位相加,注意哈,是按位相加
就是一个向量与另外一个向量按位相加,
再经过激活函数变换一下,就是一个RNNCell的输出了,也是下一个单元的隐藏层
因为矩阵运算要shape一致,

初始化向量要与单词向量相加,而单词向量比如这里的256,
先做一个全连接,变换成512,这里的初始化向量也是512,
然后它们二者就可以相加了

512+512,按位相加上shape不变,向量维度还是512
然后就开始的下一个单元,一直512下去,直到最后一个单元,还是512
输入的时间并不是一次输入一个单词,神经网络是批次计算,
因当前的单词依赖于上一个单词的输出,
所以RNN有序列依赖,可以处理序列问题,同时无法并行

隐藏向量的维度与个数

 
中间单词的shape是[seq_len,batch_size,embedding_dim],
上来先来个全连接,变成 [seq_len,batch_size,hidden_size]
初始化向量为了迎合这个矩阵运算的规则,初始化的shape为[1,batch_size,hidden_size]
序列这个维度为1,因为同一时刻与隐藏层向量计算的只有一个单词
所以,每条单向RNN链都需要一个初始化向量,
就是一个隐藏层向量,就可以知道
隐藏层dim=0维的值,
就是 RNN的层数*方向数,方向数在单向时为1,双向时为2

output输出层

 
每个单词有一个输出,所以序列维度数不变
批次维度数也不变
最后的输出向量的维度,如果是单向,就是hidden_size
如果是双向,前半部分是正向,后半部分是反向,二者连接起来了,维数为hidden_size*2 
在做优化时,比如某些transform算法将output的最后一维的两个隐藏层向量拆分出来,
相加,不是拼接,而是相加,
得到一个无论是双向还是单向其输出都是hidden_size的结果 

隐藏层输出

 
# ht是每一个时间步t的输出,hn是最后一个时间步的输出
output,hn = model(x,h0)

hn是每条单向RNN链最后一个RNNCell的输出,
融合有整个RNN链上所有单词的信息
它的shape与隐藏层初始化向量h0的shape一致

RNN自编码实现

一个单向RNN链的实现

 
import torch
from torch import nn

seq_len       = 87
batch_size    = 32
embedding_dim = 256
hidden_size   = 512

class SingleRNNDefine(nn.Module):
    def __init__(self,input_size=embedding_dim,hidden_size=hidden_size):
        super().__init__()
        # [batch_size,embedding_dim]@[embedding_dim,hidden_size] = [batch_size,hidden_size]
        self.cell_linear_x = nn.Linear(in_features=input_size,  out_features=hidden_size)
        self.cell_linear_h = nn.Linear(in_features=hidden_size, out_features=hidden_size)

    def forward(self,x,h0):
        seq_len, batch_size, embedding = x.shape
        output = []
        ht = h0[0] # [1,batch_size,embedding]
        for t in range(seq_len):
            # print(f"x[{t}].shape={x[t].shape}")  # x[86].shape=torch.Size([32, 256])
            # [batch_size,embedding] --> [batch_size,hidden_size]
            # 对于每一个时间步来说,不需要管seq_len的维度,因为一步一个单词
            each_word = self.cell_linear_x(x[t])
            # print(f"t={t},each_word.shape={each_word.shape}")
            # print(f"t={t},ht.shape={ht.shape}")

            ht = self.cell_linear_h(ht)

            ht = torch.tanh(each_word + ht)
            # print(ht.shape)  # torch.Size([32, 512])

            output.append(ht.tolist())

        hn = torch.unsqueeze(input=ht,dim=0)
        # print(hn.shape)  # torch.Size([1, 32, 512])
        output = torch.Tensor(output)
        return output,hn



if __name__=="__main__":
    rnn = SingleRNNDefine(input_size=embedding_dim, hidden_size=hidden_size)
    x = torch.randn([seq_len, batch_size, embedding_dim])

    # 每一个单元处理,都有一个隐藏层
    
    h0= torch.zeros([1, batch_size, hidden_size])

    # ht是每一个时间步所有单向RNN链t时刻的输出,hn是所有单向RNN链最后一个时间步的输出
    # 输出层output包含所有的隐藏层,[seq_len,batch_size,hidden_size]
    output,hn = rnn(x,h0)
    print(output.shape)  # torch.Size([87, 32, 512])
    print(hn.shape)      # torch.Size([1, 32, 512])

参考文章