作者 隔壁的NLP小哥

RNN神经网络和基于Pytorch的实践

本文主要讲述了RNN循环神经网络的基本原理和利用pytorch进行序列生成的实践,原理的部分主要参考 
   ,实践的部分主要参考的
   是《深度学习原理和Pytorch实战》。在这里向作者表示感谢。

本文主要包含以下三个部分

  1. RNN问题引入
  2. RNN基本原理说明
  3. 基于pytorch实现RNN的序列生成任务

1、RNN问题引入

1.1 序列生成问题

对于一段给定的文本,一段给定的语音信号等等,我们都可以将其称为是一个序列。序列中的元素可以是一个符号,一个字符,一个单词等等。以文本序列为例,假设现在我们有一个句子是由W1W2…Wn个单词所构成。其中每一个Wi是一个单词。那么,我们如何根据给定的句子,构建出一个模型来实现自动生成新的序列的功能呢?这个问题就是序列生成问题。

1.2 序列生成问题的实际应用

在自然语言处理的相关任务中,通常可以通过序列生成来完成单词预测,句子生成等任务。
在语音信号处理中,可以通过已经有的声音信号来生成语音句子。
在股票金融中,可以通过将每天的股价构建出一个时间序列来预测未来新的股票价格。

2、RNN基本原理说明

2.1 基本神经网络的结构

基本的神经网络主要包含以下三个部分:
1、输入层:主要负责接收神经网络中的训练和测试数据。
2、隐藏层:主要负责整个神经网络的特征学习。
3、输出层:负责产生模型计算后的结果。

                           

scheme中hidden如何设置_神经网络

在RNN中,其基本神经网络的结构也是由这三种结构所构成的。在本文所提出的问题中,输入层接收的数据是序列中的一个元素,我们通过RNN网络和当前输入的元素来一步一步生成新的元素。直到整个序列元素生成结束。将模型的产生的结果通过输出层进行输出。

2.2 基本的RNN网络的结构

与我们所熟知的基本的前馈神经网络相比,RNN网络最大的不同之处在于它的网络中,各个隐藏层的节点的之间是具有一定联系的。在一般的前馈神经网络中,其隐藏层的节点的输入数据全部都是来自输入层的。但是在RNN神经网络中,其隐藏层节点的数据来源不仅包含输入层输入的数据,还包含同层的前一个隐藏节点的输出。所以其隐藏层的节点以及对应的输出节点的结构如下。

                      

scheme中hidden如何设置_python_02

其中xi表示当前的输入节点,hi表示当前的隐藏层节点,hi-1表示同层的上一个隐藏状态节点。对于我们的序列生成问题而言,序列中的每一个单词是xi,可以确定的是,每一个单词xi不可能是独立的,各个输入节点的之间是存在一定的关系的。那么就需要我们的网络结构能够捕捉和感知到这种联系信息。我们将其称为单词xi的上下文。根据RNN中隐藏层节点的输入来看,hi单元的输入包含了xi和hi-1这两个部分,xi提供了当前词汇的特征,而hi-1则提供的了上下文的信息。这恰好是我们要完成序列生成所需要的。

根据上面的分析,我们可以得出来基本的RNN神经网络的结构如下:

scheme中hidden如何设置_python_03

通过上述的结构,我们不难发现基本的RNN网络机构有这样几个特点:
1、输入层的每一个节点之和隐藏层中一个与其对应的节点相连接,不与其他隐藏层的节点相连接。
2、隐藏层的每一个节点hi的输出受到前一个状态节点和输入节点两个节点的影响。
3、输入节点的数量和输出节点数量相同。

2.3 RNN相关参数的计算

首先,在原来的基本结构图上加入参数:

scheme中hidden如何设置_人工智能_04

 

由上面的图不难发现,RNN中的相关权重主要分为输入层到隐藏层的权重u,隐藏层到输出层的权重v,以及隐藏层节点之间的传播权重w。同时可以发现,输入层每个节点到隐藏层节点的输入权重u相同,同理v和w也相同。
首先的给出RNN前向传播的计算过程:

                                                       

scheme中hidden如何设置_神经网络_05

本文主要是面对初学者,所以尽量减少繁杂的数学公式,主要给出上面两个核心公式。hi表示当前的状态节点的信息。oi表示当前节点,b表示偏置,f表示激活函数。w,u,v表示权重参数,xi表示当前输入节点。

2.4 RNN的实际训练

RNN的训练的核心思想也是反向传播的方式,主要应用的是BPTT算法,这是一种对于随时间的反向传播算法,其主要的训练方式也是沿着需要优化的参数的负梯度不断寻找更优的点,直到收敛。BPTT的计算公式在这里就不列出来了,有兴趣的可以参考RNN 与 LSTM 的原理详解这篇文章。

下面介绍一下RNN在序列生成问题中的训练方式,RNN网络从左到右依次读入序列中的一个字符,并且输入一种可能的字符预测,这个预测马上匹配相应下一个字符并得到反馈误差ei。这个误差可以使用交叉熵的方式进行衡量。我们可以每个序列遍历完成之后,将累计的误差进行反向传播,更新所有的权重。进入到下一个序列进行训练。就这样,RNN周而复始的进行训练,直到所有的序列的训练都结束。

虽然RNN在每一个时刻仅仅读入一个元素,但是由于RNN的同层连接,因此多步之前输入的元素可能还会对当前的元素产生影响。所以输入给RNN的相当于是整个句子序列。

2.5 基于Pytorch的RNN的核心代码

import torch
import torch.nn
class RnnCore(nn.Module):
	def __init__(self,inputSize,hiddenSize,outputSize,layers=1):
		##
		# inputSize:输入层的大小(一般就是输入节点的数量)
		# hiddenSize:RNN隐藏层节点的数量
		# outputSize:输出层元素的数量
		# layers:隐藏层的层数
		##
		super(RnnCore,self).__init__()
		self.hidden_size = hiddenSize
		self.num_layers = layers
		
		#下面开始定义一个基本的网络结构
		#首先是定义输入层 nn.Embedding
		#通过embedding层的操作,将输入的序列的input_size个节点的序列编码成hidden_size个节点的序列
		#输入给隐藏层
		self.embedding = nn.Embedding(inputSize,hiddenSize)
		
		#然后定义RNN层
		##
		# 第一个hidden_size:也就是input_size,指的是RNN网络的输入节点的数量,
		#经过embedding层之后变成了 hiddensize
		# hidden_size:RNN层网络的节点的数量
		#num_layers:rnn的层数
		#bartch_firts :将数据的第一个维度置位为batch的大小
		#输入数据x原来的size为 length_seq,bathc_size,input_size
		#转换之后变为 batch_size,length_seq,input_size
		self.rnn = nn.RNN(hiddenSize,hiddenSize,layers,batch_first=True)
		
		#定义全连接的输出层
		self.fc = nn.Linear(hiddenSize,outputSize)
		#最后定义softmax层,从outputSize数量的元素中找出最终的预测字符
		self.softmax = nn.LogSoftmax(dim=1)
	
	#下面定义前向传播的的过程
	##
	# inputs:输入数据
	# hidden :隐藏层的初始化信息
	def forward(self,inputs,hidden):
		#inputs的尺寸为: (batch_size,num_step,data_dim)
		# num_step:一个序列的长度,扫描完一个序列需要若干个时间步
		# data_dim :序列中每一个元素的向量维度
		# x 的尺寸为:batch_size,num_step,hidden_size
		x = self.embedding(inputs)
		output,hidden = self.rnn(x,hidden)
		#获取最后一个时间步的结果。
		# output的size (batch_size,hidden_size)
		output = output[:,-1,:]
		
		#将隐藏层的最后结果输入到输出层,获取最后的结果
		output = self.fc(output)
		return output,hidden
	
	#下面定义隐藏的初始化方式
	def initHidden(self):
		# 包括 层数,batch数 以及隐藏层的节点数
		return Variable(torch.zeros(self.num_layers,1,self,hidden_size))

下面给出一份我自己的版本,对原作者的代码有了一定的新的解释

from torch import nn
import torch

class RNN(nn.Module):#定义一个模型模块
    def __init__(self,D_in,H,D_out,layers=1): #输入的维度,隐藏层维度和输出层维度,以及隐藏层的个数
        super(RNN, self).__init__()#对父类进行初始化
        self.H=H
        self.num_layers=layers

        #定义网络层,首先是embedding层
        self.embedding=nn.Embedding(D_in,H) #embedding层的目的是将输入的向量转为隐藏层的维度

        #定义RNN层,使用现成的nn.RNN模型即可
        #batch_first将数据的第一个位置置为batch的大小,因为要进行批训练
        self.rnn=nn.RNN(H,H,layers,batch_first=True)

        #定义全连接的输出层
        self.fc=nn.Linear(H,D_out)
        #定义softmax层,从D_out数量的元素中找出最终的预测字符

    #下面定义向前传播的过程
    ##inputs:输入数据
    ##hidden:隐藏层的初始化信息
    def forward(self,inputs,hidden):
        #inputs数据的尺寸为:(batch_size,num_step,data_dim)
        #data_dim:序列中每一个元素的向量维度
        # x的尺寸为:batch_size,num_step,hidden_size
        x=self.embedding(inputs)

        #得到输入数据的embeddings,一层一层算,把一个batch的所有embeddings进行计算
        output,hidden=self.rnn(x,hidden)
        #获得最后一个时间步的结果
        #output的size:(batch_size,hidden_size),因为RNN的output中会插入一个维度,这个维度
        # 和rnn中的layers相关,最后一层rnn出来后的output是在(batch_size,layers,hidden)中
        #最后一层,也就是layers=-1,才可以得到
        output=output[:,-1,:]#

        #经过全连接层得到预测结果
        output=self.fc(output)
        return output,hidden

3 基于Pytorch实现的序列生成实战

3.1 任务描述

观察以下序列:
01
0011
000111
00001111
…………
不难发下其规律:
1、它们都只包含0和1
2、它们的长度不相等
3、0和1的数量是相同的,出现是连续的
4、通用的表示为 ‘0’* n + ‘1’ *n,n表示0和1出现的数量
这个序列在计算机中,我们称其为上下文无关文法,简单的说,就是可以被一组替代规则所生成,而与所处的上下文本身是无关的。

我们举例来说明这种序列的应用
The evidence was convincing 这是一个名词[n] + 动词[v]的结构
继续拓展可以变成
The evidence the layer provided was convincing NNVV结构
…………

3.2任务分析

1、如果出现的序列是0000,那么下一位是0还是1显然不能确定
2、如果出现的序列是00001,那么下一位是1
3、如果序列是00001111,此时0和1的数量相同,显然这个序列下一步应该结束

下面我们使用RNN来完成这个序列生成的任务。主要可以分为训练学习和序列生成两个步骤,在训练阶段,RNN尝试用前面的字符来预测下一个,在生成阶段,RNN会根据给点的种子来生成一个完整的序列。

#encoding=utf-8
import torch
import torch.nn  as nn
import torch.optim
from torch.autograd import Variable

from collections import Counter

import matplotlib 
import matplotlib.pyplot as plt
from matplotlib import rc
import numpy as np

#首先构建RNN网络
class SimpleRnn(nn.Module):
    def __init__(self,input_size,hidden_size,output_size,num_layers=1):
        super(SimpleRnn,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的指标
        #在定义这个部件的时候,需要制定输入节点的数量input_size 隐藏层节点的数量hidden_size
        #和RNN层的数目
        #我们世界上使用nn.RNN 来构造多层RNN的,只不过每一层RNN的神经元的数量要相同
        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,inputs,hidden):
        '''
         基本运算过程:先进行embedding层的计算
         在将这个向量转换为一个hidden_size维度的向量。
         input的尺寸为 batch_size,num_step,data_dim
        '''
        x = self.embedding(inputs)
        #此时x的size为batch_size,num_step.hidden_size
        
        #从输入层到隐藏层的计算
        output,hidden = self.rnn(x,hidden)
        #从输出中选择最后一个时间步的数值,output中包含了所有时间步的结果
        #output的输出size batch_size,num_step,hidden_size
        
        output = output[:,-1,:]
        #此时output的尺寸为[batch_szie,hidden_size]
        
        #最后输入全连接层
        output = self.fc(output)
        #此时output的尺寸为 batch_size,hidden_size
            
        #输入softmax
        output = self.softmax(output)
            
        return output,hidden
        #对隐藏层进行初始化
        #注意尺寸的大小为layer_size,batch_size,hidden_size
    def initHidden(self):
        return Variable(torch.zeros(self.num_layers,1,self.hidden_size))


#下面是训练以及校验的过程
#首先生成01字符串类的数据以及样本的数量
train_set = []
validset = []
sample = 2000

#训练样本中最大的n值
sz = 10

#定义n的不同权重,我们按照10:6:4:3:1:1....来配置n=1,2,3,4,5
probablity = 1.0 *np.array([10,6,4,3,1,1,1,1,1,1])

#保证n的最大值是sz
probablity = probablity[:sz]

#归一化,将权重变成概率
probablity = probablity / sum(probablity)



#开始生成sample这么多个样本,
#每一个样本的长度是根据概率来定义的
for m in range(2000):
    #对于随机生成的字符串,随机选择一个n,n被选择的权重记录在probablity
    #n表示长度
    #range生成序列,p表示通过之前定义的probablity的概率分布进行抽样
    n = np.random.choice(range(1,sz+1),p=probablity)
    #生成程度为2n这个字符串,用list的形式完成记录
    inputs = [0]*n + [1]*n
    #在最前面插入3表示开始字符,在结尾插入2表示结束符
    inputs.insert(0,3)
    inputs.append(2)
    train_set.append(inputs)
    
#在生成sample/10的校验样本
for m in range(sample // 10):
    n =np.random.choice(range(1,sz+1),p=probablity)
    inputs = [0] * n + [1] *n
    inputs.insert(0,3)
    inputs.append(2)
    validset.append(inputs)
    
#再生成若干个n特别大的样本用于校验
for m in range(2):
    n = sz + m
    inputs = [0] * n + [1] *n
    inputs.insert(0,3)
    inputs.append(2)
    validset.append(inputs)

#下面是训练过程
#输入的size是4,可能的值为0,1,2,3
#输出size为3 可能为 0,1 2

rnn = SimpleRnn(input_size=4, hidden_size=2, output_size=3)
criterion = torch.nn.NLLLoss() #定义交叉熵函数
optimizer = torch.optim.Adam(rnn.parameters(),lr=0.001) #采用Adam算法

#重复进行50次试验
num_epoch = 50
results = []
for epoch in range(num_epoch):
    train_loss = 0
    np.random.shuffle(train_set)
    #对每一个序列进行训练
    for i,seq in enumerate(train_set):
        loss = 0
        hidden = rnn.initHidden() #初始化隐含层的神经元、
        #对于每一个序列的所有字符进行循环
        for t in range(len(seq)-1):
            #当前字符作为输入,下一个字符作为标签
            x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
            # x的size为 batch_size=1。time_steps=1,data_dimension = 1
            y = Variable(torch.LongTensor([seq[t+1]]))
            #y的size batch_size =1 data_dimension =1
            output,hidden = rnn(x,hidden)
            #output 的size:batch_size,output_size=3
            #hidden尺寸 layer_size = 1,batch_size = 1,hidden_size
            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))
            
    #下面在校验集上进行测试
    valid_loss = 0
    errors = 0
    show_out_p =''
    show_out_t = ''
    for i,seq in enumerate(validset):
        loss = 0
        outstring = ''
        targets = ''
        diff = 0
        hidden = rnn.initHidden()
        for t in range(len(seq)-1):
            x = Variable(torch.LongTensor([seq[t]]).unsqueeze(0))
            y = Variable(torch.LongTensor([seq[t+1]]))
            output,hidden = rnn(x,hidden)
            data = output.data.numpy()
            print("the output is ",data)
            #获取最大概率输出
            mm = torch.max(output,1)[1][0]
            #以字符的形式添加到outputstring中
            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
        if np.random.rand() < 0.1:
            #以0,1的概率记录一个输出的字符串
            show_out_p += outstring 
            show_out_t += targets
        #打印结果
        print(output[0][2].data.numpy())
        print('第{}轮,训练loss: {:.2f},校验loss:{:.2f},错误率:{:.2f}'.format(epoch,train_loss.data.numpy()/len(train_set),
                                                                                    valid_loss.data.numpy()/len(validset)
                                                                                    ,1.0*errors/len(validset)))
        print("the show output is: ",show_out_p)
        print("the show taget is: ",show_out_t)
        results.append([train_loss.data.numpy()/len(train_set),valid_loss/len(train_set),1.0*errors/len(validset)])

4 RNN网络结构扩展

本部分的内容主要参考参考RNN 与 LSTM 的原理详解这篇文章。根据我们上文所叙述的RNN网络的结构,我们直到RNN的基本网络结构是一个输入单元对应一个隐藏单元,一个隐藏单元对应一个输出单元。那么,我们可以对于这种基本结构进行扩展。

1、多输入,单输出的结构

scheme中hidden如何设置_人工智能_06

2、单输入、多出结构

scheme中hidden如何设置_人工智能_07

3、编码解码结构

scheme中hidden如何设置_神经网络_08