(这篇帖子主要是自己留个备份。想听我随便聊只看前面就行。想复制代码就直接拉到后面。)
两年前,我微调过当时开源的一些大语言模型。用阿毗达磨数据。因为,大语言模型在阿毗达磨问题上的表现总是很差。后来,每出一种更强劲的模型,我总会从写作和阿毗达磨两个方面去测试它。在文学写作上,最先达到我认为勉强可用的模型是
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 AutoTokenizer
tokenizer = 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_names
val_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 TrainingArguments
from unsloth import is_bfloat16_supported