Skip to main content

Transformer

伟大无需多言。

info

《Attention Is All You Need》截止2025年,谷歌学术总引用次数排名第2。

关于大模型,我很早就了解了它的工作原理,但是和类与对象一样,很难在短时间内向他人讲述清楚。我认为抽象的内容最需要的是可视化的展示。

Transformer可视化这个项目很好的展示了Transformer的工作原理。

以下是我配套的一些补充说明,目的是根据公式与原理逐步手写一个Transformer模型。

以下是我的基础模型配置:

dtype = 'bfloat16' # 数据类型:即每个数字占用16位(2字节),适当降低精度可以减少显存占用并轻度降智,精度低于int4时模型会断崖式降智。
batch_size = 64 # 批次大小:越大训练速度越快,但占用显存越多。对模型智力影响偏小。
block_size = 1024 # 上下文长度:越大,模型理解的上下文越多,智力越强,但占用显存越多。
n_embd = 768 # 词嵌入维度:越大,模型学习能力越强,但占用显存指数级增加。对模型智力有较大影响。
n_layer = 6 # Transformer层数:层数越多,模型表达能力越强,但训练和推理时间线性增加,显存占用也增加
n_head = 6 # 注意力头数:多头注意力机制的头数,通常设为词嵌入维度的约数,用于并行学习不同的注意力模式
# 以下和显存占用无关,和收敛速度有关
dropout = 0.0 # Dropout比率:初期设为 0 加速训练,后期如果过拟合调成 0.1,用于防止过拟合
learning_rate = 6e-4 # 学习率:控制模型参数更新的步长,过大可能导致训练不稳定,过小训练速度慢
max_iters = 5000 # 最大训练迭代次数:训练的总轮数,需要根据数据集大小和训练效果调整
lr_decay_iters = 5000 # 学习率衰减迭代次数:学习率开始衰减的迭代次数,通常与max_iters相同
min_lr = 6e-5 # 最小学习率:学习率衰减的下限,防止学习率过小导致训练停滞
# 基础配置
device = 'cuda' # 计算设备:'cuda'表示使用GPU加速,'cpu'表示使用CPU(速度较慢)
compile = True # 是否编译模型:True会使用PyTorch的torch.compile优化,可以提升训练和推理速度

通过这个单元的学习,你可以获得一个专属于自己的大模型,并可以用于自己的业务场景。同时可以回答下面的问题:

不同的大模型可以用同一套提示词吗?

使用了相同的语料、token分词算法。那么大模型的tokenizer是相似的。通常来说,同个公司的大模型,语料与算法是相似的。因此相似的提示词可以生效。

如果语料差距较大、token分词算法不同,那么提示词可能无法生效。

常见的分词算法例如:Byte-Pair Encoding (BPE)

原理:通过合并最频繁的字符对来构建词汇表,适合处理罕见词和新词。

大模型最大上下文长度可以动态变化吗?

  • Transformer模型的设计中,输入序列的长度通常在模型的构建时就被固定。这是因为模型的自注意力机制需要为每个输入token计算与其他所有token的关系,计算复杂度与输入长度的平方成正比。因此,固定的输入长度可以简化计算和内存管理。

  • Transformer使用位置编码(Positional Encoding)来 为输入序列中的每个token提供位置信息。位置编码的维度通常与模型的隐藏层维度相同,而位置编码的数量通常是根据预设的最大输入长度来定义的。如果输入长度超过了这个预设值,模型将无法正确处理超出部分的token。

  • 在训练过程中,模型通常会使用固定长度的输入序列。如果训练数据中的序列长度超过了模型的最大输入长度,通常会进行截断或填充(padding),这可能导致信息丢失或计算效率低下。

Temperature 是如何影响模型输出的?

Temperature 是一个控制模型输出随机性的超参数。它会影响模型在选择下一个token时的概率分布。公式为:

Attention(Q,K,V)=softmax(QKT/dk)VAttention(Q, K, V) = softmax(QK^T / \sqrt{d_k}) * V
  • 当 Temperature 较高时,模型会倾向于选择概率较高的token,输出结果更确定。适合生成高质量、一致性强的文本。例如数学。
  • 当 Temperature 较低时,模型会倾向于选择概率较低的token,输出结果更随机。适合生成多样性、创造性强的文本。

理论上,Temperature 的取值范围不限,但是实际使用中,通常取值在0-2之间,过高的 Temperature 会导致模型输出不准确。

如果你想生成一个童话的故事,且你可以设置了一个的 Temperature 为0,那么模型会倾向于生成传统的王子和公主的故事。因为语料中,王子和公主的故事是最多的。如果你想生成一个多样化的故事,你可以设置一个较高的 Temperature,例如1.5。但是这可能会导致故事的逻辑性不强,或者出现不符合逻辑的情节。

Embedding

提前准备:训练好的词表

  • 文本 通过提前训练好的词表拆分为 Token(分词)
  • Token 通过提前训练好的词表转化为 Token ID
  • Token ID 通过提前训练好的词表转化为 Token Embedding(词嵌入)
  • 将位置信息 转化为 位置编码(位置编码)
  • 将 Token Embedding 和 位置编码 相加 得到 最终的输入向量

分词

训练大模型之前,一般先训练词嵌入向量库。训练前的词向量库会分配给每个token一个随机权重,如果不加训练也会输出乱码。训练后的词向量库每个token权重固定,相似词向量上体现一定的相似性。如果算力不够可以选择同时开源对应权重的【词嵌入向量库】

不同的模型用了不同的语料,因此维度大小、Token划分都是不一样的(为了避免维度爆炸,我们一般指定维度大小,通过频次聚类压缩,使得维度不会太大)。即不同模型编码器与解码器不一样,如果混用则可能输出乱码,即发现乱码时需要查看 词嵌入向量 模块与模型是否出自同一家厂商。

分词:首先需要使用某种分词器对输入文本进行分词处理,将其划分为一个个 token。这个过程中英文的单词有时需要下载一个分词表(通常为 tokenizer.json:分词器配置文件,用于文本的分词处理。),单词拆为本身和前缀与后缀,这个词表是人工标注的,这也是为什么英语是大模型首选语言。

词嵌入

构建词表:根据分词结果,统计所有 token 的出现频率,并依据设定的词表大小,选择保留的词汇。通常会保留高频率词汇,同时也会加入一些特殊符号(如垫零符 <PAD>、未知词 <UNK> 等)。通常为 tokenizer_config.json:分词器配置文件,定义了分词器的行为。分好后的词放在 vocab.json:词汇表文件,存储了模型所使用的词汇。

初始化嵌入矩阵:创建一个二维矩阵,行数等于词表大小,列数等于嵌入维度(即每个词向量的维度)。这个矩阵可以随机初始化,也可以通过预训练模型加载。

学习词嵌入:在训练神经网络的过程中,嵌入矩阵会被当作可训练参数,随着反向传播不断调整,以使词向量能够更好地表示词语之间的语义关系。这一步骤可以通过多种方法实现。

获取词嵌入向量:当模型训练完成后,对于任何一个给定的 token,都可以通过查找嵌入矩阵快速得到其对应的词嵌入向量。

词嵌入的过程是分割好的词从【嵌入矩阵】中获取自己向量的过程,假设【嵌入矩阵】维度为4(GPT2选用768维)如下:

位置编码

这一步会将位置信息融入到词嵌入向量中。

词嵌入层和位置编码层是两个独立的模块,它们在模型中是并行工作的。

位置向量的维度与词嵌入向量维度相同

词嵌入向量维度通常很高,这里示例的都有768维(通常取不低于300维,768可以看作3个256维拼接)。

我输入的词可能只有几个字,维度不够,如何转化为与词嵌入向量维度相同的向量?

现在主流的大模型使用的是RoPE (Rotary Positional Embedding, 旋转位置编码)。

假设你的词向量(Token Embedding)有 4 个维度(为了方便演示,用小数字),向量数值是:

x=[x1,x2,x3,x4]\vec{x} = [x_1, x_2, x_3, x_4]

RoPE 对 Q 和 K 向量进行旋转(不是独立的向量相加)。对每一对维度 (2i, 2i+1) 应用旋转矩阵(向量维度通常取2的整数倍或整数倍拼接,一定能两两配对上)。

旋转角度 θi=base2i/dmodelmθ_i = base^{-2i / d_model} * m(m 是位置)

例如,对于维度 2i 和 2i+1:

x2i=x2icos(θi)x2i+1sin(θi)x2i+1=x2isin(θi)+x2i+1cos(θi)\begin{align*} x'_{2i} &= x_{2i} \cos(\theta_i) - x_{2i+1} \sin(\theta_i) \\ x'_{2i+1} &= x_{2i} \sin(\theta_i) + x_{2i+1} \cos(\theta_i) \end{align*}

这直接修改 Q 和 K 向量,注入位置信息,而不增加额外参数。

最终输入向量

这个相加是向量相加(维度不变),不是向量拼接。

相加是一种节省算力的高效做法(比把两个向量首尾拼接省了一半的空间)

以下是一个简单示例:

position = [0.1,0.2,0.3,0.4,0.5,0.6]
embedding = [0.5,0.6,0.7,0.8,0.9,1.0]
final_input = position + embedding
print(final_input)
# 输出: [0.6, 0.8, 1.0, 1.2, 1.4, 1.6]

很多人觉得应该把两个向量拼起来,增加维度。这是错误的,可以把它想象成给原来的语义向量染色

原本的向量表示"我是苹果"。加了位置向量后,它并没有改变维度,而是变成了"我是在这个位置的苹果"。模型在后续的运算中,有能力把这两层信息(语义+位置)从这个混合数值里解构出来。

就像无线电信号:

  • 词向量 是载波(比如调频 FM 98.7)
  • 位置向量 是调制在上面的信号。

虽然它们在物理上混合成了一个波形,但接收端(大模型的后续层)有能力通过滤波器(权重矩阵)把它们区分开来。

计算空间占用

由于我们的 batch_size 设为 6464,即并行 6464 组数据,每个单词被打成最长 10241024 个token,每个token会生成一个 768768 维的向量(词嵌入后融入 RoPE)。

输入的部分参数量就是:64×1024×768=50,331,64864 \times 1024 \times 768 = 50,331,648

以上计算方式公式写作 (B,T,C)(B, T, C) 的形式,即 batch_size * token_length * Channel /embedding_dim

每个参数的精度是 bfloat16,即每个数字占用 1616 位(22 字节),

所以总空间占用为:50,331.648×2=100,663,296=96 MB50,331.648 \times 2 = 100,663,296 = 96 \text{ MB}

所以,最终随着位置编码和词嵌入的创建和合并,总空间会先升高,再降低,最终占用是 96 MB100 MB96 \text{ MB} \approx 100 \text{ MB}。为了后续计算方便,我们取整为 100 MB100 \text{ MB}

info

即每轮进入下一步的显存占用是:100 MB100 \text{ MB}。记住这个数字,后续会用到。

这个空间占用代表了64组数据,每组数据有1024个token,每个向量有768个参数,每个参数占用16位(2字节)。

代码

开源仓库地址:

https://github.com/jingyaogong/minimind-v

https://github.com/jingyaogong/minimind

李沐的动手学深度学习