1. 简介
Char RNN是一种字符级的循环神经网络,其本质是序列数据的推测,即通过已知的字符,预测下一个字符出现的概率并选取概率最大者为下一个字符。比如,已知hello的前四个字母hell,那我们就可以据此预测下一个字符很可能是o。因为是char级别的,并没有单词或句子层次上的特征提取,相对而言比较简单。
根据Char RNN的特点,它可以用来写诗,写歌,生成文章,生成代码等。
2. 原理
2.1 RNN的原理
RNN(Recurrent Neural Networks),即循环神经网络。在实际应用中我们可能会碰到很多序列型的数据,如下图,它们可能是自然语言处理问题中的一个个单词,或者是语音处理中的每帧声音信号,也可能是时间序列问题,如每天的股票价格等,RNN就是一种对序列型数据进行建模的深度模型。
经典的RNN结构如下图所示。它的输入是x序列,输出是y序列,h序列为隐状态。
经典RNN的运算过程可以表示为
其中,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添加了,图中
到
的水平线是LSTM的主干道,
在主干道的无障碍传递(加法代替乘法)解决了在较长序列中梯度失效的问题。此外,图中
、
、
分别为遗忘门、记忆门、输出门的输出,两个tanh层则分别对应记忆单元的输入和输出,向量
由第一个tanh层生成用于更新记忆单元状态,
是Sigmoid激活函数,它的输出在0~1之间。
2.2.1 遗忘门
LSTM的每一个单元中都有一个“遗忘门”,用来控制遗忘掉的哪些部分。遗忘门的结构如下图,它的输入是
和
,
是当前时刻的输入,
为上一个时刻的隐状态。遗忘门的输出
是和
相同形状的矩阵,这个矩阵会和
逐点相乘,决定要遗忘哪些东西。显然,遗忘门输出接近0的位置的内容是要遗忘的,而接近1的部分是要保留的。
2.2.2 记忆门
LSTM在遗忘一部分内容的同时也会记住一些新的内容,所以存在下图所示的“记忆门”。记忆门的输入同样是和
,它的输出有两项,一项是
,
的值决定了当前输入
有多少将保存到记忆单元状态
中,同样经过Sigmoid函数运算得到,因此值都在0~1之间;还有一项是
,由tanh层生成,用于更新记忆单元状态。最终要“记住”的内容是
和
的逐点相乘。
遗忘和记忆的过程如下图所示,是遗忘门的输出(0~1之间),而
是要记住的新东西。
2.2.3 输出门
最后,还需要一个“输出门”,用于输出内容。如图所示,输入同样是和
,
中的每一个数值在0~1之间,
通过
得到。
需要注意的是,这里所说的输出其实是计算下一个隐状态的值,真正的输出(
)还需要对
做进一步运算得到。
2.3 Char RNN的原理
Char RNN使用的是最经典的 “N vs N” RNN模型,即输入是长度为N的序列,输出是与之等长的序列。
在模型训练过程中,输入序列是句子中的字母,输出是对应输入的下一个字母,换句话说,是用已经输入的字母去预测下一个字母的概率。如一个简单的英文句子“Hello!”,输入序列是{H, e, l, l, o},输出序列依次是{e, l, l, o, !}。
使用Char RNN测试生成序列的具体流程为:首先选择一个作为起始字符,然后通过训练好的模型得到下一个字符的概率,选取概率最大者作为下一个字符,并将该字符作为下一步的输入
,依此类推。根据需要生成的文本长度选择循环次数,即可生成所需长度的文字。
对于英文字母,一般使用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.GlobalMaxPo