变压器

RNN是建模序列的最先进方法。它们对齐输入和输出序列的符号位置,并生成一个隐藏状态序列$h_t$,作为先前隐藏状态$h_{t-1}$和位置$t$的输入的函数。这种固有的顺序性排除了并行化。

在报纸上注意力是你所需要的,谷歌研究人员提出变压器模型体系结构避免重复,而是完全依赖于注意机制来绘制输入和输出之间的全局依赖关系。虽然它在机器翻译方面取得了最先进的性能,但其应用范围更广。

附言:博客帖子杰伊·阿拉姆(Jay Alammar)有一幅很棒的插图来解释变形金刚(Transformer)。这个博客帖子哈佛NLP还提供了工作笔记本类型的解释和一些实现。

模型体系结构

变压器结构 变压器模型结构(信贷)

变压器遵循编码器-解码器架构。编码器将输入序列$(x_1,\cdots,x_n)$映射到连续表示序列$\textbf{z}=(z_1,\tdots,z_n)$。给定$\textbf{z}$,解码器一次生成一个输出序列$(y_1,\cdots,y_n)$。该模型是自动累进的,因为在每一步中,前面生成的符号都被用作额外的输入。变压器提出了几种新的机制,可以避免重复出现,即多头自我注意,点式前馈网络位置编码.

 编码器解码器(nn个.模块):
    定义 __初始化__(自己, 编码器, 解码器, 输入_嵌入, 输出_嵌入):
        超级的(编码器解码器, 自己).__初始化__()
        自己.编码器 = 编码器
        自己.解码器 = 解码器
        自己.输入_嵌入 = 输入_嵌入
        自己.输出_嵌入 = 输出_嵌入
        
    定义 向前地(自己, 输入ID, 输出ID):
        返回 自己.解码(自己.编码(输入ID), 输出ID)
    
    定义 编码(自己, 输入ID):
        输入_嵌入 = 自己.输入_嵌入(输入ID)
        返回 自己.编码器(输入_嵌入)
    
    定义 解码(自己, 编码器_输出, 输出ID):
        输出_嵌入 = 自己.输出_嵌入(输出_嵌入)
        返回 自己.解码器(输出_嵌入, 编码器_输出)

1.多头自我关注

注意 标度点积注意和多头注意(信贷)

缩放式点产品关注

注意处理一个查询和一组键值对,并输出值的加权和。Transformer使用Scaled Dot-Product Attention,它使用查询的所有键的点积,除以$\sqrt{d_k}$,并应用softmax函数获得值的权重。将点积除以$\sqrt{d_k}$可以防止其幅值过大,并使softmax函数上的梯度饱和。

\[\text{注意}(Q,K,V)=\text{softmax}\左(\frac{QK^T}{\sqrt{d_K}}\右)V\]
定义 scaled_dot产品关注(, K(K), V(V), attn_掩码):
    #Q:[batch_size,n_heads,L_Q,d_k]    #K:[批大小,n_heads,L_K,d_K]    #V:[批大小,n_heads,L_k,d_V]    达克 = .大小(1)
    非标准化attn分数 = 火炬.马特姆(, K(K).转置(-1, -2)) / 净现值.平方英尺(达克) #分数:[batch_size,n_heads,L_q,L_k]    非标准化attn分数.屏蔽填充(_F)_(attn_掩码, -第1版9) #用掩码为1的值填充自张量的元素。    标准化attn_scores = nn个.Softmax软件(昏暗的=-1)(未规范化的attn_scores)
    输出 = 火炬.马特姆(标准化attn_scores, V(V))  #[批大小,n_heads,L_q,L_v]    返回 输出

多头注意力

作者发现使用不同的学习线性投影($W_i^Q、W_i*K、W_i ^V$)对查询、键和值进行$h$次线性投影是有益的。然后对每个预测版本并行进行关注。这些被串联起来,并再次由$W^O$投影,从而得到最终值。多头注意允许模型在不同位置共同关注来自不同表示子空间的信息。

\(\text{MultiHead}(Q,K,V)=\text{Concat}\left(\text{头部}_1,\text{头}小时\右)W^O\)\(\text{头}_i=\text{Attention}\left(QW_i^Q,KW_i^K,VW_i|V\right)

 多头部注意(nn个.模块):
    定义 __初始化__(自己, n个封头, 隐藏_大小):
        超级的(多头注意力, 自己).__初始化__()
        自己.隐藏_大小 = 隐藏_大小
        自己.n个封头 = n个封头
        自己.d模型 = 整数(自己.隐藏_大小 / 自己.n个封头)
        自己.所有头部大小 = 自己.n个封头 * 自己.d模型
        自己.工作质量(_Q) = nn个.线性的(自己.隐藏_大小, 自己.所有头部大小)
        自己.W_K(_K) = nn个.线性的(自己.隐藏_大小, 自己.所有头部大小)
        自己.W(V) = nn个.线性的(自己.隐藏_大小, 自己.所有头部大小)

    定义 向前地(自己, , K(K), V(V), attn_掩码):
        #dk=dv=d模型        #q:[批次大小,L_q,d_model]        #k:[批次大小,L_k,d_model]        #v:[批次大小,L_k,d_model]        残留物, 批处理大小 = , .大小(0)
        q秒 = 自己.工作质量(_Q)().看法(批处理大小, -1, 自己.n个封头, 自己.d模型).转置(1,2)  #[批次大小,n头,长度q,d模型]        k秒 = 自己.W_K(_K)(K(K)).看法(批处理大小, -1, 自己.n个封头, 自己.d模型).转置(1,2)  #[batch_size,n_heads,len_k,d_model]批量_尺寸,n_heads,len_k,d_model]        v_s(秒) = 自己.W(V)(V(V)).看法(批处理大小, -1, 自己.n个封头, 自己.d模型).转置(1,2)  #[批大小,n头,lenk,d模型]
        attn_掩码 = attn_掩码.不挤压(1).重复(1, 自己._头, 1, 1) #attn_mask:[batch_size,n_heads,L_q,L_k]
        上下文 = scaled_dot产品关注(q秒, k秒, v_s(秒), attn_掩码)  #[批次大小,n头,长度q,d模型]        上下文 = 上下文.转置(1, 2).相邻的().看法(批处理大小, -1, 自己.所有头部大小) #[批次大小,长度q,n头*d模型]        输出 = nn个.线性的(自己.所有头部大小, 自己.隐藏_大小)(上下文)
        返回 输出

三种注意机制

  • 在编码器和解码器之间的注意层中,查询来自解码器,键值对来自编码器。这使得解码器中的每个位置都能注意到编码器中的所有位置。
  • 编码器中的注意层起到了自我注意的作用,在后者中的每个位置都会注意到前一层编码器中的所有位置。
  • 解码器中的注意层也是自注意层。为了防止向左的信息流动并保持自动累进特性(新输出会消耗左边的先前输出,而不是相反),输入中与非法连接对应的所有值都被屏蔽为$-\infty$。

2.位置-方向前馈网络

每个前馈(也称为完全连接)层分别且相同地应用于每个位置。它由两个线性变换组成,其间有一个ReLU激活。

\[FFN(x)=\text{max}(0,xW_1+b_1)W_2+b_2\]
 位置前馈(nn个.模块):
    定义 __初始化__(自己, d模型, d_ff(关闭), 辍学=0.1):
        超级的(位置前馈, 自己).__初始化__()
        自己.第1周 = nn个.线性的(d模型, d_ff(关闭))
        自己.w_2型 = nn个.线性的(d_ff(关闭), d模型)
        自己.辍学 = nn个.辍学(辍学)

    定义 向前地(自己, x个):
        返回 自己.w_2型(自己.辍学(F类.relu公司(自己.w_1(x个))))

3.位置编码

为了使模型能够利用序列的顺序,本文引入了位置编码,对序列中标记的相对或绝对位置进行编码。位置编码是添加到每个输入嵌入中的向量。它们遵循一种特定的模式,帮助模型确定序列中不同单词之间的距离。对于向量的每个维度,标记的位置与正弦/余弦函数一起编码。

\[\text(文本){聚乙烯}_{(pos,2i)}=sin\左(\frac{pos}{10000^{2i/d{model}}}\右)\\\\\\text{聚乙烯}_{(pos,2i+1)}=cos\left(\frac{pos}{10000^{2i/d{model}}}\right)\]

直觉是,每个维度对应一个波长从$2\pi$到$10000\cdots2\pi$的正弦曲线,这将允许模型通过相对位置学习注意力,因为对于任何固定偏移量$k$,$PE_{pos+k}$都可以表示为$PE_}pos}$的线性函数。如下图所示,较早的维度具有较小的波长,可以捕获短距离偏移,而较晚的维度可以捕获较长的距离偏移。虽然我理解直觉,但我很怀疑这是否真的有效。

正弦曲线 不同尺寸具有不同波长的正弦波示例(信贷)

 位置编码(nn个.模块):
    定义 __初始化__(自己, 嵌入_大小, 最大长度(_len)=64):
        超级的(位置编码, 自己).__初始化__()

        #在日志空间中计算一次位置编码。        体育课 = 火炬.零点(最大长度(_len), 嵌入_大小)
        位置 = 火炬.阿兰奇(0, 最大长度(_len)).不挤压(1)
        分词(_T) = 火炬.经验(火炬.阿兰奇(0, 嵌入_大小, 2) *
                             -(数学.日志(10000) / 嵌入_大小))
        体育课[:, 0::2] = 火炬.(位置 * 分词(_T))
        体育课[:, 1::2] = 火炬.余弦(位置 * 分词(_T))
        体育课 = 体育课.不挤压(0)
        自己.寄存器缓冲区(“pe”, 体育课)
        
    定义 向前地(自己, x个):
        返回 变量(自己.体育课[:, :x个.大小(1)], 要求_等级=False(错误))

关于自我注意的讨论

作者在论文的整个章节中,根据三个标准比较了递归层和卷积层的自我关注的各个方面:

  • 复杂性是每层所需的总计算量。
  • 顺序操作是所需的最小顺序操作数。这些操作不能并行化,因此很大程度上决定了层的实际复杂性。
  • 最大路径长度是网络中前进和后退信号必须穿越的路径长度。这些路径越短,越容易学习到长程相关性。

自我关注复杂性 自我注意层、卷积层和递归层的复杂性比较。(信贷)

在上表中,$n$是序列长度,$d$是表示维,$k$是卷积的核大小,$r$是受限自关注邻域的大小。

递归层

计算每个重复步骤需要$O(d^2)$用于矩阵乘法。单步执行整个长度为$n$的序列需要$O(nd^2)$的总计算复杂性。由于顺序性,顺序操作和最大路径长度为$O(n)$。

卷积层

假设输出特征映射是$n$乘以$d$,每个1D卷积需要$O(k\cdot d)$操作,因此总复杂度为$O(k \cdot n \cdot d^2)$。由于卷积是完全可并行的,因此顺序操作是$O(1)$。内核宽度为$k<n$的单个卷积层并没有连接所有的输入和位置对,因此需要$O(n/k)$个相邻内核的堆栈,或者如果是扩展卷积,则需要$0(log_k(n))$的堆栈。

自我关注层

计算两个位置表示之间的点积需要$O(d)$。计算所有位置对的注意力需要$O(n^2d)$。计算是可并行的,顺序操作是$O(1)$。由于输入和输出中的任意两个位置之间存在直接连接,所以自关注层通过恒定数量的操作连接所有位置$O(1)$

限制性自我注意层

为了提高涉及很长序列的任务的计算性能,自我关注可以限制为仅考虑围绕各自位置的$r$邻域大小。这将总复杂性降低到$O(r\cdot n\cdot d)$,尽管需要$O(n/r)$操作才能覆盖最大路径长度。

培训

  • 培训时间:完整型号需要3.5天才能在8个NVIDIA P100 GPU上进行训练。:0
  • 优化器:亚当。提高第一名的学习率预热_步骤训练步骤,然后减少。类似循环学习率或倾斜三角形LRULMFiT公司
  • 正规化:
    • 剩余丢失:在剩余连接和层规范化$P_{drop}=0.1之前将丢失应用于输出层$
    • 嵌入丢弃:将丢弃应用于嵌入和位置编码$P_{drop}=0.1的总和$
    • 标签平滑$\epsilon_{ls}的=0.1$。这伤害了困惑,但提高了BLEU分数。

PyTorch实施

 添加和规范(nn个.模块):
    定义 __初始化__(自己, 隐藏_大小, 辍学=0.1):
        超级的(添加和规范, 自己).__初始化__()
        自己.辍学 = nn个.辍学(辍学)
        自己.规范 = nn个.图层规格(隐藏大小(_S))
    
    定义 向前地(自己, x个, 子层):
        返回 自己.规范(x个 + 自己.辍学(子层(x个)))


 嵌入(nn个.模块):
    定义 __初始化__(自己, 嵌入_大小, 声音_大小, 最大长度(_len), 使用位置编码=False(错误)):
        超级的(嵌入, 自己).__初始化__()
        自己.使用位置编码 = 使用位置编码
        自己.标记嵌入(_E) = nn个.嵌入(声音_大小, 嵌入_大小)  #标记嵌入        自己.规范 = nn个.图层规格(嵌入_大小)
        如果 自己.使用位置编码:
            自己.位置_嵌入 = 位置编码(嵌入_大小, 最大长度(_len))
        其他的:
            自己.位置_嵌入 = nn个.嵌入(最大长度(_len), 嵌入_大小)  #位置嵌入
    定义 向前地(自己, 输入id):
        #(批次,序列)        seq_len = 输入id.大小(1)
        销售时点情报系统 = 火炬.阿兰奇(seq_len, 数据类型=火炬.长的)
        销售时点情报系统 = 销售时点情报系统.不挤压(0).展开as(输入id)  #(seq_len,)->(批大小,seq_ren)        嵌入 = 自己.标记嵌入(_E)(输入id) + 自己.位置_嵌入(销售时点情报系统)
        返回 自己.规范(嵌入)


 编码器层(nn个.模块):
    定义 __初始化__(自己, n个封头, 隐藏_大小, 关闭(_d), 辍学=0.1):
        超级的(编码器层, 自己).__初始化__()
        自己.自我关注 = 多头部注意(n个封头, 隐藏_大小)
        自己.快速傅里叶变换网络 = 位置前馈(隐藏_大小, d_ff(关闭))
        自己.标准_1 = 添加和规范(隐藏_大小)
        自己.标准2 = 添加和规范(隐藏_大小)

    定义 向前地(自己, x个, 面具):
        att_输出 = 自己.标准_1(x个, λ x个: 自己.自我注意(x个, x个, x个, 面具))
        ffn输出 = 自己.标准2(att_输出, 自己.快速傅里叶变换网络)
        返回 ffn输出

 解码器层(nn个.模块):
    定义 __初始化__(自己, n个封头, 隐藏_大小, d_ff(关闭), 辍学=0.1):
        超级的(解码器层, 自己).__初始化__()
        自己.自我关注 = 多头部注意(n个封头, 隐藏_大小)
        自己.编码器_解码器_注意 = 多头部注意(n个封头, 隐藏_大小)
        自己.快速傅里叶变换网络 = 位置前馈(隐藏_大小, d_ff(关闭))
        自己.标准_1 = 添加和规范(隐藏_大小)
        自己.标准2 = 添加和规范(隐藏_大小)
        自己.标准值3 = 添加和规范(隐藏大小(_S))

    定义 向前地(自己, x个, 编码器_输出, src掩码, 目标任务(_M)):
        #x:输出嵌入        #encoder_outputs:最后一个编码器的输出        seq_len = x个.大小(1)
        解码器掩码 = 自己.子序列掩码(seq_len) & 目标任务(_M)
        自拍 = 自己.标准_1(x个, λ x个: 自己.自我关注(x个, x个, x个, 解码器_解码器_掩码))
        编码器_解码器_附件 = 自己.标准2(自拍, λ x个: 自己.编码器_解码器_注意(x个, 编码器_输出, 编码器_输出, src掩码))
        ffn输出 = 自己.标准值3(编码器_解码器_附件, 自己.快速傅里叶变换网络)
        返回 ffn输出

    定义 后续任务(自己, 大小):
	    “掩盖后续位置。”
	    attn_形状 = (1, 大小, 大小)
	    子序列掩码 = 净现值.三个(净现值.(attn_形状), k个=1).astype类型(“uint8”)
	    返回 火炬.来自numpy(子序列掩码) == 1


 输出分类器(nn个.模块):
    定义 __初始化__(自己, 隐藏_大小, 人声):
        超级的(输出分类器, 自己).__初始化__()
        自己.稠密的 = nn个.线性的(隐藏_大小, 人声)

    定义 向前地(自己, x个):
        返回 F类.日志软最大值(自己.稠密的(x个), 昏暗的=-1)


 变压器(nn个.模块):
    定义 __初始化__(自己, n层, n个封头, 隐藏大小(_S), d_ff(关闭), 最大长度(_len), 输入_声音_大小, 输出_声音_大小, 嵌入_大小, 辍学=0.1):
        超级的(变压器, 自己).__初始化__()
        自己.输入_嵌入 = 嵌入(嵌入_大小, 输入_声音_大小, 最大长度(_len), 使用位置编码=False(错误))
        自己.输出_嵌入 = 嵌入(嵌入_大小, 输出_声音_大小, 最大长度(_len), 使用位置编码=False(错误))
        自己.编码器堆栈 = nn个.模块列表([复制.深度复制(编码器层(n个封头, 隐藏_大小, d_ff(关闭))) 对于 _ 在里面 范围(n层)])
        自己.解码器堆栈 = nn个.模块列表([复制.深度复制(解码器层(n个封头, 隐藏_大小, d_ff(关闭))) 对于 _ 在里面 范围(n层)])
        自己.输出分类器 = 输出分类器(隐藏_大小, 输出_声音_大小)
    
    定义 向前地(自己, 输入ID, 输出ID, 输入掩码):
        输入_嵌入 = 自己.输入_嵌入(输入ID)
        编码器_输出 = 输入_嵌入
        对于 编码器 在里面 自己.编码器堆栈:
            编码器_输出 = 编码器(编码器输出, 输入_任务)
        输出_嵌入 = 自己.输出_嵌入(输出ID)
        解码器_输出 = 输出_嵌入
        对于 解码器 在里面 自己.解码器堆栈:
            解码器_输出 = 解码器(解码器_输出, 编码器_输出, 输入_任务)
        logits公司 = 自己.输出分类器(解码器_输出)
        返回 logits公司