大家好,这是小白哥,因为最近在学LLM,因为网上资源太多太杂了,导致看着很简单,但自己跑起来一大堆错误,所以我在这记录一下我的LLM初体验,附赠详细的教程以及对代码解读。准备好了开始发车!!
实验环境
因为在实验室卑微,所以能捞到的GPU不多,我的实验环境如下:
四卡,但是基本都在占满的情况
Thu Sep 5 19:55:30 2024
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.90.07 Driver Version: 550.90.07 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 Tesla V100-DGXS-32GB Off | 00000000:07:00.0 Off | 0 |
| N/A 64C P0 143W / 300W | 29913MiB / 32768MiB | 87% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
| 1 Tesla V100-DGXS-32GB Off | 00000000:08:00.0 Off | 0 |
| N/A 58C P0 241W / 300W | 20777MiB / 32768MiB | 96% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
| 2 Tesla V100-DGXS-32GB Off | 00000000:0E:00.0 Off | 0 |
| N/A 48C P0 42W / 300W | 2MiB / 32768MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
| 3 Tesla V100-DGXS-32GB Off | 00000000:0F:00.0 Off | 0 |
| N/A 65C P0 265W / 300W | 29993MiB / 32768MiB | 74% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| 0 N/A N/A 1927 G /usr/lib/xorg/Xorg 9MiB |
| 0 N/A N/A 2665 G /usr/bin/gnome-shell 15MiB |
| 0 N/A N/A 28598 C python 29882MiB |
| 1 N/A N/A 40814 C python 20772MiB |
| 3 N/A N/A 3693 C python 29988MiB |
+-------------------------------------------------------------------------------
依赖库安装
本文需要到的依赖库;
accelerate\transformers\peft\datasets
accelerate 用于多卡训练
peft 用于微调
这两个库可以看一下详细介绍:
- 分布式训练
- accelerate
- peft
多说无益直接开干
微调数据以及模型
微调数据不用什么说的了,随便找一些文本资料即可
模型因为恰好本地有Baichuang-7B的权重,本着多一事不如少一事,我就使用这个进行
代码详解
代码主要分为四部分:
1. 数据预处理
2. 模型以及分词器构建
3. 微调模型
4. 分布式训练
5. 总结
接下来依次详细介绍每个部分代码,附赠详细的解释
总结部分有整体的代码,拿来可用~~~
## 先导入需要的库
from accelerate import PartialState
from datasets import load_dataset
from peft import TaskType, LoraConfig, get_peft_model
from transformers import Trainer
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from dataclasses import dataclass, field
import transformers
from itertools import chain
import torch
import warnings
# 忽略不重要的信息
warnings.filterwarnings("ignore")
数据预处理
"""
load_dataset 是transformers自带的一个加载数据函数,'text'是指加载的文本txt文件,data_dir 为txt目录 split一般对于HF提供的数据集这么写来下载固定某些子集,比如dataset = load_dataset(path='squad', split='train')即下载其train的数据集
对于本地的数据集,可以通过
dataset = load_dataset('csv', data_files={'train':['my_train_file_1.csv','my_train_file_2.csv'],'test': 'my_test_file.csv'})来构造或者跑两次这个函数即可
"""
train_dataset = load_dataset("text", data_dir="train_data", split="train")
eval_dataset = load_dataset("text", data_dir="eval_data", split="train")
def tokenization(example):
return tokenizer(example["text"])
'''
定义map函数对其每单行数据分词, remove_columns=["text"] 即丢掉原始的text列
写在with里面是只需要主程序对数据处理就行了
'''
with training_args.main_process_first(desc="dataset map tokenization"):
train_dataset = train_dataset.map(tokenization, remove_columns=["text"], num_proc=training_args.num_proc)
eval_dataset = eval_dataset.map(tokenization, remove_columns=["text"], num_proc=training_args.num_proc)
'''
因为原始文本是不等长的,所以这段代码对每行数据拼接起来再依次按照max_seq_len来截断
*是解包 [[1,2],[3,4]] -> [1,2],[3,4]
chain -> [1,2],[3,4] -> [1,2,3,4]
train_dataset.map(group_texts, num_proc=training_args.num_proc, batched=True) batched=True是把数据变为
bxl 来批量处理
'''
def group_texts(examples):
# Concatenate all texts.
concatenated_examples = {k: list(chain(*examples[k])) for k in examples.keys()}
total_length = len(concatenated_examples[list(examples.keys())[0]])
# We drop the small remainder, and if the total_length < block_size we exclude this batch and return an empty dict.
# We could add padding if the model supported it instead of this drop, you can customize this part to your needs.
total_length = (total_length // training_args.max_seq_length) * training_args.max_seq_length
# Split by chunks of max_len.
result = {
k: [t[i: i + training_args.max_seq_length] for i in range(0, total_length, training_args.max_seq_length)]
for k, t in concatenated_examples.items()
}
result["labels"] = result["input_ids"].copy()
return result
with training_args.main_process_first(desc="dataset map tokenization"):
train_dataset = train_dataset.map(group_texts, num_proc=training_args.num_proc, batched=True)
eval_dataset = eval_dataset.map(group_texts, num_proc=training_args.num_proc, batched=True)
加不加split=‘train’的区别,可以看到不加其实是生成一个DatasetDict(默认key是train) 加了是生成一个Dataset其实就是多了一层嵌套
模型以及分词器构建
看着有点长,确实有点长,慢慢看详细注释
最关键就是这一部分
'''
transformers.TrainingArguments 对argparse.Argumentparse的改进,其实就是把平时我们训练时的参数变成类的形式定义
其里面包含了许多对Trainer过程的参数默认定义,你当然可以加上自己的,格式如下
lora_r: int = field(default=8)
**********************
ataclass 和 field
dataclass: Python 3.7 引入的一个模块,用于简化数据类的定义。数据类是一种专门用于存储数据的类,dataclass 可以自动为类生成一些常用方法,比如 __init__()、__repr__() 和 __eq__()。
field(): 在 dataclass 中,field 函数用于为类字段设置额外的参数或配置。它可以用来指定默认值、默认工厂函数、字段的元数据等。
'''
@dataclass
class CustomArguments(transformers.TrainingArguments):
# LoRA_r
lora_r: int = field(default=8)
# 数据处理时的并行进程数
num_proc: int = field(default=1)
# 最大序列长度
max_seq_length: int = field(default=32)
# 验证策略,如不想进行验证,可以设置为 ‘no’
eval_strategy: str = field(default="steps")
# 每多少步进行一次验证
eval_steps: int = field(default=100)
# 随机种子
seed: int = field(default=0)
# 优化器
optim: str = field(default="adamw_torch")
# 训练epoch数
num_train_epochs: int = field(default=2)
# 每个设备上的批量大小
per_device_train_batch_size: int = field(default=1)
# 学习率
learning_rate: float = field(default=5e-5)
# 权重衰减
weight_decay: float = field(default=0)
# 预热步数
warmup_steps: int = field(default=10)
# 学习率规划期类型
lr_scheduler_type: str = field(default="linear")
# 是否使用梯度检查点
gradient_checkpointing: bool = field(default=False)
# 是否使用bf16作为混合精度训练类型
bf16: bool = field(default=True)
# 梯度累加步数
gradient_accumulation_steps: int = field(default=1)
# 日志记录的步长频率
logging_steps: int = field(default=3)
# checkpoint保存策略
save_strategy: str = field(default="steps")
# checkpoint保存的步长频率
save_steps: int = field(default=3)
# 总的保存checkpoint的数量
save_total_limit: int = field(default=2)
parser = transformers.HfArgumentParser(CustomArguments)
training_args, = parser.parse_args_into_dataclasses()
## type(training_args) type(training_args)
model_path = 'model/models--baichuan-inc--Baichuan-7B'
'''
量化的指标,其实就是确认模型用什么字节精度的内存来存比如
fb16 fp32
因为资源有限 量化为4bit
1. load_in_4bit=True
含义: 这个参数启用了4-bit量化,将模型中的线性层权重从浮点数表示转换为4-bit表示。这样可以显著减少模型的内存占用,并提升推理速度,尤其在大模型上表现明显。
用途: 使用4-bit量化来减少模型的内存占用和计算成本。
2. bnb_4bit_use_double_quant=True
含义: 启用双重量化(Double Quantization)。在这个模式下,第一次量化的量化常数会被再次量化。这可以进一步压缩模型,同时保持精度。
用途: 提供更高的量化压缩率,同时保留较好的模型精度。
3. bnb_4bit_quant_type="nf4"
含义: 指定使用的量化类型。nf4 是一种特定的量化类型,全称为 Normal Float 4-bit,这种量化方法在4-bit浮点数表示中提供了较高的精度,适用于深度学习模型。
用途: 选择适合模型的量化类型。nf4 比 fp4(普通的4-bit浮点数)具有更好的数值稳定性。
4. bnb_4bit_compute_dtype=torch.bfloat16
含义: 设置计算时使用的数据类型(dtype),这里选择了 bfloat16(Brain Floating Point 16-bit),这是一种常用于深度学习的16位浮点数格式,它能在保持精度的情况下减少内存占用。
用途: 在计算过程中使用 bfloat16 数据类型,以减少内存使用并加速计算。
'''
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_path,trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_path,
low_cpu_mem_usage=True,
quantization_config=bnb_config,
device_map={"": PartialState().process_index},
trust_remote_code=True
)
'''
这段代码是对需要微调的结构部分进行处理
W_pack 是注意力里面的映射函数 类似kqv_proj o_proj 计算主要注意力后的一个proj
gate_proj,down_proj 是FFN里
注意这些模块名字是需要自己去源码里面找的 当然你可以写其他的 但是找不到它就不会生效 一般就是modeling_modelname.py里面
'''
peft_config = LoraConfig(
r=training_args.lora_r,
target_modules=["W_pack",
"o_proj",
"gate_proj",
"down_proj",
"up_proj"
],
task_type=TaskType.CAUSAL_LM,
lora_alpha=16,
lora_dropout=0.05
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
量化前:
量化后
定义LORA前
定义LORA后
针对W_pach看 主要是多了几个LORA的层 lora_droupout、lora_A、lora_B lora_embedding_A\B 、lora_magnitude_vecto
(W_pack): lora.Linear4bit(
(base_layer): Linear4bit(in_features=4096, out_features=12288, bias=False)
(lora_dropout): ModuleDict(
(default): Dropout(p=0.05, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=12288, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
(lora_magnitude_vector): ModuleDict()
)
分布式训练
这里其实就是靠accelerate这个强大的分布式训练工具
具体的操作可以详细看
accelerate
这是我的注册config流程 注意NO是可以直接回车跳过的
accelerate launch train.py --output_dir output --lora_r 16
最后运行就能愉快的调跑起来了
总结
贴一下全部的代码吧
from accelerate import PartialState
from datasets import load_dataset
from peft import TaskType, LoraConfig, get_peft_model
from transformers import Trainer
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from dataclasses import dataclass, field
import transformers
from itertools import chain
import torch
import warnings
warnings.filterwarnings("ignore")
@dataclass
class CustomArguments(transformers.TrainingArguments):
# LoRA_r
lora_r: int = field(default=8)
# 数据处理时的并行进程数
num_proc: int = field(default=1)
# 最大序列长度
max_seq_length: int = field(default=32)
# 验证策略,如不想进行验证,可以设置为 ‘no’
eval_strategy: str = field(default="steps")
# 每多少步进行一次验证
eval_steps: int = field(default=100)
# 随机种子
seed: int = field(default=0)
# 优化器
optim: str = field(default="adamw_torch")
# 训练epoch数
num_train_epochs: int = field(default=2)
# 每个设备上的批量大小
per_device_train_batch_size: int = field(default=1)
# 学习率
learning_rate: float = field(default=5e-5)
# 权重衰减
weight_decay: float = field(default=0)
# 预热步数
warmup_steps: int = field(default=10)
# 学习率规划期类型
lr_scheduler_type: str = field(default="linear")
# 是否使用梯度检查点
gradient_checkpointing: bool = field(default=False)
# 是否使用bf16作为混合精度训练类型
bf16: bool = field(default=True)
# 梯度累加步数
gradient_accumulation_steps: int = field(default=1)
# 日志记录的步长频率
logging_steps: int = field(default=3)
# checkpoint保存策略
save_strategy: str = field(default="steps")
# checkpoint保存的步长频率
save_steps: int = field(default=3)
# 总的保存checkpoint的数量
save_total_limit: int = field(default=2)
parser = transformers.HfArgumentParser(CustomArguments)
training_args, = parser.parse_args_into_dataclasses()
model_path = 'model/models--baichuan-inc--Baichuan-7B'
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
tokenizer = AutoTokenizer.from_pretrained(model_path,trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_path,
low_cpu_mem_usage=True,
quantization_config=bnb_config,
device_map={"": PartialState().process_index},
trust_remote_code=True
)
peft_config = LoraConfig(
r=training_args.lora_r,
target_modules=["q_proj",
"v_proj",
"k_proj",
"o_proj",
"gate_proj",
"down_proj",
"up_proj"
],
task_type=TaskType.CAUSAL_LM,
lora_alpha=16,
lora_dropout=0.05
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
train_dataset = load_dataset("text", data_dir="train_data", split="train")
eval_dataset = load_dataset("text", data_dir="eval_data", split="train")
def tokenization(example):
return tokenizer(example["text"])
with training_args.main_process_first(desc="dataset map tokenization"):
train_dataset = train_dataset.map(tokenization, remove_columns=["text"], num_proc=training_args.num_proc)
eval_dataset = eval_dataset.map(tokenization, remove_columns=["text"], num_proc=training_args.num_proc)
def group_texts(examples):
# Concatenate all texts.
concatenated_examples = {k: list(chain(*examples[k])) for k in examples.keys()}
total_length = len(concatenated_examples[list(examples.keys())[0]])
# We drop the small remainder, and if the total_length < block_size we exclude this batch and return an empty dict.
# We could add padding if the model supported it instead of this drop, you can customize this part to your needs.
total_length = (total_length // training_args.max_seq_length) * training_args.max_seq_length
# Split by chunks of max_len.
result = {
k: [t[i: i + training_args.max_seq_length] for i in range(0, total_length, training_args.max_seq_length)]
for k, t in concatenated_examples.items()
}
result["labels"] = result["input_ids"].copy()
return result
with training_args.main_process_first(desc="dataset map tokenization"):
train_dataset = train_dataset.map(group_texts, num_proc=training_args.num_proc, batched=True)
eval_dataset = eval_dataset.map(group_texts, num_proc=training_args.num_proc, batched=True)
if __name__ == '__main__':
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset
)
trainer.train()
trainer.save_model("./test_train")