最近在看一篇Github上大佬的文章,从0开始训练llama3,觉得对于《从0开发大模型》有点帮助,于是翻译一下,发现其中很多内容当前系列文章的知识点相似。原文:https://github.com/naklecha/llama3-from-scratch其中meta-llama/Meta-Llama-3-8B文件地址:https://huggingface.co/meta-llama/Meta-Llama-3-8B/tree/main/original
1、Tokenizer
原始代码没有实现tokenizer,而是使用llama3的 tokenizer.model,实现代码如下:
这里用了字节对编码(BPE),和我们训练的tokenzier使用的方式一样。
2、读取模型文件
将模型文件下载到 Meta-Llama-3-8B 文件夹中,然后读取模型文件,代码如下:
其中输出的配置看:
- n_layers=32:表示该模型有32个Transformer层
- n_heads=32:表示每个Transformer层有32个注意力头
- vobac_size=128256:表示词汇表大小为128256
3、文本转换为token
使用 tiktoken(openai的库)作为 tokenizer,实现如下:
llama3-scratch
其中,128000是 <|begin_of_text|> 的token,还包括如下特殊token:
4、将token转换为embedding
将上面的 token 通过 embedding 层,[17X1] 转换为 [17X4096],即 17 个 embeding(每个token一个),长度为 4096。
llama3-scratch
代码如下:
5、使用RMS对embedding进行归一化
当前步骤不会改变形状,只是做标准化处理,需要用到norm_eps,代码如下:
llama3-scratch
6、构建Transformer第一层
6.1、归一化
获取模型的第一层权重(layer.0),进行归一化处理:
llama3-scratch
6.2、注意力机制
先加载Transformer的第一层注意力头:
llama3-scratch
- 从模型中加载wq,wk,wv,wo的权重,分别是[4096X4096],[1024X4096],[1024X4096],[4096X4096]
- 乍一看这很奇怪,因为理想情况下我们希望每个头部的每个 q、k、v 和 o 单独存在
- 代码的作者将它们捆绑在一起,因为它有助于并行化注意力头部乘法
6.3、展开query
将多个注意力头展开query,可以得到的形状为 [32X128X4096],其中32是llama3的头数,128是查询向量的大小,4096是token emebdding的大小,代码如下:
6.4、实现第一层的第一个head
访问第一层的query权重矩阵第一个head,权重矩阵的大小是[128x4096],打印一下:
6.5、query权重和token embedding相乘
token embeding的形状是[17x128],这是因为我们有17个标记,每个标记都有一个128长度的查询,将查询的权重和token embeding层相乘,代码如下:
llama3-scratch
6.6、位置编码
当前阶段我们的token都有一个query向量,但是仔细想想——单独的query向量根本不知道提示词的位置,比如:
query: "the answer to the ultimate question of life, the universe, and everything is "
在 prompt 中,使⽤了三次 "the" ,需要根据它们在prompt中的位置为每个 "the" token ⽣成不同的query向量(每个⻓度为 128),可以使⽤ RoPE (旋转位置编码)来实现这⼀点,具体RoPE实现可以参考之前的文章,也可以看看这个视频:https://www.youtube.com/watch?v=o29P0Kpobz0&t=530s。
llama3-scratch
从上面的步骤中,将query向量分成几对,每对使用旋转角度移位,现在将一个大小为[17X64X2]的向量,这个是prompt中每个标记分成64对的128个长度的query向量, 64对中的每一对都将旋转 m*theta,其中 m 是我们要旋转查询的 token 位置。
llama3-scratch
6.7、复数的点积来计算旋转向量
llama3-scratch
6.8、现在在每个token的query元素都有⼀个复数(⻆度变化向量)
我们可以将 query(分成两对的查询)转换为复数,然后使用点积根据位置旋转查询,代码如下:
6.9、获得旋转后的向量
可以通过再次将复数看作实数来返回成对的 query,代码如下:
旋转对现在已合并,现在有了⼀个新的query向量(旋转query向量),其shape为[17x128],其中17是token的数量,128是query向量的维度。
6.10、keys(操作query一样)
llama3-scratch
我太懒了,所以我不打算对key进行数学运算,唯一需要记住的是:
- key生成的key向量是128维
- key的权重数量只有查询的1/4,因为key的权重一次在4个头部之间共享,以减少所需的计算次数
- key也会选择添加位置信息,和查询一样
完成当前步骤,每个token的query和key都是[17X128]维度。
6.11、将query和key相乘
这样做会给我们一个分数,将每个token相互映射,这个分数描述了每个token的查询与每个token的键的关联程度,这是自我注意力。注意力得分矩阵 (qk_per_token) 的形状是 [17x17],其中17是提示中的token数量。
llama3-scratch
7、屏蔽QK分数
7.1、屏蔽预测的token
在llama3的训练过程中,未来的token qk分数被屏蔽,为什么?因为在训练过程中,只学习使⽤过去的token来预测token,因此,在推理过程中,将未来的token设置为零。
llama3-scratch
执行如下代码输出:
llama3-scratch
7.2、values(注意力机制最后部分)
图片
llama3-scratch
注意力分数用于确定每个token的使用多少矩阵,就像key一样,value的权重也是每4个注意力头之间共享,因此value矩阵形状是[8x128x4096]。
第一层,第一个头值权重矩阵如下:
7.3、value向量
llama3-scratch
我们现在使用value权重来获取每个token的注意力值,其大小为[17x128],其中17是提示中的token数量,128是每个token的值向量的大小。
7.4、最终的注意力输出
llama3-scratch
与每个token的值相乘后得到的注意向量形状为[17*128],输出如下:
8、多头注意力机制
llama3-scratch
现在获取了第一层和第一个头的注意力输出,只需要按照n_heads重复这个步骤,对第一层的每个头部执行与上面的单元格完全相同的数学运算。
llama3-scratch
现在又有了第一层的32个head的qkv_attention矩阵,接下来将所有的注意力分数合并为一个大小为[17x4096]的大矩阵,如下所示:
9、最后一步:计算权重矩阵
对于第0层注意力,最后要做的事情之一是将权重矩阵相乘:
9.1、简单的线性层,所以我们只需要matmul
llama3-scratch
现在embeding值变化了,添加到原始的token中嵌入:
9.2、归一化,然后通过embedding增量运⾏⼀个前馈神经⽹络
llama3-scratch
9.3、加载FFN权重并实现前馈⽹络
llama3-scratch
在llama3中,使用了SwiGLU前馈网络,这种网络架构非常适合在模型需要时添加非线性,如今,在llms中使用这种前馈网络架构相当常见。
10、在第⼀层之后,终于为每个token生成新的embeding
在我们完成之前只需要再经过31层(一个 for 循环),可以想象这个生成过的embeding具有关于第一层上提出的所有查询的信息。现在,对所有提出的问题每⼀层都会对query进⾏越来越复杂的编码,直到得到⼀个embedding,其中包含了需要的下⼀个token的所有信息。
for循环对每一层都执行相同的逻辑,最终获得final_embedding,如下所示:
11、有了final_embedding,这是模型对下一个标记做出的最佳猜测
embeding的形状和token embeding的形状相同,都是[17X4096],其中17是token的数量,4096是嵌入的维度。
12、最后,让我们将embeding解码为token
llama3-scratch
我们将使用输出解码器将最终的嵌入转换为token,如下所示:
最终我们希望这个问题the answer to the ultimate question of life, the universe, and everything is ,答案是42,现在执行如下代码:
预测下一个token的代码:
llama3-scratch
最终输出结果:
最终我整理了一下代码:https://github.com/linkxzhou/mylib/blob/master/llm/0-llama3-from-scratch.py
参考
(1)https://github.com/naklecha/llama3-from-scratch