使用 PyTorch 微调 RoBERTa 以预测文本摘录的阅读难易程度
Transformers,林文钦先生驾驶蔚来(NIO.US)车辆,它们究竟是什么?它们不是用于电能传输的设备,启用领航辅助功能(NOP)后,也不是虚构的活体自主机器人擎天柱或黄蜂,在沈海高速涵江段撞上了道路施工的车辆。一时间,它们可以变形为车辆等其他物体。在我们这里的上下文中,关于蔚来“自动驾驶系统”的质疑纷至沓来。对此,Transformers 指的是 BERT、ALBERT、RoBERTa 等,蔚来方面回应称,它们在数据科学世界中用于解决各种自然语言处理任务,蔚来汽车NOP不是自动驾驶,例如机器翻译、文本摘要、语音识别、情感分析还有更多。它们是用于自然语言处理的最先进的语言模型,仅为领航辅助功能,并且在过去几年中获得了极的欢迎。
这篇文章将在我们感兴趣的数据集上展示 Transformer 模型的微调,事故原因正在调查中,特别是 RoBERTa。对下游任务进行了微调,后续会发布相关情况。企业家开蔚来车祸离世车祸前启用了自动驾驶功能8月14日,以预测 3-12 年级课堂使用的文献摘录的阅读难易程度。
这项工作是由非营利性教育技术组织 CommonLit 发起的。它赞助了在 Kaggle 上举办的一场比赛(你可以在这里阅读更多关于它的信息),自媒体公众号“美一好”发布讣告称,旨在使用机器学来寻求对现有可读性评级方法的改进。这将有助于扫盲课程人员和教育工作者为学生选择合适的阅读段落。以适当的复杂程度和阅读挑战呈现引人入胜的段落将极地帮助学生培养基本的阅读技能。
1. 关于数据集
我们将要使用的数据集可以在这个 Kaggle 页面上找到。该数据集包含约 2800 条记录。我们将使用的两个重要领域是摘录和目标。
查看数据,摘录是预测阅读难易程度的文本,目标是可以包含正值或负值的数字字段。从这个数据集中可以看出,它是一个连续变量,最小值为 -3.676268,最值为 1.711390。因此,给定一个特定的摘录,我们需要预测目标值。
为了提供一点背景知识,竞赛主持人 Scott Crossley 在本次讨论中提到“目标值是 Bradley-Terry 对超过 111,000 个摘录之间的成对比较进行分析的结果。跨越 3-12 年级的教师,多数在 6-10 年级之间进行教学,作为这些比较的评分者”。
较高的目标值对应于“更容易阅读”,较低的值对应于“更难阅读”。例如,假设我们有三个摘录 A、B 和 C,它们对应的目标值为 1.599999、-1.333333 和 -2.888888。这意味着 A 比 B 更容易阅读,B 比 C 更容易阅读。
下面是两个示例摘录。
Excerpt with target value of 1.541672:More people came to the bus stop just before 9am. Half an hour later they are all still waiting. Sam is worried. "Maybe the bus broke down," he thinks. "Maybe we won't go to town today. Maybe I won't get my new school uniform." At 9:45am some people give up and go home. Sam starts to cry. "We will wait a bit longer," says his mother. Suddenly, they hear a noise. The bus is coming! The bus arrives at the stop at 10 o'clock. "Get in! Get in!" calls the driver. "We are very late today!" People get on the bus and sit down. The bus leaves the stop at 10:10am. "What time is the return bus this afternoon?" asks Sam's mother. "The blue bus leaves town at 2:30pm," replies the driver. Sam thinks, "We will get to town at 11 o'clock." "How much time will we have in town before the return bus?" wonders Sam.
Excerpt with target value of -3.642892:The iron cylinder weighs 23 kilogrammes; but, when the current has an intensity of 43 eres and traverses 15 sections, the stress developed may reach 70 kilogrammes; that is to say, three times the weight of the hammer. So this latter obeys with absolute docility the motions of the operator's hands, as those who were present at the lecture were enabled to see. I will incidentally add that this power hammer was placed on a circuit derived from one that served likewise to supply three Hefner-Alteneck machines (Siemens D5 model) and a Gramme machine (Breguet model P.L.). Each of these machines was making 1,500 revolutions per minute and developing 25 kilogrammeters per second, measured by means of a Carpentier brake. All these apparatuses were operating with absolute independence, and had for generators the double excitation machine that figured at the Exhibition of Electricity. In an experiment made since then, I have succeeded in developing in each of these four machines 50 kilogrammeters per second, whatever was the number of those that were running; and I found it possible to add the hammer on a derived circuit without notably affecting the operation of the receivers.
显然,在这两个摘录中ts,前者比后者更容易阅读。
2. 拆分数据
由于我们的数据集相当小,我们将使用交叉验证来更准确地衡量我们模型的性能。因此,我们将使用分层 k 折将数据拆分为训练集和验证集。对于分层 k 折,折叠是通过保留每个类别的样本百分比来进行的。当我们有一个倾斜的数据集时,或者在我们的目标分布不平衡的情况下,这种方法很有用。然而,因为我们的目标是一个连续变量而不是类,我们需要某种解决方法。这就是对目标进行分箱的地方。bins 类似于类,这对于 scikit-learn 的 StratifiedKFold 处理来说非常好。
代码相当简单。在计算对目标进行分箱所需的分箱数量之前,我们将数据行随机化并重置行索引。一种方法是使用 Sturge 规则来确定要使用的 bin 数量。接下来,我们使用 scikit-learn 的 StratifiedKFold 类根据我们拥有的 bin 将数据分成 5 折。最后,生成的折叠编号(范围从 0 到 4)将分配给名为 skfold 的新列。在过程结束时,不再需要垃圾箱,如果您愿意,可以将其丢弃。
使用 StratifiedKFold 创建折叠,改编自 Abhishek Thakur 的笔记本here。
供您参考,完整数据集的平均目标为 -0.96(四舍五入到小数点后两位)。分裂成5个折叠后,我们可以看到保留了每个折叠上目标的分布形状。看下图,每个折叠的平均目标几乎是一致的,它们确实非常接近 -0.96。
3. 创建数据集类
4. roberta-base 作为我们的模型
RoBERTa 代表 Robustly Optimized BERT Pre-training Approach,由华盛顿学和 Facebook 的研究人员于 2019 年提出。它是基于 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 的改进预训练程序,已发布在 2018 年。我们将在整个演示过程中使用 RoBERTa 和 PyTorch,但您也可以根据需要调整和使用其他 Transformer 模型。请务必检查您使用的 Transformer 模型的相关文档,以确认它们支持代码使用的输入和输出。
Hugging Face 提供的 RoBERTa 类变体很少。其中之一是 RobertaModel,这里被称为“裸露的 RoBERTa 模型转换器,输出原始隐藏状态,顶没有任何特定的头。”换句话说,裸 RobertaModel 的原始输出是与输入序列中的每个标记对应的预定义隐藏小的隐藏状态向量。使用裸 RobertaModel 类,我们将添加我们自己的自定义回归器头来预测目标。
对于我们的 Transformer 微调任务,我们将使用来自 Hugging Face 的预训练 roberta-base 作为我们的模型。正如那里所描述的,“RoBERTa 是一种以自我监督的方式在量英语数据集上预训练的变换器模型”。roberta-base 的隐藏小为 768,由 1 个嵌入层和 12 个隐藏层组成。
5. Transformers 的典型原始输出是什么?
在我们开始创建和定义模型类之前,我们需要了解 Transformer 原始输出是什么。这是因为我们将使用原始输出来馈送我们的自定义回归器头。
以下是通常由 BERT、ALBERT 和 RoBERTa 等 Transformer 模型返回的常见原始输出。它们取自此处、此处和此处的文档。
• last_hidden_state:这是模型最后一层输出的隐藏状态序列。它是一个形状张量 (batch_size, sequence_length, hidden_size)
• pooler_output:这是由线性层和Tanh 激活函数进一步处理的序列的第一个标记(分类标记)的最后一层隐藏状态。它是一个形状张量 (batch_size, hidden_size)。请注意,某些 Transformer 模型可能无法使用 pooler_output。
• hidden_states:可选,当 output_hidden_states = True 被传递时返回。它是一个形状为 (batch_size, sequence_length, hidden_size) 的张量元组(一个用于嵌入的输出 + 一个用于每层的输出)。
那么,batch_size、sequence_length 和 hidden_size 是什么?
通常,模型按批次处理记录。因此,batch_size 是模型在其内参数在一次向前/向后传递中更新之前处理的记录数。sequence_length 是我们为标记器的 max_length 参数设置的值,而 hidden_size 是隐藏状态中的特征(或元素)的数量。至于张量,您可以将其可视化为可用于任意数值计算的 n 维数组。
6. 定义模型类
在这里,我们将创建 MyModel 和子类 nn.Module。
nn.Module 是所有神经网络模块的基类,它包含层和一个方法 forward,它接受输入并返回输出。除此之外,它还包含状态和参数,并且可以循环遍历它们以进行权重更新或将其梯度归零。forward 方法是从 nn.Module 的 __call__ 函数调用的。因此,当我们运行 MyModel(inputs) 时,会调用 forward 方法。
使用 pooler_output
对于任何回归或分类任务,最简单的实现是直接采用 pooler_output 并附加一个额外的回归器或分类器输出层。
特别是在我们的例子中,我们可以在 __init__ 方法中定义一个带有一个 nn.Linear 层的回归器作为我们网络的一分。然后在前向方法中,我们将 pooler_output 馈入回归器以生成目标的预测值。
构建您自己的 Transformer 自定义头
除了简单地使用 pooler_output 之外,还有许多不同的方法可以定义和组合您自己的层和自定义头。我们将演示的一个这样的例子是注意力头,它改编自这里。
️ 注意头
在 forward 方法中,last_hidden_state 的原始输出被馈送到另一个类的实例 AttentionHead(我们将在下一段中讨论 AttentionHead)。然后将 AttentionHead 的输出传递到我们之前看到的回归器中。
那么,AttentionHead 中有什么?AttentionHead 中有两个线性层。AttentionHead 将 last_hidden_state 带入第一个线性层,并在进入第二个线性层之前通过 TanH(双曲正切)激活函数。这得出了注意力分数。然后将 Softmax 函数应用于这些注意力分数,重新缩放它们,使张量的元素位于 [0,1] 范围内并且总和为 1(好吧,尝试将其视为概率分布)。然后将这些权重与 last_hidden_state 相乘,然后对整个序列长度维度的张量求和,最终产生形状(batch_size,hidden_size)的结果。
️ 连接隐藏层
我们想分享的另一种技术是隐藏层的串联。这个想法来自 BERT:用于语言理解的深度双向转换器的预训练,其中作者提到使用基于特征的方法,连接最后四个隐藏层在他们的案例研究中提供了最佳性能。
“性能最好的方法是将来自预训练 Transformer 的前四个隐藏层的标记表示连接起来”
您可以在下面的代码中观察我们在调用我们的模型时需要如何指定 output_hidden_states = True 。这是因为我们现在想要接收和使用来自其他隐藏层的输出,而不仅仅是 last_hidden_state。
在前向方法中,hidden_states 的原始输出被堆叠,给我们一个张量形状(层,batch_size,sequence_length,hidden_size)。由于 roberta-base 总共有 13 层,这简单地转换为 (13, batch_size, sequence_length, 768) 的张量形状。接下来,我们在 hidden_size 维度上连接最后四层,这给我们留下了 (batch_size, sequence_length, 768*4) 的张量形状。连接后,我们使用序列中第一个标记的表示。我们现在有一个 (batch_size, 768*4) 的张量形状,这最终被输入到回归器中。
7. 模型训练
好的,让我们继续编写基本模型训练过程的训练代码。
由于我们不会在这篇文章中涉及用于训练 Transformer 的高级技术,我们将只创建简单的函数。现在我们需要创建一个损失函数、一个训练函数、一个验数,最后是运行训练的主函数。
由于我们使用的是预训练模型(而不是训练一个从头开始),这里的模型训练通常也称为 Transformer 微调过程。
▶️ 评估指标和损失函数
为了衡量我们模型的性能,我们将使用 RMSE(均方根误差)作为评估指标。
等等,那什么是损失函数?它是做什么用的?好吧,损失函数旨在衡量预测输出和提供的目标值之间的误差,以优化我们的模型。事实上,这是优化器将尝试最小化的函数。
有时评估指标和损失函数可能不同,尤其是对于分类任务。但在我们的例子中,由于它是一个回归任务,我们将对两者都使用 RMSE。
因此,我们将定义我们的损失函数如下:
▶️训练功能
我们正在创建的 train_fn 将使用训练数据集训练我们的模型。在运行训练时的主训练循环中,每个时期都会调用此函数。
此函数将首先将模型设置为训练模式。本质上,它将遍历数据加载器中所有批次的训练数据,获得批次的预测,反向传播错误,根据当前梯度更新参数并根据调度程序更新学率。
需要注意的一个重要事项是,我们需要在开始进行反向传播之前将梯度设置为零。这是因为 PyTorch 在随后的反向传递中累积梯度。
最后,此函数将返回它在批次中收集的训练损失和学率。
▶️ 验证功能
validate_fn 用于对我们的验证数据集进行评估。它基本上将评估我们的模型在每个时期的整个训练过程中的表现。它与我们上面写的 train_fn 非常相似,只是禁用了梯度计算。因此没有错误的反向传播,也没有参数和学率的更新。
此功能将首先将模型设置为评估模式。它将在数据加载器中循环所有批次的验证数据,对验证数据(即训练期间未看到的数据)运行批次的预测,并收集将在最后返回的验证损失。
从此处和此处的 PyTorch 文档中提取的注释:
建议我们在训练时始终使用 model.train() 并在评估我们的模型(验证/测试)时使用 model.eval(),因为我们正在使用的模块可能会更新以在训练和评估模式下表现不同。
当我们确定不会调用 .backward() 进行反向传播时,禁用梯度计算对于推理(或验证/测试)很有用。它将减少原本需要梯度计算的计算的内存消耗。
▶️跑步训练
现在我们已经创建了 train_fn 和 validate_fn,让我们继续创建运行训练的主要函数。
这个函数的上半分会做模型训练所需的必要准备。对于每个折叠,它将初始化标记器,获取并创建训练和验证数据集和数据加载器,加载模型并将其发送到设备,并获得优化器和学率调度器。
完成所有这些后,就可以进入训练循环了。训练循环将调用 train_fn 进行训练,然后调用 validate_fn 对每个 epoch 执行模型评估。一般来说,训练损失和验证损失应该在 epochs 中逐渐减少。每当验证损失有所改善(请记住,它越低越好),模型检查点就会被保存。否则循环将一直持续到最后一个时期,或者达到提前停止阈值时。基本上,当 n 次迭代后验证损失持续没有改善时,会触发提前停止,其中 n 是预设阈值。
该函数还将绘制训练和验证损失,以及每次折叠结束时的学率计划。
总结
最后,我们即将结束这篇冗长的文章。总结一下:
我们学了如何使用 scikit-learn 的 StratifiedKFold 执行分层 k-fold 以将数据拆分为训练和验证集。特别是在我们的例子中,我们使用了垃圾箱。
我们从 Transformers 获得了典型原始输出的要点。
我们创建并定义了我们的数据集和模型类。
我们探索了一些可以为我们的模型构建的自定义回归头的示例。
我们了解了模型训练过程的基础知识并为其创建了必要的功能