RNN与CharRNN

本文学习循环神经网络(RNN)以及相关的项目,首先会介绍 RNN 经典的结构,以及它的几个变体 。 接着将在 TensorFlow 中使用经典的结构实现一个有趣的项目: CharRNN。 Char RNN 可以对文本的字符级概率进行建模,从而生成各种类型的文本 。

 

一、RNN 的原理

1、经典 RNN 的结构

RNN 的英文全称是 Recurrent Neural Networks,即循环神经网络, 是一种对序列型数据进行建模的深度模型。在实际应用中有很多序列形的数据,例如:自然语言处理问题、语言处理、时间序列问题。序列形的数据不太好用原始的神经网络处理 。

 

(1)隐状态h的计算

为了处理建模序列问题 , 引入了隐状态 h ( hidden state )的概念,h 可以对序列形的数据提取特征,接着再转换为输出。

在计算时,每一步使用的参数 U、W、b 都是一样的,即每个步骤的参数都是共享的,这是 RNN 的重要特点。图中的 U、 W是参数矩阵,b是偏置项参数,f是激活函数,在经典的RNN结构中,通常使用 tanh 作为激活函数 。

 

(2)输出y的计算

目前的 RNN 还没有输出,得到输出值的方法是直接通过 h 进行计算。一个箭头表示对相应的向量做一次类似于f(Wx+b)的变换。

           

V 和 c 是新的参数,通常处理的是分类问题,因此使用 Softmax 函数将输出转换成各个类别的概率。此处的箭头表示对 hi 进行一次变换,得到输出 Yi。这就是经典的RNN结构,输入和输出序列必须是要等长的。由于输入和输出序列必须是要等长的,经典RNN的适用范围比较小 ,但也有一些问题适合用经典的RNN结构建模,如:计算视频中每一帧的分类标签;输入为字符 ,输出为下一个字符的概率。这是著名的 Char RNN 。

 

(3)经典RNN数学定义

设输入为 X1,X2,... ,对应的隐状态为 h1,h2,... , 输出为 Y1,Y2  ... ,则经典RNN的运算过程表示为

其中, U,V,W,b,c均为参数,而 f 表示激活函数,一般为 tanh 函数。

 

2、N VS 1 RNN的结构

若问题的输入是一个序列,输出是一个单独的值而不是序列,此时应该如何建模呢?只在最后一个 h 上进行输出变换就可以了。这种结构通常用来处理序列分类问题 。如输入一段文字判别所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

(1)结构

 

(2)数学定义

设输入为 X1,X2,... ,对应的隐状态为 h1,h2,... ,输出为y,那么运算过程为:,输出时对最后一个隐状态做运算即可:

 

3、1 VS N RNN的结构

输入不是序列而输出为序列的情况怎么处理?

(1)结构

可以只在序列开始进行输入计算。

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

 

(2)数学定义

这种 1 vs N 的结构可以处理的问题有:从图像生成文字、从类别生成语言或音乐等。

 

二、LSTM 的原理

本文主要讲LSTM 的内部结构以及它相对于 RNN 的优点。LSTM ( Long Short-Term Memory,长短期记忆网络),是RNN 的改进版。一个 RNN 单元的输入由两部分组成,即前一步 RNN 单元的隐状态相当前这一步的外部输入, 此外还有一个输出 。 从外部结构看, LSTM 和 RNN 的输入输出一模一样, 同样是在每一步接受外部输入和前一阶段的隐状态,并输出一个值 。所以,将之前提到的RNN及其变体的每一种结构都无缝切换到 LSTM,而不会产生任何问题。

从RNN的计算公式可以看出,RNN 每一层的隐状态都由前一层的隐状态经过变换和激活函数得到,反向传播求导时最终得到的导数会包含每一步梯度的连乘,这会引起梯度爆炸或梯度消失,所以 RNN 很难处理 “长程依赖”问题,即无法学到序列中蕴含的间隔时间较长的规律。LSTM 在隐状态计算时以加法代替了这里的迭代变换,可以避免梯度消失的问题 , 使网络学到长程的规律 。

 

(1)RNN和LSTM的整体结构

 

(2)隐状态 Ci 的传播

和 RNN 有所不同, LSTM 的隐状态有两部分,一部分是hi ,一部分是 ci, ci 是在各个步骤间传递的主要信息,而水平线可以看作是 LSTM 的“主干道” 。 通过加法, ci可以无障碍地在这条主干道上传递 ,因此较远的梯度也可以在长程上传播 。 这是 LSTM 的核心思想 。

 

(3)遗忘门的结构

LSTM 的每一个单元中都有一个“遗忘门”,用来控制遗忘掉 Ci的那些部分。σ 是 Sigmoid 激活函数,它的输出在 0~1 之间。最终遗忘门的输出是和 Ci相同形状的矩阵,这个矩阵会和 Ci逐点相乘,决定要遗忘哪些东西 。遗忘门的输入是 Xi和Hi-1, Xi是当前时刻 的输入,而 hi-1为上一个时刻的隐状态。

 

(4)记忆门的结构

LSTM 的每一个单元中都有一个“记忆门”,用来控制记忆 Ci的那些部分。记忆门的输入同样是 Xi和Hi-1, 它的输出有2项,一项是it ,it同样经过 Sigmoid 函数运算得到,因此值都在 0~1 之间,还有一项是Ct,最终要记住的内容是Ct*it。

 

(5)输出门的结构

还需要一个“输出门”,用于输出内容。这里说是输出,其实是去计算另一个隐状态的值,真正的输出(如类别)需要通过 h1做进一步运算得到。同样是根据 Xi和Ht-1计算,Ot中每一个数值在 0~1 之间,ht通过Ot * tanh(Ct)得到 。

总结一下, LSTM 每一步的输入是Xt,隐状态是ht和Ct , 最终的输出通过ht进一步变换得到 。 在大多数情况下, RNN 和 LSTM 都是可以相互替换的。

 

三、CharRNN 的原理

CharRNN 是用于学习 RNN 的一个非常好的例子。它使用的是最经典的 N vs N 的模型 , 即输入是长度为 N 的序列,输出是与之长度相等的序列 。对于 Char RNN,输入序列是句子中的字母,输出依次是该输入的下一个字母 , 换句话说,是用已经输入的字母去预测下一个字母的概率 。 如一个简单的英文句子 Hello! 输入序列是{H,e,l, l, o},输出序列依次是{e,I, I, o,!}。 注意到这两个序列是等长的,因此可以用 N VS N RNN来建模。

    

使用独热向量来表示字母,然后依次输入网络。假设一共 26 个字母,那么字母 a的表示为第一位为1,其他 25位都是 0,即(1, 0, 0, 0,...,0), 字 母b的表示是第二位为 1,真他25位都是0,即(0,1,0,0,...,0)。输出相当 于一个 26 类分类问题,因此每一步输出的向量也是 26 维,每一维代表对应字母的概率,最后的损失使用交叉楠可以直接得到。

相对于字母来说,汉字的种类比较多,可能会导致模型过大,对此有以下两种优化方法:

(1)取最常用的 N 个汉字,将剩下的汉字变成单独一类。

(2)在输入时,可以加入一层 embedding层,这个 embedding层可以将汉字转换为较为稠密的表示,它可以代替稀疏的独热表示,取得更好的效果。

 

四、TensorFlow 中的 RNN 实现方式

先是使用RNNCell对RNN模型进行单步建模。 RNNCell可以处理时间上的“一步“,即输入上一步的隐层状态和这一步的数据,计算这一步的输和隐层状态。接着,TensorFlow 使用 tf.nn.dynamic_rnn 方法在时间维度上多次运行RNN Cell。最后还需要对输出结果建立损失。

1、基本单元 RNNCell

RNNCell 是 TensorFlow 中的基本单元 。 它本身是一个抽象类,它有两个可以直接使用的子类,一个是 BasicRNNCell,还有一个 是 BasicLSTMCell,前者对应基本的 RNN ,后者是基本的 LSTM。学习 RNNCell 要重点关注三个地方:类方法 call、类属性state size、类属性 output_size。

(1)类方法 call

所有 RNNCell 的子类都会实现一个 call 函数 。利用 call 函数可以实现 RNN 的单步计算,它的调用形式为(output, next_state) = call(input, state)。例如:初始的输入为 x1,而初始的隐层状态为 h0,可以调用(output1, h1) = cell.call(x1, h0) 得到当前的隐层状态 h1。 接着调用 (output2, h2) = cell.call(x2,h1) 可以得到h2,依此类推。

(2)类属性 state_size 和 output_size

RNNCell 的类属性 state_size 和 output_size 分别规定了隐层的大小平日输 出向量的大小 。 通常是以 batch 形式输入数据 ,即 input 的形状~(batch_size, input_size), 1周用 call 函数时对应的隐层的形状是(batch_size, state_size),输 出的形状是(batch_size, output_size)。

(3)定义一个RNN基本单元

import tensorflow as tf
import numpy as np

# 在TensorFlow中定义一个基本RNN单元
run_cell = tf.nn.run_cell.BasicRNNCell(num_units=128)
print(run_cell.state_size)

# 在TensorFlow中定义一个基本LSTM单元
lstm_cell = tf.nn.run_cell.BasicLSTMCell(num_units=128)
# lstm有两个隐状态c、h
# state_size = LSTMStateTuple(c=l28, h=128)
print(lstm_cell.state_size)

# 可以通过state.h以及state.c进行访问
lstm_cell= tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
inputs= tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size 
hO = lstm_cell.zero_state(32, np.float32)
output, hl = lstm_cell.call(inputs, hO)
# shape=(32, 128)
print (hl.h)
# shape=(32, 128)
print (hl.c) 

 

2、多层RNNCell -- MultiRNNCell

将 x输入第一层RNN 后得到隐层状态 h, 这个隐层状态相当于第二层 RNN 的输入,第二层 RNN的隐层状态、又相当于第三层RNN的输入,依此类推。在 TensorFlow 中可以使用 tf.nn.rnn_cell.multiRNNCell 函数对 RNN 进行堆叠。

# 调用一次生成一个RNN单元
def get_a_cell():
    return tf.nn.run_cell.BasicRNNCell(num_units=128)
# 生成多层RNN单元
cell= tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)])
inputs= tf.placeholder(np.float32 , shape=(32, 100) )
hO = cell.zero_state(32, np.float32)
# 多层计算
output, hl = cell.call(inputs, hO)

 

3、call函数的返回值output

调用 call 函数后得到(state_size,output_size),其中 h 对应了BasicRNNCell 的 state_size。 那么, y 是不是对应了 BasicRNNCell 的 output_size 呢 ?答案是否定的 。output 其实和隐状态的值是一样的。因此,还需要额外对输出定义新的变换,才能得到图中真正的输出 y。如果处理的是分类问题,那么还需要对 h 添加单独的 Softmax 层才能得到最后的分类概率输出。

 

4、使用 tf.nn.dynamic_rnn 展开时间维度

对于单个的RNNCell, 使用call函数进行运算时,只是在序列时间上前进了一步。如使用X1、h0得到h1, 通过X2、h1得到h2,也只在时间上前进了一步。如果序列长度为 n,要调用 n 次 call 函数,比较麻烦 。对此,TensorFlow 提供了一个 tf.nn.dynamic_mn 函数 , 使用该函数相当于调用了 n 次 call 函数。

设输入数据的格式为(batch_size, time_steps, input_size),其中 batch_size 表示 batch 的大小,即一个 batch 中序列的个数。 time_steps 表示序列本身的长度。假设已经定义好了一个 RNNCell, 调用tf.nn.dynamic_rnn的代码是:

# inputs: shape = (batch size, time steps, input size)
# cell: RNNCell
# initial_state: shape = (batch_size, cell.state_size)。
outputs, state= tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)

此时,得到的 outputs 是 time_steps 步里所有的输出。它的形状为 (batch_size, time_steps, cell.output_size)。 state 是最后一步的隐状态,它的形状为(batch_size, cell.state_size)。再对每一步的输出进行变换,可以得到损失并训练模型了。

 

五、使用 TensorFlow 实现 CharRNN

1、定义输入数据(model.py)

def build_inputs(self):
    with tf.name_scope('inputs'):
        # 输入数据的形状为(self.num_seqs, self.num_steps)
        # self.num_seqs是句子的个数(相当于batch_size),而self.num_steps表示每个句子的长度
        self.inputs = tf.placeholder(tf.int32, shape=(self.num_seqs, self.num_steps), name='inputs')
        # seIf.targets是self.inputs对应的训练目标,形状和self.inputs相同,内容是self.inputs每个字母对应的下一个字母。
        self.targets = tf.placeholder(tf.int32, shape=(self.num_seqs, self.num_steps), name='targets')
        # self.keep_prob控制了Dropout层所需要的概率。在训练时,使用self.keep_prob=0.5,在测试时,self.keep_prob=1.0
        self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')

        # 对于中文,需要使用embedding层
        # 英文字母没有必要用embedding层
        if self.use_embedding is False:
            self.lstm_inputs = tf.one_hot(self.inputs, self.num_classes)
        else:
            with tf.device("/cpu:0"):
                embedding = tf.get_variable('embedding', [self.num_classes, self.embedding_size])
                self.lstm_inputs = tf.nn.embedding_lookup(embedding, self.inputs)

 

2、定义多层 LSTM 模型

def build_lstm(self):
    # 创建单个cell
    def get_a_cell(lstm_size, keep_prob):
        lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_size)
        # 加入dropout,减少过拟合
        drop = tf.nn.rnn_cell.DropoutWrapper(lstm, output_keep_prob=keep_prob)
        return drop

    with tf.name_scope('lstm'):
        # 堆叠多层cell
        cell = tf.nn.rnn_cell.MultiRNNCell(
            [get_a_cell(self.lstm_size, self.keep_prob) for _ in range(self.num_layers)]
        )
        self.initial_state = cell.zero_state(self.num_seqs, tf.float32)

        # 通过dynamic_rnn对cell展开时间维度
        self.lstm_outputs, self.final_state = tf.nn.dynamic_rnn(cell, self.lstm_inputs, initial_state=self.initial_state)

        # 通过lstm_outputs得到概率
        seq_output = tf.concat(self.lstm_outputs, 1)
        x = tf.reshape(seq_output, [-1, self.lstm_size])
        with tf.variable_scope('softmax'):
            softmax_w = tf.Variable(tf.truncated_normal([self.lstm_size, self.num_classes], stddev=0.1))
            softmax_b = tf.Variable(tf.zeros(self.num_classes))
        # Wx+b变换
        self.logits = tf.matmul(x, softmax_w) + softmax_b
        # softmax层
        self.proba_prediction = tf.nn.softmax(self.logits, name='predictions')

 

3、定义损失

def build_loss(self):
    with tf.name_scope('loss'):
        y_one_hot = tf.one_hot(self.targets, self.num_classes)
        y_reshaped = tf.reshape(y_one_hot, self.logits.get_shape())
        # proba_prediction 和 targets 的独热编码做交叉熵作为损失
        loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=y_reshaped)
        self.loss = tf.reduce_mean(loss)

 

4、使用定义好的模型训练并生成英文

训练文件 shakespeare.txt 保存在项目的 data/ 文件夹下,对应的训练的命令为:

python train.py 
--input_file data/shakespeare.txt # UTF-8格式
--name shakespeare # 模型名称,决定模型保存位置,此时保存至model/shakespeare目录下
--num_steps 50 # 序列长度
--num_seqs 32 # 序列个数
--learning_rate 0.01 
--max_steps 20000

运行后,模型保存至model/shakespeare目录下,使用该模型进行测试的命令为:

python sample.py
--converter_path model/shakespeare/converter.pkl 
--checkpoint_path model/shakespeare/
--max_length 1000

运行报错:AttributeError: 'str' object has no attribute 'decode',str.decode('utf-8') 改为 str.encode('utf-8').decode('utf-8') 即可。

参数converter_path含义:神经网络生成的是字母类别id,而不是字母,这些类别 id 在输入模型时是通过一个 converter 转换的,程序会自动把 converter保存在 model/shakespeare 目录下 。在输出时需要使用 converter 将类别 id 转换回字母。

 

5、其他参数说明

lstm_size: LSTM 隐层的大小,默认为 128。

embedding_size : embedding 空间的大小,默认为 128。该参数只在使用use_embedding 时才会生效。

num_layers: LSTM的层数,默认为2。

max vocab: 使用的字母(汉字)的最大个数,默认为 3500。程序会自动挑选出使用最多的字,将剩下的字归为一类。

 

六、总结

本文介绍了 RNN 和 LSTM 的基本结构,使用 TensorFlow 实现 RNN ( LSTM )的基本步骤,最后实现了一个 CharRNN 项目。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值