废话不多说,本文就一个目的:让普通的消费级电脑实现微调大模型的全过程

让所有爱好AI的玩家过把瘾!!!不用流着口水眼看着别人在高配GPU服务器上微调大模型,你也行!

200行代码,让你轻松玩转大模型微调!

让我们直奔主题!

1、现实意义与实现路径

现实意义:我现在教你的是如何在消费级个人PC上实现完整的大模型微调过程,掌握了此方法,你也就掌握了训练其他大模型的标准方法,彻底撕开大模型微调的神秘面纱。

实现路径:使用较小参数量的模型+较小数据集进行完整过程模拟。

2、主题干货:200行微调代码全解析

2.1 基础模型选择与数据集选择:

目标选择 名称 说明及选择原因
模型名称 distilbert/distilgpt2 • Hugging Face 模型库中的 distilgpt2
• 约 82M 参数,模型权重文件大约 330MB
• 是 GPT-2 的一个精简版本,通过知识蒸馏(distillation)从更大的 GPT-2 模型压缩而来。它保留了 GPT-2 的核心能力,但计算需求和内存占用显著降低,非常适合在个人电脑上微调。
数据集名称 Trelis/tiny-shakespeare 这个数据集包含莎士比亚作品的一个小型子集,是目前已知的最小且仍然有意义的语言训练数据集之一,数据量很小,只有1.34M, 521条记录。

注意:本文所有模型与数据集均来自HuggingFace。

2.2 工具与环境

Anaconda3 + PyCharm2024

Anaconda的安装此处没必要说了,此处直接略过,不会的朋友直接知乎即可。

编译环境与核心依赖包:

Python: 3.9.21

torch 2.6.0+cu118

pytorch 2.3.1 (py3.9_cuda11.8_cudnn8_0)

CUDA版本:11.8

huggingface_hub 0.28.0

有独立显卡的朋友,请通过命令行(nvidia-smi)查看自己cuda的版本,并安装相应的依赖包,这一点很重要,以免出现不兼容的情况导致微调代码无法运行:

比如我机器上的cuda版本是11.8,那么就需要在conda命令行安装对应的依赖包(以下命令行只需要执行一条,选择对应的cuda版本即可):可参考官方:pytorch.org/get-started

# CUDA 11.8
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=11.8 -c pytorch -c nvidia
# CUDA 12.1
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 pytorch-cuda=12.1 -c pytorch -c nvidia
# CPU Only
conda install pytorch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 cpuonly -c pytorch

2.3 import

import os
import torch
from datasets import load_dataset, DatasetDict
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    DataCollatorForLanguageModeling,
    TrainerCallback,
    BitsAndBytesConfig, Trainer
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
from config.config import TARGET_MODEL, DATASET_NAME
import matplotlib.pyplot as plt

2.4 配置8 bit量化与训练策略

8bit量化后,会进一步减少对设备性能的消耗。

# 配置 8 位量化
quantization_config = BitsAndBytesConfig(
    load_in_8bit=True,
    llm_int8_threshold=6.0,
)

# 自定义 device_map
device_map = {
    "": 0  # 将模型的所有层都映射到 GPU 0
}

# 加载 tokenizer 和模型
tokenizer = AutoTokenizer.from_pretrained(TARGET_MODEL)
tokenizer.padding_side = 'right'
tokenizer.pad_token = tokenizer.eos_token  # 添加这一行,设置 pad_token
model = AutoModelForCausalLM.from_pretrained(
    TARGET_MODEL,
    quantization_config=quantization_config,
    device_map=device_map
)

要点:

1)如果机器没有GPU,那么需要加上判断:

# 检查是否有可用的 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载模型时,指定设备
model = AutoModel.from_pretrained("bert-base-uncased")
model.to(device)

2)TARGET_MODEL表示config文件中定义的数据集文件路径,后续会给出config文件代码

2.5 配置Lora

这一步很关键, 这段代码使用了 peft 库中的 LoraConfig 来配置 LoRA(Low-Rank Adaptation)技术,用于在微调大型语言模型时高效调整模型参数,它非常高效、资源占用低,是微调玩家的首选。

# 配置 LoRA
config = LoraConfig(
    r=8,  # LoRA 秩
    lora_alpha=16,
    # 只适合Llama模型
    # target_modules=["q_proj", "v_proj"],
    # gpt类模型的注意力权重层;q/k/v 的联合投影
    target_modules=["c_attn"],
    lora_dropout=0.1,
    bias="none",
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)

2.6 加载数据集

接下去我们就直接加载数据集:DATASET_NAME也将定义在单独的config.py文件中,用来指定数据集的名称。

# 加载数据集
dataset = load_dataset(DATASET_NAME,  trust_remote_code=True)

要点:trust_remote_code=True这个参数有时候非常必要,它允许 load_dataset 执行数据集仓库中提供的自定义 Python 脚本,适用于需要动态生成或复杂处理的数据集。有些数据集必须要加这个参数,否则无法加载。此处作为一个知识点,对于本例来说,其实可以直接删掉这个参数。

2.7 预处理数据集:测试集与验证集

加载数据集之后,需要将数据集进行拆分,90%作为训练集,10%作为测试或验证集,对于 distilgpt2 和 tiny_shakespeare,无需复杂的指令格式,直接使用原始文本进行训练即可。所以train_prompt_style 直接设置为空即可:

# 检查是否有验证集,若没有则手动划分
if 'validation' not in dataset:
    # 这是 datasets 库中的一个方法,用于将数据集划分为训练集和测试集。
    # test_size=0.1:表示将数据集的 10% 分配给测试集(test),剩下的 90% 作为训练集(train)
    # shuffle = True:   表示在划分之前对数据进行随机打乱,以确保数据分布的随机性。
    # 设置随机种子,确保每次运行代码时划分结果的一致性。
    split_dataset  = dataset["train"].train_test_split(test_size=0.1, shuffle=True, seed=42)
    dataset["train"] = split_dataset ["train"]
    # 将重新划分后的测试集 dataset["test"] 重命名为验证集 dataset["validation"],以便在后续代码中明确区分训练集和验证集的用途。
    dataset["validation"] = split_dataset ["test"]


train_prompt_style = None  # 或空字符串 "",不再使用复杂的指令模板

# 轻量级数据集只有一个text字段
class DataPreprocessor:
    def __init__(self, tokenizer, prompt_template):
        self.tokenizer = tokenizer
        self.prompt_template = prompt_template  # 保留,但 tiny_shakespeare 未直接使用

    def __call__(self, examples):
        texts = examples["Text"]  # 直接使用 tiny_shakespeare 的 text 字段
        tokenized = self.tokenizer(
            texts,
            padding="longest",
            truncation=True,
            max_length=512,
            return_tensors="pt",
        )
        tokenized["labels"] = tokenized["input_ids"].clone()
        return tokenized


# 批量预处理
preprocessor = DataPreprocessor(tokenizer, train_prompt_style)

继续设置训练集和验证集的批处理参数:

# 预处理数据集
tokenized_dataset = DatasetDict()

# 对训练集进行预处理
tokenized_dataset["train"] = dataset["train"].map(
    preprocessor,
    batched=True,
    batch_size=100,
    remove_columns=dataset["train"].column_names
)

# 对验证集进行预处理
tokenized_dataset["validation"] = dataset["validation"].map(
    preprocessor,
    batched=True,
    batch_size=10,
    remove_columns=dataset["validation"].column_names
)

print(tokenized_dataset["train"][:5])
# 数据收集器 创建一个数据整理器,用于在训练 model 时处理 tokenized_dataset 中的批量数据,生成适合因果语言建模的输入和标签。
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

2.8 训练参数设置

核心配置,各参数解释见代码后附表:

# 训练参数
training_args = TrainingArguments(
    output_dir="./results",
    overwrite_output_dir=True,
    num_train_epochs=8,
    per_device_train_batch_size=8,
    gradient_accumulation_steps=4,
    per_device_eval_batch_size=8,
    save_steps=400,
    save_total_limit=2,
    prediction_loss_only=True,
    fp16=True,
    bf16=False,
    logging_steps=50,  # 日志记录步数, 大数据集
    eval_steps=200,  # 评估步数
    evaluation_strategy="steps",
    warmup_ratio=0.1,
    lr_scheduler_type="linear",
    learning_rate=5e-5,  # 设置学习率
    load_best_model_at_end=True,
    metric_for_best_model="eval_loss"
)

训练参数详解:

参数 作用 与任务的相关性
output_dir “./results” 指定保存模型和日志的目录 保存微调后的 distilgpt2 和检查点
overwrite_output_dir True 覆盖已有输出目录 方便重复实验
num_train_epochs 8 训练总轮数 可能导致 tiny_shakespeare 过拟合,建议减少到 3–5
per_device_train_batch_size 8 每个设备的训练批量大小 适合小数据集,但需检查显存,建议减小到 4 或 2
gradient_accumulation_steps 4 梯度累积步数,增大等效批量大小 有效批量 32,可能过大,建议减少到 1 或 2
per_device_eval_batch_size 8 每个设备的评估批量大小 适合验证集,需检查显存
save_steps 400 每隔 400 步保存检查点 合理,但可调为 1,000 或按总步数比例
save_total_limit 2 限制保存的检查点数量,最多保留 2 个 节省存储空间
prediction_loss_only True 仅计算预测损失,减少计算开销 适合因果语言建模,加快训练
fp16 True 启用 FP16 混合精度训练 适合 GPU 训练,需硬件支持,如果没有GPU,则改为False。
bf16 False 禁用 BF16 混合精度训练 适合非 BF16 硬件
logging_steps 50 每隔 50 步记录日志 较密,对于大数据集,d建议增至 100 或 500
eval_steps 200 每隔 200 步在验证集上评估 较疏,对于大数据集,建议减至 50 或 100
evaluation_strategy “steps” 按步数触发评估 适合小数据集
warmup_ratio 0.1 学习率预热 10% 步数 合理,但可试 0.05 或移除
lr_scheduler_type “linear” 学习率线性衰减 适合小模型,简单有效
learning_rate 5e-5 初始学习率 大数据集建议调整为 2e-5,防止过拟合
load_best_model_at_end True 训练结束加载验证性能最佳模型 防止过拟合,适合小数据集
metric_for_best_model “eval_loss” 选择最佳模型的指标为验证损失 适合因果语言建模

2.9 训练器设置

# 更新 PlotLossCallback
plot_callback = PlotLossCallback()

trainer = SFTTrainer(
    model=model,
    train_dataset=tokenized_dataset["train"],
    eval_dataset=tokenized_dataset["validation"],
    peft_config=config,
    tokenizer=tokenizer,
    args=training_args,
    data_collator=data_collator,
    callbacks=[plot_callback]
)


# 开始训练
trainer.train()

# 保存 LoRA 权重
model.save_pretrained("./fine_tuned_model")
参数名称 类型 描述
model PreTrainedModel 要训练的基础模型,通常是一个预训练的大型语言模型。
train_dataset Dataset 训练数据集,通常是一个分词后的数据集,包含输入和输出对。
eval_dataset Dataset 验证数据集,用于评估模型在训练过程中的性能。
peft_config PEFTConfig 高效微调(Parameter Efficient Fine-Tuning)配置,用于指导微调策略。
tokenizer PreTrainedTokenizer 用于分词的工具,将文本转换为模型可以理解的输入格式。
args TrainingArguments 训练参数,包括批量大小、学习率、训练周期等。
data_collator DataCollator 数据合并器,用于将多个样本合并成一个批次。
callbacks list of TrainerCallback 训练过程中的回调函数,用于监控训练进度、记录指标等。

3.0 图形化指标展示

在2.9的代码片段中,定义了一个回调类(PlotLossCallback),用于记录训练过程指标,训练结束后,将以图形化方式展示:

class PlotLossCallback(TrainerCallback):
    def __init__(self):
        self.train_losses = []
        self.eval_losses = []
        self.step_count = 0

    def on_log(self, args, state, control, logs=None, **kwargs):
        self.step_count += 1
        if logs is not None:
            if 'loss' in logs:
                self.train_losses.append(logs['loss'])
                print(f"Step {self.step_count}, Training Loss: {logs['loss']}, Train Losses: {self.train_losses}")
            if 'eval_loss' in logs:
                self.eval_losses.append(logs['eval_loss'])
                print(f"Step {self.step_count}, Validation Loss: {logs['eval_loss']}, Eval Losses: {self.eval_losses}")

    def on_train_end(self, args, state, control, **kwargs):
        print(f"Final Train Losses: {self.train_losses}")
        print(f"Final Eval Losses: {self.eval_losses}")
        if not self.train_losses:
            print("Warning: No training losses recorded!")
        if not self.eval_losses:
            print("Warning: No validation losses recorded!")
        plt.figure(figsize=(12, 6))
        if self.train_losses:
            plt.plot(self.train_losses, label='Training Loss')
        if self.eval_losses:
            plt.plot(self.eval_losses, label='Validation Loss')
        plt.xlabel('Steps')
        plt.ylabel('Loss')
        plt.title('Training and Validation Loss')
        plt.legend()
        plt.grid(True)
        plt.ylim(0, 5)
        plt.savefig('training_loss.png')
        plt.show()

3.1 执行训练

到此为止,训练代码已经解析完毕,接下去你要做的,只需要点击运行代码,然后优雅地欣赏控制台输出的训练过程。

笔者机器配置是:8G显存,32G物理内存,最终2分钟就完成了训练; 我估计16G内存的机器训练时间也不会超过5分钟。

最后,你将看到类似的plot图形:

由于数据集过小且分布不均匀,训练步数也很少,导致training loss无法收敛。不过这已经不重要了,重要的是我们已经实现了大模型的全程微调

好了,现在你已经借用一根树枝学会了六脉神剑的招式,那么换一把倚天剑,你还会忘记招式吗?

有任何问题,请评论区留言或点击咨询。

需要完整源码的请关注并在评论区留下邮箱地址。