机器翻译的进化过程以及在image caption上的迁移

本文深入探讨了机器翻译的发展历程,从基于RNN/LSTM的seq2seq模型到引入Attention机制,再到革命性的Transformer模型。详细解析了Transformer的编码器、解码器结构,以及其在机器翻译任务中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、最早的时候,机器翻译使用基于lstm或者rnn的eq2seq模型。

         

整个模型分为解码和编码两个过程,将输入序列X进行编码得到向量C,然后对C进行解码得到输出序列Y。

其中,X、Y均由各自的单词序列组成(X,Y是两种不同的语言):

X = (x1,x2,...,xm)

Y = (y1,y2,...,yn)

Encoder:是将输入序列通过非线性变换编码成一个指定长度的向量C(中间语义表示),得到c有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c。

  • 编码阶段

在RNN中,当前时间的隐藏状态由上一时间的状态和当前时间输入决定的,即:

获得了各个时间段的隐藏层以后,再将隐藏层的信息汇总,生成最后的语义向量

当然,有一种最简单的方法是将最后的隐藏层作为语义向量C,即

  • 解码阶段

可以看做编码的逆过程。这个阶段,我们根据给定的语义向量C和之前已经生成的输出序列y1,y2,...,yt-1来预测下一个输出的单词yt,即

也可以写作

在RNN中,也可以简化成

其中s是输出RNN(即RNN解码器)中的隐藏层,C代表之前编码器得到的语义向量,yt-1表示上个时间段的输出,反过来作为这个时间段的输入。g可以是一个非线性的多层神经网络,产生词典中各个词语属于yt的概率。

 

2、引入attention机制

 

encoder-decoder模型虽然非常经典,但是局限性也非常大。最大的局限性就在于编码和解码之间的唯一联系就是一个固定长度的语义向量C。也就是说,编码器要将整个序列的信息压缩进一个固定长度的向量中去。但是这样做有两个弊端,一是语义向量无法完全表示整个序列的信息,二是先输入的内容携带的信息会被后输入的信息稀释掉。输入序列越长,这个现象就越严重。这就使得在解码的时候一开始就没有获得输入序列足够的信息, 那么解码时准确率就要打一定折扣。

为了解决上述问题,在 Seq2Seq出现一年之后,Attention模型被提出了。该模型

每次生成一个单词的时候,会关注输入序列的不同范围(也就是 给输入序列不同单词以不用的权重),解码器的输入不再是前一个时刻的输出,而是在每个时刻,都利用这种attention机制生成一个上下文变量c来作为输入。

 

preview

 

 

比如:

preview

解码器序列某一时刻要输入的上下文变量c(i)是基于权重和编码器输入序列产生的隐藏态h序列来产生的。

如图 :

                                                

那么权重是怎么获得的呢?答:是根据解码器当前时刻输出序列的隐藏态H(i)和输入序列的隐藏态h序列计算得到的。

如图:

                            

 

这样我们就获得了权重,从而可以计算编码器每个时刻的上下文变量c(i),也就获得了每个时刻的输入。等等,score()是个啥?

这就涉及到了不同注意力机制的引入:

(1)加法注意力(additive attention)

                                                           preview

 

(2)乘法注意力(dot-productattention)

               

                                          

 

okay我们现在仔细看乘法注意力机制

第一个式子中的H我们用Q来表示,h我们用k来表示,在此基础上除以一个因子dk(scale factor),第二个式子用softmax来表示,第三个式子中的h我们用V来表示。

那么就可以得到乘法注意力机制的改进版:

                                               preview

图示如下:

                                                

 

是scaling factor (比例因子),去掉这个比例因子以后,改进版就和原本的乘法注意力机制一模一样了。

比较小的时候,乘法注意力和加法注意力效果差不多;但当比较大的时候,加法注意力由于不使用scaling factor ,效果要好一些。作者怀疑比较大的时候,因为乘法会比较大,容易进入softmax函数的“饱和区”,导致梯度极小。

 

上述可以做个变种,就是K和V不相等,但需要一一对应,例如:

  • V=h+x_embedding
  • Q = H
  • k=h

 

(3)self-attention

可以看到一般attention的Q来自Decoder(H),K和V来自Encoder(h)。self-attention就是attention的K、Q、V都来自encoder或者decoder,使得每个位置的表示都具有全局的语义信息,有利于建立长依赖关系。

 

我们后面要介绍的transformer就是使用了乘法注意力机制的改进版和self_attention的结合。

 

3、transformer模型

17年谷歌发布transformer模型,不同于此前的rnn、lstm,transformer不是一个循环结构。

先上模型结构图:

                            

 

可以看到,transformer结构为:N*2+N×3+1+1

(1)编码器

         N×编码器模块(一般取N=6)

                    编码器模块有两个子层:

                   1) attention+add&norm子层

                   2)feed forward+add&norm子层

 (2)    解码器

       N×解码器模块(一般取N=6)

                  解码器模块有三个子层:                              

                  1) masked self attention+add&norm子层

                  2)encoder_decoder attention+add&norm子层

                  3)feed forward+add&norm子层

(3)全连接和softmax层

 

编码器输入a语言的单词序列,解码器输出b语言的单词序列。

好,下面我们来详细地对它进行介绍。

(1)首先介绍编码器

编码器的输入不再和以前一样,只是单词的词向量。除了词向量以外,还concat了positional embedding,即给序列的不同位置1,2,3,4...n等编码(也用一个embedding表示)。然后在编码的时候可以使用正弦和余弦函数,使得位置编码具有周期性,并且有很好的表示相对位置的关系的特性(对于任意的偏移量k,PE[pos+k]可以由PE[pos]表示):

 

            

所以,input_embedding=word_embedding+positional_embedding

介绍完编码器的输入,我们来介绍它的结构。

编码器的N个模块是一样的。每一个模块由两个子层组成。

我们以最简单的序列复制任务为例,结合代码对结构进行解析。

对于编码器的N个模块,我们只讲解第一个。后面的都一样。

 

 

0)数据的预处理

我们令batch=30,输入序列的长度为10 输入序列(bacth_size,length) 

即(30,10)的向量首先通过embeddig层,每个单词被映射为word_embedding,shape为(30,10,512),

得到的word_embedding和positional_embedding相加,得到input_embeding(30,10,512)

input_embedding=word_embedding+positional_embedding

其中input_embedding是512维的向量。

代码:

src_embedding=self.src_embed(src)


#分开:
word_embedding=self.lut(src) * math.sqrt(self.d_model) 
input_embedding = word_embedding + Variable(self.pe[:, :word_embedding.size(1)],requires_grad=False)
input_embedding=self.dropout(input_embedding) 

这样,我们就获得了输入编码器的向量input embedding(30,10,512)

 

 

1) Multi-head self-attention+add&norm子层这里的attention采用的是Multi-head self-attention。

x + self.dropout(    self.self_attn(self.norm(x), self.norm(x), self.norm(x), mask)  ) 



输入x即为输入序列的input embeddings.(30,10,512)

首先经过layer_normal ,然后接Multi-head self-attention,dropout,最后载接残差连接。这就是Multi-head self-attention。

我们看一下self_attn,代码中的self_attn是MultiHeadAttention这个类:

可以看到Q、K、V我们全部用的输入序列x  这就是self_attn  。在这种情况下,这一层的某一个位置就可以关注上一层的所有位置的信息。

那么,Multi-head是个啥意思?

 

Q、K、V都是输入序列。作者发现,如果只是直接把输入序列做attention机制(single head),效果并不如Multi-head好。Multi-head允许模型共同关注在不同位置不同子空间表示的信息。

所谓的Multi-head是指将K、Q、V分别映射到h个linear 层,对于第 i 个linear层,依次得到K(i)、Q(i)、V(i),然后经过改进版的attention层,得到c(i),然后把c(i)做concat。 h 即为multi-head的head数目。公式及图示如下:

                preview

 

                                                  

我们来看具体的代码实现:

#Q K V 由于是self_attn 因此三者是同样的   形状均为(30,10,512)
    def forward(self, query, key, value, mask=None):                           
        if mask is not None:
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)
        
        #(1)映射到了8个全连接层,同时把维度从512 降到了64   
        #Q K  V 均为  (30,8,10,64)  512/8=64  8个head
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]

        #(2)8个输出都通过改进版的dot-attention层 
        #获得c (30,8,10,64)
        x, self.attn = attention(query, key, value, mask=mask, 
                                 dropout=self.dropout)

                                 
        # 3) 最后再concat成512维度的向量     (30,10,512)                     
        x = x.transpose(1, 2).contiguous() \
             .view(nbatches, -1, self.h * self.d_k)               
        return self.linears[-1](x)                       
        # 4)对caoncat的结果接liear层  维度不变  输出(30,10,512)

这里的自注意力机制用到的mask是src_mask,用于标定每个批次输入序列的长度。

 

2)feed forward+add&norm子层

还是先看整体的代码:

x + self.feed_forward(    self.self_attn(self.norm(x), self.norm(x), self.norm(x), mask)  ) 

那么feed_forward是什么?相比起multi-head,它要简单很多了。就是两个全连接层,把向量从512维升到2048,再降回512。

上代码:

#其中 d_model= 512   d_ff=2048
class PositionwiseFeedForward(nn.Module):
    "Implements FFN equation."
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w_2(self.dropout(F.relu(self.w_1(x))))

 

至此,编码器模块的两个子层( Multi-head self-attention+add&norm子层   和 feed forward+add&norm子层 )介绍完毕,后面每个模块都是对前面模块的重复。最后介绍一些用到的norm() 归一化函数。这里的归一化不是batchnormal,而是layernormal.

二者的区别和layernormal的具体含义参考https://blog.csdn.net/zlrai5895/article/details/85458203

 

(2)下面开始介绍解码器。

经过数个编码器模块以后,我们获得了编码器的输出memory,形状为(30,10,512),与输入的形状相同,但是它完成了对输入的编码。可以看到,在编码的过程中,全程都是输入序列(源语言)上的操作,而输出序列(目标语言)并没有参与进来。

因为是有监督的训练,每一个输入序列都对应了一个输出序列。输出序列有点类似于标签。那么,什么时候用输出序列呢?答案是在解码器中。

memory即为解码器的输入。

我们还是只介绍单个的模块  包含三个子层:masked self attention+add&norm子层 、encoder_decoder attention+add&norm子层和feed forward+add&norm子层。

0)数据的预处理

设置输出序列的长度为9.

首先是把输出序列同样通过embeddig层,每个单词被映射为word_embedding,shape为(30,9,512),

得到的word_embedding和positional_embedding相加,得到tar_embedding(30,9,512)

tar_embedding=word_embedding+positional_embedding

其中tar_embedding是512维的向量。

 

 

tar_embedding 、memory、 src_mask  、tgt_mask是解码器的输入。

src_mask、 tar_mask分别用来标定 一个batch中输入序列和输出序列的长度。

 

 

1)masked self attention+add&norm子层

与encoder的Multi-head attention机制类似,唯一的不同是encoder的mask是src_mask,decoder此处的mask是tgt_mask。

tgt_mask的作用是确保预测位置i的时候仅仅依赖于位置小于i的输出。

masked attention+add&norm子层输出和输入形状相同。(30,9,512)

 

2)encoder_decoder attention+add&norm子层

这里的attention不再是self attention,而是把输入序列和输出序列连接了起来。

Q:解码器前一个子层的输出  (30,9,512)

K:编码器的输出memory         (30,10,512)

V:编码器的输出memory          (30,10,512)

#    Q:(30,8,9,64)  K:(30,8,10,64)  V:(30,8,10,64)
#V对应于输入序列的隐藏态。
#输出序列 每个时刻的上下文向量是V的加权  
#输出序列的长度为9 
#所以最后的输出为(30,8,9,64)  
def attention(query, key, value, mask=None, dropout=None):    
    "Compute 'Scaled Dot Product Attention'"
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) \
             / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(scores, dim = -1)
    if dropout is not None:
        p_attn = dropout(p_attn)                                          
    return torch.matmul(p_attn, value), p_attn
    #p_attn:  (30,8,9,10)  value:(30,8,10,64)
    #matmul(p_attn,value):(30,8,9,64)

可以看到经过这一个attention之后,输出序列的每个位置都集成了对输入序列所有位置的关注。

输出为(30,9,512)

 

3)feed forward+add&norm子层

和编码器一样,不再叙述,输入是

前一个层的输出   (30,9,512)

输出是两个全连接层之后的结果  (30,9,512)

 

 

(3)最后的generator

是一个全连接层+log softmax,从512维度映射到vocabsize+1的维度。(30,9,vocab+1)

 

得到的结果与gt计算损失并且优化。这就是训练的过程了。

 

 

模型训练和测试的时候方式并不一样。以机器翻译这个任务为例,

训练时候,编码器的输入inputs是待翻译的序列。解码器的输入是对应的翻译好的序列右移一位(前面填充起始符)。

测试的时候,使用贪婪算法,每一个时刻的预测结果依赖于前一个时刻的预测结果。

假如我现在已经预测出来k个词 每个单词是d维向量。
编码器的输出  mask   预测出的输出序列 tgt mask作为decode的输入
decode的输出是(batch_size,k,d)
取最后一行预测下一个单词  
k=k+1  重复进行 直到预测完为止 。

 

 

 

最后写一下常见的往caption任务上的迁移方式。

变化其实挺小的。

提取出特征图7*7*512  reshape成49  作为编码器的输入序列。对于图像特征  直接全连接层到512维度,就不搞embeeding和position了,也就是说  src_embedding跳过,改为全连接层。

 

 

 

 

 

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值