作者: Abheesht Sharma
创建日期: 2022/05/26
最后修改: 2024/04/30
描述: 使用KerasNLP训练一个序列到序列的Transformer模型以完成机器翻译任务。
KerasNLP提供了构建NLP的基础模块(模型层、分词器、指标等),并使构建NLP管道变得方便。
在这个示例中,我们将使用KerasNLP层构建一个编码器-解码器的Transformer模型,并在英文到西班牙文的机器翻译任务上进行训练。
该示例基于fchollet的英文到西班牙文NMT示例。原始示例更为低级,且从头实现层,而这个示例使用KerasNLP展示一些更高级的方法,比如子词分词和使用指标来计算生成翻译的质量。
您将学习如何:
keras_nlp.tokenizers.WordPieceTokenizer
对文本进行分词。keras_nlp.layers.TransformerEncoder
、keras_nlp.layers.TransformerDecoder
和keras_nlp.layers.TokenAndPositionEmbedding
层来实现一个序列到序列的Transformer模型,并对其进行训练。keras_nlp.samplers
生成未见输入句子的翻译,使用top-p解码策略!如果您对KerasNLP不熟悉也不要担心。本教程将从基础知识开始。让我们直接开始吧!
在开始实现管道之前,让我们导入所有需要的库。
!pip install -q --upgrade rouge-score
!pip install -q --upgrade keras-nlp
!pip install -q --upgrade keras # 升级到Keras 3.
import keras_nlp
import pathlib
import random
import keras
from keras import ops
import tensorflow.data as tf_data
from tensorflow_text.tools.wordpiece_vocab import (
bert_vocab_from_dataset as bert_vocab,
)
错误: pip的依赖解析器目前不考虑所有已安装的包。此行为是以下依赖冲突的源头。
tensorflow 2.15.1要求keras<2.16,>=2.15.0,但您安装的是不兼容的keras 3.3.3。
我们还定义我们的参数/超参数。
BATCH_SIZE = 64
EPOCHS = 1 # 这应该至少为10以达到收敛
MAX_SEQUENCE_LENGTH = 40
ENG_VOCAB_SIZE = 15000
SPA_VOCAB_SIZE = 15000
EMBED_DIM = 256
INTERMEDIATE_DIM = 2048
NUM_HEADS = 8
我们将使用Anki提供的英文到西班牙文翻译数据集。让我们下载它:
text_file = keras.utils.get_file(
fname="spa-eng.zip",
origin="http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip",
extract=True,
)
text_file = pathlib.Path(text_file).parent / "spa-eng" / "spa.txt"
从http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip下载数据
2638744/2638744 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
每一行包含一个英文句子及其对应的西班牙句子。 英文句子是源序列,而西班牙句子是目标序列。 在将文本添加到列表之前,我们将其转换为小写。
with open(text_file) as f:
lines = f.read().split("\n")[:-1]
text_pairs = []
for line in lines:
eng, spa = line.split("\t")
eng = eng.lower()
spa = spa.lower()
text_pairs.append((eng, spa))
我们的句子对看起来是这样的:
for _ in range(5):
print(random.choice(text_pairs))
('tom heard that mary had bought a new computer.', 'tom oyó que mary se había comprado un computador nuevo.')
('will you stay at home?', '¿te vas a quedar en casa?')
('where is this train going?', '¿adónde va este tren?')
('tom panicked.', 'tom entró en pánico.')
("we'll help you rescue tom.", 'te ayudaremos a rescatar a tom.')
现在,让我们将句子对分成训练集、验证集和测试集。
random.shuffle(text_pairs)
num_val_samples = int(0.15 * len(text_pairs)) # 验证样本数量
num_train_samples = len(text_pairs) - 2 * num_val_samples # 训练样本数量
train_pairs = text_pairs[:num_train_samples]
val_pairs = text_pairs[num_train_samples : num_train_samples + num_val_samples]
test_pairs = text_pairs[num_train_samples + num_val_samples :]
print(f"{len(text_pairs)} total pairs") # 总对数
print(f"{len(train_pairs)} training pairs") # 训练对数
print(f"{len(val_pairs)} validation pairs") # 验证对数
print(f"{len(test_pairs)} test pairs") # 测试对数
118964 总对
83276 训练对
17844 验证对
17844 测试对
我们将定义两个分词器 - 一个用于源语言(英语),另一个用于目标语言(西班牙语)。我们将使用
keras_nlp.tokenizers.WordPieceTokenizer
来对文本进行分词。
keras_nlp.tokenizers.WordPieceTokenizer
需要一个 WordPiece 词汇表,并且具有分词和去分词的功能。
在定义这两个分词器之前,我们首先需要在我们拥有的数据集上训练它们。WordPiece 分词算法是一种子词分词算法;在语料库上训练它可以给我们一个子词词汇表。子词分词器是词分词器(词分词器需要非常大的词汇表来很好地覆盖输入词)和字符分词器(字符并不能像词一样编码意义)之间的妥协。幸运的是,KerasNLP 让在语料库上训练 WordPiece 变得非常简单,可以使用 keras_nlp.tokenizers.compute_word_piece_vocabulary
工具。
def train_word_piece(text_samples, vocab_size, reserved_tokens):
word_piece_ds = tf_data.Dataset.from_tensor_slices(text_samples)
vocab = keras_nlp.tokenizers.compute_word_piece_vocabulary(
word_piece_ds.batch(1000).prefetch(2),
vocabulary_size=vocab_size,
reserved_tokens=reserved_tokens,
)
return vocab
每个词汇表都有一些特殊的保留符号。我们有四个这样的符号:
"[PAD]"
- 填充符号。填充符号在输入序列长度小于最大序列长度时会附加到输入序列长度。"[UNK]"
- 未知符号。"[START]"
- 标记输入序列开始的符号。"[END]"
- 标记输入序列结束的符号。reserved_tokens = ["[PAD]", "[UNK]", "[START]", "[END]"]
eng_samples = [text_pair[0] for text_pair in train_pairs]
eng_vocab = train_word_piece(eng_samples, ENG_VOCAB_SIZE, reserved_tokens)
spa_samples = [text_pair[1] for text_pair in train_pairs]
spa_vocab = train_word_piece(spa_samples, SPA_VOCAB_SIZE, reserved_tokens)
让我们看看一些符号!
print("English Tokens: ", eng_vocab[100:110])
print("Spanish Tokens: ", spa_vocab[100:110])
English Tokens: ['at', 'know', 'him', 'there', 'go', 'they', 'her', 'has', 'time', 'will']
Spanish Tokens: ['le', 'para', 'te', 'mary', 'las', 'más', 'al', 'yo', 'tu', 'estoy']
现在,让我们定义分词器。我们将使用上述训练的词汇表配置分词器。
eng_tokenizer = keras_nlp.tokenizers.WordPieceTokenizer(
vocabulary=eng_vocab, lowercase=False
)
spa_tokenizer = keras_nlp.tokenizers.WordPieceTokenizer(
vocabulary=spa_vocab, lowercase=False
)
让我们尝试对我们数据集中的一个示例进行分词!为验证文本是否已正确分词,我们还可以将令牌列表去分词回原始文本。
eng_input_ex = text_pairs[0][0]
eng_tokens_ex = eng_tokenizer.tokenize(eng_input_ex)
print("English sentence: ", eng_input_ex)
print("Tokens: ", eng_tokens_ex)
print(
"Recovered text after detokenizing: ",
eng_tokenizer.detokenize(eng_tokens_ex),
)
print()
spa_input_ex = text_pairs[0][1]
spa_tokens_ex = spa_tokenizer.tokenize(spa_input_ex)
print("Spanish sentence: ", spa_input_ex)
print("Tokens: ", spa_tokens_ex)
print(
"Recovered text after detokenizing: ",
spa_tokenizer.detokenize(spa_tokens_ex),
)
English sentence: i am leaving the books here.
Tokens: tf.Tensor([ 35 163 931 66 356 119 12], shape=(7,), dtype=int32)
Recovered text after detokenizing: tf.Tensor(b'i am leaving the books here .', shape=(), dtype=string)
Spanish sentence: dejo los libros aquí.
Tokens: tf.Tensor([2962 93 350 122 14], shape=(5,), dtype=int32)
Recovered text after detokenizing: tf.Tensor(b'dejo los libros aqu\xc3\xad .', shape=(), dtype=string)
接下来,我们将格式化我们的数据集。
在每个训练步骤中,模型将寻求使用源句子和目标词0到N来预测目标词N+1(及以后)。
因此,训练数据集将生成一个元组 (inputs, targets)
,其中:
inputs
是一个字典,包含键 encoder_inputs
和 decoder_inputs
。encoder_inputs
是已经分词的源句子,而 decoder_inputs
是目标句子“迄今为止”的句子,即用于预测目标句子中的词N+1(及以后)的词0到N。target
是向前偏移一步的目标句子:
它提供目标句子中的下一个词 - 模型将试图预测的内容。我们将对输入西班牙语添加特殊符号 "[START]"
和 "[END]"
,
句子在对文本进行分词后。我们还将输入填充到固定长度。
这可以使用 keras_nlp.layers.StartEndPacker
轻松完成。
def preprocess_batch(eng, spa):
batch_size = ops.shape(spa)[0]
eng = eng_tokenizer(eng)
spa = spa_tokenizer(spa)
# 将 `eng` 填充到 `MAX_SEQUENCE_LENGTH`。
eng_start_end_packer = keras_nlp.layers.StartEndPacker(
sequence_length=MAX_SEQUENCE_LENGTH,
pad_value=eng_tokenizer.token_to_id("[PAD]"),
)
eng = eng_start_end_packer(eng)
# 将特殊标记(`"[START]"` 和 `"[END]"`)添加到 `spa` 中,并进行填充。
spa_start_end_packer = keras_nlp.layers.StartEndPacker(
sequence_length=MAX_SEQUENCE_LENGTH + 1,
start_value=spa_tokenizer.token_to_id("[START]"),
end_value=spa_tokenizer.token_to_id("[END]"),
pad_value=spa_tokenizer.token_to_id("[PAD]"),
)
spa = spa_start_end_packer(spa)
return (
{
"encoder_inputs": eng,
"decoder_inputs": spa[:, :-1],
},
spa[:, 1:],
)
def make_dataset(pairs):
eng_texts, spa_texts = zip(*pairs)
eng_texts = list(eng_texts)
spa_texts = list(spa_texts)
dataset = tf_data.Dataset.from_tensor_slices((eng_texts, spa_texts))
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.map(preprocess_batch, num_parallel_calls=tf_data.AUTOTUNE)
return dataset.shuffle(2048).prefetch(16).cache()
train_ds = make_dataset(train_pairs)
val_ds = make_dataset(val_pairs)
让我们快速查看一下序列形状 (我们有 64 对的批次,所有序列长度为 40 步):
for inputs, targets in train_ds.take(1):
print(f'inputs["encoder_inputs"].shape: {inputs["encoder_inputs"].shape}')
print(f'inputs["decoder_inputs"].shape: {inputs["decoder_inputs"].shape}')
print(f"targets.shape: {targets.shape}")
inputs["encoder_inputs"].shape: (64, 40)
inputs["decoder_inputs"].shape: (64, 40)
targets.shape: (64, 40)
现在,让我们进入令人兴奋的部分——定义我们的模型!
我们首先需要一个嵌入层,即为输入序列中的每个标记提供一个向量。
这个嵌入层可以随机初始化。我们还需要一个位置嵌入层,以编码序列中的词序。常规做法是将这两个嵌入相加。KerasNLP 具有一个 keras_nlp.layers.TokenAndPositionEmbedding
层,可以完成上述所有步骤。
我们的序列到序列转换器由一个 keras_nlp.layers.TransformerEncoder
层和一个 keras_nlp.layers.TransformerDecoder
层串联组成。
源序列将被传递给 keras_nlp.layers.TransformerEncoder
,该层将生成它的新表示。然后,这个新表示将与目标序列(目标词 0 到 N)一起传递给 keras_nlp.layers.TransformerDecoder
。keras_nlp.layers.TransformerDecoder
将寻求预测目标序列中的下一个词(N+1 及之后的词)。
使这成为可能的关键细节是因果掩蔽。
keras_nlp.layers.TransformerDecoder
一次可以看到整个序列,因此我们必须确保它仅使用从目标令牌 0 到 N 的信息来预测令牌 N+1(否则,它可能会使用未来的信息,这将导致模型在推理时无法使用)。因果掩蔽在 keras_nlp.layers.TransformerDecoder
中默认启用。
我们还需要掩蔽填充标记("[PAD]"
)。为此,我们可以将 keras_nlp.layers.TokenAndPositionEmbedding
层的 mask_zero
参数设置为 True。这将被传播到所有后续层。
# 编码器
encoder_inputs = keras.Input(shape=(None,), name="encoder_inputs")
x = keras_nlp.layers.TokenAndPositionEmbedding(
vocabulary_size=ENG_VOCAB_SIZE,
sequence_length=MAX_SEQUENCE_LENGTH,
embedding_dim=EMBED_DIM,
)(encoder_inputs)
encoder_outputs = keras_nlp.layers.TransformerEncoder(
intermediate_dim=INTERMEDIATE_DIM, num_heads=NUM_HEADS
)(inputs=x)
encoder = keras.Model(encoder_inputs, encoder_outputs)
# 解码器
decoder_inputs = keras.Input(shape=(None,), name="decoder_inputs")
encoded_seq_inputs = keras.Input(shape=(None, EMBED_DIM), name="decoder_state_inputs")
x = keras_nlp.layers.TokenAndPositionEmbedding(
vocabulary_size=SPA_VOCAB_SIZE,
sequence_length=MAX_SEQUENCE_LENGTH,
embedding_dim=EMBED_DIM,
)(decoder_inputs)
x = keras_nlp.layers.TransformerDecoder(
intermediate_dim=INTERMEDIATE_DIM, num_heads=NUM_HEADS
)(decoder_sequence=x, encoder_sequence=encoded_seq_inputs)
x = keras.layers.Dropout(0.5)(x)
decoder_outputs = keras.layers.Dense(SPA_VOCAB_SIZE, activation="softmax")(x)
decoder = keras.Model(
[
decoder_inputs,
encoded_seq_inputs,
],
decoder_outputs,
)
decoder_outputs = decoder([decoder_inputs, encoder_outputs])
transformer = keras.Model(
[encoder_inputs, decoder_inputs],
decoder_outputs,
name="transformer",
)
我们将使用准确性作为监控验证数据训练进度的快速方法。 请注意,机器翻译通常使用 BLEU 分数以及其他指标,而不是准确性。不过,为了使用 ROUGE、BLEU 等指标,我们需要解码概率并生成文本。文本生成计算成本高,在训练期间进行不推荐。
在这里,我们仅训练 1 个周期,但为了使模型实际收敛,您应该训练至少 10 个周期。
transformer.summary()
transformer.compile(
"rmsprop", loss="sparse_categorical_crossentropy", metrics=["accuracy"]
)
transformer.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
模型: "transformer"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ 连接到 ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ encoder_inputs │ (None, None) │ 0 │ - │ │ (输入层) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ token_and_position… │ (None, None, 256) │ 3,850,240 │ encoder_inputs[0… │ │ (TokenAndPositionE… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ decoder_inputs │ (None, None) │ 0 │ - │ │ (输入层) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ transformer_encoder │ (None, None, 256) │ 1,315,072 │ token_and_positi… │ │ (TransformerEncode… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ functional_3 │ (None, None, │ 9,283,992 │ decoder_inputs[0… │ │ (功能) │ 15000) │ │ transformer_enco… │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
总参数: 14,449,304 (55.12 MB)
可训练参数: 14,449,304 (55.12 MB)
非可训练参数: 0 (0.00 B)
1302/1302 ━━━━━━━━━━━━━━━━━━━━ 1701s 1s/step - accuracy: 0.8168 - loss: 1.4819 - val_accuracy: 0.8650 - val_loss: 0.8129
<keras.src.callbacks.history.History at 0x7efdd7ee6a50>
最后,让我们展示如何翻译全新的英文句子。
我们只需将标记化的英文句子输入到模型中,
以及目标标记 "[START]"
。模型输出下一个标记的概率。
然后我们根据迄今为止生成的标记反复生成下一个标记,直到我们遇到标记 "[END]"
。
为了解码,我们将使用 KerasNLP 中的 keras_nlp.samplers
模块。贪婪解码是一种文本解码方法,它在每个时间步骤输出最可能的下一个标记,即具有最高概率的标记。
def decode_sequences(input_sentences):
batch_size = 1
# 标记化编码器输入。
encoder_input_tokens = ops.convert_to_tensor(eng_tokenizer(input_sentences))
if len(encoder_input_tokens[0]) < MAX_SEQUENCE_LENGTH:
pads = ops.full((1, MAX_SEQUENCE_LENGTH - len(encoder_input_tokens[0])), 0)
encoder_input_tokens = ops.concatenate(
[encoder_input_tokens.to_tensor(), pads], 1
)
# 定义一个函数,该函数根据输入序列输出下一个标记的概率。
def next(prompt, cache, index):
logits = transformer([encoder_input_tokens, prompt])[:, index - 1, :]
# 暂时忽略隐藏状态;仅在对比搜索时需要。
hidden_states = None
return logits, hidden_states, cache
# 构建长度为 40 的提示,包含开始标记和填充标记。
length = 40
start = ops.full((batch_size, 1), spa_tokenizer.token_to_id("[START]"))
pad = ops.full((batch_size, length - 1), spa_tokenizer.token_to_id("[PAD]"))
prompt = ops.concatenate((start, pad), axis=-1)
generated_tokens = keras_nlp.samplers.GreedySampler()(
next,
prompt,
stop_token_ids=[spa_tokenizer.token_to_id("[END]")],
index=1, # 在开始标记后开始采样。
)
generated_sentences = spa_tokenizer.detokenize(generated_tokens)
return generated_sentences
test_eng_texts = [pair[0] for pair in test_pairs]
for i in range(2):
input_sentence = random.choice(test_eng_texts)
translated = decode_sequences([input_sentence])
translated = translated.numpy()[0].decode("utf-8")
translated = (
translated.replace("[PAD]", "")
.replace("[START]", "")
.replace("[END]", "")
.strip()
)
print(f"** 示例 {i} **")
print(input_sentence)
print(translated)
print()
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1714519073.816969 34774 device_compiler.h:186] 仅在进程的生命周期内记录一次此行,已使用 XLA 编译集群!
** 示例 0 **
i got the ticket free of charge.
me pregunto la comprome .
** 示例 1 **
i think maybe that's all you have to do.
creo que tom le dije que hacer eso .
有许多指标用于文本生成任务。在这里,为了评估我们模型生成的翻译,让我们计算 ROUGE-1 和 ROUGE-2 分数。基本上,ROUGE-N 是基于参考文本和生成文本之间的公共 n-gram 数量的得分。ROUGE-1 和 ROUGE-2 分别使用公共单元词和二元组的数量。
我们将在 30 个测试样本上计算分数(因为解码是一个昂贵的过程)。
rouge_1 = keras_nlp.metrics.RougeN(order=1)
rouge_2 = keras_nlp.metrics.RougeN(order=2)
for test_pair in test_pairs[:30]:
input_sentence = test_pair[0]
reference_sentence = test_pair[1]
translated_sentence = decode_sequences([input_sentence])
translated_sentence = translated_sentence.numpy()[0].decode("utf-8")
translated_sentence = (
translated_sentence.replace("[PAD]", "")
.replace("[START]", "")
.replace("[END]", "")
.strip()
)
rouge_1(reference_sentence, translated_sentence)
rouge_2(reference_sentence, translated_sentence)
print("ROUGE-1 分数: ", rouge_1.result())
print("ROUGE-2 分数: ", rouge_2.result())
ROUGE-1 分数: {'precision': <tf.Tensor: shape=(), dtype=float32, numpy=0.30989552>, 'recall': <tf.Tensor: shape=(), dtype=float32, numpy=0.37136248>, 'f1_score': <tf.Tensor: shape=(), dtype=float32, numpy=0.33032653>}
ROUGE-2 分数: {'precision': <tf.Tensor: shape=(), dtype=float32, numpy=0.08999339>, 'recall': <tf.Tensor: shape=(), dtype=float32, numpy=0.09524643>, 'f1_score': <tf.Tensor: shape=(), dtype=float32, numpy=0.08855649>}
经过10个时代,得分如下:
ROUGE-1 | ROUGE-2 | |
---|---|---|
精确度 | 0.568 | 0.374 |
召回率 | 0.615 | 0.394 |
F1得分 | 0.579 | 0.381 |