GitHub - chinese-poetry/chinese-poetry:中国诗词数据集
chinese-poetry/全唐诗 at master · chinese-poetry/chinese-poetry · GitHub
使用 AutoModelForCausalLM 因果语言建模:是指给定之前的词或字符序列,模型预测文本序列中下一个词或字符的任务,广泛应用于生成式任务,如对话系统、文本续写、摘要生成、创意写作、代码生成等。
使用唐诗数据集,配合LoRA进行微调。
目录
加载预训练模型model from_pretrained和分词器tokenizer
量化配置nf4_config 生成参数配置 generation_config 随机种子设置
2. 获取生成结果 evaluate函数 生成结果 prompt格式 -> inputs -> tokenizer -> generate -> decode
3. 微调前的效果 用evaluate跑 微调前的结果
用generate函数 把对话数据转化为 输入 IDs、标签labels和注意力掩码"attention_mask"的字典
5. 训练相关的参数设置 训练参数配置:批次大小 LoRA微调与分布式训练
6. trainer训练 Trainer进行训练;保存模型参数
7. 模型测试 训练好的模型跑测试集
1. 环境与参数配置
import torch
from transformers import (
AutoModelForCausalLM, # 自动加载因果语言模型
AutoTokenizer, # 自动加载对应的分词器
BitsAndBytesConfig, # 量化配置
GenerationConfig # 文本生成配置
)
import logging # 日志管理
# 设置模型名称 - 使用MediaTek的Breeze 7B指令微调模型
model_name = "MediaTek-Research/Breeze-7B-Instruct-v0_1"
量化配置:使用4-bit量化来大幅减少内存占用,使得7B模型可以在消费级GPU上运行
nf4_config = BitsAndBytesConfig(
load_in_4bit=True, # 启用4-bit量化加载
bnb_4bit_quant_type="nf4", # 使用NF4量化类型(Normal Float 4)
bnb_4bit_use_double_quant=True, # 启用双重量化,进一步压缩模型大小
bnb_4bit_compute_dtype=torch.bfloat16 # 计算时使用bfloat16精度,平衡性能和内存
)
从HuggingFace加载预训练的语言模型 from_pretrained
model = AutoModelForCausalLM.from_pretrained(
model_name,
cache_dir="./cache", # 指定缓存目录
quantization_config=nf4_config, # 应用量化配置
low_cpu_mem_usage=True, # 减少CPU内存使用
trust_remote_code=True, # 信任远程代码(如果需要)
device_map="auto" # 自动分配设备(CPU/GPU)
)
加载与模型对应的分词器 tokenizer
# 日志级别为ERROR,减少不必要的输出
logging.getLogger('transformers').setLevel(logging.ERROR)
# 加载与模型对应的分词器
tokenizer = AutoTokenizer.from_pretrained(
model_name,
add_eos_token=True, # 自动添加结束符
cache_dir="./cache",
trust_remote_code=True # 信任远程代码
)
# 设置填充token与结束token相同(常见做法)
tokenizer.pad_token = tokenizer.eos_token
配置文本生成的参数 generation_config,控制生成文本的质量和多样性
max_len = 128 # 生成文本的最大长度
generation_config = GenerationConfig(
do_sample=True, # 使用采样而非贪婪解码(生成更多样的文本)
temperature=0.1, # 温度参数:较低值=更确定性,较高值=更多样性
num_beams=1, # 束搜索大小:1=不使用束搜索(与do_sample=True配合)
top_p=0.3, # 核采样参数:只考虑概率累积前30%的token
top_k=50, # 只考虑前50个最可能的token
no_repeat_ngram_size=3, # 避免重复的3-gram,提高文本多样性
pad_token_id=tokenizer.pad_token_id, # 使用分词器的填充token ID
eos_token_id=tokenizer.eos_token_id, # 使用分词器的结束token ID
max_new_tokens=max_len, # 最大生成token数量
)
为确保深度学习实验的可重复性:
CPU GPU随机种子;CuDNN 自动优化器关闭,启用确定性模式
seed = 42
torch.backends.cudnn.deterministic = True # 启用 CuDNN(NVIDIA 的深度学习加速库)的确定性模式
torch.backends.cudnn.benchmark = False # 关闭 CuDNN 的自动优化器
torch.manual_seed(seed) # CPU 随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed) # GPU 随机种子
2. 获取生成结果 evaluate函数
设置prompt为 ChatML格式,专门用于指令微调模型。
<<SYS>> 包含系统角色设定 [INST] 和 [/INST] 包裹用户输入 {instruction} 和 {input_text}。
prompt格式 -> inputs -> tokenizer -> generate -> decode 并把格式化的部分去掉。
def evaluate(instruction, generation_config, max_len, input_text="", verbose=True):
# 构建完整的输入提示词 - 使用特定的指令格式
prompt = f"""\
[INST] <<SYS>>
You are a helpful assistant and good at writing Tang poem. 你是一個樂於助人的助手且擅長寫唐詩。
<</SYS>>
{instruction}
{input_text}
[/INST]"""
# 将提示词转换为模型所需的 token 格式
inputs = tokenizer(prompt, return_tensors="pt")
input_ids = inputs["input_ids"].cuda() # 将输入移到GPU上
# 使用模型生成回复
generation_output = model.generate(
input_ids=input_ids,
generation_config=generation_config,
return_dict_in_generate=True,
output_scores=True,
max_new_tokens=max_len,
)
# 解码并处理生成的回复
for s in generation_output.sequences:
output = tokenizer.decode(s) # 将token解码为文本
# 清理输出文本:移除指令标记和助手标识
output = output.split("[/INST]")[1] # 只取[/INST]之后的部分(模型回复)
output = output.replace("</s>", "").replace("<s>", "") # 移除特殊token
output = output.replace("Assistant:", "").replace("Assistant", "") # 移除助手标识
output = output.strip() # 去除首尾空白
if verbose:
print(output)
return output
3. 微调前的效果
# 测试样例
test_tang_list = [
'相見時難別亦難,東風無力百花殘。',
'重帷深下莫愁堂,臥後清宵細細長。',
'芳辰追逸趣,禁苑信多奇。'
]
# 获取每个样例的模型输出
demo_before_finetune = []
for tang in test_tang_list:
demo_before_finetune.append(
f'模型輸入:\n以下是一首唐詩的第一句話,請用你的知識判斷並完成整首詩。{tang}\n\n模型輸出:\n' +
evaluate('以下是一首唐詩的第一句話,請用你的知識判斷並完成整首詩。', generation_config, max_len, tang, verbose=False)
)
# 打印并将输出存储到文本文件
for idx in range(len(demo_before_finetune)):
print(f"Example {idx + 1}:")
print(demo_before_finetune[idx])
print("-" * 80)
可以发现,模型只会重复输入中的一些文字,不能很好的完成任务。
比如说“相见时难别亦难,东风无力百花残” 只是在重复 “相见难,别难,东风,百花” 这些字词。
4. 准备微调的训练数据 generate函数
数据链接 git clone https://github.com/CheeEn-Yu/GenAI-Hw5.git
将输入和输出文本转换为模型可读取的 tokens。
参数输入:- data_point: 包含对话中 "instruction"、"input" 和 "output" 字段的字典。
输出:- 包含模型输入 IDs、标签labels和注意力掩码"attention_mask"的字典。
标签掩码labels分为(1)用户提示部分:用-100标记,表示在训练时不需要计算这些token的损失(模型不应该学习预测它已经看到的内容(用户输入);
(2)模型回复部分:使用实际的token ID
设置prompt 包含 系统角色设定、{instruction} 和 {input_text} 同evaluate函数
计算用户prompt的token数量;还有full_tokens对应完整的输入输出 prompt + output
def generate_training_data(data_point):
# 构建完整的输入提示词 - 使用与推理时相同的格式
prompt = f"""\
[INST] <<SYS>>
You are a helpful assistant and good at writing Tang poem. 你是一個樂於助人的助手且擅長寫唐詩。
<</SYS>>
{data_point["instruction"]}
{data_point["input"]}
[/INST]"""
# 计算用户提示词的 token 数量(不包括模型回复部分)
len_user_prompt_tokens = (
len(
tokenizer(
prompt,
truncation=True,
max_length=CUTOFF_LEN + 1,
padding="max_length",
)["input_ids"]
) - 1 # 减去1是因为tokenizer可能会添加特殊token
)
# 将完整的输入和输出转换为 tokens(提示词 + 期望的输出)
full_tokens = tokenizer(
prompt + " " + data_point["output"] + "</s>", # 添加结束符
truncation=True,
max_length=CUTOFF_LEN + 1,
padding="max_length",
)["input_ids"][:-1] # 去掉最后一个可能的多余token
return {
"input_ids": full_tokens, # 完整的token序列
"labels": [-100] * len_user_prompt_tokens + full_tokens[len_user_prompt_tokens:],
"attention_mask": [1] * len(full_tokens), # 注意力掩码(全为1)
}
5. 训练相关的参数设置
设置用于训练的数据量 训练数据越多,模型见到的诗句越多样化,生成质量越好,但训练时间越长
# 1040条数据:微调约25分钟,完整运行约50分钟 # 5000条数据:微调约100分钟,完整运行约120分钟
num_train_data = 1040 # 设置用于训练的数据量,最大值为5000
路径与日志保存设置
# ==================== 路径设置 ====================
output_dir = "./output" # 作业结果输出目录
ckpt_dir = "./exp1" # 模型checkpoint保存目录
cache_dir = "./cache" # 缓存目录路径
dataset_dir = "./GenAI-Hw5/Tang_training_data.json" # 训练数据集路径
# ==================== 日志和保存设置 ====================
logging_steps = 20 # 每隔20步输出一次日志
save_steps = 65 # 每隔65步保存一次模型
save_total_limit = 3 # 最多保留3个模型checkpoint(自动删除旧的)
report_to = "none" # 不上报实验指标(避免外部依赖)
训练参数与批次大小设置
# ==================== 训练参数 ====================
num_epoch = 1 # 训练的总Epoch数(轮次)
LEARNING_RATE = 3e-4 # 学习率(控制参数更新幅度)
from_ckpt = False # 是否从checkpoint加载模型权重
ckpt_name = None # 加载特定checkpoint的文件名
CUTOFF_LEN = 256 # 文本截断的最大长度(防止内存溢出)
# ==================== 批次大小设置 ====================
MICRO_BATCH_SIZE = 4 # 微批次大小(每次前向传播处理的样本数)
BATCH_SIZE = 16 # 有效批次大小(梯度累积后的总批次大小)
GRADIENT_ACCUMULATION_STEPS = BATCH_SIZE // MICRO_BATCH_SIZE # 梯度累积步数 = 4
LoRA微调与分布式训练
# ==================== LoRA参数 ====================
LORA_R = 8 # LoRA的秩(rank),控制低秩近似的维度
LORA_ALPHA = 16 # LoRA的缩放参数,控制LoRA更新的幅度
LORA_DROPOUT = 0.05 # LoRA层的Dropout率(防止过拟合)
TARGET_MODULES = [ # 应用LoRA的目标模块(这些层的权重会被微调)
"q_proj", "up_proj", "o_proj", "k_proj",
"down_proj", "gate_proj", "v_proj"
]
# ==================== 分布式训练设置 ====================
VAL_SET_SIZE = 0 # 验证集大小(0表示不使用验证集)
device_map = "auto" # 自动分配设备(CPU/GPU)
world_size = int(os.environ.get("WORLD_SIZE", 1)) # 获取分布式训练的世界大小
ddp = world_size != 1 # 判断是否使用分布式数据处理
if ddp:
device_map = {"": int(os.environ.get("LOCAL_RANK") or 0)}
GRADIENT_ACCUMULATION_STEPS = GRADIENT_ACCUMULATION_STEPS // world_size
6. trainer训练
加载模型,设置保存参数的位置 ckpt_dir
os.environ["TOKENIZERS_PARALLELISM"] = "false" # 禁用并行性以避免报错
# 创建指定的输出目录
os.makedirs(output_dir, exist_ok=True) # 保存输出
os.makedirs(ckpt_dir, exist_ok=True) # 保存参数权重
# 从 checkpoint 加载模型权重
if from_ckpt:
model = PeftModel.from_pretrained(model, ckpt_name)
model = prepare_model_for_kbit_training(model) # 准备模型
使用 LoraConfig 配置 LORA 模型
批次训练时,需要将不同长度的序列填充到相同长度设置 pad_token_id=0 确保填充部分不被模型关注
config = LoraConfig(
r=LORA_R, # LoRA的秩(rank)
lora_alpha=LORA_ALPHA, # LoRA的缩放参数
target_modules=TARGET_MODULES, # 要应用LoRA的目标模块
lora_dropout=LORA_DROPOUT, # LoRA层的dropout率
bias="none", # 偏置参数处理方式
task_type="CAUSAL_LM", # 任务类型
)
model = get_peft_model(model, config)
# 将 tokenizer 的填充 token 设置为 0
tokenizer.pad_token_id = 0
加载并处理训练数据 json格式 取前num_train_data条数据(如1040条);
分为训练集和验证集;
对每个样本应用generate_training_data函数并shuffle打乱
# 加载并处理训练数据
with open(dataset_dir, "r", encoding="utf-8") as f:
data_json = json.load(f)
with open("tmp_dataset.json", "w", encoding="utf-8") as f:
json.dump(data_json[:num_train_data], f, indent=2, ensure_ascii=False)
data = load_dataset('json', data_files="tmp_dataset.json", download_mode="force_redownload")
# 将训练数据分为训练集和验证集(若 VAL_SET_SIZE 大于 0)
if VAL_SET_SIZE > 0:
train_val = data["train"].train_test_split(
test_size=VAL_SET_SIZE, shuffle=True, seed=42
)
train_data = train_val["train"].shuffle().map(generate_training_data)
val_data = train_val["test"].shuffle().map(generate_training_data)
else:
train_data = data['train'].shuffle().map(generate_training_data)
val_data = None
Trainer参数:数据加载和批次处理;学习率调度;日志记录 和 checkpoint保存;
保存训练好的LoRA适配器权重。
# 使用 Transformers Trainer 进行模型训练
trainer = transformers.Trainer(
model=model, # 要训练的模型
train_dataset=train_data, # 训练数据集
eval_dataset=val_data, # 验证数据集(可选)
args=transformers.TrainingArguments(
per_device_train_batch_size=MICRO_BATCH_SIZE, # 每个设备的批次大小
gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS, # 梯度累积步数
warmup_steps=50, # 热身步数
num_train_epochs=num_epoch, # 训练轮数
learning_rate=LEARNING_RATE, # 学习率
fp16=True, # 使用混合精度训练
logging_steps=logging_steps, # 日志输出间隔
save_strategy="steps", # 按步数保存
save_steps=save_steps, # 保存间隔
output_dir=ckpt_dir, # 输出目录
save_total_limit=save_total_limit, # 最多保存的checkpoint数
ddp_find_unused_parameters=False if ddp else None, # DDP设置
report_to=report_to, # 报告目标
),
data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
# 禁用模型的缓存功能
model.config.use_cache = False
# 开始模型训练
trainer.train()
# 将训练好的模型保存到指定目录
model.save_pretrained(ckpt_dir)
7. 模型测试
GenAI-Hw5/Tang_testing_data.json 测试数据
training 时保存的 ckpt_dir ,从中找出最后一个 checkpoint (最后训练的参数结果)但可能有些过拟合
# 查找所有可用的 checkpoints
ckpts = []
for ckpt in os.listdir(ckpt_dir):
if ckpt.startswith("checkpoint-"):
ckpts.append(ckpt)
# 列出所有的 checkpoints
ckpts = sorted(ckpts, key=lambda ckpt: int(ckpt.split("-")[-1]))
# 选择最后一个(训练的最好 但有过拟合风险)
ckpt_name = os.path.join(ckpt_dir, ckpts[-1])
释放显存 防止后续推理空间不够。
import gc
# 删除模型和 tokenizer 对象
del model
del tokenizer
# 调用垃圾回收机制,强制释放未使用的内存
gc.collect()
# 清理 GPU 缓存
torch.cuda.empty_cache()
生成的参数设置generation_config(关于温度和多样性等);把ckpt参数加载到model
# 两个路径
test_data_path = "GenAI-Hw5/Tang_testing_data.json" # 测试数据集的路径
output_path = os.path.join(output_dir, "results.txt") # 生成结果的输出路径
# 从 checkpoint 加载已保存的模型权重
model = PeftModel.from_pretrained(model, ckpt_name, device_map={'': 0})
results = []
max_len = 128 # 生成回复的最大长度
# 设置生成配置,包括随机度、束搜索等参数
generation_config = GenerationConfig(
do_sample=True, # 启用采样生成(而非贪婪搜索),增加输出多样性
num_beams=1, # 束搜索大小,1表示不使用束搜索(与do_sample=True配合使用)
pad_token_id=2, # 设置填充token的ID,确保生成时正确处理序列填充
temperature=0.1, # 温度参数:控制生成随机性,值越小(0.1)生成的回复越稳定和确定性
top_p=0.3, # Top-p(核采样)概率阈值:只考虑累积概率前30%的token,控制生成多样性
top_k=5, # Top-k采样值:每步只从概率最高的5个token中采样,增加多样性并避免重复词汇
no_repeat_ngram_size=3, # 禁止重复的N-gram大小:避免生成重复的3-gram片段,提高文本质量
)
文件读取json.load 与写入write,evaluate预测并输出结果;
# 读取测试数据集
with open(test_data_path, "r", encoding="utf-8") as f:
test_datas = json.load(f)
# 对每个测试样例生成预测,并保存结果
with open(output_path, "w", encoding="utf-8") as f:
for (i, test_data) in enumerate(test_datas):
predict = evaluate(test_data["instruction"], generation_config, max_len, test_data["input"], verbose=False)
f.write(f"{i+1}. " + test_data["input"] + predict + "\n")
print(f"{i+1}. " + test_data["input"] + predict)
以下为测试的输入输出,有一些文乎的感觉。
8. 比较与之前的输出
# 使用之前的测试例子
test_tang_list = [
'相見時難別亦難,東風無力百花殘。',
'重帷深下莫愁堂,臥後清宵細細長。',
'芳辰追逸趣,禁苑信多奇。'
]
# 使用微调后的模型进行推理,直接输出结果
for idx, tang in enumerate(test_tang_list):
print(f"Example {idx + 1}:")
print(f'模型輸入:\n以下是一首唐詩的第一句話,請用你的知識判斷並完成整首詩。{tang}')
print('\n模型輸出:')
output = evaluate('以下是一首唐詩的第一句話,請用你的知識判斷並完成整首詩。', generation_config, max_len, tang, verbose=False)
print(output)
print("-" * 80)
可以看到和最开始有着很大的差别,最起码不是复读机了。