从零搭建 LLaMA2 大模型
前言
在前面,我已经动手复现了 transformer 模型的整体架构,对其中的每一个模块都有了比较好的了解。但是我对现在主流的一些大模型的结构还不是很了解,之前搭建的 transformer 也只有模型结构,并没有和模型的具体任务进行关联。所以这次我根据 datawhale 的happy-llm 教程,来走一遍从模型搭建到 tokenizer 训练到模型预训练到 SFT 微调的全流程。
大模型从零开始全套流程
- 任务定义与架构选择
- 任务类型:
- 序列任务:文本生成(GPT 类)、翻译(Encoder-Decoder)、分类(BERT 类)。
- 非序列任务:图像(ViT)、多模态(CLIP)。
- 模型变体选择:
- 纯 Decoder(GPT):自回归生成。
- 纯 Encoder(BERT):双向上下文编码。
- Encoder-Decoder(T5):序列到序列任务。
- Tokenizer 训练
- 数据收集:
- 核心原则
- 覆盖性:需涵盖模型未来可能接触的所有文本领域(如多语言、专业术语、网络用语等)。
- 一致性:与预训练数据分布尽量一致(避免预训练时出现大量未登录词)。
- 核心原则
- 常见数据源
- 通用领域:
- Wikipedia(多语言版本)
- Common Crawl(网页爬取,需清洗)
- 图书语料(如 Project Gutenberg)
- 新闻语料(如 News Commentary)
- 专业领域:
- 医学论文(PubMed)、法律文本、代码(GitHub)
- 通用领域:
- 训练 Tokenizer:
- 子词算法:BPE(GPT)、WordPiece(BERT)、Unigram(SentencePiece)。
- 关键步骤:
1
2
3
4from tokenizers import Tokenizer, models, trainers
tokenizer = Tokenizer(models.BPE())
trainer = trainers.BpeTrainer(special_tokens=["[PAD]", "[UNK]"])
tokenizer.train(files=["data.txt"], trainer=trainer)
- 模型预训练
- 训练数据集
- 大规模无标注文本(如 Wikipedia、Common Crawl)。
- 数据预处理
- 用刚刚训练好的 Tokenizer 把原始文本转化为 id 序列。
- 开训!
- 最小化预测损失(如交叉熵)
- 有监督微调(SFT, Supervised Fine-Tuning)
- 适用场景
- 当预训练模型需适配具体任务时(如对话、分类)。
- 数据要求:高质量标注数据(规模远小于预训练数据)。
- 微调方法
- 全参数微调:
- 直接更新所有模型参数。
- 学习率通常比预训练更低(如 1e-5)。
- 参数高效微调(PEFT):
- LoRA:仅训练低秩适配矩阵。
- Adapter:插入小型网络模块。
- Prefix Tuning:学习任务特定的前缀 token。
- 全参数微调:
流程图
1 | [原始文本] → Tokenizer训练 → [Token化数据] → 预训练 → [预训练模型] → SFT → [任务模型] |
LLaMA2 大模型
LLaMA2 是一个 decoder-only 模型,具体框架是在 transformer 基础上演变的,我没有关注模型本身的细节,可以去参考 happy-llm 的具体实现
Tokenizer 训练
在自然语言处理 (NLP) 中,Tokenizer 是一种将文本分解为较小单位(称为 token)的工具。这些 token 可以是词、子词、字符,甚至是特定的符号。Tokenization 是 NLP 中的第一步,直接影响后续处理和分析的效果。不同类型的 tokenizer 适用于不同的应用场景,以下是几种常见的 tokenizer 及其特点。
1. Word-based Tokenizer
Word-based Tokenizer 是最简单和直观的一种分词方法。它将文本按空格和标点符号分割成单词。这种方法的优点在于其简单和直接,易于实现,且与人类对语言的直觉相符。然而,它也存在一些明显的缺点,如无法处理未登录词(OOV,out-of-vocabulary)和罕见词,对复合词(如 “New York”)或缩略词(如 “don’t”)的处理也不够精细。此外,Word-based Tokenizer 在处理不同语言时也会遇到挑战,因为一些语言(如中文、日文)没有显式的单词分隔符。
示例:
1 | Input: "Hello, world! There is Datawhale." |
在这个例子中,输入的句子被分割成一系列单词和标点符号,每个单词或标点符号都作为一个独立的 token。
2. Character-based Tokenizer
Character-based Tokenizer 将文本中的每个字符视为一个独立的 token。这种方法能非常精细地处理文本,适用于处理拼写错误、未登录词或新词。由于每个字符都是一个独立的 token,因此这种方法可以捕捉到非常细微的语言特征。这对于一些特定的应用场景,如生成式任务或需要处理大量未登录词的任务,特别有用。但是,这种方法也会导致 token 序列变得非常长,增加了模型的计算复杂度和训练时间。此外,字符级的分割可能会丢失一些词级别的语义信息,使得模型难以理解上下文。
示例:
1 | Input: "Hello" |
在这个例子中,单词 “Hello” 被分割成单个字符,每个字符作为一个独立的 token。这种方法能够处理任何语言和字符集,具有极大的灵活性。
3. Subword Tokenizer
Subword Tokenizer 介于词和字符之间,能够更好地平衡分词的细粒度和处理未登录词的能力。Subword Tokenizer 的关键思想是将文本分割成比单词更小的单位,但又比字符更大,这样既能处理未知词,又能保持一定的语义信息。常见的子词分词方法包括 BPE、WordPiece 和 Unigram。
(1)Byte Pair Encoding (BPE)
BPE 是一种基于统计方法,通过反复合并频率最高的字符或字符序列对来生成子词词典。这种方法的优点在于其简单和高效,能够有效地处理未知词和罕见词,同时保持较低的词典大小。BPE 的合并过程是自底向上的,逐步将频率最高的字符对合并成新的子词,直到达到预定的词典大小或不再有高频的字符对。
示例:
1 | Input: "lower" |
在这个例子中,单词 “lower” 被分割成子词 “low” 和 “er”,而 “newest” 被分割成 “new” 和 “est”。这种方法有效地处理了词干和词缀,保持了单词的基本语义结构。
(2)WordPiece
WordPiece 是另一种基于子词的分词方法,最初用于谷歌的 BERT 模型。与 BPE 类似,WordPiece 通过最大化子词序列的似然函数来生成词典,但在合并子词时更注重语言模型的优化。WordPiece 会优先选择能够最大化整体句子概率的子词,使得分词结果在语言模型中具有更高的概率。
示例:
1 | Input: "unhappiness" |
在这个例子中,单词 “unhappiness” 被分割成子词 “un” 和 “##happiness”,其中 “##” 表示这是一个后缀子词。通过这种方式,WordPiece 能够更好地处理复合词和派生词,保留更多的语义信息。
(3)Unigram
Unigram 分词方法基于概率模型,通过选择具有最高概率的子词来分割文本。Unigram 词典是通过训练语言模型生成的,可以处理多种语言和不同类型的文本。Unigram 模型会为每个子词分配一个概率,然后根据这些概率进行最优分割。
示例:
1 | Input: "unhappiness" |
在这个例子中,单词 “unhappiness” 被分割成子词 “un” 和 “happiness”,而 “newest” 被分割成 “new” 和 “est”。这种方法通过概率模型有效地处理了子词分割,使得分割结果更符合语言使用习惯。
每种 Tokenizer 方法都有其特定的应用场景和优缺点,选择适合的 Tokenizer 对于自然语言处理任务的成功至关重要。
4. 训练一个 Tokenizer
这里我们选择使用 BPE 算法来训练一个 Subword Tokenizer。BPE
是一种简单而有效的分词方法,能够处理未登录词和罕见词,同时保持较小的词典大小。我们将使用
Hugging Face 的 tokenizers
库来训练一个 BPE Tokenizer。
预训练模型
⚠️ 待完善
SFT
⚠️ 待完善