irpas技术客

【NLP】Transformer—用注意力机制改进自然语言处理_Sonhhxg_柒_transformer在nlp中的应用

大大的周 5992

?

???🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客?📃

🎁欢迎各位→点赞👍 + 收藏?? + 留言📝?

📣系列专栏 - 机器学习【ML】?自然语言处理【NLP】? 深度学习【DL】

?

?🖍foreword

?说明?本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

在上一章中,我们通过情感分析项目了解了循环神经网络(?RNN?) 及其在自然语言处理(?NLP ) 中的应用。然而,最近出现了一种新的架构,该架构已被证明优于多个 NLP 任务中基于 RNN 的序列到序列(?seq2seq?) 模型。这就是所谓的变压器架构。

变形金刚已经彻底改变了自然语言处理,并处于许多令人印象深刻的应用程序的前沿,包括自动语言翻译 (?https://ai.googleblog.com/2020/06/recent-advances-in-google-translate.html?) 和建模基础蛋白质序列 (?https://·/rasbt/" ... "machine-learning-book/raw/" ... "main/ch08/movie_data.csv.gz") >>> filename = url.split("/")[-1] >>> with open(filename, "wb") as f: ... r = requests.get(url) ... f.write(r.content) >>> with gzip.open('movie_data.csv.gz', 'rb') as f_in: ... with open('movie_data.csv', 'wb') as f_out: ... shutil.copyfileobj(f_in, f_out)

如果你的硬盘上还有第 8 章movie_data.csv的文件,你可以跳过这个下载和解压过程。

接下来,我们将数据加载到pandasDataFrame并确保它看起来没问题:

>>> df = pd.read_csv('movie_data.csv') >>> df.head(3)

图 16.17:IMDb 影评数据集的前三行

下一步是将数据集拆分为单独的训练、验证和测试集。在这里,我们将 70% 的评论用于训练集,10% 用于验证集,剩下的 20% 用于测试:

>>> train_texts = df.iloc[:35000]['review'].values >>> train_labels = df.iloc[:35000]['sentiment'].values >>> valid_texts = df.iloc[35000:40000]['review'].values >>> valid_labels = df.iloc[35000:40000]['sentiment'].values >>> test_texts = df.iloc[40000:]['review'].values >>> test_labels = df.iloc[40000:]['sentiment'].values 标记数据集

至此,我们获得了用于训练、验证和测试集的文本和标签。现在,我们将使用从预训练模型类继承的标记器实现将文本标记为单个单词标记:

>>> tokenizer = DistilBertTokenizerFast.from_pretrained( ... 'distilbert-base-uncased' ... ) >>> train_encodings = tokenizer(list(train_texts), truncation=True, padding=True) >>> valid_encodings = tokenizer(list(valid_texts), truncation=True, padding=True) >>> test_encodings = tokenizer(list(test_texts), truncation=True, padding=True)

选择不同的分词器

如果您对应用不同类型的分词器感兴趣,请随意探索包tokenizers(?Tokenizers?),它也是由 Hugging Face 构建和维护的。然而,继承的分词器保持了预训练模型和数据集之间的一致性,这为我们节省了寻找模型对应的特定分词器的额外工作。换句话说,如果您想微调预训练模型,推荐使用继承的标记器。

最后,让我们将所有内容打包到一个名为IMDbDataset并创建相应数据加载器的类中。这样一个自定义的数据集类让我们可以为我们的自定义电影评论数据集定制所有相关的特性和功能,DataFrame格式为:

>>> class IMDbDataset(torch.utils.data.Dataset): ... def __init__(self, encodings, labels): ... self.encodings = encodings ... self.labels = labels >>> def __getitem__(self, idx): ... item = {key: torch.tensor(val[idx]) ... for key, val in self.encodings.items()} ... item['labels'] = torch.tensor(self.labels[idx]) ... return item >>> def __len__(self): ... return len(self.labels) >>> train_dataset = IMDbDataset(train_encodings, train_labels) >>> valid_dataset = IMDbDataset(valid_encodings, valid_labels) >>> test_dataset = IMDbDataset(test_encodings, test_labels) >>> train_loader = torch.utils.data.DataLoader( ... train_dataset, batch_size=16, shuffle=True) >>> valid_loader = torch.utils.data.DataLoader( ... valid_dataset, batch_size=16, shuffle=False) >>> test_loader = torch.utils.data.DataLoader( ... test_dataset, batch_size=16, shuffle=False)

虽然从前面的章节中应该对整体数据加载器设置很熟悉,但一个值得注意的细节是方法中的item变量__getitem__。我们之前生成的编码存储了大量关于标记化文本的信息。通过字典我们用来将字典分配给item变量的理解,我们只提取最相关的信息。例如,生成的字典条目包括input_ids(与标记对应的词汇表中的唯一整数)、labels(类标签)和attention_mask。这里,attention_mask是一个具有二进制值(0 和 1)的张量,表示模型应该关注哪些标记。特别是,0 对应于用于将序列填充为相等长度的标记,并且被模型忽略;1 对应于实际的文本标记。

加载和微调预训练的 BERT 模型

照顾了数据准备,在本小节中,您将看到如何加载预训练的 DistilBERT 模型并使用我们刚刚创建的数据集对其进行微调。加载预训练模型的代码如下:

>>> model = DistilBertForSequenceClassification.from_pretrained( ... 'distilbert-base-uncased') >>> model.to(DEVICE) >>> model.train() >>> optim = torch.optim.Adam(model.parameters(), lr=5e-5)

DistilBertForSequenceClassification指定我们想要微调模型的下游任务,在这种情况下是序列分类。如前所述,'distilbert-base-uncased'它是 BERT 无外壳基础模型的轻量级版本,具有可管理的大小和良好的性能。请注意,“未区分大小写”表示模型不区分大小写字母。

使用其他预训练的转换器

变压器包_还提供了许多其他预训练模型和各种下游任务进行微调。在https://huggingface.co/transformers/上查看它们。

现在,是时候训练了该模型。我们可以把它分成两部分。首先,我们需要定义一个准确度函数来评估模型的性能。请注意,此精度函数计算常规分类精度。为什么这么冗长?在这里,我们将逐批加载数据集,以解决使用大型深度学习模型时 RAM 或 GPU 内存 (VRAM) 的限制:

>>> def compute_accuracy(model, data_loader, device): ... with torch.no_grad(): ... correct_pred, num_examples = 0, 0 ... for batch_idx, batch in enumerate(data_loader): ... ### Prepare data ... input_ids = batch['input_ids'].to(device) ... attention_mask = \ ... batch['attention_mask'].to(device) ... labels = batch['labels'].to(device) ... outputs = model(input_ids, ... attention_mask=attention_mask) ... logits = outputs['logits'] ... predicted_labels = torch.argmax(logits, 1) ... num_examples += labels.size(0) ... correct_pred += \ ... (predicted_labels == labels).sum() ... return correct_pred.float()/num_examples * 100

在compute_accuracy函数中,我们加载给定的批次,然后从输出中获取预测标签。在执行此操作时,我们通过 跟踪示例总数num_examples。correct_pred同样,我们通过变量跟踪正确预测的数量。最后,在我们遍历整个数据集之后,我们将准确率计算为正确预测标签的比例。

总体而言,通过该compute_accuracy功能,您已经可以一瞥我们如何使用变压器模型来获得类标签。也就是说,我们向模型input_ids提供attention_mask信息以及在此表示标记是实际文本标记还是用于将序列填充为相等长度的标记的信息。然后model调用返回输出,这是一个特定于转换器库的SequenceClassifierOutput对象。然后,我们从这个对象中获取我们通过函数转换为类标签的 logits,argmax就像我们在前几章中所做的那样。

最后,让我们进入主要部分:训练(或者更确切地说,微调)循环。您会注意到,从转换器库中微调模型与在纯 PyTorch 中从头开始训练模型非常相似:

>>> start_time = time.time() >>> for epoch in range(NUM_EPOCHS): ... model.train() ... for batch_idx, batch in enumerate(train_loader): ... ### Prepare data ... input_ids = batch['input_ids'].to(DEVICE) ... attention_mask = batch['attention_mask'].to(DEVICE) ... labels = batch['labels'].to(DEVICE) ... ### Forward pass ... outputs = model(input_ids, ... attention_mask=attention_mask, ... labels=labels) ... loss, logits = outputs['loss'], outputs['logits'] ... ### Backward pass ... optim.zero_grad() ... loss.backward() ... optim.step() ... ### Logging ... if not batch_idx % 250: ... print(f'Epoch: {epoch+1:04d}/{NUM_EPOCHS:04d}' ... f' | Batch' ... f'{batch_idx:04d}/' ... f'{len(train_loader):04d} | ' ... f'Loss: {loss:.4f}') ... model.eval() ... with torch.set_grad_enabled(False): ... print(f'Training accuracy: ' ... f'{compute_accuracy(model, train_loader, DEVICE):.2f}%' ... f'\nValid accuracy: ' ... f'{compute_accuracy(model, valid_loader, DEVICE):.2f}%') ... print(f'Time elapsed: {(time.time() - start_time)/60:.2f} min') ... print(f'Total Training Time: {(time.time() - start_time)/60:.2f} min') ... print(f'Test accuracy: {compute_accuracy(model, test_loader, DEVICE):.2f}%')

产生的输出通过前面的代码如下(注意代码不是完全确定的,这就是为什么你得到的结果可能会略有不同):

Epoch: 0001/0003 | Batch 0000/2188 | Loss: 0.6771 Epoch: 0001/0003 | Batch 0250/2188 | Loss: 0.3006 Epoch: 0001/0003 | Batch 0500/2188 | Loss: 0.3678 Epoch: 0001/0003 | Batch 0750/2188 | Loss: 0.1487 Epoch: 0001/0003 | Batch 1000/2188 | Loss: 0.6674 Epoch: 0001/0003 | Batch 1250/2188 | Loss: 0.3264 Epoch: 0001/0003 | Batch 1500/2188 | Loss: 0.4358 Epoch: 0001/0003 | Batch 1750/2188 | Loss: 0.2579 Epoch: 0001/0003 | Batch 2000/2188 | Loss: 0.2474 Training accuracy: 96.32% Valid accuracy: 92.34% Time elapsed: 20.67 min Epoch: 0002/0003 | Batch 0000/2188 | Loss: 0.0850 Epoch: 0002/0003 | Batch 0250/2188 | Loss: 0.3433 Epoch: 0002/0003 | Batch 0500/2188 | Loss: 0.0793 Epoch: 0002/0003 | Batch 0750/2188 | Loss: 0.0061 Epoch: 0002/0003 | Batch 1000/2188 | Loss: 0.1536 Epoch: 0002/0003 | Batch 1250/2188 | Loss: 0.0816 Epoch: 0002/0003 | Batch 1500/2188 | Loss: 0.0786 Epoch: 0002/0003 | Batch 1750/2188 | Loss: 0.1395 Epoch: 0002/0003 | Batch 2000/2188 | Loss: 0.0344 Training accuracy: 98.35% Valid accuracy: 92.46% Time elapsed: 41.41 min Epoch: 0003/0003 | Batch 0000/2188 | Loss: 0.0403 Epoch: 0003/0003 | Batch 0250/2188 | Loss: 0.0036 Epoch: 0003/0003 | Batch 0500/2188 | Loss: 0.0156 Epoch: 0003/0003 | Batch 0750/2188 | Loss: 0.0114 Epoch: 0003/0003 | Batch 1000/2188 | Loss: 0.1227 Epoch: 0003/0003 | Batch 1250/2188 | Loss: 0.0125 Epoch: 0003/0003 | Batch 1500/2188 | Loss: 0.0074 Epoch: 0003/0003 | Batch 1750/2188 | Loss: 0.0202 Epoch: 0003/0003 | Batch 2000/2188 | Loss: 0.0746 Training accuracy: 99.08% Valid accuracy: 91.84% Time elapsed: 62.15 min Total Training Time: 62.15 min Test accuracy: 92.50%

在这段代码中,我们迭代多个时代。在每个时期,我们执行以下步骤:

将输入加载到我们正在处理的设备(GPU 或 CPU)中计算模型输出和损失通过反向传播损失调整权重参数在训练集和验证集上评估模型性能

请注意,训练时间可能因不同设备而异。在三个 epoch 之后,测试数据集的准确率达到了 93% 左右,与第 15 章RNN 实现的 85% 的测试准确率相比,这是一个巨大的进步。

使用 Trainer API 更方便地微调转换器

在上一个在小节中,我们在 PyTorch 中手动实现了训练循环,以说明微调变压器模型与从头开始训练 RNN 或 CNN 模型并没有太大区别。但是,请注意,该transformers库包含几个不错的额外功能以提供额外的便利,例如我们将在本小节中介绍的 Trainer API。

Hugging Face 提供的 Trainer API 针对 Transformer 模型进行了优化,具有广泛的训练选项和各种内置功能。使用 Trainer API 时,我们可以跳过自己编写训练循环的工作,训练或微调 Transformer 模型就像调用函数(或方法)一样简单。让我们看看这在实践中是如何工作的。

通过加载预训练模型后

>>> model = DistilBertForSequenceClassification.from_pretrained( ... 'distilbert-base-uncased') >>> model.to(DEVICE) >>> model.train();

然后可以用以下代码替换上一节中的训练循环:

>>> optim = torch.optim.Adam(model.parameters(), lr=5e-5) >>> from transformers import Trainer, TrainingArguments >>> training_args = TrainingArguments( ... output_dir='./results', ... num_train_epochs=3, ... per_device_train_batch_size=16, ... per_device_eval_batch_size=16, ... logging_dir='./logs', ... logging_steps=10, ... ) >>> trainer = Trainer( ... model=model, ... args=training_args, ... train_dataset=train_dataset, ... optimizers=(optim, None) # optim and learning rate scheduler ... )

在前面的代码片段中,我们首先定义了训练参数,它们是关于输入和输出位置、时期数和批量大小的相对不言自明的设置。我们试图使设置尽可能简单;但是,还有许多其他设置可用,我们建议您查阅TrainingArguments文档页面以获取更多详细信息:https?://huggingface.co/transformers/main_classes/trainer.html#trainingarguments 。

然后我们将这些TrainingArguments设置传递给Trainer类以实例化一个新trainer对象。在trainer使用设置、要微调的模型以及训练和评估集启动之后,我们可以通过调用该trainer.train()方法来训练模型(稍后我们将进一步使用该方法)。就是这样,使用 Trainer API 就像前面的代码一样简单,不需要更多的样板代码。

然而,你可能已经注意到这些代码片段中没有涉及测试数据集,并且我们没有在本小节中指定任何评估指标。这是因为 Trainer API 默认只显示训练损失,不提供模型评估。有两种方法可以显示最终模型的性能,我们将在下面进行说明。

评估最终模型的第一种方法是将评估函数定义compute_metrics为另一个Trainer实例的参数。该compute_metrics函数对模型的测试预测作为 logits(这是模型的默认输出)和测试标签进行操作。要实例化这个函数,我们建议datasets通过安装 Hugging Face 的库pip install datasets并使用它,如下所示:

>>> from datasets import load_metric >>> import numpy as np >>> metric = load_metric("accuracy") >>> def compute_metrics(eval_pred): ... logits, labels = eval_pred ... # note: logits are a numpy array, not a pytorch tensor ... predictions = np.argmax(logits, axis=-1) ... return metric.compute( ... predictions=predictions, references=labels)

更新后的Trainer实例化(现在包括compute_metrics)如下:

>>> trainer=Trainer( ... model=model, ... args=training_args, ... train_dataset=train_dataset, ... eval_dataset=test_dataset, ... compute_metrics=compute_metrics, ... optimizers=(optim, None) # optim and learning rate scheduler ... )

现在,让我们训练模型(再次注意,代码不是完全确定的,这就是为什么您可能会得到略有不同的结果):

>>> start_time = time.time() >>> trainer.train() ***** Running training ***** Num examples = 35000 Num Epochs = 3 Instantaneous batch size per device = 16 Total train batch size (w. parallel, distributed & accumulation) = 16 Gradient Accumulation steps = 1 Total optimization steps = 6564 Step Training Loss 10 0.705800 20 0.684100 30 0.681500 40 0.591600 50 0.328600 60 0.478300 ... >>> print(f'Total Training Time: ' ... f'{(time.time() - start_time)/60:.2f} min') Total Training Time: 45.36 min

训练完成后(根据您的 GPU 可能需要长达一个小时),我们可以调用trainer.evaluate()以获取测试集上的模型性能:

>>> print(trainer.evaluate()) ***** Running Evaluation ***** Num examples = 10000 Batch size = 16 100%|█████████████████████████████████████████| 625/625 [10:59<00:00, 1.06s/it] {'eval_loss': 0.30534815788269043, 'eval_accuracy': 0.9327, 'eval_runtime': 87.1161, 'eval_samples_per_second': 114.789, 'eval_steps_per_second': 7.174, 'epoch': 3.0}

我们可以看,评估准确率约为 94%,类似于我们自己之前使用的 PyTorch 训练循环。(请注意,我们跳过了训练步骤,因为在model上一次调用之后已经对其进行了微调trainer.train()。)我们的手动训练方法和使用类之间存在小的差异Trainer,因为Trainer类使用了一些不同的设置和一些额外的设置。

我们可以用来计算最终测试集准确度的第二种方法是重用compute_accuracy我们在上一节中定义的函数。我们可以通过运行以下代码直接评估微调模型在测试数据集上的性能:

>>> model.eval() >>> model.to(DEVICE) >>> print(f'Test accuracy: {compute_accuracy(model, test_loader, DEVICE):.2f}%') Test accuracy: 93.27%

事实上,如果你想在训练过程中定期检查模型的性能,你可以通过定义训练参数来要求训练器在每个 epoch 之后打印模型评估,如下所示:

>>> from transformers import TrainingArguments >>> training_args = TrainingArguments("test_trainer", ... evaluation_strategy="epoch", ...)

但是,如果您计划更改或优化超参数并多次重复微调过程,我们建议为此目的使用验证集,以保持测试集的独立性。我们可以通过实例化Trainerusing来实现这一点valid_dataset:

>>> trainer=Trainer( ... model=model, ... args=training_args, ... train_dataset=train_dataset, ... eval_dataset=valid_dataset, ... compute_metrics=compute_metrics, ... )

在本节中,我们看到了我们如何微调 BERT 模型进行分类。这与使用其他深度学习架构(如 RNN)不同,我们通常从头开始训练。然而,除非我们正在进行研究并试图开发新的变压器架构——这是一项非常昂贵的工作——没有必要对变压器模型进行预训练。由于 Transformer 模型是在一般的、未标记的数据集资源上进行训练的,因此我们自己对它们进行预训练可能不会很好地利用我们的时间和资源;微调是要走的路。

概括

在本章中,我们介绍了一种全新的自然语言处理模型架构,即转换器架构。Transformer 架构建立在一个叫做 self-attention 的概念上,我们开始逐步介绍这个概念。首先,我们研究了一个配备注意力的 RNN,以提高其对长句的翻译能力。然后,我们轻轻介绍了 self-attention 的概念,并解释了它是如何在 Transformer 内的 multi-head attention 模块中使用的。

自 2017 年最初的 Transformer 发布以来,Transformer 架构的许多不同衍生产品已经出现和发展。在本章中,我们重点选择了一些最受欢迎的产品:GPT 模型系列、BERT 和 BART。GPT 是一种单向模型,特别擅长生成新文本。BERT 采用双向方法,更适合其他类型的任务,例如分类。最后,BART 结合了 BERT 的双向编码器和 GPT 的单向解码器。感兴趣的读者可以通过以下两篇调查文章了解其他基于变压器的架构:

自然语言处理的预训练模型:邱及其同事的一项调查,2020 年。可在https://arxiv.org/abs/2003.08271AMMUS:Kayan及其同事对自然语言处理中基于 Transformer 的预训练模型的调查,2021 年。可在https://arxiv.org/abs/2108.05542

Transformer 模型通常比 RNN 更需要数据,并且需要大量数据进行预训练。预训练利用大量未标记的数据来构建通用语言模型,然后可以通过在较小的标记数据集上对其进行微调来专门针对特定任务。

为了了解这在实践中是如何工作的,我们从 Hugging Facetransformers库中下载了一个预训练的 BERT 模型,并对其进行了微调,以便在 IMDb 电影评论数据集上进行情感分类。

在下一章中,我们将讨论生成对抗网络。顾名思义,生成对抗网络是可用于生成新数据的模型,类似于我们在本章中讨论的 GPT 模型。然而,我们现在将自然语言建模这个话题抛在脑后,将在计算机视觉和生成新图像的背景下研究生成对抗网络,这些网络最初是为这项任务而设计的。


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #nlp #任务中基于 #RNN # #模型