作者: 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 # Upgrade to 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,
)
[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
tensorflow 2.15.1 requires keras<2.16,>=2.15.0, but you have keras 3.3.3 which is incompatible.[31m
我們還要定義我們的參數/超參數。
BATCH_SIZE = 64
EPOCHS = 1 # This should be at least 10 for convergence
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"
Downloading data from 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 total pairs
83276 training pairs
17844 validation pairs
17844 test pairs
我們將定義兩個標記生成器 - 一個用於來源語言(英文),另一個用於目標語言(西班牙文)。我們將使用 keras_nlp.tokenizers.WordPieceTokenizer
對文字進行標記化。 keras_nlp.tokenizers.WordPieceTokenizer
採用 WordPiece 詞彙表,並具有用於對文字進行標記化和對標記序列進行去標記化的函數。
在定義兩個標記生成器之前,我們首先需要使用我們擁有的資料集對其進行訓練。WordPiece 標記化演算法是一種子詞標記化演算法;在語料庫上對其進行訓練可以讓我們獲得子詞詞彙表。子詞標記生成器是詞標記生成器(詞標記生成器需要非常大的詞彙表才能很好地涵蓋輸入詞)和字元標記生成器(字元不像詞那樣真正編碼含義)之間的折衷方案。幸運的是,KerasNLP 可以使用 keras_nlp.tokenizers.compute_word_piece_vocabulary
工具程式輕鬆地在語料庫上訓練 WordPiece。
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)
# Pad `eng` to `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)
# Add special tokens (`"[START]"` and `"[END]"`) to `spa` and pad it as well.
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
層,它可以為我們完成上述所有步驟。
我們的序列到序列 Transformer 由一個 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
一次看到整個序列,因此我們必須確保它在預測標記 N+1 時僅使用來自目標標記 0 到 N 的信息(否則,它可能會使用來自未來的信息,這將導致模型在推理時無法使用)。在 keras_nlp.layers.TransformerDecoder
中默認啟用因果遮罩。
我們還需要遮罩填充標記("[PAD]"
)。為此,我們可以將 keras_nlp.layers.TokenAndPositionEmbedding
層的 mask_zero
參數設置為 True。然後,這將傳播到所有後續層。
# Encoder
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
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 個 epoch,但要讓模型真正收斂,您應該至少訓練 10 個 epoch。
transformer.summary()
transformer.compile(
"rmsprop", loss="sparse_categorical_crossentropy", metrics=["accuracy"]
)
transformer.fit(train_ds, epochs=EPOCHS, validation_data=val_ds)
Model: "transformer"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ encoder_inputs │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ token_and_position… │ (None, None, 256) │ 3,850,240 │ encoder_inputs[0… │ │ (TokenAndPositionE… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ decoder_inputs │ (None, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ transformer_encoder │ (None, None, 256) │ 1,315,072 │ token_and_positi… │ │ (TransformerEncode… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ functional_3 │ (None, None, │ 9,283,992 │ decoder_inputs[0… │ │ (Functional) │ 15000) │ │ transformer_enco… │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 14,449,304 (55.12 MB)
Trainable params: 14,449,304 (55.12 MB)
Non-trainable params: 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
# Tokenize the encoder input.
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
)
# Define a function that outputs the next token's probability given the
# input sequence.
def next(prompt, cache, index):
logits = transformer([encoder_input_tokens, prompt])[:, index - 1, :]
# Ignore hidden states for now; only needed for contrastive search.
hidden_states = None
return logits, hidden_states, cache
# Build a prompt of length 40 with a start token and padding tokens.
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, # Start sampling after start token.
)
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"** Example {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] Compiled cluster using XLA! This line is logged at most once for the lifetime of the process.
** Example 0 **
i got the ticket free of charge.
me pregunto la comprome .
** Example 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 Score: ", rouge_1.result())
print("ROUGE-2 Score: ", rouge_2.result())
ROUGE-1 Score: {'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 Score: {'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 個 epoch 後,分數如下
ROUGE-1 | ROUGE-2 | |
---|---|---|
精確率 | 0.568 | 0.374 |
召回率 | 0.615 | 0.394 |
F1 分數 | 0.579 | 0.381 |