作者两年前微调了开源的大语言模型,并在阿毗达磨问题上进行了测试。他发现模型在阿毗达磨上的表现相对较好,但网上的数据质量极差。作者通过刘琨的帮助,使用unsloth库进行了模型微调,并使用了txt格式的训练数据。他详细解释了安装依赖、配置PEFT模型、数据处理、训练配置和推理的代码。最后,他给出了对代码和性能的详细解释和建议。
作者微调了开源的大语言模型,并测试了它在阿毗达磨问题上的表现。
作者发现网上的阿毗达磨数据质量极差,只有1%的数据是正确的。
作者使用unsloth库进行模型微调,提高了训练效率。
作者详细解释了数据处理和训练配置的代码,包括安装依赖、配置PEFT模型、数据处理和训练参数。
作者介绍了用于模型推理的代码,并给出了性能优化和错误处理的建议。
(这篇帖子主要是自己留个备份。想听我随便聊只看前面就行。想复制代码就直接拉到后面。)
两年前,我微调过当时开源的一些大语言模型。用阿毗达磨数据。因为,大语言模型在阿毗达磨问题上的表现总是很差。后来,每出一种更强劲的模型,我总会从写作和阿毗达磨两个方面去测试它。在文学写作上,最先达到我认为勉强可用的模型是Claude 3.5 Sonnet(2024年10月版),我是从去年11月开始用sonnet写作的。deepseek R1出现之后,基本替换成R1。R1在阿毗达磨上的表现比现在一般人能用的模型都要相对好一点。注意,只是相对。我现在一般会用三个比较简单的问题问它们:1、阿毗达磨里的98随眠具体是哪些?2、色界见灭所断随眠有哪些?3、色界见灭所断随眠中有哪些是无漏缘惑?——包括o1和o3-mini在内的模型,都还从来没回答对第二个问题过。R1在表现最好的时候,第二个问题回答对了(大部分时候回答不对)。这主要不是模型能力差,因为模型能做对很多数学竞赛和编程题,那些比阿毗达磨题目难太多了(更何况上面列举的只是非常基础的阿毗达磨题目)。主要原因是什么呢?是网上的阿毗达磨数据质量太差了,99%的数据都是错的。说99%毫不夸张。这也是我想尝试微调开源模型的原因。我完全不懂编程。连一行代码都看不明白。能够跑通,首先得益于刘琨的指导和帮助,其次得益于有诸多AI可用。也可以说主要是刘琨跑通的,我主要负责写这个帖子。所以,我把微调的代码放出来,以便自己以后还想微调有个备份(两年前微调的那些流程我清理电脑已经找不到了)。不过一开始,还是试了jsonl数据。方法是,先分卷把《俱舍论》塞给Claude,让它从每一卷中提取出40个问题,列成jsonl格式。效果如下(举三例):{"instruction": "", "input": "什么是"色蘊"?", "output": "色蘊包含五根(眼、耳、鼻、舌、身根)、五境(色、声、香、味、触)以及无表色。"}{"instruction": "", "input": "文中提到的
"五根"具体指什么?", "output": "五根指眼根、耳根、鼻根、舌根、身根,这五种净色是识的所依。"}{"instruction": "", "input": "文中提到的"界"是什么意思?", "output": "界的含义是法的种族义,如同一座山中有多种金银铜铁等族群,一身或一相续中有十八类诸法种族,故名十八界。"}复制这些内容成jsonl,还会碰到一些问题,一个是要把空行删除,再一个是内容中的引号会让程序出错,以及可能还会有一些其他问题。但这些都不是最关键的问题。最关键的问题是,jsonl类型的阿毗达磨数据质量高不了。高质量的数据只能手动去写,但我不可能有那个精力。AI根据《俱舍》《婆沙》生成的1万条jsonl数据,假设其中包含20万字,它的价值只相当于俱舍论的4000字,这就是为什么用jsonl数据训练很不可行。所以,我简单试了下jsonl数据,还是回归到txt数据。我是在google colab上微调的。一开始是用10MB的数据,包含《俱舍》《婆沙》《正理》三部阿毗达磨文献全文。还有个更大的,整个毗昙部的文献,48MB左右,想先跑通一遍流程,没用那个。用的是T4 GPU,最早的代码,显示微调完成需要36小时。我想先跑通流程,所以修改参数到最小,又换成一个4MB的数据(只包含《俱舍》《正理》),训练需要1小时左右。因此,现在放上来的也是这个。以下的代码解释,每一部分放在一个单元格中,解释是让Claude完成的。%%capture
!pip install
!pip install
!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer
!pip install
好的,我来详细解释这段安装依赖的代码。让我们一步步来看:- 它的作用是隐藏命令执行时的输出信息,让notebook看起来更整洁- 通常情况下,只需要运行 `pip install unsloth` 就够了- 但在2025年1月底,由于Colab环境的特殊性,需要分步安装以提高效率- 这种方式可以把安装时间从3分钟缩短到1分钟以内!pip install --no-deps bitsandbytes accelerate xformers==0.0.29 peft trl triton- `--no-deps`: 表示只安装指定的包,不安装它们的依赖包- `bitsandbytes`: 用于模型量化,可以减少显存占用- `accelerate`: Hugging Face的加速训练工具- `xformers`: Facebook开发的Transformer优化库- `peft`: Parameter-Efficient Fine-Tuning的缩写,用于高效微调- `trl`: Transformer Reinforcement Learning,用于强化学习训练- `triton`: 一个编程语言,用于编写高效的GPU代码!pip install --no-deps cut_cross_entropy unsloth_zoo- `cut_cross_entropy`: 优化的交叉熵损失函数实现- `unsloth_zoo`: unsloth的模型库!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer- `sentencepiece`: 用于文本分词- `datasets`: Hugging Face的数据集工具- `huggingface_hub`: 用于与Hugging Face模型库交互- `hf_transfer`: 优化的模型下载工具!pip install --no-deps unsloth这些库构成了一个完整的大语言模型微调环境。使用unsloth可以帮助我们更高效地进行模型微调,它提供了很多优化和加速的特性。小提示:如果安装过程中遇到错误,可以去掉`%%capture`来查看具体的错误信息,这样更容易排查问题。from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit",
max_seq_length = 512,
dtype = None,
load_in_4bit = True,
)
好的,我来详细解释这段代码的作用。这是加载预训练模型的关键步骤,让我们逐行分析:from unsloth import FastLanguageModel- `FastLanguageModel` 是unsloth提供的快速语言模型加载工具- `torch` 是PyTorch深度学习框架,为模型训练提供基础支持model, tokenizer = FastLanguageModel.from_pretrained( model_name = "unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit", - 这里使用的是DeepSeek-R1-Distill-Llama-8B模型 - "unsloth"前缀表明这是经过unsloth优化的版本- `max_seq_length = 512`: - 数据类型设置为None,表示使用模型默认的数据类型 - 相比16位或32位精度,显存占用可以减少4-8倍- `model`: 加载好的语言模型,用于后续的训练和推理- `tokenizer`: 分词器,用于将文本转换为模型可以理解的数字序列2. 第一次运行时会从网络下载模型,可能需要一些时间3. 建议确保有足够的显存(建议至少10GB以上)才运行这段代码model = FastLanguageModel.get_peft_model(
model,
r = 8,
target_modules = ["q_proj", "k_proj", "v_proj"],
lora_alpha = 8,
lora_dropout = 0,
bias = "none",
use_gradient_checkpointing = "unsloth",
random_state = 3407,
)
让我详细解释这段配置PEFT(Parameter-Efficient Fine-Tuning)模型的代码。这是使用LoRA方法进行高效微调的重要设置:1. `FastLanguageModel.get_peft_model()`的作用:- 将普通的语言模型转换为可以进行高效参数微调的版本- 使用LoRA(Low-Rank Adaptation)技术来减少训练参数量- `target_modules = ["q_proj", "k_proj", "v_proj"]`: - 这里只选择了注意力机制中最核心的三个投影矩阵:- `use_gradient_checkpointing = "unsloth"`: - 虽然会稍微降低训练速度,但对于大模型来说很有必要 - 增加target_modules(比如加入"o_proj")3. 这些参数设置体现了"够用就好"的原则,在效果和资源消耗之间找到平衡通过这样的设置,我们可以用很小的计算资源实现模型微调,这对于个人开发者来说特别友好。from datasets import load_dataset
def formatting_prompts_func(examples):
texts = []
for example in examples["text"]:
text = example + EOS_TOKEN
texts.append(text)
return {"text": texts}
dataset = load_dataset("text", data_files={"train": "jud.txt"})
split_dataset = dataset["train"].train_test_split(test_size=0.1, seed=42)
dataset = {
"train": split_dataset["train"],
"validation": split_dataset["test"]
}
我来详细解释这段数据处理的代码。这是准备训练数据的关键步骤:from datasets import load_dataset- `datasets`是Hugging Face提供的数据集处理库def formatting_prompts_func(examples): for example in examples["text"]: text = example + EOS_TOKEN- 在每个文本末尾添加结束符(EOS_TOKEN)dataset = load_dataset("text", data_files={"train": "jud.txt"})- 使用`load_dataset`加载本地文本文件split_dataset = dataset["train"].train_test_split(- `test_size=0.1`表示验证集占总数据的10% "train": split_dataset["train"], "validation": split_dataset["test"]- 包含"train"和"validation"两个部分这段代码为后续的模型训练做好了数据准备工作。好的数据预处理对训练效果至关重要。from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit")
dataset = load_dataset(
"text",
data_files={"train": "jud.txt"},
split="train"
)
split_dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = split_dataset["train"]
val_dataset = split_dataset["test"]
print("数据集列名:", train_dataset.column_names)
print("\n第一条数据样例:", train_dataset[0])
def format_text(examples):
"""处理纯文本数据"""
formatted_texts = []
for text in examples["text"]:
formatted_text = text + tokenizer.eos_token
formatted_texts.append(formatted_text)
return {"text": formatted_texts}
train_dataset = train_dataset.map(
format_text,
batched=True,
remove_columns=train_dataset.column_names
)
val_dataset = val_dataset.map(
format_text,
batched=True,
remove_columns=val_dataset.column_names
)
print("\n处理后的训练集样例:")
print(train_dataset[0]["text"][:200], "...")
print("\n处理后的验证集样例:")
print(val_dataset[0]["text"][:200], "...")
让我详细解释这段数据处理和分词的代码。这是对前面代码的优化和扩展版本:from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("unsloth/DeepSeek-R1-Distill-Llama-8B-unsloth-bnb-4bit") data_files={"train": "jud.txt"},split_dataset = dataset.train_test_split(test_size=0.1, seed=42)train_dataset = split_dataset["train"]val_dataset = split_dataset["test"]print("数据集列名:", train_dataset.column_names)print("\n第一条数据样例:", train_dataset[0])def format_text(examples): for text in examples["text"]: formatted_text = text + tokenizer.eos_token formatted_texts.append(formatted_text) return {"text": formatted_texts}train_dataset = train_dataset.map( remove_columns=train_dataset.column_namesval_dataset = val_dataset.map( remove_columns=val_dataset.column_names- `batched=True`: 启用批处理以提高效率- `remove_columns`: 移除原有列,保持数据结构清晰print(train_dataset[0]["text"][:200], "...")print(val_dataset[0]["text"][:200], "...") - 可以添加cache_file_name参数来缓存处理结果这段代码提供了更完整的数据处理流程,并加入了必要的验证步骤,这对确保训练数据质量非常重要。from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = train_dataset,
eval_dataset = val_dataset,
dataset_text_field = "text",
max_seq_length = 512, # 减小序列长度
dataset_num_proc = 1,
packing = True,
args = TrainingArguments(
per_device_train_batch_size = 4, # 增加batch size
gradient_accumulation_steps = 2, # 减少梯度累积
warmup_steps = 5,
num_train_epochs = 1, # 只训练1轮
learning_rate = 3e-4, # 稍微提高学习率
fp16 = not is_bfloat16_supported(),
bf16 = is_bfloat16_supported(),
logging_steps = 1,
optim = "adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "linear",
seed = 3407,
output_dir = "outputs",
report_to = "none",
),
)
让我详细解释这段配置训练器的代码。这是整个训练过程中最关键的配置部分:from trl import SFTTrainer
from transformers import TrainingArgumentsfrom unsloth import is_bfloat16_supported- `SFTTrainer`: 用于监督微调(Supervised Fine-Tuning)的训练器- `TrainingArguments`: 训练参数配置类- `is_bfloat16_supported`: 检查是否支持bfloat16格式 train_dataset = train_dataset, eval_dataset = val_dataset, dataset_text_field = "text", args = TrainingArguments(...)- `train_dataset`和`eval_dataset`: 训练集和验证集- `dataset_text_field`: 指定数据集中的文本字段名- `max_seq_length = 512`: 限制序列长度,节省显存- `dataset_num_proc = 1`: 数据处理的进程数- `packing = True`: 启用序列打包,提高训练效率3. TrainingArguments详细配置:per_device_train_batch_size = 4, # 每个设备的批次大小gradient_accumulation_steps = 2, # 梯度累积步数num_train_epochs = 1, # 训练轮数learning_rate = 3e-4, # 学习率optim = "adamw_8bit", # 使用8位AdamW优化器weight_decay = 0.01, # 权重衰减率lr_scheduler_type = "linear", # 学习率调度策略fp16 = not is_bfloat16_supported(), # 如果不支持bf16就用fp16bf16 = is_bfloat16_supported(), # 优先使用bf16logging_steps = 1, # 每步都记录日志output_dir = "outputs", # 输出目录report_to = "none", # 不使用外部记录工具 - 如果显存充足,可以适当增加`per_device_train_batch_size` - 如果显存不足,可以增加`gradient_accumulation_steps` - 实际批次大小 = per_device_train_batch_size × gradient_accumulation_steps - `warmup_steps`可以帮助稳定训练初期 - 可以通过减小max_seq_length进一步节省显存这些配置体现了在有限资源下的优化策略,既要保证训练效果,又要确保训练能够顺利进行。
trainer_stats = trainer.train()
print("\n训练统计:")
print(f"总训练时间: {trainer_stats.metrics['train_runtime']:.2f} 秒")
print(f"训练损失: {trainer_stats.metrics['train_loss']:.4f}")
if 'eval_loss' in trainer_stats.metrics:
print(f"验证损失: {trainer_stats.metrics['eval_loss']:.4f}")
final_metrics = trainer.evaluate()
print("\n最终验证集评估结果:")
print(f"验证损失: {final_metrics['eval_loss']:.4f}")
trainer_stats = trainer.train()print(f"总训练时间: {trainer_stats.metrics['train_runtime']:.2f} 秒")print(f"训练损失: {trainer_stats.metrics['train_loss']:.4f}")- `train_runtime`: 整个训练过程耗时- `train_loss`: 训练集上的平均损失值if 'eval_loss' in trainer_stats.metrics: print(f"验证损失: {trainer_stats.metrics['eval_loss']:.4f}")final_metrics = trainer.evaluate()print(f"验证损失: {final_metrics['eval_loss']:.4f}")这段代码是整个训练过程的收尾,通过这些指标可以判断训练是否成功,模型效果如何。建议保存这些指标,方便与其他训练配置进行比较。
alpaca_prompt = """
指令:{0}
输入:{1}
输出:{2}
"""
FastLanguageModel.for_inference(model)
inputs = tokenizer(
[
alpaca_prompt.format(
"",
"三界是什么?",
"",
)
], return_tensors = "pt").to("cuda")
from transformers import TextStreamer
text_streamer = TextStreamer(tokenizer)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 512)
FastLanguageModel.for_inference(model) "", # output - leave this blank for generation! ], return_tensors = "pt").to("cuda")from transformers import TextStreamertext_streamer = TextStreamer(tokenizer) streamer = text_streamer, - `max_new_tokens`: 限制生成的最大token数 - 可以调整max_new_tokens控制输出长度 - 可以设置top_p或top_k参数控制采样策略这段代码展示了如何使用微调后的模型进行实际的文本生成。通过调整各种参数,可以获得不同风格和质量的输出。建议多做实验,找到最适合您需求的设置。