单语适配器
代码复现
本次复现是基于fairseq框架对Monolingual Adapters for Zero-Shot Neural Machine Translation这篇论文提出的单语适配器结构进行复现,因为fairseq已经实现了multilingual transformer结构,所以本篇文章就直接对框架中的transformer进行复用。
采用的数据集是iwslt2014中的de-en和fa-en语言向的数据,前期的数据处理部分这里就忽略了。
github仓库:https://github.com/rainyuniverse/fairseq-adapter
-
添加参数
首先需要参数来指定是否为微调模式,以及adapter中间映射的维度
# multilingual_transformer.py args.finetune = getattr(args, "finetune", False) args.adapter_latent_size = getattr(args, "adapter_latent_size", 64) # transformer_config.py finetune: bool = field( default=False, metadata={"help": "False for training mode, and True for finetuning mode"} ) adapter_latent_size: Optional[int] = field( default=64, metadata={"help": "Dimensions of the adapter intermediate mapping"} )
-
定义单语adapter结构
文章中提到,单语adapter结构是放在每一层编码器和解码器之后的,并且每一种源语言和目标语言单独使用一个adapter,其结构图如下所示,简单来说,就是首先进行了layernorm,然后再进行down projection、nonlinearity和up projection操作,最后再往外面套一个残差,为了重复使用该结构,笔者将adapter类定义在transformer_layer.py文件中
class AdapterLayer(nn.Module):
def __init__(self,
adapter_input_size,
adapter_latent_size,
residual=True):
super(AdapterLayer, self).__init__()
self.adapter_input_size = adapter_input_size
self.adapter_latent_size = adapter_latent_size
# nonlinearity function
self.non_linearity = torch.relu
self.residual = residual
# down-project
self.down_proj = nn.Linear(self.adapter_input_size, self.adapter_latent_size)
# up-project
self.up_proj = nn.Linear(self.adapter_latent_size, self.adapter_input_size)
self.init_weights()
def init_weights(self):
'''
Initialize the adapter weights
'''
std = 0.01
self.down_proj.weight.data.normal_(mean=0.0, std=std)
self.down_proj.bias.data.zero_()
self.up_proj.weight.data.normal_(mean=0.0, std=std)
self.up_proj.bias.data.zero_()
def forward(self, x):
output = self.up_proj(self.non_linearity(self.down_proj(x)))
if self.residual:
output = x + output
return output
- homogeneous batches实现
homogeneous batches指的是每一个batch中只包含一种语言对的数据,这样可以在微调的时候对每一个单语adapter进行分别训练,为了实现这一点,同时不改变原有的采样策略,笔者在原有的基础上去除每个batch包含的其他语言对的数据(在字典中将其他语言对的数据pop出来),具体当前batch包含哪一种语言对取决于当前batch序数对包含的所有语言对长度取余的结果
# train.py
# finetune mode
# one language pair for one batch
if cfg.model.finetune:
lang_pair_length = len(cfg.task.lang_pairs)
for j in range(lang_pair_length):
# remain
if j == (i % lang_pair_length):
continue
# remove
else:
for k in range(len(samples)):
samples[k].pop(cfg.task.lang_pairs[j])
在multilingual_translation中还有对所有语言对的判断(如果batch中的数据没有包含所有语言对会抛出异常),这里根据是否是微调模式进行一下特判
if self.args.finetune:
curr_lang_pairs = [
lang_pair
for lang_pair in sample.keys()
]
else:
curr_lang_pairs = [
lang_pair
for lang_pair in self.model_lang_pairs
if sample[lang_pair] is not None and len(sample[lang_pair]) != 0
]
- 将adapter结构加入到transformer每一层编码器和解码器的后面
- 对当前所有的语言对进行记录,然后根据当前的源语言数量和目标语言数量初始化adapter,分别将编码器的adapter和解码器的adapter封装在moduledict中,以语言的名字作为key,需要注意的是,以下代码需要分别写在TransformerEncoderLayerBase类的init函数和TransformerDecoderLayerBase类的init函数中,笔者这里是为了方便理解所以写到了一起
# transformer_layer.py
# Adapter config
self.lang_pairs = cfg.lang_pairs
self.src_lang_list = []
self.tgt_lang_list = []
for i in range(len(self.lang_pairs)):
cur_lang_pair = self.lang_pairs[i].split("-")
if cur_lang_pair[0] not in self.src_lang_list:
self.src_lang_list.append(cur_lang_pair[0])
if cur_lang_pair[1] not in self.tgt_lang_list:
self.tgt_lang_list.append(cur_lang_pair[1])
self.finetune = cfg.finetune
self.adapter_latent_size = cfg.adapter_latent_size
# encoder
self.adapters = torch.nn.ModuleDict({
self.src_lang_list[i] : AdapterLayer(self.embed_dim, self.adapter_latent_size)
for i in range(len(self.src_lang_list))
})
# decoder
self.adapters = torch.nn.ModuleDict({
self.tgt_lang_list[i] : AdapterLayer(self.embed_dim, self.adapter_latent_size)
for i in range(len(self.tgt_lang_list))
})
- 分别在编码器层和解码器层的forward函数最后加入adapter
# transformer_layer.py
# encoder
# monolingual adapter
if self.finetune:
src_lang = kwargs['lang_pair'].split("-")[0]
x = self.adapters[src_lang](x)
# decoder
# monolingual adapter
if self.finetune:
tgt_lang = kwargs['lang_pair'].split("-")[1]
x = self.adapters[tgt_lang](x)
其实这里还涉及到参数的传递问题,为了不破坏原有fairseq框架的完整性,同时还需要将每个batch的lang_pair参数在训练的过程中传递进来,笔者将lang_pair参数作为**kwargs集合关键字参数,再往前看,在label_smoothed_cross_entropy.py文件中,实际上是将lang_pair作为网络的一个输入参数传递进来的
# label_smoothed_cross_entropy.py
# forward(....,lang_pair,...)
sample["net_input"]['lang_pair'] = lang_pair
net_output = model(**sample["net_input"])
另外还需要修改评估模式下的代码,保证参数传递的正确性
# sequence_generator.py
kwargs = {'lang_pair': net_input['lang_pair']}
lprobs, avg_attn_scores = self.model.forward_decoder(
tokens[:, : step + 1],
encoder_outs,
incremental_states,
self.temperature,
**kwargs
)
- 在微调模式下冻结除adapter外所有的结构
为了实现这一点,笔者在transformer_layer.py文件中的编码器层和解码器层在初始化(init函数中)时就先判断一下是否为微调模式,如果是微调模式,则将这一层中adapter前所有结构的参数冻结(将require_grad变为False)
# freeze above parameters if finetune
if self.finetune:
for param in self.parameters():
param.requires_grad = False
另外需要注意的是,除了transformer_layer中的这些结构外,还有编码器和解码器的embedding层、以及解码器后的output_project层是需要单独进行参数冻结的,它们分别在transformer_encoder.py文件和transformer_decoder.py文件中
训练
-
训练脚本
CUDA_VISIBLE_DEVICES=4,5 fairseq-train /data2/lypan/data/data-bin \ --max-epoch 45 \ --ddp-backend=legacy_ddp \ --task multilingual_translation --lang-pairs de-en,fa-en \ --arch multilingual_transformer_iwslt_de_en \ --share-decoders --share-decoder-input-output-embed \ --optimizer adam --adam-betas '(0.9, 0.98)' \ --lr 0.0005 --lr-scheduler inverse_sqrt \ --warmup-updates 4000 --warmup-init-lr '1e-07' \ --label-smoothing 0.1 --criterion label_smoothed_cross_entropy \ --dropout 0.3 --weight-decay 0.0001 \ --save-dir ../checkpoints \ --max-tokens 4000 \ --update-freq 8 \ --tensorboard-logdir ../logs
-
训练过程loss曲线变化
# 使用tensorboard可视化 tensorboard --logdir logs
-
评估
-
评估脚本
fairseq-generate ../data/data-bin \ --path ../fairseq/checkpoints/checkpoint_best.pt \ --task multilingual_translation --lang-pairs de-en,fa-en \ --source-lang de --target-lang en \ --batch-size 128 --beam 5 --remove-bpe
-
评估结果
# de-en BLEU4 = 30.90, 65.6/40.2/26.5/17.8 (BP=0.925, ratio=0.928, syslen=128648, reflen=138697) # fa-en BLEU4 = 21.35, 58.4/30.0/17.7/10.8 (BP=0.888, ratio=0.894, syslen=71057, reflen=79524)
-
微调
- 微调过程loss曲线变化
-
评估结果
# de-en BLEU4 = 31.52, 65.0/39.9/26.5/17.9 (BP=0.947, ratio=0.948, syslen=131493, reflen=138697) # fa-en BLEU4 = 22.18, 57.6/29.8/17.7/10.9 (BP=0.926, ratio=0.929, syslen=73854, reflen=79524)