手搓Transformer:Embedding
《Attention Is All You Need》里的Transformer架构

先从 Transformer 的 Embedding 部分写起。当我们输入文本到 Transformer 网络中时,模型接收的是存储着数值的向量而不是文字,因而第一步要做的事就是将这个 text 转化为词向量。
1. Tokenizer 处理得到词索引
假定我们输入的 text 文本是一个包含许多单词的句子,我们需要想办法用数值表达这个句子,一种做法就是对每一个单词取一个唯一的索引值,用这个数值来表示这个句子。这些索引映射关系存起来就是词表。
- 当然分词粒度也不一定就是一个单词。只是说单词分词法(word base)最直观。单词分词法将一个word作为最小元,也就是根据空格或者标点分词。最详尽的分词是单字分词法(character-base)。单字分词法会穷举所有出现的字符,所以是最完整的。另外还有一种最常用的、介于两种方法之间的分词法叫子词分词法,会把一个句子分成最小可分的子词例如[‘To’, ‘day’, ‘is’, ‘S’, ‘un’, ‘day’]。
比如 text = “Today is a nice day!”,token = tokenizer(text),那么处理后得到的 tokens 就是一个字典,其中包含了 “input_ids” 词索引序列:’input_ids’: [[101, 2769, 3221, …, 102, 0, 0]]。0 是 padding 的填充项(补全 seq length 长度)
2. Token_Embedding 词嵌入处理
经过 tokenizer 分词处理后,我们的输入 x 就变为一个形状 [batch_size, seq_len] 的向量,例如 [[101, 2054, 2003, 2026, 3793, 102], [101, 2054, 2064, 2017, 102, 0]] 表示输入 batch_size = 2 两个句子。
那么 embedding 操作就是把输入向量升维。我们输入的 x 对一个单词的表示现在相当于是一个索引值,而我们要把每一个单词都变成一个唯一的向量模型才能学习到其空间嵌入特征。具体而言,假设我们有以下配置:
批次大小 (batch_size) = 2
序列长度 (seq_len) = 4
嵌入维度 (d_model) = 6
词汇表大小 (vocab_size) = 10000
x = [
[101, 2054, 2003, 102], # 第一个句子的词索引
[101, 5243, 3122, 102] # 第二个句子的词索引
]
嵌入后的张量可能就是这样,形状变为 [2,4,6] (batch_size, seq_len, d_model)
token_embedding = [
[ # 第一个句子
[0.1, -0.2, 0.3, 0.1, -0.5, 0.7], # 词索引101的嵌入向量
[0.5, 0.2, -0.3, 0.4, 0.1, -0.2], # 词索引2054的嵌入向量
[-0.3, 0.1, 0.5, -0.2, 0.4, 0.3], # 词索引2003的嵌入向量
[0.2, -0.4, -0.1, 0.5, 0.3, 0.1] # 词索引102的嵌入向量
],
[ # 第二个句子
[0.1, -0.2, 0.3, 0.1, -0.5, 0.7], # 词索引101的嵌入向量
[0.4, 0.3, 0.2, -0.1, -0.3, 0.5], # 词索引5243的嵌入向量
[-0.2, 0.5, 0.1, 0.3, -0.4, 0.2], # 词索引3122的嵌入向量
[0.2, -0.4, -0.1, 0.5, 0.3, 0.1] # 词索引102的嵌入向量
]
]
嵌入转化的原理实际就是一个查找表。我们首先把每个单词的词索引转化成 one-hot 向量,那么可以想象这个向量维数应该很大,因此再降维。从每个单词的 one-hot 向量经过 Embedding 矩阵得到降维后的结果。比如有三个单词, one-hot 处理后的结果是 $36$ 大小的向量,那么经过一个 $64$ 大小的权重矩阵就可以乘出 $3*4$ 大小的结果从而降维(d_model = 4)。因此这个嵌入矩阵的形状就应该是[vocab_size, d_model] 大小。
继承 torch 里的 Embedding 类,forward 的时候把输入 x 转换为嵌入向量。具体实现:
# 将输入的词汇表索引转换为指定维度的Embedding
# 每个token的词索引升维到d_model维,padding_idx=1表示填充词的索引为1
# 继承nn.Embedding在训练中前向传播,反向传播,更新参数
class TokenEmbedding(nn.Embedding):
def __init__(self, vocab_size, d_model):
super(TokenEmbedding, self).__init__(vocab_size, d_model, padding_idx=1)
2. Position_Embedding 位置编码
Transformer 架构引入了位置编码,为每个单词向量生成一个固定的位置向量,来表达其位置相对关系。
这部分是固定计算的,对于每个 seq_len 长度的句子输入,会返回一个 [seq_len, d_model] 形状即符合当前序列长度的位置编码。把词嵌入和位置编码相加才是最终的词向量。
# 通过位置编码计算输入每个词生成的正弦余弦位置编码
# 创建的是固定不变的位置编码,在训练中不更新,直接基于公式计算这个序列长度的位置编码矩阵
class PositionalEmbedding(nn.Module):
def __init__(self, d_model, max_len, device):
super(PositionalEmbedding, self).__init__()
# 初始化一个大小为(max_len, d_model)的零矩阵
self.encoding = torch.zeros(max_len, d_model, device=device)
self.encoding.requires_grad = False
pos = torch.arange(0, max_len, device=device)
pos = pos.float().unsqueeze(dim=1)
_2i = torch.arange(0, d_model, step=2, device=device).float()
self.encoding[:, 0::2] = torch.sin(pos / (10000 ** (_2i / d_model)))
self.encoding[:, 1::2] = torch.cos(pos / (10000 ** (_2i / d_model)))
def forward(self, x):
# x 形状: [batch_size, seq_len],也就是词索引
batch_size, seq_len = x.size()
# 返回适合当前序列长度的位置编码
return self.encoding[:seq_len, :]
3. Transformer_Embedding
最后把词嵌入和位置编码合并,即直接相加。(这里在嵌入层神经网络应用一个 dropout 防止过拟合)
# 嵌入层的输入是经过tokenizer处理后的词索引,输出是词的Embedding和位置编码
class TransformerEmbedding(nn.Module):
def __init__(self, vocab_size, d_model, max_len, drop_prob, device):
super(TransformerEmbedding, self).__init__()
self.token_embedding = TokenEmbedding(vocab_size, d_model)
self.positional_embedding = PositionalEmbedding(d_model, max_len, device)
self.dropout = nn.Dropout(p=drop_prob)
def forward(self, x):
token_embedding = self.token_embedding(x)
positional_embedding = self.positional_embedding(x)
return self.dropout(token_embedding + positional_embedding)