01
引言
我们在前面一系列文章中介绍了Transformer相关的理论知识,从本文开始我们开始动手实现Transformer。本文首先介绍Embedding Layer的相关知识,希望大家可以建立对其直观的理解。
02
背景介绍
嵌入层Embedding Layer的目标是使模型能够更多地了解单词、Tokens或其他输入之间的关系。如下所示:
当模型的词汇表只由十几个Token构成时,one-hot编码向量通常是一种方便的表示方法。然而,大型语料库可能有数十万个Token。此时,使用嵌入层Embedding Layer可以将向量映射到更小的维度,而不是使用无法表达太多含义、充满零的稀疏向量。这些嵌入向量经过训练后,可以传达更多关于每个单词及其与其他单词关系的信息。
基本上,每个单词都由一个 d_model 维向量表示,其中 d_model 可以是任何数字。它只是表示嵌入维度。如果 d_model 为 2 或 3,则可以直观地显示每个词之间的关系,但根据任务的不同,通常使用256、512 或1024 的值。
下面是一个嵌入向量可视化的例子,相似类型的书籍的嵌入向量在向量空间彼此靠近:
03
Embedding Vectors
通过上述说明,我们知道嵌入向量表示的维度为:
(seq_length,vocab_size)X(vocab_size,d_model)=(seq_length,d_model)
观察上述输出,我们知道当一个one-hot编码矩阵与一个嵌入矩阵相乘时,嵌入矩阵的相应位置处的向量将不做任何改变地返回。下面是one-hot编码向量与嵌入矩阵之间的矩阵乘法,输出仍然为嵌入矩阵。
这表明有一种更简便的方法可以获得这些嵌入向量的值,而无需使用矩阵乘法,因为矩阵乘法可能会耗费大量资源。为了简化上述过程,我们可以直接利用分配给每个单词的整数来直接索引固定次序下单词表的嵌入表示。这就好比从1维索引获取d_model维嵌入向量表示,这将提供更多关于输入Token的信息。
下图显示了如何在不相乘的情况下得到上图相同的结果:
04
代码实现之创建词汇表
有了上述词汇表中固定单词排序下的嵌入表示后,我们可以方便的通过索引来获取输入序列的嵌入表示,过程如下:
上图的简单过程可以用 Python实现。获取输入序列的嵌入表示需要一个tokenizer、一个词汇表及其索引,以及词汇表中每个单词的三维嵌入。Tokenizer将输入序列分割成单个Token,本例中的Token为小写单词。下面的简单函数会移除序列中的标点符号,将其拆分成Token,并将其转化小写。
# importing required libraries
import math
import copy
import numpy as np
# torch packages
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
# visualization packages
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt
example = "Hello! This is an example of a paragraph that has been split into its basic components. I wonder what will come next! Any guesses?"
def tokenize(sequence):
# remove punctuation
for punc in ["!", ".", "?"]:
sequence = sequence.replace(punc, "")
# split the sequence on spaces and lowercase each token
return [token.lower() for token in sequence.split(" ")]
print(tokenize(example))
输出如下:
['hello', 'this', 'is', 'an', 'example', 'of', 'a', 'paragraph', 'that',
'has', 'been', 'split', 'into', 'its', 'basic', 'components', 'i',
'wonder', 'what', 'will', 'come', 'next', 'any', 'guesses']
def build_vocab(data):
# tokenize the data and remove duplicates
vocab = list(set(tokenize(data)))
# sort the vocabulary
vocab.sort()
# assign an integer to each word
stoi = {word:i for i, word in enumerate(vocab)}
return stoi
# build the vocab
stoi = build_vocab(example)
print(stoi)
{'a': 0,
'an': 1,
'any': 2,
'basic': 3,
'been': 4,
'come': 5,
'components': 6,
'example': 7,
'guesses': 8,
'has': 9,
'hello': 10,
'i': 11,
'into': 12,
'is': 13,
'its': 14,
'next': 15,
'of': 16,
'paragraph': 17,
'split': 18,
'that': 19,
'this': 20,
'what': 21,
'will': 22,
'wonder': 23}
05
代码实现之获取嵌入向量
接着我们可以使用上述词汇表将任何Token序列转换为整数表示。
sequence = [stoi[word] for word in tokenize("I wonder what will come next!")]
print(sequence)
结果如下:
[11, 23, 21, 22, 5, 15]
下一步是创建嵌入层,它只不过是一个大小为(vocab_size,d_model)的随机值矩阵。这些值可以使用 torch.rand 生成。
# vocab size
vocab_size = len(stoi)
# embedding dimensions
d_model = 3
# generate the embedding layer
embeddings = torch.rand(vocab_size, d_model) # matrix of size (24, 3)
print(embeddings)
输出如下:
tensor([[0.7629, 0.1146, 0.1228],
[0.3628, 0.5717, 0.0095],
[0.0256, 0.1148, 0.1023],
[0.4993, 0.9580, 0.1113],
[0.9696, 0.7463, 0.3762],
[0.5697, 0.5022, 0.9080],
[0.2689, 0.6162, 0.6816],
[0.3899, 0.2993, 0.4746],
[0.1197, 0.1217, 0.6917],
[0.8282, 0.8638, 0.4286],
[0.2029, 0.4938, 0.5037],
[0.7110, 0.5633, 0.6537],
[0.5508, 0.4678, 0.0812],
[0.6104, 0.4849, 0.2318],
[0.7710, 0.8821, 0.3744],
[0.6914, 0.9462, 0.6869],
[0.5444, 0.0155, 0.7039],
[0.9441, 0.8959, 0.8529],
[0.6763, 0.5171, 0.9406],
[0.1294, 0.6113, 0.5955],
[0.3806, 0.7946, 0.3526],
[0.2259, 0.4360, 0.6901],
[0.6300, 0.2691, 0.9785],
[0.2094, 0.9159, 0.7973]])
创建嵌入层后,索引序列可用于为每个标记Token选择合适的嵌入。原始序列的形状为 (6, ),值为 [11, 23, 21, 22, 5, 15],则通过索引获得的嵌入表示为:
# embed the sequence
embedded_sequence = embeddings[sequence]
embedded_sequence
输出为:
tensor([[0.7110, 0.5633, 0.6537],
[0.2094, 0.9159, 0.7973],
[0.2259, 0.4360, 0.6901],
[0.6300, 0.2691, 0.9785],
[0.5697, 0.5022, 0.9080],
[0.6914, 0.9462, 0.6869]])
现在,六个Tokens中的每个Token都被一个3维向量取代。这表示每个Token均可以从三个维度进行映射。下图显示了未经训练的嵌入矩阵,而经过训练的嵌入矩阵则会将相似的词映射到彼此附近,就像前面提到的例子一样。
# visualize the embeddings in 3 dimensions
x, y, z = embedded_sequences[:, 0], embedded_sequences[:, 1], embedded_sequences[:, 2]
ax = plt.axes(projection='3d')
ax.scatter3D(x, y, z)
结果如下:
06
Pytorch实现
由于将使用 PyTorch 来实现Transformer,因此可以对 nn.Embedding 模块进行分析。PyTorch 将其定义为:
一个简单的查找表,用于存储固定字典和大小的嵌入。该模块通常用于存储单词嵌入,并使用索引来进行检索。该模块的输入是索引列表,输出是相应的词嵌入。
这与上一示例中使用索引而非one-hot向量时的操作完全相同。nn.Embedding 需要 vocab_size 和嵌入维度(今后将继续记为 d_model)。提醒一下,这是模型维度的简称。
# vocab size
vocab_size = len(stoi) # 24
# embedding dimensions
d_model = 3
# create the embeddings
lut = nn.Embedding(vocab_size, d_model) # look-up table (lut)
# view the embeddings
print(lut.state_dict()['weight'])
tensor([[-0.3959, 0.8495, 1.4687],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ]], grad_fn=<EmbeddingBackward0>)
如果向它传递与之前相同的索引序列 [11,23,21,22,5,15],输出将是一个维度为(6,3)矩阵,其中每个标记Token均由其三维嵌入向量表示。索引必须是张量形式,数据类型为integer或long。
indices = torch.Tensor(sequence).long()
embeddings = lut(indices)
print(embeddings)
输出如下:
tensor([[ 0.7584, 0.2332, -1.2062],
[ ],
[ ],
[ ],
[ ],
[ ]], grad_fn=<EmbeddingBackward0>)
07
Transformer中的嵌入层
在原论文中,编码器和解码器均都使用了嵌入层。nn.Embedding 模块中唯一增加的是一个标量。嵌入层的权重乘以 √(d_model)。这有助于在下一步将单词嵌入添加到位置编码中时保留基本含义。这实质上使位置编码相对变小,减少了对单词嵌入向量的影响。Stack Overflow 的这一主题对此进行了更多讨论。
网址:https://stackoverflow.com/questions/56930821/why-does-embedding-vector-multiplied-by-a-constant-in-transformer-model
为了实现这一点, 我们可以创建一个类,它将被称为Embeddings,并利用PyTorch 的 nn.Embedding 模块。该实现基于 The Annotated Transformer 的实现。
网址:https://nlp.seas.harvard.edu/annotated-transformer/#embeddings-and-softmax
代码如下:
class Embeddings(nn.Module):
def __init__(self, vocab_size: int, d_model: int):
"""
Args:
vocab_size: size of vocabulary
d_model: dimension of embeddings
"""
# inherit from nn.Module
super().__init__()
# embedding look-up table (lut)
self.lut = nn.Embedding(vocab_size, d_model)
# dimension of embeddings
self.d_model = d_model
def forward(self, x: Tensor):
"""
Args:
x: input Tensor (batch_size, seq_length)
Returns: embedding vector
"""
# embeddings by constant sqrt(d_model)
return self.lut(x) * math.sqrt(self.d_model)
前向过程:
lut = Embeddings(vocab_size, d_model)
print(lut(indices))
tensor([[-1.1189, 0.7290, 1.0581],
[ ],
[ ],
[ ],
[ ],
[ ]], grad_fn=<MulBackward0>)
07
批量推理
到目前为止,每次嵌入都只使用一个序列。然而,模型通常是用一批次序列来训练的。此时,输入基本上是一个序列列表,并将其转换为索引,然后进行嵌入。如下图所示。
# list of sequences (3, )
sequences = ["I wonder what will come next!",
"This is a basic example paragraph.",
"Hello, what is a basic split?"]
虽然前面的例子比较简单,但它可以将其推广到成批次的序列。上图所示的例子是一个包含三个序列的批次batch;经过Tokenization后,每个序列由六个Token构成。Tokenize后的序列形状为(3,6),与(batch_size,seq_length)相关。本质上,这是由三个句子构成,每个句子由六个单词构成。
# tokenize the sequences
tokenized_sequences = [tokenize(seq) for seq in sequences]
print(tokenized_sequences)
[['i', 'wonder', 'what', 'will', 'come', 'next'],
['this', 'is', 'a', 'basic', 'example', 'paragraph'],
['hello', 'what', 'is', 'a', 'basic', 'split']]
然后,这些Tokenize序列就可以使用词汇表转换为索引表示法。
# index the sequences
indexed_sequences = [[stoi[word] for word in seq] for seq in tokenized_sequences]
print(indexed_sequences)
[[11, 23, 21, 22, 5, 15],
[20, 13, 0, 3, 7, 17],
[10, 21, 13, 0, 3, 18]]
最后,这些索引序列可以转换成张量,并传递到嵌入层,如下:
# convert the sequences to a tensor
tensor_sequences = torch.tensor(indexed_sequences).long()
print(lut(tensor_sequences))
结果如下:
tensor([[[ 0.1348, -1.3131, 2.8429],
[ ],
[ ],
[ ],
[ ],
[ ]],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ]],
[ ],
[ ],
[ ],
[ ],
[ ],
[ ]]], grad_fn=<MulBackward0>)
输出将是一个(3,6,3)矩阵,与(batch_size、seq_length、d_model)相关。从本质上讲,每个索引Token都会被相应的三维嵌入向量取代。
在进入下一节之前,了解这些数据的形状(batch_size、seq_length、d_model)极为重要:
batch_size: 每次提供的序列数目有关,通常为8,16,32,64 seq_length: 与tokenization化后每个序列中的token数目相关 d_model: 与每个token嵌入向量的维度有关
下一篇文章将介绍位置编码。欢迎大家点赞关注和分享!
点击上方小卡片关注我
添加个人微信,进专属粉丝群!