跟着问题学10——RNN详解及代码实战

1 循环神经网络 Recurrent Neural Network

什么是序列信息呢?通俗理解就是一段连续的信息,前后信息之间是有关系地,必须将不同时刻的信息放在一起理解。

比如一句话,虽然可以拆分成多个词语,但是需要将这些词语连起来理解才能得到一句话的意思。

RNN就是用来处理这些序列信息的任务,比如NLP中的语句生成问题,一句话中的每个词并不是单独存在地,而是根据上下文信息,与他的前后词有关。

如:我吃XXX,吃是一个动词,按照语法规则,那么它后面接名词的概率就比较大,在预测XXX是什么的时候就要考虑前面的动词吃的信息,如果没考虑上下文信息而预测XXX是一个动词的话,动词+动词,很大概率是不符合语言逻辑地。

为了解决这一问题,循环神经网络 RNN也就应运而生了。

1.1循环神经网络 RNN的结构

从特征角度考虑:输入特征是n*1的单维向量(这也是为什么linear层前要把所有特征层展平的原因),然后根据隐含层神经元的数量m将前层输入的特征用m*1的单维向量进行表示(对特征进行了提取变换),单个隐含层的神经元数量就代表网络参数,可以设置多个隐含层;最终根据输出层的神经元数量y输出y*1的单维向量。

 从特征角度考虑:输入特征是width*height*channel的张量, 然后根据通道channel的数量c会有c个卷积核将前层输入的特征用k*k*c的张量进行卷积(对特征进行了提取变换,k为卷积核尺寸),卷积核的大小和数量k*k*c就代表网络参数,可以设置多个隐含层;每一个channel都代表提取某方面的一种特征,该特征用width*height的二维张量表示,不同特征层之间是相互独立的(可以进行融合)。最终根据场景的需要设置后面的输出。

从特征角度考虑:输入特征是T_seq*feature_size的张量(T_seq代表序列长度),每个时刻t可以类似于CNN的通道channel,只是时刻t的特征(channel)是和t之前时刻的特征(channel)相关联的,所以H_t是由X_t和H_t-1共同作为输入决定的,每个时刻t的特征表示是用feature_size*1的单维向量表示的,每个隐状态H_t类似于一个channel,特征的表示是用hidden_size*1的单维向量表示的,H_t的channel总数就是输入的序列长度,所以一个隐含层是T_seq*hidden_size的张量,如图中所示,同一个隐含层不同时刻的参数W_ih和W_hh是共享的;隐含层可以有num_layers个(图中只有1个)

以t时刻具体阐述一下:

X_t是t时刻的输入,是一个feature_size*1的向量

W_ih是输入层到隐藏层的权重矩阵

H_t是t时刻的隐藏层的值,是一个hidden_size*1的向量

W_hh是上一时刻的隐藏层的值传入到下一时刻的隐藏层时的权重矩阵

Ot是t时刻RNN网络的输出

从上右图中可以看出这个RNN网络在t时刻接受了输入Xt之后,隐藏层的值是St,输出的值是Ot。但是从结构图中我们可以发现St并不单单只是由Xt决定,还与t-1时刻的隐藏层的值St-1有关。

怎么理解这个参数共享呢?

虽然说X{t-1},X{t},X{t+1}是表示不同时刻的输入,但是他们输入到RNN网络中的时候并不是作为单独的向量一个一个输入地,而是组合在一起形成一个矩阵输入,然后这个矩阵再通过权重矩阵U的变化,其实是同一时刻输入地,只是计算的先后顺序不同。因此同一个隐藏层中,不同时刻的输入他们的W,V,U参数是共享地。

2 经典的RNN结构

N vs N

在实际应用中,我们还会遇到很多序列形的数据:

如:自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推。

语音处理。此时,x1、x2、x3……是每帧的声音信号。

时间序列问题。例如每天的股票价格等等

序列形的数据就不太好用原始的神经网络处理了。为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,h可以对序列形的数据提取特征,接着再转换为输出。先从h1的计算开始看:

图示中记号的含义是:

  • 圆圈或方块表示的是向量。
  • 一个箭头就表示对该向量做一次变换。如上图中h0和x1分别有一个箭头连接,就表示对h0和x1各做了一次变换。

在很多论文中也会出现类似的记号,初学的时候很容易搞乱,但只要把握住以上两点,就可以比较轻松地理解图示背后的含义。

h2的计算和h1类似。要注意的是,在计算时,每一步使用的参数U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记。

依次计算剩下来的(使用相同的参数U、W、b):

我们这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。

我们目前的RNN还没有输出,得到输出值的方法就是直接通过h进行计算:

正如之前所说,一个箭头就表示对对应的向量做一次类似于f(Wx+b)的变换,这里的这个箭头就表示对h1进行一次变换,得到输出y1。

剩下的输出类似进行(使用和y1同样的参数V和c):

这就是最经典的RNN结构,我们像搭积木一样把它搭好了。它的输入是x1, x2, .....xn,输出为y1, y2, ...yn,也就是说,输入和输出序列必须要是等长的

由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
  • 输入为字符,输出为下一个字符的概率。这就是著名的Char RNN
  • (详细介绍请参考:The Unreasonable Effectiveness of Recurrent Neural Networks,Char RNN可以用来生成文章,诗歌,甚至是代码,非常有意思)。

N VS 1

有的时候,我们要处理的问题输入是一个序列,输出是一个单独的值而不是序列,应该怎样建模呢?实际上,我们只在最后一个h上进行输出变换就可以了:

这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

1 VS N

输入不是序列而输出为序列的情况怎么处理?我们可以只在序列开始进行输入计算:

还有一种结构是把输入信息X作为每个阶段的输入:

下图省略了一些X的圆圈,是一个等价表示

这种1 VS N的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子
  • 从类别生成语音或音乐等

N vs M

下面我们来介绍RNN最重要的一个变种:N vs M。这种结构又叫Encoder-Decoder模型,也可以称之为Seq2Seq模型。

原始的N vs N RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。

为此,Encoder-Decoder结构先将输入数据编码成一个上下文向量c:

得到c有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。

拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。具体做法就是将c当做之前的初始状态h0输入到Decoder中:

还有一种做法是将c当做每一步的输入:

由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:

  • 机器翻译。Encoder-Decoder的最经典应用,事实上这一结构就是在机器翻译领域最先提出的
  • 文本摘要。输入是一段文本序列,输出是这段文本序列的摘要序列。
  • 阅读理解。将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
  • 语音识别。输入是语音信号序列,输出是文字序列。
  • …………

代码

model.py

import torch
import torch.nn as nn

import random
class RNN(nn.Module):
    def __init__(self,feature_size,hidden_size,num_layers,output_size):
        super(RNN,self).__init__()
        self.rnn=nn.RNN(
            input_size=feature_size,hidden_size=hidden_size,\
            num_layers=num_layers,batch_first=True
        )
        for k in self.rnn.parameters():
            nn.init.normal_(k,mean=0.0,std=0.001)
        self.linear=nn.Linear(hidden_size,output_size)
        self.hidden_size=hidden_size

    def forward(self,x,hidden_prev):
        out,hidden_prev=self.rnn(x,hidden_prev)
        print("out1&hidden_prev.shape",out.shape,hidden_prev.shape)
        #view()相当于reshape、resize,重新调整PyTorch 中的 Tensor 形状,若非 Tensor 类型,可使用 data = torch.tensor(data)来进行转换。
       # out=out.view(-1,self.hidden_size)
        print("out2.shape", out.shape)
        out=self.linear(out)
        print("out3.shape", out.shape)
       # out=out.unsqueeze(0)
        print("out4.shape", out.shape)
        return out,hidden_prev

#自己实现一个RNN函数
def rnn_forward(input,weight_ih,weight_hh,bias_ih,bias_hh,h_prev):
    batch_size,T_seq,feature_size=input.shape
    hidden_size=weight_ih.shape[0]
    h_out=torch.zeros(batch_size,T_seq,hidden_size)
    for t in range(T_seq):
        x=input[:,t,:].unsqueeze(2)
      #  print("xt.shape",x.shape)
            #unsqueeze,在给定维度上(从0开始)扩展1个维度,负数代表从后开始数
        weight_ih_batch=weight_ih.unsqueeze(0).tile(batch_size,1,1)
     #   print("weight_ih_batch.shape", weight_ih_batch.shape)
        weight_hh_batch=weight_hh.unsqueeze(0).tile(batch_size,1,1)
     #   print("weight_hh_batch.shape", weight_hh_batch.shape)

        #计算两个tensor的矩阵乘法,torch.bmm(a,b),tensor a 的size为(b,h,w),tensor b的size为(b,w,m)
        # 也就是说两个tensor的第一维是相等的,然后第一个数组的第三维和第二个数组的第二维度要求一样,
        # 对于剩下的则不做要求,输出维度 (b,h,m)
        # weight_ih_batch=batch_size*hidden_size*feature_size
        #x=batch_size*feature_size*1
        #w_times_x=batch_size*hidden_size*feature_size
        ##squeeze,在给定维度(维度值必须为1)上压缩维度,负数代表从后开始数

        w_times_x=torch.bmm(weight_ih_batch,x).squeeze(-1)#
      #  print("w_times_x.shape", w_times_x.shape)

        w_times_h=torch.bmm(weight_hh_batch,h_prev.unsqueeze(2)).squeeze(-1)
      #  print("w_times_h.shape", w_times_h.shape)

        h_prev=torch.tanh(w_times_x+bias_ih+w_times_h+bias_hh)
        print("h_prev.shape", h_prev.shape)
        h_out[:,t,:]=h_prev
        print("h_out.shape", h_out.shape)

    return h_out,h_prev.unsqueeze(0)

if __name__=="__main__":
    # input=torch.randn(batch_size,T_seq,feature_size)
    # h_prev=torch.zeros(batch_size,hidden_size)

    # rnn=nn.RNN(input_size,hidden_size,batch_first=True)
    # output,state_final=rnn(input,h_prev.unsqueeze(0))

    # print(output)
    # print(state_final)
    batch_size, T_seq =10, 30  # 批大小,输入序列长度
    feature_size, hidden_size = 5, 8  #
    num_layers, output_size=1,3
    input = torch.randn(batch_size, T_seq, feature_size)
    h_prev = torch.zeros(1,batch_size, hidden_size)#.unsqueeze(0)
    #my_rnn=RNN(feature_size,hidden_size,num_layers,output_size)
    rnn=nn.RNN(feature_size,hidden_size,batch_first=True)
   # rnn_output, state_final = rnn(input, h_prev.unsqueeze(0))
    # for k,v in rnn.named_parameters():
    #     print(k,v.shape)
    my_rnn_output,my_state_final=rnn_forward(input,rnn.weight_ih_l0,rnn.weight_hh_l0,\
                                             rnn.bias_ih_l0,rnn.bias_hh_l0,h_prev)
    print(my_rnn_output.shape)
    print(my_state_final.shape)


train.py

import torch
import torch.nn as nn
import numpy as np
from models.nlp import RNN
from models.nlp.RNN import *

import datetime
import torch.optim as optim
from matplotlib import pyplot as plt

batch_size=2#批大小
T_seq=30#输入序列长度(时间步)
feature_size=3#输入特征维度

hidden_size=3#隐含层维度
output_size=2#输出层维度

num_layers=1
lr_rate=0.001
epoch=1000
#input 即RNN网络的输入,维度应该为(T_seq, batch_size, input_size)。如果设置batch_first=True,输入维度则为(batch, seq_len, input_size)
input=torch.randn(batch_size,T_seq,feature_size)

def train(input):
    model=RNN(feature_size,hidden_size,num_layers,output_size)
    print("model:\n",model)
    # 设置损失函数
    loss_fn=nn.MSELoss()
    # 设置优化器
    optimizer=optim.Adam(model.parameters(),lr_rate)
    # 初始化h_prev,它和输入x本质是一样的,hidden_size就是它的特征维度
    #维度应该为(num_layers * num_directions, batch, hidden_size)。num_layers表示堆叠的RNN网络的层数。
    # 对于双向RNNs而言num_directions= 2,对于单向RNNs而言,num_directions= 1
    hidden_prev=torch.zeros(1,batch_size,hidden_size)
    loss_plt=[]
    #开始训练
    for iter in range(epoch):
        x = input
        print("x:", x.shape)

        output,hidden_prev=model(x,hidden_prev)
        print("output_size:",output.shape)
        y = torch.randn(batch_size,T_seq,output_size)
        print("y:", y.shape)
        #返回一个新的tensor,从当前计算图中分离下来的,但是仍指向原变量的存放位置,
        # 不同之处只是requires_grad为false,得到的这个tensor永远不需要计算其梯度,不具有grad。
        hidden_prev=hidden_prev.detach()

        loss=loss_fn(output,y)
        model.zero_grad()
        loss.backward()
        optimizer.step()
        if iter%100==0:
            print("iteration:{} loss {}".format(iter,loss.item()))
            loss_plt.append(loss.item())


    plt.plot(loss_plt, 'r')
    plt.xlabel('epcoh')
    plt.ylabel('loss')
    plt.title('RNN-train-loss')

    return hidden_prev, model


if __name__ == '__main__':
    start_time = datetime.datetime.now()
    hidden_pre, model = train(input)
    end_time = datetime.datetime.now()
    print('The training time: %s' % str(end_time - start_time))
    plt.show()



参考资料

8.4. 循环神经网络 — 动手学深度学习 2.0.0 documentation

https://zhuanlan.zhihu.com/p/28054589

29、PyTorch RNN的原理及其手写复现_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值