精通Transformer——提升模型性能

493 阅读25分钟

到目前为止,我们已经使用常见的方法解决了许多任务,并取得了一些成功。然而,通过利用特定的技术,我们可以进一步提高任务的性能。在文献中有几种方法可以提高Transformer模型的性能。在本章中,我们将探讨其中的一些技术,并展示如何通过数据增强或领域适应等方式,超越常规训练流程来提升模型。数据增强是一种强大的技术,广泛用于提高深度学习模型的准确性。通过增强数据点,深度学习模型可以更有效地捕捉数据中的潜在模式和关系。

另一种提高模型性能的方法是领域适应。由于大型语言模型是在通用和多样化的文本上训练的,当应用于特定领域时,可能会存在不一致性。我们可能需要通过整合多个因素,根据特定的应用领域调整这些语言模型。

超参数优化(HPO)是深度学习的每个子领域中广泛使用的技术,用于实现最佳性能。在训练深度学习模型时,某些参数(称为超参数)无法在训练过程中学习。HPO帮助我们找到这些参数。

在本章中,我们将涵盖以下主题:

  • 通过数据增强提高性能
  • 使模型适应领域
  • 使用HPO优化参数

技术要求

我们需要安装以下Python库:nlpaug、datasets、transformers、sacremoses和optuna。

通过数据增强提高性能

数据增强是深度学习中提高模型成功率的常见技术。它涉及通过做一些不显著改变数据点意义的变动来复制数据点。这种技术已经广泛应用于图像处理和自然语言处理(NLP),因为用更多的数据训练神经模型通常会带来更好的性能。

我们可以向现有数据引入噪声或扰动,创建同一数据的新变种或新方面,或者通过插值生成全新的数据点,其中新数据基于几个现有的相邻数据点生成。数据增强在训练数据有限时尤其有用。它可以使模型更加稳健,并提高其在下游任务中的表现。执行数据增强的方法有很多种。在图像处理领域,这可能包括翻转图像或改变图像亮度。在NLP中,这可能包括删除或交换单词、插入随机字符/单词、改变句子中单词的顺序以及许多高级技术。

复制的过程完全取决于创造力和当前的NLP任务。如果你正在处理语言可接受性分类任务,打乱单词顺序可能不是一个有用的增强方法。如果你在进行情感分类任务,这种打乱顺序的过程可能是实用的。关键问题是需要分别评估每种增强技术,以确定哪些技术有效,哪些技术有害,因为在没有试验的情况下很难知道哪些技术对我们有帮助。

最后一点,我们可以说,这种增强过程也广泛用于对比学习领域。对比学习方法允许我们轻松地使用这些增强技术为自监督学习创建正样本和负样本。因此,我们不仅可以通过增加数据量来受益于数据增强,还可以在自监督学习中受益。

现在,让我们使用IMDb情感分类示例来处理一些数据增强技术。首先,让我们讨论一些数据增强技术并使用它们来提高模型性能。

字符级增强

仅对字符进行操作的增强属于这一类别。我们可以通过操作字符来简单地增强数据。虽然可能无法详细讨论每种方法,但让我们重点介绍一些最重要的方法。以下是一些示例:

字符级增强原文增强后
键盘增强替换为键盘邻近字符I love cats and dogs
随机增强随机插入/交换/删除字符I love cats and dogs
OCR错误模拟OCR错误:e -> o, l -> 1branches

单词级增强

与字符级方法相比,单词级方法有更多的可能性。在这一点上,我们可以依赖单词语义,这为增强提供了许多技术。此类别下使用的技术大多基于预定义的单词列表,且易于应用。

单词级增强原文增强后
拼写错误词典使用预定义的常见拼写错误词典achieve
上下文词嵌入增强使用深度学习模型(如word2vec或BERT)替换上下文意义最接近的单词Banana
同义词替换基于同义词词典automobile
反义词替换基于反义词词典large
随机单词操作删除/插入单词I love cats and dogs
单词打乱改变单词顺序I love cats and dogs

虽然单词级增强可能有用,但它也有局限性,因为单词级操作独立于上下文,无法利用句子的语义信息。因此,我们应该考虑使用基于句子语义的更高级技术,如下一节中所述。

句子级增强

句子级增强是一种考虑整个句子及其意义的数据增强方法。随着语言模型的不断发展,这种技术变得越来越普遍。我们将讨论两个示例。

反向翻译是一种将文本翻译成另一种语言(例如,从英文翻译成法文),然后再翻译回原始语言(从法文翻译回英文)的方法。另一种示例是将文本从语言A翻译到语言B,再翻译到语言C,最后再以相同的方式翻译回原始语言(从C到B到A)。

另一种方法是通过训练好的模型生成语义上重构的文本,这被称为释义(Paraphrasing)。这些模型会生成与原文本意义相同的另一个文本。因此,这种方法非常适合数据增强。然而,对于这两种方法,由于神经模型的推理时间较长,增强过程会比之前的技术稍慢。

以下是一些句子级增强的示例:

句子级增强原文增强后
反向翻译将文本翻译成另一种语言(EN-FRA),然后再翻译回原始语言(FRA-EN)You have to think a lot to understand
释义使用文本到文本模型Life is the future, not the past

太好了!我们已经准备好将这些技术应用于任何NLP任务,并观察它们是否能提高性能。我们将在下一部分看到这一点。

通过数据增强提升IMDb文本分类

现在,我们将通过利用nlpaug库来检查一些这些增强方法,从而提高IMDb情感任务的模型性能。有关更多信息,请访问以下链接:nlpaug GitHub

这个Python库可以帮助我们增强IMDb数据集的文本数据点(IMDb数据集)。你可以在这个库中找到更多的数据增强方法。我们这里只应用了一些随机选择的方法进行演示,但为了确定最适合你NLP任务的技术组合,进行详细的分析是很重要的。

我们首先安装以下必要的包:

pip install nlpaug datasets transformers sacremoses

以下是增强所需的导入:

import nlpaug.augmenter.char as nac
import nlpaug.augmenter.word as naw
import nlpaug.augmenter.sentence as nas
import nlpaug.flow as nafc
from nlpaug.util import Action

在第5章中,我们使用了IMDb数据集的一小部分,出于两个原因:为了快速原型开发,并评估增强技术对模型性能的影响。

以下代码展示了我们如何选择2000个示例作为训练数据:

from datasets import load_dataset
imdb_train = load_dataset('imdb', split="train[:1000]+train[-1000:]")
imdb_test = load_dataset('imdb', split="test[:500]+test[-500:]")
imdb_val = load_dataset('imdb', split="test[500:1000]+test[-1000:-500]")
imdb_train.shape, imdb_test.shape, imdb_val.shape
# OUTPUT> ((2000, 2), (1000, 2), (1000, 2))

我们定义一些必要的函数,这些函数还将在本章后面看到的示例中使用:

from sklearn.metrics import accuracy_score, precision_recall_fscore_support

def compute_metrics(pred):
    labels = pred.label_ids
    preds = pred.predictions.argmax(-1)
    precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average='macro')
    acc = accuracy_score(labels, preds)
    return {
        'Accuracy': acc,
        'F1': f1,
        'Precision': precision,
        'Recall': recall
    }

def tokenize_it(e):
    return tokenizer(e['text'], padding=True, truncation=True)

现在,我们使用nlpaug库实例化10个不同的随机增强对象,如下所示:

import nlpaug.augmenter.word as naw

# 替换为键盘邻近字符
aug1 = nac.KeyboardAug(aug_word_p=0.2, aug_char_max=2, aug_word_max=4)

# 随机插入/交换/删除字符
aug2 = nac.RandomCharAug(action="insert", aug_char_max=1)
aug3 = nac.RandomCharAug(action="swap", aug_char_max=1)
aug4 = nac.RandomCharAug(action="delete", aug_char_max=1)

# 拼写错误
aug5 = naw.SpellingAug()

# 上下文单词插入/替换
aug6 = naw.ContextualWordEmbsAug(model_path='bert-base-uncased', action="insert")
aug7 = naw.ContextualWordEmbsAug(model_path='bert-base-uncased', action="substitute")

# 基于WordNet的同义词替换
aug8 = naw.SynonymAug(aug_src='wordnet')

# 随机删除单词
aug9 = naw.RandomWordAug()

# 反向翻译
aug10 = naw.BackTranslationAug(from_model_name='facebook/wmt19-en-de', to_model_name='facebook/wmt19-de-en', device='cuda')

现在,让我们编写一个包装函数 augment_it(),将这些技术中的10种结合并应用。参数是要增强的原始文本(text)和原始标签(label)。我们为每个增强文本复制原始标签:

def augment_it(text, label):
    result = [eval("aug"+str(i)).augment(text)[0] for i in range(1, 11)]
    return result, [label] * len(result)

以下是一个运行示例:

augment_it("i like cats and dogs", 1)

输出:

(['i li.W cats and dogs',
 'i lBike cats and zdogs',
 'i liek ctas and dogs',
 'i ike ats and dogs',
 'in like cats and gogs',
 'mostly i like my cats and dogs',
 'i like this no dogs',
 'i like cats and dog',
 'like cats and',
 'I like cats and dogs'],
 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1])

请注意,反向翻译没有生成新句子,并返回了相同的句子,因为给定的示例对翻译来说非常简单。对于更复杂的示例,我们可以得到不同的返回值。我们还如预期返回了标签1的10个副本。

使用以下代码片段,IMDB数据集的10%随机样本将被复制。对于每个选择的示例,将生成10个增强句子。然后,输入数据集的大小将增加一倍。由于此代码片段使用类似BERT的模型,生成过程会有些慢。

我们可以通过调整增强的程度(frac-> [0.1 - 1.0])来观察对性能的影响,使用整个数据集或增加增强技术的数量:

import pandas as pd

imdb_df = pd.DataFrame(imdb_train)
texts = []
labels = []
for r in imdb_df.sample(frac=0.1).itertuples(index=False):
    t, l = augment_it(r.text, r.label)
    texts += t
    labels += l

aug_df = pd.DataFrame()
aug_df["text"] = texts
aug_df["label"] = labels
imdb_augmented = pd.concat([imdb_df, aug_df])
imdb_df.shape, imdb_augmented.shape
# OUTPUT: ((2000, 2), (4000, 2))

我们将数据大小翻倍(从2K到4K),并获得了 imdb_augmented

以下代码包含两个数据集:一个是正常数据集(imdb_train),另一个是增强数据集(imdb_augmented)。目前选择的是正常数据集,而增强数据集被注释掉了。当你第二次运行代码时,应该交换它们(选择imdb_augmented数据集,并注释掉正常数据集):

from transformers import BertTokenizerFast, BertForSequenceClassification

model_path = 'bert-base-uncased'
tokenizer = BertTokenizerFast.from_pretrained(model_path)

# IMDb训练数据,带有增强
# imdb_augmented2 = Dataset.from_pandas(imdb_augmented)
# enc_train = imdb_augmented2.map(tokenize_it, batched=True, batch_size=1000)

# IMDb训练数据,不带增强
enc_train = imdb_train.map(tokenize_it, batched=True, batch_size=1000)
enc_test = imdb_test.map(tokenize_it, batched=True, batch_size=1000)
enc_val = imdb_val.map(tokenize_it, batched=True, batch_size=1000)

model = BertForSequenceClassification.from_pretrained(
    model_path,
    id2label={0: "NEG", 1: "POS"},
    label2id={"NEG": 0, "POS": 1}
)

通过这段代码,我们微调了 bert-base-uncased 模型。以下代码片段与我在第5章中应用的完全相同:

from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir='./MyIMDBModel',
    do_train=True,
    do_eval=True,
    num_train_epochs=3,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    fp16=True,
    load_best_model_at_end=True
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=enc_train,
    eval_dataset=enc_val,
    compute_metrics=compute_metrics
)

trainer.train()
q = [trainer.evaluate(eval_dataset=data) for data in [enc_train, enc_val, enc_test]]
pd.DataFrame(q, index=["train", "val", "test"]).iloc[:, :5]

不使用增强时,得到的输出如下:

image.png

使用增强技术后,我们得到如下结果:

image.png

正如我们所见,评估的F1分数从86.8提高到了88.6(+1.8)。此外,其他参数似乎也有所改善。为了进一步提高性能,我们可以扩大增强技术的应用范围,并对不同的增强方法进行详细分析以便选择最佳方法。

干得好!在下一节中,我们将尝试通过领域适应来进一步提高模型性能!

使模型适应领域

尽管Transformer架构的微调方法通常表现良好,但源数据和目标数据分布之间的差异可能会显著影响微调的效果(参见《自然语言处理中的迁移学习》,Ruder等,NAACL 2019)。如果源数据集和目标数据集存在显著差异,微调在没有适应的情况下可能难以学习。

以往的研究表明,预训练的Transformer是应对单词分布变化最为稳健的架构,并且比其他架构具有更强的泛化能力(Yildirim, Savas, et al.《针对软件需求数据的多分类自适应微调》,arXiv预印本 arXiv:2301.00495 (2023))。然而,使用额外的领域内数据,仍有改进的空间(参见《不要停止预训练:使语言模型适应领域和任务》,Gururangan等,ACL 2020)。通过将模型专门化到目标数据,我们期望提高其在下游任务中的表现。也就是说,一个预训练模型会通过在更接近目标分布的目标数据上使用预训练目标进一步训练。最终,我们将得到预训练模型的另一个版本(例如,adapted-bert),这个版本仍然需要针对下游任务进行微调。以下图表总结了我们的自适应微调框架:

image.png

如图底部所示,自适应微调包含三个阶段。目标函数通常是掩码语言模型(MLM)。在第一步中,文献中已经有许多预训练的检查点可供使用,例如 bert-base-uncasedroberta-large。因此,我们不需要从头开始预训练模型。这些预训练模型已经在大量多样化的数据上进行了训练,其参数中已编码了足够深的句法和语义知识,正如我们在第3到第5章中讨论的那样。

在第二阶段,预训练模型使用相同的源目标MLM,但通过目标(领域内)无监督数据集进行训练以进行适应。需要注意的是,可以利用其他种类的辅助目标函数来提高适应性。在此阶段,我们不会改变原始BERT模型架构的目标(MLM),也不会添加任何额外的参数。经过这个过程后,我们将获得一个适应后的检查点,准备好对任何下游任务进行微调,如第5章所述。

让我们使用领域数据适应BERT模型,然后对其进行微调。我们将使用相同的IMDb数据集。我们首先定义 model_path 并加载分词器,如下所示:

from transformers import (BertTokenizerFast, BertForSequenceClassification)
model_path = 'bert-base-uncased'
tokenizer = BertTokenizerFast.from_pretrained(model_path)

以下代码加载一个由4000个训练样本、1000个验证样本和1000个测试样本组成的数据集。如果需要,你可以将其扩展为整个数据集:

from datasets import load_dataset
imdb_train = load_dataset('imdb', split="train[:2000]+train[-2000:]")
imdb_test = load_dataset('imdb', split="test[:500]+test[-500:]")
imdb_val = load_dataset('imdb', split="test[500:1000]+test[-1000:-500]")
imdb_train.shape, imdb_test.shape, imdb_val.shape
# OUTPUT: ((4000, 2), (1000, 2), (1000, 2))

然后我们对数据集进行分词和编码,为训练做准备,如下所示:

def tokenize_it(e):
    return tokenizer(e['text'], padding=True, truncation=True)
enc_train = imdb_train.map(tokenize_it, batched=True, batch_size=1000)
enc_test = imdb_test.map(tokenize_it, batched=True, batch_size=1000)
enc_val = imdb_val.map(tokenize_it, batched=True, batch_size=1000)

此时,我们将使用可用训练数据集中所有原始文本来适应 bert-base-uncased 模型。在这里不会使用任何监督方法。你可以在此阶段使用你领域中的所有原始文本数据。因此,只会使用IMDb训练数据的文本部分。数据大小对于良好的适应性至关重要;因此,我们将获取所有训练数据并进行适应。

让我们加载整个训练数据,如下所示:

dataset_for_adaptation = load_dataset('imdb', split="train")
imdb_sentences = dataset_for_adaptation["text"]
train_sentences = imdb_sentences[:20000]
dev_sentences = imdb_sentences[20000:]

然后,加载模型:

from transformers import AutoModelForMaskedLM, AutoTokenizer
model = AutoModelForMaskedLM.from_pretrained(model_path)

以下是适应阶段的一些超参数:

batch_size = 16
num_train_epochs = 15
max_length = 100
mlm_prob = 0.25

在这里,我们将epoch数设置得比正常微调过程更长。另一方面,BERT的原始MLM率值为0.15。MLM率值表示输入标记被掩码的百分比。在一些研究中,已经表明如果这个值在15%到40%之间会更有效。随着被掩码标记数量的增加,模型能够提取更强的表示来捕捉意义。同样地,可能需要将步长或epoch值设置得更长。因此,应用试错方法或HPO(我们将在下一节中看到)将是有益的。在本研究中,我们将MLM值保持在0.25。你可以根据GPU的容量增加最大长度值和批处理值。

我们借用 TokenizedSentencesDataset 类定义自SBERT GitHub库。你还可以在那里找到命令行脚本代码,直接运行Python代码而无需深入Jupyter代码块(链接)。

我们可以在以下代码中看到数据集类的定义:

class TokenizedSentencesDataset:
    def __init__(self, sentences, tokenizer, max_length, cache_tokenization=False):
        self.tokenizer = tokenizer
        self.sentences = sentences
        self.max_length = max_length
        self.cache_tokenization = cache_tokenization
    def __getitem__(self, item):
        if not self.cache_tokenization:
            return self.tokenizer(self.sentences[item],
                add_special_tokens=True,
                truncation=True,
                max_length=self.max_length,
                return_special_tokens_mask=True)
        if isinstance(self.sentences[item], str):
            self.sentences[item] = self.tokenizer(self.sentences[item],
                add_special_tokens=True,
                truncation=True,
                max_length=self.max_length,
                return_special_tokens_mask=True)
        return self.sentences[item]
    def __len__(self):
        return len(self.sentences)

让我们对适应的数据集进行分词,如下所示:

train_dataset = TokenizedSentencesDataset(train_sentences, tokenizer, max_length)
dev_dataset = TokenizedSentencesDataset(dev_sentences, tokenizer, max_length)

现在,我们使用 Trainer 类来准备运行环境。我们将 mlm_prob = 0.25 传递给 DataCollator 对象,如下所示:

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True, mlm_probability=mlm_prob)
training_args = TrainingArguments(
    num_train_epochs=num_train_epochs,
    evaluation_strategy="steps",
    per_device_train_batch_size=batch_size,
    prediction_loss_only=True,
    fp16=True)
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
    eval_dataset=dev_dataset)

让我们运行适应过程:

trainer.train()

现在,我们可以看到训练过程的输出如下:

image.png

我们可以在前面的图中看到仅显示了3个训练epoch和验证损失,但在实际运行中你会看到15个epoch。根据你的GPU容量和数据集的大小,这个过程可能会花费较长时间。

干得好!我们将保存适应后的模型以便稍后使用,代码如下:

adapted_model_path = "adapted-bert"
model.save_pretrained(adapted_model_path)
tokenizer.save_pretrained(adapted_model_path)

现在,我们有两个检查点,如下所示:

  • bert-base-uncased # (原始BERT)
  • adapted-bert # (适应后的BERT)

在以下代码中,我们注释掉原始BERT,并首先微调 adapted-bert,然后通过交换注释微调 vanilla bert 检查点:

model_path = "adapted-bert" # 1) 适应后的模型
#model_path = "bert-base-uncased" # 2) 原始BERT
model = BertForSequenceClassification.from_pretrained(
    model_path,
    id2label={0: "NEG", 1: "POS"},
    label2id={"NEG": 0, "POS": 1}
)

在接下来的步骤中,我们以与“通过数据增强提高性能”部分相同的方式创建 TrainingArgumentsTrainer 对象。让我们以简略的方式展示微调阶段的代码:

training_args = TrainingArguments(...)
trainer = Trainer(…)
trainer.train()

让我们比较 vanilla bertadapted-bert 的微调性能。以下输出是 vanilla bert 的微调结果:

image.png

以下是适应后训练的输出结果:

image.png

正如图中所示,在所有指标中,适应后的模型略优于 bert-base(原始)模型。我们以一种简单直接的方式解决了适应性问题。当然,还有许多方法可以增强适应过程。

现在,让我们列出一些关于自适应学习的观察和提示,供你参考:

  1. 注意灾难性遗忘:这种现象可能会导致训练模型权重中大量信息的丢失。因此,如果遇到这种情况,建议将学习率设置得更低,并仔细监控验证损失值。如果不注意这些因素,可能会妨碍我们当前所追求的改进。
  2. 顺序特性:我们当前的迁移学习策略具有顺序特性。因此,我们可以根据需要多次将这个训练步骤添加到流程中。
  3. 领域和任务适应:我们已经使用领域特定文本进行了无监督领域自适应训练,然后使用特定任务目标进行了任务自适应训练。在最后的微调之前,你可以添加一些与你的任务相似的任务。比如,我们想要解决医疗领域的特定问答(QA)问题。我们可以使用通用数据集,例如PudMed原始文本、类似SquAD的医生-患者对话数据集以及我们自己的医学问答任务。首先,我们使用无监督的PudMed数据集适应模型,然后用有监督的医生-患者数据集训练模型,最后训练我们的QA任务。这种类型的顺序训练(领域适应和任务适应)会提高模型性能的成功率。
  4. 序列标注任务的例子:例如,我们想解决一个序列标注任务,比如在文本中检测敏感信息(如ID、银行信息等)。已有许多检查点用于命名实体识别(NER)任务,这是一种流行的序列标注任务。我们可以直接使用那些已经训练过的NER检查点,而不是 bert-base。这是顺序迁移学习方法带来的重要优势。

在前几节中,我们大多选择了默认参数。在下一节中,我们将探索是否可以使用更有效的选择策略。

使用HPO优化参数

超参数是深度学习模型在训练期间无法学习的参数。因此,我们需要找到一种方法来指定它们。通常的第一种方式是遵循文献中的常见做法。例如,微调BERT模型的典型epoch值约为3。一种策略是手动调整这些参数,同时监控模型性能,特别是监控验证损失。

此外,还可以通过某些算法系统地确定最优的参数集。一种方法是通过在预定义的超参数集上进行搜索来探索所有可能性,这种朴素的过程称为网格搜索(grid search)。然而,类似网格搜索的模型并不总是实用的,因为训练单个Transformer模型是一个耗时的过程。另一种选择是使用随机搜索。它通过特定的随机化实现了与网格搜索相似的结果,同时减少了网格搜索的复杂性。这种方法随机采样搜索空间,直到满足停止条件为止。

HPO(超参数优化)有许多现代算法,例如网格搜索、贝叶斯优化、粒子群优化。树结构的Parzen估计器(TPE)是一种贝叶斯优化算法,它使用概率模型来评估不同超参数的性能。具体来说,它基于历史测量数据逐步构建模型,然后选择新的超参数。有关详细信息,请参见以下论文:Bergstra, James, et al., Algorithms for hyper-parameter optimization, Advances in neural information processing systems 24 (2011)

现在,我们将使用Optuna框架(Optuna GitHub)实现HPO。Optuna是一个为HPO设计的框架,包含多种优化方法。我们将使用TPE算法,这是默认选项。

我们首先安装必要的包:

pip install datasets transformers optuna

加载相同的IMDb数据集,我们选择2K作为训练集,1K作为测试集,1K作为验证集,如下所示:

from datasets import load_dataset
imdb_train = load_dataset('imdb', split="train[:1000]+train[-1000:]")
imdb_test = load_dataset('imdb', split="test[:500]+test[-500:]")
imdb_val = load_dataset('imdb', split="test[500:1000]+test[-1000:-500]")
imdb_train.shape, imdb_test.shape, imdb_val.shape

以下函数来自本章前几节(通过数据增强提高性能和使模型适应领域):

def compute_metrics(pred):
def tokenize_it(e):

由于我们优化了 bert-base-uncased 模型,我们根据它对数据集进行分词。我们已经熟悉以下代码:

from transformers import (BertTokenizerFast, BertForSequenceClassification)
model_path = 'bert-base-uncased'
tokenizer = BertTokenizerFast.from_pretrained(model_path)
enc_train = imdb_train.map(tokenize_it, batched=True, batch_size=1000)
enc_test = imdb_test.map(tokenize_it, batched=True, batch_size=1000)
enc_val = imdb_val.map(tokenize_it, batched=True, batch_size=1000)

我们可以优化许多超参数。Transformer模型最重要的超参数如下:

  • 学习率
  • 训练的epoch数
  • 批量大小
  • 优化算法
  • 调度器类型

为了简单起见,我们只将学习率和批量大小作为示例参数来演示HPO应用。

我们将使用一个 trial 对象来创建我们的超参数采样。我们可以利用以下Optuna功能:

  • 类别型:optuna.trial.Trial.suggest_categorical()
  • 整数型:optuna.trial.Trial.suggest_int()
  • 浮点型:optuna.trial.Trial.suggest_float()

我们将在范围 [1e-6 – 1e-4] 内搜索学习率,这意味着可能的值是 1e-61e-51e-4。如果选择 log=False(默认),因为它是在线性域中搜索,你需要指定一个离散化的步长。对于批量大小,我们提供一个类别型列表而不是范围,如下所示:

def hp_space(trial):
    hp = {
        "learning_rate": trial.suggest_float("learning_rate", 1e-6, 1e-4, log=True),
        "per_device_train_batch_size": trial.suggest_categorical("per_device_train_batch_size", [8, 16]),
    }
    return hp

在代码的最后,返回的对象 hp 是用于搜索空间的字典类型。我们的 Trainer 对象将使用它进行优化。

从现在开始,以下代码片段将与前几节(通过数据增强提高性能和使模型适应领域)中的相同:

training_args = TrainingArguments(...
trainer = Trainer(...

我们将以不同的方式运行 trainer。不再调用 trainer.train(),而是调用 trainer.hyperparameter_search(),如下所示:

best_run = trainer.hyperparameter_search(
    direction="maximize",
    hp_space=hp_space
)

通过 direction 参数,我们目标是最大化 compute_metric() 函数。当你运行以下代码时,你会看到微调过程重复了多次。最终,Optuna将为我们提供优化的参数。当你通过添加更多待优化参数并增加搜索空间来重复实验时,整个过程将花费更长的时间。让我们查看 best_run 来看看Optuna优化了什么,如下所示:

best_run
# BestRun(run_id='11', objective=3.63, hyperparameters={
# 'learning_rate': 2.25e-05,
# 'per_device_train_batch_size': 16})

优化后的学习率和批量大小分别为 2.25e-0516。请记住,我们在另一个实验中选择的学习率默认值是 5e-5,批量大小是 16。让我们比较它们在任务性能方面的表现。当我们用默认参数和优化后的参数训练模型时,我们将得到以下性能表格。

不使用HPO,并将学习率默认设置为 5e-05 时,我们得到如下输出:

image.png

使用HPO并将学习率设置为2.25e-05时,我们得到如下输出:

image.png

当我们查看测试数据的结果时,可以发现通过HPO进行的实验略优于使用默认训练的结果,差异非常小。所有数值略微提高了约0.2(例如,F1分数从90.09提高到90.28)。尽管差异非常微小,但仍需进行统计测试以确认其显著性。例如,可以使用McNemar检验、t检验或5×2交叉验证来比较两个机器学习模型。然而,我们的HPO实验保持在非常简单的水平。如果我们进行更详细的研究,可能会在统计显著性方面取得更好的结果。

干得好!到目前为止,我们从三个不同的角度尝试改进模型,并且在所有三个方面都取得了成功。我们尽可能保持实验简单,但不要忘记,进行更全面的研究将是有益的。测试和分析不同的数据增强技术、提高领域适应中的数据质量、尝试各种检查点以及扩大HPO的搜索空间都是有用的。

总结

在本章中,我们探讨了提高模型性能的技术。我们的重点是数据增强、领域适应和HPO,这些方法都有助于改进分类任务的表现。然而,我们并不局限于这些方法。文献中还有许多提高模型性能的方法可供参考。在本章中,我们只介绍了最常见的方法。到目前为止,我们在微调时更新了整个模型参数。

在即将到来的章节中,我们将讨论如何通过特殊的参数高效技术来提高模型微调的参数效率,以减少时间复杂度。