1. 什么是RNN
Recurrent Neural Network(循环神经网络)是一种特殊的神经网络,与前馈神经网络或卷积神经网络不同的是它含有一个或多个循环层。因此它可以处理序列数据,例如文本、语音、时间序列等。RNN在处理时,在每个时间步都会收到一个输入和一个隐藏状态,输出与隐藏状态有关。隐藏状态除了作为输出,还会在下一个时间步作为输入传入下一层RNN,这样就实现了信息的“循环”,在处理序列数据时表现出很好的优势。
1.1 RNN结构
RNN结构可以用下面的图示表示:
其中,$x_t$是当前时间步的输入,$h_t$是当前时间步的隐藏状态,同时也是下一个时间步的输入。经过变换得到的向量$y_t$是当前时间步的输出。权重$W_{hh}$、$W_{hx}$、$W_{yh}$和偏置$b_h$、$b_y$是模型参数。
1.2 RNN原理
在一个简单的RNN中,隐藏状态$h_t$的计算方式为:
$$h_t=tanh(W_{hh}h_{t-1}+W_{hx}x_t+b_h)$$
其中$tanh$是双曲正切函数,将输入转换为[-1,1]之间的值,将非线性映射到了一条s曲线上。我们可以通过这种方式增加模型的拟合能力。若我们在最后一层加一个线性变换,就可以将输出映射到我们需要的维度上。
$$y_t=W_{yh}h_{t}+b_y$$
2. 用TensorFlow2实现RNN
接下来,我们就来看看如何用TensorFlow2实现一个简单的RNN,用于生成文本。我们先来看看模型的基本架构:
model = keras.Sequential([
keras.layers.SimpleRNN(units=64, input_shape=[None, char_size],
activation='tanh'),
keras.layers.Dense(char_size, activation='softmax')
])
这里我们使用了一个SimpleRNN层,将一个隐藏状态$h_t$在每个时间步传递给下一个时间步。我们使用Softmax作为输出层的激活函数,并且将模型的输入形状设置成[input_length,char_size],其中input_length是需要生成的文本的长度。
3. 文本预处理和处理数据集
在训练模型之前,我们需要将文本进行预处理并处理成能够用于训练的数据集。
3.1 文本预处理
在我们的文本预处理中,我们需要做以下几个步骤:
将所有大写字母转换为小写字母
删除所有非字母字符(包括标点符号和数字)
将所有行进行合并
将文本转换为字符集
def preprocess(text):
text = text.lower()
text = re.sub(r'[^a-z]', ' ', text)
text = ' '.join(text.split())
chars = set(text)
return text, chars
text, chars = preprocess(text)
char_size = len(chars)
# 生成文本和字符集的映射
char2idx = {char: idx for idx, char in enumerate(chars)}
idx2char = np.array(list(chars))
3.2 处理数据集
我们需要创建一个数据集,它包含了所有可用于学习的训练序列。我们将文本划分成N个序列,每个序列的长度为input_length+1,即模型的输入和输出都是一个字符偏移量大小的滑动窗口。这意味着,模型可以在窗口中提取序列,并预测下一个字符。
def prepare_data(text, input_length):
inputs = []
targets = []
for i in range(len(text) - input_length):
inputs.append(text[i:i+input_length])
targets.append(text[i+input_length])
inputs_onehot = np.array([text2onehot(seq) for seq in inputs], np.float32)
targets_idx = np.array([char2idx[char] for char in targets], np.int32)
dataset = tf.data.Dataset.from_tensor_slices((inputs_onehot, targets_idx))
dataset = dataset.shuffle(1000).batch(batch_size).repeat()
return dataset
def text2onehot(text):
onehot = np.zeros((len(text), char_size))
for idx, char in enumerate(text):
onehot[idx, char2idx[char]] = 1.0
return onehot
input_length = 100
batch_size = 128
dataset = prepare_data(text, input_length)
4. 训练模型并生成文本
现在可以开始训练模型并使用该模型生成新文本了。我们将使用负对数似然来作为损失函数(加权交叉熵会更好,但是我们以这种方式进行的简单RNN可能没有足够的表达能力),并且使用优化器Adam。
# 编译模型
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
# 训练模型
steps_per_epoch = len(text) // input_length // batch_size
history = model.fit(dataset, steps_per_epoch=steps_per_epoch, epochs=epochs)
# 使用模型生成文本
def generate_text(start_string, temperature=1.0):
generated = start_string
for i in range(num_generate):
# 将起始字符串编码为一个onehot向量
input_eval = [char2idx[char] for char in generated]
input_eval = np.expand_dims(text2onehot(generated)[-1], axis=0)
# 模型预测一下一个字符的概率分布
predictions = model.predict(input_eval)
predictions = predictions.squeeze().astype('float64')
# 使用温度调整给预测概率分布赋值
predictions = np.log(predictions) / temperature
predictions = np.exp(predictions) / np.sum(np.exp(predictions))
# 选择预测概率最高的字符
predicted_id = np.random.choice(len(predictions), p=predictions)
# 最后将该字符添加到生成文本中
generated += idx2char[predicted_id]
return generated
4.1 生成文本的相关参数
在生成新文本之前,可以先设置一些相关参数。
num_generate:生成文本的长度
start_string:可以任意设置一个起始字符串
temperature(温度):用于调整下一次字符的采样
num_generate = 1000
start_string = 'hello'
temperature = 0.6
generated_text = generate_text(start_string, temperature)
print(generated_text)
5. 总结
在本文中,我们介绍了什么是RNN并且使用TensorFlow2代码实现了一个简单的RNN,用于生成文本。我们通过设置预训练文本、文本预处理、数据集处理、训练模型和生成新文本等多个步骤讲解了模型的训练和使用方法。
正如您所看到的,训练模型和使用模型生成文本并不难。通过调整模型结构和训练参数,我们可以获得更好的效果,例如增加训练数据、增加训练轮数、增加训练样本等。