周末看到Robbin推荐了Baby LLaMA2项目,OpenAI的创始人之一Andrej Karpathy,用一个周末的时间,搞出来一个微型的Baby LLaMA 2,核心的C语言代码就490行:llama.c。一个适合新手入门的大模型体验项目。正好周末无视可以学习消遣一下, 废话不多说了从readme开始入手。
First Run
// 下载作者训练好的model
wget https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.bin
// 编译代码
make run
// 运行模型
./run stories15M.bin
得到结果如下:
然后我们得到了一个自动生成的小故事,看一看到生成速率大约每秒钟50多个字。当然作者提供了另外一些参数更大的模型可以尝试。
作者提到这个模型可以通过交互的方式,以生成更长更连贯的故事。还可以通过前缀提示,也就是你写个开头,模型帮你补充的后面的内容.我们可以尝试
./run stories42M.bin 1.0 256 "My little pig feeds me."
有意思的是 起初我的参数是 "My little pig feeds me" 模型似乎感知到不合理的地方,强行将me 改为meadow, 得到了 “My little pig feeds meadow to the fairheart.” 这样的开头,所以可以猜测模型可以对入参进行纠正.后面我们将提示参数改为"My little pig feeds me." 强制结尾,导致他只能硬着头皮去写了
关于模型的介绍
由于神经网络架构是相同的,所以也可以用 Meta 发布的 Llama 2 模型。但是由于商业原因,作者无法提供下载这个模型,所以你需要自己去获取(https://github.com/facebookresearch/llama),获取之后,通过程序将模型转换为当前项目支持的格式获取。
python export_meta_llama_bin.py path/to/llama/model/7B llama2_7b.bin
这样你就可以运行更大的模型用来编造故事了?????
对于大模型执行的性能作者提供了一些经验,总结一句就是,在更大的模型或者某些更大规模的数据集上训练,普通的pc基本上很难跑的动,当然这些不是讨论的重点,我们先忽略过去把。
难能可贵的是作者为了照顾小白,提供了从头开始训练模型的示例,我在 TinyStories 上训练了一系列小型模型。训练环境中(4X A100 40GB GPU)只需要几个小时就可以训练完成。110M 模型需要大约 24 小时的时间。
目前作者提供了 几种不同参数的已经训练好的模型
可以看到 其中110M 的model规模已经相当于GPT-1,或者GPT-2中最小的规模,只是上下文长度仅为1024。与 GPT-1/2 架构相比,Llama有一些显著的变化,包括使用 RoPE 相对位置嵌入而不是绝对/学习位置嵌入、MLP 中使用了更加高级的 SwiGLU 非线性、使用 RMSNorm 而不是 LayerNorm、所有线性层的偏置都为 False ,(没错,上面的话是直接翻译过来的,具体模型原理不太清楚,根据这些词大概也许猜测是做了一些性能优化,具体还需要参看相关论文)。
训练模型
现在激动人心的时刻到了,让我们从头开始训练一个 Baby Llama2 模型,首先下载数据集,可以不通过网址下载,贴心的作者直接写好了程序自动给我们下载,这就是工匠精神呀。直接喂饭到嘴边的感动。
python tinystories.py download
经过漫长的等待。数据集下载完毕得到了类似data00.json
data01.json
……dataxx.json
的文件,
暂时忽略里面的bin后缀。我们得到了50个训练文本,每个大约200M左右,我们打开一个训练文件,先看看是什么。
可以看到里面是标注好的数据,包含 提示, 正文,关键词,摘要。来源,不知道是人工标注还是机器生成的,不过这个暂时不太重要。
得到训练数据之后需要对模型中单词进行token,以更方便的进行训练
python tinystories.py pretokenize
实际上述工作就是对词语进行token化,每个词分配一个id。包含标点符号。我们可以直接参看代码
def process_shard(shard):
with open(shard, "r") as f:
data = json.load(f) # 打开json文件
all_tokens = []
for example in tqdm(data): # 迭代每一份数据
text = example["story"] # 只处理政五年字段
text = text.strip() # 去掉开始和结束的空格
tokens = enc.encode(text, bos=True, eos=False) # 编码文本,将文本转换为tonken
all_tokens.extend(tokens)
all_tokens = np.array(all_tokens, dtype=np.uint16) # 将所有的token转换为uint16类型的nparray
tokenized_filename = shard.replace(".json", ".bin")
with open(tokenized_filename, "wb") as f:
f.write(all_tokens.tobytes())
print(f"Saved {tokenized_filename}")
代码比较生硬我们可以通过打印查看一下这个过程的变化。
Lily and Ben are friends. They like to play ……
.[1, 365, 2354, 322, 4111, 526, 7875, 29889, 2688, 763, 304, 1708, 297,……]
每一个故事的开头都是以1 来标识。
Lily定义为tonken 365, 以此类推,ps 标点符号和某一句的开头也会分配tonken
这样相当于准好好训练数据集了。聪明的你一定想到我们可以自己定义一份中文数据集,来训练自己的模型了哈。
下一步开始训练
python train.py
很遗憾训练需要显卡,貌似不支持cpu,而且我估计cpu训练起来会非常耗时。
那就,先这样把。回头我们可以看看他的核心C代码进行分析。