TensorFlow2.0实战笔记之(6):Char RNN 文本生成

1. 简介

Char RNN是一种字符级的循环神经网络,其本质是序列数据的推测,即通过已知的字符,预测下一个字符出现的概率并选取概率最大者为下一个字符。比如,已知hello的前四个字母hell,那我们就可以据此预测下一个字符很可能是o。因为是char级别的,并没有单词或句子层次上的特征提取,相对而言比较简单。

根据Char RNN的特点,它可以用来写诗,写歌,生成文章,生成代码等。

2. 原理

2.1 RNN的原理

RNN(Recurrent Neural Networks),即循环神经网络。在实际应用中我们可能会碰到很多序列型的数据,如下图,它们可能是自然语言处理问题中的一个个单词,或者是语音处理中的每帧声音信号,也可能是时间序列问题,如每天的股票价格等,RNN就是一种对序列型数据进行建模的深度模型。

                                                                           

经典的RNN结构如下图所示。它的输入是x序列,输出是y序列,h序列为隐状态。

                                                                  

经典RNN的运算过程可以表示为

                                                                     h_t=f(Ux_t+Wh_{t-1}+b})

                                                                     y_t=Softmax(Vh_t+c)

其中,U,W和V是参数矩阵,b和c是偏置参数,f表示激活函数。需要注意的是,在每一步的计算中使用的参数U,W,b,V,c都是一样的,即是参数共享的。

在经典的 “N vs N” RNN结构中,输入和输出序列的等长的。除此之外,还有“N vs 1” RNN以及“1 vs N” RNN的结构。

                                             

2.2 LSTM的原理

LSTM(Long Short-Term Memory),即长短期记忆网络。在经典的RNN中,每一层的隐状态都由前一层的隐状态经过变换和激活函数得到,反向传播求导时最终得到的导数会包含每一步梯度的连乘,这会引起梯度爆炸或梯度消失的现象,所以RNN无法学到序列中蕴含的间隔时间较长的规律。LSTM是一种RNN的变体结构,每级LSTM的结构如下。从外部来看,两者的输入和输出都是一样的,但在内部,LSTM的隐状态相较于RNN添加了C_t,图中C_{t-1}C_t的水平线是LSTM的主干道,C_t在主干道的无障碍传递(加法代替乘法)解决了在较长序列中梯度失效的问题。此外,图中f_ti_to_t分别为遗忘门、记忆门、输出门的输出,两个tanh层则分别对应记忆单元的输入和输出,向量\tilde{C}_t由第一个tanh层生成用于更新记忆单元状态,\sigma是Sigmoid激活函数,它的输出在0~1之间。

                                                   

2.2.1 遗忘门

LSTM的每一个单元中都有一个“遗忘门”,用来控制遗忘掉C_{t-1}的哪些部分。遗忘门的结构如下图,它的输入是x_th_{t-1}x_t是当前时刻的输入,h_{t-1}为上一个时刻的隐状态。遗忘门的输出f_t是和C_{t-1}相同形状的矩阵,这个矩阵会和C_{t-1}逐点相乘,决定要遗忘哪些东西。显然,遗忘门输出接近0的位置的内容是要遗忘的,而接近1的部分是要保留的。

                                                     

2.2.2 记忆门

LSTM在遗忘一部分内容的同时也会记住一些新的内容,所以存在下图所示的“记忆门”。记忆门的输入同样是x_th_{t-1},它的输出有两项,一项是i_ti_t的值决定了当前输入x_t有多少将保存到记忆单元状态C_t中,同样经过Sigmoid函数运算得到,因此值都在0~1之间;还有一项是\tilde{C}_t,由tanh层生成,用于更新记忆单元状态。最终要“记住”的内容是\tilde{C}_ti_t的逐点相乘。

                                                     

 

遗忘和记忆的过程如下图所示,f_t是遗忘门的输出(0~1之间),而\tilde{C}_t*i_t是要记住的新东西。

                                                     

2.2.3 输出门

最后,还需要一个“输出门”,用于输出内容。如图所示,输入同样是x_th_{t-1}o_t中的每一个数值在0~1之间,h_t通过o_t*tanh(C_t)得到。

需要注意的是,这里所说的输出其实是计算下一个隐状态h_t的值,真正的输出(y_t)还需要对h_t做进一步运算得到。

                                                    

2.3 Char RNN的原理

Char RNN使用的是最经典的 “N vs N” RNN模型,即输入是长度为N的序列,输出是与之等长的序列。

在模型训练过程中,输入序列是句子中的字母,输出是对应输入的下一个字母,换句话说,是用已经输入的字母去预测下一个字母的概率。如一个简单的英文句子“Hello!”,输入序列是{H, e, l, l, o},输出序列依次是{e, l, l, o, !}。

                                                                      

使用Char RNN测试生成序列的具体流程为:首先选择一个x_1作为起始字符,然后通过训练好的模型得到下一个字符的概率,选取概率最大者作为下一个字符,并将该字符作为下一步的输入x_2,依此类推。根据需要生成的文本长度选择循环次数,即可生成所需长度的文字。

对于英文字母,一般使用one-hot编码,假设一共有26个字符,那么字母a的one-hot编码为(1, 0, 0, 0, ..., 0),即第一位为1,其余25位都是0。输出相当于一个26分类问题,每一步的输出向量是26维的,每一维代表相应字母的概率,最后的损失使用交叉熵可以直接得到。在实际模型中,由于字母有大小之分以及其他标点符号和空格等,因此总类别数会比26多。

在对中文建模时,由于汉字总数比较多,可能会导致模型过大,对此有两种优化方法:

  • 取最常用的N个汉字,将剩下的汉字单独归为一类,并用一个特殊的字符<unk>进行标注。
  • 在输入时,可以加入一层embedding层,该层可以将汉字转换为较为稠密的表示,它可以代替稀疏的one-hot表示方法,取得更好的效果。embedding的参数可以直接从数据中学到。

中文汉字的输出层和处理英文字母类似,都相当于一个N分类问题。

3. Tensorflow中RNN的实现方式

3.1 版本兼容问题

本文中使用的Tensorflow版本为2.3.1,python版本为3.8.5。

2019年10月1日,tensorflow正式发布了2.0版本,相对于1.0版本发生了很大的变化-->tensorflow2.0 新特性,而目前能查阅到的使用tensorflow实现RNN的资料基本上都是基于tensorflow 1.0版本的,为了与时俱进,本文将根据v1版本的资料,使用v2版本的一些新的API对其实现方式进行更新。

首先是v1版本的几个常用API,

tf.nn.rnn_cell.BasicRNNCell  # 定义一个基本RNN单元
tf.nn.rnn_cell.BasicLSTMCell  # 定义一个基本LSTM单元

tf.nn.rnn_cell.MultiRNNCell  # 对单层RNN进行堆叠

tf.nn.dynamic_rnn  # 展开时间维度

在tensorflow v2版本中,上述API都已被弃用,并会在将来的版本中删除。如果你仍然想在v2版本中使用这些API,则可以以如下方式调用,也就是在每个调用中都加入了“compat.v1”,简直难以忍受有木有!

tf.compat.v1.nn.rnn_cell.BasicRNNCell  # 定义一个基本RNN单元
tf.compat.v1.nn.rnn_cell.BasicLSTMCell  # 定义一个基本LSTM单元

tf.compat.v1.nn.rnn_cell.MultiRNNCell  # 对单层RNN进行堆叠

tf.compat.v1.nn.dynamic_rnn  # 展开时间维度

如果你不想使用上面兼容的版本,则可以顺应时代发展趋势,使用tensorflow v2中推荐的相应的替代API,

tf.compat.v1.nn.rnn_cell.BasicRNNCell   # --> tf.keras.layers.SimpleRNNCell
tf.compat.v1.nn.rnn_cell.BasicLSTMCell  # --> tf.keras.layers.LSTMCell

tf.compat.v1.nn.rnn_cell.MultiRNNCell   # --> tf.keras.layers.StackedRNNCells

tf.compat.v1.nn.dynamic_rnn             # --> tf.keras.layers.RNN

此外,在tensorflow v2中,placeholder也已被移除,可以使用tf.compat.v1.placeholder代替,当然,这仍然是v1版本中的实现方法,如果想要迁移到v2版本,则可以选择使用tf.keras.Input代替,它用于实例化一个Keras张量(调用后返回一个tensor),参数如下:

tf.keras.Input(
    shape=None,        # 整数,表示输入向量的维度大小;设置为'None'表示维度未知
    batch_size=None,   # 整数,表示可选的静态batch大小
    name=None,         # 字符串,表示层的可选名称,在model中应该是唯一的;如果未提供,将自动生成
    dtype=None,        # 字符串,表示输入的数据类型(如float32, float64, int32等)
    sparse=False,      # 布尔值,指定要创建的placeholder是否稀疏。'sparse'和'ragged'只有一个可以为'True'
    tensor=None,       # 可选择将现有的张量封装到输入层。如果设置,该层将不会创建placeholder张量
    ragged=False,      # 布尔值,指定要创建的placeholder是否不规则
    **kwargs           # 弃用参数支持。支持batch_shape和batch_input_shape
)

Keras张量是Tensorflow符号张量对象,并使用了某些属性对其进行扩充,这些属性使我们仅通过了解模型的输入和输出即可构建Keras模型。例如,如果a,b,c是Keras张量,则可以:

model = Model(input=[a, b], output=c)

tf.keras.Input的使用举例1(Keras 函数式API):

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(784,), name='img')
h1 = layers.Dense(32, activation='relu')(inputs)
h2 = layers.Dense(32, activation='relu')(h1)
outputs = layers.Dense(10, activation='softmax')(h2)
model = keras.Model(inputs=inputs, outputs=outputs, name='mymodel')
model.summary()

tf.keras.Input的使用举例2(Keras Sequential API):

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential()
model.add(keras.Input(shape=(250, 250, 3)))  # 250x250 RGB images
model.add(layers.Conv2D(32, 5, strides=2, activation="relu"))
model.add(layers.Conv2D(32, 3, activation="relu"))
model.add(layers.MaxPooling2D(3))

# Can you guess what the current output shape is at this point? Probably not.
# Let's just print it:
model.summary()

# The answer was: (40, 40, 32), so we can keep downsampling...

model.add(layers.Conv2D(32, 3, activation="relu"))
model.add(layers.Conv2D(32, 3, activation="relu"))
model.add(layers.MaxPooling2D(3))
model.add(layers.Conv2D(32, 3, activation="relu"))
model.add(layers.Conv2D(32, 3, activation="relu"))
model.add(layers.MaxPooling2D(2))

# And now?
model.summary()

# Now that we have 4x4 feature maps, time to apply global max pooling.
model.add(layers.GlobalMaxPooling2D())

# Finally, we add a classification layer.
model.add(layers.Dense(
  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值