卷积神经网络利用数据的局部相关性和权值共享的思想大大减少了网络的参数量,非常适合于图片这种具有空间 (Spatial) 局部相关性的数据。自然界的信号除了具有空间维度之外,还有一个时间 (Temporal) 维度。具有时间维度的信号非常常见,比如文本、说话时发出的语音信号、随着时间变化的股市参数等。这类数据并不一定具有局部相关性,同时数据在时间维度上的长度也是可变的,如何解决这一类信号的分析、识别等问题是将人工智能推向通用人工智能路上必须解决的一项任务。
在介绍循环神经网络之前,首先我们来介绍对于具有时间先后顺序的数据的表示方法。具有先后顺序的数据一般叫作序列 (Sequence),比如随时间而变化的商品价格数据就是非常典型的序列。考虑某件商品 A 在 1 月到 6 月之间的价格变化趋势,我们记为一维向量:[1, 2, 3, 4, 5, 6],它的 shape 为 [6]。如果要表示 件商品在 1 月到 6 月之间的价格变化趋势,可以记为 2 维张量,张量 shape 为[, 6],其中表示商品的数量。
我们把文字编码为数值的过程叫作 Word mbedding。One-hot 的编码方式实现 Word Embedding 简单直观,编码过程不需要学习和训练。但是 One-hot 编码的向量是高维度而且极其稀疏的,计算效率较低,同时也不利于神经网络的训练;另从语义角度来讲,One-hot 编码还有一个严重的问题,它忽略了单词先天具有的语义相关性。对于一组单词来说,如果采用 One-hot 编码,得到的向量之间没有相关性,不能很好地体现原有文字的语义相关度,因此 One-hot 编码具有明显的缺陷。
在自然语言处理领域,用余弦相关度 (Cosine similarity) 衡量词向量之间相关度。
similarity(,) ≜ cos() = ⋅ ∣ ∣ ∙ ∣ ∣ \frac{ ⋅ }{|| ∙ ||} ∣a∣∙∣b∣a⋅b
其中和代表了两个词向量。下图演示了单词“France”和“Italy”的相似度,以及单词 “ball” 和 “crocodile” 的相似度,为两个词向量之间的夹角。可以看到cos()较好地反映了语义相关性。
在神经网络中,单词的表示向量可以直接通过训练的方式得到,单词的表示层叫作 Embedding 层。Embedding 层负责把单词编码为某个词向量,它接受的是采用数字编码的单词编号 ,如 2 表示“I”,3 表示“me”等,系统总单词数量记为 vocab,输出长度为 的向量 :
Embedding 层实现起来非常简单,构建一个 shape 为 [vocab, ] 的查询表对象 table,对于任意的单词编号 ,只需要查询到对应位置上的向量并返回即可:
Embedding 层是可训练的,它可放置在神经网络之前,完成单词到向量的转换,得到的表示向量可以继续通过神经网络完成后续任务,并计算误差ℒ,采用梯度下降算法来实现端到端(end-to-end)的训练。
在 TensorFlow 中,可以通过 layers.Embedding(vocab,) 来定义一个 Word Embedding 层,其中 vocab参数指定词汇数量, 指定单词向量的长度。例如,创建 10 个单词的 Embedding 层,每个单词用长度为 4 的向量表示,可以传入数字编码为 0~9 的输入,得到这 10 个单词的词向量,这些词向量随机初始化的,尚未经过网络训练。
x = tf.range(10) # 生成 10 个单词的数字编码
x = tf.random.shuffle(x) # 打散
# 创建共 10 个单词,每个单词用长度为 4 的向量表示的层
net = layers.Embedding(10, 4)
out = net(x) # 获取词向量
print(out)
# 可以直接查看 Embedding 层内部的查询表 table:
print(net.embeddings)
# 可以查看 net.embeddings 张量的可优化属性,为 True 即可以通过梯度下降算法优化
print(net.embeddings.trainable)
Embedding 层的查询表是随机初始化的,需要从零开始训练。实际上,可以使用预训练的 Word Embedding 模型来得到单词的表示方法,基于预训练模型的词向量相当于迁移了整个语义空间的知识,往往能得到更好的性能。
因此,对于 Embedding 层,不再采用随机初始化的方式,而是利用我们已经预训练好的模型参数去初始化 Embedding 层的查询表。例如:
# 从预训练模型中加载词向量表
embed_glove = load_embed('glove.6B.50d.txt')
# 直接利用预训练的词向量表初始化 Embedding 层
net.set_weights([embed_glove]
经过预训练的词向量模型初始化的 Embedding 层可以设置为不参与训练:net.trainable = False,那么预训练的词向量就直接应用到此特定任务上;如果希望能够学到区别于预训练词向量模型不同的表示方法,那么可以把 Embedding 层包含进反向传播算法中去,利用梯度下降来微调单词表示方法。
现在我们来考虑如何处理序列信号,以文本序列为例,考虑一个句子:“I hate this boring movie” 通过 Embedding 层,可以将它转换为 shape 为 [, , ] 的张量, 为句子数量, 为句子长度, 为词向量长度。上述句子可以表示为 shape 为[1,5,10]的张量,其中 5 代表句子单词长度,10 表示词向量长度。
以情感分类任务为例,情感分类任务通过分析给出的文本序列,提炼出文本数据表达的整体语义特征,从而预测输入文本的情感类型:正面评价或者负面评价,如下图:
从分类角度来看,情感分类问题就是一个简单的二分类问题,与图片分类不一样的是,由于输入是文本序列,传统的卷积神经网络并不能取得很好的效果。那么,什么类型的网络擅长处理序列数据,全连接层可行吗?
首先我们想到的是,对于每个词向量,分别使用一个全连接层网络 = ( + ) 提取语义特征,如下图所示:
各个单词的词向量通过 个全连接层分类网络 1 提取每个
单词的特征,所有单词的特征最后合并,并通过分类网络 2 输出序列的类别概率分布,对于长度为的句子来说,至少需要个全网络层。
这种方案的缺点有:1、网络参数量较多,内存占用和计算代价较高,同时由于每个序列的长度 并不相同,网络结构是动态变化的;2、每个全连接层子网络 和 只能感受当前词向量的输入,并不能感知之前和之后的语
境信息,导致句子整体语义的缺失,每个子网络只能根据自己的输入来提取高层特征。
与卷积神经网络相同,利用权值共享的思想,可以大大减少了网络的参数量,使得网络训练起来更加高效。我们尝试将 个网络层参数共享, 相当于使用一个全连接网络来提取所有单词的特征信息,如下图所示:
通过权值共享后,参数量大大减少,网络训练变得更加稳定高效。但是,这种网络结构并没有考虑序列之间的先后顺序,将词向量打乱次序仍然能获得相同的输出,无法获取有效的全局语义信息。
如何赋予网络提取整体语义特征的能力呢?或者说,如何让网络能够按序提取词向量的语义信息,并累积成整个句子的全局语义信息呢?
我们想到了内存 (Memory) 机制。如果网络能够提供一个单独的内存变量,每次提取词向量的特征并刷新内存变量,直至最后一个输入完成,此时的内存变量即存储了所有序列的语义特征,并且由于输入序列之间的先后顺序,使得内存变量内容与序列顺序紧密关联。
Memory 机制实现为一个状态张量 ,如下图所示,除了原来的 参数共享外,这里额外增加了一个 参数,每个时间戳 上状态张量 h 刷新机制为:
其中状态张量 h0 为初始的内存状态,可以初始化为全 0,经过 个词向量的输入后得到网络最终的状态张量 h, h 较好地代表了句子的全局语义信息,基于 h 通过某个全连接层分类器即可完成情感分类任务。
通过一步步探索,我们最终提出了一种 “新型” 的网络结构,如图下所示:
在每个时间戳 ,网络层接受当前时间戳的输入 和上一个时间戳的网络状态向量 h−1,经过
变换后得到当前时间戳的新状态向量 h,并写入内存状态中,其中 代表了网络的运算逻辑, 为网络参数集。在每个时间戳上,网络层均有输出产生 , = ( h),即将网络的状态向量变换后输出。
将上述网络结构在时间戳上折叠,如下图:
网络循环接受序列的每个特征向量 t,并刷新内部状态向量 h,同时形成输出 t。对于这种网络结构,我们把它叫做循环网络结构(Recurrent Neural Network,简称 RNN)。
更特别地,如果使用张量 xh 、hh 和偏置 来参数化 网络,并按照
方式更新内存状态,我们把这种网络叫做基本的循环神经网络,如无特别说明,一般说的循环神经网络即指这种实现。在循环神经网络中,激活函数更多地采用 tanh 函数,并且可以选择不使用偏执 来进一步减少参数量。状态向量 h 可以直接用作输出,即 = h,也可以对 做一个简单的线性变换 = hh 后得到每个时间戳上的网络输出。
通过循环神经网络的更新表达式可以看出输出对张量xh 、hh 和偏置均是可导的,则可以利用自动梯度算法来求解网络的梯度。此处仅简单地推导一下 RNN 的梯度传播公式,并观察其特点。
考虑梯度 d L d h h \frac{dℒ}{dhh} dWhhdL,其中 ℒ 为网络的误差,只考虑最后一个时刻 的输出 与真实值之间的差距。由于 hh 被每个时间戳 上权值共享,在计算 d L d h h \frac{dℒ}{dhh} dWhhdL 时需要将每个中间时间戳上面的梯度求和,利用链式法则展开为
其中 d L d \frac{dℒ}{d} dotdL 可以基于损失函数直接求得, d o t d h \frac{dot}{dh} dhtdot 在 = h 的情况下:
而 d + h d h h \frac{d+ h}{dℎℎ} dWhhd+hi 的梯度将 h 展开后也可以求得:
其中 d + h d h h \frac{d+ h}{dℎℎ} dWhhd+hi 只考虑到一个时间戳的梯度传播,即 “直接” 偏导数,与 d L d h h \frac{dℒ}{dℎℎ} dWhhdL 考虑 = 1, ⋯ , 所有的时间戳的偏导数不同。因此,只需要推导出 d h t d h i \frac{dht}{dℎi} dhidht 的表达式即可完成循环神经网络的梯度推导。利用链式法则,把 d h t d h i \frac{dht}{dℎi} dhidht 分拆分连续时间戳的梯度表达式:
其中()把向量 x 的每个元素作为矩阵的对角元素,得到其它元素全为 0 的对角矩阵,例如:
在 TensorFlow 中,可以通过 layers.SimpleRNNCell 来完成 ( + −1 + ) 计算。需要注意的是,在 TensorFlow 中,RNN 表示通用意义上的循环神经网络,对于上述基础循环神经网络,它一般叫做 SimpleRNN。SimpleRNN 与 SimpleRNNCell 的区别在于,带 Cell 的层仅仅是完成了一个时间戳的前向运算,不带 Cell 的层一般是基于 Cell 层实现的,它在内部已经完成了多个时间戳的循环运算,因此使用起来更为方便快捷。
以某输入特征长度 = 4,Cell 状态向量特征长度 ℎ = 3 为例,首先需要新建一个 SimpleRNNCell,不需要指定序列长度 ,代码如下:
import tensorflow as tf
from tensorflow.keras import layers
cell = layers.SimpleRNNCell(3) # 创建 RNN Cell,内存向量长度为 3
cell.build(input_shape=(None,4)) # 输出特征长度 n=4
cell.trainable_variables # 打印 wxh, whh, b 张量
可以看到,SimpleRNNCell 内部维护了 3 个张量,kernel 变量即 xh 张量,recurrent_kernel 变量即 hh 张量,bias 变量即偏置 向量。但是 RNN 的 Memory 向量 h 并不由 SimpleRNNCell 维护,需要用户自行初始化向量 h 并记录每个时间戳上的 h。
通过调用 Cell 实例即可完成前向运算:
对于 SimpleRNNCell 来说, = h,并没有经过额外的线性层转换,是同一个对象;[ h] 通过一个 List 包裹起来,这么设置是为了与 LSTM、GRU 等 RNN 变种格式统一。在循环神经网络的初始化阶段,状态向量 h一般初始化为全 0 向量,例如:
# 初始化状态向量,用列表包裹,统一格式
h0 = [tf.zeros([4, 64])]
x = tf.random.normal([4, 80, 100]) # 生成输入张量,4 个 80 单词的句子
xt = x[:,0,:] # 所有句子的第 1 个单词
# 构建输入特征 n=100,序列长度 s=80,状态长度=64 的 Cell
cell = layers.SimpleRNNCell(64)
out, h1 = cell(xt, h0) # 前向计算
print(out.shape, h1[0].shape)
(4, 64) (4, 64)
可以看到经过一个时间戳的计算后,输出和状态张量的 shape 都为[, ℎ],这两者的 id 一致,即状态向量直接作为输出向量。对于长度为 的训练来说,需要循环通过 Cell 类 次才算完成一次网络层的前向运算。例如:
h = h0 # h 保存每个时间戳上的状态向量列表
# 在序列长度的维度解开输入,得到 xt:[b,n]
for xt in tf.unstack(x, axis=1):
out, h = cell(xt, h) # 前向计算,out 和 h 均被覆盖
# 最终输出可以聚合每个时间戳上的输出,也可以只取最后时间戳的输出
out = out
最后一个时间戳的输出变量 out 将作为网络的最终输出。实际上,也可以将每个时间戳上的输出保存,然后求和或者均值,将其作为网络的最终输出。
和卷积神经网络一样,循环神经网络虽然在时间轴上面展开了多次,但只能算一个网络层。通过在深度方向堆叠多个 Cell 类来实现深层卷积神经网络一样的效果,大大的提升网络的表达能力。但是和卷积神经网络动辄几十、上百的深度层数来比,循环神经网络很容易出现梯度弥散和梯度爆炸到现象,深层的循环神经网络训练起来非常困难,目前常见的循环神经网络模型层数一般控制在十层以内。
以两层的循环神经网络为例,介绍利用 Cell 方式构建多层 RNN 网络。首先,新建两个 SimpleRNNCell 单元,代码如下:
x = tf.random.normal([4,80,100])
xt = x[:,0,:] # 取第一个时间戳的输入 x0
# 构建 2 个 Cell,先 cell0,后 cell1,内存状态向量长度都为 64
cell0 = layers.SimpleRNNCell(64)
cell1 = layers.SimpleRNNCell(64)
h0 = [tf.zeros([4,64])] # cell0 的初始状态向量
h1 = [tf.zeros([4,64])] # cell1 的初始状态向量
在时间轴上面循环计算多次来实现整个网络的前向运算,每个时间戳上的输入 xt 首先通过第一层,得到输出 out0,再通过第二层,得到输出 out1;即先完成一个时间戳上的输入在所有层上的传播,再循环计算完所有时间戳上的输入。代码如下:
for xt in tf.unstack(x, axis=1):
# xt 作为输入,输出为 out0
out0, h0 = cell0(xt, h0)
# 上一个 cell 的输出 out0 作为本 cell 的输入
out1, h1 = cell1(out0, h1)
实际上,也可以先完成输入在第一层上所有时间戳的计算,并保存第一层在所有时间戳上的输出列表,再计算第二层、第三层等的传播。代码如下:
# 保存上一层的所有时间戳上面的输出
middle_sequences = []
# 计算第一层的所有时间戳上的输出,并保存
for xt in tf.unstack(x, axis=1):
out0, h0 = cell0(xt, h0)
middle_sequences.append(out0)
# 计算第二层的所有时间戳上的输出
# 如果不是末层,需要保存所有时间戳上面的输出
for xt in middle_sequences:
out1, h1 = cell1(xt, h1)
使用这种方式的话,我们需要一个额外的 List 来保存上一层所有时间戳上面的状态信息:iddle_sequences.append(out0)。这两种方式效果相同,可以根据个人喜好选择编程风格。需要注意的是,循环神经网络的每一层、每一个时间戳上面均有状态输出,一般来说,最末层 Cell 的状态有可能保存了高层的全局语义特征,因此一般使用最末层的输出作为后续任务网络的输入。更特别地,每层最后一个时间戳上的状态输出包含了整个序列的全局信息,如果只希望选用一个状态变量来完成后续任务,比如情感分类问题,一般选用最末层、最末时间戳的状态输出最为合适。
通过 SimpleRNNCell 层的使用,我们可以非常深入地理解循环神经网络前向运算的每个细节,但是在实际使用中,为了简便,不希望手动参与循环神经网络内部的计算过程,比如每一层的 h 状态向量的初始化,以及每一层在时间轴上展开的运算。通过 SimpleRNN 层高层接口可以非常方便地实现此目的。
比如完成单层循环神经网络的前向运算,可以方便地实现如下:
layer = layers.SimpleRNN(64) # 创建状态向量长度为 64 的 SimpleRNN 层
x = tf.random.normal([4, 80, 100])
out = layer(x) # 和普通卷积网络一样,一行代码即可获得输出
out.shape
TensorShape([4, 64])
可以看到,通过 SimpleRNN 可以仅需一行代码即可完成整个前向运算过程,它默认返回最后一个时间戳上的输出。如果希望返回所有时间戳上的输出列表,可以设置 return_sequences=True 参数,代码如下:
# 创建 RNN 层时,设置返回所有时间戳上的输出
layer = layers.SimpleRNN(64,return_sequences=True)
out = layer(x) # 前向计算
out # 输出,自动进行了 concat 操作
可以看到,返回的输出张量 shape 为[4,80,64],中间维度的 80 即为时间戳维度。同样的,对于多层循环神经网络,可以通过堆叠多个 SimpleRNN 实现,如两层的网络,用法和普通的网络类似。例如:
net = keras.Sequential([ # 构建 2 层 RNN 网络
# 除最末层外,都需要返回所有时间戳的输出,用作下一层的输入
layers.SimpleRNN(64, return_sequences=True),
layers.SimpleRNN(64),
])
out = net(x) # 前向计算
每层都需要上一层在每个时间戳上面的状态输出,因此除了最末层以外,所有的 RNN 层都需要返回每个时间戳上面的状态输出,通过设置 return_sequences=True 来实现。可以看到,使用 SimpleRNN 层,与卷积神经网络的用法类似,非常简洁和高效。
现在利用基础的 RNN 网络来挑战情感分类问题。网络结构如下图:
RNN 网络共两层,循环提取序列信号的语义特征,利用第 2 层 RNN 层的最后时间戳的状态向量 h (2) 作为句子的全局语义特征表示,送入全连接层构成的分类网络 3,得到样本 为积极情感的概率 P(为积极情感|) ∈ [0,1]。
使用经典的 IMDB 影评数据集来完成情感分类任务。IMDB 影评数据集包含了 50000 条用户评价,评价的标签分为消极和积极,其中 IMDB 评级<5 的用户评价标注为0,即消极;IMDB 评价>=7 的用户评价标注为 1,即积极。25000 条影评用于训练集,25,000 条用于测试集。
通过 Keras 提供的数据集 datasets 工具即可加载 IMDB 数据集,代码如下:
batchsz = 128 # 批量大小
total_words = 10000 # 词汇表大小 N_vocab
max_review_len = 80 # 句子最大长度 s,大于的句子部分将截断,小于的将填充
embedding_len = 100 # 词向量特征长度 n
# 加载 IMDB 数据集,此处的数据采用数字编码,一个数字代表一个单词
(x_train, y_train),(x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
# 打印输入的形状,标签的形状
print(x_train.shape, len(x_train[0]), y_train.shape)
print(x_test.shape, len(x_test[0]), y_test.shape)
可以看到,x_train 和 x_test 是长度为 25,000 的一维数组,数组的每个元素是不定长 List,保存了数字编码的每个句子,例如训练集的第一个句子共有 218 个单词,测试集的第一个句子共有 68 个单词,每个句子都包含了句子起始标志 ID。
那么,每个单词是如何编码为数字的呢?我们可以通过查看它的编码表获得编码方案,例如:
# 数字编码表
word_index = keras.datasets.imdb.get_word_index()
# 打印出编码表的单词和对应的数字
for k,v in word_index.items():
print(k,v)
由于编码表的键为单词,值为 ID,这里翻转编码表,并添加标志位的编码 ID,代码如下:
# 前面 4 个 ID 是特殊位
word_index = {k:(v+3) for k,v in word_index.items()}
word_index["" ] = 0 # 填充标志
word_index["" ] = 1 # 起始标志
word_index["" ] = 2 # 未知单词的标志
word_index["" ] = 3
# 翻转编码表
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
# 打印出编码表的单词和对应的数字
for k,v in reverse_word_index.items():
print(k,v)
def decode_review(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
例如转换某个句子,代码如下:
def decode_review(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
decode_review(x_train[1])
对于长度参差不齐的句子,人为设置一个阈值,对大于此长度的句子,选择截断部分单词,可以选择截去句首单词,也可以截去句末单词;对于小于此长度的句子,可以选择在句首或句尾填充,句子截断功能可以通过 keras.preprocessing.sequence.pad_sequences()函数方便实现,例如:
from keras_preprocessing import sequence
# 截断和填充句子,使得等长,此处长句子保留句子后面的部分,短句子在前面填充
x_train = sequence.pad_sequences(x_train,maxlen=max_review_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_review_len)
截断或填充为相同长度后,通过 Dataset 类包裹成数据集对象,并添加常用的数据集处理流程,代码如下:
# 构建数据集,打散,批量,并丢掉最后一个不够 batchsz 的 batch
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_train = db_train.shuffle(1000).batch(batchsz, drop_remainder=True)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsz, drop_remainder=True)
# 统计数据集属性
print('x_train shape:', x_train.shape, tf.reduce_max(y_train),tf.reduce_min(y_train))
print('x_test shape:', x_test.shape)
x_train shape: (25000, 80) tf.Tensor(1, shape=(), dtype=int64) tf.Tensor(0, shape=(), dtype=int64)
x_test shape: (25000, 80)
x_test shape: (25000, 80)
可以看到截断填充后的句子长度统一为 80,即设定的句子长度阈值。drop_remainder=True 参数设置丢弃掉最后一个 Batch,因为其真实的 Batch Size 可能小于预设的 Batch Size。
创建自定义的模型类 MyRNN,继承自 Model 基类,需要新建 Embedding 层,两个 RNN 层,分类网络层,代码如下:
class MyRNN(keras.Model):
# Cell 方式构建多层网络
def __init__(self, units):
super(MyRNN, self).__init__()
# [b, 64],构建 Cell 初始化状态向量,重复使用
self.state0 = [tf.zeros([batchsz, units])]
self.state1 = [tf.zeros([batchsz, units])]
# 词向量编码 [b, 80] => [b, 80, 100]
self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
# 构建 2 个 Cell,使用 dropout 技术防止过拟合
self.rnn_cell0 = layers.SimpleRNNCell(units, dropout=0.5)
self.rnn_cell1 = layers.SimpleRNNCell(units, dropout=0.5)
# 构建分类网络,用于将 CELL 的输出特征进行分类,2 分类
# [b, 80, 100] => [b, 64] => [b, 1]
# 其中词向量编码为长度 = 100,RNN 的状态向量长度 ℎ = units 参数,分类网络完成 2 分类任务,故输出节点设置为 1。
self.outlayer = layers.Dense(1)
# 前向传播:输入序列通过 Embedding 层完成词向量编码,循环通过两个 RNN层,提取语义特征
# 取最后一层的最后时间戳的状态向量输出送入分类网络,经过Sigmoid 激活函数后得到输出概率
def call(self, inputs, training=None):
x = inputs # [b, 80]
# 获取词向量: [b, 80] => [b, 80, 100]
x = self.embedding(x)
# 通过 2 个 RNN CELL,[b, 80, 100] => [b, 64]
state0 = self.state0
state1 = self.state1
for word in tf.unstack(x, axis=1): # word: [b, 100]
out0, state0 = self.rnn_cell0(word, state0, training)
out1, state1 = self.rnn_cell1(out0, state1, training)
# 末层最后一个输出作为分类网络的输入: [b, 64] => [b, 1]
x = self.outlayer(out1, training)
# 通过激活函数,p(y is pos|x)
prob = tf.sigmoid(x)
return prob
为了简便,使用 Keras 的 Compile&Fit 方式训练网络,设置优化器为 Adam 优化器,学习率为 0.001,误差函数选用 2 分类的交叉熵损失函数 BinaryCrossentropy,测试指标采用准确率即可。代码如下:
def main():
units = 64 # RNN 状态向量长度 n
epochs = 20 # 训练 epochs
model = MyRNN(units) # 创建模型
# 装配
model.compile(optimizer = optimizers.Adam(0.001),
loss = losses.BinaryCrossentropy(),
metrics=['accuracy'])
# 训练和验证
model.fit(db_train, epochs=epochs, validation_data=db_test)
# 测试
model.evaluate(db_test)
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Sequential, layers, datasets, optimizers, losses
import numpy as np
# 加载数据
batchsz=128 #批量大小
total_words=10000 #词汇表大小N_vocab
max_review_len=80 #句子最大长度 s,大于的句子部分将截断,小于的将填充
embedding_len=100 #词向量特征长度
# 加载IMDB数据集,此处的数据采用数字编码,一个数字代表一个单词
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
# 截断和填充句子,使得等长,此处长句子保留句子后面的部分,短句子在前面填充
x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len)
# 构建数据集,打散,批量,并丢掉最后一个不够batchsize的batch
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_train = db_train.shuffle(1000).batch(batchsize, drop_remainder=True)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsize, drop_remainder=True)
# 网络模型
class MyRNN(keras.Model):
# Cell方式构建多层网络
def __init__(self, units):
super(MyRNN, self).__init__()
# [b, 64], 构建Cell初始化状态向量,重复使用
self.state0 = [tf.zeros([batchsize, units])]
self.state1 = [tf.zeros([batchsize, units])]
# 词向量编码[b, 80] => [b, 80, 100]
self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
# 构建两个Cell,使用dropout技术防止过拟合
self.rnn_cell0 = layers.SimpleRNNCell(units, dropout=0.5)
self.rnn_cell1 = layers.SimpleRNNCell(units, dropout=0.5)
# 构建分类网络,用于将CELL的输出特征进行分类,2分类
# [b, 80, 100] => [b, 64] => [b, 1]
self.outlayer = Sequential([
layers.Dense(units),
layers.Dropout(rate=0.5),
layers.ReLU(),
layers.Dense(1)])
def call(self, inputs, training=None):
# [b, 80]
x = inputs
# 获取词向量:[b, 80] => [b, 80, 100]
x = self.embedding(x)
# 通过2个RNN CELL,[b, 80, 100] => [b, 64]
state0 = self.state0
state1 = self.state1
# word:[b, 100]
for word in tf.unstack(x, axis=1):
out0, state0 = self.rnn_cell0(word, state0, training)
out1, state1 = self.rnn_cell1(out0, state1, training)
# 末层最后一个输出作为分类网络的输入:[b, 64] => [b, 1]
x = self.outlayer(out1, training)
# 通过激活函数,p(y is pos|x)
prob = tf.sigmoid(x)
return prob
def main():
# RNN 状态向量长度n
units = 64
# 训练世代
epochs = 20
# 创建模型
model = MyRNN(units)
# 装配
model.compile(optimizer=optimizers.Adam(1e-3),
loss=losses.BinaryCrossentropy(),
metrics=['accuracy'],
experimental_run_tf_function=False)
# 训练和验证
model.fit(db_train, epochs=epochs, validation_data=db_test)
# 测试
model.evaluate(db_test)
if __name__ == '__main__':
main()