动手实现 Transformer,算法面试轻松搞定
*(https://arxiv.org/abs/1901.07291):**跨语言语言模型 (XLM) 探索了构建多语言模型的多个预训练目标,包括来自 GPT 的自回归语言建模和来自 BERT 的 MLM,还将 MLM 拓展到多语言输入,提出了翻译语言建模 (Translation Language Modeling, TLM)。**(https://arxiv.org/abs/2007.1406
最近已有不少大厂停止秋招宣讲,准备计划准备春招吧。
节前,我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。
针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行了深入的讨论。
总结链接如下:
喜欢本文记得收藏、关注、点赞。
算法面试必问模型
Transformer结构
标准的 Transformer 模型主要由两个模块构成:
Encoder(左边):负责理解输入文本,为每个输入构造对应的语义表示(语义特征);
Decoder(右边):负责生成输出,使用 Encoder 输出的语义表示结合其他输入来生成目标序列。
纯 Encoder 模型: 适用于只需要理解输入语义的任务,例如句子分类、命名实体识别;
纯 Decoder 模型: 适用于生成式任务,例如文本生成;
Encoder-Decoder 模型或 Seq2Seq 模型:适用于需要基于输入的生成式任务,例如翻译、摘要。
注意力层
Transformer 模型的标志就是采用了注意力层 (Attention Layers) 的结构,前面也说过,提出 Transformer 结构的论文名字就叫《Attention Is All You Need》(https://arxiv.org/abs/1706.03762)。顾名思义,注意力层的作用就是让模型在处理文本时,将注意力只放在某些词语上。
例如要将英文“You like this course”翻译为法语,由于法语中“like”的变位方式因主语而异,因此需要同时关注相邻的词语“You”。同样地,在翻译“this”时还需要注意“course”,因为“this”的法语翻译会根据相关名词的极性而变化。对于复杂的句子,要正确翻译某个词语,甚至需要关注离这个词很远的词。
同样的概念也适用于其他 NLP 任务:虽然词语本身就有语义,但是其深受上下文的影响,同一个词语出现在不同上下文中可能会有完全不同的语义(例如“我买了一个苹果”和“我买了一个苹果手机”中的“苹果”)。
这也是 Word2Vec 这些静态模型所解决不了的。
原始结构
Transformer 模型本来是为了翻译任务而设计的。在训练过程中,Encoder 接受源语言的句子作为输入,而 Decoder 则接受目标语言的翻译作为输入。在 Encoder 中,由于翻译一个词语需要依赖于上下文,因此注意力层可以访问句子中的所有词语;而 Decoder 是顺序地进行解码,在生成每个词语时,注意力层只能访问前面已经生成的单词。
例如,假设翻译模型当前已经预测出了三个词语,我们会把这三个词语作为输入送入 Decoder,然后 Decoder 结合 Encoder 所有的源语言输入来预测第四个词语。
实际训练中为了加快速度,会将整个目标序列都送入 Decoder,然后在注意力层中通过 Mask 遮盖掉未来的词语来防止信息泄露。例如我们在预测第三个词语时,应该只能访问到已生成的前两个词语,如果 Decoder 能够访问到序列中的第三个(甚至后面的)词语,就相当于作弊了。
原始的 Transformer 模型结构如下图所示,Encoder 在左,Decoder 在右:
其中,Decoder 中的第一个注意力层关注 Decoder 过去所有的输入,而第二个注意力层则是使用 Encoder 的输出,因此 Decoder 可以基于整个输入句子来预测当前词语。这对于翻译任务非常有用,因为同一句话在不同语言下的词语顺序可能并不一致(不能逐词翻译),所以出现在源语言句子后部的词语反而可能对目标语言句子前部词语的预测非常重要。
在 Encoder/Decoder 的注意力层中,我们还会使用 Attention Mask 遮盖掉某些词语来防止模型关注它们,例如为了将数据处理为相同长度而向序列中添加的填充 (padding) 字符。
Transformer 家族
虽然新的 Transformer 模型层出不穷,但是它们依然可以被归纳到以下三种结构中:
Encoder 分支
纯 Encoder 模型只使用 Transformer 模型中的 Encoder 模块,也被称为自编码 (auto-encoding) 模型。在每个阶段,注意力层都可以访问到原始输入句子中的所有词语,即具有“双向 (Bi-directional)”注意力。
纯 Encoder 模型通常通过破坏给定的句子(例如随机遮盖其中的词语),然后让模型进行重构来进行预训练,最适合处理那些需要理解整个句子语义的任务,例如句子分类、命名实体识别(词语分类)、抽取式问答。
BERT 是第一个基于 Transformer 结构的纯 Encoder 模型,它在提出时横扫了整个 NLP 界,在流行的 GLUE 基准上超过了当时所有的最强模型。随后的一系列工作对 BERT 的预训练目标和架构进行调整以进一步提高性能。目前,纯 Encoder 模型依然在 NLP 行业中占据主导地位。
下面简略介绍一下 BERT 模型及它的常见变体:
BERT**(https://arxiv.org/abs/1810.04805):** 通过预测文本中被遮盖的词语和判断一个文本是否跟随另一个来进行预训练,前一个任务被称为遮盖语言建模 (Masked Language Modeling, MLM),后一个任务被称为下句预测 (Next Sentence Prediction, NSP);
DistilBERT**(https://arxiv.org/abs/1910.01108):**尽管 BERT 性能优异,但它的模型大小使其难以部署在低延迟需求的环境中。通过在预训练期间使用知识蒸馏 (knowledge distillation) 技术,DistilBERT 在内存占用减少 40%、计算速度提高 60% 的情况下,依然可以保持 97% 的性能;
RoBERTa**(https://arxiv.org/abs/1907.11692):**BERT 之后的一项研究表明,通过修改预训练方案可以进一步提高性能。RoBERTa 在更多的训练数据上,以更大的批次训练了更长的时间,并且放弃了 NSP 任务。与 BERT 模型相比,这些改变显著地提高了模型的性能;
XLM**(https://arxiv.org/abs/1901.07291):**跨语言语言模型 (XLM) 探索了构建多语言模型的多个预训练目标,包括来自 GPT 的自回归语言建模和来自 BERT 的 MLM,还将 MLM 拓展到多语言输入,提出了翻译语言建模 (Translation Language Modeling, TLM)。XLM 在多个多语言 NLU 基准和翻译任务上都取得了最好的性能;
XLM-RoBERTa**(https://arxiv.org/abs/1911.02116):**跟随 XLM 和 RoBERTa,XLM-RoBERTa (XLM-R) 通过升级训练数据来改进多语言预训练。其基于 Common Crawl 创建了一个 2.5 TB 的语料,然后运用 MLM 训练编码器,由于没有平行对照文本,因此移除了 XLM 的 TLM 目标。最终,该模型大幅超越了 XLM 和多语言 BERT 变体;
ALBERT**(https://arxiv.org/abs/1909.11942):**ALBERT 通过三处变化使得 Encoder 架构更高效:首先将词嵌入维度与隐藏维度解耦以减少模型参数;其次所有模型层共享参数;最后将 NSP 任务替换为句子排序预测(判断句子顺序是否被交换)。这些变化使得可以用更少的参数训练更大的模型,并在 NLU 任务上取得了优异的性能;
ELECTRA**(https://arxiv.org/abs/2003.10555):**MLM 在每个训练步骤中只有被遮盖掉词语的表示会得到更新。ELECTRA 使用了一种双模型方法来解决这个问题:第一个模型继续按标准 MLM 工作;第二个模型(鉴别器)则预测第一个模型的输出中哪些词语是被遮盖的,这使得训练效率提高了 30 倍。下游任务使用时,鉴别器也参与微调;
DeBERTa**(https://arxiv.org/abs/2006.03654):**DeBERTa 模型引入了两处架构变化。首先将词语的内容与相对位置分离,使得自注意力层 (Self-Attention) 层可以更好地建模邻近词语对的依赖关系;此外在解码头的 softmax 层之前添加了绝对位置嵌入。DeBERTa 是第一个在 SuperGLUE(https://arxiv.org/abs/1905.00537) 基准上击败人类的模型。
Decoder 分支
纯 Decoder 模型只使用 Transformer 模型中的 Decoder 模块。在每个阶段,对于给定的词语,注意力层只能访问句子中位于它之前的词语,即只能迭代地基于已经生成的词语来逐个预测后面的词语,因此也被称为自回归 (auto-regressive) 模型。
纯 Decoder 模型的预训练通常围绕着预测句子中下一个单词展开。纯 Decoder 模型适合处理那些只涉及文本生成的任务。
对 Transformer Decoder 模型的探索在在很大程度上是由 OpenAI 带头进行的,通过使用更大的数据集进行预训练,以及将模型的规模扩大,纯 Decoder 模型的性能也在不断提高。
下面就简要介绍一些常见的生成模型:
GPT**(https://openai.com/blog/language-unsupervised):**结合了 Transformer Decoder 架构和迁移学习,通过根据上文预测下一个单词的预训练任务,在 BookCorpus 数据集上进行了预训练。GPT 模型在分类等下游任务上取得了很好的效果;
GPT-2**(https://openai.com/blog/better-language-models/):**受简单且可扩展的预训练方法的启发,OpenAI 通过扩大原始模型和训练集创造了 GPT-2,它能够生成篇幅较长且语义连贯的文本;
CTRL**(https://arxiv.org/abs/1909.05858):**GPT-2 虽然可以根据模板 (prompt) 续写文本,但是几乎无法控制生成序列的风格。条件 Transformer 语言模型 (Conditional Transformer Language, CTRL) 通过在序列开头添加特殊的“控制符”以控制生成文本的风格,这样只需要调整控制符就可以生成多样化的文本;
GPT-3**(https://arxiv.org/abs/2005.14165):**将 GPT-2 进一步放大 100 倍,GPT-3 具有 1750 亿个参数。除了能生成令人印象深刻的真实篇章之外,还展示了小样本学习 (few-shot learning) 的能力。这个模型目前没有开源;
GPT-Neo**(https://zenodo.org/record/5297715)/** GPT-J-6B**(https://github.com/kingoflolz/mesh-transformer-jax):**由于 GPT-3 没有开源,因此一些旨在重新创建和发布 GPT-3 规模模型的研究人员组成了 EleutherAI,训练出了类似 GPT 的 GPT-Neo 和 GPT-J-6B 。当前公布的模型具有 1.3、2.7、60 亿个参数,在性能上可以媲美较小版本的 GPT-3 模型。
Encoder-Decoder 分支
Encoder-Decoder 模型(又称 Seq2Seq 模型)同时使用 Transformer 架构的两个模块。在每个阶段,Encoder 的注意力层都可以访问初始输入句子中的所有单词,而 Decoder 的注意力层则只能访问输入中给定词语之前的词语(即已经解码生成的词语)。
Encoder-Decoder 模型可以使用 Encoder 或 Decoder 模型的目标来完成预训练,但通常会包含一些更复杂的任务。例如,T5 通过随机遮盖掉输入中的文本片段进行预训练,训练目标则是预测出被遮盖掉的文本。Encoder-Decoder 模型适合处理那些需要根据给定输入来生成新文本的任务,例如自动摘要、翻译、生成式问答。
下面简单介绍一些在自然语言理解 (NLU) 和自然语言生成 (NLG) 领域的 Encoder-Decoder 模型:
T5**(https://arxiv.org/abs/1910.10683):**将所有 NLU 和 NLG 任务都转换为 Seq2Seq 形式统一解决(例如,文本分类就是将文本送入 Encoder,然后 Decoder 生成文本形式的标签)。T5 通过 MLM 及将所有 SuperGLUE 任务转换为 Seq2Seq 任务来进行预训练。最终,具有 110 亿参数的大版本 T5 在多个基准上取得了最优性能。
BART**(https://arxiv.org/abs/1910.13461):**同时结合了 BERT 和 GPT 的预训练过程。将输入句子通过遮盖词语、打乱句子顺序、删除词语、文档旋转等方式破坏后传给 Encoder 编码,然后要求 Decoder 能够重构出原始的文本。这使得模型可以灵活地用于 NLU 或 NLG 任务,并且在两者上都实现了最优性能。
M2M-100**(https://arxiv.org/abs/2010.11125):**语言对之间可能存在共享知识可以用来处理小众语言之间的翻译。M2M-100 是第一个可以在 100 种语言之间进行翻译的模型,并且对小众的语言也能生成高质量的翻译。该模型使用特殊的前缀标记来指示源语言和目标语言。
BigBird**(https://arxiv.org/abs/2007.14062):**由于注意力机制 O(n2) 的内存要求,Transformer 模型只能处理一定长度内的文本。BigBird 通过使用线性扩展的稀疏注意力形式,将可处理的文本长度从大多数模型的 512 扩展到 4096,这对于处理文本摘要等需要捕获长距离依赖的任务特别有用。
注意力机制
Attention
在 Transformer 模型提出之前,对 token 序列 XX的常规编码方式是通过循环网络 (RNNs) 和卷积网络 (CNNs)。
RNN 的序列建模方式虽然与人类阅读类似,但是递归的结构导致其无法并行计算,因此速度较慢。而且 RNN 本质是一个马尔科夫决策过程,难以学习到全局的结构信息;
CNN 则通过滑动窗口基于局部上下文来编码文本,例如核尺寸为 3 的卷积操作就是使用每一个词自身以及前一个和后一个词来生成嵌入式表示:
CNN 能够并行地计算,因此速度很快,但是由于是通过窗口来进行编码,所以更侧重于捕获局部信息,难以建模长距离的语义依赖。
Google《Attention is All You Need》提供了第三个方案:直接使用 Attention 机制编码整个文本。相比 RNN 要逐步递归才能获得全局信息(因此一般使用双向 RNN),而 CNN 实际只能获取局部信息,需要通过层叠来增大感受野,Attention 机制一步到位获取了全局信息:
Scaled Dot-product Attention
虽然 Attention 有许多种实现方式,但是最常见的还是 Scaled Dot-product Attention。
Scaled Dot-product Attention 共包含 2 个主要步骤:
**1、计算注意力权重:**使用某种相似度函数度量每一个 query 向量和所有 key 向量之间的关联程度。对于长度为 [ m ] 的 Query 序列和长度为 [ n ] 的 Key 序列,该步骤会生成一个尺寸为 [ m \times nm×n ] 的注意力分数矩阵。
特别地,Scaled Dot-product Attention 使用点积作为相似度函数,这样相似的 queries 和 keys 会具有较大的点积。
下面我们通过 Pytorch 来手工实现 Scaled Dot-product Attention:
首先需要将文本分词为词语 (token) 序列,然后将每一个词语转换为对应的词向量 (embedding)。Pytorch 提供了 torch.nn.Embedding 层来完成该操作,即构建一个从 token ID 到 token embedding 的映射表:
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
from torch import nn
from transformers import AutoConfig
from transformers import AutoTokenizer
model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
text = "time flies like an arrow"
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
print(inputs.input_ids)
config = AutoConfig.from_pretrained(model_ckpt)
token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
print(token_emb)
inputs_embeds = token_emb(inputs.input_ids)
print(inputs_embeds.size())
为了演示方便,这里我们通过设置 add_special_tokens=False 去除了分词结果中的 [CLS] 和 [SEP]。
可以看到,BERT-base-uncased 模型对应的词表大小为 30522,每个词语的词向量维度为 768。Embedding 层把输入的词语序列映射到了尺寸为 [batch_size, seq_len, hidden_dim] 的张量。
接下来就是创建 query、key、value 向量序列 Q,K,V,并且使用点积作为相似度函数来计算注意力分数:
import torch
from math import sqrt
Q = K = V = inputs_embeds
dim_k = K.size(-1)
print("dim_k",dim_k)
print("K.shape",K.shape)
print("K.transpose(1,2)",K.transpose(1,2).shape)
scores = torch.bmm(Q, K.transpose(1,2)) / sqrt(dim_k)
print("scores.size()",scores.size())
这里 Q,K 的序列长度都为 5,因此生成了一个 5×5 的注意力分数矩阵,接下来就是应用 Softmax 标准化注意力权重:
import torch.nn.functional as F
weights = F.softmax(scores, dim=-1)
print("weights",weights)
print("weights.shape",weights.shape)
print("weights.sum(dim=-1).shape",weights.sum(dim=-1).shape)
sum_weights = weights.sum(dim=-1)
# 访问并打印sum_weights张量中的每个元素
for i in range(sum_weights.size(1)):
print(f"Query {i+1} sum of weights: {sum_weights[0, i].item()}")
1、F.softmax(scores, dim=-1):计算scores张量在最后一个维度上的softmax,得到注意力权重weights。
2、weights.sum(dim=-1):计算weights张量在最后一个维度上的和,即对每个query对应的所有key的权重求和。
由于softmax函数的性质,对于每个query,其对应的所有key的权重和应该等于1。因此,weights.sum(dim=-1)的结果应该接近于一个全为1的张量。这里的代码实际上是在验证这一点,确保softmax函数正确地归一化了注意力权重。
最后将注意力权重与 value 序列相乘:
attn_outputs = torch.bmm(weights, V)
print("attn_outputs.shape",attn_outputs.shape)
至此就实现了一个简化版的 Scaled Dot-product Attention。可以将上面这些操作封装为函数以方便后续调用:
import torch
import torch.nn.functional as F
from math import sqrt
def scaled_dot_product_attention(query, key, value, query_mask=None, key_mask=None, mask=None):
dim_k = query.size(-1)
scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
if query_mask is not None and key_mask is not None:
mask = torch.bmm(query_mask.unsqueeze(-1), key_mask.unsqueeze(1))
if mask is not None:
scores = scores.masked_fill(mask == 0, -float("inf"))
weights = F.softmax(scores, dim=-1)
return torch.bmm(weights, value)
上面的代码还考虑了 Q,K,V 序列的 Mask。填充 (padding) 字符不应该参与计算,因此将对应的注意力分数设置为 −∞,这样 softmax 之后其对应的注意力权重就为 0 了(e−∞=0)。
注意!上面的做法会带来一个问题:当 Q 和 K 序列相同时,注意力机制会为上下文中的相同单词分配非常大的分数(点积为 1),而在实践中,相关词往往比相同词更重要。例如对于上面的例子,只有关注“time”和“arrow”才能够确认“flies”的含义。
因此,多头注意力 (Multi-head Attention) 出现了!
Multi-head Attention
Multi-head Attention 首先通过线性映射将 Q, K, V 序列映射到特征空间,每一组线性投影后的向量表示称为一个头 (head),然后在每组映射后的序列上再应用 Scaled Dot-product Attention:
每个注意力头负责关注某一方面的语义相似性,多个头就可以让模型同时关注多个方面。因此与简单的 Scaled Dot-product Attention 相比,Multi-head Attention 可以捕获到更加复杂的特征信息。
形式化表示为:
下面我们首先实现一个注意力头:
from torch import nn
class AttentionHead(nn.Module):
def __init__(self, embed_dim, head_dim):
super().__init__()
self.q = nn.Linear(embed_dim, head_dim)
self.k = nn.Linear(embed_dim, head_dim)
self.v = nn.Linear(embed_dim, head_dim)
def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
attn_outputs = scaled_dot_product_attention(
self.q(query), self.k(key), self.v(value), query_mask, key_mask, mask)
return attn_outputs
每个头都会初始化三个独立的线性层,负责将 Q, K, V 序列映射到尺寸为 [batch_size, seq_len, head_dim] 的张量,其中 head_dim 是映射到的向量维度。
实践中一般将 head_dim 设置为 embed_dim 的因数,这样 token 嵌入式表示的维度就可以保持不变,例如 BERT 有 12 个注意力头,因此每个头的维度被设置为 768/12=64。
最后只需要拼接多个注意力头的输出就可以构建出 Multi-head Attention 层了(这里在拼接后还通过一个线性变换来生成最终的输出张量):
class MultiHeadAttention(nn.Module):
def __init__(self, config):
super().__init__()
embed_dim = config.hidden_size
num_heads = config.num_attention_heads
head_dim = embed_dim // num_heads
self.heads = nn.ModuleList(
[AttentionHead(embed_dim, head_dim) for _ in range(num_heads)]
)
self.output_linear = nn.Linear(embed_dim, embed_dim)
def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
x = torch.cat([
h(query, key, value, query_mask, key_mask, mask) for h in self.heads
], dim=-1)
x = self.output_linear(x)
return x
这里使用 BERT-base-uncased 模型的参数初始化 Multi-head Attention 层,并且将之前构建的输入送入模型以验证是否工作正常:
from transformers import AutoConfig
from transformers import AutoTokenizer
model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
text = "time flies like an arrow"
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
config = AutoConfig.from_pretrained(model_ckpt)
token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
inputs_embeds = token_emb(inputs.input_ids)
print("inputs_embeds.shape",inputs_embeds.shape)
multihead_attn = MultiHeadAttention(config)
query = key = value = inputs_embeds
attn_output = multihead_attn(query, key, value)
print(attn_output.size())
当然,以下是文件中关于Transformer Encoder的所有内容,使用Markdown格式:
Transformer Encoder
回忆一下上一章中介绍过的标准 Transformer 结构,Encoder 负责将输入的词语序列转换为词向量序列,Decoder 则基于 Encoder 的隐状态来迭代地生成词语序列作为输出,每次生成一个词语。
其中,Encoder 和 Decoder 都各自包含有多个 building blocks。下图展示了一个翻译任务的例子:
输入的词语首先被转换为词向量。由于注意力机制无法捕获词语之间的位置关系,因此还通过 positional embeddings 向输入中添加位置信息;
Encoder 由一堆 encoder layers (blocks) 组成,类似于图像领域中的堆叠卷积层。同样地,在 Decoder 中也包含有堆叠的 decoder layers;
Encoder 的输出被送入到 Decoder 层中以预测概率最大的下一个词,然后当前的词语序列又被送回到 Decoder 中以继续生成下一个词,重复直至出现序列结束符 EOS 或者超过最大输出长度。
The Feed-Forward Layer
Transformer Encoder/Decoder 中的前馈子层实际上就是两层全连接神经网络,它单独地处理序列中的每一个词向量,也被称为 position-wise feed-forward layer。常见做法是让第一层的维度是词向量大小的 4 倍,然后以 GELU 作为激活函数。
下面实现一个简单的 Feed-Forward Layer:
class FeedForward(nn.Module):
def __init__(self, config):
super().__init__()
self.linear_1 = nn.Linear(config.hidden_size, config.intermediate_size)
self.linear_2 = nn.Linear(config.intermediate_size, config.hidden_size)
self.gelu = nn.GELU()
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, x):
x = self.linear_1(x)
x = self.gelu(x)
x = self.linear_2(x)
x = self.dropout(x)
return x
将前面注意力层的输出送入到该层中以测试是否符合我们的预期:
feed_forward = FeedForward(config)
ff_outputs = feed_forward(attn_output)
print("ff_outputs.size()",ff_outputs.size())
Layer Normalization
Layer Normalization 负责将一批 (batch) 输入中的每一个都标准化为均值为零且具有单位方差;Skip Connections 则是将张量直接传递给模型的下一层而不进行处理,并将其添加到处理后的张量中。
向 Transformer Encoder/Decoder 中添加 Layer Normalization 目前共有两种做法:
Post layer normalization:Transformer 论文中使用的方式,将 Layer normalization 放在 Skip Connections 之间。但是因为梯度可能会发散,这种做法很难训练,还需要结合学习率预热 (learning rate warm-up) 等技巧;
Pre layer normalization:目前主流的做法,将 Layer Normalization 放置于 Skip Connections 的范围内。这种做法通常训练过程会更加稳定,并且不需要任何学习率预热。
本章采用第二种方式来构建 Transformer Encoder 层:
class TransformerEncoderLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
self.layer_norm_2 = nn.LayerNorm(config.hidden_size)
self.attention = MultiHeadAttention(config)
self.feed_forward = FeedForward(config)
def forward(self, x, mask=None):
# Apply layer normalization and then copy input into query, key, value
hidden_state = self.layer_norm_1(x)
# Apply attention with a skip connection
x = x + self.attention(hidden_state, hidden_state, hidden_state, mask=mask)
# Apply feed-forward layer with a skip connection
x = x + self.feed_forward(self.layer_norm_2(x))
return x
同样地,这里将之前构建的输入送入到该层中进行测试:
encoder_layer = TransformerEncoderLayer(config)
print("inputs_embeds.shape",inputs_embeds.shape)
print("encoder_layer(inputs_embeds).size()",encoder_layer(inputs_embeds).size())
结果符合预期!至此,本章就构建出了一个几乎完整的 Transformer Encoder 层。
Positional Embeddings
前面讲过,由于注意力机制无法捕获词语之间的位置信息,因此 Transformer 模型还使用 Positional Embeddings 添加了词语的位置信息。
Positional Embeddings 基于一个简单但有效的想法:使用与位置相关的值模式来增强词向量。
如果预训练数据集足够大,那么最简单的方法就是让模型自动学习位置嵌入。下面本章就以这种方式创建一个自定义的 Embeddings 模块,它同时将词语和位置映射到嵌入式表示,最终的输出是两个表示之和:
class Embeddings(nn.Module):
def __init__(self, config):
super().__init__()
self.token_embeddings = nn.Embedding(config.vocab_size,
config.hidden_size)
self.position_embeddings = nn.Embedding(config.max_position_embeddings,
config.hidden_size)
self.layer_norm = nn.LayerNorm(config.hidden_size, eps=1e-12)
self.dropout = nn.Dropout()
def forward(self, input_ids):
# Create position IDs for input sequence
seq_length = input_ids.size(1)
position_ids = torch.arange(seq_length, dtype=torch.long).unsqueeze(0)
# Create token and position embeddings
token_embeddings = self.token_embeddings(input_ids)
position_embeddings = self.position_embeddings(position_ids)
# Combine token and position embeddings
embeddings = token_embeddings + position_embeddings
embeddings = self.layer_norm(embeddings)
embeddings = self.dropout(embeddings)
return embeddings
embedding_layer = Embeddings(config)
print("embedding_layer(inputs.input_ids).size()",embedding_layer(inputs.input_ids).size())
除此以外,Positional Embeddings 还有一些替代方案:
绝对位置表示:使用由调制的正弦和余弦信号组成的静态模式来编码位置。当没有大量训练数据可用时,这种方法尤其有效;
相对位置表示:在生成某个词语的词向量时,一般距离它近的词语更为重要,因此也有工作采用相对位置编码。因为每个词语的相对嵌入会根据序列的位置而变化,这需要在模型层面对注意力机制进行修改,而不是通过引入嵌入层来完成,例如 DeBERTa 等模型。
完整的 Transformer Encoder
下面将所有这些层结合起来构建完整的 Transformer Encoder:
class TransformerEncoder(nn.Module):
def __init__(self, config):
super().__init__()
self.embeddings = Embeddings(config)
self.layers = nn.ModuleList([TransformerEncoderLayer(config)
for _ in range(config.num_hidden_layers)])
def forward(self, x, mask=None):
x = self.embeddings(x)
for layer in self.layers:
x = layer(x, mask=mask)
return x
同样地,我们对该层进行简单的测试:
encoder = TransformerEncoder(config)
print(encoder(inputs.input_ids).size())
Transformer Decoder
Transformer Decoder 与 Encoder 最大的不同在于 Decoder 有两个注意力子层,如下图所示:
attention layer:确保在每个时间步生成的词语仅基于过去的输出和当前预测的词,否则 Decoder 相当于作弊了;
Encoder-decoder attention layer:以解码器的中间表示作为 queries,对 encoder stack 的输出 key 和 value 向量执行 Multi-head Attention。通过这种方式,Encoder-Decoder Attention Layer 就可以学习到如何关联来自两个不同序列的词语,例如两种不同的语言。解码器可以访问每个 block 中 Encoder 的 keys 和 values。
与 Encoder 中的 Mask 不同,Decoder 的 Mask 是一个下三角矩阵:
seq_len = inputs.input_ids.size(-1)
mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0)
print(mask[0])
tensor([[1., 0., 0., 0., 0.],
[1., 1., 0., 0., 0.],
[1., 1., 1., 0., 0.],
[1., 1., 1., 1., 0.],
[1., 1., 1., 1., 1.]])
这里使用 PyTorch 自带的 tril() 函数来创建下三角矩阵,然后同样地,通过 Tensor.masked_fill() 将所有零替换为负无穷大来防止注意力头看到未来的词语而造成信息泄露:
scores.masked_fill(mask == 0, -float("inf"))
tensor([[[26.8082, -inf, -inf, -inf, -inf],
[-0.6981, 26.9043, -inf, -inf, -inf],
[-2.3190, 1.2928, 27.8710, -inf, -inf],
[-0.5897, 0.3497, -0.3807, 27.5488, -inf],
[ 0.5275, 2.0493, -0.4869, 1.6100, 29.0893]]],
grad_fn=<MaskedFillBackward0>)
本章对 Decoder 只做简单的介绍,如果你想更深入的了解可以加入我们
更多推荐
所有评论(0)