“他山之石,可以攻玉”,站在巨人的肩膀才能看得更高,走得更远。在科研的道路上,更需借助东风才能更快前行。为此,我们特别搜集整理了一些实用的代码链接,数据集,软件,编程技巧等,开辟“他山之石”专栏,助你乘风破浪,一路奋勇向前,敬请关注。
作者|Benedikt Droste
编译|VK
来源|Towards Data Science
原文链接:https://towardsdatascience.com/
自然语言处理是深度学习中最令人兴奋的领域之一。
在计算机视觉领域,我们已经使用迁移学习好几年了,并且有非常强大的预训练模型,如VGG、Resnet或EfficientNet。
随着著名的《Attention Is All You Need》的问世,自然语言处理领域也取得了突破。
Huggingface上有数千个预先训练的NLP任务模型,使我们能够用比以往更少的数据创建最先进的模型。
01
关于比赛
比赛的主持人是非营利教育技术组织CommonLit。他们提供免费的阅读和写作课程。
在教育中,给学生提供适合他们阅读水平的文本是很重要的。老师不应该直接让一个10岁的孩子去读歌德的《浮士德》,因为这些文本仍然具有挑战性。
这就是为什么CommonLit经常要求教师和学者根据可读性对某些文本进行排名:
目标值是布拉德利·特里(Bradley Terry)对11.1万份摘录进行两两比较后得出的结果。3-12年级的教师(大部分在6-10年级之间授课)担任这些比较的评分员。
结果是评分从-4到 2,数字越大表示可读性越强。每个摘录都由几个人一次评分。然后将平均值作为最终得分。
在挑战赛中,有一个包含文本和相应分数的训练数据集。该模型应该学习分数,然后预测新文本的分数。
02
共同办法
预训练的HuggingFace模型已经非常流行于任何类型的NLP任务:分类、回归、摘要、文本生成等。
很明显,在比赛初期,transformer体系结构的性能明显优于传统的机器学习方法或LSTM体系结构。因此,大部分参与者将重点放在transformer的微调上。
由于生成带标签训练数据的过程需要大量资源,因此可获得的示例相对较少,约为2800个。
大多数人最初使用的是Roberta base,一种具有12层、12个头和1.25亿个参数的transformer,这种transformer在没有太多微调的情况下已经产生了很好的效果。
但是,我们确定了几个可以显著提高性能的领域,我将在后面简要解释:
- 大模型
- 判别学习率
- 定制头
- Bagging和Stacking
- 伪标记
- 基础结构
03
大模型
从其他比赛中可以看出,预训练模型的较大版本通常表现更好。
某些体系结构通常有小型、基本和大型版本(如RoBERTa)。这些差异主要体现在隐藏层的数量、隐藏状态的大小和头部的数量上。
这种联系具有直观的意义:更多的参数可以更好地映射模式,即模型可以了解更多更深入的信息。更大的模型在这次比赛中表现也更好。
然而,由于数据有限,许多团队未能使模型收敛,或者他们只是在训练数据上过拟合了模型。对我们来说,突破点在于学习率和定制头。
04
判别学习率
在迁移学习中,以相同的学习速率训练所有层次并不总是有意义的,这已经不是什么秘密了。
有时所有嵌入层都被冻结,头部以较高的学习率进行训练,然后所有层以较低的学习率再次进行训练。
其思想是,神经网络的第一层学习一般概念,然后每层学习更多的任务特定信息。
由于这个原因,我们不得不调整第一个层,例如,新的头部,它在初始化时包含随机权值。
因此,我们实现了一个定制的优化器。我们对RoBERTa基本架构上使用线性递增的学习率,对头部使用固定的1e-3或2e-4(取决于预训练的模型)学习率。学习率从第一层的1e-5开始,到最后一层的5e-5结束。
代码语言:javascript复制def create_optimizer(model,adjust_task_specific_lr=False):
named_parameters = list(model.named_parameters())
roberta_parameters = named_parameters[:388]
attention_parameters = named_parameters[388:392]
regressor_parameters = named_parameters[392:]
attention_group = [params for (name, params) in attention_parameters]
regressor_group = [params for (name, params) in regressor_parameters]
parameters = []
if adjust_task_specific_lr:
for layer_num, (name, params) in enumerate(attention_parameters):
weight_decay = 0.0 if "bias" in name else 0.01
parameters.append({"params": params,
"weight_decay": weight_decay,
"lr": Config.task_specific_lr})
for layer_num, (name, params) in enumerate(regressor_parameters):
weight_decay = 0.0 if "bias" in name else 0.01
parameters.append({"params": params,
"weight_decay": weight_decay,
"lr": Config.task_specific_lr})
else:
parameters.append({"params": attention_group})
parameters.append({"params": regressor_group})
increase_lr_every_k_layer = 1
lrs = np.linspace(1, 5, 24 // increase_lr_every_k_layer)
for layer_num, (name, params) in enumerate(roberta_parameters):
weight_decay = 0.0 if "bias" in name else 0.01
splitted_name = name.split('.')
lr = Config.lr
if len(splitted_name) >= 4 and str.isdigit(splitted_name[3]):
layer_num = int(splitted_name[3])
lr = lrs[layer_num // increase_lr_every_k_layer] * Config.lr
parameters.append({"params": params,
"weight_decay": weight_decay,
"lr": lr})
return AdamW(parameters)
05
定制头
当你微调一个预先训练好的模型时,你通常会移除神经网络的最后一层(例如分类头),并用一个新的来替换它。
transformer通常会输出最后一个隐藏状态。这包含各个序列的所有单词的所有最后隐藏状态。
在开始时,总是有特殊的CLS标记,根据BERT论文的作者,它可以用于下游任务(或者也可以用于分类,而无需进一步微调)。其思想是,该标记已经是整个序列的表示。此标记在本次比赛中经常用作回归。另一种可能是额外输出池状态。它包含CLS标记的最后隐藏状态,由线性层和Tanh激活函数进一步处理。这些输出也可用作回归头的输入。还有其他无数的可能,这里可以找到一个非常全面的总结:
https://www.kaggle.com/rhtsingh/utilizing-transformer-representations-efficiently
我们尝试了不同的表述。最后,我们还使用了CLS标记和一种注意池形式。BERT论文的作者在他们的一项测试中表明,在多个层上进行连接比只使用最后一层可以产生更好的结果。
背后的想法是,不同的层包含不同的信息。因此,我们连接了最后4层的CLS标记。此外,我们还为最后4层生成了注意权重。然后我们将结果连接起来,并通过最后一个线性层传递它们。以下是实施方案:
代码语言:javascript复制class AttentionHead(nn.Module):
def __init__(self, h_size, hidden_dim=512):
super().__init__()
self.W = nn.Linear(h_size, hidden_dim)
self.V = nn.Linear(hidden_dim, 1)
def forward(self, features):
att = torch.tanh(self.W(features))
score = self.V(att)
attention_weights = torch.softmax(score, dim=1)
context_vector = attention_weights * features
context_vector = torch.sum(context_vector, dim=1)
return context_vector
class CLRPModel(nn.Module):
def __init__(self,transformer,config):
super(CLRPModel,self).__init__()
self.h_size = config.hidden_size
self.transformer = transformer
self.head = AttentionHead(self.h_size*4)
self.linear = nn.Linear(self.h_size*2, 1)
self.linear_out = nn.Linear(self.h_size*8, 1)
def forward(self, input_ids, attention_mask):
transformer_out = self.transformer(input_ids, attention_mask)
all_hidden_states = torch.stack(transformer_out.hidden_states)
cat_over_last_layers = torch.cat(
(all_hidden_states[-1], all_hidden_states[-2], all_hidden_states[-3], all_hidden_states[-4]),-1
)
cls_pooling = cat_over_last_layers[:, 0]
head_logits = self.head(cat_over_last_layers)
y_hat = self.linear_out(torch.cat([head_logits, cls_pooling], -1))
return y_hat
06
Bagging和Stacking
如前所述,我们在这次比赛中没有那么多的训练数据。超过两个epoch的训练导致了过拟合。
我们使用5折来训练数据,并为每个折创建一个模型。我们创建了一个评估程序,以便随着分数的提高(就较低的RMSE而言)更频繁地进行评估。保存两个epoch内验证得分最高的模型。我们设法与排行榜紧密相关:cv越低,我们的lb分数越好。
这是每个挑战中最重要的事情之一:
如果可能的话,你应该尽量缩小本地cv和排行榜之间的差距。
07
伪标记
如前所述,训练数据集非常小。我们使用了新的、未标记的文本,比如Wikipedia文章(可通过api免费获取),并根据训练示例的长度调整了文本的长度。然后,我们使用现有的集合预测新数据的分数,并使用新数据和旧数据重新训练模型。
一个重要的发现是,用更好的集合重新计算伪标签并不能显著提高分数。聚合更多的数据总是比提高伪标签的质量更重要。
08
基础设施
我们将Kaggle基础设施(内核、数据存储)和GoogleDrive与GoogleColab结合使用。
我们更加灵活,因为我们可以在多个实例上进行训练,并且在Colab上每个帐户最多可以访问3个GPU。结构良好的工作空间有助于组织和跟踪实验。通过使用Kaggle api,可以非常轻松地将数据从Colab推送到Kaggle并返回。我们还利用一个松弛的渠道来讨论我们的想法和跟踪我们的实验。
09
不可思议的队友
我的队友尤金(Eugene)与社区分享了他的笔记本,编写了自定义头并创建了训练程序。他的方法被广泛使用,他还公布了他真正有效的RoBERTa-large训练笔记本。祝贺他在第一次比赛中获得第一枚金牌。
10
结论
HuggingFace是各种NLP任务的优秀平台,并提供大量预训练的模型。
然而,在这场比赛中,如何进一步调整模型以获得更好的结果变得非常清楚。如果没有预训练好的模型,结果会更糟,同时仍有优化的潜力。
Kaggle参与者表示,从模型体系结构到优化器,再到训练程序,所有领域都有改进的余地。这些方法也可以转移到其他任务中。我希望CommonLit能够利用这些结果,使教师在将来更容易为学生提供正确的文本。
本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。