一.第一章 欢迎来到transformer的世界
1.解码器-编码器框架
2.注意力机制
3.NLP的迁移学习
4.Hugging Face Transformers库:提供规范化接口
5.Transformer应用概览
二.第二章,文本分类
现在想象一下,你是一名数据科学家,需要构建一个系统,可以自动 识别人们在Twitter上对你公司产品表达的情感状态,例如愤怒或喜 悦。在本章中,我们将使用一种名为DistilBERT 的BERT变体来解决 这个任务。该模型的主要优点是,在实现与BERT相当的性能的同时, 体积更小、效率更高。这使我们能够在几分钟内训练一个分类器,如 果你想训练一个更大的BERT模型,则只需更改预训练模型的 checkpoint。checkpoint对应于加载到给定Transformer架构中的权重 集。
1.数据集
3.训练文本分类器
如第1章所述,像DistilBERT这样的模型被预训练用于预测文本序列中 的掩码单词。然而,这些语言模型不能直接用于文本分类,我们需要 稍微修改它们。为了理解需要做哪些修改,我们来看一下基于编码器 的模型(如DistilBERT)的架构,如图2-4所示。
在DistilBERT的情况下,它在猜测掩码词元。
首先,文本会被词元化并表示为称为词元编码的独热向量。词元编码 的维度由词元分析器词表的大小决定,通常包括两万到二十万个唯一 性词元。接下来,这些词元编码会被转换为词元嵌入,即存在于低维 空间中的向量。然后,这些词元嵌入会通过编码器块层传递,以产生 每个输入词元的隐藏状态。对于语言建模的预训练目标 ,每个隐藏 状态都被馈送到一个层,该层预测掩码输入词元。对于分类任务,我 们将语言建模层替换为分类层。
实际上,PyTorch在实现中跳过了为词元编码创建独热向量的步骤,因 为将矩阵与独热向量相乘等同于从矩阵中选择一列。这可以通过直接 从矩阵中获取词元ID对应的列来完成。当我们使用nn.Embedding类 时,我们将在第3章中看到这一点。
我们有以下两种选择来基于Twitter数据集进行模型训练:
特征提取
我们将隐藏状态用作特征,只需训练分类器,而无须修改预训练模 型。
微调
我们对整个模型进行端到端的训练,这样还会更新预训练模型的参 数。 接下来我们将讲述基于DistilBERT的以上两种选择,以及这两种选择 的权衡取舍。
使用Transformer作为特征提取器
使用Transformer作为特征提取器相当简单。如图2-5所示,我们在训 练期间冻结主体的权重,并将隐藏状态用作分类器的特征。这种方法 的优点是,我们可以快速训练一个小型或浅层模型。这样的模型可以 是神经分类层或不依赖于梯度的方法,例如随机森林。这种方法特别 适用于没有GPU的场景,因为隐藏状态只需要预计算一次。
加载预训练模型
我们将使用Hugging Face Transformers库中另一个很方便的自动类 AutoModel。与AutoTokenizer类似,AutoModel具有 from_pretrained()方法,可用于加载预训练模型的权重。现在我们使 用该方法来加载DistilBERT checkpoint:
这里我们使用PyTorch来检查GPU是否可用(即代码 torch.cuda.is available()),然后将PyTorch的nn.Module.to()方 法与模型加载器链接起来(即代码to(device))。这确保了如果有 GPU,模型将在GPU上运行。如果没有,模型将在CPU上运行,不过这样 可能会慢很多。
AutoModel类将词元编码转换为嵌入向量,然后将它们馈送到编码器栈 中以返回隐藏状态。我们看一下如何从语料库中提取这些状态。
提取最终隐藏状态
作为预热,我们检索一个字符串的最终隐藏状态。我们需要做的第一 件事是对字符串进行编码并将词元转换为PyTorch张量。可以通过向词 元分析器提供return_tensors=”pt”参数来实现。具体如下:
我们可以看到,生成的张量形状为[batch size,n_tokens]。现在我 们已经将编码作为张量获取,最后一步是将它们放置在与模型相同的 设备上,并按以下方式传输入:
这里我们使用了torch.no_grad()上下文管理器来禁用梯度的自动计 算。这对推理很有用,因为它减少了计算的内存占用。根据模型配 置,输出可以包含多个对象,例如隐藏状态、损失或注意力,这些对 象以类似于Python中的namedtuple的形式排列。在我们的示例中,模 型输出是一个BaseModelOutput实例,并且包含了其属性名,我们可以 通过这些属性名来获取其详情。我们看到,我们的模型只有一个属 性,即last_hidden_state(最终隐藏状态)
现在我们知道如何针对单个字符串获取最终隐藏状态。我们通过创建 一个新的hidden_state列来对整个数据集执行相同的操作,以存储所 有这些向量。就像我们在词元分析器中所做的那样,我们将使用 DatasetDict的map()方法一次性提取所有隐藏状态。我们需要做的第 一件事是将先前的步骤封装在一个处理函数中:
这个函数和我们之前的逻辑的唯一不同在于最后一步,即将最终的隐 藏状态作为NumPy数组放回CPU。当我们使用批量输入时,map()方法要 求处理函数返回Python或NumPy对象。 由于我们的模型期望输入张量,下一步需要将input_ids和 attention_mask列转换为torch格式,具体如下:
现在我们已经得到了与每个推文相关联的隐藏状态,下一步是基于它 们训练一个分类器。为了做到这一点,我们需要一个特征矩阵,我们 来看一下。
创建特征矩阵
可视化训练集
由于在768维度中可视化隐藏状态是个艰难的任务,因此我们将使用强 大的UMAP算法将向量投影到2D平面上 。由于UMAP在特征缩放到[0, 1]区间内时效果最佳,因此我们将首先应用一个MinMaxScaler,然后 使用umap-learn库的UMAP实现来缩放隐藏状态:
训练一个简单的分类器
我们已经看到,不同情感的隐藏状态是不同的,尽管其中一些情感并 没有明显的界限。现在让我们使用这些隐藏状态来训练一个逻辑回归 模型(使用Scikit-learn)。训练这样一个简单的模型速度很快,而 且不需要GPU:
从准确率上看,我们的模型似乎只比随机模型稍微好一点,但由于我 们处理的是一个不平衡的多分类数据集,它实际上会显著地表现更 好。我们可以通过将其与简单基准进行比较来检查我们的模型是否良 好。在Scikit-learn中,有一个DummyClassifier可以用于构建具有简 单启发式的分类器,例如始终选择多数类或始终选择随机类。在这种 情况下,表现最佳的启发式是始终选择最常见的类,这会产生约35%的 准确率:
因此,使用DistilBERT嵌入的简单分类器明显优于我们的基线。我们 可以通过查看分类器的混淆矩阵来进一步研究模型的性能,该矩阵告 诉我们真实标注和预测标注之间的关系:
这里我们可以看到,anger和fear最常与sadness混淆,这与我们可视 化嵌入时所观察到的一致。此外,love和surprise经常与joy混淆。 接下来我们将探究微调方法,这种方法可以带来更好的分类效果。但 是,重要的是要注意,微调需要更多的计算资源,比如GPU,而你的组 织可能没有GPU。在这种情况下,基于特征的方法可以是传统机器学习 和深度学习之间的一个很好的折中方案。
微调Transformer模型
现在我们探讨如何进行端到端的Transformer模型微调。在使用微调方 法时,我们不使用隐藏状态作为固定特征,而是如图2-6所示那样进行 训练。这要求分类头是可微的,这就是为什么这种方法通常使用神经 网络进行分类。
训练用作分类模型输入的隐藏状态将有助于我们避免使用可能不适合 分类任务的数据的问题。相反,初始隐藏状态在训练过程中适配,以 降低模型损失并提高其性能。
加载预训练模型
定义性能指标
为了在训练期间监控指标,我们需要为Trainer定义一个 compute_metrics()函数。该函数接收一个EvalPrediction对象(这是 一个具有predictions和label_ids属性的命名元组),并需要返回一 个将每个指标名称映射到其值的字典。对于我们的应用,我们将计算 模型的F1分数和准确率:
有了数据集和度量指标后,在定义Trainer类之前,我们只需要处理最 后两件事情: 1.登录我们的Hugging Face Hub账户。从而让我们能够将我们的微 调模型推送到Hub上,并与社区分享它。 2.定义训练运行的所有超参数。
登录hugging-face
from huggingface_hub import notebook_login
notebook_login()
训练模型
我们将使用TrainingArguments类来定义训练参数。此类存储了大量信 息,从而为训练和评估提供细粒度的控制。最重要的参数是 output_dir,它是存储训练过程中所有工件的位置。以下是 TrainingArguments的完整示例:
我们可以看到我们的模型在验证集上的F1分数约为92%,这比基于特征 的方法有了显著的提升! 我们可以通过计算混淆矩阵来更详细地查看训练指标。为了可视化混 淆矩阵,我们首先需要获取验证集上的预测结果。Trainer类的predict()方法返回了几个有用的对象,我们可以用它们进行评估
predict()方法的输出是一个PredictionOutput对象,它包含了 predictions和label_ids的数组,以及我们传给训练器的度量指标。 我们可以通过以下方式访问验证集上的度量指标:
它还包含了每个类别的原始预测值。我们可以使用np.argmax()进行贪 婪解码预测,然后会得到预测标注,并且结果格式与前面的基于特征 的方法相同,以便我们进行比较:
我们可以基于这个预测结果再次绘制混淆矩阵:
可见,与前面的基于特征的方法相比,微调方法的结果更接近于理想 的对角线混淆矩阵。love类别仍然经常与joy混淆,这点逻辑上也讲得 过去。surprise也经常被错误地识别为joy,或者与fear混淆。总体而 言,模型的性能似乎非常不错,但在我们结束之前,让我们深入了解 模型可能会犯的错误的类型。
储存和共享模型
下载自己的模型
使用自己的模型预测
4.本章小结
祝贺你,现在你知道了如何训练一个用于分类推文情感的Transformer
模型!我们已经讨论了基于特征和微调的两种互补方法,并研究了它
们的优势和劣势。
然而,这只是使用Transformer模型构建实际应用程序的第一步,我们
还有很多工作要做。以下是在NLP旅程中可能遇到的挑战清单:
我的老板希望我的模型昨天就上线了!
在大多数应用程序中,你并不希望你的模型闲置,你希望将它用于预
测!当模型被推送到Hub时,会自动创建一个推理端点,可以使用HTTP
请求调用它。如果你想了解更多信息,建议查看推理API的文档
(https://oreil.ly/XACF5)。
我的用户需要更快的预测!
我们已经讨论了解决此问题的一种方法:使用DistilBERT。在第8章
中,我们将深入研究知识蒸馏(DistilBERT创建的过程)以及其他加
速Transformer模型的技巧。
你的模型是否也可以执行X任务?
正如我们在本章中所提到的,Transformer模型非常多才多艺。在本书
的其余部分中,我们将使用相同的基本架构探索一系列任务,例如问
答和命名实体识别。
我的文本不是英语!
事实证明,Transformer模型能够支持多语言,在第4章中,我们将使
用它们同时处理多种语言。
我没有标注数据!
如果可用的标注数据非常少,则可能不能进行微调。在第9章中,我们
将探讨一些应对这种情况的技术。
现在我们已经了解了如何训练和共享Transforme。在第3章中,我们将
探索如何从头开始实现我们自己的Transformer模型。