【学习】手搓Transformer:Embedding


手搓Transformer:Embedding


《Attention Is All You Need》里的Transformer架构

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)

文章作者: Cyan.
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cyan. !
 上一篇
【学习】手搓Transformer:Multi-Attention 【学习】手搓Transformer:Multi-Attention
回想起上次仔细看transformer还是上次,现在遗忘得又有点多了,干脆从原理到代码重新过一遍,于是就有了这篇博客~
2024-12-25
下一篇 
【学习】“视界感知者”项目开发文档 【学习】“视界感知者”项目开发文档
上传一下大学期间做的最有成就感的一个项目!“视界感知者”是完全由我们大学本科生团队一起开发的一款盲杖产品,虽然技术实现并不复杂,主要牵涉API掉用、服务器通信与工作流搭建,但经过不断迭代最终成功完成产品还是很自豪的~
2024-09-18
  目录