目录
当我们谈论预训练和监督微调(SFT)之间的差异时,目标、使用的数据集和所需的 GPU 数量都是不同的。但是,如果我们要从深度学习训练的本质来解释差异,那就是:
预训练是随机初始化模型参数,构建模型,然后在大量未标注数据上进行训练,学习语料库的共性特征;而微调是加载预训练模型中的参数,保留预训练中学习到的共性特征,并在少量高质量标注数据上进行训练,以提升模型在特定任务上的能力和性能。
上面提到的参数包括:权重、偏差、Word Embeddings、Positional Encoding、注意力机制参数等。
更详细的解释
预训练旨在使用大规模无监督数据集(例如文本语料库)学习语言的基本结构和语义特征。预训练通常涉及以下步骤:
- 权重的随机初始化:模型的参数,例如权重和偏差,在预训练开始时随机初始化。
- 大规模数据集:使用大量无监督数据进行训练。
- 学习一般特征:模型通过优化损失函数(例如语言模型的交叉熵损失)来学习语言的一般特征。
预训练的要点
- 随机初始化:预训练开始时,所有模型参数(权重、偏差等)都是随机的。
- 大规模数据:使用大规模无监督数据集进行训练。
- 总体特征:模型学习语言的基本结构和语义特征,为后续任务提供良好的起点。
微调旨在使用特定于任务的数据集优化模型在特定任务上的性能。微调通常涉及以下步骤:
- 加载预先训练的权重:模型的权重和偏差从预先训练的模型中加载。
- 特定任务数据:使用特定于任务的数据集进行训练。
- 优化任务性能:模型通过优化损失函数来调整其参数,以提高特定任务的性能。
微调的重点
- 加载预训练权重:从预训练模型中加载模型的参数,保留预训练期间学习到的一般特征。
- 特定任务数据:使用特定于任务的数据集进行训练。
- 任务优化:进一步调整模型的参数,以优化在特定任务上的性能。
概括
- 训练效率:预训练通常需要大量的计算资源和时间,因为它涉及在大规模数据集上训练所有模型参数。微调相对高效,因为它建立在预训练模型的基础上,只需要在特定任务的数据上进一步优化。
- 模型性能:预训练模型已经学习了通用语言特征,因此可以通过微调更快地收敛并在特定任务上表现更好。从随机初始化训练特定任务的模型通常需要更多的数据和时间,其性能可能不如预训练 + 微调方法。
- 应用场景:预训练模型可以作为通用的基础模型,适用于各种下游任务。通过微调可以快速适应不同的任务需求,而无需从头开始训练模型。
预训练代码演示
以 GPT-2 为例
https://huggingface.co/docs/transformers/v4.44.0/en/model_doc/gpt2#transformers.GPT2LMHeadModel
为了预训练 GPT-2,我们需要使用类GPT2LMHeadModel和GPT2Config:
<span style="background-color:#2b2b2b"><span style="color:#f8f8f2"><code class="language-applescript">config <span style="color:#00e0e0">=</span> GPT2Config<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span>
model <span style="color:#00e0e0">=</span> GPT2LMHeadModel<span style="color:#fefefe">(</span>config<span style="color:#fefefe">)</span>
tokenizer <span style="color:#00e0e0">=</span> GPT2Tokenizer.from_pretrained<span style="color:#fefefe">(</span><span style="color:#abe338">"gpt2"</span><span style="color:#fefefe">)</span>
tokenizer.pad_token <span style="color:#00e0e0">=</span> tokenizer.eos_token
dataset <span style="color:#00e0e0">=</span> load_dataset<span style="color:#fefefe">(</span><span style="color:#abe338">"wikitext"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"wikitext-2-raw-v1"</span><span style="color:#fefefe">)</span>
def tokenize_function<span style="color:#fefefe">(</span>examples<span style="color:#fefefe">)</span><span style="color:#fefefe">:</span>
<span style="color:#00e0e0">return</span> tokenizer<span style="color:#fefefe">(</span>examples[<span style="color:#abe338">"text"</span>]<span style="color:#fefefe">,</span> truncation<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">,</span> padding<span style="color:#00e0e0">=</span><span style="color:#abe338">"max_length"</span><span style="color:#fefefe">,</span> max_length<span style="color:#00e0e0">=</span><span style="color:#00e0e0">512</span><span style="color:#fefefe">,</span> return_special_tokens_mask<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">)</span>
tokenized_datasets <span style="color:#00e0e0">=</span> dataset.map<span style="color:#fefefe">(</span>tokenize_function<span style="color:#fefefe">,</span> batched<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">,</span> remove_columns<span style="color:#00e0e0">=</span>[<span style="color:#abe338">"text"</span>]<span style="color:#fefefe">)</span>
print<span style="color:#fefefe">(</span><span style="color:#abe338">"Train dataset size:"</span><span style="color:#fefefe">,</span> len<span style="color:#fefefe">(</span>tokenized_datasets[<span style="color:#abe338">"train"</span>]<span style="color:#fefefe">)</span><span style="color:#fefefe">)</span>
print<span style="color:#fefefe">(</span><span style="color:#abe338">"Validation dataset size:"</span><span style="color:#fefefe">,</span> len<span style="color:#fefefe">(</span>tokenized_datasets[<span style="color:#abe338">"validation"</span>]<span style="color:#fefefe">)</span><span style="color:#fefefe">)</span>
data_collator <span style="color:#00e0e0">=</span> DataCollatorForLanguageModeling<span style="color:#fefefe">(</span>tokenizer<span style="color:#00e0e0">=</span>tokenizer<span style="color:#fefefe">,</span> mlm<span style="color:#00e0e0">=</span>False<span style="color:#fefefe">)</span>
training_args <span style="color:#00e0e0">=</span> TrainingArguments<span style="color:#fefefe">(</span>
output_dir<span style="color:#00e0e0">=</span><span style="color:#abe338">"./results"</span><span style="color:#fefefe">,</span>
overwrite_output_dir<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">,</span>
num_train_epochs<span style="color:#00e0e0">=</span><span style="color:#00e0e0">5</span><span style="color:#fefefe">,</span>
per_device_train_batch_size<span style="color:#00e0e0">=</span><span style="color:#00e0e0">64</span><span style="color:#fefefe">,</span>
save_steps<span style="color:#00e0e0">=</span>10_000<span style="color:#fefefe">,</span>
save_total_limit<span style="color:#00e0e0">=</span><span style="color:#00e0e0">2</span><span style="color:#fefefe">,</span>
remove_unused_columns<span style="color:#00e0e0">=</span>False<span style="color:#fefefe">,</span>
report_to<span style="color:#00e0e0">=</span>[]<span style="color:#fefefe">,</span>
learning_rate<span style="color:#00e0e0">=</span><span style="color:#00e0e0">5e-4</span>
<span style="color:#fefefe">)</span>
trainer <span style="color:#00e0e0">=</span> Trainer<span style="color:#fefefe">(</span>
model<span style="color:#00e0e0">=</span>model<span style="color:#fefefe">,</span>
args<span style="color:#00e0e0">=</span>training_args<span style="color:#fefefe">,</span>
data_collator<span style="color:#00e0e0">=</span>data_collator<span style="color:#fefefe">,</span>
train_dataset<span style="color:#00e0e0">=</span>tokenized_datasets[<span style="color:#abe338">"train"</span>]<span style="color:#fefefe">,</span>
eval_dataset<span style="color:#00e0e0">=</span>tokenized_datasets[<span style="color:#abe338">"validation"</span>]
<span style="color:#fefefe">)</span>
<span style="color:#00e0e0">if</span> torch.cuda.is_available<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span><span style="color:#fefefe">:</span>
model.cuda<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span>
trainer.train<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span> </code></span></span>
由于模型很小,因此可以使用单个 H100 GPU 进行预训练。
训练结果如下:
步 | 训练损失 |
---|---|
500 | 6.505700 |
1000 | 5.657100 |
1500 | 5.269900 |
2000 | 4.972000 |
2500 | 4.725000 |
训练好的模型可以用于推理验证。
<span style="background-color:#2b2b2b"><span style="color:#f8f8f2"><code class="language-applescript">model <span style="color:#00e0e0">=</span> GPT2LMHeadModel.from_pretrained<span style="color:#fefefe">(</span><span style="color:#abe338">"./results/checkpoint-2870"</span><span style="color:#fefefe">)</span>
tokenizer <span style="color:#00e0e0">=</span> GPT2Tokenizer.from_pretrained<span style="color:#fefefe">(</span><span style="color:#abe338">"gpt2"</span><span style="color:#fefefe">)</span>
tokenizer.pad_token <span style="color:#00e0e0">=</span> tokenizer.eos_token
device <span style="color:#00e0e0">=</span> torch.device<span style="color:#fefefe">(</span><span style="color:#abe338">"cuda"</span> <span style="color:#00e0e0">if</span> torch.cuda.is_available<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span> <span style="color:#00e0e0">else</span> <span style="color:#abe338">"cpu"</span><span style="color:#fefefe">)</span>
model.<span style="color:#00e0e0">to</span><span style="color:#fefefe">(</span>device<span style="color:#fefefe">)</span>
model.eval<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span>
input_text <span style="color:#00e0e0">=</span> <span style="color:#abe338">"Once upon a time"</span>
inputs <span style="color:#00e0e0">=</span> tokenizer<span style="color:#fefefe">(</span>input_text<span style="color:#fefefe">,</span> return_tensors<span style="color:#00e0e0">=</span><span style="color:#abe338">"pt"</span><span style="color:#fefefe">,</span> padding<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">)</span>.<span style="color:#00e0e0">to</span><span style="color:#fefefe">(</span>device<span style="color:#fefefe">)</span>
<span style="color:#00e0e0">with</span> torch.no_grad<span style="color:#fefefe">(</span><span style="color:#fefefe">)</span><span style="color:#fefefe">:</span>
outputs <span style="color:#00e0e0">=</span> model.generate<span style="color:#fefefe">(</span>
inputs.input_ids<span style="color:#fefefe">,</span>
attention_mask<span style="color:#00e0e0">=</span>inputs.attention_mask<span style="color:#fefefe">,</span>
max_length<span style="color:#00e0e0">=</span><span style="color:#00e0e0">100</span><span style="color:#fefefe">,</span>
num_return_sequences<span style="color:#00e0e0">=</span><span style="color:#00e0e0">1</span><span style="color:#fefefe">,</span>
no_repeat_ngram_size<span style="color:#00e0e0">=</span><span style="color:#00e0e0">2</span><span style="color:#fefefe">,</span>
early_stopping<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">,</span>
temperature<span style="color:#00e0e0">=</span><span style="color:#00e0e0">0.7</span><span style="color:#fefefe">,</span>
top_p<span style="color:#00e0e0">=</span><span style="color:#00e0e0">0.9</span><span style="color:#fefefe">,</span>
do_sample<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">,</span>
pad_token_id<span style="color:#00e0e0">=</span>tokenizer.eos_token_id
<span style="color:#fefefe">)</span>
generated_text <span style="color:#00e0e0">=</span> tokenizer.decode<span style="color:#fefefe">(</span>outputs[<span style="color:#00e0e0">0</span>]<span style="color:#fefefe">,</span> skip_special_tokens<span style="color:#00e0e0">=</span>True<span style="color:#fefefe">)</span>
print<span style="color:#fefefe">(</span>generated_text<span style="color:#fefefe">)</span> </code></span></span>
推理结果如下:
大地震发生后,当地社区成立了新政府,新政府中以“最突出”的军政府开始运作。
微调代码演示
当我们对模型进行微调时,通常指的是监督微调(SFT)。SFT 可分为参数高效微调(PEFT)和完全微调。在 PEFT 实现中,LoRA、QLoRA 和 GA-LoRA 等方法非常流行。
首先我们来看一下如何加载模型进行完全微调。我们使用AutoModelForCausalLM.from_pretrained类来检索预训练模型的参数。
<span style="background-color:#2b2b2b"><span style="color:#f8f8f2"><code class="language-applescript"> model <span style="color:#00e0e0">=</span> AutoModelForCausalLM.from_pretrained<span style="color:#fefefe">(</span>
model_name<span style="color:#fefefe">,</span> attn_implementation<span style="color:#00e0e0">=</span>attn_implementation<span style="color:#fefefe">,</span> device_map<span style="color:#00e0e0">=</span><span style="color:#fefefe">{</span><span style="color:#abe338">""</span><span style="color:#fefefe">:</span> <span style="color:#00e0e0">0</span><span style="color:#fefefe">}</span>
<span style="color:#fefefe">)</span>
model.gradient_checkpointing_enable<span style="color:#fefefe">(</span>gradient_checkpointing_kwargs<span style="color:#00e0e0">=</span><span style="color:#fefefe">{</span>'use_reentrant'<span style="color:#fefefe">:</span>True<span style="color:#fefefe">}</span><span style="color:#fefefe">)</span></code></span></span>
有关完整的微调代码,请参阅存储库:
https://github.com/davidsajare/david-share/tree/master/Deep-Learning/SmolLM-Full-Fine-Tuning
接下来我们看一下fine-tuning、LoRA、QLoRA在代码实现上的区别。在加载模型和训练参数方面,Full Fine-Tuning、LoRA、QLoRA有以下几点区别:
加载模型的差异
- 全面微调
- 直接加载完整模型进行训练。
- 使用AutoModelForCausalLM.from_pretrained加载模型。
- 洛拉
- 加载模型,然后使用 LoRA 配置进行参数有效的微调。
- 使用peft库中的LoraConfig来配置 LoRA 参数。
- 目标模块通常是特定的投影层,例如k_proj 、q_proj等。
- 量子LoRA
- 它基于LoRA,结合量化技术(例如4位量化)以减少内存使用量。
- 使用BitsAndBytesConfig进行量化配置。
- 调用prepare_model_for_kbit_training准备模型。
训练参数的差异
- 全面微调
- 训练所有模型参数。
- 通常需要更多的内存和计算资源。
- 使用标准优化器,如adamw_torch 。
- 洛拉
- 仅训练LoRA插入的低秩矩阵,保持其他参数不变。
- 训练速度更快,内存使用更少。
- 使用像paged_adamw_8bit这样的优化器。
- 量子LoRA
- 结合LoRA和量化技术,进一步减少内存使用量。
- 适用于在资源受限的环境中微调大型模型。
- 还使用paged_adamw_8bit优化器。
需要注意的是,在进行LoRA或QLoRA微调时,我们可以指定需要训练的模块,例如:
<span style="background-color:#2b2b2b"><span style="color:#f8f8f2"><code class="language-applescript">model <span style="color:#00e0e0">=</span> FastLanguageModel.get_peft_model<span style="color:#fefefe">(</span>
model<span style="color:#fefefe">,</span>
r <span style="color:#00e0e0">=</span> <span style="color:#00e0e0">128</span><span style="color:#fefefe">,</span>
target_modules <span style="color:#00e0e0">=</span> [<span style="color:#abe338">"q_proj"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"k_proj"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"v_proj"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"o_proj"</span><span style="color:#fefefe">,</span>
<span style="color:#abe338">"gate_proj"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"up_proj"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"down_proj"</span><span style="color:#fefefe">,</span>
<span style="color:#abe338">"embed_tokens"</span><span style="color:#fefefe">,</span> <span style="color:#abe338">"lm_head"</span><span style="color:#fefefe">,</span>]<span style="color:#fefefe">,</span> <span style="color:#d4d0ab"># Add for continual pretraining</span>
lora_alpha <span style="color:#00e0e0">=</span> <span style="color:#00e0e0">32</span><span style="color:#fefefe">,</span>
lora_dropout <span style="color:#00e0e0">=</span> <span style="color:#00e0e0">0</span><span style="color:#fefefe">,</span> <span style="color:#d4d0ab"># Supports any, but = 0 is optimized</span>
bias <span style="color:#00e0e0">=</span> <span style="color:#abe338">"none"</span><span style="color:#fefefe">,</span> <span style="color:#d4d0ab"># Supports any, but = "none" is optimized</span>
use_gradient_checkpointing <span style="color:#00e0e0">=</span> <span style="color:#abe338">"unsloth"</span><span style="color:#fefefe">,</span> <span style="color:#d4d0ab"># True or "unsloth" for very long context</span>
random_state <span style="color:#00e0e0">=</span> <span style="color:#00e0e0">3407</span><span style="color:#fefefe">,</span>
use_rslora <span style="color:#00e0e0">=</span> True<span style="color:#fefefe">,</span>
<span style="color:#fefefe">)</span></code></span></span>
详细信息请参考:
https://github.com/davidsajare/david-share/tree/master/Deep-Learning/Continue-Pre-training
分布式训练实现
毫无疑问,大型语言模型的预训练需要多节点、多 GPU 的配置,这就需要分布式训练。目前底层的分布式预训练可以通过调用 NCCL 来实现,更上层的工具有 Megatron、DeepSpeed、HF 的加速库(目前支持 FSDP)等,这些工具可以有效实现 DP/PP/TP。
关于使用 Megatron 结合 DeepSpeed 进行预训练的详细信息,参考:
DeepSpeed
有关使用 DeepSpeed 实现 SFT 的示例,参考:
目前一些开源的微调工具如Axolotl也可以直接与DeepSpeed对接,示例参考:
https://github.com/davidsajare/david-share/tree/master/Deep-Learning/Fine-tuning-with-Axolotl
加速
当使用 FSDP 与加速时,可以结合其他并行策略来实现更高效的训练。
- 数据并行(DP)
- FSDP本身是一种数据并行策略,通过分片模型参数实现。
- 流水线并行性 (PP)
- 模型可以分为多个阶段,每个阶段在不同的设备上运行。这需要手动对模型进行划分并管理数据流。
- 张量并行(TP)
- 单层的计算分布在多个设备上。这需要修改模型的计算图。
结合这些策略通常需要对模型和训练脚本进行大量的定制和调整。accelerate提供了一些工具来简化这些过程,但具体实现可能需要结合其他 PyTorch 库(如torch.distributed )和自定义代码。
有关带有加速的FSDP 的示例,请参阅:
https://github.com/davidsajare/david-share/tree/master/Deep-Learning/Llama-3.1-70B-FSDP-Fine-Tuning