具有先后顺序的数据一般叫作序列(Sequence)。 比如随时间而变化的商品价格数据,某件商品 A 在 1 月到 6 月之间的价格变化趋势,记为一维向量: [𝑥1, 𝑥2, 𝑥3, 𝑥4, 𝑥5, 𝑥6]。如果要表示b件商品的在1月到6月之间的变化趋势,可以记为 2 维张量:[[𝑥1(1), 𝑥2(1), ⋯ , 𝑥6(1)] , [𝑥1(2), 𝑥2(2), ⋯ , 𝑥6(2)] , ⋯ , [𝑥1(𝑏), 𝑥2(𝑏), ⋯ , 𝑥6(𝑏)]]其中𝑏表示商品的数量, 张量 shape 为[𝑏, 6]。
但是,对于许多信号并不能直接用一个标量数值表。每个时间戳产生长度为𝑛的特征向量,则需要 shape 为[𝑏, 𝑠, 𝑛]的张量才能表示。 文本数据它在每个时间戳上面产生的单词是一个字符,并不是数值, 不能直接用某个标量表示。 神经网络本质上是一系列的矩阵相乘、相加等数学运算, 它并不能够直接处理字符串类型的数据。如果希望神经网络能够用于自然语言处理任务,那么怎么把单词或字符(其他非数值类型的信号)转化为数值就变得尤为关键
2.One-hot对于一个含有𝑛个单词的句子, 单词的一种简单表示方法使用One-hot编码。 以英文句子为例, 假设只考虑最常用的 1 万个单词,那么每个单词就可以表示为某位为 1,其它位置为 0 且长度为 1 万的稀疏 One-hot 向量;对于中文句子, 如果也只考虑最常用的 5000 个汉字,一个汉字可以用长度为 5000 的 One-hot 向量表示。 如图所示,如果只考虑𝑛个地名单词,可以将每个地名编码为长度为𝑛的 Onehot 向量
把文字编码为数值的过程叫作 Word Embedding。 One-hot 的编码方式实现 Word Embedding 编码,过程不需要学习和训练。但是 One-hot 编码的向量是高维度而且极其稀疏的,大量的位置为 0, 计算效率较低,同时也不利于神经网络的训练,而且它忽略了单词先天具有的语义相关性,不能很好地体现原有文字的语义相关度
3.Word Vector在自然语言处理领域,有专门的一个研究方向在探索如何学习到单词的表示向量(Word Vector),使得语义层面的相关性能够很好地通过 Word Vector 体现出来。一个衡量词向量之间相关度的方法就是余弦相关度(Cosine similarity):其中𝒂和𝒃代表了两个词向量。
单词“France”和“Italy”的相似度,以及单词“ball”和“crocodile”的相似度, 𝜃为两个词向量之间的夹角。 可以看到cos(𝜃)较好地反映了语义相关性
4.Embeddibg层在神经网络中,单词的表示向量可以直接通过训练的方式得到,把单词的表示层叫作Embedding 层。 Embedding 层负责把单词编码为某个词向量𝒗, 它接受的是采用数字编码的单词编号𝑖 系统总单词数量记为𝑁vocab, 输出长度为𝑛的向量𝒗:𝒗 = 𝑓𝜃(𝑖|𝑁vocab, 𝑛)。
实现Embedding层,构建一个 shape 为[𝑁vocab, 𝑛]的查询表对象 table,对任意的单词编号𝑖,只需要查询到对应位置上的向量并返回即可:𝒗 = 𝑡𝑎𝑏𝑙𝑒[𝑖]
Embedding 层是可训练的, 它可放置在神经网络之前,完成单词到向量的转换,得到的表示向量可以继续通过神经网络完成后续任务,并计算误差ℒ,采用梯度下降算法来实现端到端(end-to-end)的训练
在 TensorFlow 中,可以通过 layers.Embedding(𝑁vocab,𝑛)来定义一个 Word Embedding层,其中𝑁vocab参数指定词汇数量, 𝑛指定单词向量的长度。
x = tf.range(10) # 生成10个单词的数字编码
x = tf.random.shuffle(x) # 打散
# 创建10个单词,每个单词的用长度为4的向量表示的层
net = layers.Embedding(10, 4)
out = net(x) # 获取词向量
创建了 10 个单词的 Embedding 层,每个单词用长度为 4 的向量表示,可以传入数字编码为 0~9 的输入,得到这 4 个单词的词向量,这些词向量随机初始化的,尚未经过网络训练
Embedding 层的查询表是随机初始化的, 需要从零开始训练。 实际上, 可以使用预训练的 Word Embedding 模型来得到单词的表示方法,基于预训练模型的词向量相当于迁移了整个语义空间的知识,往往能得到更好的性能。比如预训练模型有 Word2Vec 和 GloVe
利用预训练好的模型参数去初始化 Embedding 层的查询表
# 从预训练模型中加载词向量表
embed_glove = load_embed('glove.6B.50d.txt')
# 直接利用预训练的词向量表初始化 Embedding 层
net.set_weights([embed_glove])
经过预训练的词向量模型初始化的 Embedding 层可以设置为不参与训练: net.trainable = False, 预训练的词向量直接应用到此特定任务上
二、循环神经网络原理传统的神经网络的结构比较简单:输入层 – 隐藏层 – 输出层。RNN 跟传统神经网络最大的区别在于每次都会将前一次的输出结果,带到下一次的隐藏层中,一起训练
在每个时间戳𝑡,网络层接受当前时间戳的输入𝒙𝑡和上一个时间戳的网络状态向量 𝑡-1,经过ht =f𝜃(h(t-1), xt) 变换后得到当前时间戳的新状态向量 h𝑡, 并写入内存状态中,其中𝑓𝜃代表了网络的运算逻辑, 𝜃为网络参数集。在每个时间戳上,网络层均有输出产生𝒐𝑡, 𝒐𝑡 = 𝑔𝜙( 𝑡), 即将网络的状态向量变换后输出
Memory 机制:如果网络能够提供一个单独的内存变量,每次提取词向量的特征并刷新内存变量,直至最后一个输入完成,此时的内存变量即存储了所有序列的语义特征,并且由于输入序列之间的先后顺序,使得内存变量内容与序列顺序紧密关联
Memory 机制实现为一个状态张量,状态张量h0为初始的内存状态, 可以初始化为全 0。除了原来的𝑾xh参数共享外, 额外增加了一个𝑾hh参数, 每个时间戳𝑡上状态张量 刷新机制为:,经过𝑠个词向量的输入后得到网络最终的状态张量 h𝑠, h𝑠较好地代表了句子的全局语义信息
上述网络结构在时间戳上折叠,如图所示, 网络循环接受序列的每个特征向量𝒙t,并刷新内部状态向量 𝑡,同时形成输出𝒐t。对于这种网络结构,把它叫做循环网络结构(Recurrent Neural Network, 简称 RNN)。
如果使用张量、
和偏置𝒃来参数化𝑓𝜃网络, 并按照
方式更新内存状态,把这种网络叫做基本的循环神经网络,如无特别说明,一般说的循环神经网络即指这种实现。 在循环神经网络中,激活函数更多地采用 tanh 函数,并且可以选择不使用偏执𝒃来进一步减少参数量。 状态向量
可以直接用作输出, 即
=
, 也可以对 𝑡做一个简单的线性变换𝒐𝑡 =
*
后得到每个时间戳上的网络输出
CNN 与RNN:
CNN:借助卷积核(kernel)提取特征后,送入后续网络(BN,池化,激活,Dense)进行分类、目标检测等操作。CNN借助卷积核从空间维度提取信息,卷积核参数空间共享
RNN:借助循环核(Cell)提取特征后,送入后续网络(激活,Dense)进行预测等操作。RNN借助循环核从时间维度提前信息,循环核参数时间共享
三、RNN实现在 TensorFlow 中,可以通过 layers.SimpleRNNCell 来完成网络搭建。 RNN 表示通用意义上的循环神经网络,对于目前介绍的基础循环神经网络(SimpleRNN) 。SimpleRNN 与 SimpleRNNCell 的区别在于,带 Cell 的层仅仅是完成了一个时间戳的前向运算,不带 Cell 的层一般是基于Cell 层实现的, 它在内部已经完成了多个时间戳的循环运算
1.SimpleRNNCell(Cell方式)函数原型:
tf.keras.layers.SimpleRNNCell(
units, activation='tanh', use_bias=True,
kernel_initializer='glorot_uniform',
recurrent_initializer='orthogonal',
bias_initializer='zeros', kernel_regularizer=None,
recurrent_regularizer=None, bias_regularizer=None, kernel_constraint=None,
recurrent_constraint=None, bias_constraint=None, dropout=0.0,
recurrent_dropout=0.0, **kwargs
)
SimpleRNNCell 内部维护了 3 个张量, kernel 变量即𝑾xh 张量, recurrent_kernel变量即𝑾hh 张量, bias 变量即偏置𝒃向量。 但是 RNN 的 Memory 向量 并不由SimpleRNNCell 维护,需要用户自行初始化向量 h𝟎并记录每个时间戳上的 h𝒕。
多层RNN网络
x = tf.random.normal([4, 80, 100]) # 生成输入张量,4个100单词的句子,80是时间维度
# 构建2层Cell,内存状态向量长度都为 64
# 输入特征n =100, 序列长度s=80, 状态长度=64的Cell
cell0 = layers.SimpleRNNCell(64)
cell1 = layers.SimpleRNNCell(64)
h0 = [tf.zeros([4, 64])] # cell0 的初始状态向量
h1 = [tf.zeros([4, 64])] # cell1 的初始状态向量
# 在时间轴上面循环计算
for xt in tf.unstack(x, axis=1):
# xt,h0输入,输出out0,h0
out0, h0 = cell0(xt, h0)
# 上一个cell的输出作为本cell的输入
out1, h1 = cell1(out0, h1)
有多少层rnn就需要用户维护几个初始状态向量,h[ 𝑡]通过一个 List 包裹起来, 这么设置是为了与 LSTM、 GRU 等 RNN 变种格式统一。在循环神经网络的初始化阶段, 状态向量 𝟎一般初始化为全 0 向量,通过调用 Cell 实例即可完成前向运算:
对于 SimpleRNNCell 来说, =
,并没有经过额外的线性层转换, 是同一个对象, 两者 id 一致,即状态向量直接作为输出向量。 对于长度为𝑠的训练来说,需要循环通过Cell 类𝑠次才算完成一次网络层的前向运算,在时间维度上解开,循环多次实现整个网络的向前计算
循环神经网络的每一层、每一个时间戳上面均有状态输出,一般来说,最末层 Cell 的状态有可 能保存了高层的全局语义特征, 因此一般使用最末层的输出作为后续任务网络的输入。 更 特别地, 每层最后一个时间戳上的状态输出包含了整个序列的全局信息
2.SimpleRNN(层方式)通过 SimpleRNNCell 层的使用, 可以非常深入地理解循环神经网络前向运算的每个细节,但是在实际使用中, 为了简便,不希望手动参与循环神经网络内部的计算过程,比如每一层的状态向量的初始化,以及每一层在时间轴上展开的运算。 通过 SimpleRNN层高层接口可以非常方便地帮助实现此目的
多次RNN
# 创建 RNN 层时,设置返回所有时间戳上的输出
x = tf.random.normal([4, 80, 100])
net = Sequential([
# 除最末层外,都需要返回所有时间戳的输出,用作下一层的输入
layers.SimpleRNN(64, return_sequences=True),
layers.SimpleRNN(64)
])
out = net(x)
每层都需要上一层在每个时间戳上面的状态输出,因此除了最末层以外,所有的 RNN 层都需要返回每个时间戳上面的状态输出,通过设置 return_sequences=True 来实现,用做下一层的输入