使用循环神经网络对序列数据进行建模
1. 情感分析任务的RNN模型构建
1.1 双向LSTM模型
由于数据集中的序列较长,我们使用LSTM层来处理长期依赖,并将其放在双向包装器中,使循环层能够从两个方向处理输入序列。以下是构建双向LSTM模型的代码:
import tensorflow as tf
embedding_dim = 20
vocab_size = len(token_counts) + 2
tf.random.set_seed(1)
# 构建模型
bi_lstm_model = tf.keras.Sequential([
tf.keras.layers.Embedding(
input_dim=vocab_size,
output_dim=embedding_dim,
name='embed-layer'),
tf.keras.layers.Bidirectional(
tf.keras.layers.LSTM(64, name='lstm-layer'),
name='bidir-lstm'),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
bi_lstm_model.summary()
# 编译和训练
bi_lstm_model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),
metrics=['accuracy'])
history = bi_lstm_model.fit(
train_data,
validation_data=valid_data,
epochs=10)
# 在测试数据上评估
test_results = bi_lstm_model.evaluate(test_data)
print('Test Acc.: {:.2f}%'.format(test_results[1]*100))
经过10个epoch的训练,该模型在测试数据上的准确率达到了85.15%。
1.2 简单RNN模型的问题
我们也可以尝试使用SimpleRNN层,但对于这个数据集,使用普通循环层构建的模型无法达到良好的预测性能,即使在训练数据上也是如此。例如,将双向LSTM层替换为单向SimpleRNN层并在完整长度的序列上训练模型时,训练过程中损失甚至不会下降。这是因为数据集中的序列太长,SimpleRNN层无法学习长期依赖,可能会遇到梯度消失或爆炸的问题。
1.3 双向RNN的更多信息
双向包装器会对每个输入序列进行两次遍历:前向遍历和反向遍历(注意这与反向传播中的前向和反向传播不同)。默认情况下,这两次遍历的结果会被连接起来。如果需要更改此行为,可以将 merge_mode 参数设置为 'sum' (求和)、 'mul' (相乘)、 'ave' (求平均)、 'concat' (默认)或 None (返回一个包含两个张量的列表)。更多信息可查看官方文档: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/Bidirectional 。
1.4 截断序列以使用SimpleRNN
为了在使用SimpleRNN时获得合理的预测性能,我们可以截断序列。根据领域知识,电影评论的最后段落可能包含大部分情感信息,因此我们可以只关注每个评论的最后部分。为此,我们定义了一个辅助函数 preprocess_datasets() 来组合预处理步骤2 - 4,该函数的可选参数 max_seq_length 决定了每个评论应使用的标记数量。以下是该函数的代码:
from collections import Counter
def preprocess_datasets(
ds_raw_train,
ds_raw_valid,
ds_raw_test,
max_seq_length=None,
batch_size=32):
# (步骤1已经完成)
# 步骤2: 查找唯一标记
tokenizer = tfds.features.text.Tokenizer()
token_counts = Counter()
for example in ds_raw_train:
tokens = tokenizer.tokenize(example[0].numpy()[0])
if max_seq_length is not None:
tokens = tokens[-max_seq_length:]
token_counts.update(tokens)
print('Vocab-size:', len(token_counts))
# 步骤3: 编码文本
encoder = tfds.features.text.TokenTextEncoder(
token_counts)
def encode(text_tensor, label):
text = text_tensor.numpy()[0]
encoded_text = encoder.encode(text)
if max_seq_length is not None:
encoded_text = encoded_text[-max_seq_length:]
return encoded_text, label
def encode_map_fn(text, label):
return tf.py_function(encode, inp=[text, label],
Tout=(tf.int64, tf.int64))
ds_train = ds_raw_train.map(encode_map_fn)
ds_valid = ds_raw_valid.map(encode_map_fn)
ds_test = ds_raw_test.map(encode_map_fn)
# 步骤4: 批量处理数据集
train_data = ds_train.padded_batch(
batch_size, padded_shapes=([-1],[]))
valid_data = ds_valid.padded_batch(
batch_size, padded_shapes=([-1],[]))
test_data = ds_test.padded_batch(
batch_size, padded_shapes=([-1],[]))
return (train_data, valid_data,
test_data, len(token_counts))
另外,我们还定义了一个辅助函数 build_rnn_model() ,用于更方便地构建不同架构的模型:
from tensorflow.keras.layers import Embedding
from tensorflow.keras.layers import Bidirectional
from tensorflow.keras.layers import SimpleRNN
from tensorflow.keras.layers import LSTM
from tensorflow.keras.layers import GRU
def build_rnn_model(embedding_dim, vocab_size,
recurrent_type='SimpleRNN',
n_recurrent_units=64,
n_recurrent_layers=1,
bidirectional=True):
tf.random.set_seed(1)
# 构建模型
model = tf.keras.Sequential()
model.add(
Embedding(
input_dim=vocab_size,
output_dim=embedding_dim,
name='embed-layer')
)
for i in range(n_recurrent_layers):
return_sequences = (i < n_recurrent_layers-1)
if recurrent_type == 'SimpleRNN':
recurrent_layer = SimpleRNN(
units=n_recurrent_units,
return_sequences=return_sequences,
name='simprnn-layer-{}'.format(i))
elif recurrent_type == 'LSTM':
recurrent_layer = LSTM(
units=n_recurrent_units,
return_sequences=return_sequences,
name='lstm-layer-{}'.format(i))
elif recurrent_type == 'GRU':
recurrent_layer = GRU(
units=n_recurrent_units,
return_sequences=return_sequences,
name='gru-layer-{}'.format(i))
if bidirectional:
recurrent_layer = Bidirectional(
recurrent_layer, name='bidir-' +
recurrent_layer.name)
model.add(recurrent_layer)
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
return model
以下是使用这两个辅助函数构建并训练一个截断序列为100个标记的双向SimpleRNN模型的示例:
batch_size = 32
embedding_dim = 20
max_seq_length = 100
train_data, valid_data, test_data, n = preprocess_datasets(
ds_raw_train, ds_raw_valid, ds_raw_test,
max_seq_length=max_seq_length,
batch_size=batch_size
)
vocab_size = n + 2
rnn_model = build_rnn_model(
embedding_dim, vocab_size,
recurrent_type='SimpleRNN',
n_recurrent_units=64,
n_recurrent_layers=1,
bidirectional=True)
rnn_model.summary()
rnn_model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss=tf.keras.losses.BinaryCrossentropy(
from_logits=False), metrics=['accuracy'])
history = rnn_model.fit(
train_data,
validation_data=valid_data,
epochs=10)
results = rnn_model.evaluate(test_data)
print('Test Acc.: {:.2f}%'.format(results[1]*100))
截断序列为100个标记并使用双向SimpleRNN层的模型在测试数据上的分类准确率达到了80.70%。虽然与之前的双向LSTM模型相比预测准确率略有下降,但在截断序列上的性能比在完整长度电影评论上使用SimpleRNN要好得多。
1.5 情感分析模型构建流程
graph LR
A[准备数据] --> B[构建模型]
B --> C[编译模型]
C --> D[训练模型]
D --> E[评估模型]
2. TensorFlow中的字符级语言建模
2.1 语言建模概述
语言建模是一个有趣的应用,它使机器能够执行与人类语言相关的任务,如生成英语句子。我们要构建的模型的输入是一个文本文档,目标是开发一个能够生成与输入文档风格相似的新文本的模型。在字符级语言建模中,输入被分解为字符序列,逐个字符输入到网络中,网络结合之前看到的字符的记忆来预测下一个字符。
2.2 数据集预处理
2.2.1 下载数据
可以从Project Gutenberg网站( https://www.gutenberg.org/ )下载免费电子书作为输入数据。例如,下载儒勒·凡尔纳的《神秘岛》( http://www.gutenberg.org/files/1268/1268-0.txt )。如果使用macOS或Linux操作系统,可以在终端中使用以下命令下载文件:
curl -O http://www.gutenberg.org/files/1268/1268-0.txt
2.2.2 读取和处理文本
import numpy as np
# 读取和处理文本
with open('1268-0.txt', 'r') as fp:
text = fp.read()
start_indx = text.find('THE MYSTERIOUS ISLAND')
end_indx = text.find('End of the Project Gutenberg')
text = text[start_indx:end_indx]
char_set = set(text)
print('Total Length:', len(text))
print('Unique Characters:', len(char_set))
2.2.3 字符编码
chars_sorted = sorted(char_set)
char2int = {ch:i for i,ch in enumerate(chars_sorted)}
char_array = np.array(chars_sorted)
text_encoded = np.array(
[char2int[ch] for ch in text],
dtype=np.int32)
print('Text encoded shape:', text_encoded.shape)
print(text[:15], '== Encoding ==>', text_encoded[:15])
print(text_encoded[15:21], '== Reverse ==>',
''.join(char_array[text_encoded[15:21]]))
2.2.4 创建TensorFlow数据集
import tensorflow as tf
ds_text_encoded = tf.data.Dataset.from_tensor_slices(
text_encoded)
for ex in ds_text_encoded.take(5):
print('{} -> {}'.format(ex.numpy(), char_array[ex.numpy()]))
2.2.5 构建输入和目标序列
seq_length = 40
chunk_size = seq_length + 1
ds_chunks = ds_text_encoded.batch(chunk_size,
drop_remainder=True)
# 定义分割输入和目标的函数
def split_input_target(chunk):
input_seq = chunk[:-1]
target_seq = chunk[1:]
return input_seq, target_seq
ds_sequences = ds_chunks.map(split_input_target)
for example in ds_sequences.take(2):
print(' Input (x): ',
repr(''.join(char_array[example[0].numpy()])))
print('Target (y): ',
repr(''.join(char_array[example[1].numpy()])))
print()
2.2.6 批量处理数据集
BATCH_SIZE = 64
BUFFER_SIZE = 10000
ds = ds_sequences.shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
2.3 构建字符级RNN模型
def build_model(vocab_size, embedding_dim, rnn_units):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim),
tf.keras.layers.LSTM(
rnn_units,
return_sequences=True),
tf.keras.layers.Dense(vocab_size)
])
return model
# 设置训练参数
charset_size = len(char_array)
embedding_dim = 256
rnn_units = 512
tf.random.set_seed(1)
model = build_model(
vocab_size=charset_size,
embedding_dim=embedding_dim,
rnn_units=rnn_units)
model.summary()
model.compile(
optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True
))
model.fit(ds, epochs=20)
2.4 字符级语言建模流程
graph LR
A[下载数据] --> B[处理文本]
B --> C[字符编码]
C --> D[创建数据集]
D --> E[构建输入和目标序列]
E --> F[批量处理数据集]
F --> G[构建模型]
G --> H[训练模型]
2.5 数据集预处理步骤总结
| 步骤 | 描述 |
|---|---|
| 1 | 下载数据 |
| 2 | 读取和处理文本,去除不必要的部分 |
| 3 | 查找唯一字符并创建字符到整数的映射 |
| 4 | 将文本编码为整数序列 |
| 5 | 创建TensorFlow数据集 |
| 6 | 分割序列为输入和目标序列 |
| 7 | 批量处理数据集 |
2.6 模型结构分析
在构建的字符级 RNN 模型中,各层的作用和特点如下:
- Embedding 层 :将输入的字符索引转换为固定长度的密集向量表示,有助于模型学习字符之间的语义关系。其输入维度为字符集的大小,输出维度为设定的嵌入维度(这里是 256)。
- LSTM 层 :长短期记忆网络层,能够处理序列数据中的长期依赖关系。设置 return_sequences=True 会返回每个时间步的输出,适用于生成任务。该层有 512 个单元。
- Dense 层 :全连接层,输出维度为字符集的大小,用于对每个字符进行分类预测。
2.7 训练效果分析
在训练过程中,模型的损失逐渐下降,这表明模型在不断学习数据中的模式。例如,在第一个 epoch 时,损失为 2.3437,到第 20 个 epoch 时,损失显著下降。不过,损失的下降速度会逐渐变缓,这是因为模型逐渐接近最优解。
2.8 超参数调整建议
- 序列长度 :序列长度是一个重要的超参数。较长的序列可以包含更多的上下文信息,有助于生成更有意义的句子,但也会增加模型的训练难度和计算量,并且可能导致梯度消失或爆炸问题。较短的序列可能使模型更关注单个单词的生成,而忽略上下文。这里选择 40 是一个折中的选择,实际应用中需要根据具体情况进行调整。
- 嵌入维度 :嵌入维度决定了字符向量的表示长度。较大的嵌入维度可以捕捉更多的语义信息,但也会增加模型的参数数量和计算复杂度。较小的嵌入维度则可能无法充分表示字符之间的关系。可以尝试不同的嵌入维度,如 128、256、512 等,观察模型的性能变化。
- LSTM 单元数量 :LSTM 单元数量影响模型的记忆能力和表达能力。更多的单元可以学习更复杂的模式,但也容易过拟合。可以通过交叉验证等方法选择合适的单元数量。
3. 不同 RNN 模型对比
3.1 性能对比
| 模型类型 | 序列处理方式 | 准确率 | 特点 |
|---|---|---|---|
| 双向 LSTM | 处理完整长度序列 | 85.15% | 能够处理长期依赖关系,适合处理较长的序列,但计算复杂度较高 |
| 双向 SimpleRNN(截断序列) | 截断序列为 100 个标记 | 80.70% | 计算简单,但对于长序列处理能力有限,截断序列后性能有所提升 |
3.2 适用场景
- 双向 LSTM :当数据集中的序列较长且需要捕捉长期依赖关系时,双向 LSTM 是一个较好的选择。例如在情感分析任务中,如果评论内容较长,双向 LSTM 可以更好地理解整个评论的情感倾向。
- 双向 SimpleRNN(截断序列) :对于计算资源有限或者数据序列过长导致 SimpleRNN 无法正常学习的情况,可以考虑截断序列并使用双向 SimpleRNN。这种方法可以在一定程度上提高模型的性能,同时降低计算复杂度。
3.3 选择建议
在实际应用中,需要根据数据集的特点、计算资源和任务要求来选择合适的 RNN 模型。如果数据序列较短,SimpleRNN 可能就足够了;如果序列较长且对性能要求较高,双向 LSTM 可能更合适;如果计算资源有限,可以尝试截断序列并使用双向 SimpleRNN。
4. 总结与展望
4.1 总结
本文介绍了使用循环神经网络对序列数据进行建模的两种应用场景:情感分析和字符级语言建模。在情感分析任务中,我们对比了双向 LSTM 和双向 SimpleRNN 模型的性能,并通过截断序列的方法提高了 SimpleRNN 模型的性能。在字符级语言建模中,详细介绍了数据集预处理、模型构建和训练的过程。
4.2 展望
- 模型优化 :可以尝试使用更复杂的模型结构,如 GRU 等,或者结合注意力机制,进一步提高模型的性能。
- 应用拓展 :将这些模型应用到更多的自然语言处理任务中,如文本分类、机器翻译等。
- 超参数优化 :使用更先进的超参数优化算法,如贝叶斯优化、遗传算法等,自动寻找最优的超参数组合。
4.3 操作建议总结
如果想要使用这些技术进行实践,可以按照以下步骤进行:
1. 情感分析 :
- 准备数据集,进行必要的预处理。
- 根据数据集的特点选择合适的模型(双向 LSTM 或双向 SimpleRNN)。
- 构建模型并编译,设置合适的优化器和损失函数。
- 训练模型并在验证集上进行评估,调整超参数。
- 在测试集上评估最终的模型性能。
2. 字符级语言建模 :
- 下载合适的数据集,如 Project Gutenberg 上的电子书。
- 对文本进行处理,包括去除不必要的部分、字符编码等。
- 构建数据集,包括分割序列为输入和目标序列、批量处理等。
- 构建字符级 RNN 模型,设置合适的超参数。
- 训练模型并观察损失的变化,根据需要调整超参数。
graph LR
A[选择应用场景] --> B{情感分析}
A --> C{字符级语言建模}
B --> D[准备数据]
B --> E[选择模型]
B --> F[构建和编译模型]
B --> G[训练和评估模型]
C --> H[下载数据]
C --> I[处理文本]
C --> J[构建数据集]
C --> K[构建模型]
C --> L[训练模型]
通过以上的介绍和分析,希望读者能够更好地理解循环神经网络在序列数据建模中的应用,并能够根据实际情况选择合适的模型和方法。
超级会员免费看
9422

被折叠的 条评论
为什么被折叠?



