程式碼範例 / 自然語言處理 / 使用遷移學習的多重選擇任務

使用遷移學習的多重選擇任務

作者: Md Awsafur Rahman
建立日期 2023/09/14
上次修改日期 2023/09/14
描述: 使用預訓練的 NLP 模型進行多重選擇任務。

ⓘ 此範例使用 Keras 2

在 Colab 中檢視 GitHub 原始碼


簡介

在此範例中,我們將示範如何透過微調預訓練的 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)

中繼資料

  • train.csv - 將用於訓練。
  • sent1sent2:這些欄位顯示句子如何開始,如果您將這兩個欄位放在一起,您會得到 startphrase 欄位。
  • ending_<i>:表示句子可能如何結束的可能結尾,但其中只有一個是正確的。* label:識別正確的句子結尾。
  • val.csv - 與 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_idspadding_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

DataLoader

下面的程式碼使用 tf.data.Dataset 設定強大的資料流程管線以進行資料處理。tf.data 的重要方面包括其簡化管線建構並以序列表示元件的能力。

若要深入瞭解 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 排程

實作學習速率排程器對於遷移學習至關重要。學習速率從 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)

png


回呼

下面的函式將收集所有訓練回呼,例如 lr_schedulermodel_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 模型架構的全面、即用型實作。它具有各種預訓練模型,包括 BertRobertaDebertaV3 等等。在此筆記本中,我們將展示 DistillBert 的用法。不過,您可以隨時瀏覽 KerasHub 文件中所有可用的模型。此外,若要深入瞭解 KerasHub,請參閱內容豐富的入門指南

我們的方法涉及使用 keras_hub.models.XXClassifier 來處理每個問題和選項配對(例如 (Q+A)、(Q+B) 等等),以產生 logits。然後,結合這些 logits 並傳遞給 softmax 函式,以產生最終輸出。

多重選擇任務的分類器

在處理多重選擇問題時,我們不會將問題和所有選項一起提供給模型 (Q + A + B + C ...),而是一次向模型提供一個選項以及問題。例如,(Q + A)(Q + B) 等等。一旦我們取得所有選項的預測分數(logits),我們就會使用 Softmax 函式將它們組合起來以取得最終結果。如果我們一次將所有選項提供給模型,文字的長度會增加,使得模型更難處理。下圖說明了這個概念

Model Diagram

圖片來源: @johnowhitaker


從程式碼的角度來看,請記住,我們對所有五個選項使用相同的模型,並共用權重。儘管該圖顯示五個獨立的模型,但事實上,它們是一個具有共用權重的模型。另一個要考慮的重點是 Classifier 和 MultipleChoice 的輸入形狀。

  • 多重選擇的輸入形狀:$(batch_size, num_choices, seq_length)$
  • 分類器的輸入形狀:$(batch_size, seq_length)$

當然,顯然我們無法直接將多重選擇任務的資料提供給模型,因為輸入形狀不符。為了處理這個問題,我們將使用切片。這表示我們將分隔每個選項的功能,例如 $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)

png


訓練

# 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.
------------------------------------------------------------------------------------------ 

參考資料