在小数据集上更稳定地微调 BERT 模型

2023/05/30 LM 共 4733 字,约 14 分钟

论文:Revisiting Few-sample BERT Fine-tuning

《Revisiting Few-sample BERT Fine-tuning》

  • URL:https://arxiv.org/abs/2006.05987

  • Code:https://github.com/asappresearch/revisit-bert-finetuning

  • 单位:ASAPP Inc & Stanford University

  • 会议:ICLR 2021

三种稳定微调 BERT 模型的方法

1、BERTAdam 的 debiasing 影响

在 BERTAdam 的实现中,并没有进行动量偏差修正,因此在模型训练初期以及指数衰减率超参数 \(\beta\) 很小的时候,动量估计值很容易往 0 的方向偏移。因此,作者做了个小的偏差修正,对梯度均值 \(m_t, v_t\) 进行偏差纠正,降低偏差(bias)对训练初期的影响,如下图所示:

也许在规模庞大的数据量和很多的训练步数下,偏差修正能起到的作用很小,模型会在训练后期趋于稳定。但是在实际做 finetune 任务中,训练样本较少,这种优化方案就会出现训练的不稳定。

为了验证这个结论,论文作者做了非常详细的对比试验,他们在四个不同的数据集上,尝试了50种不同的随机种子,分别用带偏移修正的原始 Adam 和不带修正的 BERTAdam 去做 finetune 任务。实验结果分别从不同角度来验证上述的观点,如下图所示:

这是一个模型在不同数据集上的测试集效果 箱线图,图中表明在四个数据集上,使用偏移修正的 Adam 能够极大提升模型在测试集上的效果。

作者还通过 train loss 变化,突出偏差修正的好处,如下图所示:

这张图反应了在小数据上,使用偏移修正进行 finetune,模型能够更快的达到收敛,获得更小的 train loss

2、权重参数再初始化

论文 《Revisiting Few-sample BERT Fine-tuning》 中提到的第二个优化点是 权重的重新初始化

我们在做下游任务的 finetune 时,主流的做法是直接将预训练好的参数直接迁移过来,初始化模型的参数在训练,这样有助于将之前大量语料预训练的语言知识带到下游任务中。但存在一个问题,BERT 的层数较多,不同层学到的信息不一样,那 究竟哪些层的信息对下游任务会起到帮助?

对于 BERT 模型中每层学到了哪些信息,在 ACL 2019 年的论文中 《What does BERT learn about the structure of language?》 进行了仔细的分析,其主要结论就是:

  • BERT 的低层网络就学习到了 短语级别 的信息表征

  • BERT 的中层网络就学习到了丰富的 语言学 特征

  • BERT 的高层网络则学习到了丰富的 语义信息 特征

也就是说,不同层的网络学到的信息存在差异,尤其在越高层,学到的信息越贴近下游任务

预训练时主要的任务是 masked word prediction、next sentence prediction 任务的相关知识,这也就使得我们在做下游任务时,需要根据具体的实际任务场景,来选择网络层。如果我们的实际任务与预训练任务差距比较大(例如序列标注),那么使用预训练时高层学到的信息反而会拖累整体的 finetune 进程,使得模型在 finetune 初期产生不稳定


论文作者做了以下实验进行了验证:重新初始化 BERT 的 pooler 层(文本分类会用到),同时尝试重新初始化 BERT 的 top-L 层参数的权重(\(1 \le L \le 6\)),如下图所示:

论文基于 12 层的 BERT,对 Top 6 层进行重新初始化(正态分布初始化),在 RTE、MRPC 等数据集上进行 finetune,结果如下表所示:

结果很明显,相对于完全使用 BERT 进行初始化,部分初始化对最后模型的性能都有一定程度的提升。

那么具体应该对多少层的权重做重新初始化呢,作者也做了些对比实验,如下图所示:

实验结果表明,并没有一个显著的规律,具体的层数与下游任务场景和数据集有关

小总结

  • figture 6:说明随机初始化 top N(\(N<6\))层或多或少都能提升模型的性能;

  • figture 7:说明随机初始化 top N 某些层,能加快模型收敛速度;

  • figture 8:说明随机初始化 top N 某些层,对高层的参数改变的更小,但随着 \(N\) 的增大,比如 \(N=10\),会使得底层的参数改变严重;

3、finetune 更长的 step

训练更长的时间有利于提高模型的性能,特别是数据集小于 1000 时,如下图所示:

代码

针对 BERT 模型的重新初始化(reinit),参考了论文 《Revisiting Few-sample BERT Fine-tuning》原始代码,并进行了简化:

import torch
import torch.nn as nn
from transformers import (
    AutoModelForSequenceClassification,
)


"""
一些参数
"""
model_type = "bert"
model_name_or_path = "bert-base-uncased"
cache_dir = "./pretrained"
reinit_pooler = True
reinit_layers = 5


"""
创建模型
"""
model = AutoModelForSequenceClassification.from_pretrained(
    model_name_or_path,
    from_tf=bool(".ckpt" in model_name_or_path),
    # config=config,
    cache_dir=cache_dir if cache_dir else None,
)


# re_init pooler
# 使用正态分布对 weight 进行初始化,对 bias 使用全零初始化
if reinit_pooler:
    if model_type in ["bert", "roberta"]:
        encoder_temp = getattr(model, model_type)
        encoder_temp.pooler.dense.weight.data.normal_(mean=0.0, std=encoder_temp.config.initializer_range)
        encoder_temp.pooler.dense.bias.data.zero_()
        # 重新训练 pooler
        for p in encoder_temp.pooler.parameters():
            p.requires_grad = True


if reinit_layers > 0:
    if model_type in ["bert", "roberta", "electra"]:
        # 从深层到浅层,选择 reinit_layers 层进行重新初始化
        for layer in encoder_temp.encoder.layer[-reinit_layers :]:
            for module in layer.modules():
                if isinstance(module, (nn.Linear, nn.Embedding)):
                    # Slightly different from the TF version which uses truncated_normal for initialization
                    # cf https://github.com/pytorch/pytorch/pull/5617
                    module.weight.data.normal_(mean=0.0, std=encoder_temp.config.initializer_range)
                elif isinstance(module, nn.LayerNorm):
                    module.bias.data.zero_() # bias 全零
                    module.weight.data.fill_(1.0) # weight 全 1
                if isinstance(module, nn.Linear) and module.bias is not None:
                    module.bias.data.zero_() # bias 全零


# 打印重新初始化之后的 model
for name, param in model.named_parameters():
    print(name, ": ", param)
print("=====" * 10)

更多

1、具体的超参数设置

论文中具体的超参数设置如下图所示:

2、其他稳定微调的方法

除了上面的几种稳定微调的方法,还有一些其他的方法,例如 WarmupLearning Rate DecayWeight Decay 以及 Frzone Parameters 等。

论文 《Revisiting Few-sample BERT Fine-tuning》 中也使用 Warmup 和 Linear LR Decay。

1)Weight Decay (L2 正则化)

由于在 BERT 官方的代码中对于 bias 项、LayerNorm.biasLayerNorm.weight 项是 免于正则化 的。因此,经常在 BERT 的训练中会采用与 BERT 原训练方式一致的做法,也就是下面这段代码:

param_optimizer = list(multi_classification_model.named_parameters())

no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']

optimizer_grouped_parameters = [
    {'params': [p for n, p in param_optimizer
                if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
    {'params': [p for n, p in param_optimizer
                if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]

optimizer = transformers.AdamW(optimizer_grouped_parameters, lr=config.lr, correct_bias=not config.bertadam)

注意:实践出真知,具体需不需要加,以实际的实验、任务为主!

2)冻结部分层参数(Frozen Parameters)

冻结参数经常在一些大模型的训练中使用,主要是对于一些参数较多的模型,冻结部分参数在不太影响结果精度的情况下,可以减少参数的迭代计算,加快训练速度

在 BERT 中 fine-tune 中也常用到这种措施,一般会 冻结的是 BERT 前几层,因为有研究 BERT 结构的论文表明,BERT 前面几层冻结是不太影响模型最终结果表现的。

这个就有点类似与图像类的深度网络,模型前面层学习的都是一些通用且广泛的知识(比如一些基础的线、点形状类似),这类知识都差不多。

关于冻结参数主要有下面两种方法:

# 方法 1: 设置requires_grad = False
for param in model.parameters():
    param.requires_grad = False


#  方法 2: torch.no_grad()
class net(nn.Module):
    def __init__():
        ......
        
    def forward(self.x):
        with torch.no_grad():  # no_grad下参数不会迭代 
            x = self.layer(x)
            ......
        x = self.fc(x)
        return x

参考

文档信息

-->

Search

    Table of Contents