作者: Md Awsafur Rahman
建立日期 2023/09/14
上次修改日期 2023/09/14
描述: 使用預訓練的 NLP 模型進行多重選擇任務。
在此範例中,我們將示範如何透過微調預訓練的 DebertaV3 模型來執行多重選擇任務。在此任務中,會提供數個候選答案以及上下文,並且訓練模型來選擇正確答案,這與問答不同。我們將使用 SWAG 資料集來示範此範例。
import keras_hub
import keras
import tensorflow as tf # For tf.data only.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
在此範例中,我們將使用 SWAG 資料集進行多重選擇任務。
!wget "https://github.com/rowanz/swagaf/archive/refs/heads/master.zip" -O swag.zip
!unzip -q swag.zip
--2023-11-13 20:05:24-- https://github.com/rowanz/swagaf/archive/refs/heads/master.zip
Resolving github.com (github.com)... 192.30.255.113
Connecting to github.com (github.com)|192.30.255.113|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://codeload.github.com/rowanz/swagaf/zip/refs/heads/master [following]
--2023-11-13 20:05:25-- https://codeload.github.com/rowanz/swagaf/zip/refs/heads/master
Resolving codeload.github.com (codeload.github.com)... 20.29.134.24
Connecting to codeload.github.com (codeload.github.com)|20.29.134.24|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/zip]
Saving to: ‘swag.zip’
swag.zip [ <=> ] 19.94M 4.25MB/s in 4.7s
2023-11-13 20:05:30 (4.25 MB/s) - ‘swag.zip’ saved [20905751]
!ls swagaf-master/data
README.md test.csv train.csv train_full.csv val.csv val_full.csv
class CFG:
preset = "deberta_v3_extra_small_en" # Name of pretrained models
sequence_length = 200 # Input sequence length
seed = 42 # Random seed
epochs = 5 # Training epochs
batch_size = 8 # Batch size
augment = True # Augmentation (Shuffle Options)
設定亂數種子值,以便在每次執行時產生相似的結果。
keras.utils.set_random_seed(CFG.seed)
sent1
和 sent2
:這些欄位顯示句子如何開始,如果您將這兩個欄位放在一起,您會得到 startphrase
欄位。ending_<i>
:表示句子可能如何結束的可能結尾,但其中只有一個是正確的。* label
:識別正確的句子結尾。train.csv
類似,但將用於驗證。# Train data
train_df = pd.read_csv(
"swagaf-master/data/train.csv", index_col=0
) # Read CSV file into a DataFrame
train_df = train_df.sample(frac=0.02)
print("# Train Data: {:,}".format(len(train_df)))
# Valid data
valid_df = pd.read_csv(
"swagaf-master/data/val.csv", index_col=0
) # Read CSV file into a DataFrame
valid_df = valid_df.sample(frac=0.02)
print("# Valid Data: {:,}".format(len(valid_df)))
# Train Data: 1,471
# Valid Data: 400
我們的方法需要向模型提供問題和答案配對,而不是對所有五個選項使用單一問題。實際上,這表示對於五個選項,我們將向模型提供相同的五個問題集,並結合每個各自的答案選項(例如,(Q + A)
、(Q + B)
等等)。這個比喻與在考試期間多次複習問題以促進對手頭問題的更深入理解的做法類似。
值得注意的是,在 SWAG 資料集的上下文中,問題是句子的開頭,而選項是該句子的可能結尾。
# Define a function to create options based on the prompt and choices
def make_options(row):
row["options"] = [
f"{row.startphrase}\n{row.ending0}", # Option 0
f"{row.startphrase}\n{row.ending1}", # Option 1
f"{row.startphrase}\n{row.ending2}", # Option 2
f"{row.startphrase}\n{row.ending3}",
] # Option 3
return row
將 make_options
函式套用至資料框架的每一列
train_df = train_df.apply(make_options, axis=1)
valid_df = valid_df.apply(make_options, axis=1)
運作方式:預處理器會接收輸入字串,並將它們轉換成包含預處理張量的字典(token_ids
、padding_mask
)。此流程從符號化開始,將輸入字串轉換成符號 ID 序列。
重要性:最初,原始文字資料由於其高維度而難以處理和建模。透過將文字轉換成一組精簡的符號,例如將 "The quick brown fox"
轉換成 ["the", "qu", "##ick", "br", "##own", "fox"]
,我們簡化了資料。許多模型依賴特殊符號和其他張量來理解輸入。這些符號有助於分隔輸入和識別填充,以及其他工作。透過填充使所有序列長度相同可以提高計算效率,使後續步驟更加順暢。
瀏覽以下頁面以存取 KerasHub 中可用的預處理和符號化器層: - 預處理 - 符號化器
preprocessor = keras_hub.models.DebertaV3Preprocessor.from_preset(
preset=CFG.preset, # Name of the model
sequence_length=CFG.sequence_length, # Max sequence length, will be padded if shorter
)
現在,讓我們檢視預處理層的輸出形狀為何。該層的輸出形狀可以表示為 $(num_choices, sequence_length)$。
outs = preprocessor(train_df.options.iloc[0]) # Process options for the first row
# Display the shape of each processed output
for k, v in outs.items():
print(k, ":", v.shape)
CUDA backend failed to initialize: Found CUDA version 12010, but JAX was built against version 12020, which is newer. The copy of CUDA that is installed must be at least as new as the version against which JAX was built. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
token_ids : (4, 200)
padding_mask : (4, 200)
我們將使用 preprocessing_fn
函式,使用 dataset.map(preprocessing_fn)
方法轉換每個文字選項。
def preprocess_fn(text, label=None):
text = preprocessor(text) # Preprocess text
return (
(text, label) if label is not None else text
) # Return processed text and label if available
在此筆記本中,我們將嘗試一種有趣的擴增技術 option_shuffle
。由於我們一次向模型提供一個選項,因此我們可以對選項順序引入隨機排序。例如,選項 [A, C, E, D, B]
將重新排列為 [D, B, A, E, C]
。此做法將有助於模型專注於選項本身的内容,而不是受到其位置的影響。
注意:儘管 option_shuffle
函式是以純 TensorFlow 撰寫,但它可以與任何後端(例如 JAX、PyTorch)搭配使用,因為它只在與 Keras 3 常式相容的 tf.data.Dataset
管線中使用。
def option_shuffle(options, labels, prob=0.50, seed=None):
if tf.random.uniform([]) > prob: # Shuffle probability check
return options, labels
# Shuffle indices of options and labels in the same order
indices = tf.random.shuffle(tf.range(tf.shape(options)[0]), seed=seed)
# Shuffle options and labels
options = tf.gather(options, indices)
labels = tf.gather(labels, indices)
return options, labels
在以下函式中,我們將合併所有擴增函式以套用至文字。這些擴增將使用 dataset.map(augment_fn)
方法套用至資料。
def augment_fn(text, label=None):
text, label = option_shuffle(text, label, prob=0.5) # Shuffle the options
return (text, label) if label is not None else text
下面的程式碼使用 tf.data.Dataset
設定強大的資料流程管線以進行資料處理。tf.data
的重要方面包括其簡化管線建構並以序列表示元件的能力。
def build_dataset(
texts,
labels=None,
batch_size=32,
cache=False,
augment=False,
repeat=False,
shuffle=1024,
):
AUTO = tf.data.AUTOTUNE # AUTOTUNE option
slices = (
(texts,)
if labels is None
else (texts, keras.utils.to_categorical(labels, num_classes=4))
) # Create slices
ds = tf.data.Dataset.from_tensor_slices(slices) # Create dataset from slices
ds = ds.cache() if cache else ds # Cache dataset if enabled
if augment: # Apply augmentation if enabled
ds = ds.map(augment_fn, num_parallel_calls=AUTO)
ds = ds.map(preprocess_fn, num_parallel_calls=AUTO) # Map preprocessing function
ds = ds.repeat() if repeat else ds # Repeat dataset if enabled
opt = tf.data.Options() # Create dataset options
if shuffle:
ds = ds.shuffle(shuffle, seed=CFG.seed) # Shuffle dataset if enabled
opt.experimental_deterministic = False
ds = ds.with_options(opt) # Set dataset options
ds = ds.batch(batch_size, drop_remainder=True) # Batch dataset
ds = ds.prefetch(AUTO) # Prefetch next batch
return ds # Return the built dataset
現在讓我們使用上述函式建立訓練和驗證 DataLoader。
# Build train dataloader
train_texts = train_df.options.tolist() # Extract training texts
train_labels = train_df.label.tolist() # Extract training labels
train_ds = build_dataset(
train_texts,
train_labels,
batch_size=CFG.batch_size,
cache=True,
shuffle=True,
repeat=True,
augment=CFG.augment,
)
# Build valid dataloader
valid_texts = valid_df.options.tolist() # Extract validation texts
valid_labels = valid_df.label.tolist() # Extract validation labels
valid_ds = build_dataset(
valid_texts,
valid_labels,
batch_size=CFG.batch_size,
cache=True,
shuffle=False,
repeat=False,
augment=False,
)
實作學習速率排程器對於遷移學習至關重要。學習速率從 lr_start
開始,並使用餘弦曲線逐漸遞減到 lr_min
。
重要性:結構良好的學習速率排程對於有效的模型訓練至關重要,可確保最佳收斂並避免過衝或停滯等問題。
import math
def get_lr_callback(batch_size=8, mode="cos", epochs=10, plot=False):
lr_start, lr_max, lr_min = 1.0e-6, 0.6e-6 * batch_size, 1e-6
lr_ramp_ep, lr_sus_ep = 2, 0
def lrfn(epoch): # Learning rate update function
if epoch < lr_ramp_ep:
lr = (lr_max - lr_start) / lr_ramp_ep * epoch + lr_start
elif epoch < lr_ramp_ep + lr_sus_ep:
lr = lr_max
else:
decay_total_epochs, decay_epoch_index = (
epochs - lr_ramp_ep - lr_sus_ep + 3,
epoch - lr_ramp_ep - lr_sus_ep,
)
phase = math.pi * decay_epoch_index / decay_total_epochs
lr = (lr_max - lr_min) * 0.5 * (1 + math.cos(phase)) + lr_min
return lr
if plot: # Plot lr curve if plot is True
plt.figure(figsize=(10, 5))
plt.plot(
np.arange(epochs),
[lrfn(epoch) for epoch in np.arange(epochs)],
marker="o",
)
plt.xlabel("epoch")
plt.ylabel("lr")
plt.title("LR Scheduler")
plt.show()
return keras.callbacks.LearningRateScheduler(
lrfn, verbose=False
) # Create lr callback
_ = get_lr_callback(CFG.batch_size, plot=True)
下面的函式將收集所有訓練回呼,例如 lr_scheduler
、model_checkpoint
。
def get_callbacks():
callbacks = []
lr_cb = get_lr_callback(CFG.batch_size) # Get lr callback
ckpt_cb = keras.callbacks.ModelCheckpoint(
f"best.keras",
monitor="val_accuracy",
save_best_only=True,
save_weights_only=False,
mode="max",
) # Get Model checkpoint callback
callbacks.extend([lr_cb, ckpt_cb]) # Add lr and checkpoint callbacks
return callbacks # Return the list of callbacks
callbacks = get_callbacks()
KerasHub
程式庫提供熱門 NLP 模型架構的全面、即用型實作。它具有各種預訓練模型,包括 Bert
、Roberta
、DebertaV3
等等。在此筆記本中,我們將展示 DistillBert
的用法。不過,您可以隨時瀏覽 KerasHub 文件中所有可用的模型。此外,若要深入瞭解 KerasHub
,請參閱內容豐富的入門指南。
我們的方法涉及使用 keras_hub.models.XXClassifier
來處理每個問題和選項配對(例如 (Q+A)、(Q+B) 等等),以產生 logits。然後,結合這些 logits 並傳遞給 softmax 函式,以產生最終輸出。
在處理多重選擇問題時,我們不會將問題和所有選項一起提供給模型 (Q + A + B + C ...)
,而是一次向模型提供一個選項以及問題。例如,(Q + A)
、(Q + B)
等等。一旦我們取得所有選項的預測分數(logits),我們就會使用 Softmax
函式將它們組合起來以取得最終結果。如果我們一次將所有選項提供給模型,文字的長度會增加,使得模型更難處理。下圖說明了這個概念
從程式碼的角度來看,請記住,我們對所有五個選項使用相同的模型,並共用權重。儘管該圖顯示五個獨立的模型,但事實上,它們是一個具有共用權重的模型。另一個要考慮的重點是 Classifier 和 MultipleChoice 的輸入形狀。
當然,顯然我們無法直接將多重選擇任務的資料提供給模型,因為輸入形狀不符。為了處理這個問題,我們將使用切片。這表示我們將分隔每個選項的功能,例如 $feature_{(Q + A)}$ 和 $feature_{(Q + B)}$,並將它們一一提供給 NLP 分類器。在我們取得所有選項的預測分數 $logits_{(Q + A)}$ 和 $logits_{(Q + B)}$ 之後,我們將使用 Softmax 函式(例如 $\operatorname{Softmax}([logits_{(Q + A)}, logits_{(Q + B)}])$)將它們組合起來。這個最後步驟有助於我們做出最終的決定或選擇。
請注意,在分類器中,我們設定
num_classes=1
而不是5
。這是因為分類器會為每個選項產生單一輸出。在處理五個選項時,這些個別輸出會結合在一起,然後通過 softmax 函式處理,以產生最終結果,其維度為5
。
# Selects one option from five
class SelectOption(keras.layers.Layer):
def __init__(self, index, **kwargs):
super().__init__(**kwargs)
self.index = index
def call(self, inputs):
# Selects a specific slice from the inputs tensor
return inputs[:, self.index, :]
def get_config(self):
# For serialize the model
base_config = super().get_config()
config = {
"index": self.index,
}
return {**base_config, **config}
def build_model():
# Define input layers
inputs = {
"token_ids": keras.Input(shape=(4, None), dtype="int32", name="token_ids"),
"padding_mask": keras.Input(
shape=(4, None), dtype="int32", name="padding_mask"
),
}
# Create a DebertaV3Classifier model
classifier = keras_hub.models.DebertaV3Classifier.from_preset(
CFG.preset,
preprocessor=None,
num_classes=1, # one output per one option, for five options total 5 outputs
)
logits = []
# Loop through each option (Q+A), (Q+B) etc and compute associated logits
for option_idx in range(4):
option = {
k: SelectOption(option_idx, name=f"{k}_{option_idx}")(v)
for k, v in inputs.items()
}
logit = classifier(option)
logits.append(logit)
# Compute final output
logits = keras.layers.Concatenate(axis=-1)(logits)
outputs = keras.layers.Softmax(axis=-1)(logits)
model = keras.Model(inputs, outputs)
# Compile the model with optimizer, loss, and metrics
model.compile(
optimizer=keras.optimizers.AdamW(5e-6),
loss=keras.losses.CategoricalCrossentropy(label_smoothing=0.02),
metrics=[
keras.metrics.CategoricalAccuracy(name="accuracy"),
],
jit_compile=True,
)
return model
# Build the Build
model = build_model()
讓我們檢視模型摘要,以更深入瞭解模型。
model.summary()
Model: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩ │ padding_mask │ (None, 4, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ token_ids │ (None, 4, None) │ 0 │ - │ │ (InputLayer) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ padding_mask_0 │ (None, None) │ 0 │ padding_mask[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ token_ids_0 │ (None, None) │ 0 │ token_ids[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ padding_mask_1 │ (None, None) │ 0 │ padding_mask[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ token_ids_1 │ (None, None) │ 0 │ token_ids[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ padding_mask_2 │ (None, None) │ 0 │ padding_mask[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ token_ids_2 │ (None, None) │ 0 │ token_ids[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ padding_mask_3 │ (None, None) │ 0 │ padding_mask[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ token_ids_3 │ (None, None) │ 0 │ token_ids[0][0] │ │ (SelectOption) │ │ │ │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ deberta_v3_classif… │ (None, 1) │ 70,830… │ padding_mask_0[0][0… │ │ (DebertaV3Classifi… │ │ │ token_ids_0[0][0], │ │ │ │ │ padding_mask_1[0][0… │ │ │ │ │ token_ids_1[0][0], │ │ │ │ │ padding_mask_2[0][0… │ │ │ │ │ token_ids_2[0][0], │ │ │ │ │ padding_mask_3[0][0… │ │ │ │ │ token_ids_3[0][0] │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ concatenate │ (None, 4) │ 0 │ deberta_v3_classifi… │ │ (Concatenate) │ │ │ deberta_v3_classifi… │ │ │ │ │ deberta_v3_classifi… │ │ │ │ │ deberta_v3_classifi… │ ├─────────────────────┼───────────────────┼─────────┼──────────────────────┤ │ softmax (Softmax) │ (None, 4) │ 0 │ concatenate[0][0] │ └─────────────────────┴───────────────────┴─────────┴──────────────────────┘
Total params: 70,830,337 (270.20 MB)
Trainable params: 70,830,337 (270.20 MB)
Non-trainable params: 0 (0.00 B)
最後,如果一切就緒,讓我們從視覺上檢查模型結構。
keras.utils.plot_model(model, show_shapes=True)
# Start training the model
history = model.fit(
train_ds,
epochs=CFG.epochs,
validation_data=valid_ds,
callbacks=callbacks,
steps_per_epoch=int(len(train_df) / CFG.batch_size),
verbose=1,
)
Epoch 1/5
183/183 ━━━━━━━━━━━━━━━━━━━━ 5087s 25s/step - accuracy: 0.2563 - loss: 1.3884 - val_accuracy: 0.5150 - val_loss: 1.3742 - learning_rate: 1.0000e-06
Epoch 2/5
183/183 ━━━━━━━━━━━━━━━━━━━━ 4529s 25s/step - accuracy: 0.3825 - loss: 1.3364 - val_accuracy: 0.7125 - val_loss: 0.9071 - learning_rate: 2.9000e-06
Epoch 3/5
183/183 ━━━━━━━━━━━━━━━━━━━━ 4524s 25s/step - accuracy: 0.6144 - loss: 1.0118 - val_accuracy: 0.7425 - val_loss: 0.8017 - learning_rate: 4.8000e-06
Epoch 4/5
183/183 ━━━━━━━━━━━━━━━━━━━━ 4522s 25s/step - accuracy: 0.6744 - loss: 0.8460 - val_accuracy: 0.7625 - val_loss: 0.7323 - learning_rate: 4.7230e-06
Epoch 5/5
183/183 ━━━━━━━━━━━━━━━━━━━━ 4517s 25s/step - accuracy: 0.7200 - loss: 0.7458 - val_accuracy: 0.7750 - val_loss: 0.7022 - learning_rate: 4.4984e-06
# Make predictions using the trained model on last validation data
predictions = model.predict(
valid_ds,
batch_size=CFG.batch_size, # max batch size = valid size
verbose=1,
)
# Format predictions and true answers
pred_answers = np.arange(4)[np.argsort(-predictions)][:, 0]
true_answers = valid_df.label.values
# Check 5 Predictions
print("# Predictions\n")
for i in range(0, 50, 10):
row = valid_df.iloc[i]
question = row.startphrase
pred_answer = f"ending{pred_answers[i]}"
true_answer = f"ending{true_answers[i]}"
print(f"❓ Sentence {i+1}:\n{question}\n")
print(f"✅ True Ending: {true_answer}\n >> {row[true_answer]}\n")
print(f"🤖 Predicted Ending: {pred_answer}\n >> {row[pred_answer]}\n")
print("-" * 90, "\n")
50/50 ━━━━━━━━━━━━━━━━━━━━ 274s 5s/step
# Predictions
❓ Sentence 1:
The man shows the teens how to move the oars. The teens
✅ True Ending: ending3
>> follow the instructions of the man and row the oars.
🤖 Predicted Ending: ending3
>> follow the instructions of the man and row the oars.
------------------------------------------------------------------------------------------
❓ Sentence 11:
A lake reflects the mountains and the sky. Someone
✅ True Ending: ending2
>> runs along a desert highway.
🤖 Predicted Ending: ending1
>> remains by the door.
------------------------------------------------------------------------------------------
❓ Sentence 21:
On screen, she smiles as someone holds up a present. He watches somberly as on screen, his mother
✅ True Ending: ending1
>> picks him up and plays with him in the garden.
🤖 Predicted Ending: ending0
>> comes out of her apartment, glowers at her laptop.
------------------------------------------------------------------------------------------
❓ Sentence 31:
A woman in a black shirt is sitting on a bench. A man
✅ True Ending: ending2
>> sits behind a desk.
🤖 Predicted Ending: ending0
>> is dancing on a stage.
------------------------------------------------------------------------------------------
❓ Sentence 41:
People are standing on sand wearing red shirts. They
✅ True Ending: ending3
>> are playing a game of soccer in the sand.
🤖 Predicted Ending: ending3
>> are playing a game of soccer in the sand.
------------------------------------------------------------------------------------------