作者: Abheesht Sharma
建立日期 2022/05/26
上次修改日期 2024/04/30
說明: 使用 KerasHub 在機器翻譯任務上訓練序列到序列 Transformer 模型。
KerasHub 為 NLP 提供建構模塊(模型層、分詞器、指標等),使其方便構建 NLP 流程。
在此範例中,我們將使用 KerasHub 層來建構編碼器-解碼器 Transformer 模型,並在英翻西機器翻譯任務上訓練它。
此範例基於 fchollet 的英翻西 NMT 範例。原始範例更底層,從頭開始實現各層,而此範例使用 KerasHub 來展示一些更進階的方法,例如子詞分詞和使用指標來計算生成翻譯的品質。
您將學會如何
keras_hub.tokenizers.WordPieceTokenizer
對文字進行分詞。keras_hub.layers.TransformerEncoder
、keras_hub.layers.TransformerDecoder
和 keras_hub.layers.TokenAndPositionEmbedding
層實現序列到序列 Transformer 模型,並對其進行訓練。keras_hub.samplers
,透過 top-p 解碼策略生成未見輸入句子的翻譯!如果您不熟悉 KerasHub,請不用擔心。本教學課程將從基礎知識開始。讓我們直接開始吧!
在我們開始實作流程之前,讓我們先匯入所有需要的程式庫。
!pip install -q --upgrade rouge-score
!pip install -q --upgrade keras-hub
!pip install -q --upgrade keras # Upgrade to Keras 3.
import keras_hub
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_hub.tokenizers.WordPieceTokenizer
來對文字進行分詞。keras_hub.tokenizers.WordPieceTokenizer
採用 WordPiece 詞彙表,並具有用於對文字進行分詞和將詞符序列還原為原始文字的函數。
在我們定義兩個分詞器之前,我們首先需要在我們擁有的資料集上訓練它們。WordPiece 分詞演算法是一種子詞分詞演算法;在語料庫上訓練它會給我們提供一個子詞詞彙表。子詞分詞器是詞分詞器(詞分詞器需要非常大的詞彙表才能很好地涵蓋輸入詞)和字元分詞器(字元並不真正像詞一樣編碼意義)之間的折衷方案。幸運的是,KerasHub 可以使用 keras_hub.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_hub.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_hub.tokenizers.WordPieceTokenizer(
vocabulary=eng_vocab, lowercase=False
)
spa_tokenizer = keras_hub.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_hub.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_hub.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_hub.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)
現在,讓我們繼續進入令人興奮的部分 - 定義我們的模型!我們首先需要一個嵌入層,也就是說,我們輸入序列中每個詞符的向量。此嵌入層可以隨機初始化。我們還需要一個位置嵌入層,該層會編碼序列中的詞順序。慣例是新增這兩個嵌入。KerasHub 有一個 keras_hub.layers.TokenAndPositionEmbedding
層,它會為我們執行上述所有步驟。
我們的序列到序列 Transformer 由一個 keras_hub.layers.TransformerEncoder
層和一個 keras_hub.layers.TransformerDecoder
層鏈接在一起組成。
來源序列將傳遞給 keras_hub.layers.TransformerEncoder
,它將產生一個新的表示形式。然後,此新的表示形式將與「到目前為止」的目標序列(目標詞 0 到 N)一起傳遞給 keras_hub.layers.TransformerDecoder
。keras_hub.layers.TransformerDecoder
隨後將嘗試預測目標序列中的下一個詞(N+1 及之後)。
其中一個關鍵細節是因果遮罩 (causal masking)。keras_hub.layers.TransformerDecoder
會一次看到整個序列,因此我們必須確保它在預測第 N+1 個 token 時,只使用目標 tokens 0 到 N 的資訊 (否則,它可能會使用來自未來的資訊,這會導致模型在推論時無法使用)。在 keras_hub.layers.TransformerDecoder
中,因果遮罩預設為啟用。
我們還需要遮罩填充 tokens ("[PAD]"
)。為此,我們可以將 keras_hub.layers.TokenAndPositionEmbedding
層的 mask_zero
參數設定為 True。這會傳播到所有後續的層。
# Encoder
encoder_inputs = keras.Input(shape=(None,), name="encoder_inputs")
x = keras_hub.layers.TokenAndPositionEmbedding(
vocabulary_size=ENG_VOCAB_SIZE,
sequence_length=MAX_SEQUENCE_LENGTH,
embedding_dim=EMBED_DIM,
)(encoder_inputs)
encoder_outputs = keras_hub.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_hub.layers.TokenAndPositionEmbedding(
vocabulary_size=SPA_VOCAB_SIZE,
sequence_length=MAX_SEQUENCE_LENGTH,
embedding_dim=EMBED_DIM,
)(decoder_inputs)
x = keras_hub.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>
最後,讓我們示範如何翻譯全新的英文句子。我們只需將 token 化的英文句子以及目標 token "[START]"
輸入到模型中。模型輸出下一個 token 的機率。然後,我們反覆生成下一個 token,並以目前生成的 tokens 為條件,直到我們遇到 token "[END]"
。
對於解碼,我們將使用 KerasHub 中的 keras_hub.samplers
模組。貪婪解碼是一種文本解碼方法,它在每個時間步輸出最有可能的下一個 token,也就是機率最高的 token。
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_hub.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 分別使用共同 unigram 和 bigram 的數量。
我們將在 30 個測試樣本上計算分數 (因為解碼是一個耗費資源的過程)。
rouge_1 = keras_hub.metrics.RougeN(order=1)
rouge_2 = keras_hub.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 | |
---|---|---|
精確率 (Precision) | 0.568 | 0.374 |
召回率 (Recall) | 0.615 | 0.394 |
F1 分數 (F1 Score) | 0.579 | 0.381 |