RNN循环神经网络

一、预备知识

1.序列

具有先后顺序的数据一般叫作序列(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_{t} = \sigma (W_{xh}x_{t} + W_{hh}h_{t-1} + b),经过𝑠个词向量的输入后得到网络最终的状态张量 h𝑠, h𝑠较好地代表了句子的全局语义信息

上述网络结构在时间戳上折叠,如图所示, 网络循环接受序列的每个特征向量𝒙t,并刷新内部状态向量 𝑡,同时形成输出𝒐t。对于这种网络结构,把它叫做循环网络结构(Recurrent Neural Network, 简称 RNN)。

如果使用张量W_{xh}W_{hh}和偏置𝒃来参数化𝑓𝜃网络, 并按照h_{t} = \sigma (W_{xh}x_{t} + W_{hh}h_{t-1} + b)
方式更新内存状态,把这种网络叫做基本的循环神经网络,如无特别说明,一般说的循环神经网络即指这种实现。 在循环神经网络中,激活函数更多地采用 tanh 函数,并且可以选择不使用偏执𝒃来进一步减少参数量。 状态向量 h_{t}可以直接用作输出, 即o_{t}=h_{t}, 也可以对 𝑡做一个简单的线性变换𝒐𝑡 = W_{ho} * h_{t}后得到每个时间戳上的网络输出o_{t}

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 实例即可完成前向运算:o_{t}, [h_{t}] = Cell(x_{t}, [h_{t-1}])

对于 SimpleRNNCell 来说, o_t = h_t,并没有经过额外的线性层转换, 是同一个对象,  两者 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 来实现,用做下一层的输入
 

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Super.Bear

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值