Skip to content

基于 LoRA 的 Llama2 二次预训练

整理自:AI GC面试宝典 作者:宁静致远 日期:2024年01月27日


目录

  • 一、为什么需要对 Llama2 做基于 LoRA 的二次预训练?
  • 二、基于 LoRA 的 Llama2 二次预训练的目标是什么?
  • 三、基于 LoRA 的 Llama2 二次预训练的思想是什么?
  • 四、基于 LoRA 的 Llama2 二次预训练语料构建思路
  • 五、如何基于 LoRA 的 Llama2 二次预训练?
    • 5.1 二次预训练参数介绍
    • 5.2 二次预训练实战
  • 六、如何基于 LoRA 的 Llama2 微调?
    • 6.1 训练数据介绍
    • 6.2 微调参数介绍
    • 6.3 微调实战
  • 七、如何使用基于 LoRA 的 Llama2 做推理?
  • 致谢

一、为什么需要对 Llama2 做基于 LoRA 的二次预训练?

Llama 2 是 Meta 公司开源的大语言模型,默认主要支持英文。通过加入中文训练语料进行二次预训练,可以增强模型的中文理解和生成能力。

📝通俗解释:Llama 2 就像一个只懂英文的外国人,通过二次预训练让它学会中文,这样它就能用中文回答问题了。


二、基于 LoRA 的 Llama2 二次预训练的目标是什么?

在保持预训练模型权重不变的情况下,通过添加额外的网络层并仅训练这些新增的网络层参数,实现大模型的高效微调(PEFT,Parameter-Efficient Fine-Tuning)。

📝通俗解释:就像给汽车加装一个智能驾驶辅助系统,不需要改造汽车发动机,只需要训练辅助系统就能让汽车实现自动驾驶。LoRA 就是这个"辅助系统"。


三、基于 LoRA 的 Llama2 二次预训练的思想是什么?

核心思想:基于对模型本征维度(Intrinsic Dimension)的理解。

  • 本征维度:指模型中真正有用的、能够影响模型输出的参数数量。
  • Aghajanyan 研究发现:预训练模型的内在维度实际上非常小,即只有一小部分参数对模型输出有显著影响。
  • 存在一个极低维度的参数空间,微调它和全参数空间微调能起到相同的效果。

LoRA 原理

  • 假设模型在任务适配过程中权重的改变量是低秩(Low Rank)
  • 公式:$W = W_0 + \Delta W$,其中 $\Delta W = BA$
  • 参数更新范围:只训练新增的网络层参数(LoRA 矩阵 A 和 B)

📝通俗解释:想象一个有1000个按钮的控制面板,但真正影响输出的可能只有10个旋钮。LoRA 就是找到这10个"关键旋钮"并只调整它们,而不是去拨动所有1000个按钮。这样既省力又有效。


四、基于 LoRA 的 Llama2 二次预训练语料构建思路?

4.1 预训练数据集下载

本项目的二次预训练语料来自中文书籍,项目地址:

bash
git clone https://github.com/shjwudp/shu.git

4.2 数据集格式介绍

数据集为 .txt 格式的文本文件,包含以下中文典籍:

书名Book Title作者出版年代版权过期
尚书The Shoo King-公元前1949
诗经The Canon of Poetry未知公元前7世纪1949
红楼梦The Dream of the Red Chamber曹雪芹、高鹗18世纪1949
曾国藩家书Zeng Guo Fan Jia Shu曾国藩19世纪1949
隋唐演义Heroes in Sui and Tang Dynasties褚人獲16951949
韩非子Hanfeizi韩非公元前3世纪1949
盐铁论Discourses on Salt and Iron桓宽公元前1世纪1949
南方草木状NanFangCaoMuZhuang嵇含3-5世纪1949
论语The Analects of Confucius孔子公元前5世纪1949
骆驼祥子Camel Xiangzi老舍19372016
道德经The Tao-te Ching老子-1949
太平广记Taiping Guangji李昉、扈蒙978年1949
本草纲目Herbal Foundation Compendium李时珍1596年1949
淮南子Huainanzi刘安公元前2世纪1949
战国策Stratagems of the Warring States刘向-1949
呐喊Call to Arms鲁迅19231949
三国演义Romance of Three Kingdoms罗贯中14世纪1949
吕氏春秋The Annals of Lu Buwei吕不韦公元前239年1949
墨子Mozi墨翟公元前5世纪1949
梦溪笔谈The Dream Pool Essays沈括11世纪1949
水浒传All Men Are Brothers施耐庵14世纪1949
史记The Records of the Grand Historian司马迁公元前90年1949
资治通鉴History as a Mirror司马光1084年1949
天工开物Tian Gong Kai Wu宋应星1637年1949
孙子兵法The Art of War孙武公元前515-512年1949
儒林外史The Scholars吴敬梓16世纪1949
西游记Journey to the West吴承恩1592年1949
庄子The Book of Chuang Tzu庄子公元前4世纪1949
春秋左传Chun Qiu Zuo Zhuan左丘明公元前468-300年1949

4.3 数据集文件结构

├── 近代/
├── 秦汉/
├── 先秦/
├── 本草纲目.txt (5,422 KB)
├── 曾国藩家书.txt (321 KB)
├── 红楼梦.txt (2,562 KB)
├── 三国演义.txt (1,671 KB)
├── 水浒传.txt (2,540 KB)
├── 西游记.txt (2,121 KB)
├── 太平广记.txt (1,924 KB)
└── ... (其他中文典籍)

📝通俗解释:这些中文典籍都是1949年以前出版的作品,已经过了版权保护期,可以自由使用。训练数据选用了大量中国古典文学名著,可以帮助模型学习中文的语言风格和文化背景。


五、如何基于 LoRA 的 Llama2 二次预训练?

5.1 二次预训练参数介绍

5.1.1 预训练模型参数

python
@dataclass
class ModelArguments:
    model_name_or_path: Optional[str] = field(
        default=None,
        metadata={"help": "预训练模型路径或HuggingFace模型ID"}
    )
    tokenizer_name_or_path: Optional[str] = field(
        default=None,
        metadata={"help": "分词器路径或HuggingFace分词器ID"}
    )
    model_type: Optional[str] = field(
        default=None,
        metadata={"help": "模型类型,如 llama2"}
    )
    config_overrides: Optional[str] = field(
        default=None,
        metadata={"help": "覆盖默认配置参数"}
    )
    config_name: Optional[str] = field(
        default=None,
        metadata={"help": "预训练配置名称或路径"}
    )
    tokenizer_name: Optional[str] = field(
        default=None,
        metadata={"help": "预训练分词器名称或路径"}
    )
    cache_dir: Optional[str] = field(
        default=None,
        metadata={"help": "模型缓存目录"}
    )
    use_fast_tokenizer: bool = field(
        default=True,
        metadata={"help": "是否使用快速分词器"}
    )
    model_revision: str = field(
        default="main",
        metadata={"help": "模型版本(分支名、标签或提交ID)"}
    )
    use_auth_token: bool = field(
        default=False,
        metadata={"help": "是否使用认证令牌访问私有模型"}
    )
    torch_dtype: Optional[str] = field(
        default=None,
        metadata={
            "help": "模型数据类型",
            "choices": ["auto", "bfloat16", "float16", "float32"]
        }
    )

关键参数说明:

参数名说明
model_name_or_path预训练模型地址(本地路径或 HuggingFace 模型 ID)
tokenizer_name_or_path分词器地址(需使用中文分词器以支持中文)
model_type大模型类型
torch_dtype模型精度类型,建议使用 float16 或 bfloat16

5.1.2 数据训练参数

python
@dataclass
class DataTrainingArguments:
    dataset_dir: Optional[str] = field(
        default=None,
        metadata={"help": "数据集目录"}
    )
    dataset_config_name: Optional[str] = field(
        default=None,
        metadata={"help": "数据集配置名称"}
    )
    train_file: Optional[str] = field(
        default=None,
        metadata={"help": "训练数据文件路径"}
    )
    validation_file: Optional[str] = field(
        default=None,
        metadata={"help": "验证数据文件路径"}
    )
    max_train_samples: Optional[int] = field(
        default=None,
        metadata={"help": "最大训练样本数"}
    )
    max_eval_samples: Optional[int] = field(
        default=None,
        metadata={"help": "最大验证样本数"}
    )
    streaming: bool = field(
        default=False,
        metadata={"help": "是否启用流式模式"}
    )
    block_size: Optional[int] = field(
        default=None,
        metadata={"help": "tokenization后的序列长度"}
    )
    overwrite_cache: bool = field(
        default=False,
        metadata={"help": "是否覆盖缓存"}
    )
    validation_split_percentage: Optional[float] = field(
        default=0.05,
        metadata={"help": "验证集比例"}
    )
    preprocessing_num_workers: Optional[int] = field(
        default=None,
        metadata={"help": "预处理工作进程数"}
    )
    keep_linebreaks: bool = field(
        default=True,
        metadata={"help": "是否保留换行符"}
    )
    data_cache_dir: Optional[str] = field(
        default="./",
        metadata={"help": "处理后的数据集缓存目录"}
    )

5.1.3 LoRA 训练参数

python
@dataclass
class MyTrainingArguments(TrainingArguments):
    trainable: Optional[str] = field(default="q_proj,v_proj")
    lora_rank: Optional[int] = field(default=8)
    lora_dropout: Optional[float] = field(default=0.1)
    lora_alpha: Optional[float] = field(default=32.)
    modules_to_save: Optional[str] = field(default=None)
    debug_mode: Optional[bool] = field(default=False)
    peft_path: Optional[str] = field(default=None)
    flash_attn: Optional[bool] = field(default=False)
    double_quant: Optional[bool] = field(default=True)
    quant_type: Optional[str] = field(default="nf4")
    load_in_kbits: Optional[int] = field(default=16)

LoRA 关键参数说明:

参数说明常用值
lora_rankLoRA 低秩矩阵的维数,越大效果越好但显存占用越高64, 128
lora_alphaLoRA 低秩矩阵的缩放系数,调整它与调整学习率类似128, 256
lora_dropoutLoRA 层的 dropout 率0.05, 0.1
trainable可训练的 LoRA 模块q_proj, v_proj, k_proj, o_proj, gate_proj, down_proj, up_proj
modules_to_save需要保存的完整模块(非 LoRA)embed_tokens, lm_head

5.1.4 模型路径配置参考

用途model_name_or_pathtokenizer_name_or_path最终词表大小
基于原版 Llama2 训练中文 LoRA原版 HF 格式的 Llama2中文 Llama2 的 tokenizer55296
基于中文 Llama2 继续预训练完整中文 Llama2中文 Llama2 的 tokenizer55296
基于中文 Alpaca2 继续预训练完整中文 Alpaca2中文 Llama2 的 tokenizer55296

📝通俗解释:

  • lora_rank 就像"调整旋钮的精细程度",数字越大能表达的变化越丰富,但需要更多计算资源
  • lora_alpha 是"缩放因子",用来调节 LoRA 影响的强度
  • trainable 参数指定哪些层需要添加 LoRA 适配器,通常选择注意力机制的相关层

5.2 二次预训练实战

5.2.1 训练参数配置

bash
# 学习率和 LoRA 参数
lr=2e-4                    # 学习率
lora_rank=64               # LoRA 低秩矩阵维度
lora_alpha=128             # LoRA 缩放系数

# 可训练的 LoRA 模块(全部Transformer层)
lora_trainable="q_proj,v_proj,k_proj,o_proj,gate_proj,down_proj,up_proj"

# 需要保存的完整模块(词嵌入和输出层)
modules_to_save="embed_tokens,lm_head"

lora_dropout=0.05          # Dropout 率

# 模型和数据路径
pretrained_model=/root/llama/all_transformer       # 预训练模型路径
chinese_tokenizer_path=/root/llama/all_transformer # 中文分词器路径
dataset_dir=/root/llama/data                        # 数据集路径
data_cache=./cache/                                 # 数据缓存路径

# 训练批次配置
per_device_train_batch_size=1      # 每个设备训练批次大小
gradient_accumulation_steps=1      # 梯度累积步数
block_size=512                     # 最大序列长度

# 训练步数
training_steps=25000

# DeepSpeed 配置文件
deepspeed_config_file=scripts/training/ds_zero2_no_offload.json

参数说明:

  • q_proj、k_proj、v_proj:多头注意力中的查询、键、值投影矩阵
  • o_proj:多头注意力的输出投影矩阵
  • gate_proj、down_proj、up_proj:FFN(前馈神经网络)中的门控和上/下投影矩阵
  • embed_tokens:词嵌入层,将 token ID 映射为向量
  • lm_head:语言模型头,将隐藏状态映射到词汇表概率分布

5.2.2 启动训练命令

bash
torchrun --nnodes 1 --nproc_per_node 1 scripts/training/run_clm_pt_with_peft.py \
    --deepspeed ${deepspeed_config_file} \
    --model_name_or_path ${pretrained_model} \
    --tokenizer_name_or_path ${chinese_tokenizer_path} \
    --dataset_dir ${dataset_dir} \
    --data_cache_dir ${data_cache} \
    --validation_split_percentage 0.001 \
    --per_device_train_batch_size ${per_device_train_batch_size} \
    --do_train \
    --seed $RANDOM \
    --fp16 \
    --max_steps ${training_steps} \
    --num_train_epochs 1 \
    --lr_scheduler_type cosine \
    --learning_rate ${lr} \
    --warmup_ratio 0.05 \
    --weight_decay 0.01 \
    --logging_strategy steps \
    --logging_steps 10 \
    --save_strategy steps \
    --save_total_limit 3 \
    --save_steps 500 \
    --gradient_accumulation_steps ${gradient_accumulation_steps} \
    --preprocessing_num_workers 8 \
    --block_size ${block_size} \
    --output_dir ${output_dir} \
    --overwrite_output_dir \
    --ddp_timeout 30000 \
    --logging_first_step True \
    --lora_rank ${lora_rank} \
    --lora_alpha ${lora_alpha} \
    --trainable ${lora_trainable} \
    --modules_to_save ${modules_to_save} \
    --lora_dropout ${lora_dropout} \
    --torch_dtype float16 \
    --resume True \
    --resume_from_checkpoint ${resume_from} \
    --gradient_checkpointing \
    --ddp_find_unused_parameters False

5.2.3 GPU 状态监控

使用 nvidia-smi 命令监控 GPU 状态:

GPU型号温度性能功耗显存使用GPU利用率
0NVIDIA A100-SXM4-40GB53°CP0324W/400W20449MiB/40960MiB95%
1NVIDIA A100-SXM4-40GB54°CP0364W/400W20749MiB/40960MiB94%
2NVIDIA A100-SXM4-40GB48°CP0326W/400W20265MiB/40960MiB89%
3NVIDIA A100-SXM4-40GB53°CP0337W/400W20265MiB/40960MiB89%
4NVIDIA A100-SXM4-40GB52°CP0335W/400W20737MiB/40960MiB92%
5NVIDIA A100-SXM4-40GB48°CP0319W/400W20449MiB/40960MiB93%
6NVIDIA A100-SXM4-40GB30°CP052W/400W2MiB/40960MiB0%

📝通俗解释:7 块 GPU 中有 6 块在同时训练(GPU 6 空闲),每块显存占用约 20GB,总共占用约 120GB 显存。GPU 利用率都在 90% 左右,说明训练效率很高。

5.2.4 训练输出模型结构

训练完成后,会在输出目录生成以下文件:

bash
ls -lh ./
total 1.2G
-rw-r--r-- 1 root root  484 Sep  1 15:36 adapter_config.json    # LoRA 配置
-rw-r--r-- 1 root root 1.2G Sep  1 15:36 adapter_model.bin       # LoRA 权重
-rw-r--r-- 1 root root  435 Sep  1 15:36 special_tokens_map.json # 特殊 token 映射
-rw-r--r-- 1 root root  844 Sep  1 15:36 tokenizer_config.json   # 分词器配置
-rw-r--r-- 1 root root  825K Sep  1 15:36 tokenizer.model         # 分词器模型

📝通俗解释:训练完成后只保存了 LoRA 的适配器权重(adapter_model.bin 约 1.2GB),而不是完整的模型权重。这就是 LoRA 的优势——大幅减少存储空间。


六、如何基于 LoRA 的 Llama2 微调?

二次预训练后,还需要进行监督微调(Supervised Fine-Tuning,SFT)让模型具备指令跟随能力。

6.1 训练数据介绍

本项目使用 Stanford 开源的 stanford_alpaca 项目方法,通过 OpenAI API 生成指令微调数据。

由于原版数据是英文的,这里使用 Chinese-LLaMA-Alpaca 开源的中文 Alpaca 数据集,包含 51k 条指令数据。

数据格式(JSONL):

json
[
    {
        "instruction": "我们如何在日常生活中减少用水?",
        "input": "",
        "output": "1. 使用节水装置,如节水淋浴喷头和水龙头。\n2. 使用水箱或水桶收集家庭废水,例如洗碗和洗浴。\n3. 在社区中提高节水意识。\n4. 检查水管和灌溉系统的漏水情况,并及时修复它们。\n5. 洗澡时间缩短,使用低流量淋浴头节约用水。\n6. 收集雨水,用于园艺或其他非饮用目的。\n7. 刷牙或擦手时关掉水龙头。\n8. 减少浇水草坪的时间。\n9. 尽可能多地重复使用灰水。\n10. 只购买能源效率高的洗碗机和洗衣机。"
    },
    {
        "instruction": "编辑文章,使其更吸引读者。",
        "input": "自主机器人是计算机控制的机器,被编程执行特定任务而不需要任何人类输入。自主机器人在各个行业中被越来越广泛地应用。",
        "output": "自主机器人是计算机控制的机器,被编程执行特定任务而不需要任何人类输入,从而实现了新的效率、精确度和可靠性水平。自主机器人在各个行业中被越来越广泛地应用,从制造业到医疗保健再到安全..."
    }
]

📝通俗解释:微调数据采用"指令-输入-输出"格式,让模型学会理解人类的指令并给出合适的回答。51k 条数据足以让模型掌握基本的对话能力。

6.2 微调参数介绍

6.2.1 数据参数

python
@dataclass
class DataTrainingArguments:
    dataset_dir: Optional[str] = field(
        default=None,
        metadata={"help": "数据集目录"}
    )
    train_file: Optional[str] = field(
        default=None,
        metadata={"help": "训练数据文件"}
    )
    validation_file: Optional[str] = field(
        default=None,
        metadata={"help": "验证数据文件"}
    )
    overwrite_cache: bool = field(
        default=False,
        metadata={"help": "是否覆盖缓存"}
    )
    validation_split_percentage: Optional[float] = field(
        default=0.05,
        metadata={"help": "验证集比例"}
    )
    preprocessing_num_workers: Optional[int] = field(
        default=None,
        metadata={"help": "预处理工作进程数"}
    )
    keep_linebreaks: bool = field(
        default=True,
        metadata={"help": "是否保留换行符"}
    )
    data_cache_dir: Optional[str] = field(
        default=None,
        metadata={"help": "数据集缓存目录"}
    )
    max_seq_length: Optional[int] = field(default=1024)  # 最大序列长度

6.2.2 LoRA 参数

python
@dataclass
class MyTrainingArguments(TrainingArguments):
    trainable: Optional[str] = field(default="q_proj,v_proj")
    lora_rank: Optional[int] = field(default=8)
    lora_dropout: Optional[float] = field(default=0.1)
    lora_alpha: Optional[float] = field(default=32.)
    modules_to_save: Optional[str] = field(default=None)
    peft_path: Optional[str] = field(default=None)
    flash_attn: Optional[bool] = field(default=False)
    double_quant: Optional[bool] = field(default=True)
    quant_type: Optional[str] = field(default="nf4")
    load_in_kbits: Optional[int] = field(default=16)

6.3 微调实战

6.3.1 训练参数配置

bash
# 学习率和 LoRA 参数
lr=1e-4                    # 学习率(微调阶段通常比预训练低)
lora_rank=64               # LoRA 维度
lora_alpha=128             # LoRA 缩放系数

# 可训练模块和保存模块
lora_trainable="q_proj,v_proj,k_proj,o_proj,gate_proj,down_proj,up_proj"
modules_to_save="embed_tokens,lm_head"
lora_dropout=0.05

# 模型和数据路径
pretrained_model=/root/llama/correspond_output_dir  # 二次预训练后的模型
chinese_tokenizer_path=/root/llama/correspond_output_dir
dataset_dir=data_pt
validation_file=data_pt/alpaca_data_zh_51k.json  # 中文 Alpaca 数据

# 训练配置
per_device_train_batch_size=1
per_device_eval_batch_size=1
gradient_accumulation_steps=8    # 梯度累积(增大有效批次)
max_seq_length=512
training_steps=6000

output_dir=sft_output_dir2
deepspeed_config_file=scripts/training/ds_zero2_no_offload.json

6.3.2 启动微调命令

bash
torchrun --nnodes 1 --nproc_per_node 7 scripts/training/run_clm_sft_with_peft.py \
    --deepspeed ${deepspeed_config_file} \
    --model_name_or_path ${pretrained_model} \
    --tokenizer_name_or_path ${chinese_tokenizer_path} \
    --dataset_dir ${dataset_dir} \
    --per_device_train_batch_size ${per_device_train_batch_size} \
    --per_device_eval_batch_size ${per_device_eval_batch_size} \
    --do_train \
    --do_eval \
    --eval_steps 1000 \
    --seed $RANDOM \
    --fp16 \
    --num_train_epochs 1 \
    --lr_scheduler_type cosine \
    --learning_rate ${lr} \
    --warmup_ratio 0.03 \
    --weight_decay 0 \
    --logging_strategy steps \
    --logging_steps 10 \
    --save_strategy steps \
    --save_total_limit 3 \
    --evaluation_strategy steps \
    --eval_steps 6000 \
    --save_steps 3000 \
    --gradient_accumulation_steps ${gradient_accumulation_steps} \
    --preprocessing_num_workers 8 \
    --max_steps ${training_steps} \
    --max_seq_length ${max_seq_length} \
    --output_dir ${output_dir} \
    --overwrite_output_dir \
    --ddp_timeout 30000 \
    --logging_first_step True \
    --lora_rank ${lora_rank} \
    --lora_alpha ${lora_alpha} \
    --trainable ${lora_trainable} \
    --lora_dropout ${lora_dropout} \
    --modules_to_save ${modules_to_save} \
    --torch_dtype float16 \
    --validation_file ${validation_file}

6.3.3 微调阶段 GPU 状态

GPU型号温度功耗显存使用GPU利用率
0NVIDIA A100-SXM4-40GB44°C126W/400W23423MiB/40960MiB53%
1NVIDIA A100-SXM4-40GB48°C234W/400W24961MiB/40960MiB60%
2NVIDIA A100-SXM4-40GB42°C249W/400W25529MiB/40960MiB61%
3NVIDIA A100-SXM4-40GB44°C189W/400W24225MiB/40960MiB55%
4NVIDIA A100-SXM4-40GB45°C170W/400W24453MiB/40960MiB60%
5NVIDIA A100-SXM4-40GB41°C198W/400W23771MiB/40960MiB54%
6NVIDIA A100-SXM4-40GB44°C178W/400W25549MiB/40960MiB68%

📝通俗解释:微调阶段使用了 7 块 GPU(比预训练多一块),因为微调阶段 batch_size 更小但梯度累积步数更大,需要更多 GPU 来加速训练。

6.3.4 微调输出模型

bash
ls -lh ./
total 1.2G
-rw-r--r-- 1 root root  490 Sep  4 13:30 adapter_config.json
-rw-r--r-- 1 root root 1.2G Sep  4 13:30 adapter_model.bin
-rw-r--r-- 1 root root  435 Sep  4 13:30 special_tokens_map.json

七、如何使用基于 LoRA 的 Llama2 做推理?

7.1 推理命令

bash
python scripts/inference/inference_hf.py \
  --base_model correspond_output_dir \       # 基础模型路径(二次预训练后的模型)
  --lora_model sft_output_dir2/sft_lora_model \  # LoRA 权重路径(微调后的 LoRA)
  --tokenizer_path correspond_output_dir \   # 分词器路径
  --with_prompt  # 自动用提示符包装输入

7.2 推理效果示例

输入:

Why do you need to protect environment? Please answer in Chinese!

输出:

为了保护环境,我们需要采取行动,因为它是我们唯一的家园,它是我们生命的源泉。

📝通俗解释:模型能够理解英文问题并用中文回答,说明二次预训练(中文能力)和微调(指令跟随能力)都成功了。


致谢

感谢以下开源项目和资料:


整理完成

基于 MIT 许可发布