Transformer结构--输入编码(BPE,PE)
在Transformer结构中,输入编码是模型处理文本数据的关键步骤,其中**BPE(Byte Pair Encoding,字节对编码)和PE(Positional Encoding,位置编码)**是两种重要的编码方式,它们分别解决了分词和位置信息的问题。以下是对这两种编码方式的详细解析:
BPE(Byte Pair Encoding)
1. 背景与目的
- 传统分词问题:在自然语言处理中,传统的分词方式(如基于空格或标点符号的分词)往往无法有效处理未知词(OOV,Out-of-Vocabulary)或稀有词。这些词在训练数据中未出现或出现频率极低,导致模型难以学习到它们的表示。
- BPE的引入:BPE是一种数据压缩算法,最初用于文本压缩。在NLP中,BPE被用作一种子词(subword)分词算法,旨在将文本分解为更小的单元(子词),从而更有效地处理未知词和稀有词。
2. 算法原理
- 初始化词表:BPE从一个基础词表开始,通常包含所有单个字符。
- 迭代合并:BPE通过迭代地合并最频繁出现的字符对(或子词对)来扩展词表。每次合并后,新的子词被添加到词表中,而原始的字符对被替换为新的子词。
- 终止条件:合并过程持续进行,直到词表大小达到预设的阈值或合并次数达到上限。
3. 优点
- 处理未知词:BPE能够将未知词分解为已知的子词组合,从而允许模型学习到这些词的表示。
- 灵活性:BPE可以根据不同的任务和数据集调整词表大小,以平衡分词粒度和模型性能。
BPE代码
import tiktokendef main():print("Hello from hello-world!")# 获取编码enc = tiktoken.get_encoding("cl100k_base")# 编码文本tokens = enc.encode("tiktoken是OpenAI开源的一个快速分词工具。它将一个文本字符串(例如“tiktoken很棒!”)和一个编码(例如“cl100k_base”)作为输入,然后将字符串拆分为标记列表。-----aa aa aa bb bb bb cc cc aabbcc aabb ")print(tokens) # 输出: [24912, 2375]# 解码文本text = enc.decode(tokens)print(text) # 输出: hello worldif __name__ == "__main__":main()
PE(Positional Encoding)
1. 背景与目的
- Transformer的局限性:Transformer模型基于自注意力机制,能够并行处理输入序列中的所有元素。然而,这种并行性导致模型无法直接捕捉到序列中元素的位置信息。
- 位置编码的引入:为了解决这个问题,Transformer引入了位置编码(PE),为输入序列中的每个元素添加位置信息。
2. 实现方式
-
正弦和余弦函数:Transformer使用正弦和余弦函数来生成位置编码。对于位置 p o s pos pos和维度 i i i,位置编码的值由以下公式给出:
P E ( p o s , 2 i ) = sin ( p o s 1000 0 2 i / d model ) PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)
P E ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 2 i / d model ) PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)其中, d model d_{\text{model}} dmodel是模型的维度。
-
添加到输入嵌入:生成的位置编码被添加到输入序列的词嵌入(word embedding)中,形成最终的输入表示。
3. 优点
- 捕捉位置信息:位置编码允许Transformer模型捕捉到输入序列中元素的位置信息,从而更准确地理解序列的结构和语义。
- 可学习性:虽然位置编码是预先计算的,但它与词嵌入一起被输入到模型中,允许模型在训练过程中学习到更优的位置表示。
BP代码
PyTorch提供了torch.nn.Embedding和自定义操作,可以更高效地实现PE:
import torch
import mathclass PositionalEncoding(torch.nn.Module):def __init__(self, d_model, max_len=5000):super(PositionalEncoding, self).__init__()self.d_model = d_model# 创建位置编码矩阵pe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))pe[:, 0::2] = torch.sin(position * div_term) # 偶数维度pe[:, 1::2] = torch.cos(position * div_term) # 奇数维度pe = pe.unsqueeze(0) # 添加batch维度,形状: (1, max_len, d_model)self.register_buffer('pe', pe) # 注册为缓冲区,不参与训练def forward(self, x):""":param x: 输入张量 (batch_size, seq_len, d_model):return: 添加位置编码后的张量"""x = x + self.pe[:, :x.size(1)] # 截取与输入序列长度匹配的PEreturn x# 示例:生成长度为10,维度为512的位置编码
d_model = 512
pe_layer = PositionalEncoding(d_model)
x = torch.zeros(32, 10, d_model) # 假设batch_size=32, seq_len=10
x_with_pe = pe_layer(x)
print(x_with_pe.shape) # 输出: torch.Size([32, 10, 512])