一、摘要

摘要:Bi-SimCut是一种简单但有效的训练策略,以提高神经机器翻译(NMT)的性能,它包括两个过程:双向预训练和单向微调,这两个过程都使用了SimCut, 这是一种简单的正则化方法,强调原始语句和经过Cutoff的语句的输出分布之间的一致性。SimCut并不是一种新的方法,而是Cutoff的简化版本。

二、Token Cutoff介绍、公式

2-1、背景介绍

背景:SimCut并不是一种新的方法,而是Shen等人在论文《A simple but tough to-beat data augmentation approach for natural language understanding and generation》提出的Token Cutoff的简化版本。Shen等人介绍了一套cutoff数据增强方法,并且利用JS散度损失在训练过程中使得原始样本和经过Cutoff之后的样本的输出分布一致。虽然性能可观,但是消耗资源巨大(为其中的四个超参数寻找合适的值耗时并且耗费资源),而Bi-SImCut可以解决这个问题(简单且有效)。

此外:文章还展示了

  • Simcut与预训练语言模型例如mBART的兼容性。
  • 结果表明:使用Bi-SimCut的NMT训练在不同量级上的数据都比Transformer取得了显著的改进,并且在几个基准上优于当前的SOTA方法BIBERT。

2-2、Cutoff介绍、架构示意图、公式详解

Cutoff介绍:Shen等人在论文《A simple but tough to-beat data augmentation approach for natural language understanding and generation》提出的Token Cutoff,介绍了一套简单而有效的数据增强策略,建议在一个训练实例中删除部分信息以产生多个扰动样本。为了确保模型完全不能利用已删除输入中的信息,删除过程发生在输入的嵌入层中。

CutOff架构示意图(图示来源于论文):Cutoff示意图,包含

  • Token Cutoff(令牌截断):从句子中删除几个单词的情况下,鼓励模型使用其余单词来得到正确的结果。
  • feature Cutoff(特征切断):每个输入维度都包含一定的语义信息,某些输入维度被删掉后,模型为了正确预测,就需要给与其余维度封装更加丰富的信息,增加了模型的鲁棒性。
  • span cutoff(跨度截断):删除了连续的文本块,鼓励模型利用其余特征来进行预测而不仅仅只是依赖于一小部分显著特征。

Bi-SimCut: A Simple Strategy for Boosting Neural Machine Translation 论文笔记-LMLPHP

(公式详解)
Token Cutoff的交叉熵损失函数为:由三部分组成(原交叉熵损失 L c e ( θ ) L_{ce}(θ) Lce(θ)、使用cutoff的交叉熵损失、kl散度,α和β是平衡他们的标量超参数)

L t o k c u t ( θ ) = L c e ( θ ) + α L c u t ( θ ) + β L k l ( θ ) L_{tokcut}(θ) = L_{ce}(θ)+αL_{cut}(θ)+βL_{kl}(θ) Ltokcut(θ)=Lce(θ)+αLcut(θ)+βLkl(θ)

其中:不使用数据增强的交叉熵损失 L c e ( θ ) L_{ce}(θ) Lce(θ)可以表示为,其中θ是一组模型参数,x,y 代表的是平行语料库, f ( x , y ; θ ) f(x,y;θ) f(x,y;θ)是一系列的预测概率,ÿ是y的一系列独热码向量。
L c e ( θ ) = l ( f ( x , y ; θ ) , y ¨ ) L_{ce}(θ)= l(f(x,y;θ),ÿ) Lce(θ)=l(f(x,y;θ),y¨)

L c u t ( θ ) = 1 N ∑ i = 1 N l ( f ( x c u t i , y c u t i ; θ ) , y ¨ ) L_{cut}(θ) =\frac{1}{N}\sum_{i=1}^N l(f(x_{cut}^i,y_{cut}^i;θ),ÿ) Lcut(θ)=N1i=1Nl(f(xcuti,ycuti;θ),y¨)

这里:KL(·|·)表示两个分布的KL散度。有关于KL散度的更多信息请详见附录(有熵到KL散度、JS散度的说明) L k l ( θ ) L_{kl}(θ) Lkl(θ) 是为了保证原始样本和N个不同的cutoff样本的输出分布的一致性。

L k l ( θ ) = 1 N + 1 { ∑ i = 1 N K L ( f ( x c u t i , y c u t i ; θ ) ∣ ∣ p a v g ) + K L ( f ( x , y ; θ ) ∣ ∣ p a v g ) } L_{kl}(θ) = \frac{1}{N+1}\{\sum_{i=1}^N KL(f(x_{cut}^i,y_{cut}^i;θ)||p_{avg})+KL(f(x,y;θ)||p_{avg})\} Lkl(θ)=N+11{i=1NKL(f(xcuti,ycuti;θ)∣∣pavg)+KL(f(x,y;θ)∣∣pavg)}

p a v g = 1 N + 1 { ∑ i = 1 N f ( x c u t i , y c u t i ; θ ) + f ( x , y ; θ ) } p_{avg} = \frac{1}{N+1}\{\sum_{i=1}^N f(x_{cut}^i,y_{cut}^i;θ) + f(x,y;θ)\} pavg=N+11{i=1Nf(xcuti,ycuti;θ)+f(x,y;θ)}

三、Bi-SimCut介绍、公式

3-1、Bi-SimCut介绍

Bi-SimCut定义:Bi-SimCut是一种简单但有效的训练策略,以提高神经机器翻译(NMT)的性能,它包括两个过程:双向预训练和单向微调,这两个过程都使用了SimCut, 这是一种简单的正则化方法,强调原始语句和经过Cutoff的语句的输出分布之间的一致性。

提出Bi-SimCut的必要性:尽管Shen等人提出的Token Cutoff令人印象深刻,但是在资源有限的情况下,找到合适的超参数(pcut、α、β、N)是十分耗费时间和资源的,为了减少超参数搜索负担,我们提出了SimCut,一个简单的正则化方法使得原始句子和经过cutoff的样本输出分布保持一致。

使用到的数据集介绍: 使用到的详细数据集如下所示。

  • 低资源IWSLT14: en-de
  • 标准资源WMT14: en-de
  • 高资源WMT17: zh-en

Bi-SimCut: A Simple Strategy for Boosting Neural Machine Translation 论文笔记-LMLPHP

3-2、Bi-SimCut公式

公式:公式是由受虚拟对抗训练(VAT,Sato 等人介绍的一种基于KL的的对抗正则化)启发产生的。对于每个句子对(x,y),我们只生成一个cutoff样本。并且与Token Cutoff论文中使用的策略相同,对于每对句子,SImCut的训练目标为:
L s i m c u t ( θ ) = L c e ( θ ) + α L s i m k l ( θ ) L_{simcut}(θ) = L_{ce}(θ)+αL_{simkl}(θ) Lsimcut(θ)=Lce(θ)+αLsimkl(θ)

其中
L s i m k l ( θ ) = K L ( f ( x , y ; θ ) ∣ ∣ f ( x c u t , y c u t ; θ ) ) L_{simkl}(θ) = KL(f(x,y;θ)||f(x_{cut},y_{cut};θ)) Lsimkl(θ)=KL(f(x,y;θ)∣∣f(xcut,ycut;θ))

Bi-SimCut优点:SimCut中只有两个超参数α和 p c u t p_{cut} pcut,大大简化了Token Cutoff中的超参数搜索步骤。注意:VAT只允许梯度通过KL发散项的右侧反向传播,而梯度在SimCut中被设计为通过KL正则化的的两侧反向传播。

α和 p c u t p_{cut} pcut的影响:α是我们优化问题中控制正则化强度的惩罚参数, p c u t p_{cut} pcut控制在SimCut中cutoff的百分比。α=3, p c u t p_{cut} pcut=0.05时表现最佳。实验表明,太小或者太大的α和 p c u t p_{cut} pcut都不利于模型训练。

总结 L s i m k l ( θ ) L_{simkl}(θ) Lsimkl(θ)保证了原始样本和Cutoff样本的一致性。

3-3、Bi-SimCut训练策略:双向预训练和单向微调

双向预训练和单向微调:首先预训练一个双向NMT模型,并且将其作为初始化来微调一个单向NMT模型,假设我们要训练一个英语-》德语的NMT模型,我们首先将训练句对重构为英语+德语-》德语+英语,其中训练数据集翻倍。然后,我们用新的训练句对训练一个新的双向NMT模型,

四、代码阅读(损失函数部分)

# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import math
from dataclasses import dataclass, field

import torch
from fairseq import metrics, utils
from fairseq.criterions import FairseqCriterion, register_criterion
from fairseq.criterions.label_smoothed_cross_entropy import label_smoothed_nll_loss
from fairseq.dataclass import FairseqDataclass
from omegaconf import II

# 这段代码是用 Python 的 dataclass 装饰器定义了一个名为 LabelSmoothedCrossEntropyCriterionWithSimCutConfig 的数据类,用于存储模型训练过程中的配置参数。

@dataclass
class LabelSmoothedCrossEntropyCriterionWithSimCutConfig(FairseqDataclass):
    label_smoothing: float = field(
    	# 表示平滑系数,为0则表示没有平滑。
        default=0.0,
        metadata={"help": "epsilon for label smoothing, 0 means no label smoothing"},
    )
    alpha: float = field(
    	#  表示α参数,为0则表示不进行simcut
        default=0.0,
        metadata={"help": "alpha for simcut, 0 means no simcut"},
    )
    p: float = field(
    	# cutoff的概率参数,为0则表示不进行simcut中的cutoff操作
        default=0.0,
        metadata={"help": "probability for cutoff in simcut, 0 means no cutoff in simcut"},
    )
    report_accuracy: bool = field(
    	# 表示是否需要报告准确性指标。
        default=False,
        metadata={"help": "report accuracy metric"},
    )
    ignore_prefix_size: int = field(
    	# 忽略token的数量。
        default=0,
        metadata={"help": "Ignore first N tokens"},
    )
    # 布尔值,即是否进行句子级别的平均,具体作用是用于计算loss值时,是否对每个句子的loss求平均,如果为True,则对整个batch的所有句子的loss值求平均。
    sentence_avg: bool = II("optimization.sentence_avg")


# 这段代码是使用fairseq库中的@register_criterion装饰器将LabelSmoothedCrossEntropyCriterionWithSimCutConfig类注册为label_smoothed_cross_entropy_with_simcut
# 这意味着在训练模型时可以使用label_smoothed_cross_entropy_with_simcut作为损失函数。
# @register_criterion装饰器提供了一种方便的方式,使用户可以自定义和注册新的损失函数。

@register_criterion(
    "label_smoothed_cross_entropy_with_simcut", dataclass=LabelSmoothedCrossEntropyCriterionWithSimCutConfig
)
class LabelSmoothedCrossEntropyWithSimCutCriterion(FairseqCriterion):
    def __init__(
    	# 该类接受一下参数
        self,
        # 任务对象
        task,
        # 一个布尔值,表示损失是否以句子为单位进行平均。如果为True,则每个句子的损失将被平均,否则将对所有标记的损失进行平均。
        sentence_avg,
        # 平滑
        label_smoothing,
        # 正则化项系数
        alpha=0.0,
        # 表示simcut方法中的p值
        p=0.0,
        # 一个整数,表示需要忽略的前缀的长度。
        ignore_prefix_size=0,
        report_accuracy=False,
    ):
        super().__init__(task)
        self.sentence_avg = sentence_avg
        self.eps = label_smoothing
        self.ignore_prefix_size = ignore_prefix_size
        self.report_accuracy = report_accuracy
        self.alpha = alpha
        self.p = 1 - p

    def simcut(self, model, omega, sample, reduce):
    	"""
		
		"""
		# 首先对omega
        prob = torch.nn.functional.softmax(omega, dim=-1)
        valid_indices = (sample["target"] != self.padding_idx)
		
        encoder_out = model.encoder(
            src_tokens=sample["net_input"]["src_tokens"],
            src_lengths=sample["net_input"]["src_lengths"],
            simcut_p=self.p)
        decoder_out = model.decoder(
            prev_output_tokens=sample["net_input"]["prev_output_tokens"],
            encoder_out=encoder_out,
            simcut_p=self.p)

        loss = torch.nn.functional.kl_div(
            input=torch.nn.functional.log_softmax(decoder_out[0], dim=-1),
            target=prob, reduction='none')
        loss = loss.sum(dim=-1)
        loss = loss * valid_indices.float()
        if reduce:
            loss = loss.sum()

        return loss

    def forward(self, model, sample, reduce=True):
        """Compute the loss for the given sample.
        Returns a tuple with three elements:
        1) the loss
        2) the sample size, which is used as the denominator for the gradient
        3) logging outputs to display while training
        """
        net_output = model(**sample["net_input"])
        loss, nll_loss = self.compute_loss(model, net_output, sample, reduce=reduce)

        if model.training:
            loss += self.alpha * self.simcut(model, net_output[0], sample, reduce)

        sample_size = (
            sample["target"].size(0) if self.sentence_avg else sample["ntokens"]
        )
        logging_output = {
            "loss": loss.data,
            "nll_loss": nll_loss.data,
            "ntokens": sample["ntokens"],
            "nsentences": sample["target"].size(0),
            "sample_size": sample_size,
        }
        if self.report_accuracy:
            n_correct, total = self.compute_accuracy(model, net_output, sample)
            logging_output["n_correct"] = utils.item(n_correct.data)
            logging_output["total"] = utils.item(total.data)
        return loss, sample_size, logging_output

    def get_lprobs_and_target(self, model, net_output, sample):
        lprobs = model.get_normalized_probs(net_output, log_probs=True)
        target = model.get_targets(sample, net_output)
        if self.ignore_prefix_size > 0:
            # lprobs: B x T x C
            lprobs = lprobs[:, self.ignore_prefix_size :, :].contiguous()
            target = target[:, self.ignore_prefix_size :].contiguous()
        return lprobs.view(-1, lprobs.size(-1)), target.view(-1)

    def compute_loss(self, model, net_output, sample, reduce=True):
        lprobs, target = self.get_lprobs_and_target(model, net_output, sample)
        loss, nll_loss = label_smoothed_nll_loss(
            lprobs,
            target,
            self.eps,
            ignore_index=self.padding_idx,
            reduce=reduce,
        )
        return loss, nll_loss

    def compute_accuracy(self, model, net_output, sample):
        lprobs, target = self.get_lprobs_and_target(model, net_output, sample)
        mask = target.ne(self.padding_idx)
        n_correct = torch.sum(
            lprobs.argmax(1).masked_select(mask).eq(target.masked_select(mask))
        )
        total = torch.sum(mask)
        return n_correct, total

    @classmethod
    def reduce_metrics(cls, logging_outputs) -> None:
        """Aggregate logging outputs from data parallel training."""
        loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
        nll_loss_sum = sum(log.get("nll_loss", 0) for log in logging_outputs)
        ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
        sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)

        metrics.log_scalar(
            "loss", loss_sum / sample_size / math.log(2), sample_size, round=3
        )
        metrics.log_scalar(
            "nll_loss", nll_loss_sum / ntokens / math.log(2), ntokens, round=3
        )
        metrics.log_derived(
            "ppl", lambda meters: utils.get_perplexity(meters["nll_loss"].avg)
        )

        total = utils.item(sum(log.get("total", 0) for log in logging_outputs))
        if total > 0:
            metrics.log_scalar("total", total)
            n_correct = utils.item(
                sum(log.get("n_correct", 0) for log in logging_outputs)
            )
            metrics.log_scalar("n_correct", n_correct)
            metrics.log_derived(
                "accuracy",
                lambda meters: round(
                    meters["n_correct"].sum * 100.0 / meters["total"].sum, 3
                )
                if meters["total"].sum > 0
                else float("nan"),
            )

    @staticmethod
    def logging_outputs_can_be_summed() -> bool:
        """
        Whether the logging outputs returned by `forward` can be summed
        across workers prior to calling `reduce_metrics`. Setting this
        to True will improves distributed training speed.
        """
        return True

附录一:熵以及信息熵

:用于描述不确定性,表示系统混乱的程度,越整齐熵也就越小,越混乱不确定的程度越大,熵也就越大,因此整个环境会自发的朝着混乱的方向发展,也就是熵增原理。

信息熵含义:信息熵表示随机变量不确定的程度。一件事情发生的概率越高,那么他的确定性也就越大,那么它的熵也就越小。信息熵常常被作为一个系统的信息含量的量化指标。

性质:信息熵非负。当一件事发生的概率为1时,信息就没有不确定,那么它的熵就是0。
公式:p(x)代表的是事件x发生的概率。
H ( X ) = − ∑ x ∈ X p ( x ) l o g p ( x ) H(X)=- \sum_{x∈X} p(x)logp(x) H(X)=xXp(x)logp(x)

总结:那些接近确定性的分布(输出几乎可以确定)具有较低的熵,那些接近均匀分布的概率分布具有较高的熵。

附录二:KL散度(相对熵)

定义:在机器学习领域,KL散度用来度量两个函数(概率分布)的相似程度或者相近程度,是用来描述两个概率分布差异的一种方法,也叫做相对熵。也就是说KL散度可以作为一种损失,来计算两者之间的概率差异。
公式
K L ( p ∣ ∣ q ) = ∑ p ( x ) l o g p ( x ) q ( x ) = ∑ p ( x ) ( l o g p ( x ) − l o g q ( x ) ) KL(p||q)= \sum p(x)log\frac{p(x)}{q(x)} = \sum p(x)(logp(x)-logq(x)) KL(p∣∣q)=p(x)logq(x)p(x)=p(x)(logp(x)logq(x))

性质

  • KL散度的值始终>=0,当且仅当P(x)=Q(x)时等号成立。
  • KL散度并不是一个对称量,KL(p||q)不等于KL(q||p)

双向KL散度定义:通过交换这两种分布的位置以间接使用整体对称的KL散度。

双向 K L 散度 = 0.5 ∗ K L ( A ∣ B ) + 0.5 ∗ K L ( B ∣ A ) 双向KL散度 = 0.5*KL(A|B) + 0.5*KL(B|A) 双向KL散度=0.5KL(AB)+0.5KL(BA)

附录三:JS散度

定义:KL散度是不对称的,训练神经网络会因为不同的顺序造成不一样的训练结果,为了克服这个问题,提出了JS散度。

J S ( P 1 ∣ ∣ P 2 ) = 1 2 K L ( P 1 ∣ ∣ P 1 + P 2 2 ) + 1 2 K L ( P 2 ∣ ∣ P 1 + P 2 2 ) JS(P1||P2)= \frac{1}{2}KL(P1||\frac{P1+P2}{2}) + \frac{1}{2}KL(P2||\frac{P1+P2}{2}) JS(P1∣∣P2)=21KL(P1∣∣2P1+P2)+21KL(P2∣∣2P1+P2)

性质

  • JS散度的值域范围是[0,1],相同为0,相反则为1,相比于KL,对相似度的判断更加准确了。
  • JS散度是一个对称量,JS(p||q)等于JS(q||p), 对称可以让散度度量更加准确,下边是证明代码。
import numpy as np
import math


# 离散随机变量的KL散度和JS散度的计算方法

def KL(p, q):
    # p,q为两个list,里面存着对应的取值的概率,整个list相加为1
    if 0 in q:
        raise ValueError
    return sum(_p * math.log(_p / _q) for (_p, _q) in zip(p, q) if _p != 0)


def JS(p, q):
    M = [0.5 * (_p + _q) for (_p, _q) in zip(p, q)]
    return 0.5 * (KL(p, M) + KL(q, M))


def exp(a, b):
    a = np.array(a, dtype=np.float32)
    b = np.array(b, dtype=np.float32)
    a /= a.sum()
    b /= b.sum()
    print(a)
    print(b)
    print(KL(a, b))
    print(JS(a, b))


if __name__ == '__main__':
    # exp1
    print('exp1: Start')
    print(exp([1, 2, 3, 4, 5], [5, 4, 3, 2, 1]))
    print('exp1: End')

    # exp2
    # 把公式中的第二个分布做修改,假设这个分布中有某个值的取值非常小,就有可能增加两个分布的散度值
    print('exp2: Start')
    print(exp([1, 2, 3, 4, 5], [1e-12, 4, 3, 2, 1]))
    print(exp([1, 2, 3, 4, 5], [5, 4, 3, 2, 1e-12]))
    print('exp2: End')

    # exp3
    print('exp3: Start')
    print(exp([1e-12,2,3,4,5],[5,4,3,2,1]))
    print(exp([1,2,3,4,1e-12],[5,4,3,2,1]))
    print('exp3: End')

输出
exp1: Start
[0.06666667 0.13333334 0.2 0.26666668 0.33333334]
[0.33333334 0.26666668 0.2 0.13333334 0.06666667]
0.5216030835963031
0.11968758856917597
None
exp1: End
exp2: Start
[0.06666667 0.13333334 0.2 0.26666668 0.33333334]
[1.e-13 4.e-01 3.e-01 2.e-01 1.e-01]
2.065502018456509
0.0985487692550548
None
[0.06666667 0.13333334 0.2 0.26666668 0.33333334]
[3.5714287e-01 2.8571430e-01 2.1428572e-01 1.4285715e-01 7.1428574e-14]
9.662950847122168
0.19399530008415986
None
exp2: End
exp3: Start
[7.1428574e-14 1.4285715e-01 2.1428572e-01 2.8571430e-01 3.5714287e-01]
[0.33333334 0.26666668 0.2 0.13333334 0.06666667]
0.7428131560123377
0.19399530008415986
None
[1.e-01 2.e-01 3.e-01 4.e-01 1.e-13]
[0.33333334 0.26666668 0.2 0.13333334 0.06666667]
0.38315075574389773
0.0985487692550548
None
exp3: End

  • 将第一个实验与第二个实验做对比,可以看出KL散度的波动比较大,而JS的波动相对小。
  • 如果将第二个实验和第三个实验做对比,可以发现KL散度在衡量两个分布的差异时具有很大的不对称性。如果后面的分布在某一个值上缺失,就会得到很大的散度值;但是如果前面的分布在某一个值上缺失,最终的KL散度并没有太大的波动。这个demo可以清楚地看出KL不对称性带来的一些小问题,而JS具有对称性,所以第二个实验和第三个实验的JS散度实际上是距离相等的分布组。

附录四:互信息

定义:互信息衡量的是两种度量间相互关联的程度,极端一点来理解,如果X,Y相互独立,那么互信息为0,因为两者不相关;而如果X,Y相互的关系确定(比如Y是X的函数),那么此时X,Y是“完全关联的”。
公式
I ( X ; Y ) = ∑ x , y p ( x , y ) l o g p ( x , y ) p ( x ) p ( y ) = H ( X ) − H ( X ∣ Y ) = H ( Y ) − H ( Y ∣ X ) I(X;Y)= \sum_{x,y} p(x,y)log\frac{p(x,y)}{p(x)p(y)} = H(X) - H(X | Y) = H(Y) - H(Y | X) I(XY)=xyp(xy)logp(x)p(y)p(xy)=H(X)H(XY)=H(Y)H(YX)

总结

好烦,又想到了被论文支配的恐惧😱。

03-30 16:57