文章目录
在 Tensorflow2.0之从零开始实现循环神经网络中,我们介绍了循环神经网络的原理以及如何使用低级API来实现循环神经网络,在这篇文章中,我们将使用 tensorflow2.0 来更简洁地实现循环神经网络。
在这里,我们将实现一个循环神经网络来自动生成周杰伦歌词。代码中的数据集加载以及采样函数在 Tensorflow2.0之语言模型数据集(周杰伦专辑歌词)预处理中已经介绍过了,在这里不再赘述。
1、导入需要的库
import tensorflow as tf
from tensorflow import keras
import numpy as np
import zipfile
import math
2、加载数据集
def load_data_jay_lyrics():
"""加载周杰伦歌词数据集"""
with zipfile.ZipFile('./jaychou_lyrics.txt.zip') as zin:
with zin.open('jaychou_lyrics.txt') as f:
corpus_chars = f.read().decode('utf-8')
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
corpus_chars = corpus_chars[0:10000]
idx_to_char = list(set(corpus_chars))
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
vocab_size = len(char_to_idx)
corpus_indices = [char_to_idx[char] for char in corpus_chars]
return corpus_indices, char_to_idx, idx_to_char, vocab_size
(corpus_indices, char_to_idx, idx_to_char, vocab_size) = load_data_jay_lyrics()
3、相邻采样
def data_iter_consecutive(corpus_indices, batch_size, num_steps, ctx=None):
corpus_indices = np.array(corpus_indices)
data_len = len(corpus_indices)
batch_len = data_len // batch_size
indices = corpus_indices[0: batch_size*batch_len].reshape((
batch_size, batch_len))
epoch_size = (batch_len - 1) // num_steps
for i in range(epoch_size):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
4、定义模型
4.1 定义循环神经网络层
Keras 的 Rnn 模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层 rnn_layer,并对权重做初始化。
num_hiddens = 256
cell = keras.layers.SimpleRNNCell(num_hiddens,
kernel_initializer='glorot_uniform')
rnn_layer = keras.layers.RNN(cell,time_major=True,
return_sequences=True,return_state=True)
其中,rnn_layer 的输入形状为 (时间步数, 批量大小, 词典大小),在前向计算后会分别返回输出和隐藏状态。
其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输入。需要强调的是,该“输出”本身并不涉及输出层计算,形状为 (时间步数, 批量大小, 隐藏单元个数)。
返回的隐藏状态指的是隐藏层在最后时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中。
举例来说:
我们要先初始化隐藏状态
batch_size = 2
state = rnn_layer.cell.get_initial_state(batch_size=batch_size,dtype=tf.float32)
state.shape
TensorShape([2, 256])
可见隐藏状态的形状为 (批量大小, 隐藏单元个数)。
num_steps = 35
X = tf.random.uniform(shape=(num_steps, batch_size, vocab_size))
print(X.shape)
Y, state_new = rnn_layer(X, state)
print(Y.shape)
print(len(state_new))
print(state_new[0].shape)
(35, 2, 1027)
(35, 2, 256)
2
(256,)
4.2 定义循环神经网络
class RNNModel(tf.keras.Model):
def __init__(self, rnn_layer, vocab_size):
super().__init__()
self.rnn = rnn_layer
self.vocab_size = vocab_size
self.dense = keras.layers.Dense(vocab_size)
def call(self, inputs, state):
# 将输入转置成(num_steps, batch_size)后获取one-hot向量表示
X = tf.one_hot(tf.transpose(inputs), self.vocab_size)
Y, state = self.rnn(X, state)
# 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens),
# 它的输出形状为(num_steps * batch_size, vocab_size)
output = self.dense(tf.reshape(Y, (-1, Y.shape[-1])))
return output, state
def get_initial_state(self, *args, **kwargs):
return self.rnn.cell.get_initial_state(*args, **kwargs)
model = RNNModel(rnn_layer, vocab_size)
将经过采样后的样本直接输入此网络中时,它经过的处理操作依次为:
- 输入形状为 (批量大小, 时间步数)
- 将输入转置成 (时间步数, 批量大小)
- 利用独热编码得到 (时间步数, 批量大小, 词典大小)
- 输入到 rnn 层,得到 (时间步数, 批量大小, 隐藏单元个数)
- reshape 成 (时间步数x批量大小, 隐藏单元个数)
- 经过 Dense 层,得到 (时间步数x批量大小, 词典大小)
5、定义预测函数
def predict_rnn_keras(prefix, num_chars):
# 使用model的成员函数来初始化隐藏状态
state = model.get_initial_state(batch_size=1,dtype=tf.float32)
output = [char_to_idx[prefix[0]]]
for t in range(num_chars + len(prefix) - 1):
X = np.array([output[-1]]).reshape((1, 1))
Y, state = model(X, state) # 前向计算不需要传入模型参数
if t < len(prefix) - 1:
output.append(char_to_idx[prefix[t + 1]])
else:
output.append(int(np.array(tf.argmax(Y,axis=-1))))
return ''.join([idx_to_char[i] for i in output])
其中,num_chars 表示要预测多少个字符。
我们可以先试验一下:
predict_rnn_keras('分开', 10)
得到:
扫描二维码关注公众号,回复:
10045363 查看本文章
'分开缸池升脑霜陪身纳梦旋'
因为模型参数为随机值,所以预测结果也是随机的。
6、裁剪梯度
循环神经网络中较容易出现梯度衰减或梯度爆炸。为了应对梯度爆炸,我们可以裁剪梯度(clip gradient)。假设我们把所有模型参数梯度的元素拼接成一个向量 ,并设裁剪的阈值是 。裁剪后的梯度
的 范数不超过 。
# 计算裁剪后的梯度
def grad_clipping(grads,theta):
norm = np.array([0])
for i in range(len(grads)):
norm+=tf.math.reduce_sum(grads[i] ** 2)
norm = np.sqrt(norm).item()
new_gradient=[]
if norm > theta:
for grad in grads:
new_gradient.append(grad * theta / norm)
else:
for grad in grads:
new_gradient.append(grad)
return new_gradient
7、定义模型训练函数
7.1 困惑度
我们通常使用困惑度(perplexity)来评价语言模型的好坏。困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,
- 最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
- 最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
- 基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
显然,任何一个有效模型的困惑度必须小于类别个数。在本例中,困惑度必须小于词典大小vocab_size。
7.2 初始化优化器
lr = 1e2
optimizer=tf.keras.optimizers.SGD(learning_rate=lr)
7.3 定义梯度下降函数
def train_step(X, state, Y, clipping_theta=1e-2):
with tf.GradientTape(persistent=True) as tape:
(outputs, state) = model(X, state)
y = Y.T.reshape((-1,))
l = loss_object(y,outputs)
grads = tape.gradient(l, model.variables)
# 梯度裁剪
grads=grad_clipping(grads, clipping_theta)
optimizer.apply_gradients(zip(grads, model.variables)) # 因为已经误差取过均值,梯度不用再做平均
return l, y
7.4 定义训练函数
- num_epochs:训练次数;
- batch_size:批次大小;
- pred_period:间隔多少次展示一次结果;
- pred_len:要求预测的字符长度。
def train_and_predict_rnn_keras(num_epochs, batch_size, pred_period, pred_len, prefixes):
for epoch in range(num_epochs):
l_sum, n = 0.0, 0
data_iter = data_iter_consecutive(
corpus_indices, batch_size, num_steps)
state = model.get_initial_state(batch_size=batch_size,dtype=tf.float32)
for X, Y in data_iter:
l, y = train_step(X, state, Y)
l_sum += np.array(l).item() * len(y)
n += len(y)
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f' % (
epoch + 1, math.exp(l_sum / n)))
for prefix in prefixes:
print(' -', predict_rnn_keras(prefix, pred_len))
7.5 训练
num_epochs, batch_size = 250, 32
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
train_and_predict_rnn_keras(num_epochs, batch_size, pred_period,
pred_len, prefixes)