【他山之石】Kaggle NLP比赛的技巧

2022-02-24 08:58:22 浏览数 (1)

“他山之石,可以攻玉”,站在巨人的肩膀才能看得更高,走得更远。在科研的道路上,更需借助东风才能更快前行。为此,我们特别搜集整理了一些实用的代码链接,数据集,软件,编程技巧等,开辟“他山之石”专栏,助你乘风破浪,一路奋勇向前,敬请关注。

作者|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能够利用这些结果,使教师在将来更容易为学生提供正确的文本。

本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。

0 人点赞