01
引言
本文是手撕Transformer系列的第二篇。它从头开始介绍位置编码。然后,它解释了 PyTorch 如何实现位置编码。
闲话少说,我们直接开始吧!
02
背景介绍
位置编码(Positional Encoding)多用于为序列中的每个标记Token提供相对位置信息。在阅读句子时,每个单词都依赖于其周围的单词。例如,有些单词在不同的语境中有不同的含义,因此模型应该能够理解这些变化以及每个单词所依赖的语境。比如单词"trunk "就是一个例子,在一种情况下,它可以指大象用鼻子来喝水。在另一种情况下,它可以指树的树干被闪电击中。
由于模型使用长度为 d_model 的嵌入向量来表示每个单词,因此任何位置编码都必须兼容该向量。使用整数似乎很自然,第一个标记Token的位置编码为 0,第二个标记Token的位置编码为 1,以此类推。但是,这个数字很快就会增大,而且不容易被添加到嵌入矩阵中。取而代之的是为每个位置创建一个位置编码向量,这意味着可以创建一个位置编码矩阵来表示一个单词可能出现的所有位置。
为了确保每个位置都有一个唯一的表示,《Attention is all you need》的作者使用正弦和余弦函数为序列中的每个位置生成一个唯一的向量。虽然这看起来很奇怪,但有几个原因可以说明它的用处。首先,正弦和余弦的输出范围为 [-1, 1],这是经过归一化处理后的。它不会像整数那样增长到难以管理的大小。其次,由于每个位置都会生成唯一的表示,因此无需进行额外的训练。
用于生成位置编码向量的方程下图所示看似复杂,但它们只是正弦和余弦函数的变种版。位置嵌入向量可以表示的最大位置数将用 L 表示:
上述公式说明,对于每个位置编码向量,每两个元素中的偶数元素设置为 PE(k,2i),奇数元素设置为 PE(k,2i+1)。然后,重复上述步骤,直到该向量中有 d_model 个元素为止。
每个位置编码向量的维数 d_model 与嵌入向量的维数相同。k 代表位置,从 0 到 L-1。i 可以设置的最高值是 d_model 除以 2,因为位置嵌入公式中的每个元素的计算公式都是交替的。在下图中,以下参数用于计算 6 个标记序列的位置编码向量:
n = 10,000
L = 6
d_model = 4
k随嵌入矩阵中的每一行而变化,从0到5,最大长度为6。每个向量有d_model=4个元素。
03
简单实现
要了解这些独特的位置编码向量如何与单词嵌入向量配合使用,最好参照本系列前一篇文中单词嵌入向量的示例。
本实现将直接基于上一篇文章中的实现。下面代码的输出表示句子个数为bs=3个,每个句子的token数目为seq_len=6个,每个token的嵌入向量的维度d_model=4。
# tokenize the sequences
tokenized_sequences = [tokenize(seq) for seq in sequences]
# index the sequences
indexed_sequences = [[stoi[word] for word in seq] for seq in tokenized_sequences]
# convert the sequences to a tensor
tensor_sequences = torch.tensor(indexed_sequences).long()
# vocab size
vocab_size = len(stoi)
# embedding dimensions
d_model = 4
# create the embeddings
lut = nn.Embedding(vocab_size, d_model) # look-up table (lut)
# embed the sequence
word_embeddings = lut(tensor_sequences)
print(word_embeddings)
输出如下:
下一步是通过位置编码对每个token在每个句子序列中的位置进行编码。下面的函数沿用了上面变量的定义。唯一值得一提的是,L 被记为 max_length。它通常被设置为一个以千/万为单位的整数值,以确保几乎每个序列都能被适当编码。这确保了相同的位置编码矩阵可以适用于不同长度的输入序列。
def gen_pe(max_length, d_model, n):
# generate an empty matrix for the positional encodings (pe)
pe = np.zeros(max_length*d_model).reshape(max_length, d_model)
# for each position
for k in np.arange(max_length):
# for each dimension
for i in np.arange(d_model//2):
# calculate the internal value for sin and cos
theta = k / (n ** ((2*i)/d_model))
# even dims: sin
pe[k, 2*i] = math.sin(theta)
# odd dims: cos
pe[k, 2*i+1] = math.cos(theta)
return pe
# maximum sequence length
max_length = 10
n = 100
encodings = gen_pe(max_length, d_model, n)
print(encodings)
上述代码运行后,输出为包含 10 个位置的位置编码向量。每个编码向量维度为d_model,结果如下:
如前文所述,上述例子中我们将max_length 设置为 10。它确保了如果另一个应用场景下的输入序列的长度为 7、8、9 或 10时,我们仍然可以使用相同的位置编码矩阵。只需将其切成适当的长度即可。下面展示了我们例子中的输入序列 seq_length=6时,我们切分后编码矩阵的代码如下:
# select the first six tokens
seq_length = word_embeddings.shape[1]
encodings[:seq_length]
得到切分后的编码矩阵为:
本例中的单词嵌入向量的维度为 (3,6,4),位置编码被切成(6,4),然后广播该矩阵,将二者相加,我们就可以创建下图中的 (3, 6, 4) 最终编码矩阵。
代码如下:
embeddings = word_embeddings + encodings[:seq_length]
print(embeddings)
单词编码向量和位置编码向量求和后的最终嵌入向量为:
该输出将被传递到模型的下一层,也就是下一篇文章将介绍的 “多头注意力”。然而,由于本文实现位置编码的过程使用了嵌套循环,这种实际应用时并不高效,尤其是在使用较大的 d_model 和 max_length 值时。取而代之的是Pytorch中更加高效的实现。
针对所有自学遇到困难的同学们,我帮大家系统梳理大模型学习脉络,将这份 LLM大模型资料
分享出来:包括LLM大模型书籍、640套大模型行业报告、LLM大模型学习视频、LLM大模型学习路线、开源大模型学习教程
等, 😝有需要的小伙伴,可以 扫描下方二维码领取🆓↓↓↓
👉[CSDN大礼包🎁:全网最全《LLM大模型入门+进阶学习资源包》免费分享(安全链接,放心点击)]()👈
04
Pytorch高效实现
我们首先来回顾下对数的一些基本计算规则,如下图所示:
为了利用 PyTorch 的功能,需要利用对数规则修改原始方程,特别是除数操作。
要修改除数,首先利用指数性质,把 n 移入分子。然后,使用规则 7 将整个等式提升为 e 的指数。然后进行简化,得出结果
这一点非常重要,因为它可以用来一次性生成位置编码的所有除数。从下面可以看出,4 维嵌入只需要两个除数,因为除数每 2i 才会改变一次,其中 i 是维数。这种情况在每个位置上都重复出现:
由于 i 的最大取值只能是 d_model 除以 2,因此只需计算一次:
d_model = 4
n = 100
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(n) / d_model))
这段简短的代码可以用来生成所有需要的除数。在本例中,d_model 设置为 4,n 设置为 100。输出结果是两个除数:
tensor([1.0000, 0.1000])
接着大家就可以通过利用 PyTorch 的索引功能,用几行代码来创建整个位置编码矩阵。我们首先利用以下代码来生成从 k 到 L-1 的每个位置表示。
max_length = 10
# generate the positions into a column matrix
k = torch.arange(0, max_length).unsqueeze(1)
print(k)
输出如下:
tensor([[0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9]]
有了位置表示和相应的除数,就可以轻松计算正弦和余弦函数的输入了,如下:
此时,只需要将 k 和 div_term 相乘,可以计算出每个位置的输入。PyTorch 会自动广播矩阵,以便进行乘法运算。如下图所示:
k*div_term
计算的输出结果如上图所示。剩下要做的就是将上述结果输入到 cos 和 sin 函数中,并将其保存到矩阵中。
我们可以先创建一个适当大小的空矩阵:
# generate an empty tensor
pe = torch.zeros(max_length, d_model)
print(pe)
输出如下:
现在,可以用 pe[:, 0::2] 来选择偶数列,也就是 sin的输入。这将告诉 PyTorch 选择每一行和每一偶数列。同样的方法也可以用于奇数列,即 cos的输入pe[:, 1::2]。这再次告诉 PyTorch 选择每一行和每一奇数列。k*div_term 的结果中存储了所有必要的输入,代码实现如下:
# set the odd values (columns 1 and 3)
pe[:, 0::2] = torch.sin(k * div_term)
# set the even values (columns 2 and 4)
pe[:, 1::2] = torch.cos(k * div_term)
# add a dimension for broadcasting across sequences: optional
pe = pe.unsqueeze(0)
print(pe)
得到结果如下:
可以看到,这些值与通过嵌套 for 循环获取的值完全相同。
看懂后,我们将上述过程封装成函数,如下所示:
def gen_pe(max_length, d_model, n):
# calculate the div_term
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(n) / d_model))
# generate the positions into a column matrix
k = torch.arange(0, max_length).unsqueeze(1)
# generate an empty tensor
pe = torch.zeros(max_length, d_model)
# set the even values
pe[:, 0::2] = torch.sin(k * div_term)
# set the odd values
pe[:, 1::2] = torch.cos(k * div_term)
# add a dimension
pe = pe.unsqueeze(0)
# the output has a shape of (1, max_length, d_model)
return pe
虽然它更为复杂,但 PyTorch 使用这种实现方式可以增强代码运行时的性能。
05
官方实现
既然所有的艰苦工作都已完成,那么官方实现起来也就简单明了了。以下代码源自于 The Annotated Transformer 。
网址:https://nlp.seas.harvard.edu/annotated-transformer/#embeddings-and-softmax
请注意,这里n 的默认值是 10,000,默认 max_length 是 5,000。
这个官方的版本还包含了dropout层,它以给定的概率p随机清零其输入的一些元素。这有助于正则化,并防止过拟合。代码如下:
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, dropout: float = 0.1, max_length: int = 5000):
"""
Args:
d_model: dimension of embeddings
dropout: randomly zeroes-out some of the input
max_length: max sequence length
"""
# inherit from Module
super().__init__()
# initialize dropout
self.dropout = nn.Dropout(p=dropout)
# create tensor of 0s
pe = torch.zeros(max_length, d_model)
# create position column
k = torch.arange(0, max_length).unsqueeze(1)
# calc divisor for positional encoding
div_term = torch.exp(
torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)
# calc sine on even indices
pe[:, 0::2] = torch.sin(k * div_term)
# calc cosine on odd indices
pe[:, 1::2] = torch.cos(k * div_term)
# add dimension
pe = pe.unsqueeze(0)
# buffers are saved in state_dict but not trained by the optimizer
self.register_buffer("pe", pe)
def forward(self, x: Tensor):
"""
Args:
x: embeddings (batch_size, seq_length, d_model)
Returns:
embeddings + positional encodings (batch_size, seq_length, d_model)
"""
# add positional encoding to the embeddings
x = x + self.pe[:, : x.size(1)].requires_grad_(False)
# perform dropout
return self.dropout(x)
有了上面的介绍,希望大家都可以看懂位置编码的代码实现。
读者福利:如果大家对大模型感兴趣,这套大模型学习资料一定对你有用
对于0基础小白入门:
如果你是零基础小白,想快速入门大模型是可以考虑的。
一方面是学习时间相对较短,学习内容更全面更集中。
二方面是可以根据这些资料规划好学习计划和方向。
包括:大模型学习线路汇总、学习阶段,大模型实战案例,大模型学习视频,人工智能、机器学习、大模型书籍PDF。带你从零基础系统性的学好大模型!
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓
👉AI大模型学习路线汇总👈
大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)
第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;
第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;
第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;
第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;
第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;
第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;
第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。
👉大模型实战案例👈
光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉大模型视频和PDF合集👈
观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;
• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;
• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;
• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。
👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费
】🆓