视频详细讲解(一行一行代码讲解实现过程):代码实现Transformer的LLM模型和原理讲解:Token、向量化、位置向量运算_哔哩哔哩_bilibili
1 Token、向量化、位置向量相加运算
2 将输入转成Token
'''
1、使用tiktoken获取编码器,编码器类型为"cl100k_base(词汇表大小为40,960个tokens)",
除了 cl100k_base 之外,tiktoken 还支持:
r50k_base(词汇表大小为50,278个tokens)、
p50k_base(词汇表大小为50,258个tokens)、
gpt2(词汇表大小为50,257个tokens)
2、什么是词汇表
词汇表可以被看作是一个映射表,它把文本中的每个唯一词或标记映射到一个唯一的数字ID
'''
encoding = tiktoken.get_encoding("cl100k_base")
# 将文本转换为token序列
tokenized_text = encoding.encode(text)
3 向量化
3.1 处理流程
通过nn.Embedding(max_token_value+1, model_dimension)后初始化向量。
当你使用 self.token_embedding_lookup_table(train_data)
调用嵌入层时,实际上是在根据 train_data
中提供的索引来查找对应的嵌入向量。
-
输入数据 (
train_data
):train_data
是一个整数型的张量,其中的每个元素代表一个标记(token)。这些整数在[0, max_token_value]
的范围内。例如,如果你有一个句子 "I love PyTorch",并且已经将其转换为整数索引,那么train_data
可能是一个类似于[1, 345, 23, 758]
的张量,这里的每个数字都是一个标记ID。 -
输出:调用嵌入层之后,你会得到一个形状为
(batch_size, sequence_length, model_dimension)
的张量。其中:batch_size
是你在一次前向传播中处理的数据样本数量。sequence_length
是输入序列的长度,即每个样本中包含的标记数量。model_dimension
是每个标记嵌入向量的长度,也就是你传递给nn.Embedding
的第二个参数。
假设你的 train_data
的形状是 (batch_size, sequence_length)
,那么调用 self.token_embedding_lookup_table(train_data)
后的结果就是一个三维张量,其中包含了每个标记的嵌入向量。
假设你有以下情况:
max_token_value = 1000
model_dimension = 50
train_data
是一个形状为(64, 10)
的张量,表示一个批次大小为 64,每个样本有 10 个标记的输入数据。
那么,self.token_embedding_lookup_table(train_data)
的结果将会是一个形状为 (64, 10, 50)
的张量,每个元素都是对应标记的嵌入向量。
# 随机采样起始索引,确保不会超出数据范围
random_index = torch.randint(low=0, high=len(data) - context_length, size=(batch_size,))
# 根据起始索引,构建训练输入train_data,每个输入包含context_length长度的序列
train_data = torch.stack([data[idx:idx + context_length] for idx in random_index]).to(device)
# 构建训练目标y,target_label为train_data的下一个字符,用于预测任务
target_label = torch.stack([data[idx + 1:idx + context_length + 1] for idx in random_index]).to(device)
train_data
tensor([[ 235, 35287, 15120, 31640, 17792, 9554, 17161, 33208], [ 2118, 15120, 8107, 55030, 32943, 84150, 106, 20834], [ 198, 15722, 99, 164, 245, 97, 6708, 108], [ 109, 18259, 238, 21990, 76706, 1811, 92776, 33655]])
对应文本内容:(�了一代人的文化,“一年之内售出、武藤兰。乐生活。即使�)
target_label
tensor([[35287, 15120, 31640, 17792, 9554, 17161, 33208, 34208], [15120, 8107, 55030, 32943, 84150, 106, 20834, 32335], [15722, 99, 164, 245, 97, 6708, 108, 3922], [18259, 238, 21990, 76706, 1811, 92776, 33655, 28194]])
对应文本内容:(了一代人的文化和,一年之内售出最,武藤兰,,�引退。武�)
target_label是在train_data往后移动一位,也就是在计算损失时,不断计算train_data的预测结果与target_label的标签的差距(也就是损失),不断的训练和反向传播去更新权重值,直到损失值很小,目的就是train_data的预测结果越来越接近target_label的标签。
'''
初始化一个嵌入层,用于将token转换为嵌入向量,得到一个巨大的矩阵,矩阵大小[max_token_value, model_dimension],
这个矩阵后续会投影到一个线形层。
max_token_value 参数定义了词汇表的大小(即可以索引的最大整数值+1),
model_dimension 定义了每个嵌入向量的维度。如果选择64作为维度,则每个单词会被映射到一个64维的向量上。
嵌入层的工作原理就像是查找表一样——输入一个索引,返回该索引对应的向量。
'''
self.token_embedding_lookup_table = nn.Embedding(max_token_value+1, model_dimension)
token_embedding = self.token_embedding_lookup_table(train_data)
nn.Embedding(max_token_value+1, model_dimension)后得到初始的向量词汇矩阵,矩阵大小是[99850, 16],16是每个字的维度。
token_embedding = self.token_embedding_lookup_table(train_data)执行后,得到:
3.2 嵌入层nn.Embedding工作原理
假设我们有一个非常小的词汇表,只包含四个词:“the”,“cat”,“sat”,“on”。我们希望用一个较低维度的向量来表示这些词,这样在处理自然语言任务时,可以更高效地进行计算,并且捕捉到词与词之间的关系。
3.2.1 定义词汇表
首先定义词汇表及其索引:
1word_to_idx = {"the": 0, "cat": 1, "sat": 2, "on": 3}
2idx_to_word = {v: k for k, v in word_to_idx.items()}
3vocab_size = len(word_to_idx) # 词汇表大小为4
4embedding_dim = 3 # 假设每个词的嵌入维度为3
3.2.2 初始化嵌入层
接下来,我们初始化一个嵌入层,它将会为词汇表中的每个词学习一个三维的向量表示:
1import torch
2import torch.nn as nn
3
4embedding_layer = nn.Embedding(vocab_size, embedding_dim)
此时,embedding_layer
的权重矩阵是一个形状为 (4, 3)
的张量,每一行对应一个词的嵌入向量。
3.2.3 准备训练数据
现在我们需要准备一些训练数据。假设我们有一个句子:“The cat sat on the mat”。我们需要将这个句子转换成索引形式:
1sentence = ["the", "cat", "sat", "on"]
2train_data = torch.tensor([word_to_idx[word] for word in sentence], dtype=torch.long)
3print(train_data)
输出可能会是这样的:
1tensor([0, 1, 2, 3])
这是一个一维张量,包含了句子中每个词对应的索引。
3.2.4 应用嵌入层
现在我们可以把索引形式的数据传递给嵌入层,嵌入层会根据索引查找对应的嵌入向量:
1embedded_sentence = embedding_layer(train_data)
2print(embedded_sentence)
输出将会是一个形状为 (4, 3)
的张量,其中每一行都是句子中相应词的嵌入向量。
3.2.5 结果分析
假设嵌入层的权重矩阵是这样的(这是随机初始化的结果,实际应用中会通过训练调整):
1[[0.1, 0.2, 0.3],
2 [0.4, 0.5, 0.6],
3 [0.7, 0.8, 0.9],
4 [1.0, 1.1, 1.2]]
那么,对于句子 "the cat sat on"
,得到的嵌入向量将是:
1[[0.1, 0.2, 0.3], # "the"
2 [0.4, 0.5, 0.6], # "cat"
3 [0.7, 0.8, 0.9], # "sat"
4 [1.0, 1.1, 1.2]] # "on"
这就是 nn.Embedding
层如何将整数索引转换成对应词的嵌入向量的过程。每一个整数索引直接对应了嵌入矩阵的一行,因此可以通过索引来获取对应的嵌入向量。
4 位置编码
4.1 位置编码执行过程
4.1.1 先初始化位置编码查找表
得到矩阵[8, 16],8行对应训练的8个文字长度,16是一个字有16个维度。
input_context_length = train_data.shape[1]
# 初始化一个位置编码的查找表
position_encoding_lookup_table = torch.zeros(input_context_length, model_dimension, device=device)
4.1.2生成一个8行一列的张量
position = torch.arange(0, input_context_length, dtype=torch.float).unsqueeze(1)
4.1.3 计算正弦余弦
position_encoding_lookup_table[:, 0::2] = torch.sin(position * wave_length)
position_encoding_lookup_table[:, 1::2] = torch.cos(position * wave_length)
position * wave_length得出以下结果
4.2 位置向量原理解析
4.2.1 为什么要将position 和 wave_length 相乘
将 position
和 wave_length相乘是为了在生成位置编码时,根据不同的位置和维度引入不同的频率成分。这种设计的目的在于使得每个位置在不同维度上的位置编码都有独特的模式,同时还能保持一定的规律性和周期性。
具体来说:
-
位置信息:
position
是一个形状为(input_context_length, 1)
的张量,每一行对应序列中的一个位置。- 这个张量包含了从0到
input_context_length - 1
的整数序列,代表序列中每个元素的位置。
-
频率因子:
wavelength
是一个形状为(model_dimension // 2)
的张量,包含了用于计算正弦和余弦函数的频率因子。- 频率因子的计算方式为
exp(-ln(10000) / model_dimension * i)
,其中i
从0到model_dimension - 1
每隔2取一个数。这使得频率因子在不同的维度上有不同的衰减速率。
-
相乘操作:
- 当
position
与wavelength
相乘时,实际上是为每个位置分配了一个独特的频率成分。 - 由于
position
是一个二维张量,而wavelength
是一个一维张量,相乘操作会自动广播wavelength
到所有位置,产生一个形状为(input_context_length, model_dimension // 2)
的结果。
- 当
-
正弦和余弦函数:
- 使用正弦和余弦函数分别应用于
position * wavelength
的结果,可以为每个位置在不同维度上生成独特的编码。 - 正弦和余弦函数在不同的维度上以不同的频率变化,这样可以保证位置编码在各个维度上都是唯一的,并且具有周期性。
- 使用正弦和余弦函数分别应用于
这种设计的好处包括:
- 区分性:每个位置都有独特的编码,这有助于模型学习到不同位置的信息。
- 周期性:正弦和余弦函数的周期性使得位置编码可以推广到任意长度的序列,即使在训练时没有遇到过这么长的序列。
- 平滑过渡:不同位置之间的编码变化是连续和平滑的,有助于模型捕捉到位置间的关系。
因此,将 position
和 wavelength
相乘是生成位置编码的核心步骤之一,它确保了位置编码既具有区分性又具有规律性。
4.2.2 通俗讲解位置编码的原理
想象一下你在看一本漫画书,每一页都有不同的画面。如果你要告诉你的朋友某一页的内容,你需要知道页码,也就是这本书中的“位置”。
在计算机处理信息的时候,特别是处理像句子这样的序列数据时,也需要知道每个单词的位置。这是因为句子中词的位置很重要,比如,“狗追猫”和“猫追狗”意思完全不同。
位置的重要性
在自然语言处理中,我们使用一种叫作位置编码的技术来告诉模型每个词在句子中的位置。这就好比给每个单词一个编号,比如第一个词编号为1,第二个词编号为2,以此类推。
为什么需要不同的频率?
为了让模型能够理解这些位置,我们需要给每个位置一个独特的标识。但是,如果我们只是简单地使用一个数字来表示位置,那么模型可能无法很好地利用这些信息。因此,我们使用了一种巧妙的方法:通过不同的频率来标记位置。
不同频率的正弦波
想象一下,你在海边听海浪的声音。海浪有大有小,有快有慢。如果我们将海浪比作正弦波,那么大的波就是低频的,小的波就是高频的。当我们把每个位置与不同频率的正弦波相结合时,每个位置就有了自己独特的声音。
结合位置与频率
在这个例子中,position
就像是每个词的位置编号,而 wave_length就像是不同频率的正弦波。我们将位置编号与不同频率的正弦波结合(即相乘),就可以得到一个对于每个位置而言独一无二的标识符。
这样做之后,模型就可以更好地理解和利用每个词的位置信息了。这就是为什么我们要将 position
和 wave_length相乘的原因——为了给每个位置赋予一个独特的标识,帮助模型更好地工作。
5 训练向量与位置向量相加
'''
1、将训练数据 train_data 输入数据转换为其对应的嵌入向量。
2、将token向量和位置嵌入相加
'''
position_train_data = self.token_embedding_lookup_table(train_data) + position_embedding
与位置向量相加后得到,4个批次,一个批次有8个token,一个token有16个维度,得到[4, 8, 16]的矩阵。