许多人可能感到在新时代中有些不适应。面对新颖算法和高实验条件,似乎难以适应这个新世界。然而,有一种方法可以在“一无所有”的状态下开始探索。更新思维,勇敢成为“调包侠”,利用Hugging Face的Transformers等现代包,让人能够轻松上手。
相信很多人和我一样都是从“古典深度学习”时代一路走来的,面对当今“LLM才是AI“的时代,有着很多的不适应症状。看着那么多的新论文里的算法,想从头开始撸一遍发现要学习一大堆有的没的的东西,然后 paper 里提到的实验条件还吓人的高,似乎这个喧嚣的新世界和自己有点遥不可及。
那么,有没有什么办法能在“一无所有”的状态下做点啥呢?
首先,进入新时代了,要更新好自己的思想,放心大胆的当“调包侠”。而现在的确有很多很科学的包!
其中以 hugging face 的 transformers 为集大成者。实际上,从自娱自乐的 toy example,到一系列还挺有影响力的项目比如 Vicuna,LLAVA,翻开代码库,都能看到那行金光闪闪的
甚至,翻遍整个repo,都找不到“网络结构”写在哪里,只有一行
在等着你。
而在 2024 年了,这些包的安装也没那么“阴间”,直接 pip install 回来的 transformers,accelerate,就是亲测能用的(当然,假设你已经把 pytorch、CUDA 的安装和修bug搞定了)。反正我在玩的时候真就是装了就能用,没啥玄学。
对于模型训练 ,官方文档并没有一个“最小集”的样本,不过对着文档琢磨一下还是很容易写出来的:
嗯,是的,一共就16行,配好两个对象,然后 Trainer.train() 就成了…… 我第一次用的时候没配 save_steps 导致跑完了不知道模型存哪了,查了一下文档才搞明白。
其中,dataset对象 就是 torch.utils.data.Dataset,要实现 __ len __ 和 __ getitem __ 的接口,这个自己搞搞就好了。
而要使用一个训练好的模型,直接
都不用自己去手工维护模型的超参数列表,这个库在 checkpoint 文件夹里已经自己按约定存好了。
然后肯定有人会提出质疑了,这么直接搞真的严肃么?
不过,让我们回想一下为啥 CV 类的模型的训练代码都那么“复杂”,然后就能发现现在的这种搞法的“科学”之处了:
- 数据集。CV 类模型往往要使用大量的压缩(jpg/h264)后的数据并进行大量的增广来进行训练,训练过程中总的 IO 吞吐是恐怖的。于是乎几乎人人都要会写分布式 data provider 框架,甚至还要自己搞专门的数据提供的加速办法(例如,多个实验共享data provider)。然而,在 NLP 领域,就算是大如 GPT level 的模型,也就几 T tokens 就能训明白了,弄个 SSD 本地存一下挺香的。。。最多是 tokenizer 之类的预处理部分折腾一下。因为每 token 的训练 flo 的巨大区别,在这个地方花费的精力是可以极大的节省的。
- 调参。CNN 类的模型,好多地方调一调参还是挺有用的。而来到了 2024 年的 transformers,模型一方面很“简单”,大量的 transformer block 的堆叠,想乱改涨点还不好涨;另一方面又很“复杂 ”,面对着跑一次要花很多 $ 的基础模型调参实验(比如你想把gelu换回relu?),穷人也就不用去想着折腾了。开源世界调出来的那一组“默认参数”,其实还挺 work 的,于是乎就成了一个“非必要别乱调”的格局。
- 表示。搞 Vision 的模型“如何表示输入输出”是一门大学问,改一点点影响巨大。而搞序列建模的时候……输入输出就是 tokens 的序列,torch.LongTensor,没了。。管你的 tokens 里是画了一棵树,建了一张图,还是存了一大段 json ,在 NN 这边看来,都(至少在观念上)是一样一样的。
- 部署。“古典”AI模型的一大主题是如何在端上跑,所以就产生了一大堆的模型打包、静态化、计算图转化等等的 concern。不过在“新时代”,还远没有卷到这个地步,能 python 跑就还算可以了,这也就减少了大量为“训推一体”付出的精力。
transformers 里的 Trainer 集成了一些很科学的默认行为,例如定期存 checkpoint 放到实验名的文件夹下面,把各种曲线信息同步到 tensorboard、clearml、wandb 等监控软件,啥参数都不传也是可以接受的选择。
而如果你就是要调参,在 TrainingArguments、Trainer、GPT2Config 等地方一共有 138 个可以传的参数,以及大量通过调方法来填的参数,能满足不少的需要。比如,可以传一个 fp16 = True 来“一键”加速训练,而它背后是 apex.amp 这种库在支撑。
所以来说,把“靠谱”的库“集成”在一起,暴露出一组带有合理默认参数的接口,这个方法论在 2024 年来看的确还是可行的。当层层调用的这些库不“炸”的时候,使用体验相当的丝滑。
然而,如果“炸”了,或者想搞一些比较深度的 hack (比如定制某个算子在训练的时候触发一个神奇的行为),就会发现,在一行看似无辜的网络定义之上,还摞着层层叠叠的 加速库、优化库、分布式库、混合精度库等等,想去“一层层debug”就成了一件恐怖的事情。
好不容易(?)学会了序列建模这个技能(又称:transformers 库的安装与使用),不整点啥活似乎没意思。
于是,我用我的笔记本电脑(带3060显卡)训练了一个 GPT2-small 级别的模型来做 C++ 代码的补全。
我从 github 上找了一些经典的 c/c++ 的 repo(如,linux kernel,gcc,cpython,等),收集了 1G 多的 .zip,从里面找出所有的 c类源文件,形成了一个 2.2 GB 的训练集。
不做tokenization,23万步,长度 512,批大小 4,训练大概需要不到一天。
然后搞了一个贪心的补全推荐的方法,试了试,好像也不是完全不能用:
视频发不了...
训练出来的语言模型压缩率大概是 0.7 bits / byte,比 xz -9 还是要强一点的。
考虑到实现出这样的效果,从搜索库名字到实现出来只需要一天,这还是挺惊人的了。
当然,一旦想从这个 baseline 开始提升效果,那么 language model 里各种考量的的“洪流”就要来了:
- 用 tokenizer 进行文本的预压缩,提升 token 的效率(window size是一种昂贵的资源),同时让网络不用去学习各种“显然”的东西。各种 tokenizer 以及训练加速的方法就可以选一选了
- 清洗数据,减少类似训练数据被 linux kernel 里的 #define 淹没带来的副作用,提升“高质量”数据的占比
- 更大的模型!找更多更好的卡!另外还要搞预训练
- 最优的批大小、并行数、learning rate调整,对着 loss 曲线“围炉炼丹”
- 把 suffix 预测改为 infix 预测,加入文件名、函数名等的各种 hint
- 考虑用户反馈数据的利用,考虑加入用户问答,考虑针对用户的 finetune
- 等等等等……
这就是一个很深很深的坑了,而 copilot 之类就是“登峰造极”之后的产物。想往这方面卷,就会一步步走入“去哪融资,去哪买卡,国产半导体何时崛起”的无尽焦虑中。
不过好的一面在于,如果我们不把自己当做一个 “训 LLM” 的人,而是当一个 “用 transfomers 库进行序列建模” 的,就会发现事情也没那么坏。虽然几十M的“小”(对 vision 来说其实不小了)模型并不会表现出“大”模型的一些独特能力,但是也已经可以做一些很有意义的事情了(我还真没试过用 CNN 硬怼上面的这个 demo 是什么效果……)。
刨开训练 transformer 类模型的一些新的技巧,当前这个时代其实最关键的问题就变成了:手头有啥好的问题,可以表达成一个序列建模吗?
如果有,或者原来的某些“老”问题可以这么表示,那不妨用 transformer 们来试一试,也许就有全新的可能。
新的时代,就应该去拥抱新的方法。
最后,附代码补全demo的全套代码:训练
train.py
演示
demo.py