鹤啸九天 自律更自由,平凡不平庸 Less is More

Transformer知识点汇总

Notes(温馨提示):

  1. ★ 首次阅读建议浏览:导航指南, 或划到本页末尾, 或直接点击跳转, 查看全站导航图
  2. 右上角工具条搜索文章,右下角二维码关注微信公众号(鹤啸九天),底栏分享、赞赏、评论
  3. ★ 转载请注明文章来源,知识点积累起来不容易,水滴石穿,绳锯木断,谢谢理解
  4. ★ 如有疑问,邮件讨论,欢迎贡献优质资料


Transformer 学习笔记

Transformer 可视化

三棕一蓝

【2024-4-2】三蓝一棕出品: 可视化讲解 transformer

3D可视化

【2023-7-28】关于 AI 的深度研究:ChatGPT 正在产生心智吗?,Transformer 原理 3D 可视化

Transformer Explainer

【2024-7-11】Transformer Explainer

总结

Transformer,从NLP领域横跨到语音图像领域,最终统一几乎所有模态的架构。

  • 基于 Transformers 架构的大型语言模型 (LLM),如 GPTT5BERT,已经在各种自然语言处理 (NLP) 任务中取得了 SOTA 结果。
  • 此外,涉足其他领域,例如:计算机视觉 (VITStable DiffusionLayoutLM) 和音频 (WhisperXLS-R)

Google 2017年发的一篇论文,标题叫《Attention Is All You Need》,核心是Self-Attention机制,中文也叫自注意力

  • 在语言模型建模过程中,把注意力放在那些重要的Token上。

基于transformer的多模态模型

  • ViT: 2020, 图像任务
  • CLIP: 2021, 文本和图像混合
  • KOSMOS-1: 2023, 多模态大规模语言模型

【2024-3-8】transformer Transformer 逐层图解, medium 文章翻译

  • 整体结构
  • 词嵌入(Embedding ) + 位置编码(Position Encoding)
    • Transformer 输入关注每个词的信息:含义和序列位置。
      • 嵌入层对词含义编码。
      • 位置编码层表示词的位置。一条正弦曲线(偶数)和余弦曲线(奇数)
  • 矩阵维度(Matrix Dimensions)
    • 嵌入层接受一个 (samples, sequence_length) 形状的二维单词ID矩阵,将每个单词ID编码成一个单词向量,其大小为 embedding_size,得到(samples, sequence_length, embedding_size) 形状的三维输出矩阵
    • 由嵌入层和位置编码层产生的(samples, sequence_length, embedding_size) 形状在模型中被保留下来,随数据在编码器和解码器堆栈中流动,直到它被最终的输出层改变形状(实际上变成了samples, sequence_length, vocab_size) 。
  • 编码器 (Encoder)
    • 编码器和解码器堆栈分别由几个(通常是 6 个)编码器和解码器组成,按顺序连接。
    • 堆栈中的第一个编码器从嵌入和位置编码中接收其输入。堆栈中的其他编码器从前一个编码器接收它们的输入。
    • 当前编码器接受上一个编码器的输入,并将其传入当前编码器的自注意力层。当前自注意力层的输出被传入前馈层,然后将其输出至下一个编码器。
    • 自注意力层和前馈网络都会接入一个残差连接,之后再送入正则化层。注意,上一个解码器的输入进入当前解码器时,也有一个残差连接。
    • 编码器堆栈中的最后一个编码器的输出,会送入解码器堆栈中的每一个解码器中。
  • 解码器(Decoder)
    • 解码器结构与编码器类似,但有一些区别。
      • 像编码器一样,堆栈中的第一个解码器从嵌入层(词嵌入+位置编码)中接受输入;堆栈中的其他解码器从上一个解码器接受输入。
      • 在一个解码器内部,输入首先进入自注意力层,这一层的运行方式与编码器相应层的区别在于:
        • 训练过程中,每个时间步的输入,是直到当前时间步所对应的目标序列,而不仅是前一个时间步对应的目标序列(即输入的是step0-stepT-1,而非仅仅stepT-1)。
        • 推理过程中,每个时间步的输入,是直到当前时间步所产生的整个输出序列。
        • 解码器的上述功能主要是通过 mask 方法进行实现。
      • 解码器与编码器的另一个不同:解码器有第二个注意层层,即编码器-解码器注意力层 Encoder-Decoder-attention 层。其工作方式与自注意力层类似,只是其输入来源有两处:位于其前的自注意力层及 E解码器堆栈的输出。
      • Encoder-Decoder attention 的输出被传入前馈层,然后将其输出向上送至下一个Decoder。
      • Decoder 中的每一个子层,即 Multi-Head-Self-Attention、Encoder-Decoder-attention 和 Feed-forward层,均由一个残差连接,并进行层规范化。
  • 注意力(Attention)
    • 注意力被用在三个地方:
      • Encoder 中的 Self-attention:输入序列对自身的注意力计算;
      • Decoder 中的 Self-attention:目标序列对自身的注意力计算;
      • Decoder 中的Encoder-Decoder-attention:目标序列对输入序列的注意力计算。
    • 注意力层(Self-attention 层及 Encoder-Decoder-attention 层)以三个参数的形式接受其输入,称为查询(Query)、键(Key)和值(Value)
      • Decoder self-attention,解码器的输入同样被传递给所有三个参数,Query、Key和 Value。
      • Encoder-Decoder-attention,编码器堆栈中最后一个编码器的输出被传递给Value和Key参数。位于其前的 Self-attention 和 Layer Norm 模块的输出被传递给 Query 参数。
    • 自注意力计算方式
      • ![])(https://pic3.zhimg.com/80/v2-7cda913d104961a1db0e5ea6ff3a8b86_1440w.webp)
  • 多头注意力(Multi-head Attention)
    • 多头注意力–Multi-head Attention 通过融合几个相同的注意力计算,使注意力计算具有更强大的分辨能力
  • 掩码

Transformer 架构理解

Transformer是一种Encoder-Decoder架构(Seq2Seq架构也是),先把输入映射到Encoder,可以把Encoder想象成RNN,Decoder也是。

Transformer 这个架构基于Seq2Seq,同时处理NLUNLG任务,而且Self Attention机制的特征提取能力很强。

  • 不同于Seq2Seq, Transformer 是一个 set-to-set 模型,不再依赖串行,解决了seq2seq并行能力问题
    • seq2seq: 序列到序列模式
    • transformer: 集合到集合模式
  • 只要数据是基本单位组成的集合(a set of units),就可以应用 transformer;

这样,左边负责编码,右边则负责解码。不同的是

  • (1) 编码时,因为知道数据,所以建模时可以同时利用当前Token的历史Token未来Token
    • Encoder的block分两个模块:Multi-Head AttentionFeed Forward
    • Multi-Head Attention用到Self Attention,和Attention类似,不过它是Token和Token的重要性权重Multi-Head将自注意力重复n次,每个token注意到的信息不一样,可以捕获到更多信息。
      • 比如:「我喜欢在深夜的星空下伴随着月亮轻轻地想你」,有的Head「我」注意到「喜欢」,有的Head「我」注意到「深夜」,有的Head「我」注意到「想你」……
    • Feed Forward相当于「记忆层」,大模型大部分知识都存在此,Multi-Head Attention根据不同权重的注意提取知识。
  • (2) 但解码时逐个Token输出,所以只能根据历史Token以及Encoder的Token表示进行建模,而不能利用未来Token。

NLP典型任务

任务 理解(NLU) 生成(NLG) 输入/输出模式 分析
文本分类 多对一 适合Encoder
文本匹配 近似多对一 适合Encoder
文本生成 多对多,变长 适合Decoder
序列标注 多对多,定长 适合Encoder
文本摘要 多对多,变长,一般变少 适合Decoder
机器翻译 多对多,变长 适合Decoder
改写/纠错 多对多,维度近似 适合Decoder
问答系统 多对多,维度不定 适合Decoder

然而,大多数NLP任务其实并不是Seq2Seq,典型代表:句子级别分类、Token级别分类(也叫序列标注)、相似度匹配和生成;

  • 而前三种应用最为广泛。这时候EncoderDecoder可以拆开用。
    • 左边的Encoder在把句子表示成一个向量时,利用上下文信息,也就是双向
    • 右边的Decoder不能看到未来的Token,一般只利用上文,是单向的。
  • 虽然都可以用来完成刚刚提到的几个任务,但从效果上来说
    • Encoder更加适合非生成类(即理解类)任务
    • Decoder更加适合生成类任务。

NLP领域一般分别叫做NLU(Natural Language Understanding,自然语言理解)任务和NLG(Natural Language Generation,自然语言生成)任务。

  • NLU任务:句子级别分类,给定一个句子输出一个类别。
    • 因为句子可以表示为一个向量,经过张量运算映射到每个类的概率分布。
    • 这和之前的语言模型没有本质区别,只是语言模型的类别是整个词表大小,而分类的类别看具体任务,有二分类多分类多标签分类等等。
  • NLG任务: 除了生成外,常见的有文本摘要机器翻译改写纠错等。

transformer 解决什么问题

针对rnn和cnn的缺陷,Transformer怎么解决这些问题?

  • 并行化
  • 长程依赖学习
  • 层次化建模

Transformer视频极速讲解

Transformer模型

  • img

上图下面部分,训练用的输入和输出数据的 embedding,都会先加上一个position encoding来补充一下位置信息。

  • Encoder
    • 途中左侧部分是encoder块,encoder中6层相同结构堆叠而成,在每层中又可以分为2个子层,底下一层是multihead self-attention层,上面是一个FC feed-forward层,每一个子层都有residual connection,,然后在进行Layer Normalization. 为了引入redisual connenction简化计算,每个层的输入维数和embedding层保持一致。
  • Decoder
    • 同样是一个6层的堆叠,每层有三个子层,其中底下两层都是multihead self-attention层,最底下一层是有mask的,只有当前位置之前的输入有效,中间层是encode和decode的连接层,输出的self-attention层和输入的encoder输出同时作为MSA的输入,实现encoder和decoder的连接,最上层和encoder的最上层是一样的,不在单说,每个子层都有有residual connection,和Layer Normalization

【2021-8-25】Transformer结构中,左边叫做编码端(Encoder),右边叫做解码端(Decoder)。不要小看这两个部分,其中左边的编码端最后演化成了最后鼎鼎大名的Bert,右边的解码端在最近变成了无人不知的GPT模型。

【2023-2-15】transformer 出现后,迅速取代了 RNN系列 变种,跻身主流模型架构基础。

transformer 结构分成:

  • (1)自回归系列:偏好 文本生成,示例:GPT-3;
  • (2)双向自编码系列:偏好 自然语言理解,示例:BERT,双向transformer+Mask自编码系列
  • (3)encoder-decoder系列:偏好 条件文本生成,示例:T5,双向/单向attention

RNN系列

详见站内专题:RNN和seq2seq演变

亮点

  • Self Attention
    • 传统的编解码结构中,将输入输入编码为一个定长语义编码,然后通过这个编码在生成对应的输出序列。它存在的一个问题在于:输入序列不论长短都会被编码成一个固定长度的向量表示,而解码则受限于该固定长度的向量表示
    • attention机制: encoder的输出不是一个语义向量,是一个语义向量的序列
    • Transformer的Attenion函数称为scaled dot-Product Attention
  • MultiHead Attention
    • self attention计算时会分为两个阶段,第一个阶段计算出softmax部分,第二部分是在乘以 Value部分,这样还是串行化的,并行化不够。
    • MultiHeadAttention,对query,key,value各自进行一次不同的线性变换,然后在执行一次softmax操作,这样可以提升并行度,论文中的head数是8个

img

  • position Encoding
    • 语言是有序的,在cnn中,卷积的形状包含了位置信息,在rnn中,位置的先后顺序其实是通过送入模型的先后来保证。transformer抛弃了cnn和rnn,那么数据的位置信息怎么提供呢?
    • Transformer通过position Encoding来额外的提供位置信息,每一个位置对应一个向量,这个向量和word embedding求和后作为 encoder和decoder的输入。这样,对于同一个词语来说,在不同的位置,他们送入encoder和decoder的向量不同。

总结

  • 结构
  • 训练过程

作者:Transformer模型学习

图解 Transformer

Transformer架构

  • transformer 结构图:
  • transformer_architecture

首先,Transformer模型使用经典的encoer-decoder架构,由encoder和decoder两部分组成。

  • 上图的左半边用Nx框出来的,encoder的一层。encoder一共有6层这样的结构。
  • 上图的右半边用Nx框出来的,decoder的一层。decoder一共有6层这样的结构。
  • 输入序列经过word embeddingpositional encoding相加后,输入到encoder。
  • 输出序列经过word embeddingpositional encoding相加后,输入到decoder。
  • 最后,decoder输出的结果,经过一个线性层,然后计算softmax。

word embeddingpositional encoding后面会解释。首先详细地分析一下encoder和decoder的每一层是怎么样的。

序列化

模型只认识数字,因此,输入前需要将各种模态的数据序列化成数组/向量

数据模态

  • 文本
  • 语音
  • 图像

参考知乎

文本序列化

  • 文字序列根据 BPE 或者其它编码方法得到 Token
    • 文字编码方式:一个英文单词编码在 1~2 个 token, 一个汉字编码是 1~3 个 token,每个 token 都是一个数字
  • Token 通过查表直接得到 Embeding矩阵
    • 这个表通常非常大 ,比如 GPT3 可能是 12288x4096, 12288是 token 个数,4096 是维度,每个 token 查表后有 4096 维,这个是训练出来的
  • Token 通过 Postion 计算 Positional Encoding(标准算法公式)
  • 将 Embedding 与 Positional Encoding 相加得到 Transformer的输入

Token 查表示意图

  • 计算公式
  • 每个token的位置embedding受多个因素影响:词库总数n, embedding维度d, token对应的词库id(k), 句子中的第几个(i), sin还是cos
  • 示意图

图像序列化

图像的 token 化

  • 直接把图像矩阵分割成小块(如 16x16)
  • 再按顺序排好
  • 然后加位置编码

示例

图片被切割拉平后,直接扔到一个 CNN 网络里变成 Transformer 的输入部分

语音序列化

声音的 token 化最简单,因为天生就有二维特征: mel 谱数据

以 openai 的 whisper 项目为例

  • 声音输入的 token 每 30ms 一个,80 个log mel 谱数据。
  • 这样只要不断切段,这个声音就直接变成了二维矩阵

声音输入也有 position embedding

Encoder

Encoder 由6层相同的层组成,每一层分别由两部分组成:

    • 第一部分是一个multi-head self-attention mechanism
    • 第二部分是一个position-wise feed-forward network,是一个全连接层

两个部分,都有一个 残差连接(residual connection),然后接着一个Layer Normalization

  • ENCODER
  • An encoder block from the original transformer paper can take inputs up until a certain max sequence length (e.g. 512 tokens). It’s okay if an input sequence is shorter than this limit, we can just pad the rest of the sequence.

新手可能会问:

    • multi-head self-attention 是什么呢?
    • 参差结构是什么呢?
    • Layer Normalization又是什么?

Decoder

和 encoder 类似,decoder由6个相同的层组成,每层包括3个部分:

  • 第一个部分是multi-head self-attention mechanism
  • 第二部分是multi-head context-attention mechanism
  • 第三部分是一个position-wise feed-forward network
  • DECODER

三个部分都有一个残差连接,后接一个Layer Normalization

相同

  • 都有 自注意力层(self-attention)
  • 都有 前向全连接层(feed forward neural network)

不同于 encoder:

  • 自注意力层将待预测的token屏蔽掉(mask),所以是 masked self-attention。掩码方法不同于BERT的置为 [MASK],而是继承到自注意力计算中。
    • img
  • 新增 编码器-解码器自注意力层(encoder-decoder self-attention)

但是,decoder出现了一个新的东西multi-head context-attention mechanism。这个东西其实也不复杂,理解了multi-head self-attention 可以理解multi-head context-attention

GPT-2 用的 Decoder 结构

  • decoder
  • 去掉 transformer decoder结构里的 编码器-解码器自注意力层

Attention 机制

直观理解

语言的含义极度依赖上下文,比如,机器人第二法则:

  • 机器人第二法则机器人必须遵守人类给命令,除非该命令违背了第一法则

这句话中高亮了三个地方,指代其它单词。需要把这些词指代的上下文联系起来,才能理解或处理这些词语。模型处理这句话时,必须知道:

  • 「它」指代机器人
  • 「命令」指代前半句话中人类给机器人下的命令,即「人类给它的命令」
  • 「第一法则」指机器人第一法则的完整内容

自注意力机制

  • 处理每个单词(将其传入神经网络)之前,融入了模型解释某个单词的上下文的相关单词的理解。
  • 给序列中每一个单词都赋予一个相关度得分,之后对向量表征求和。

Attention是指对于某个时刻的输出y,它在输入x上各个部分的注意力。这个注意力实际上可以理解为权重

Attention 机制也可以分成很多种。Attention? Attention! 一文有一张比较全面的表格:

  • attention_mechanism
  • Figure 2. a summary table of several popular attention mechanisms.

上面第一种additive attention是以前seq2seq模型里面,使用attention机制,这种加性注意力(additive attention)用的很多。Google的项目 tensorflow/nmt 里面这两种attention机制都有实现。

为什么这种attention叫做additive attention呢?

  • 对于输入序列隐状态 $h_i$ 和输出序列的隐状态 $s_t$ ,它的处理方式很简单,直接合并,变成$[s_t;h_i]$

但是 transformer模型用的不是这种attention机制,使用的是另一种,叫做乘性注意力(multiplicative attention)

那么这种乘性注意力机制是怎么样的呢?从上表中的公式也可以看出来:两个隐状态进行点积

注意力机制 核心概念: Query(查询)、Key(键)和 Value(值)。

  • Query (Q):当前单词的一种表示,对所有其他单词进行评分(用Key)。只关心当前正在处理的token的 Query 。生成任务中,通常是最后一个token的表示。
  • Key (K):所有单词的标签。它们是我们在搜索相关单词时所匹配的内容。用于与Query进行匹配,决定应该关注哪些信息。
  • Value (V):单词实际表示,一旦对每个单词的相关性进行了评分,聚合起来表示当前单词的值。

工作原理类比:

  • 假设输入为: “A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.”
  • Query (查询):手里拿着一张便利贴,上面写着”it”。当前词的查询向量, 例子中是第9个位置的查询向量。
  • Key (键):文件柜中每个文件夹的标签, 像键向量, 代表序列中每个词的”标识”。4个文件夹,分别标记为”a”, “robot”, “must”, “obey”。
  • Value (值):每个文件夹里的实际内容对应着值向量,包含词具体信息。

注意力计算过程:

  • 拿着”it”便利贴(Query #9),与每个文件夹的标签(Key #1到#4)进行比较。比较结果决定了你对每个文件夹内容的关注程度。
  • “a” 文件夹得到30%的关注
  • “robot”文件夹得到50%的关注
  • “must”和”obey”文件夹各得到0.1%的关注

百分比是注意力权重,决定从每个文件夹中提取信息的比例。

信息综合:

最后根据这些权重从各个文件夹中提取信息,并将它们综合起来,形成对”it”这个词的理解。

最终的表示是多个信息源的加权组合。这个加权组合可以用一个简单的形式表达:

  • it的表示 = 0.3a + 0.5robot + 0.001must + 0.001obey

每个词前的系数代表注意力权重,而词本身代表了其 Value 向量。这种加权求和方式使模型能根据当前上下文需求,灵活整合来自不同位置的信息,从而形成对当前词”it”的理解。

这个简化表达忽略了很多细节,但基本表达了注意力机制中信息综合的核心思想。

注意力机制本质:

  • Query 向量代表当前 token 的”问题”或”需求”。
  • Key 向量代表每个 token 可能提供的”信息”。
  • Value 向量是实际传递的”信息内容”。

生成新 token 时,新”问题”(Query)查询所有历史”信息”(Key)并获取相关的”内容”(Value)。

注意力机制允许模型动态地“查阅”之前的信息。不同信息源(早先的词)会根据其相关性获得不同程度的”注意力”。最终表示是多个信息源的加权组合

注意力与机器翻译

Attention 机制 为机器翻译任务带来了曙光

  • Attention 显著地提高了翻译算法的表现。使Decoder网络注意原文中的某些重要区域来得到更好的翻译。
  • Attention 解决了信息瓶颈问题。原先 Encoder-Decoder 网络的中间状态只能存储有限的文本信息,从繁重的记忆任务中解放出来了,它只需要完成如何分配注意力任务即可。
  • Attention 减轻了梯度消失问题。Attention 在网络后方到前方建立了连接捷径,使得梯度可以更好的传递。
  • Attention 提供了一些可解释性。通过观察网络运行过程中产生的注意力分布,可知道网络在输出某句话时都把注意力集中在哪里;而且通过训练网络还得到了一个免费的翻译词典(soft alignment), 尽管未曾明确地告诉网络两种语言之间的词汇对应关系,但是显然网络依然学习到了一个大体上是正确的词汇对应表。
  • Attention 代表一种更为广泛的运算。之前学 Attention机制在机器翻译问题上的应用,实际上 Attention 还可以使用于更多任务中。
    • Attention机制的广义定义:给定一组向量Value和一个查询Query,Attention是一种分配技术,根据Query需求和内容计算出Value的加权和。
    • Attention 被认为是大量信息的选择性总结归纳,或给定一些表示(query)的情况下,用一个固定大小的表示( Ck )来表示任意许多其他表示集合的方法(Key)。

Attention 类型

Attention

  • Hard Attention: 只选取向量中一个元素, 赋值1, 其余都是0
  • Soft Attention
Attention 种类 原理 实现方法 优点 缺点 其它
Hard Attention 只选取向量中一个元素, 赋值1, 其余都是0 1. max(a_ki) 最大采样, 选最高权重的隐含层
2. 按 a_ki 分布进行随机采样
简单 最大采样/随机采样选择信息, 导致损失函数与注意力分布函数关系不可导, 无法用BP训练
Soft Attention 常规意义上的Attention实现, 加权平均 逐点相乘再累加 Hard Attention改进 全局对齐,计算量大,运行效率低
Global Attention 以上都是Global 所有位置参与运算   计算量大  
Local Attention 改进: 只对局部计算注意力 找对齐位置pt,前后扩展D个长度(如正态分布),局部范围内计算注意力
1. pt周围固定范围
2. pt周围正态分布
计算量降低, 大大加速  
Not-Self Attention Encoder-Decoder结构只适用于seq2seq任务 Hhc 计算模式   无法解决非seq2seq任务, 如阅读理解 kv相同
Self Attention 自监督(Self-Attention)也叫 Intra-Attention, 寻找一段文本内关系 QKV 计算模式
用Query在数据库中按照Key进行筛选
自监督注意力适合非seq2seq任务
阅读理解,文本摘要,文本蕴含
  kv不同
Attention          

参考 第三章 Transformer原理、结构详解

Attention 是 BERT 乃至整个预训练语言模型的基石,接棒CNN/RNN,成为特征抽取的新利器。Attention is all you need !

注意力可视化

【2024-9-20】三蓝一棕 可视化注意力机制

self-attention

Self-Attention 是能力超强的特征提取器,跟 RNNCNN 相比

电影推荐

以电影推荐为例

传统推荐系统:特性向量点积用户偏好

  • 人工设计一些电影特征,比如:浪漫指数、动作指数,
  • 人工设计一些用户特征,例如:喜欢浪漫电影或动作片的可能性;

有了这两个维度的数据(特征向量)之后,对二者做点积(dot product), 得到电影属性与用户喜欢程度之间的匹配程度,用得分表示

  • 电影推荐:电影特征向量(浪漫、动作、喜剧)与用户特性向量(喜欢浪漫、动作、喜剧的程度)做点积运算

得分数值:

  • 如果特征符号相同,例如“浪漫电影 && 用户喜欢浪漫电影”, 或者“不是浪漫电影 && 用户不喜欢浪漫电影”,得到的点积就是正数;反之就是负数
  • 特征值大小决定该特征对总分的贡献大小: 一部电影可能有点浪漫,但不是很明显,或者用户可能只是不喜欢浪漫,但也没到讨厌的程度。

分析

  • 优点:简单直接,很容易上手;
  • 缺点:规模大了很难搞, 因为对几百万部电影打标的成本非常高,精确标记用户喜欢或不喜欢什么也几乎是不可能的。

基于 self-attention 的推荐系统

电影特征和用户特征作为模型参数,匹配已知用户偏好

两步:

  • 电影特征和用户特征不再直接做点积运算,而是作为模型参数(parameters of the model);
  • 收集少量的用户偏好作为目标,然后通过优化用户特征和电影特征(模型参数), 使二者的点积匹配已知的用户喜好。

这就是 self-attention 的基本原理。

以一串单词作为输入,原理上只要将其作为 input vector 送到 self-attention 模型。

  • 但实际上要对 input vector 做预处理,生成一个中间表示,即序列建模中的嵌入层。为每个单词 t 分配一个嵌入向量(embedding vector) 𝐯t(我们后面将学习到这个值)。
  • input vector -> embedding vector -> self-attention -> output vector
  • (the, cat) -> (V_the, V_cat) -> 加权求和 -> y_the, y_cat

不同于一般的 sequence-to-sequence 运算:

  • self-attention 将输入当做一个集合(set)而不是序列(sequence)。
  • 如果对输入序列进行重排(permute),输出序列除了也跟着重排,其他方面将完全相同,self-attention 是排列等变的(permutation equivariant)。
  • 构建完整的 transformer 时,会引入一些东西来保持输入的顺序信息,但要明白 self-attention 本身不关心输入的顺序属性(sequential nature)

Self-attention 是什么?

什么是self-attention

self-attention 运算是所有 transformer 架构的基本运算, 而 Self-attention 是 sequence-to-sequence 运算:

  • 输入一个向量序列(x1,x2,…,xm),输出另一个向量序列 (y1,y2,…,yn),所有字符都映射成k维向量;
  • 输出向量是x的加权平均: yi = ∑ wi * xi
  • 计算权重矩阵W 最简单函数就是点积(dot product): $ w_ij = x_i^T * x_j $
  • 结果取值范围是正负无穷,为了使累加和(表示概率)等于 100%, 需要做归一化, 即 softmax
  • 总结起来就是两点:
    • vector-to-vector 运算:self-attention 是对 input vector 做矩阵运算,得到一个加权结果作为 output vector;
    • 加权矩阵计算:权重矩阵不是常量,而是跟它所在的位置 (i,j) 直接相关,根据对应位置的 input vector 计算。
    • output vector 中的每个元素 yj 都是对 input vector 中所有元素的加权和;
    • 对于 yj,加权矩阵由 input 元素 xj 与每个 input 元素计算得到;

self-attention 是整个架构中唯一在 input & output vector 间做运算;

  • Transformer 架构中的其他运算都是单纯对 input vector 做运算。

self-attention 模型简单,本质是加权平均公式,为什么效果这么好?

self-attention 结构图。原文

  • 一个输入序列的向量集合(矩阵),经过Wq、Wk、Wv三个权重矩阵计算之后,生成了Q、K、V三个矩阵,经过FF网络,最后生成了新的向量集合。
  • img
  • img

attention 机制涉及两个隐状态: $ h_i $ 和 $s_t$,前者是输入序列第i个位置产生的隐状态,后者是输出序列在第t个位置产生的隐状态。

self-attention实际是:输出序列就是输入序列

因此,计算自己的 attention 得分,就叫做self-attention

最上层的 transformer 模块在处理单词「it」的时候会关注「a robot」,所以「a」、「robot」、「it」这三个单词与其得分相乘加权求和后的特征向量会被送入之后的神经网络层。

自注意力机制沿着序列中每一个单词的路径进行处理,主要由 3 个向量组成:

  • 查询向量(Query 向量):当前单词的查询向量和其它单词的键向量相乘,得到其它词相对于当前词的注意力得分。只关心目前正在处理的单词的查询向量。
  • 键向量 (Key 向量):键向量就像是序列中每个单词的标签,搜索相关单词时用来匹配的对象。
  • 值向量 (Value 向量):值向量是单词真正的表征,当算出注意力得分后,使用值向量进行加权求和得到能代表当前位置上下文的向量。

比喻: 档案柜中找文件

  • 查询向量就像一张便利贴,上面写着正在研究的课题。
  • 键向量像是档案柜中文件夹上贴的标签。当你找到和便利贴上所写相匹配的文件夹时,拿出它,文件夹里的东西便是值向量。只不过最后找的并不是单一的值向量,而是很多文件夹值向量的混合。

将单词查询向量分别乘以每个文件夹的键向量,得到各个文件夹对应的注意力得分

  • 乘指的是向量点乘,乘积会通过 softmax 函数处理。

将值向量加权混合得到一个向量

  • 将其 50% 的「注意力」放在了单词「robot」上,30% 的注意力放在了「a」上,还有 19% 的注意力放在「it」上

嵌入矩阵的每行都对应模型词汇表中一个单词的嵌入向量。乘法操作得到词汇表中每个单词对应的注意力得分

Attention 解决什么问题

背景:

  • RNN 处理序列数据时,token 逐个喂给模型。比如在a3的位置,模型要等a1和a2的信息都处理完成后,才可以生成a3。
  • 问题:
    • a. 随着序列长度增加,模型并行计算的能力变差。
    • b. 随着token间距离的增加,对于远距离处的信息,RNN很难捕获其依赖关系。

改进:提升模型的并行运算能力,序列中的每个token 无损地捕获序列里的其他tokens信息。

  • Attention: 在每个位置,例如在a2处产生b2时,attention将会同时看过a1到a4的每个token。此外,每个token生成其对应的输出的过程是同时进行的,计算不需要等待。
  • Attention 机制是Transformer架构引入的提取信息的方法。Attention机制通过对模型的输入部分赋予不同的权重,对value值进行加权求和。以此来抽取数据中更重要的信息。

Attention机制的核心: 从关注全部到关注重点

Attention机制本质是对源数据中元素的值(value)进行加权求和,而其中查询(query)和键(key)用于计算权重系数。

Attention函数本质:

  • Attention函数描述为将一个查询(query)映射到一系列键值对(key-value)的过程,其中通过计算查询(query)和键(key)的相似性或相关性来得到每个键对应值的权重系数,最终对值(value)进行加权求和,以产生Attention数值。

Attention的优点:

  1. 相对于传统的CNN和RNN,Attention参数数量更少。
  2. 使 Transformer模型在计算Query时实现并行计算
  3. 使得模型能够更好地捕捉长距离依赖关系,模型效果更好。

Self-Attention 解决什么问题

提出 Self-Attention 原因:

  • 传统 Attention 模块能捕获source端和target端的token间的依赖关系,但不能捕获source端或target端自身token间的依赖关系
  • self-attention 可以学习source端句子内部的token间的依赖关系,捕获句子的内部信息。

【2024-8-3】【LLM基础知识】LLMs-Attention知识总结笔记v4.0

Attention 和 Self-Attention 区别

Self-Attention 多两个约束条件:

  1. Q,K,V 计算输入同源,K-Q-V三者都来源于 X。
  2. Q,K,V 遵循 attention做法。

Self 的意思是 Attention完全来自输入序列自己,而不来自外部信息(比如output)。

Self-Attention 如何解决长距离依赖?

解决方式:

  • 利用注意力机制来“动态”生成不同连接的权重,从而处理变长的信息序列。

具体介绍:

  • 对于当前query,需要与句子中所有 key 进行点乘后再 Softmax ,以获得句子中所有 key 对于当前query的score(权重)
  • 然后与所有词 的value向量进行加权融合之后,就能使当前token学习到句子中其他词的信息;

self-attention 如何并行化?

Transformer 的并行化主要体现在 self-attention 模块

  • 在Encoder端 Transformer可以并行处理整个序列,并得到整个输入序列经过 Encoder 端的输出
  • 在 self-attention 模块,对于某个序列(x1,x2,…xn),self-attention 模块可以直接计算xi,xj的点乘结果,而RNN系列的模型就必须按照顺序从x1计算到xn。

Self-Attention 并行计算句子中不同的query,每个query之间并不存在先后依赖关系,使得transformer能够并行化;

Self-Attention 在计算的过程中,如何对padding位做mask? 知乎

Attention与MLP层的区别?

既然 Attention 是为了关注某些局部信息,那些不就相当于连上一层在关注的部分权重更大的全连接层吗,二者的区别何在?

  • Attention的最终输出可看成是一个“在关注部分权重更大的全连接层”。但是与全连接层的区别在于,注意力机制可利用输入特征信息确定哪些部分更重要

两个 FFN 层作用

注意力计算后,用了两个FFN层,为什么第一个FFN层先把维提升,第二个FFN层再把维度降回原大小?

解释

  • 1、提升维度:类似SVM kernel,通过提升维度识别一些在低维无法识别的特征。
  • 2、提升维度:更大的可训练参数,提升模型容量。
  • 3、降回原维度:方便多层注意力层和残差模块进行拼接,而无需进行额外处理。

QKV 哪里来?

WQ, WK, WV 由来

  • 先初始化为[h,h]维度,再用模型训练学习这里面的参数。

为什么要计算Q和K点乘?

Q和K点乘是为了计算序列中每个token与其他token的相似度, 得到 attention score 矩阵,用来对V进行提纯。

假设句子 “Hello, how are you?” 长度是6,embedding维度是 300,那么 Q,K,V都是(6, 300)的矩阵。

  • “Hello, how are you?” 这句话,当前token为”you”的时候,可知道”you“对于”Hello”, ” , “, “how”, “are”, “?”这几个token对应的关注度是多少。
  • 有了这个 attention score,可知道处理到”you“的时候,模型在关注句子中的哪些token。

Q和K为什么用不同?

Q和K 为什么用不同的权重矩阵进行线性变换投影?

  • 如果 WQ 和 WK 一样,则 QKᐪ结果是对称矩阵,这样就减弱了模型的表达能力
  • 同时在对称矩阵中,对角线的值会比较大,导致每个token过分关注自己。
  • 使用不同的投影矩阵,参数增多,可以增强模型表达能力。

Self-Attention 计算时乘上WQ.WK.WV的好处?

  1. 增加了参数量,增加模型的表达能力
  2. 加入了不同线性变换相当于对x 做了不同的投影,将向量x 投影到不同空间,增加模型的泛化能力
  3. 允许某个token对其他位置token的注意力大于对自己的注意力,才能更好的捕捉全局位置的注意力

知乎

能不能只用QV,KV或V?

为什么要用 Q,K,V? 仅仅使用QV,KV或者V行不行?

不行

  • 使用 QKV 主要为了增强网络的容量表达能力
  • self-attention 使用 Q,K,V 这三个参数独立,模型的表达能力和灵活性显然会比只用 QV 或者只用 V 要好些

自注意力实现

Self-Attention 计算公式

  • QKV计算不加偏置项: 缩放点积注意力:Scaled Dot-Product Attention
  • QKV计算加偏置项: Q = WQ*x+bQ

计算量多大

Self-Attention 时间复杂度多少?

Self-Attention 时间复杂度:O(n²⋅d)

  • n是序列的长度seq_length
  • d是embedding的维度d_model。

Self-Attention 包括三个步骤:相似度计算,softmax和加权平均,时间复杂度分别是:

  • 相似度计算: (n,d)(d,n)矩阵相乘: (n,d)∗(d,n)=O(n²⋅d)
  • softmax 直接计算,时间复杂度为 O(n²)
  • 加权平均: (n,n)(n,d)的矩阵相乘: (n,n)∗(n,d)=O(n²⋅d)

Self-Attention 时间复杂度是 O(n²⋅d)

知乎

Self-Attention和Multi-head Attention的参数量怎么计算?

self-attention块的模型参数有Q,K,V的权重矩阵WQ,WK,WV和偏置,输出权重矩阵Wo和偏置。

4个权重矩阵的形状为【h,h】,4个偏置的形状为【h】。总参数量为4h²+4h.

Multi-head Attention也符合这个参数量,但实际上需要分head计算再组合。

Context-attention 是什么?

知道了self-attention,那你肯定猜到了context-attention是什么了:它是encoder和decoder之间的attention!所以,你也可以称之为encoder-decoder attention!

context-attention一词并不是本人原创,有些文章或者代码会这样描述,我觉得挺形象的,所以在此沿用这个称呼。其他文章可能会有其他名称,但是不要紧,我们抓住了重点即可,那就是两个不同序列之间的attention,与self-attention相区别。

不管是self-attention还是context-attention,它们计算attention分数的时候,可以选择很多方式,比如上面表中提到的:

  • additive attention
  • local-base
  • general
  • dot-product
  • scaled dot-product

那么Transformer模型采用的是哪种呢?答案是:scaled dot-product attention

点乘注意力是什么?

什么是 点乘注意力 Scaled dot-product attention ?

论文Attention is all you need对 attention机制的描述:

An attention function can be described as a query and a set of key-value pairs to an output, where the query, keys, values, and output are all vectors. The output is computed as a weighted sum of the values, where the weight assigned to each value is computed by a compatibility of the query with the corresponding key.

翻译:通过确定Q和K之间的相似程度来选择V

公式描述更加清晰:

\[\text{Attention}(Q,K,V)=softmax(\frac{QK^T}{\sqrt d_k})V\]

scaled dot-product attentiondot-product attention唯一的区别:

  • scaled dot-product attention 有个缩放因子 $ \frac{1}{\sqrt d_k} $。

$d_k$ 表示 K的维度,论文里默认是64

为什么加缩放因子?

提示: 面试题

论文解释:

  • 对于 $d_k$ 很大时,点积得到的结果维度很大,使得结果处于 softmax 函数梯度很小的区域, 对反向传播不利。

为了克服这个负面影响,除以一个缩放因子,一定程度上减缓这种情况。

为什么是 1/dk ?

(1) 为什么是 $\frac{1}{\sqrt d_k}$

论文没有进一步说明。可用其他缩放因子,看看模型效果有没有提升。

提示: 面试题

scaled 参数除 $ \sqrt d_k $?

  1. 使 QKᐪ 结果满足 r ~ N(0,1) , 期望为0,方差为1的标准正态分布,类似于归一化
  2. 使输入值进入 softmax 敏感区间, 导数为0, 防止梯度消失

作者发现

  • 当维度dk值很大时,输入softmax的值QKᐪ就越大,导致后面的softmax计算会有极小的梯度,不利于更新学习
  • 因此除以dk,防止梯度消失。(softmax值过大,其偏导数趋于0)

(2) 必须是 $ \sqrt d_k $ 吗?

只要做到每层参数的梯度保持在训练敏感范围内,使网络好训练。缓解梯度消失就可以。

  • 不用除根号dk方式有 Google T5 的 Xavier初始化

(3) 为什么要满足 标准正态分布?

  • 权重一般初始化为正态分布

KQV 是什么

K、Q、V 是什么:

  • 在 encoder 的 self-attention中,Q、K、V都来自同一个地方(相等),他们是上一层encoder的输出。对于第一层encoder,它们就是word embedding和positional encoding相加得到的输入。
  • 在 decoder 的 self-attention中,Q、K、V都来自于同一个地方(相等),它们是上一层decoder的输出。对于第一层decoder,它们就是word embedding和positional encoding相加得到的输入。但是对于decoder,我们不希望它能获得下一个time step(即将来的信息),因此我们需要进行sequence masking
  • 在 encoder-decoder attention 中,Q来自于decoder的上一层的输出,K和V来自于encoder的输出,K和V是一样的。
  • Q、K、V三者的维度一样,即 $d_q=d_k=d_v$。

上面 scaled dot-product attention 和 decoder 的 self-attention都出现了masking这样一个东西。

那么这个mask到底是什么呢?这两处的mask操作是一样的吗?

self-attention 实现

基础 self-attention

最基础的 self-attention 模型实现:

  • 2次 矩阵乘法 和 1次 归一化(softmax)。
import torch
import torch.nn.functional as F

# 假设我们有一些 tensor x 作为输入,它是 (b, t, k) 维矩阵
x = ...

# torch.bmm() 是批量矩阵乘法(batched matrix multiplication)函数,对一批矩阵执行乘法操作
raw_weights = torch.bmm(x, x.transpose(1, 2))
# 正值化、归一化
weights = F.softmax(raw_weights, dim=2)
# 计算输出
y = torch.bmm(weights, x)

点乘注意力实现

scaled dot-product attention 实现代码如下:

import torch
import torch.nn as nn

class ScaledDotProductAttention(nn.Module):
    """Scaled dot-product attention mechanism."""

    def __init__(self, attention_dropout=0.0):
        super(ScaledDotProductAttention, self).__init__()
        self.dropout = nn.Dropout(attention_dropout)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, q, k, v, scale=None, attn_mask=None):
        """前向传播.

        Args:
        	q: Queries张量,形状为[B, L_q, D_q]
        	k: Keys张量,形状为[B, L_k, D_k]
        	v: Values张量,形状为[B, L_v, D_v],一般来说就是k
        	scale: 缩放因子,一个浮点标量
        	attn_mask: Masking张量,形状为[B, L_q, L_k]

        Returns:
        	上下文张量和attetention张量
        """
        attention = torch.bmm(q, k.transpose(1, 2))
        if scale:
        	attention = attention * scale
        if attn_mask:
        	# 给需要mask的地方设置一个负无穷
        	attention = attention.masked_fill_(attn_mask, -np.inf)
		# 计算softmax
        attention = self.softmax(attention)
		# 添加dropout
        attention = self.dropout(attention)
		# 和V做点积
        context = torch.bmm(attention, v)
        return context, attention

self-attention 改进

现代 transformer 对 self-attention 扩展

  • 引入控制参数(queries, keys and values)
  • 对点积做缩放处理(scaling the dot product)
    • softmax 函数对非常大的输入值敏感。这些 input 会梯度消失,学习变慢甚至完全停止。
    • 由于点积平均值随着嵌入维度 k 的增加而增大,因此点积送到 softmax 之前进行缩放有助于缓解这个问题。
    • $ w_ij = q_i^T k_j$ -> $ w_ij = \frac{q_i^T k_j}{\sqrt{k}}$
  • 引入 multi-head attention
    • 同一个单词随着相邻单词们的不同表示的意思也可能不同, 基本的 self-attention 欠缺了很多灵活性
    • 如何理解?让模型有更强的辨识力,一种解法:组合多个 self-attention(用 r 索引), 每个对应不同的 query/key/value 参数矩阵 $ 𝐖^r_q$ , $ 𝐖^r_k $, $ 𝐖^r_v $, 称为 attention heads(注意力头)。
    • 对于 input 𝐱i,每个 attention head 产生不同的 output vector $ 𝐲^r_i $(一部分输出)。 最后再将这些部分输出连接起来,通过线性变换来降维回 k。

multi-head self-attention 提效:query/key/value 降维

  • multi-head self-attention 看作多个并行 self-attention 机制,每个都有自己的键、值和查询转换。
  • Multi-head self-attention 的缺点: ,对于 R 头,慢 R 倍。

优化:

  • 实现这 multi-head self-attention,既能利用多个 self-attention 提升辨识力, 又与 single-head self-attention 基本一样快。
  • 每个 head 对 query/key/value 降维。

如果输入向量有 k=256 维,模型有 h=4 个 attention head,则降维操作包括:

  • 将输入向量乘以一个 256×64 矩阵,这会将 input vector 从 256 维降到 64 维;
  • 对于每个 head 需要执行 3 次降维:分别针对 query/key/value 的计算。

甚至只用三次 k×k 矩阵乘法就能实现 multi-head 功能, 唯一需要的额外操作是将生成的 output vector 重新按块排序

multi-head self-attention 完整流程

4-head self-attention 的直观解释。对输入进行降维,针对 key/value/query 分别进行矩阵运算来实现。

从左到右分为 5 列:

  • 原始 256-维 input vector;
  • 输入降维:将 input vector 乘以 256x64 矩阵,降维到 64 维;(256/4=64)
    • 注意:对每个 input vector 需要分别针对 query/key/value 降维,总共是 3 遍;
  • 将降维后的 input 分别输入多个并行的 self-attention;
  • 计算得到多个降维之后的 output vector;
  • 对低维度 output vectors 进行拼接,重新回到与 input vectors 一样的维度。

参数规模

Multi-head attention 又是什么

提示: 面试题

自注意力机制缺陷:

  • 模型在对当前位置信息进行编码时,会过度的将注意力集中于自身的位置

多头注意力机制解决这一问题,还能给予注意力层的输出包含有不同子空间中的编码表示信息,从而增强模型的表达能力。

多头自注意力机制允许模型在不同子空间上同时捕捉信息,从而增强了输入序列的表达能力。

  • 每个头关注输入序列的不同部分,然后将结果拼起来,以获得更全面的特征表示。

论文提到

  • 将 Q、K、V 通过一个线性映射之后,分成 h 份,对每份进行 scaled dot-product attention 效果更好。
  • 把各个部分结果合并起来,再次经过线性映射,得到最终的输出。

所谓的multi-head attention。超参数 h 是heads数量。论文默认是8

multi-head attention 结构图:

注意:

  • 分成 h 份是在 $d_k$、$d_q$、$d_v$ 维度上切分。
  • 因此,进入到 scaled dot-product attention 的 $d_k$ 实际上等于未进入之前的 $D_K/h$ 。

Multi-head attention 允许模型加入不同位置的表示子空间信息。

Multi-head attention 公式:

  • \[\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_ 1,\dots,\text{head}_ h)W^O\]

其中,$ \text{head}_ i = \text{Attention}(QW_i^Q,KW_i^K,VW_i^V) $

相同维度下用单头多头的区别是什么

  • $d_{model}=512$,$h=8$。
  • 在 scaled dot-product attention 里 $d_q = d_k = d_v = d_{model}/h = 512/8 = 64$

如果

  • h=1,得到一个各个位置只集中于自身位置的注意力权重矩阵;
  • h=2,得到另一个注意力权重稍微分配合理的权重矩阵;

多头恰好克服「模型在对当前位置的信息进行编码时,会过度的将注意力集中于自身的位置」的问题。

这里再插入一张真实场景下同一层的不同注意力权重矩阵可视化结果图:

当 h 不一样时,dk 取值也不一样,使得对权重矩阵的scale程度不一样。

当模型维度 dm 确定时,一定程度上 h 越大, 整个模型的表达能力越强,越能提高模型对于注意力权重的合理分配。

实现2

【2024-9-10】代码

import math
import torch
import torch.nn as nn
import torch.nn.functional as F

class SelfAttention(nn.Module):
    """
        自注意力机制
    """
    def __init__(self, d, d_q, d_k, d_v):
        super(SelfAttention, self).__init__()
        self.d = d
        self.d_q = d_q
        self.d_k = d_k
        self.d_v = d_v
        self.W_query = nn.Parameter(torch.rand(d, d_q))
        self.W_key = nn.Parameter(torch.rand(d, d_k))
        self.W_value = nn.Parameter(torch.rand(d, d_v))
        
    def forward(self, x):
        """
            前向传播
        """
        Q = x @ self.W_query
        K = x @ self.W_key
        V = x @ self.W_value
        attention_scores = Q @ K.T / math.sqrt(self.d_k)
        attention_weights = F.softmax(attention_scores, dim=-1)
        context_vector = attention_weights @ V
        return context_vector

if __name__ == '__main__':
    sentence = 'the quick brown fox jumps over the lazy dog'
    print('提取词库')
    dc = {s: i for i, s in enumerate(sorted(sentence.replace(',', '').split()))}
    print(dc)
    print('映射 str -> id')
    r = [dc[i] for i in sentence.replace(',', '').split()]
    sentence_int = torch.tensor(r)
    print(sentence_int)
    
    print('id -> embedding')
    vocab_size = 50000  # Assume a large vocabulary size
    torch.manual_seed(123)
    embed = nn.Embedding(vocab_size, 3)
    embedded_sentence = embed(sentence_int).detach()
    print(embedded_sentence)
    
    print('计算自注意力')
    sa = SelfAttention(d=3, d_q=2, d_k=2, d_v=4)
    cv = sa(embedded_sentence)
    print(cv.shape)
    print(cv)

输出

提取词库
{'brown': 0, 'dog': 1, 'fox': 2, 'jumps': 3, 'lazy': 4, 'over': 5, 'quick': 6, 'the': 8}
映射 str -> id
tensor([8, 6, 0, 2, 3, 5, 8, 4, 1])
id -> embedding
tensor([[ 0.4965, -1.5723,  0.9666],
        [-0.1690,  0.9178,  1.5810],
        [ 0.3374, -0.1778, -0.3035],
        [-0.2196, -0.3792,  0.7671],
        [-1.1925,  0.6984, -1.4097],
        [ 0.2692, -0.0770, -1.0205],
        [ 0.4965, -1.5723,  0.9666],
        [ 0.1794,  1.8951,  0.4954],
        [-0.5880,  0.3486,  0.6603]])
计算自注意力
torch.Size([9, 4])
tensor([[-0.0269, -0.0440, -0.0042,  0.0399],
        [ 0.4747,  0.1601,  0.6337,  0.7438],
        [ 0.1518, -0.0326,  0.0235,  0.2049],
        [ 0.1134, -0.0163,  0.0691,  0.1872],
        [ 0.0674, -0.0990, -0.1767,  0.0518],
        [ 0.1159, -0.0648, -0.0747,  0.1341],
        [-0.0269, -0.0440, -0.0042,  0.0399],
        [ 0.5645,  0.1703,  0.7147,  0.8803],
        [ 0.2060,  0.0059,  0.1400,  0.2985]], grad_fn=<MmBackward0>)

Multi-head attention的实现

multi-head attention 实现代码如下:

import torch
import torch.nn as nn

class MultiHeadAttention(nn.Module):

    def __init__(self, model_dim=512, num_heads=8, dropout=0.0):
        super(MultiHeadAttention, self).__init__()

        self.dim_per_head = model_dim // num_heads
        self.num_heads = num_heads
        self.linear_k = nn.Linear(model_dim, self.dim_per_head * num_heads)
        self.linear_v = nn.Linear(model_dim, self.dim_per_head * num_heads)
        self.linear_q = nn.Linear(model_dim, self.dim_per_head * num_heads)

        self.dot_product_attention = ScaledDotProductAttention(dropout)
        self.linear_final = nn.Linear(model_dim, model_dim)
        self.dropout = nn.Dropout(dropout)
		# multi-head attention之后需要做layer norm
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, key, value, query, attn_mask=None):
		# 残差连接
        residual = query

        dim_per_head = self.dim_per_head
        num_heads = self.num_heads
        batch_size = key.size(0)

        # linear projection
        key = self.linear_k(key)
        value = self.linear_v(value)
        query = self.linear_q(query)

        # split by heads
        key = key.view(batch_size * num_heads, -1, dim_per_head)
        value = value.view(batch_size * num_heads, -1, dim_per_head)
        query = query.view(batch_size * num_heads, -1, dim_per_head)

        if attn_mask:
            attn_mask = attn_mask.repeat(num_heads, 1, 1)
        # scaled dot product attention
        scale = (key.size(-1) // num_heads) ** -0.5
        context, attention = self.dot_product_attention(
          query, key, value, scale, attn_mask)

        # concat heads
        context = context.view(batch_size, -1, dim_per_head * num_heads)

        # final linear projection
        output = self.linear_final(context)

        # dropout
        output = self.dropout(output)

        # add residual and norm layer
        output = self.layer_norm(residual + output)

        return output, attention

终于出现了Residual connectionLayer normalization。现在来解释它们。

Attention 细节

2.1. 点积attention

介绍一下attention的具体计算方式。attention 很多种计算方式:

  • 加性 attention
  • 点积 attention
  • 带参数的计算方式

着重介绍一下点积attention的公式:

  • [公式]
  • Attention中(Q^T)*K矩阵计算,query和key的维度要保持一致

如上图所示, [公式] , [公式] 分别是query和key,其中,query可以看作M个维度为d的向量(长度为M的sequence的向量表达)拼接而成,key可以看作N个维度为d的向量(长度为N的sequence的向量表达)拼接而成。

  • 【一个小问题】为什么有缩放因子 [公式] ?
  • 先一句话回答这个问题: 缩放因子的作用是归一化。
  • 假设[公式] , [公式]里的元素的均值为0,方差为1,那么 [公式] 中元素的均值为0,方差为d. 当d变得很大时, [公式] 中的元素的方差也会变得很大,如果 [公式] 中的元素方差很大,那么[公式] 的分布会趋于陡峭(分布的方差大,分布集中在绝对值大的区域)。总结一下就是[公式]的分布会和d有关。因此[公式] 中每一个元素乘上 [公式] 后,方差又变为1。这使得[公式] 的分布“陡峭”程度与d解耦,从而使得训练过程中梯度值保持稳定。

2.2. Attention机制涉及到的参数

一个完整的attention层涉及到的参数有:

  • [公式] , [公式] , [公式]分别映射到[公式] , [公式] , [公式]的线性变换矩阵 [公式] ( [公式] ), [公式]( [公式] ), [公式] ( [公式] )
  • 把输出的表达 [公式] 映射为最终输出 [公式] 的线性变换矩阵 [公式] ( [公式] )

2.3. Query, Key, Value

Query和Key作用得到的attention权值作用到Value上。因此它们之间的关系是:

  1. Query [公式] 和 Key[公式]的维度必须一致,Value [公式] 和Query/Key的维度可以不一致。
  2. Key [公式]和Value [公式]的长度必须一致。Key和Value本质上对应了同一个Sequence在不同空间的表达。
  3. Attention得到的Output [公式] 的维度和Value的维度一致,长度和Query一致。
  4. Output每个位置 i 是由value的所有位置的vector加权平均之后的向量;而其权值是由位置为i 的query和key的所有位置经过attention计算得到的 ,权值的个数等于key/value的长度。

  • Attention示意图

在经典的Transformer结构中,我们记线性映射之前的Query, Key, Value为q, k, v,映射之后为Q, K, V。那么:

  1. self-attention的q, k, v都是同一个输入, 即当前序列由上一层输出的高维表达。
  2. cross-attention的q代表当前序列,k,v是同一个输入,对应的是encoder最后一层的输出结果(对decoder端的每一层来说,保持不变)

而每一层线性映射参数矩阵都是独立的,所以经过映射后的Q, K, V各不相同,模型参数优化的目标在于将q, k, v被映射到新的高维空间,使得每层的Q, K, V在不同抽象层面上捕获到q, k, v之间的关系。一般来说,底层layer捕获到的更多是lexical-level的关系,而高层layer捕获到的更多是semantic-level的关系。

2.4. Attention 作用

下面这段我会以机器翻译为例,用通俗的语言阐释一下attention的作用,以及query, key, value的含义。

Transformer模型Encoder, Decoder的细节图(省去了FFN部分)

query对应的是需要被表达的序列(称为序列A),key和value对应的是用来表达A的序列(称为序列B)。其中key和query是在同一高维空间中的(否则无法用来计算相似程度),value不必在同一高维空间中,最终生成的output和value在同一高维空间中。上面这段巨绕的话用一句更绕的话来描述一下就是:

序列A和序列B在高维空间 [公式] 中的高维表达 [公式] 的每个位置分别和 [公式] 计算相似度,产生的权重作用于序列B在高维空间 [公式] 中的高维表达 [公式] ,获得序列A在高维空间 [公式] 中的高维表达 [公式]

Encoder部分中只存在self-attention,而Decoder部分中存在self-attention和cross-attention

  • 【self-attention】encoder中的self-attention的query, key, value都对应了源端序列(即A和B是同一序列),decoder中的self-attention的query, key, value都对应了目标端序列。
  • 【cross-attention】decoder中的cross-attention的query对应了目标端序列,key, value对应了源端序列(每一层中的cross-attention用的都是encoder的最终输出)

2.5. Decoder端的Mask

Transformer模型属于自回归模型(p.s. 非自回归的翻译模型我会专门写一篇文章来介绍),也就是说后面的token的推断是基于前面的token的。Decoder端的Mask的功能是为了保证训练阶段和推理阶段的一致性。

论文原文中关于这一点的段落如下:

We also modify the self-attention sub-layer in the decoder stack to prevent from attending to subsequent positions. This masking, combined with the fact that the output embeddings are offset by one position, ensures that the predictions for position i can depend only on the known outputs at positions less than i.

在推理阶段,token是按照从左往右的顺序推理的。也就是说,在推理timestep=T的token时,decoder只能“看到”timestep < T的 T-1 个Token, 不能和timestep大于它自身的token做attention(因为根本还不知道后面的token是什么)。为了保证训练时和推理时的一致性,所以,训练时要同样防止token与它之后的token去做attention。

2.6. 多头Attention (Multi-head Attention)

Attention是将query和key映射到同一高维空间中去计算相似度,而对应的multi-head attention把query和key映射到高维空间 [公式] 的不同子空间 [公式] 中去计算相似度。

为什么要做multi-head attention?论文原文里是这么说的:

Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions. With a single attention head, averaging inhibits this.

也就是说,这样可以在不改变参数量的情况下增强每一层attention的表现力。

Multi-head Attention示意图

Multi-head Attention的本质是,在参数总量保持不变的情况下,将同样的query, key, value映射到原来的高维空间的不同子空间中进行attention的计算,在最后一步再合并不同子空间中的attention信息。这样降低了计算每个head的attention时每个向量的维度,在某种意义上防止了过拟合;由于Attention在不同子空间中有不同的分布,Multi-head Attention实际上是寻找了序列之间不同角度的关联关系,并在最后concat这一步骤中,将不同子空间中捕获到的关联关系再综合起来。

从上图可以看出, [公式][公式] 之间的attention score从1个变成了h个,这就对应了h个子空间中它们的关联度。

  1. Transformer模型架构中的其他部分

3.1. Feed Forward Network

每一层经过attention之后,还会有一个FFN,这个FFN的作用就是空间变换。FFN包含了2层linear transformation层,中间的激活函数是ReLu。

曾经我在这里有一个百思不得其解的问题:attention层的output最后会和 [公式] 相乘,为什么这里又要增加一个2层的FFN网络?

其实,FFN的加入引入了非线性(ReLu激活函数),变换了attention output的空间, 从而增加了模型的表现能力。把FFN去掉模型也是可以用的,但是效果差了很多。

3.2. Positional Encoding

位置编码层只在encoder端和decoder端的embedding之后,第一个block之前出现,它非常重要,没有这部分,Transformer模型就无法用。位置编码是Transformer框架中特有的组成部分,补充了Attention机制本身不能捕捉位置信息的缺陷。

  • position encoding

Positional Embedding的成分直接叠加于Embedding之上,使得每个token的位置信息和它的语义信息(embedding)充分融合,并被传递到后续所有经过复杂变换的序列表达中去。

论文中使用的Positional Encoding(PE)是正余弦函数,位置(pos)越小,波长越长,每一个位置对应的PE都是唯一的。同时作者也提到,之所以选用正余弦函数作为PE,是因为这可以使得模型学习到token之间的相对位置关系:因为对于任意的偏移量k, [公式] 可以由 [公式] 的线性表示:

  • [公式]
  • [公式]

上面两个公式可以由 [公式][公式]的线性组合得到。也就是 [公式]乘上某个线性变换矩阵就得到了[公式]

p.s. 后续有一个工作在attention中使用了“相对位置表示” (Self-Attention with Relative Position Representations) ,有兴趣可以看看。

3.3. Layer Normalization

在每个block中,最后出现的是Layer Normalization。Layer Normalization是一个通用的技术,其本质是规范优化空间,加速收敛。

当我们使用梯度下降法做优化时,随着网络深度的增加,数据的分布会不断发生变化,假设feature只有二维,那么用示意图来表示一下就是:

数据的分布发生变化,左图比较规范,右图变得不规范

为了保证数据特征分布的稳定性(如左图),我们加入Layer Normalization,这样可以加速模型的优化速度。

Residual connection是什么?

残差连接其实很简单!给你看一张示意图你就明白了:

  • residual_conn
    Figure 5. Residual connection.

假设网络中某个层对输入x作用后的输出是$F(x)$,那么增加residual connection之后,就变成了:$F(x)+x$

这个+x操作就是一个shortcut。那么残差结构有什么好处呢?显而易见:因为增加了一项$x$,那么该层网络对x求偏导的时候,多了一个常数项$1$!所以在反向传播过程中,梯度连乘,也不会造成梯度消失

所以,代码实现residual connection很非常简单:

def residual(sublayer_fn,x):
	return sublayer_fn(x)+x

文章开始的transformer架构图中的Add & Norm中的Add也就是指的这个shortcut

至此,residual connection的问题理清楚了。更多关于残差网络的介绍可以看文末的参考文献。

Pre-LN VS Post-LN

【2023-6-14】此「错」并非真的错:从四篇经典论文入手,理解Transformer架构图「错」在何处

  • Sebastian: 指出谷歌大脑团队论文《Attention Is All You Need》中 Transformer 构架图与代码不一致
  • 最初的 Transformer 构架图确实与代码不一致, 但 2017 年提交的代码版本进行了修改,但同时没有更新架构图。这也是造成「不一致」讨论的根本原因。

Layer Norm 位置 – 详细解答见为什么Pre Norm的效果不如Post Norm?

  • Pre-LN: LN 在 self-attention 之前, 放残差连接里
    • 效果:梯度更好, 可解决梯度问题, 更容易训练, 但可能导致表征崩溃
    • 分析:
      • 容易训练: 因为 恒等路径更突出
      • 效果不好: Pre Norm结构无形地增加了模型宽度而降低了模型深度,而深度通常比宽度更重要,所以降低深度导致最终效果变差了
  • Post-LN: LN 在 self-attention 和 FFN 之后
    • 效果: 预期的梯度被放大, 最终效果更好
    • 分析: 每Norm一次就削弱一次恒等分支的权重,所以Post Norm反而是更突出残差分支的,因此Post Norm中的层数更加“足秤”,一旦训练好之后效果更优。
  • Deep-LN: 未知

结论:

同一设置之下,Pre Norm 结构往往更容易训练,但最终效果通常不如Post Norm。

2020年的论文: On Layer Normalization in the Transformer Architecture

Transformer 架构论文中的层归一化表明,Pre-LN 工作得更好,可解决梯度问题。

  • 面试题目: 为什么 Trans/GPT-1 采用 Post-LN 而 GPT-2 采用 Pre-LN ?

Layer normalization是什么?

GRADIENTS, BATCH NORMALIZATION AND LAYER NORMALIZATION一文对normalization有很好的解释:

Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。

说到normalization,那就肯定得提到Batch Normalization。BN在CNN等地方用得很多。

BN的主要思想就是:在每一层的每一批数据上进行归一化。

我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。

BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:

可以看到,右半边求均值是沿着数据批量N的方向进行的

Batch normalization的计算公式如下:

  • \[BN(x_i)=\alpha\times\frac{x_i-u_B}{\sqrt{\sigma_B^2+\epsilon}}+\beta\]

具体的实现可以查看上图的链接文章。

说完Batch normalization,就该说说咱们今天的主角Layer normalization

那么什么是Layer normalization呢?:它也是归一化数据的一种方式,不过LN是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差

下面是LN的示意图:

  • layer_normalization
    Figure 7. Layer normalization example.

和上面的BN示意图一比较就可以看出二者的区别啦!

下面看一下LN的公式,也BN十分相似:

  • \[LN(x_i)=\alpha\times\frac{x_i-u_L}{\sqrt{\sigma_L^2+\epsilon}}+\beta\]

Layer normalization的实现

上述两个参数$\alpha$和$\beta$都是可学习参数。下面我们自己来实现Layer normalization(PyTorch已经实现啦!)。代码如下:

import torch
import torch.nn as nn


class LayerNorm(nn.Module):
    """实现LayerNorm。其实PyTorch已经实现啦,见nn.LayerNorm。"""

    def __init__(self, features, epsilon=1e-6):
        """Init.

        Args:
            features: 就是模型的维度。论文默认512
            epsilon: 一个很小的数,防止数值计算的除0错误
        """
        super(LayerNorm, self).__init__()
        # alpha
        self.gamma = nn.Parameter(torch.ones(features))
        # beta
        self.beta = nn.Parameter(torch.zeros(features))
        self.epsilon = epsilon

    def forward(self, x):
        """前向传播.

        Args:
            x: 输入序列张量,形状为[B, L, D]
        """
        # 根据公式进行归一化
        # 在X的最后一个维度求均值,最后一个维度就是模型的维度
        mean = x.mean(-1, keepdim=True)
        # 在X的最后一个维度求方差,最后一个维度就是模型的维度
        std = x.std(-1, keepdim=True)
        return self.gamma * (x - mean) / (std + self.epsilon) + self.beta

顺便提一句,Layer normalization多用于RNN这种结构。

Mask 是什么?

现在终于轮到讲解mask了!mask顾名思义就是掩码,在我们这里的意思大概就是对某些值进行掩盖,使其不产生效果

需要说明的是,我们的Transformer模型里面涉及两种mask。分别是padding masksequence mask。其中后者我们已经在decoder的self-attention里面见过啦!

  • padding mask在所有的scaled dot-product attention里面都需要用到
  • sequence mask只有在decoder的self-attention里面用到。

所以,我们之前ScaledDotProductAttentionforward方法里面的参数attn_mask在不同的地方会有不同的含义。这一点我们会在后面说明。

Padding mask

什么是padding mask呢?回想一下,我们的每个批次输入序列长度是不一样的!也就是说,我们要对输入序列进行对齐!具体来说,就是给在较短的序列后面填充0。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

具体的做法是,把这些位置的值加上一个非常大的负数(可以是负无穷),这样的话,经过softmax,这些位置的概率就会接近0

而我们的padding mask实际上是一个张量,每个值都是一个Boolen,值为False的地方就是我们要进行处理的地方。

下面是实现:

def padding_mask(seq_k, seq_q):
	# seq_k和seq_q的形状都是[B,L]
    len_q = seq_q.size(1)
    # `PAD` is 0
    pad_mask = seq_k.eq(0)
    pad_mask = pad_mask.unsqueeze(1).expand(-1, len_q, -1)  # shape [B, L_q, L_k]
    return pad_mask

Sequence mask

文章前面也提到,sequence mask是为了使得decoder不能看见未来的信息。也就是对于一个序列,在time_step为t的时刻,我们的解码输出应该只能依赖于t时刻之前的输出,而不能依赖t之后的输出。因此我们需要想一个办法,把t之后的信息给隐藏起来。

那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为1,下三角的值权威0,对角线也是0。把这个矩阵作用在每一个序列上,就可以达到我们的目的啦。

具体的代码实现如下:

def sequence_mask(seq):
    batch_size, seq_len = seq.size()
    mask = torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8),
                    diagonal=1)
    mask = mask.unsqueeze(0).expand(batch_size, -1, -1)  # [B, L, L]
    return mask

哈佛大学的文章The Annotated Transformer有一张效果图:

  • sequence_mask
    Figure 8. Sequence mask.

值得注意的是,本来mask只需要二维的矩阵即可,但是考虑到我们的输入序列都是批量的,所以我们要把原本二维的矩阵扩张成3维的张量。上面的代码可以看出,我们已经进行了处理。

回到本小结开始的问题,attn_mask参数有几种情况?分别是什么意思?

  • 对于decoder的self-attention,里面使用到的scaled dot-product attention,同时需要padding masksequence mask作为attn_mask,具体实现就是两个mask相加作为attn_mask。
  • 其他情况,attn_mask一律等于padding mask

至此,mask相关的问题解决了。

Positional encoding 是什么?

【2021-8-25】面经:什么是Transformer位置编码?

位置编码是结构图里的Positional encoding

序列顺序是一个很重要的信息,如果缺失,结果就是:所有词语都对了,但是无法组成有意义的语句

Self-attention 一次性将所有字都当做输入,感知不到方向、位置、间距。

但是NLP输入的文本要按照一定的顺序才可以。不同语序就有不同语义。

  • 句子1:我喜欢吃洋葱
  • 句子2:洋葱喜欢吃我

Transformer结构为了更好的发挥并行输入的特点,首先要让输入内容具有一定位置信息。

于是,论文提出了Positional encoding

  • 对序列中的词语出现的位置进行编码,如果对位置进行编码,那模型就可以捕捉顺序信息

为什么用位置编码

【2023-6-13】如何理解Transformer论文中的positional encoding,和三角函数有什么关系?

为什么考虑顺序?

  • 捕捉序列顺序,交换单词位置后 attention map 的对应位置数值也会交换,产生数值变化,补充词序信息。
  • 不同距离的单词影响程度不同

Transformer 模型本身不包含循环卷积结构,无法捕捉序列中的位置信息。因此,需要额外的位置编码来提供每个位置上的信息,以便模型能够区分不同位置的输入元素。

为什么用相对位置?

tokens 位置信息有:

  • (1)绝对位置信息。a1是第一个token,a2是第二个token……
  • (2)相对位置信息。a2在a1的后面一位,a4在a2的后面两位……
  • (3)不同位置间距。a1和a3差两个位置,a1和a4差三个位置….

但是这些信息 self-attention 都无法分辩

  • 因为self-attention的运算是无向的。
  • 需要想办法把tokens的位置信息喂给模型。

分析

  • 顺序编码: 无界且不利于模型泛化
  • 相对编码: 相对距离不同,无法反应间距信息
    • 如将位置编号归一化到 [0,1] 区间
    • 同样是间隔3个位置,相对距离可能是 0.33, 0.5, …
  • 理想的编码方式满足:
    • (1)能表示token在序列中的绝对位置,且连续有界
    • (2)序列长度不同时,token的相对位置/距离也要保持一致
    • (3)支持训练过程中没有的句子长度。
    • (4)不同位置向量可以通过线性变化得到

二进制函数不行?

  • 0/1 离散空间,one-hot表示字符,显示不出差距

为什么用sin函数?

  • 词序信息表示方法很丰富,但都需要对不同维度的不同位置生成合理的数值表示。
    • 合理:不同位置的同一维度的位置向量之间,含有相对位置信息,而相对位置信息可以通过函数周期性实现。
    • 论文解释:对不同维度使用不同频率的正/余弦公式进而生成不同位置的高维位置向量。
  • 猜测
    • 周期性频率采样(OFDM、FFT等),可能确实迎合了语言的某种属性。比如,诗歌就是一类频率体现很明显的句子。

为什么奇偶维度之间需要作出区分,分别使用 sin 和 cos 呢?

  • 三角函数的积化和差公式
  • 奇偶区分可以通过全连接层帮助重排坐标,所以直接简单地分为两段(前 256 维使用 sin,后 256 维使用 cos)。

即满足(4)不同位置向量可以通过线性变化得到

  • sin(a+b)=sin(a)
\[\left(\begin{array}{c} \cos (\theta+\phi) \\ \sin (\theta+\phi) \end{array}\right)=\left(\begin{array}{cc} \cos \phi & -\sin \phi \\ \sin \phi & \cos \phi \end{array}\right)\left(\begin{array}{c} \cos \theta \\ \sin \theta \end{array}\right)\]

Transformer位置编码可视化

  • 一串序列长度为50,位置编码维度为128的位置编码可视化结果
  • 由于sin/cos函数的性质,位置向量每个值都位于[-1, 1]之间。
  • 同时,纵向来看,图的右半边几乎都是蓝色的,因为越往后的位置,频率越小,波长越长,所以不同的t对最终结果影响不大。而越往左边走,颜色交替的频率越频繁。

位置编码分类

位置编码分类

位置编码分为两个类型:函数型表格型

  • 函数型:通过输入token位置信息得到相应的位置编码;
    • 方法①:使用[0, 1]范围分配。第一个token分配0,最后一个token分配去1,其余token按照文章长度平均分配。
      • 示例:
        • 我喜欢吃洋葱 【0 0.16 0.32.....1】
        • 我真的不喜欢吃洋葱【0 0.125 0.25.....1】
      • 问题:如果句子长度不同,那么位置编码是不一样,所以, 无法表示句子之间有什么相似性
    • 方法②:1-n正整数范围分配
      • 直观,按照输入顺序,一次分配给token所在的索引位置。具体形式如下:
        • 我喜欢吃洋葱 【1,2,3,4,5,6】
        • 我真的不喜欢吃洋葱【1,2,3,4,5,6,7】
      • 问题:句子越长,后面值越大,数字越大说明这个位置占的权重也越大,无法凸显每个位置的真实权重
    • 总结:
      • 过去的方法有各种不足,所以Transformer对于位置信息编码做了改进
  • 表格型:建长度为L的词表,按词表长度来分配位置id
    • 相对位置编码关注token与token距离的相对位置(距离差几个token)。位置1和位置2的距离比位置3和位置10的距离更近,位置1和位置2与位置3和位置4都只相差1。这种方法可以知道单词之间的距离远近关系。
    • 图示
    • 问题:虽说可以表示出相对的距离关系,但是也有局限。
      • 只能的到相对关系,无法得到方向关系。对于两个token谁在谁的前面/后面,无法判断。

Transformer位置编码采用函数型,GPT-3论文给出公式:

  • 公式
  • 注意:每一个Token的位置信息编码不是数字,而是一个不同频率分割出来,和文本一样维度的向量。不同频率是通过Wn来表示。
  • 得到位置向量P之后,将和模型的embedding向量相加,得到进入Transformer模型的最终表示 公式, 其中,$w_i=1/10000^{2i/d_{model}}$, t是每个token的位置,比如说是位置1,位置2,以及位置n

transformer怎么做呢?论文的实现很有意思,使用正余弦函数。公式如下:

  • \[PE(pos,2i) = sin(pos/10000^{2i/d_{model}})\]
  • \[PE(pos,2i+1) = cos(pos/10000^{2i/d_{model}})\]

其中,pos是指词语在序列中的位置。可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码

上面公式中的$d_{model}$是模型的维度,论文默认是512

这个编码公式的意思就是:给定词语的位置$\text{pos}$,我们可以把它编码成$d_{model}$维的向量!也就是说,位置编码的每一个维度对应正弦曲线,波长构成了从$2\pi\(到\)10000*2\pi$的等比序列。

上面的位置编码是绝对位置编码。但是词语的相对位置也非常重要。这就是论文为什么要使用三角函数的原因!

正弦函数能够表达相对位置信息。主要数学依据是以下两个公式:

  • \[sin(\alpha+\beta) = sin\alpha cos\beta + cos\alpha sin\beta\]
  • \[cos(\alpha+\beta) = cos\alpha cos\beta - sin\alpha sin\beta\]

上面的公式说明,对于词汇之间的位置偏移k,$PE(pos+k)$可以表示成$PE(pos)$和$PE(k)$的组合形式,这就是表达相对位置的能力!

以上就是$PE$的所有秘密。说完了positional encoding,那么我们还有一个与之处于同一地位的word embedding

Word embedding大家都很熟悉了,它是对序列中的词汇的编码,把每一个词汇编码成$d_{model}$维的向量!看到没有,Postional encoding是对词汇的位置编码,word embedding是对词汇本身编码

所以,我更喜欢positional encoding的另外一个名字Positional embedding

图解位置编码

输入 attention 结构之前,每个字做 word embedding 和 positional embedding。

  • 加位置 embedding是为了服务于 self-attention 的目标,即得到一个 word 序列中每两个word 之间的相关性。
  • word之间的相关性,只跟相对位置有关、而与绝对位置无关。img
  • img

Positional encoding 实现

PE的实现也不难,按照论文的公式即可。代码如下:

import torch
import torch.nn as nn

class PositionalEncoding(nn.Module):
    
    def __init__(self, d_model, max_seq_len):
        """初始化。
        Args:
            d_model: 一个标量。模型的维度,论文默认是512
            max_seq_len: 一个标量。文本序列的最大长度
        """
        super(PositionalEncoding, self).__init__()
        
        # 根据论文给的公式,构造出PE矩阵
        position_encoding = np.array([
          [pos / np.pow(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)]
          for pos in range(max_seq_len)])
        # 偶数列使用sin,奇数列使用cos
        position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
        position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

        # 在PE矩阵的第一行,加上一行全是0的向量,代表这`PAD`的positional encoding
        # 在word embedding中也经常会加上`UNK`,代表位置单词的word embedding,两者十分类似
        # 那么为什么需要这个额外的PAD的编码呢?很简单,因为文本序列的长度不一,我们需要对齐,
        # 短的序列我们使用0在结尾补全,我们也需要这些补全位置的编码,也就是`PAD`对应的位置编码
        pad_row = torch.zeros([1, d_model])
        position_encoding = torch.cat((pad_row, position_encoding))
        
        # 嵌入操作,+1是因为增加了`PAD`这个补全位置的编码,
        # Word embedding中如果词典增加`UNK`,我们也需要+1。看吧,两者十分相似
        self.position_encoding = nn.Embedding(max_seq_len + 1, d_model)
        self.position_encoding.weight = nn.Parameter(position_encoding,
                                                     requires_grad=False)
    def forward(self, input_len):
        """神经网络的前向传播。
        Args:
          input_len: 一个张量,形状为[BATCH_SIZE, 1]。每一个张量的值代表这一批文本序列中对应的长度。

        Returns:
          返回这一批序列的位置编码,进行了对齐。
        """
        # 找出这一批序列的最大长度
        max_len = torch.max(input_len)
        tensor = torch.cuda.LongTensor if input_len.is_cuda else torch.LongTensor
        # 对每一个序列的位置进行对齐,在原序列位置的后面补上0
        # 这里range从1开始也是因为要避开PAD(0)的位置
        input_pos = tensor(
          [list(range(1, len + 1)) + [0] * (max_len - len) for len in input_len])
        return self.position_encoding(input_pos)

Word embedding的实现

Word embedding应该是老生常谈了,它实际上就是一个二维浮点矩阵,里面的权重是可训练参数,我们只需要把这个矩阵构建出来就完成了word embedding的工作。

所以,具体的实现很简单:

import torch.nn as nn


embedding = nn.Embedding(vocab_size, embedding_size, padding_idx=0)
# 获得输入的词嵌入编码
seq_embedding = seq_embedding(inputs)*np.sqrt(d_model)

上面vocab_size就是词典的大小,embedding_size就是词嵌入的维度大小,论文里面就是等于$d_{model}=512$。所以word embedding矩阵就是一个vocab_size*embedding_size的二维张量。

如果你想获取更详细的关于word embedding的信息,可以看我的另外一个文章word2vec的笔记和实现

位置编码: BERT vs Trans

各个模型的位置编码差异

  • Word2Vec 没有位置编码
  • Trans 位置编码是一个sin和cos函数算出来的固定值,只能标记这是某一个位置,并不能标记这个位置有啥用。
    • 满足条件:绝对位置、相对位置、考虑远近、便于线性变换
  • BERT 位置编码是一个可学习的embedding,所以不仅可以标注这一个位置,还能学习这个位置有什么作用
    • 维护3个embedding矩阵,词、段、位置。词是怎么取embedding的,段和位置就怎么取embedding
    • img

BERT

与 TransformerEncoder不同, BERTEncoder 使用片段嵌入和可学习的位置嵌入

  • nn.Parameter 传入的是一个随机数
#@save
class BERTEncoder(nn.Module):
    """BERT编码器"""
    def __init__(self, vocab_size, num_hiddens, norm_shape, ffn_num_input,
                 ffn_num_hiddens, num_heads, num_layers, dropout,
                 max_len=1000, key_size=768, query_size=768, value_size=768,
                 **kwargs):
        super(BERTEncoder, self).__init__(**kwargs)
        self.token_embedding = nn.Embedding(vocab_size, num_hiddens)
        self.segment_embedding = nn.Embedding(2, num_hiddens)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module(f"{i}", d2l.EncoderBlock(
                key_size, query_size, value_size, num_hiddens, norm_shape,
                ffn_num_input, ffn_num_hiddens, num_heads, dropout, True))
        # 在BERT中,位置嵌入是可学习的,因此我们创建一个足够长的位置嵌入参数
        self.pos_embedding = nn.Parameter(torch.randn(1, max_len, num_hiddens))

    def forward(self, tokens, segments, valid_lens):
        # 在以下代码段中,X的形状保持不变:(批量大小,最大序列长度,num_hiddens)
        X = self.token_embedding(tokens) + self.segment_embedding(segments)
        X = X + self.pos_embedding.data[:, :X.shape[1], :]
        for blk in self.blks:
            X = blk(X, valid_lens)
        return X

详见:BERT

Position-wise Feed-Forward network是什么?

这就是一个全连接网络,包含两个线性变换和一个非线性函数(实际上就是ReLU)。公式如下:

\[FFN(x)=max(0,xW_1+b_1)W_2+b_2\]

这个线性变换在不同的位置都表现地一样,并且在不同的层之间使用不同的参数。

论文提到,这个公式还可以用两个核大小为1的一维卷积来解释,卷积的输入输出都是$d_{model}=512$,中间层的维度是$d_{ff}=2048$。

实现如下:

import torch
import torch.nn as nn


class PositionalWiseFeedForward(nn.Module):

    def __init__(self, model_dim=512, ffn_dim=2048, dropout=0.0):
        super(PositionalWiseFeedForward, self).__init__()
        self.w1 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.w2 = nn.Conv1d(model_dim, ffn_dim, 1)
        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(model_dim)

    def forward(self, x):
        output = x.transpose(1, 2)
        output = self.w2(F.relu(self.w1(output)))
        output = self.dropout(output.transpose(1, 2))

        # add residual and norm layer
        output = self.layer_norm(x + output)
        return output

Transformer 实现

pytorch 版本

Transformer模型的PyTorch实现

  • Google 2017年的论文 Attention is all you need 阐释了什么叫做大道至简!该论文提出了Transformer模型,完全基于Attention mechanism,抛弃了传统的RNNCNN
  • 根据论文的结构图,一步一步使用 PyTorch 实现这个Transformer模型。

自注意力实现

Self-Attention的代码实现

# Self-Attention 机制的实现
from math import sqrt
import torch
import torch.nn as nn

class Self_Attention(nn.Module):
    # input : batch_size * seq_len * input_dim
    # q : batch_size * input_dim * dim_k
    # k : batch_size * input_dim * dim_k
    # v : batch_size * input_dim * dim_v
    def __init__(self,input_dim, dim_k, dim_v):
        super(Self_Attention,self).__init__()
        self.q = nn.Linear(input_dim,dim_k)
        self.k = nn.Linear(input_dim,dim_k)
        self.v = nn.Linear(input_dim,dim_v)
        self._norm_fact = 1 / sqrt(dim_k)
        
    def forward(self,x):
        Q = self.q(x) # Q: batch_size * seq_len * dim_k
        K = self.k(x) # K: batch_size * seq_len * dim_k
        V = self.v(x) # V: batch_size * seq_len * dim_v
         
        atten = nn.Softmax(dim=-1)(torch.bmm(Q,K.permute(0,2,1))) * self._norm_fact # Q * K.T() # batch_size * seq_len * seq_len
        
        output = torch.bmm(atten,V) # Q * K.T() * V # batch_size * seq_len * dim_v
        
        return output


if __name__ == '__main__':

    X = torch.randn(4,3,2)
    print(X.size(), X)
    sa = Self_Attention(2,4,5)
    res = sa(X)
    print(res)

另一种写法

import torch.nn as nn

class SelfAttention(nn.Module):

    def __init__(self, d_in, d_out_kq, d_out_v):
        super().__init__()
        self.d_out_kq = d_out_kq
        self.W_query = nn.Parameter(torch.rand(d_in, d_out_kq))
        self.W_key   = nn.Parameter(torch.rand(d_in, d_out_kq))
        self.W_value = nn.Parameter(torch.rand(d_in, d_out_v))

    def forward(self, x):
        keys = x @ self.W_key
        queries = x @ self.W_query
        values = x @ self.W_value
        
        attn_scores = queries @ keys.T  # unnormalized attention weights    
        attn_weights = torch.softmax(
            attn_scores / self.d_out_kq**0.5, dim=-1
        )
        
        context_vec = attn_weights @ values
        return context_vec

多头注意力实现

# Muti-head Attention 机制的实现
from math import sqrt
import torch
import torch.nn as nn

class Self_Attention_Muti_Head(nn.Module):
    # input : batch_size * seq_len * input_dim
    # q : batch_size * input_dim * dim_k
    # k : batch_size * input_dim * dim_k
    # v : batch_size * input_dim * dim_v
    def __init__(self,input_dim,dim_k,dim_v,nums_head):
        super(Self_Attention_Muti_Head,self).__init__()
        assert dim_k % nums_head == 0
        assert dim_v % nums_head == 0
        self.q = nn.Linear(input_dim,dim_k)
        self.k = nn.Linear(input_dim,dim_k)
        self.v = nn.Linear(input_dim,dim_v)
        
        self.nums_head = nums_head
        self.dim_k = dim_k
        self.dim_v = dim_v
        self._norm_fact = 1 / sqrt(dim_k)
    
    def forward(self,x):
        Q = self.q(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.nums_head) 
        K = self.k(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.nums_head) 
        V = self.v(x).reshape(-1,x.shape[0],x.shape[1],self.dim_v // self.nums_head)
        print(x.shape)
        print(Q.size())
        atten = nn.Softmax(dim=-1)(torch.matmul(Q,K.permute(0,1,3,2))) # Q * K.T() # batch_size * seq_len * seq_len
        output = torch.matmul(atten,V).reshape(x.shape[0],x.shape[1],-1) # Q * K.T() * V # batch_size * seq_len * dim_v
        return output

【2023-5-10】点的self、cross注意力机制实现

def attention(query, key, value):
    dim = query.shape[1]
    scores = torch.einsum('bdhn,bdhm->bhnm', query, key) / dim**.5
    prob = torch.nn.functional.softmax(scores, dim=-1)
    return torch.einsum('bhnm,bdhm->bdhn', prob, value), prob

class MultiHeadedAttention(nn.Module):
    """ 
      Multi-head attention to increase model expressivitiy 
    """
    def __init__(self, num_heads: int, d_model: int):
        super().__init__()
        assert d_model % num_heads == 0
        self.dim = d_model // num_heads
        self.num_heads = num_heads
        self.merge = nn.Conv1d(d_model, d_model, kernel_size=1)
        self.proj = nn.ModuleList([deepcopy(self.merge) for _ in range(3)])

    def forward(self, query, key, value):
        batch_dim = query.size(0)
        query, key, value = [l(x).view(batch_dim, self.dim, self.num_heads, -1)
                             for l, x in zip(self.proj, (query, key, value))]
        x, prob = attention(query, key, value)
        self.prob.append(prob)
        return self.merge(x.contiguous().view(batch_dim, self.dim*self.num_heads, -1))

需要实现6层 encoder和decoder。

encoder代码实现如下:

import torch
import torch.nn as nn

class EncoderLayer(nn.Module):
	"""Encoder的一层。"""

    def __init__(self, model_dim=512, num_heads=8, ffn_dim=2018, dropout=0.0):
        super(EncoderLayer, self).__init__()

        self.attention = MultiHeadAttention(model_dim, num_heads, dropout)
        self.feed_forward = PositionalWiseFeedForward(model_dim, ffn_dim, dropout)

    def forward(self, inputs, attn_mask=None):
        # self attention
        context, attention = self.attention(inputs, inputs, inputs, padding_mask)
        # feed forward network
        output = self.feed_forward(context)
        return output, attention

class Encoder(nn.Module):
	"""多层EncoderLayer组成Encoder。"""

    def __init__(self,
               vocab_size,
               max_seq_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.0):
        super(Encoder, self).__init__()

        self.encoder_layers = nn.ModuleList(
          [EncoderLayer(model_dim, num_heads, ffn_dim, dropout) for _ in
           range(num_layers)])

        self.seq_embedding = nn.Embedding(vocab_size + 1, model_dim, padding_idx=0)
        self.pos_embedding = PositionalEncoding(model_dim, max_seq_len)

    def forward(self, inputs, inputs_len):
        output = self.seq_embedding(inputs)
        output += self.pos_embedding(inputs_len)

        self_attention_mask = padding_mask(inputs, inputs)

        attentions = []
        for encoder in self.encoder_layers:
            output, attention = encoder(output, self_attention_mask)
            attentions.append(attention)

        return output, attentions

通过文章前面的分析,代码不需要更多解释了。同样的,我们的decoder代码如下:

import torch
import torch.nn as nn


class DecoderLayer(nn.Module):

    def __init__(self, model_dim, num_heads=8, ffn_dim=2048, dropout=0.0):
        super(DecoderLayer, self).__init__()

        self.attention = MultiHeadAttention(model_dim, num_heads, dropout)
        self.feed_forward = PositionalWiseFeedForward(model_dim, ffn_dim, dropout)

    def forward(self,
              dec_inputs,
              enc_outputs,
              self_attn_mask=None,
              context_attn_mask=None):
        # self attention, all inputs are decoder inputs
        dec_output, self_attention = self.attention(
          dec_inputs, dec_inputs, dec_inputs, self_attn_mask)

        # context attention
        # query is decoder's outputs, key and value are encoder's inputs
        dec_output, context_attention = self.attention(
          enc_outputs, enc_outputs, dec_output, context_attn_mask)

        # decoder's output, or context
        dec_output = self.feed_forward(dec_output)

        return dec_output, self_attention, context_attention


class Decoder(nn.Module):

    def __init__(self,
               vocab_size,
               max_seq_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.0):
        super(Decoder, self).__init__()

        self.num_layers = num_layers

        self.decoder_layers = nn.ModuleList(
          [DecoderLayer(model_dim, num_heads, ffn_dim, dropout) for _ in
           range(num_layers)])

        self.seq_embedding = nn.Embedding(vocab_size + 1, model_dim, padding_idx=0)
        self.pos_embedding = PositionalEncoding(model_dim, max_seq_len)

    def forward(self, inputs, inputs_len, enc_output, context_attn_mask=None):
        output = self.seq_embedding(inputs)
        output += self.pos_embedding(inputs_len)

        self_attention_padding_mask = padding_mask(inputs, inputs)
        seq_mask = sequence_mask(inputs)
        self_attn_mask = torch.gt((self_attention_padding_mask + seq_mask), 0)

        self_attentions = []
        context_attentions = []
        for decoder in self.decoder_layers:
            output, self_attn, context_attn = decoder(
            output, enc_output, self_attn_mask, context_attn_mask)
            self_attentions.append(self_attn)
            context_attentions.append(context_attn)

        return output, self_attentions, context_attentions

最后,把encoder和decoder组成Transformer模型!

代码如下:

import torch
import torch.nn as nn


class Transformer(nn.Module):

    def __init__(self,
               src_vocab_size,
               src_max_len,
               tgt_vocab_size,
               tgt_max_len,
               num_layers=6,
               model_dim=512,
               num_heads=8,
               ffn_dim=2048,
               dropout=0.2):
        super(Transformer, self).__init__()
        self.encoder = Encoder(src_vocab_size, src_max_len, num_layers, model_dim,
                               num_heads, ffn_dim, dropout)
        self.decoder = Decoder(tgt_vocab_size, tgt_max_len, num_layers, model_dim,
                               num_heads, ffn_dim, dropout)
        self.linear = nn.Linear(model_dim, tgt_vocab_size, bias=False)
        self.softmax = nn.Softmax(dim=2)

    def forward(self, src_seq, src_len, tgt_seq, tgt_len):
        context_attn_mask = padding_mask(tgt_seq, src_seq)
        output, enc_self_attn = self.encoder(src_seq, src_len)
        output, dec_self_attn, ctx_attn = self.decoder(
          tgt_seq, tgt_len, output, context_attn_mask)
        output = self.linear(output)
        output = self.softmax(output)
        return output, enc_self_attn, dec_self_attn, ctx_attn

至此,Transformer模型已经实现了!

pytorch代码

【2021-11-1】

完整代码

# @Author:Yifx
# @Contact: Xxuyifan1999@163.com
# @Time:2021/9/16 20:02
# @Software: PyCharm

"""
文件说明:
"""

import torch
import torch.nn as nn
import numpy as np
import math

class Config(object):
    # 模型超参类
    def __init__(self):
        self.vocab_size = 6

        self.d_model = 20
        self.n_heads = 2

        assert self.d_model % self.n_heads == 0
        dim_k  = self.d_model // self.n_heads
        dim_v = self.d_model // self.n_heads

        self.padding_size = 30
        self.UNK = 5
        self.PAD = 4

        self.N = 6
        self.p = 0.1

config = Config()

class Embedding(nn.Module):
    def __init__(self,vocab_size):
        super(Embedding, self).__init__()
        # 一个普通的 embedding层,我们可以通过设置padding_idx=config.PAD 来实现论文中的 padding_mask
        self.embedding = nn.Embedding(vocab_size,config.d_model,padding_idx=config.PAD)


    def forward(self,x):
        # 根据每个句子的长度,进行padding,短补长截
        for i in range(len(x)):
            if len(x[i]) < config.padding_size:
                x[i].extend([config.UNK] * (config.padding_size - len(x[i]))) # 注意 UNK是你词表中用来表示oov的token索引,这里进行了简化,直接假设为6
            else:
                x[i] = x[i][:config.padding_size]
        x = self.embedding(torch.tensor(x)) # batch_size * seq_len * d_model
        return x

class Positional_Encoding(nn.Module):

    def __init__(self,d_model):
        super(Positional_Encoding,self).__init__()
        self.d_model = d_model

    def forward(self,seq_len,embedding_dim):
        positional_encoding = np.zeros((seq_len,embedding_dim))
        for pos in range(positional_encoding.shape[0]):
            for i in range(positional_encoding.shape[1]):
                positional_encoding[pos][i] = math.sin(pos/(10000**(2*i/self.d_model))) if i % 2 == 0 else math.cos(pos/(10000**(2*i/self.d_model)))
        return torch.from_numpy(positional_encoding)

class Mutihead_Attention(nn.Module):
    def __init__(self,d_model,dim_k,dim_v,n_heads):
        super(Mutihead_Attention, self).__init__()
        self.dim_v = dim_v
        self.dim_k = dim_k
        self.n_heads = n_heads

        self.q = nn.Linear(d_model,dim_k)
        self.k = nn.Linear(d_model,dim_k)
        self.v = nn.Linear(d_model,dim_v)

        self.o = nn.Linear(dim_v,d_model)
        self.norm_fact = 1 / math.sqrt(d_model)

    def generate_mask(self,dim):
        # 此处是 sequence mask ,防止 decoder窥视后面时间步的信息。
        # padding mask 在数据输入模型之前完成。
        matirx = np.ones((dim,dim))
        mask = torch.Tensor(np.tril(matirx))

        return mask==1

    def forward(self,x,y,requires_mask=False):
        assert self.dim_k % self.n_heads == 0 and self.dim_v % self.n_heads == 0
        # size of x : [batch_size * seq_len * batch_size]
        # 对 x 进行自注意力
        Q = self.q(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        K = self.k(x).reshape(-1,x.shape[0],x.shape[1],self.dim_k // self.n_heads) # n_heads * batch_size * seq_len * dim_k
        V = self.v(y).reshape(-1,y.shape[0],y.shape[1],self.dim_v // self.n_heads) # n_heads * batch_size * seq_len * dim_v
        # print("Attention V shape : {}".format(V.shape))
        attention_score = torch.matmul(Q,K.permute(0,1,3,2)) * self.norm_fact
        if requires_mask:
            mask = self.generate_mask(x.shape[1])
            # masked_fill 函数中,对Mask位置为True的部分进行Mask
            attention_score.masked_fill(mask,value=float("-inf")) # 注意这里的小Trick,不需要将Q,K,V 分别MASK,只MASKSoftmax之前的结果就好了
        output = torch.matmul(attention_score,V).reshape(y.shape[0],y.shape[1],-1)
        # print("Attention output shape : {}".format(output.shape))

        output = self.o(output)
        return output

class Feed_Forward(nn.Module):
    def __init__(self,input_dim,hidden_dim=2048):
        super(Feed_Forward, self).__init__()
        self.L1 = nn.Linear(input_dim,hidden_dim)
        self.L2 = nn.Linear(hidden_dim,input_dim)

    def forward(self,x):
        output = nn.ReLU()(self.L1(x))
        output = self.L2(output)
        return output

class Add_Norm(nn.Module):
    def __init__(self):
        self.dropout = nn.Dropout(config.p)
        super(Add_Norm, self).__init__()

    def forward(self,x,sub_layer,**kwargs):
        sub_output = sub_layer(x,**kwargs)
        # print("{} output : {}".format(sub_layer,sub_output.size()))
        x = self.dropout(x + sub_output)

        layer_norm = nn.LayerNorm(x.size()[1:])
        out = layer_norm(x)
        return out


class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)

        self.add_norm = Add_Norm()


    def forward(self,x): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list

        x += self.positional_encoding(x.shape[1],config.d_model)
        # print("After positional_encoding: {}".format(x.size()))
        output = self.add_norm(x,self.muti_atten,y=x)
        output = self.add_norm(output,self.feed_forward)

        return output

# 在 Decoder 中,Encoder的输出作为Query和KEy输出的那个东西。即 Decoder的Input作为V。此时是可行的
# 因为在输入过程中,我们有一个padding操作,将Inputs和Outputs的seq_len这个维度都拉成一样的了
# 我们知道,QK那个过程得到的结果是 batch_size * seq_len * seq_len .既然 seq_len 一样,那么我们可以这样操作
# 这样操作的意义是,Outputs 中的 token 分别对于 Inputs 中的每个token作注意力

class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.positional_encoding = Positional_Encoding(config.d_model)
        self.muti_atten = Mutihead_Attention(config.d_model,config.dim_k,config.dim_v,config.n_heads)
        self.feed_forward = Feed_Forward(config.d_model)
        self.add_norm = Add_Norm()

    def forward(self,x,encoder_output): # batch_size * seq_len 并且 x 的类型不是tensor,是普通list
        # print(x.size())
        x += self.positional_encoding(x.shape[1],config.d_model)
        # print(x.size())
        # 第一个 sub_layer
        output = self.add_norm(x,self.muti_atten,y=x,requires_mask=True)
        # 第二个 sub_layer
        output = self.add_norm(x,self.muti_atten,y=encoder_output,requires_mask=True)
        # 第三个 sub_layer
        output = self.add_norm(output,self.feed_forward)
        return output

class Transformer_layer(nn.Module):
    def __init__(self):
        super(Transformer_layer, self).__init__()
        self.encoder = Encoder()
        self.decoder = Decoder()

    def forward(self,x):
        x_input,x_output = x
        encoder_output = self.encoder(x_input)
        decoder_output = self.decoder(x_output,encoder_output)
        return (encoder_output,decoder_output)

class Transformer(nn.Module):
    def __init__(self,N,vocab_size,output_dim):
        super(Transformer, self).__init__()
        self.embedding_input = Embedding(vocab_size=vocab_size)
        self.embedding_output = Embedding(vocab_size=vocab_size)

        self.output_dim = output_dim
        self.linear = nn.Linear(config.d_model,output_dim)
        self.softmax = nn.Softmax(dim=-1)
        self.model = nn.Sequential(*[Transformer_layer() for _ in range(N)])


    def forward(self,x):
        x_input , x_output = x
        x_input = self.embedding_input(x_input)
        x_output = self.embedding_output(x_output)

        _ , output = self.model((x_input,x_output))

        output = self.linear(output)
        output = self.softmax(output)

        return output

Transformer 改进

Transformer 问题

【2023-9-18】RetNet:万众期待的 Transformers 杀手, 头条

Transformer 已成为大语言模型上的架构,因为它有效地克服了循环神经网络 (RNN) 的顺序训练问题。

然而,Transformer 并不完美,因为仅解决了所谓“impossible triangle”的条臂。

“不可能三角”代表当前序列模型无法同时实现训练并行性低成本推理以及强大性能的所有3个期望维度。

三角上的方法表示实现的两个维度,但缺少第三个顶点的所需属性。

可解释性

白盒 transformer – CRATE

【2023-11-30】「GPT-4只是在压缩数据」,马毅团队造出白盒Transformer,可解释的大模型要来了吗?

伯克利和香港大学的马毅教授领导的一个研究团队给出了自己的最新研究结果:

包括 GPT-4 在内的当前 AI 系统所做的正是压缩。

提出的新深度网络架构 CRATE,通过数学方式验证了这一点。

  • CRATE 是一种白盒 Transformer,其不仅能在几乎所有任务上与黑盒 Transformer 相媲美,而且还具备非常出色的可解释性

基于此,马毅教授还在 Twitter 上分享了一个有趣的见解:

  • 既然当前的 AI 只是在压缩数据,那么就只能学习到数据中的相关性 / 分布,所以就并不真正具备因果或逻辑推理或抽象思考能力。

因此,当今的 AI 还算不是 AGI,即便近年来在处理和建模大量高维和多模态数据方面,深度学习在实验中取得了巨大的成功。

这种成功归功于深度网络能有效学习数据分布中可压缩的低维结构,并将该分布转换为简约(即紧凑且结构化的)表征。这样的表征可用于帮助许多下游任务,比如视觉、分类、识别和分割、生成。

表征学习是通过压缩式编码和解码实现的

白盒深度网络理论。为学习紧凑和结构化的表征提出了一个统一目标,有原理保证的优良度度量。对于学习到的表征,该目标旨在既优化其在编码率下降方面的内在复杂性,也优化其在稀疏性方面的外在复杂性。该目标称为 稀疏率下降(sparse rate reduction)。

为了优化这个目标,提出学习一个增量映射序列,模拟展开目标函数的某些类似梯度下降的迭代优化方案。这得到一个类似 Transformer 的深度网络架构,并且它完全是一个「白盒」—— 其优化目标、网络算子和学习到的表征在数学上是完全可解释的。

这个白盒深度架构命名为 CRATECRATE-Transformer,这是 Coding-RATE transformer 的缩写。还通过数学方式证明这些增量映射在分布的意义上是可逆的,并且它们的逆映射本质上由同一类数学算子构成。

因此,可以将几乎完全一样的 CRATE 架构用于编码器、解码器或自动编码器。

模型结构

如果说 RetNet 是从平行推理效能的角度革新了网络架构,那么 BitNet 则从正交角度提升了推理效率。

这两者的结合,以及融合其他提升模型效率的技术比如混合专家模型(MoE)和稀疏注意力机制(Sparse Attention),将成为未来基础模型网络架构的基础。

RetNet

【2023-9-18】RetNet:万众期待的 Transformers 杀手, 头条

微软的 RetNet 位于这个“impossible triangle”的正中心,胜过了所有尝试过但未能实现这一壮举的方法。RetNet 设法在单个框架下实现所有属性。

突破:

  • RetNet 具有更好的语言建模性能
  • RetNet 内存消耗降低了 3.4 倍
  • ….8.4 倍更高的吞吐量
  • …延迟降低 15.6 倍

这速度比当前的 SOTA 快几个数量级,同时还提供更好的性能!如果其他团队能够复制这一点并且进入开源领域,这将是巨大的进步,但目前微软绝对是「遥遥领先」

RetNet的主要贡献可以概括为两大点

  • RetNet引入多尺度保留机制来替代多头注意力。这是消除自注意力机制中的魔鬼这一组成部分的关键。尽管如此,这种保留机制有一个小小的理论上的缺点。
  • RetNet 适用于三种计算范式,而只有一种 Transformer 在训练和推理过程中使用相同的序列处理范式。
    • A. 并行表示使训练并行性能够充分利用 GPU 设备。
    • B. 循环表示在内存和计算方面可实现高效的 O(1) 推理。可以显着降低部署成本和延迟。此外,在没有键值缓存技巧的情况下,实现也得到了极大的简化。
    • C. 分块循环表示可以执行有效的长序列建模。对每个本地块进行并行编码以提高计算速度,同时对全局块进行循环编码以节省 GPU 内存。

新型基础网络架构 Retentive Network(RetNet)成功突破了所谓的“不可能三角”难题,实现了帕累托(Pareto)优化。

  • RetNet 在保持良好的扩展性能和并行训练的同时,实现了低成本部署和高效率推理。

RetNet 推理成本与模型序列长度无关,这表示无论是处理长文本序列,还是长图像序列,亦或是未来更长的音视频序列,RetNet 都可以保持稳定的高效推理。

微软 BitNet

【2024-2-29】BitNet b1.58:开启1-bit大语言模型时代

微软亚洲研究院推出了 1-bit LLM 新变体:BitNet b1.58

该模型每个参数仅使用三值表示,即-1, 0 或 1。因此,在 LLM 的矩阵乘法操作中只需要整数加法,而不需要任何浮点数乘法或加法。在语言模型困惑度和下游任务性能的评估中

  • BitNet b1.58 能够与具有相同参数量和训练数据量的全精度(即FP16或BF16)Transformer LLM 相匹敌。
  • 与此同时,它在速度、内存使用、吞吐量和能耗等方面具有大幅优势。

BitNet b1.58 为训练新一代高性能高效率的 LLMs 确立了新的扩展定律(scaling law)和方法。此外引领了一种全新的计算范式,并为开发专为 1-bit LLMs 优化的硬件设备铺平了道路。

BitNet 是第一个支持训练1比特大语言模型的新型网络结构,具有强大的可扩展性和稳定性,能够显著减少大语言模型的训练和推理成本。

与最先进的8比特量化方法和全精度 Transformer 基线相比,BitNet 在大幅降低内存占用和计算能耗的同时,表现出了极具竞争力的性能。

此外,BitNet 拥有与全精度 Transformer 相似的规模法则(Scaling Law),在保持效率和性能优势的同时,还可以更加高效地将其能力扩展到更大的语言模型上,从而让1比特大语言模型(1-bit LLM)成为可能。

微软 YOCO

【2024-5-13】YOCO:打破传统Decoder-only架构,内存消耗仅为Transformer的六分之一

模型架构还只有三大类:Decoder-Only、Encoder-Only、Encoder-Decoder。

微软亚洲研究院推出了一种创新性的 Decoder-Decoder 架构 YOCO(You Only Cache Once)。通过自解码器交叉解码器的独特架构,YOCO 仅需缓存一次键值对,从而显著降低 GPU 内存的使用。

模型评估中,YOCO 展现出与同规模 Transformer 模型相媲美的性能,并在语言建模评估、模型大小扩展以及长上下文处理方面具有显著优势。特别是在降低 GPU 内存占用和缩短预填充延迟方面,

YOCO 整体架构设计如下,分为自解码器(Self-Decoder)和交叉解码器(Cross-Decoder)两部分。

YOCO 实现了“模型越大,内存越省”,为自然语言处理领域带来了全新的研究和应用范式。

  • YOCO 仅缓存一次键值对,可大幅降低 GPU 内存需求,且保留全局注意力能力。

打破 GPT 系列开创的 Decoder-Only 架构——提出 Decoder-Decoder 新型架构,名为 YOCO (You Only Cache Once)。

  • 在处理 512K 上下文长度时,标准 Transformer 内存使用是 YOCO 的6.4倍,预填充延迟是 YOCO 的30.3倍,而 YOCO 的吞吐量提升到标准 Transformer 的9.6倍。

位置编码方式

2021.3.23 Roformer

【2021-3-23】Rotary Transformer,简称 RoFormer,是追一科技苏剑林自研的语言模型之一,主要是为Transformer结构设计了新的旋转式位置编码(Rotary Position Embedding,RoPE)。

  • RoPE具有良好的理论性质,且是目前唯一一种用到线性Attention的绝对位置编码,目前来看实验结果也颇为不错。
  • 参考配置:在24G显存的3090上,跑maxlen=1024,batch_size能跑到8以上。

详细介绍:

使用

from transformers import RoFormerTokenizerFast

tokenizer = RoFormerTokenizerFast.from_pretrained("junnyu/roformer_chinese_base")
tokenizer.tokenize("今天天气非常好。")

检索增强

增大模型并不是提升性能的唯一路径,用一种搜索/查询信息的方式来增强模型,小的生成语言模型也能达到之前大模型才能达到的性能。

语言模型的任务是做填空题,这对于语言信息有意义,但是对于事实信息和世界知识信息是无效的。

  • 有时需要与事实有关的信息

代表

  • DeepMind 的 RETRO Transformer
    • DeepMind 的 RETRO(Retrieval-Enhanced TRansfOrmer)模型。该模型与 GPT-3 性能相当,但参数量仅为 GPT-3 的 4%。
  • OpenAI 的 WebGPT

2021.12.16 WebGPT

OpenAI 推出 WebGPT, 解决 long-form quesion-answering (LFQA) 的方案, 开放域QA回复更长更可靠。

WebGPT 思路类似 Knowledge-Grounded Conversation,利用搜索引擎做相关文档检索,从而生成更长的答案。主要的两个贡献:

  • 微调的语言模型可以与一个基于文本的Web浏览环境交互,从而可以端到端地使用模仿和强化学习优化检索和聚合效果。
  • 参考Web检索出来的信息生成回复。labeler可以根据检索出来的信息判断factual准确率,降低了独立调研问题正确性的难度。

这个想法并非 WebGPT首次提出

WebGPT 思路更进一步,完全模拟了人使用搜索引擎的方法(有更多action: 搜索、点击、翻页、回退等等),而非仅生成search query并使用其结果。

2022.2.7 RETRO

DeepMind 推出 RETRO, 整合了从数据库中检索到的信息,将其参数从昂贵的事实和世界知识存储中解放出来。

加入检索方法之后,语言模型可以缩小很多。

  • 神经数据库可以帮助模型检索它需要的事实信息。

模型结构

结构

  • RETRO 是 编码器 - 解码器模型,像原始的 Transformer。
  • 然而在检索数据库的帮助下增加了输入序列
  • 该模型在数据库中找到最可能的序列,并添加到输入中。
  • RETRO 利用它的魔力生成输出预测。

RETRO 检索数据库

这里的数据库是一个键值存储(key-value store)数据库。

  • key 是标准的 BERT 句子嵌入,value 是由两部分组成的文本
  • Neighbor,用于计算 key;
  • Completion,原文件中文本的延续。

RETRO 数据库包含基于 MassiveText 数据集的 2 万亿个多语言 token。neighbor chunk 和 completion chunk 的长度最多为 64 个 token。

数据库查找

进入 RETRO 前

  • 输入提示进入 BERT。对输出的上下文向量进行平均以构建句子嵌入向量。
  • 然后,使用该向量查询数据库。近似最近邻搜索。检索两个最近邻
  • 将这些添加到语言模型的输入中
    • 检索出的文本成为 RETRO 输入的一部分,Transformer 和 RETRO 块将信息合并到它们的处理中

高层次的 RETRO 架构

RETRO 架构由一个编码器堆栈和一个解码器堆栈组成。

  • 编码器由标准的 Transformer 编码器块(self-attention + FFNN)组成。Retro 使用由两个 Transformer 编码器块组成的编码器。
    • 编码器堆栈会处理检索到的近邻,生成后续将用于注意力的 KEYS 和 VALUES 矩阵
  • 解码器堆栈包含了两种解码器 block:
    • 标准 Transformer 解码器块(ATTN + FFNN)
    • RETRO 解码器块(ATTN + Chunked cross attention (CCA) + FFNN)
  • 解码器 block 像 GPT 一样处理输入文本。对提示 token 应用自注意力(因此只关注之前的 token),然后通过 FFNN 层。只有到达 RETRO 解码器时,它才开始合并检索到的信息。从 9 开始的每个第三个 block 是一个 RETRO block(允许其输入关注近邻)。所以第 9、12、15…32 层是 RETRO block。

输入输出 改进

输入长度改进

2023.7.8 LongNet

【2023-7-8】1000000000!微软改进Transformer一次能记住这么多token了

  • 最强的GPT-4也才最大支持一次处理32k token,相当于50页文字。
  • 而能够只用1分钟看完一本数万字小说的Claude,其token数也不过“才”100k(10万)。

一次性扩展到10亿,并且这个数字理论上其实还是无限的,这不就意味着:不久的将来,整个语料库甚至互联网都能视为一个序列?

作者提出一个Transformer变体:LongNet,它应用了一种叫做“膨胀注意力(dilated attention)”的机制,可以随着距离的增长,让注意力场(模型感知范围)呈指数级扩展。

具体而言,dilated attention替代了普通Transformer中的注意力机制的,其一般的设计原则是:

让注意力的分配随着token之间距离的增长,呈指数级下降。

dilated attention能够产生线性计算复杂度和token之间的对数依赖性,从而解决了注意力资源有限,但每一个token都可访问的矛盾。

MLP 改进

多层感知器(MLP)被称为全连接前馈神经网络,是当今深度学习模型的基础构建块。

MLP 重要性无论怎样强调都不为过,是机器学习中用于逼近非线性函数的默认方法。

然而,MLP 是否最佳非线性回归器呢?

尽管 MLP 被广泛使用,但存在明显缺陷。

  • 例如,在 Transformer 模型中,MLP 几乎消耗了所有非嵌入式参数,并且通常在没有后处理分析工具的情况下,相对于注意力层来说,它们的可解释性较差。

KAN

【2024-5-3】Transformer要变Kansformer?用了几十年的MLP迎来挑战者KAN

MIT 提出的 KAN 灵感来源于 Kolmogorov-Arnold 表示定理的网络。

KAN 在准确性和可解释性方面表现优于 MLP,而且能以非常少的参数量胜过以更大参数量运行的 MLP。

有研究者将 KAN 创新架构的理念扩展到卷积神经网络,将卷积的经典线性变换更改为每个像素中可学习的非线性激活函数,提出并开源 KAN 卷积(CKAN)

Kolmogorov 1957 年就发现了多层神经网络,比 Rumerhart、Hinton 和 William 的 1986 年论文发表的时间要早得多,但他却被西方忽视了。

一种有前景的多层感知器(MLP)的替代方案,称为 Kolmogorov-Arnold Networks(KAN)。

  • MLP 的设计灵感来源于通用近似定理 (通用逼近定理)
  • 而 KAN 设计灵感则来源于 Kolmogorov-Arnold 表示定理

Kolmogorov-Arnold 表示定理

  • Vladimir Arnold 和 Andrey Kolmogorov 证明了如果 f 是一个在有界域上的多变量连续函数,那么 f 可以写成一个单变量连续函数二元加法运算的有限组合。

与 MLP 类似,KAN 拥有全连接结构。而 MLP 在节点(神经元)上放置固定激活函数,KAN 则在边(权重)上放置可学习的激活函数。

因此,KAN 完全没有线性权重矩阵对比图

  • 每个权重参数都被替换为一个可学习的一维函数,参数化为样条(spline)。
  • KAN 的节点仅对传入信号进行求和,而不应用任何非线性变换。
  • 对比图

尽管 KAN 数学解释能力不错,但实际上只是样条MLP 的组合,利用了二者的优点,避免了缺点的出现。

  • 样条在低维函数上准确度高,易于局部调整,并且能够在不同分辨率之间切换。然而,由于样条无法利用组合结构,因此存在严重 COD 问题。
  • 另一方面,MLP 由于其特征学习能力,较少受到 COD 的影响,但在低维空间中却不如样条准确,因为它们无法优化单变量函数。

KAN 的最大瓶颈: 训练速度慢。

  • 相同数量的参数下,KAN 的训练耗时通常是 MLP 的 10 倍。
  • KAN 训练速度慢更像是一个未来可以改进的工程问题,而不是一个根本性的限制

Attention 改进

QKV

MHA、GQA、MQA、MLA 原理对比

  • 传统 Transformer 采用 MHA,但 KV Cache 在推理过程中可能成为性能瓶颈。
  • MQAGQA 虽然在一定程度上可以减少KV Cache的占用,但效果通常不如 MHA
  • MLA 通过低秩 Key-Value联合压缩技术,不仅实现了比MHA更优的效果,还大幅减少了所需的KV Cache大小。

GQA: Grouped-Query Attention

Grouped-Query Attention :对于更大参数量、更大的 context length、更大的 batchsize 来说,原始的MHA(multi-head attention)的内存占用会更高(因为在计算时要缓存pre token的K、V矩阵)。

  • MQA(multi-query attention)让所有的 head 共享 1 个 KV projection 矩阵;
  • GQA(grouped-query attention )使用 8 个 KV projections(选择8是因为A100 8GPUs) 来减少内存占用。

在 30B 模型上训练 150B tokens,发现 GQA 效果和 MHA 差不多,比 MQA 要好;在 1 个node的 8 个 A100 GPUs 上推理速度 GQA 和 MQA差不多,比 MHA 要好(MQA 在推理的时候,要把 KV projections 复制到8张卡上)。

MQA: Muti Query Attention

MQA 是 2019 年提出的一种新的 Attention 机制,其能够在保证模型效果的同时加快 decoder 生成 token 的速度。

MQA 在 encoder 上的提速没有非常明显,但在 decoder 上的提速是很显著的

Multi Query Attention(MQA) 和 Multi Head Attention(MHA)只差了一个单词,从「Head」变成了「Query」。

MQA 让所有的头之间 共享 同一份 Key 和 Value 矩阵,每个头只单独保留了一份 Query 参数,从而大大减少 Key 和 Value 矩阵的参数量。

  • 「参数共享」并不是新奇思路,Albert 通过使用跨层共享参数(Cross-layer parameter sharing)方式来大大减少 bert 的参数量
  • MQA 实际上是将 head 中的 key 和 value 矩阵抽出来单独存为一份共享参数,而 query 则是依旧保留在原来的 head 中,每个 head 有一份自己独有的 query 参数。

代码见原文

MLA: Multi-head Latent Attention

【2024-9-26】注意力机制的变体之MLA

MLA(Multi-head Latent Attention) 是 杭州深度求索人工智能在DeepSeek V2 提出的一种注意力机制变体

MLA 解决推理过程中, 由于attention机制中KV Cache占用过多内存而导致的性能瓶颈问题。

MLA 引入了低秩KV压缩技术,有效减少了KV Cache 大小,从而缓解了这一问题。

MLA 通过低秩 Key-Value联合压缩技术,不仅实现了比MHA更优的效果,还大幅减少了所需的KV Cache大小。

MLA通过低秩联合压缩key和value来减少kv cache。

从注意力机制的步骤来分析:

  • 通过输入x乘以不同矩阵参数Wq、Wk、Wv, 得到不同的QKV向量
  • 转换到QKV向量时,将x乘以一个低秩矩阵,得到低阶矩阵表示
  • 再通过高阶矩阵来恢复原来的特征空间。由于矩阵是模型的权重参数已经保存,所以只需要保存一个低秩的潜层特征就可以恢复成KV,而不是像之前需要同时缓存KV。

为什么LoRA提出这么久了,直到 MLA 才提出对KV Cache低秩分解的做法?

推理加速

芯片

【2023-12-19】美国芯片初创公司 Etched AI 宣称开创了一项新的技术,将 Transformer 架构直接“烧录”到了芯片中😂,创造出了世界上最强大的专门用于Transformer推理的服务器。可以运行万亿参数的模型!🤔 甩英伟达icon几百条街🤓

将 Transformer架构直接“烧录”到芯片中,这意味着Transformer模型的推理可以在专门的硬件上运行,而不需要依赖传统的CPU或GPU。这将大大提高推理速度,降低功耗,并提高模型的性能。

  • 解码速度远超 A100, H100: NVIDIA A100(1x) < NVIDIA H100(5x) < Etched Sohu(15+x)

功能:

  • 实时语音代理:能够在毫秒内处理成千上万的词。
  • • 更好的编码与树搜索:可以并行比较数百个响应。
  • • 多播推测解码:实时生成新内容。
  • • 运行未来的万亿参数模型:只需一个核心,支持全开源软件栈,可扩展至100T参数模型。
  • • 高级解码技术:包括光束搜索和MCTS解码。
  • • 每个芯片144 GB HBM3E:支持MoE和转换器变体。

这对于英伟达来说是巨大的挑战。英伟达一直是人工智能领域的领导者之一,其GPU被广泛应用于深度学习模型的训练和推理。然而,Etched AI的技术可能改变这一格局。

详细:iconetched.ai

TransNAR

拯救Transformer推理能力DeepMind新研究,TransNAR:给模型嵌入算法推理大脑

【2024-6-19】DeepMind 论文提出用混合架构方法,解决Transformer模型的推理缺陷。

将Transformer的NLU技能与基于GNN的神经算法推理器(NAR)的强大算法推理能力相结合,可以实现更加泛化、稳健、准确的LLM推理。

  • TransNAR:用预训练NAR增强Transformer

神经算法推理(NAR)由作者之一Petar Veleckovic, 2021年与人合著的一篇论文中提出,并被接收为Patterns期刊的opinion paper。

NAR被称为「构建能执行算法的神经网络的艺术」。算法与深度学习的本质不同,但如果神经网络能够更好地模仿算法,它甚至可能具备算法的强泛化性。

NAR 整体想法:

  • 训练一个高维隐空间中的处理器网络P(processor network),旨在不断逼近算法的运行结果A(x)。
  • 但由于算法的输入和输出一般是图、树、矩阵等抽象、结构化的形式,这与深度学习模型高维、嘈杂且多变的输入很不兼容,因此还需要训练编码器f和解码器g,将抽象形式转换为自然形式。

NAR 泛化能力似乎远远优于Transformer架构

详见: 拯救Transformer推理能力!DeepMind新研究TransNAR:给模型嵌入「算法推理大脑」

计算效率

attention 存在 $n^2$ 的计算复杂度,如何实现更长文本的计算?

  • 基于状态迭代: TransformerXL RMT
  • 基于位置编码外推能力: ALiBi xPos Unlimiformer
  • 基于工程优化: FlashAttention
  • 基于高效Attention: Reformer LinFormer Flash
  • 其他; S4, FLASH

2023.6.14 FlashAttention

【2023-6-14】FlashAttention: 更快训练更长上下文的GPT

2023.6.24 PageAttention – 管理qkv缓存

【2023-6-24】UC Berkeley 团队推出一个用于加速LLM推理的开源库vLLM,Vicuna 在线推理服务的幕后英雄。

  • 利用 PagedAttention 技术,有效管理Attention模块中的Key和Value的Cache,重新定义了LLM的推理服务。
  • 无需更改任何模型架构,吞吐量比原生 HF Transformers 高出24倍

KV Cache 核心思想

  • 缓存并重用之前计算过的Key和Value, 避免重复计算。

现有 Cache 仍存在一些问题,

  • Large 大:对于LLaMA-13B中的单个序列,它占用高达1.7GB的内存。
  • Dynamic 动态:大小取决于序列长度,而序列长度具有高度可变和不可预测的特点。

因此,高效地管理 KV Cache 是重大挑战。

  • 现有系统(HuggingFace 默认实现是pytorch的内存分配策略)由于内存碎片化和过度预留而浪费了60%-80%的内存。

为了解决这个问题,引入了 PagedAttention,一种受传统操作系统虚拟内存分页概念启发的注意力算法。

  • 与传统注意力算法不同,PagedAttention 允许将连续的键和值存储在非连续的内存空间中。

PagedAttention 将每个序列的 KV 缓存分成多个块,每个块包含固定数量的标记的键和值。

  • 在注意力计算过程中,PagedAttention Kernel高效地识别和获取这些块,采用并行的方式加速计算。(和ByteTransformer的思想有点像)

vLLM 原理详解

2023.7.4 FasterTransfomer

【2023-7-4】FasterTransfomer 是 NVIDIA 高度优化的 Transformer 模型库,在生成时达到 2.5倍的速度,详见 Inference with FasterTransformer

MHA -> DCMHA

KAN

【2024-5-25】ICML2024高分论文!大模型计算效率暴涨至200%

KAN突然爆火,成为可以替代MLP的一种全新神经网络架构,200个参数顶30万参数;而且,GPT-4o的生成速度也是惊艳了一众大模型爱好者。

大模型的计算效率很重要,提升大模型的tokens生成速度是很关键的一环。

而提升大模型的tokens生成速度,除了花钱升级GPU外,更长效的做法是改善Transformer模型架构的计算效率。

彩云科技 对Transformer计算最耗时的核心组件——多头注意力模块(MHA)下手,将Transformer计算性能提升了有2倍之高。

Github上已开源这项工作的代码、模型和训练数据集。

承载Transformer计算量的核心模块是多头注意力(MHA)模块,位置(position=i)上的每一个注意力头(attention head)会与全部位置上的注意力头计算出一个注意力分布矩阵。

  • 这个过程中,位置 i 上的各个注意力头计算出来的注意力分布矩阵是相互独立的。

这种多头独立计算的机制会带来两大问题:

  • 低秩瓶颈(Low-rank Bottleneck):注意力矩阵的秩较低,模型的表达能力受限
  • 头冗余(Head Redundancy):不同的注意力头可能会学习到相似的模式,导致冗余

因此,彩云科技提出了一种叫动态可组合多头注意力(DCMHA)的机制,DCMHA 通过一个核心的组合函数(Compose function),以输入依赖的方式转换注意力得分和权重矩阵,从而动态地组合注意力头,解决了传统MHA模块中存在的上述低秩瓶颈和头冗余问题。

DCMHA旨在提高模型的表达能力,同时保持参数和计算效率,可以作为任何Transformer架构中MHA模块的即插即用替代品,以获得相应的DCFormer模型。

DCMHA机制的核心是引入的Compose函数。这个Compose函数可以视为一个可学习的参数,它可以动态地组合不同头的QK矩阵和VO矩阵,内部通过一系列变换来分解和重构注意力向量。可以近似理解为:经过组合映射后,H个基础的注意力头可组合成多至H*H个注意力头。

根据输入数据调整头之间的交互方式

  • 一是打破头的独立性
  • 二是可以根据输入数据动态组合

从而可以增强模型的表达能力。

效果

论文通过实验表明, DCFormer 在不同的架构和模型规模下,在语言建模方面显著优于Transformer,与计算量增加1.7倍至2倍的模型性能相匹配。

DCFormer可提高70%~100%的模型计算效率

  • DCFormer 在不同参数规模下(405M到6.9B参数),对 Transformer 和 Transformer++ 模型的性能提升显著
  • DCPythia-6.9B 在预训练困惑度和下游任务评估方面优于开源的Pythia-12B。
  • ImageNet-1K数据集上的实验验证了DCMHA在非语言任务中也是有效性的。

相同的参数量下,使用DCFormer将具备更强的模型表达能力;用更少的参数量,拥有相同的模型表示效果。

DCFormer在不同的架构和模型规模下,在语言建模方面显著优于Transformer,与计算量增加1.7倍至2倍的模型性能相匹配。

长度限制

文本长度一直是 transformer 的硬伤。

  • 不同于 RNN,transformer 在训练时必须卡在一个最大长度上,这将导致训练好的模型无法在一个与训练时的长度相差较远的句子上取得较好的推理结果。

Transformer 中,由于 token 和 token 之间是没有顺序之分的. 因此,通常在输入添加 Position Embedding 来表征每一个 token 在句子中的位置。

Position Embedding 的如何选择实在是一个难题,通常有以下几种:

  • 可学习的参数:这种比较常见,BRET 中就是这么做的,但这种方式弊端很明显,因为位置信息是学习出来的,所以如果训练集里面没有见过覆盖某个长度,推理的效果就无法得到保证。
  • 正弦位置编码:这是早期 transformer 使用的位置编码,论文中有尝试做实验,这种编码会随着训练/预测时的文本长度差异增大,(超过 50 个token 后)性能显著下降。
  • 旋转编码:论文中提到这种方式是比较不错的,只不过因其在每一层都要做一次向量旋转,从而降低训练和推理的速度。

transformer 这类模型的 时间复杂度、内存使用复杂度都是 n^2(n为序列长度)

  • 当序列长度超过 512 时,模型对算力的要求将会大幅提高。

最近一些文章 Longformer, Performer, Reformer, Clustered attention 都试图通过近似全注意力机制改善该问题。

准BERT注意力机制时,问题可能有:

  • 每个词与其他所有词都有关系吗?
  • 为什么每个词的注意力不仅仅集中在最重要的词
  • 如何知道哪些词是重要的
  • 如何有效的让注意力仅考虑个别一些词

【2020-12-2】AllenAI Longformer

【2020-12-2】Allen AI 推出 Longformer

Transformer 计算复杂度随输入序列的增加而呈二次曲线增加, 时间和内存占用非常大

  • 原因:Transformer 主要部分 – 缩放点积自注意力(Scaled Dot-Product Self-Attention)
  • 自注意力的计算复杂度为 O(N^2) ,当包含长句时,内存使用量会随着输入量的增加而呈4倍增长。

Longformer 是基于 Transformer 的可扩展模型,用于处理长文档,可轻松执行各种文档级 NLP 任务,而无需对长输入进行分块或缩短,也无需使用复杂的架构来组合各块信息。

Longformer 结合本地和全局信息,以及三种注意力(滑动窗口注意力、放大滑动窗口注意力和全局注意力)。窗口注意和全局注意)。

效果

  • Longformer 还在 text8 和 enwik8 任务中取得了最佳性能。
  • Longformer 在长文档表现一直优于 RoBERTa,并且在预训练后的 WikiHop 和 TriviaQA 任务中表现最佳。

RoBERTa 只有 512 个位置嵌入,因此需要复制 8 个位置嵌入来容纳 4096 个字。尽管它很简单,但据称却非常有效,这显然是因为复制消除了分区边界。

【2021-1-8】谷歌 BigBird

【2021-1-8】谷歌推出 BigBird, 基于稀疏注意力的Transformer,将基于Transformer的模型(例如 BERT)扩展到更长的序列。

开源中文 bigbird 预训练模型,从tiny至base共5个级别预训练模型。可从huggingface hub直接下载使用

BigBird 模型实现了三种注意力机制:随机注意力窗口注意力全局注意力,这与LongFormer几乎相似

与BERT同等计算力下,可处理序列长度达到4096。

  • 很多长文本序列的任务上达到SOTA效果,例如:长文本摘要、长文本问答。
  • BigBird RoBERTa 模型在Transformers仓库中使用。

BigBird的注意力机制是一个近似BERT的全注意力机制,因此不是比BERT的注意力机制效果更好,而是运行效率更高

  • BERT的注意力机制存储与序列长度是二次方关系,在长文本情况下的存储需求就已经开始令人难以忍受
  • 而 BigBird 的 block sparse attention 就是为了解决这个问题。无限长长度序列上,计算无穷次 次时,把BERT的全注意力机制换成 block sparse attention。

BigBird有两种长程注意力方式,可以让计算变的更有效:

  • 全局词(Global token):有一些词,需要考虑其他所有词,其他所有词也需要考虑它。例如”HuggingFace is building nice libraries for easy NLP“。如果”building“是一个全局词,模型在有的人物中需要知道词”NLP“和词”HuggingFace“的关系(这两个词在最左边和最右边),那么词”building“需要被设置成全局词,从而处理与”NLP“和”HuggingFace“的关系。
  • 随机词(Random tokens):随机选择一些词,把信息传递给其他词,这可以降低词与词之间的信息交互难度。
# 例如第一个词和最后一个词是全局的
global_tokens = ["BigBird", "answering"]
# 将全局词加入至key_tokens集合中
key_tokens.append(global_tokens)
# 现在用词”is“做随机词
random_tokens = ["is"]
key_tokens.append(random_tokens)
key_tokens # {'now', 'is', 'in', 'answering', 'available', 'BigBird'}
# 现在,词”available“可以只与这些词做注意力计算,而不是所有词

参考

2022.. Attention with Linear Bias(ALiBi)

ALiBi 是 2022 年提出的一种方法,解决 transformer 训练和推理时文本长度不一致的难题,

如何实现?

  • ALiBi 实现思路很直觉,模型在接收输入时直接去掉 Position Embedding 向量,而是在 Attention 中计算 query·Key 的值后面加入一个偏置常量(非训练变量),来达到注入位置信息的效果。这个常量是一个 事先计算好 的数值,并且每个头(head)的值都有所不同。
  • 通过「相对位置信息」就能在一定程度上缓解「绝对位置信息」造成的训练和推理过程中长度编码不一致的问题

代码见原文

2024.4.10 Infini-Transformer

【2024-4-11】Google 提出Infini-Transformer架构,可让LLMs处理无限长上下文,内存节约114倍

对于批量大小为 512、上下文长度为 2048 的 500B 模型,注意力键值 (KV) 状态的内存占用为 3TB

面对超长序列,相比注意力机制,内存压缩技术更具扩展性。

  • 内存压缩不使用随输入序列长度而增长的数组,而是在有限的内存资源上,维护固定数量的参数来进行信息的存储和回调。
  • 然而,目前的LLMs尚未有一种有效、实用的内存压缩技术,可以在简单性与质量之间取得平衡。

基于以上背景,作者提出了一种新架构:Infini-Transformer,能够让基于Transformer的大模型在有限内存、计算资源的条件下,处理无限长的上下文输入。

Infini-Transformer 可在有限内存条件下,让基于Transformer的大语言模型(LLMs)高效处理无限长的输入序列。

与Transformer-XL类似,Infini-Transformer处理的是一系列片段。

  • 每个片段内 计算 standard causal 点积attention context(注意力上下文)。因此,点积注意力计算在某种意义上是局部的,覆盖了索引为 S 的当前片段的总共 N 个标记。
  • 然而,局部注意力在处理下一个片段时会丢弃前一个片段的注意力状态。在Infini-Transformer中,并没有忽略旧的键值(KV)注意力状态,而是通过内存压缩技术重新使用它们来保持整个上下文历史。
  • 因此,Infini-Transformer的每个注意力层都具有全局压缩和局部细粒度状态,这就是前面提到的无限注意力(Infini-attention)。

实验结果表明:

  • Infini-Transformer在长上下文语言建模任务上超越了基线模型,内存最高可节约114倍。

TTT

【2024-7-20】彻底改变语言模型:全新架构TTT超越Transformer,ML模型代替RNN隐藏状态

问题

  • 长上下文的挑战是 RNN 层本质上所固有的:与自注意力机制不同,RNN 层必须将上下文压缩为固定大小的隐藏状态,更新规则需要发现数千甚至数百万个 token 之间的底层结构和关系。

斯坦福大学、加州大学伯克利分校、加州大学圣迭戈分校和 Meta 设计了一种新架构 TTT,用机器学习模型取代了 RNN 隐藏状态

  • 该模型通过输入 token 的实际梯度下降来压缩上下文。
  • 测试时训练(Test-Time Training)
  • TTT 层直接取代 Attention,并通过表达性记忆解锁线性复杂性架构,使我们能够在上下文中训练具有数百万(有时是数十亿)个 token 的 LLM。

TTT 层作为一种新的信息压缩和模型记忆机制,可简单地直接替代 Transformer 中的自注意力层。

  • 与 Mamba 相比,TTT-Linear 的困惑度更低,FLOP 更少(左),对长上下文的利用更好(右):

全新的大语言模型(LLM)架构有望代替至今在 AI 领域如日中天的 Transformer,性能也比 Mamba 更好。

稀疏Attention

起因

transformer能捕捉输入序列token之间的关系,即使是长距离。

长序列输入受到注意力计算和内存资源限制,随着序列长度n二次增长。

  • DeepSpeed提供了 稀疏 attention kernel —— 支持长序列模型输入,包括文本输入,图像输入和语音输入。
  • 通过块稀疏计算将注意力的计算和内存需求降低几个数量级。

该方法不仅缓解了注意力计算的内存瓶颈,而且可以有效地执行稀疏计算。

除了提供广泛的稀疏性结构外,还具有处理任何用户定义的块稀疏结构的灵活性。

总结

稀疏Attention

  • Atrous Self Attention 空洞自注意力,只计算第k,2k,3k,4k…元素
  • Local Self Attention
  • Sparse Self Attention: OpenAI在image transformer中引入了Sparse self-attention,把两者结合在一块,既可以学习到局部的特性,又可以学习到远程稀疏的相关性
稀疏Attention 名称 说明  
Atrous Self Attention 空洞自注意力  
Local Self Attention 局部自注意力  
Sparse Self Attention 稀疏自注意力 综合以上优点

【2019-7-27】苏剑林,节约而生:从标准Attention到稀疏Attention 节约时间、显存。

Attention的核心在于Q,K,V 三个向量序列的交互和融合,其中Q,K 的交互给出了两两向量之间的某种相关度(权重),而最后的输出序列则是把V按照权重求和得到的

理论上,Self Attention 计算时间显存占用量都是 𝒪(n^2) 级别的(n是序列长度)

  • 如果序列长度变成原来的2倍,显存占用量就是原来的4倍,计算时间也是原来的4倍
  • 当然,假设并行核心数足够多的情况下,计算时间未必会增加到原来的4倍,但是显存的4倍却是实实在在的,无可避免,这也是微调Bert时OOM的原因。

为什么是 𝒪(n^2)?

  • 要对序列中的任意两个向量都要计算相关度,得到一个$n^2$大小的相关度矩阵
  • 左边显示了注意力矩阵,右变显示了关联性,这表明每个元素都跟序列内所有元素有关联。

所以,节省显存,加快计算速度,一个解法是减少关联性计算

Atrous Self Attention 膨胀注意力

Atrous Self Attention,“膨胀自注意力”、“空洞自注意力”、“带孔自注意力”等。

  • 名称是自定义, 原论文《Generating Long Sequences with Sparse Transformers》没有出现过这两个概念

Atrous Self Attention 启发于“膨胀卷积(Atrous Convolution)”,如下图所示,它对相关性进行了约束,强行要求每个元素只跟它相对距离为k,2k,3k,… 的元素关联,其中k>1是预先设定的超参数。从下左的注意力矩阵看,就是强行要求相对距离不是k 的倍数的注意力为0(白色代表0):

  • Atrous Self Attention的注意力矩阵(左)和关联图示(右)

由于现在计算注意力是“跳着”来了,所以实际上每个元素只跟大约n/k个元素算相关性,这样理想情况下运行效率和显存占用都变成了𝒪(n^2/k),也就是说能直接降低到原来的1/k。

Local Self Attention 局部自注意力

Local Self Attention,中文称“局部自注意力”。

  • 自注意力机制在CV领域统称为“Non Local”
  • 而Local Self Attention则要放弃全局关联,重新引入局部关联。约束每个元素只与前后k个元素以及自身有关联,如下图所示:
  • Local Self Attention的注意力矩阵(左)和关联图示(右)
  • 从注意力矩阵来看,就是相对距离超过k的注意力都直接设为0。

其实 Local Self Attention 跟普通卷积很像了,都是保留了一个 2k+1 大小的窗口,然后在窗口内进行一些运算,不同的是普通卷积是把窗口展平然后接一个全连接层得到输出,而现在是窗口内通过注意力来加权平均得到输出。对于Local Self Attention来说,每个元素只跟 2k+1 个元素算相关性,这样一来理想情况下运行效率和显存占用都变成了 𝒪((2k+1)n)∼𝒪(kn) 了,也就是说随着n 而线性增长,这是一个很理想的性质——当然也直接牺牲了长程关联性。

Sparse Self Attention – OpenAI改进,综合以上两种

现在可以很自然地引入OpenAI的 Sparse Self Attention了。

  • Atrous Self Attention 有一些洞,而 Local Self Attention正好填补了这些洞,所以一个简单方式就是将Local Self Attention和Atrous Self Attention 交替使用,两者累积起来,理论上也可以学习到全局关联性,也省了显存。
  • 思路:第一层用Local Self Attention,输出的每个向量都融合了局部几个输入向量,然后第二层用Atrous Self Attention,虽然跳着来,但是因为第一层的输出融合了局部的输入向量,所以第二层的输出理论上可以跟任意的输入向量相关,也就是说实现了长程关联
  • 但是OpenAI直接将两个Atrous Self Attention和Local Self Attention合并为一个,如下图:
  • Sparse Self Attention的注意力矩阵(左)和关联图示(右)

从注意力矩阵上看就很容易理解了,就是除了相对距离不超过k的、相对距离为k,2k,3k,… 的注意力都设为0,这样一来Attention就具有“局部紧密相关和远程稀疏相关”的特性,这对很多任务来说可能是一个不错的先验,因为真正需要密集的长程关联的任务事实上是很少的。

OpenAI 开源了官方实现 sparse_attention

Transformer-Decoder

【2021-4-19】https://zhuanlan.zhihu.com/p/179959751

Transformer 原始论文发表之后,「Generating Wikipedia by Summarizing Long Sequences」提出用另一种 transformer 模块的排列方式来进行语言建模

  • 直接扔掉了所有的 transformer 编码器模块……「Transformer-Decoder」模型。

早期的基于 transformer 的模型由 6 个 transformer 解码器模块堆叠而成:

解码器模块

  • 和 transformer 原始解码器模块相比,去掉了第二个自注意力层。

一个相似的架构在字符级别的语言建模中也被验证有效,使用更深的自注意力层构建语言模型,一次预测一个字母/字符。

所有解码器模块都一样。使用带掩模的自注意力层。

  • 该模型在某个片段中可以支持最长 4000 个单词的序列,相较于 transformer 原始论文中最长 512 单词的限制有了很大的提升。

GPT-2

OpenAI 的 GPT-2 模型就用了这种只包含编码器(decoder-only)模块

GPT-2 可以处理最长 1024 个单词的序列。每个单词都会和前续路径一起「流过」所有的解码器模块。

训练 GPT-2 模型,最简单的方法

  • 自己随机工作(生成无条件样本)。
  • 给它一点提示,说一些关于特定主题的话(即生成交互式条件样本)。

在随机情况下,只简单地提供一个预先定义好的起始单词,然后自己生成文字。

  • 训练好的模型使用「|endoftext|」作为起始单词,不妨将其称为<s>

  • 模型的输入只有一个单词,所以只有这个单词的路径是活跃的。
  • 单词经过层层处理,最终得到一个向量。向量对于词汇表的每个单词计算一个概率
    • 词汇表是模型能「说出」的所有单词,GPT-2 的词汇表中有 50000 个单词
  • 选择概率最高的单词「The」作为下一个单词。
  • 将输出的单词添加在输入序列的尾部构建新的输入序列,让模型进行下一步的预测

问题:重复

  • 陷入推荐同一个词的循环中,除非采用其他单词才能跳出

GPT-2 有个「top-k」的参数

  • 模型会从概率前 k 大的单词中随机抽样选取下一个单词。
  • 之前情况下,top-k = 1

GPT-2 从嵌入(Embedding)矩阵中找单词对应的嵌入向量,该矩阵也是模型训练结果的一部分。

  • 嵌入矩阵的每一行都对应模型词汇表中一个单词的嵌入向量。
  • embedding size
    • small : 768个字符,117m
    • medium : 1024,345m
    • large : 1280,762m
    • extra large : 1600, 1542m

每一行都是一个词嵌入向量:一个能够表征某个单词,并捕获其数字列表。

  • 嵌入向量的长度和 GPT-2 模型的大小有关,最小的模型使用了长为 768 的嵌入向量来表征一个单词。

在嵌入矩阵中查找起始单词<s>对应的嵌入向量。

  • 但在将其输入给模型之前,引入位置编码—— 一些向 transformer 模块指出序列中的单词顺序的信号。
  • 1024 个输入序列位置中的每一个都对应一个位置编码,编码矩阵也是训练模型的一部分。

GPT-2 模型训练后包含两个权值矩阵:嵌入矩阵位置编码矩阵

单词输入第一个 transformer 模块之前, 查到对应的嵌入向量,加上 1号位置对应的位置向量

堆栈之旅: 第一个 transformer 模块处理单词的步骤:

  • 通过自注意力层处理,传给神经网络层。第一个 transformer 模块处理完但此后,会将结果向量被传入堆栈中的下一个 transformer 模块,继续进行计算。每一个 transformer 模块的处理方式都是一样的,但每个模块都会维护自己的自注意力层和神经网络层中的权重。

最上层的 transformer 模块在处理单词「it」的时候会关注「a robot」,所以「a」、「robot」、「it」这三个单词与其得分相乘加权求和后的特征向量会被送入之后的神经网络层。

Lite Transformer (边缘设备)

【2020-6-7】模型压缩95%,MIT韩松等人提出新型Lite Transformer

  • MIT 最近的研究《Lite Transformer with Long-Short Range Attention》中,MIT 与上海交大的研究人员提出了一种高效的移动端 NLP 架构 Lite Transformer,向在边缘设备上部署移动级 NLP 应用迈进了一大步。该论文已被人工智能顶会 ICLR 2020 收录。代码
  • 核心是长短距离注意力(Long-Short Range Attention,LSRA),其中一组注意力头(通过卷积)负责局部上下文建模,而另一组则(依靠注意力)执行长距离关系建模。
  • 对于移动 NLP 设置,Lite Transformer 的 BLEU 值比基于 AutoML 的 Evolved Transformer 高 0.5,而且它不需要使用成本高昂的架构搜索。
  • 从 Lite Transformer 与 Evolved Transformer、原版 transformer 的比较结果中可以看出,Lite Transformer 的性能更佳,搜索成本相比 Evolved Transformer 大大减少

Transformer-XL 和 XLNet

XLNet引入了自回归语言模型以及自编码语言模型

杨植麟介绍

循环智能(Recurrent):用AI重塑沟通

【2022-1-17】杨植麟博士,循环智能(Recurrent AI)联合创始人,清华大学交叉信息院助理教授,智源青年科学家。

2016年5月联合创办的Recurrent AI,核心技术包括自然语言理解、语音识别、语气识别、声纹识别和推荐系统。其中,自然语言理解来自公司的核心原创算法XLNet,这套算法刷新了18项NLP(自然语言处理)任务。如今累计融资4亿元,连续三年营收增长超200%,服务银行保险等行业的头部客户,日均处理对话一亿条、覆盖数百万终端用户。

  • 循环智能创始团队,从左到右:COO揭发、CTO张宇韬、CEO陈麒聪以及AI和产品负责人杨植麟

其研究成果累计Google Scholar引用10,000余次;作为第一作者发表Transformer-XL 和 XLNet ,对NLP领域产生重大影响,分别是ACL 2019和NeurIPS 2019最高引论文之一;主导开发的盘古NLP大模型获2021年世界人工智能大会“卓越人工智能引领者之星奖”。曾入选2021年福布斯亚洲30 under 30;曾效力于Google Brain和Facebook AI。博士毕业于美国卡内基梅隆大学,本科毕业于清华大学

1. 什么是XLNet

XLNet 是一个类似 BERT 的模型,而不是完全不同的模型。总之,XLNet是一种通用的自回归预训练方法。它是CMU和Google Brain团队在2019年6月份发布的模型,最终,XLNet 在 20 个任务上超过了 BERT 的表现,并在 18 个任务上取得了当前最佳效果(state-of-the-art),包括机器问答、自然语言推断、情感分析和文档排序。

BERT 这样基于去噪自编码器的预训练模型可以很好地建模双向语境信息,性能优于基于自回归语言模型的预训练方法。然而,由于需要 mask 一部分输入,BERT 忽略了被 mask 位置之间的依赖关系,因此出现预训练和微调效果的差异(pretrain-finetune discrepancy)。

基于这些优缺点,该研究提出了一种泛化的自回归预训练模型 XLNet。XLNet 可以:

  1. 通过最大化所有可能的因式分解顺序的对数似然,学习双向语境信息;
  2. 用自回归本身的特点克服 BERT 的缺点;
  3. 此外,XLNet 还融合了当前最优自回归模型 Transformer-XL 的思路。

2. 自回归语言模型(Autoregressive LM)

在ELMO/BERT出来之前,大家通常讲的语言模型其实是根据上文内容预测下一个可能跟随的单词,就是常说的自左向右的语言模型任务,或者反过来也行,就是根据下文预测前面的单词,这种类型的LM被称为自回归语言模型。GPT 就是典型的自回归语言模型。ELMO尽管看上去利用了上文,也利用了下文,但是本质上仍然是自回归LM,这个跟模型具体怎么实现有关系。ELMO是做了两个方向(从左到右以及从右到左两个方向的语言模型),但是是分别有两个方向的自回归LM,然后把LSTM的两个方向的隐节点状态拼接到一起,来体现双向语言模型这个事情的。所以其实是两个自回归语言模型的拼接,本质上仍然是自回归语言模型。

自回归语言模型有优点有缺点:

  • 缺点是只能利用上文或者下文的信息,不能同时利用上文和下文的信息,当然,貌似ELMO这种双向都做,然后拼接看上去能够解决这个问题,因为融合模式过于简单,所以效果其实并不是太好。
  • 优点其实跟下游NLP任务有关,比如生成类NLP任务,比如文本摘要,机器翻译等,在实际生成内容的时候,就是从左向右的,自回归语言模型天然匹配这个过程。而Bert这种DAE模式,在生成类NLP任务中,就面临训练过程和应用过程不一致的问题,导致生成类的NLP任务到目前为止都做不太好。

3. 自编码语言模型(Autoencoder LM)

自回归语言模型只能根据上文预测下一个单词,或者反过来,只能根据下文预测前面一个单词。相比而言,Bert通过在输入X中随机Mask掉一部分单词,然后预训练过程的主要任务之一是根据上下文单词来预测这些被Mask掉的单词,如果你对Denoising Autoencoder比较熟悉的话,会看出,这确实是典型的DAE的思路。那些被Mask掉的单词就是在输入侧加入的所谓噪音。类似Bert这种预训练模式,被称为DAE LM。

这种DAE LM的优缺点正好和自回归LM反过来,它能比较自然地融入双向语言模型,同时看到被预测单词的上文和下文,这是好处。缺点是啥呢?主要在输入侧引入[Mask]标记,导致预训练阶段和Fine-tuning阶段不一致的问题,因为Fine-tuning阶段是看不到[Mask]标记的。DAE吗,就要引入噪音,[Mask] 标记就是引入噪音的手段,这个正常。

XLNet的出发点就是:能否融合自回归LM和DAE LM两者的优点。就是说如果站在自回归LM的角度,如何引入和双向语言模型等价的效果;如果站在DAE LM的角度看,它本身是融入双向语言模型的,如何抛掉表面的那个[Mask]标记,让预训练和Fine-tuning保持一致。当然,XLNet还讲到了一个Bert被Mask单词之间相互独立的问题。

4. XLNet模型

4.1 排列语言建模(Permutation Language Modeling)

Bert的自编码语言模型也有对应的缺点,就是XLNet在文中指出的:

  1. 第一个预训练阶段因为采取引入[Mask]标记来Mask掉部分单词的训练模式,而Fine-tuning阶段是看不到这种被强行加入的Mask标记的,所以两个阶段存在使用模式不一致的情形,这可能会带来一定的性能损失;
  2. 另外一个是,Bert在第一个预训练阶段,假设句子中多个单词被Mask掉,这些被Mask掉的单词之间没有任何关系,是条件独立的,而有时候这些单词之间是有关系的。

上面两点是XLNet在第一个预训练阶段,相对Bert来说要解决的两个问题。

其实思路也比较简洁,可以这么思考:XLNet仍然遵循两阶段的过程,第一个阶段是语言模型预训练阶段;第二阶段是任务数据Fine-tuning阶段。它主要希望改动第一个阶段,就是说不像Bert那种带Mask符号的Denoising-autoencoder的模式,而是采用自回归LM的模式。就是说,看上去输入句子X仍然是自左向右的输入,看到Ti单词的上文Context_before,来预测Ti这个单词。但是又希望在Context_before里,不仅仅看到上文单词,也能看到Ti单词后面的下文Context_after里的下文单词,这样的话,Bert里面预训练阶段引入的Mask符号就不需要了,于是在预训练阶段,看上去是个标准的从左向右过程,Fine-tuning当然也是这个过程,于是两个环节就统一起来。当然,这是目标。剩下是怎么做到这一点的问题。

首先,需要强调一点,尽管上面讲的是把句子X的单词排列组合后,再随机抽取例子作为输入,但是,实际上你是不能这么做的,因为Fine-tuning阶段你不可能也去排列组合原始输入。所以,就必须让预训练阶段的输入部分,看上去仍然是x1,x2,x3,x4这个输入顺序,但是可以在Transformer部分做些工作,来达成我们希望的目标。

具体而言,XLNet采取了Attention掩码的机制,你可以理解为,当前的输入句子是X,要预测的单词Ti是第i个单词,前面1到i-1个单词,在输入部分观察,并没发生变化,该是谁还是谁。但是在Transformer内部,通过Attention掩码,从X的输入单词里面,也就是Ti的上文和下文单词中,随机选择i-1个,放到Ti的上文位置中,把其它单词的输入通过Attention掩码隐藏掉,于是就能够达成我们期望的目标(当然这个所谓放到Ti的上文位置,只是一种形象的说法,其实在内部,就是通过Attention Mask,把其它没有被选到的单词Mask掉,不让它们在预测单词Ti的时候发生作用,如此而已。看着就类似于把这些被选中的单词放到了上文Context_before的位置了)。

具体实现的时候,XLNet是用“双流自注意力模型”实现的,细节可以参考论文,但是基本思想就如上所述,双流自注意力机制只是实现这个思想的具体方式,理论上,你可以想出其它具体实现方式来实现这个基本思想,也能达成让Ti看到下文单词的目标。

这里简单说下“双流自注意力机制”,一个是内容流自注意力,其实就是标准的Transformer的计算过程;主要是引入了Query流自注意力,这个是干嘛的呢?其实就是用来代替Bert的那个[Mask]标记的,因为XLNet希望抛掉[Mask]标记符号,但是比如知道上文单词x1,x2,要预测单词x3,此时在x3对应位置的Transformer最高层去预测这个单词,但是输入侧不能看到要预测的单词x3,Bert其实是直接引入[Mask]标记来覆盖掉单词x3的内容的,等于说[Mask]是个通用的占位符号。而XLNet因为要抛掉[Mask]标记,但是又不能看到x3的输入,于是Query流,就直接忽略掉x3输入了,只保留这个位置信息,用参数w来代表位置的embedding编码。其实XLNet只是扔了表面的[Mask]占位符号,内部还是引入Query流来忽略掉被Mask的这个单词。和Bert比,只是实现方式不同而已。

上面讲的Permutation Language Model是XLNet的主要理论创新,所以介绍的比较多,从模型角度讲,这个创新还是挺有意思的,因为它开启了自回归语言模型如何引入下文的一个思路,相信对于后续工作会有启发。当然,XLNet不仅仅做了这些,它还引入了其它的因素,也算是一个当前有效技术的集成体。感觉XLNet就是Bert、GPT 2.0和Transformer XL的综合体变身

  1. 首先,它通过PLM(Permutation Language Model)预训练目标,吸收了Bert的双向语言模型;
  2. 然后,GPT2.0的核心其实是更多更高质量的预训练数据,这个明显也被XLNet吸收进来了;
  3. 再然后,Transformer XL的主要思想也被吸收进来,它的主要目标是解决Transformer对于长文档NLP应用不够友好的问题。

4.2 Transformer XL

目前在NLP领域中,处理语言建模问题有两种最先进的架构:RNN和Transformer。RNN按照序列顺序逐个学习输入的单词或字符之间的关系,而Transformer则接收一整段序列,然后使用self-attention机制来学习它们之间的依赖关系。这两种架构目前来看都取得了令人瞩目的成就,但它们都局限在捕捉长期依赖性上。

为了解决这一问题,CMU联合Google Brain在2019年1月推出的一篇新论文《Transformer-XL:Attentive Language Models beyond a Fixed-Length Context》同时结合了RNN序列建模和Transformer自注意力机制的优点,在输入数据的每个段上使用Transformer的注意力模块,并使用循环机制来学习连续段之间的依赖关系。

4.2.1 vanilla Transformer

为何要提这个模型?因为Transformer-XL是基于这个模型进行的改进。

Al-Rfou等人基于Transformer提出了一种训练语言模型的方法,来根据之前的字符预测片段中的下一个字符。例如,它使用 𝑥1,𝑥2,…,𝑥𝑛−1x1,x2,…,xn−1 预测字符 𝑥𝑛xn,而在 𝑥𝑛xn 之后的序列则被mask掉。论文中使用64层模型,并仅限于处理 512个字符这种相对较短的输入,因此它将输入分成段,并分别从每个段中进行学习,如下图所示。 在测试阶段如需处理较长的输入,该模型会在每一步中将输入向右移动一个字符,以此实现对单个字符的预测。

该模型在常用的数据集如enwik8和text8上的表现比RNN模型要好,但它仍有以下缺点:

  • 上下文长度受限:字符之间的最大依赖距离受输入长度的限制,模型看不到出现在几个句子之前的单词。
  • 上下文碎片:对于长度超过512个字符的文本,都是从头开始单独训练的。段与段之间没有上下文依赖性,会让训练效率低下,也会影响模型的性能。
  • 推理速度慢:在测试阶段,每次预测下一个单词,都需要重新构建一遍上下文,并从头开始计算,这样的计算速度非常慢。

4.2.2 Transformer XL

Transformer-XL架构在vanilla Transformer的基础上引入了两点创新:循环机制(Recurrence Mechanism)和相对位置编码(Relative Positional Encoding),以克服vanilla Transformer的缺点。与vanilla Transformer相比,Transformer-XL的另一个优势是它可以被用于单词级和字符级的语言建模。

  1. 引入循环机制

与vanilla Transformer的基本思路一样,Transformer-XL仍然是使用分段的方式进行建模,但其与vanilla Transformer的本质不同是在于引入了段与段间的循环机制,使得当前段在建模的时候能够利用之前段的信息来实现长期依赖性。如下图所示:

在训练阶段,处理后面的段时,每个隐藏层都会接收两个输入:

  • 这两个输入会被拼接,然后用于计算当前段的Key和Value矩阵。
  • 该方法可以利用前面更多段的信息,测试阶段也可以获得更长的依赖。在测试阶段,与vanilla Transformer相比,其速度也会更快。在vanilla Transformer中,一次只能前进一个step,并且需要重新构建段,并全部从头开始计算;而在Transformer-XL中,每次可以前进一整个段,并利用之前段的数据来预测当前段的输出。
  • 该段的前面隐藏层的输出,与vanilla Transformer相同(上图的灰色线)。
  • 前面段的隐藏层的输出(上图的绿色线),可以使模型创建长期依赖关系。
  1. 相对位置编码

在Transformer中,一个重要的地方在于其考虑了序列的位置信息。在分段的情况下,如果仅仅对于每个段仍直接使用Transformer中的位置编码,即每个不同段在同一个位置上的表示使用相同的位置编码,就会出现问题。比如,第i−2i-2i−2段和第i−1i-1i−1段的第一个位置将具有相同的位置编码,但它们对于第iii段的建模重要性显然并不相同(例如第i−2i-2i−2段中的第一个位置重要性可能要低一些)。因此,需要对这种位置进行区分。

论文对于这个问题,提出了一种新的位置编码的方式,即会根据词之间的相对距离而非像Transformer中的绝对位置进行编码。从另一个角度来解读公式的话,可以将attention的计算分为如下四个部分:

详细公式见:Transformer-XL解读(论文 + PyTorch源码)

  • 基于内容的“寻址”,即没有添加原始位置编码的原始分数。
  • 基于内容的位置偏置,即相对于当前内容的位置偏差。
  • 全局的内容偏置,用于衡量key的重要性。
  • 全局的位置偏置,根据query和key之间的距离调整重要性。

5. XLNet与BERT比较

尽管看上去,XLNet在预训练机制引入的Permutation Language Model这种新的预训练目标,和Bert采用Mask标记这种方式,有很大不同。其实你深入思考一下,会发现,两者本质是类似的。

区别主要在于

  • Bert是直接在输入端显示地通过引入Mask标记,在输入侧隐藏掉一部分单词,让这些单词在预测的时候不发挥作用,要求利用上下文中其它单词去预测某个被Mask掉的单词;
  • 而XLNet则抛弃掉输入侧的Mask标记,通过Attention Mask机制,在Transformer内部随机Mask掉一部分单词(这个被Mask掉的单词比例跟当前单词在句子中的位置有关系,位置越靠前,被Mask掉的比例越高,位置越靠后,被Mask掉的比例越低),让这些被Mask掉的单词在预测某个单词的时候不发生作用。

所以,本质上两者并没什么太大的不同,只是Mask的位置,Bert更表面化一些,XLNet则把这个过程隐藏在了Transformer内部而已。这样,就可以抛掉表面的[Mask]标记,解决它所说的预训练里带有[Mask]标记导致的和Fine-tuning过程不一致的问题。至于说XLNet说的,Bert里面被Mask掉单词的相互独立问题,也就是说,在预测某个被Mask单词的时候,其它被Mask单词不起作用,这个问题,你深入思考一下,其实是不重要的,因为XLNet在内部Attention Mask的时候,也会Mask掉一定比例的上下文单词,只要有一部分被Mask掉的单词,其实就面临这个问题。而如果训练数据足够大,其实不靠当前这个例子,靠其它例子,也能弥补被Mask单词直接的相互关系问题,因为总有其它例子能够学会这些单词的相互依赖关系。

当然,XLNet这种改造,维持了表面看上去的自回归语言模型的从左向右的模式,这个Bert做不到,这个有明显的好处,就是对于生成类的任务,能够在维持表面从左向右的生成过程前提下,模型里隐含了上下文的信息。所以看上去,XLNet貌似应该对于生成类型的NLP任务,会比Bert有明显优势。另外,因为XLNet还引入了Transformer XL的机制,所以对于长文档输入类型的NLP任务,也会比Bert有明显优势。

6. 代码实现

2023.5.24 RWKV

【2023-5-24】RWKV论文燃爆!将RNN崛起进行到底!可扩百亿级参数,与Transformer表现相当

RWKV结合了RNN和Transformer的优势:

  • 一方面,抛弃传统的点积自注意力,使用线性注意力,解决transformer内存和计算复杂度随序列增长呈平方缩放的瓶颈;
  • 另一方面,突破了RNN梯度消失、并行化和可扩展性等限制。

实现 O(Td) 的时间复杂度和 O(d) 的空间复杂度!

问题

  • RNN分解为两个线性块(和)和一个特定于RNN块,但对于先前时间步的数据依赖阻止了RNN的并行化。
  • RWKV与QRNN和RNN(Vanilla、LSTM、GRU等)的架构对比

RWKV 模型架构

  • The Receptance Weighted Key Value (RWKV) 的名字来自于时间混合 (time-mixing) 和通道混合 (channel-mixing) 块中使用的四个主要元素:
  • R (Receptance) :接受过去信息的接受向量;
  • W (Weight):位置权重衰减向量(可训练的模型参数);
  • K (Key) :键是类似于传统注意力中的向量;
  • V (Value):值是类似于传统注意力中的向量。

每个时间步,主要元素之间通过乘法进行交互。

RWKV 架构由一系列堆叠的残差块组成,每个残差块由具有循环结构的时间混合和通道混合子块组成。

效果

  • 与具有相同参数和训练token数量的传统transformer架构(Pythia、OPT、BLOOM、GPT-Neo)相比,RWKV在六个基准测试(Winogrande、PIQA、ARC-C、ARC-E、LAMBADA和SciQ)上均具有竞争力。RWKV甚至在四项任务中超越了Pythia和GPT-Neo.

RWKV-4和ChatGPT / GPT-4的比较研究显示,RWKV-4对提示工程非常敏感。当将指令风格从适合GPT调整为更适合RWKV时,RTE的F1性能甚至从44.2%增加到74.8%。作者猜想是因为RNN不能回溯处理 ( retrospective processing) 来重新调整先前信息的权重。因此为了让性能更好,期望信息应该在问题之后展示。

RWKV与Transformer表现相当,且能在训练时能够并行、在推理时保持恒定的计算和内存复杂度。

但RWKV也存在局限:

  • 比起标准Transformer的平方注意力所维护的完整信息,线性注意力递归架构使信息通过单个向量表示在多个时间步上漏斗式传递,可能限制模型回忆非常长的上下文中细节信息的能力。并且,提示工程变得更加重要。

参考资料

参考文章

  1. 为什么ResNet和DenseNet可以这么深?一文详解残差块为何有助于解决梯度弥散问题
  2. GRADIENTS, BATCH NORMALIZATION AND LAYER NORMALIZATION
  3. The Annotated Transformer
  4. Building the Mighty Transformer for Sequence Tagging in PyTorch : Part I
  5. Building the Mighty Transformer for Sequence Tagging in PyTorch : Part II
  6. Attention?Attention!

参考代码

  1. jadore801120/attention-is-all-you-need-pytorch
  2. JayParks/transformer

卷积

各类卷积讲解:A Comprehensive Introduction to Different Types of Convolutions in Deep Learning

  • 卷积与互相关(信号处理)
  • 深度学习中的卷积(单通道/多通道)
  • 3D卷积1 x 1卷积卷积运算(Convolution Arithmetic)
  • 转置卷积(反卷积,checkerboard artifacts)
  • 扩张卷积(空洞卷积)
  • 可分离卷积(空间可分离卷积,深度卷积)
  • 扁平卷积(Flattened Convolution)
  • 分组卷积(Grouped Convolution)
  • 随机分组卷积(Shuffled Grouped Convolution)
  • 逐点分组卷积(Pointwise Grouped Convolution)

作者:初识CV

空洞卷积 diolation

内部卷积 involution

结束


支付宝打赏 微信打赏

~ 海内存知已,天涯若比邻 ~

Share

Similar Posts

Related Posts

标题:文本生成之序列解码专题 - Decoding Strategy in Text Generation

摘要:文本生成里的序列解码专题笔记

标题:GPT 系列大模型

摘要:大语言模型之GPT,极其一系列模型

站内可视化导航

文章可视化导读:鼠标划过图形块时,如果出现蓝色光环, 点击即可跳转到对应主题

Comments

--disqus--

    My Moment ( 微信公众号 )
    欢迎关注鹤啸九天