44、利用循环神经网络对序列数据进行建模

利用循环神经网络对序列数据进行建模

1. 情感分析任务的RNN模型构建

在处理情感分析任务时,由于序列较长,我们会使用LSTM层来处理长期依赖效应。同时,将LSTM层置于双向包装器中,使循环层能从输入序列的起始到结束以及反向两个方向进行处理。以下是构建双向LSTM模型的代码:

import tensorflow as tf
from collections import Counter

# 定义参数
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%。不过,这一结果与IMDb数据集上的先进方法相比并非最佳,这里只是为了展示RNN的工作原理。

我们也可以尝试其他类型的循环层,如SimpleRNN。但事实证明,使用常规循环层构建的模型无法达到良好的预测性能(即使在训练数据上也是如此)。例如,用单向SimpleRNN层替换上述代码中的双向LSTM层,并在全长序列上训练模型,会发现训练过程中损失甚至不会下降。这是因为该数据集中的序列太长,带有SimpleRNN层的模型无法学习长期依赖关系,还可能会遇到梯度消失或爆炸的问题。

2. 双向RNN的更多信息

双向包装器会对每个输入序列进行两次处理:一次正向处理和一次反向处理(注意,这与反向传播中的正向和反向传播不同)。默认情况下,正向和反向处理的结果会被拼接。如果想改变这种行为,可以将参数 merge_mode 设置为 'sum' (求和)、 'mul' (将两次处理的结果相乘)、 'ave' (取两者的平均值)、 'concat' (默认值)或 None (返回一个包含两个张量的列表)。更多关于双向包装器的信息,可查看官方文档:https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/Bidirectional 。

3. 截断序列以提升SimpleRNN性能

为了让SimpleRNN在该数据集上获得合理的预测性能,我们可以截断序列。利用领域知识,我们推测电影评论的最后段落可能包含了大部分情感信息,因此可以只关注每个评论的最后部分。为此,我们定义了一个辅助函数 preprocess_datasets() 来组合预处理步骤2 - 4。该函数的一个可选参数 max_seq_length 决定了每个评论应使用的标记数量。例如,如果将 max_seq_length 设置为100,而某个评论的标记数超过100,则只使用最后100个标记。如果将 max_seq_length 设置为 None ,则会像之前一样使用全长序列。尝试不同的 max_seq_length 值,能让我们更深入了解不同RNN模型处理长序列的能力。

from collections import Counter

def preprocess_datasets(
    ds_raw_train,
    ds_raw_valid,
    ds_raw_test,
    max_seq_length=None,
    batch_size=32):

    # 步骤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

以下是使用这两个辅助函数,尝试一个具有单个SimpleRNN层的模型,并将序列截断为最大长度为100个标记的示例代码:

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模型(测试数据集上的准确率为85.15%)相比,预测准确率略有下降,但在这些截断序列上的性能比在全长电影评论上使用SimpleRNN要好得多。

4. 字符级语言建模流程

字符级语言建模的实现可分为三个步骤,具体流程如下:

graph LR
    A[准备数据] --> B[构建RNN模型]
    B --> C[进行下一个字符预测和采样以生成新文本]
5. 数据集预处理

要获取输入数据,可访问古登堡计划网站(https://www.gutenberg.org/ ),该网站提供数千本免费电子书。以《神秘岛》(The Mysterious Island)为例,可从http://www.gutenberg.org/files/1268/1268 - 0.txt 下载纯文本格式的书籍。如果使用macOS或Linux操作系统,可在终端使用以下命令下载文件:

curl -O http://www.gutenberg.org/files/1268/1268-0.txt

下载数据集后,可将其作为纯文本读取到Python会话中。以下代码用于直接从下载的文件中读取文本,并去除开头和结尾的部分内容(这些内容包含古登堡计划的相关描述),然后创建一个Python变量 char_set ,表示文本中观察到的唯一字符集:

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))

由于大多数神经网络库和RNN实现无法处理字符串格式的输入数据,因此需要将文本转换为数字格式。我们创建一个简单的Python字典 char2int ,将每个字符映射到一个整数,同时还需要一个反向映射,将模型的输出结果转换回文本。使用NumPy数组并通过索引将索引映射到那些唯一字符会更高效。

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]]))

接下来,我们从这个NumPy数组创建一个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()]))

为了实现文本生成任务,我们将序列长度裁剪为40。输入张量 x 由40个标记组成,输入 x 和目标 y 会偏移一个字符。因此,我们将文本分割成大小为41的块,前40个字符构成输入序列 x ,后40个元素构成目标序列 y

seq_length = 40
chunk_size = seq_length + 1
ds_chunks = ds_text_encoded.batch(chunk_size, 
                                  drop_remainder=True)

# 定义分割x和y的函数
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()

最后,将数据集划分为小批量:

BATCH_SIZE = 64
BUFFER_SIZE = 10000
ds = ds_sequences.shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

利用循环神经网络对序列数据进行建模

6. 构建字符级RNN模型

数据集准备好后,构建模型相对简单。为了提高代码的可重用性,我们编写一个函数 build_model ,使用Keras的Sequential类定义一个RNN模型,然后指定训练参数并调用该函数来获得一个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()

以下是模型的概要信息:
| 层类型 | 输出形状 | 参数数量 |
| ---- | ---- | ---- |
| Embedding | (None, None, 256) | 20480 |
| LSTM | (None, None, 512) | 1574912 |
| Dense | (None, None, 80) | 41040 |
| 总计 | - | 1636432 |

该模型中的LSTM层输出形状为 (None, None, 512) ,这意味着LSTM的输出是一个三维张量。第一个维度表示批次数量,第二个维度表示输出序列的长度,最后一个维度对应隐藏单元的数量。之所以LSTM层有三维输出,是因为我们在定义LSTM层时指定了 return_sequences=True 。全连接层(Dense)接收LSTM单元的输出,并为输出序列的每个元素计算对数几率。因此,模型的最终输出也是一个三维张量。

此外,我们为最终的全连接层指定了 activation=None ,这是因为我们需要模型的输出为对数几率,以便从模型预测中进行采样来生成新文本。接下来进行模型训练:

model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(
        from_logits=True
    ))
model.fit(ds, epochs=20)
7. 总结与不同模型对比

下面通过表格对比不同模型在情感分析和字符级语言建模任务中的表现:
| 模型类型 | 任务 | 处理方式 | 性能表现 | 问题 |
| ---- | ---- | ---- | ---- | ---- |
| 双向LSTM | 情感分析 | 对输入序列进行双向处理,结合LSTM处理长期依赖 | 测试准确率85.15% | - |
| 单向SimpleRNN | 情感分析 | 对输入序列进行单向处理 | 训练时损失不下降,无法学习长期依赖 | 梯度消失或爆炸 |
| 双向SimpleRNN(截断序列) | 情感分析 | 截断序列后进行双向处理 | 测试准确率80.70% | 预测准确率略低于双向LSTM |
| 字符级LSTM | 字符级语言建模 | 对字符序列进行处理,预测下一个字符 | 通过训练学习文本模式生成新文本 | - |

在实际应用中,我们可以根据具体任务和数据特点选择合适的模型。如果数据序列较长且需要捕捉长期依赖关系,双向LSTM是一个不错的选择;如果想要尝试更简单的模型,可以考虑截断序列后使用双向SimpleRNN。对于字符级语言建模任务,字符级LSTM能够较好地学习文本的模式并生成新文本。

整个循环神经网络处理序列数据的流程可以用以下mermaid流程图表示:

graph LR
    A[数据获取] --> B[数据预处理]
    B --> C{选择模型类型}
    C -->|情感分析| D[构建RNN模型(如双向LSTM等)]
    C -->|字符级语言建模| E[构建字符级RNN模型]
    D --> F[模型训练]
    E --> F
    F --> G[模型评估]
    G --> H[应用与生成新数据]

通过以上的分析和实践,我们可以更深入地理解循环神经网络在处理序列数据方面的原理和应用,并且能够根据不同的任务需求选择合适的模型和处理方法,以达到更好的性能和效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值