如何训练你的大语言模型:使用 Unsloth 进行低秩适配微调!

本文较长,建议点赞收藏。更多AI大模型应用开发学习视频及资料,在智泊AI

大语言模型(LLM)微调就像训练一条龙。图片由ChatGPT根据作者提示生成 训练大语言模型的体验,很像在《驯龙高手》里教"无牙"(Toothless)飞行:"无牙"无疑是条强大的龙,但它少了一只尾翼;同理,基础大语言模型看似功能强大,但若未经训练、无法获取你的领域知识,就容易出现"幻觉"(生成虚假信息),给出不可靠的回复。

我们的期望是什么?希望通过少量微调,让模型学习你的数据------最终在需要时能"精准咬合"(输出符合需求的结果)。

在本文中,我将一步步拆解微调流程:从数据集准备到训练执行,并分享实际操作中的结果。剧透一下:第一次尝试并没有"喷火"般的惊艳效果,但这正是实验最有意思的地方。

所有代码均可在我的GitHub仓库中获取。

一、先聊聊大语言模型微调

这部分内容至关重要,在写一行代码之前,我们先来深入探讨一下。

大语言模型的微调,和其他AI模型的微调截然不同。因为大语言模型在规模和能力上都独树一帜:它们通过海量文本语料训练,能够形成对语言的理解能力。

1. 大语言模型的"理解能力"

随便问ChatGPT西兰花的健康益处,你都会为之惊叹------它的回答不仅快速、准确,还带有惊人的细节丰富度。

大语言模型的能力甚至可以延伸到"推理":尤其当它们在包含"思维链"(Chain-of-Thought, CoT)推理的数据集上训练时,推理能力会更突出。如今,大多数主流大语言模型都融入了这类复杂的训练机制。

不过,大语言模型微调仍是一个存在争议的话题,学术论文中支持与反对的观点并存。一些研究警告,微调可能导致"灾难性遗忘"(模型忘记原有知识)、安全护栏弱化、幻觉风险增加以及隐私问题;更令人担忧的是,在教模型学习特定领域知识时,可能会无意间让它丧失推理能力。尽管有研究提出了缓解这些问题的技术,但持续的争议表明,解决方案并非那么简单。

2. 微调的核心突破:参数高效微调(PEFT)

大语言模型微调领域的一项重大突破,是"参数高效微调"(Parameter-Efficient Fine-Tuning, PEFT)。这是一种足以让人拍案叫绝的"跨时代思路",如今已被快速纳入开源框架,用于生产环境。

大语言模型的规模正在飞速增长。例如,Meta的Llama 4 Behemoth模型据称拥有2万亿个参数,极少有机构能拥有重训这类超大模型的计算资源。此外,为每个下游任务单独存储和部署微调模型的成本极高------因为每个微调模型的规模几乎与原始模型相当。

PEFT的目标就是解决这两个核心问题:参数负担与部署成本,同时也能在一定程度上缓解前文提到的风险(尽管争议表明效果参差不齐)。PEFT技术仅对少量新增参数进行微调,而将预训练模型的大部分参数"冻结"(不更新),这大大降低了计算和存储需求。

3. 低秩适配(LoRA)登场

在各类PEFT方法中,"低秩适配"(Low-Rank Adaptation, LoRA)是目前应用最广泛的一种。它不更新模型的所有权重,而是冻结预训练模型的参数,通过注入可训练的低秩矩阵,实现任务特定的学习。

LoRA的工作原理如下:

  • 确定目标层:LoRA通常应用于线性层(例如Transformer注意力机制中的查询层、键层、值层和输出投影层);
  • 添加低秩矩阵:对于每个冻结的目标权重矩阵W₀,LoRA引入两个更小的矩阵A和B,使得它们的乘积BA具有较低的秩r(r远小于W₀的维度);
  • 训练低秩矩阵:微调过程中,仅更新矩阵A和B的参数,原始权重W₀保持不变,最终的有效权重矩阵为W = W₀ + BA;
  • 推理时可选合并矩阵:训练完成后,可将低秩矩阵BA合并回原始权重矩阵W₀,形成新的权重矩阵W。这意味着,推理时的计算量与全量微调模型完全一致,无需额外开销。

4. LoRA会影响大语言模型的激活值吗?

答案是肯定的。低秩矩阵A和B用于计算原始权重矩阵的更新量(BA);当输入经过LoRA适配层时,输出结果(以及后续的激活值)会与未适配的原始模型产生差异。

本质上,LoRA通过注入可训练的低秩矩阵,修改了大语言模型的有效权重。这些修改后的权重会在推理时产生新的激活模式------且这种模式是针对特定任务定制的,同时还能保持较低的计算和内存成本。这是一种实用、精妙的优化手段,正在重塑大语言模型的实际应用方式。

5. 要不要微调?关键看模型规模

在我看来,微调的收益在"小型大语言模型"上体现得最明显。这类模型通常通过"知识蒸馏"从更大规模的"兄弟模型"中学习:尽管能继承基础的语言理解能力,但深度和广度往往不及大型模型。

但这一点反而可能成为优势。实证研究表明,小型大语言模型的"灾难性遗忘"现象更轻微------微调时覆盖原有预训练知识的风险更低。作为回报,你只需付出远低于大型模型的训练、部署和推理成本,就能获得模型的"领域特定能力"。

那么,"小型"具体指多大?在本文中,我们将微调Llama 3.2 10亿参数模型(Llama 3.2 1bn)------它是目前主流大语言模型中,规模较小但能力足够的代表之一。

二、步骤1:构建训练集与测试集

关于大语言模型微调,有一个常见误区:"训练数据永远不够"。但事实并非如此------因为你完全可以用另一个大语言模型来生成训练数据!

首先,我们来搭建 notebook 环境:

python 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
import nest_asyncio  
nest_asyncio.apply()  
  
from llama_index.core import SimpleDirectoryReader  
from llama_index.core.llama_dataset.generator import RagDatasetGenerator  
from llama_index.llms.ollama import Ollama  
  
# 创建数据目录并下载示例文档(关于"非传统资质"的论文)  
!mkdir  -p ../data  
!wget "https://arxiv.org/pdf/2405.00247.pdf" -O "../data/non_traditional_credentials.pdf"  
  
# 加载文档  
docs = SimpleDirectoryReader("../data/").load_data(show_progress=True)

我们将使用LlamaIndex的RagDatasetGenerator工具来构建数据集,具体代码如下:

ini 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
data_gen = RagDatasetGenerator.from_documents(  
    docs,  
    llm=Ollama("qwen2.5"),  # 使用Qwen 2.5模型生成数据  
    # 生成指令:模拟教师/教授,基于文档上下文生成1个问题及对应答案  
    question_gen_query="You are a teacher/professor. Using the provided context, formulate a single question and its answer",  
    num_questions_per_chunk=10  # 每个文档片段生成10个问题  
)  
# 从文档节点生成问答数据集  
qa_dataset = data_gen.generate_dataset_from_nodes()

💡 实用技巧:生成问答对时,以及评估待微调的小型模型性能时,一定要用更强大(规模更大)的大语言模型。如果这个"强模型"对小型模型的输出满意,就说明你成功打造了一个"小而强"的紧凑模型。

为了避免API调用次数限制和降低成本,我选择使用本地部署的Qwen 2.5 7B模型生成训练数据------它的能力足以支撑数据生成,且无需承担调用云端API的额外开销。

接下来,我们将数据集划分为训练集和保留集(用于后续评估),并将保留集导出为CSV文件:

python 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
from sklearn.model_selection import train_test_split  
from llama_index.core.llama_dataset import LabelledRagDataset  
import json  
import pandas as pd  
  
# 提取所有示例  
all_examples = qa_dataset.examples  
# 按8:2比例划分训练集和测试集(随机种子确保可复现)  
train_examples, test_examples = train_test_split(  
    all_examples,  
    test_size=0.2,          # 20%数据作为保留集  
    random_state=42,        # 随机种子,保证结果可复现  
    shuffle=True  
)  
print(f"训练集样本数:{len(train_examples)},测试集样本数:{len(test_examples)}")  
  
# 构建带标签的RAG数据集  
training_dataset = LabelledRagDataset(examples=train_examples)  
holdout_dataset = LabelledRagDataset(examples=test_examples)  
  
# 将保留集转换为DataFrame并导出为CSV  
records = []  
for ex in holdout_dataset.examples:  
    records.append({  
        "query": ex.query,  
        # 对参考上下文列表进行JSON编码  
        "reference_contexts": json.dumps(ex.reference_contexts),  
        "reference_answer": ex.reference_answer,  
        # 对CreatedBy对象进行JSON编码  
        "query_by": ex.query_by.model_dump_json(),  
        "reference_answer_by": ex.reference_answer_by.model_dump_json(),  
    })  
  
df = pd.DataFrame.from_records(records)  
df.to_csv("holdout_dataset.csv", index=False)  
  
print(f"训练集样本数:{len(train_examples)},测试集样本数:{len(test_examples)}")

最后,我们将训练集序列化为训练所需的JSONL格式:

python 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
def serialize_to_jsonl(examples, out_path="train.jsonl"):  
    """  
    将带标签的RAG数据示例序列化为JSONL格式  
    参数:  
        examples: LabelledRagDataExample列表,每个示例包含.query和.reference_answer字段  
        out_path: JSONL文件的输出路径  
    """  
    def strip_prefix(text):  
        # 移除文本开头的"**Question:**"或"**Answer:**"前缀(若存在)  
        for p in ("**Question:**", "**Answer:**"):  
            if text.strip().startswith(p):  
                return text.strip()[len(p):].strip()  
        return text  
  
    with open(out_path, "w", encoding="utf8") as f:  
        for ex in examples:  
            q_raw = ex.query or ""  
            a_raw = getattr(ex, "reference_answer", None)  
            # 仅序列化包含"Question"前缀且有答案的示例  
            if q_raw.lower().startswith("**question") and a_raw:  
                q = strip_prefix(q_raw)  
                a = a_raw.strip()  
                # 构建符合格式的消息对象  
                obj = {  
                    "messages": [  
                        {"role": "user",      "content": q},  
                        {"role": "assistant", "content": a}  
                    ]  
                }  
                f.write(json.dumps(obj, ensure_ascii=False) + "\n")  
  
# 序列化训练集  
serialize_to_jsonl(train_examples)

大功告成!我们的训练集已经准备好了。

三、步骤2:先评估"未微调"的基础模型性能

在开始微调前,必须先评估基础模型的"原生性能"------这是重要的基准。我们将使用之前保存的保留集,进行一次简单的"检索增强生成"(RAG)评估。

1. 加载保留集(从CSV文件)

python 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
from llama_index.core.llama_dataset import (  
    LabelledRagDataset,  
    LabelledRagDataExample,  
    CreatedBy,  
)  
  
def get_rag_dataset_from_csv(csv_path: str):  
    """从CSV文件加载带标签的RAG数据集"""  
    # 定义CSV列的转换器(处理JSON格式数据)  
    converters = {  
        "reference_contexts":    lambda s: json.loads(s),  
        "query_by":             lambda s: CreatedBy.model_validate_json(s),  
        "reference_answer_by":  lambda s: CreatedBy.model_validate_json(s),  
    }  
    # 读取CSV文件  
    df = pd.read_csv(csv_path, converters=converters)  
    examples = []  
    for _, row in df.iterrows():  
        # 构建LabelledRagDataExample对象  
        examples.append(  
            LabelledRagDataExample(  
                query=row["query"],  
                query_by=row["query_by"],                      # 转换为CreatedBy对象  
                reference_contexts=row["reference_contexts"],   # 转换为字符串列表  
                reference_answer=row["reference_answer"],  
                reference_answer_by=row["reference_answer_by"], # 转换为CreatedBy对象  
            )  
        )  
    # 构建并返回带标签的RAG数据集  
    dataset = LabelledRagDataset(examples=examples)  
    return dataset  
  
# 加载保留集  
holdout_dataset = get_rag_dataset_from_csv("holdout_dataset.csv")

2. 构建RAG引擎

ini 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
from llama_index.embeddings.ollama import OllamaEmbedding  
from llama_index.llms.ollama import Ollama  
from llama_index.core import VectorStoreIndex  
  
# 初始化嵌入模型(用于文本向量转换)  
embed_model = OllamaEmbedding(model_name="nomic-embed-text")  
# 从文档构建向量存储索引  
index = VectorStoreIndex.from_documents(docs, embed_model=embed_model)  
# 构建查询引擎(使用待微调的基础模型Llama 3.2 1B)  
query_engine = index.as_query_engine(  
      similarity_top_k=6,  # 检索Top 6个相似文档片段  
      llm=Ollama("llama3.2:1b")  # 基础模型(后续将微调此模型)  
)

3. 执行RAG评估

ini 复制代码
# ./notebooks/training_dataset_gen.ipynb  
  
from llama_index.core.llama_pack import download_llama_pack  
  
# 下载RAG评估工具包  
RagEvaluatorPack = download_llama_pack("RagEvaluatorPack", "./pack")  
# 初始化评估器  
rag_evaluator = RagEvaluatorPack(  
    query_engine=query_engine,   
    rag_dataset=holdout_dataset,  
    # 使用生成数据集时的同一模型(Qwen 2.5)作为评估裁判  
    judge_llm=Ollama("qwen2.5", request_timeout=120.0),  
    embed_model=OllamaEmbedding(model_name="nomic-embed-text")  
)  
# 运行评估并获取结果  
benchmark_df = rag_evaluator.run()

评估指标说明:

  • • "正确性"(Correctness):评分范围为1-5分;
  • • 其他指标(如相关性、忠实度、流畅度):评分范围为0-1分。

整体来看,基础模型的表现不算差,但"忠实度"(Faithfulness)得分明显偏低------这是一个关键短板。

"忠实度"衡量的是生成回答与检索到的上下文之间的"事实一致性":只有当回答中的每一个主张都能被检索上下文直接支持时,才算"忠实"。忠实度低,意味着模型存在"幻觉"(生成无依据内容)或"过度泛化"(超出上下文范围推断)的问题------这正是我们希望通过微调解决的核心痛点。

四、步骤3:终于到微调环节!

接下来就是最有趣的部分:微调!这里要介绍一个非常出色的框架------Unsloth.ai

Unsloth的核心优势在于,它通过手动推导反向传播步骤,绕开了PyTorch的标准自动求导(autograd)系统。这种优化大幅降低了计算需求,显著加快了训练速度,非常适合在本地或资源有限的硬件上运行。

需要明确的是,Unsloth.ai的核心创新并非"替代训练框架",而是"优化LLM模块"------专门针对PEFT微调场景进行改写。事实上,Unsloth的大多数示例代码(cookbook)都能在免费的Google Colab实例上运行,甚至能微调参数规模达140亿的模型!

实际微调过程中,我们仍会使用Hugging Face的"Transformer强化学习"(TRL)框架------这是一个基于PyTorch的高层抽象框架,支持:

  • • 监督式微调(包括推理密集型数据集);
  • • PEFT(参数高效微调)方法;
  • • 基于强化学习的微调(如GRPO)。

Unsloth与TRL的组合,为资源有限场景下的大语言模型训练提供了高效且灵活的解决方案。

1. 加载数据集与模型

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
  
import json  
from datasets import Dataset  
from unsloth import FastLanguageModel  
import torch  
  
# 加载数据集(添加系统提示词)  
def load_messages_with_system(path, system_content="You are a helpful assistant."):  
    examples = []  
    with open(path, 'r', encoding='utf8') as f:  
        for line in f:  
            obj = json.loads(line)  
            # 构建系统提示词消息  
            sys_msg = {"role": "system", "content": system_content}  
            # 获取用户-助手消息对  
            ua_msgs = obj.get("messages", [])  
            # 组合系统消息与用户-助手消息  
            examples.append({"messages": [sys_msg] + ua_msgs})  
    return examples  
  
# 加载训练集(JSONL格式)  
examples = load_messages_with_system("train.jsonl")  
# 转换为Hugging Face Dataset格式  
dataset = Dataset.from_list(examples)  
  
# 加载模型与分词器(使用Unsloth的FastLanguageModel包装器)  
model, tokenizer = FastLanguageModel.from_pretrained(  
    model_name="unsloth/Llama-3.2-1B-Instruct",  # Unsloth预训练的Llama 3.2 1B模型  
    max_seq_length=2048,  # 长上下文长度(可按需设置)  
    load_in_4bit=True,   # 4位量化,减少内存占用  
    load_in_8bit=False,  # 8位量化(精度略高,但内存占用翻倍)  
    full_finetuning=False,  # 关闭全量微调(启用PEFT)  
)

步骤说明:

  • • 首先将JSONL格式的训练集加载为Hugging Face Dataset格式;
  • • 然后通过Unsloth的FastLanguageModel包装器加载基础模型与分词器------model_name参数对应Unsloth在Hugging Face Hub上发布的模型,可根据需求浏览并选择兼容模型。

2. 配置LoRA参数(启用PEFT)

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
  
model = FastLanguageModel.get_peft_model(  
    model,  
    r=16,  # 低秩矩阵的秩(常用值:8、16、32、64、128)  
    # LoRA适配的目标层(不同模型可能不同,需参考官方文档)  
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",  
                    "gate_proj", "up_proj", "down_proj"],  
    lora_alpha=16,  # LoRA缩放因子  
    lora_dropout=0,  # LoRA dropout率(设为0以优化性能)  
    bias="none",     # 偏置项处理("none"最节省内存)  
    use_gradient_checkpointing="unsloth",  # 使用Unsloth梯度检查点,减少30%显存占用  
    random_state=3407,  # 随机种子,保证可复现  
    use_rslora=False,   # 关闭秩稳定LoRA(按需启用)  
    loftq_config=None,  # 关闭LoftQ量化(按需配置)  
)

通过FastLanguageModel.get_peft_model(),我们将基础模型转换为支持PEFT的模型。其中,target_modules定义了LoRA将适配的层------不同模型的目标层可能不同,建议参考Unsloth官方示例代码,确认对应模型的目标层设置。

3. 格式化训练数据(适配Llama 3.2指令格式)

Llama 3.2的指令微调需要特定的对话格式,示例如下:

sql 复制代码
<|begin_of_text|><|start_header_id|>system<|end_header_id|>  
  
知识截止日期:2023年12月  
当前日期:2025年5月1日  
  
你是一个乐于助人的助手。<|eot_id|><|start_header_id|>user<|end_header_id|>  
  
1+1等于多少?<|eot_id|><|start_header_id|>assistant<|end_header_id|>  
  
2<|eot_id|>

注意:该格式是"模型专属"的,但Unsloth提供了对话模板映射工具,可简化这一步骤:

python 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
  
def formatting_prompts_func(batch):  
    """将对话消息转换为Llama 3.2所需的格式"""  
    convos = batch["messages"]  # 对话消息列表(每个元素是一条对话)  
    # 应用分词器的对话模板  
    texts = [  
        tokenizer.apply_chat_template(convo,  
                                      tokenize=False,  
                                      add_generation_prompt=False)  
        for convo in convos  
    ]  
    return {"text": texts}  
  
# 批量处理数据集(不删除原始"messages"字段)  
dataset = dataset.map(  
    formatting_prompts_func,  
    batched=True,  
)

现在,数据集已完成格式转换,可用于训练!

4. 配置并启动微调

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
import os  
from trl import SFTTrainer, SFTConfig  
  
# 初始化监督式微调(SFT)训练器  
trainer = SFTTrainer(  
    model=model,  
    tokenizer=tokenizer,  
    train_dataset=dataset,  
    eval_dataset=None,  # 可按需配置评估集  
    args=SFTConfig(  
        dataset_text_field="text",  # 数据集中文本字段的名称  
        per_device_train_batch_size=2,  # 单设备训练批次大小  
        gradient_accumulation_steps=4,  # 梯度累积步数(用于模拟更大批次)  
        warmup_steps=5,  # 预热步数  
        max_steps=60,  # 总训练步数(也可注释此参数,设置num_epochs=1)  
        learning_rate=2e-4,  # 学习率  
        logging_steps=1,  # 日志记录步数  
        optim="adamw_8bit",  # 优化器(8位AdamW,节省内存)  
        weight_decay=0.01,  # 权重衰减(防止过拟合)  
        lr_scheduler_type="linear",  # 学习率调度器类型  
        seed=2025,  # 随机种子  
        report_to="none",  # 关闭日志上报(如需使用WandB等工具,可修改此参数)  
    ),  
)  
# 启动训练  
trainer_stats = trainer.train()  
  
# 将微调后的模型推送到Hugging Face Hub  
model.push_to_hub(  
  "tituslhy/retrained_llama32-1bn-finetuned",   
  token=os.environ["HUGGINGFACE_ACCESS_TOKEN"]  # 从环境变量获取Hub令牌  
)   
# 将分词器推送到Hugging Face Hub  
tokenizer.push_to_hub(  
  "tituslhy/retrained_llama32-1bn-finetuned",   
  token=os.environ["HUGGINGFACE_ACCESS_TOKEN"]  
)

训练配置说明:

  • • 微调的核心是SFTConfig:它控制所有训练参数。本文中,我们选择按"固定步数"(60步)训练,而非按"完整轮次"(epoch)------按轮次训练耗时更长,按步数训练更适合快速原型验证;
  • • Hugging Face的TRL框架封装了PyTorch的底层细节(如zero_grad()和自动求导调用),因此只需一行代码配置训练器、一行代码启动训练。

训练启动后,你会看到Unsloth标志性的控制台输出:

ini 复制代码
==((====))==  Unsloth - 2x faster free finetuning | Num GPUs used = 1  
   \\   /|    Num examples = 16 | Num Epochs = 30 | Total steps = 60  
O^O/ \_/ \    Batch size per device = 2 | Gradient accumulation steps = 4  
\        /    Data Parallel GPUs = 1 | Total batch size (2 x 4 x 1) = 8  
 "-____-"     Trainable parameters = 11,272,192/1,000,000,000 (1.13% trained)

没错,输出里有一个树懒图案!🦥 在10亿个参数中,我们仅训练了1127万个参数------这就是LoRA低秩适配的魔力。

训练完成后,可通过model.push_to_hub()将模型推送到Hugging Face Hub,方便后续调用。

5. (可选)推送量化版本模型

如果需要推送量化版本的模型,Unsloth底层使用llama.cpp实现该功能(注意:需确保llama.cpp安装正确,我曾在此步骤遇到过问题,重新安装几次后才解决)。代码如下:

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
model.push_to_hub_gguf(  
    "tituslhy/retrained_llama32-1bn-finetuned",   
    tokenizer,  
    quantization_method=["q4_k_m", "q8_0", "q5_k_m"],  # 量化方式  
    token=os.environ["HUGGINGFACE_ACCESS_TOKEN"],   
)

另外,你可能会注意到控制台输出显示"30轮训练",但我们明明只设置了60步------这是因为训练数据集规模过小:少量样本需要循环多次,才能达到设定的步数。

现在,关键问题来了:这种训练方式到底好不好?让我们通过评估来寻找答案。

五、步骤4:使用Ollama本地部署模型

首先,在Hugging Face Hub上找到你的模型,点击"Use this model",再选择"Ollama"。

你可以直接将页面提供的命令复制到终端运行,也可以稍作修改:将run改为pull,将模型下载到本地Ollama环境中。

六、步骤5:评估微调后的模型

1. 基于RAG的评估

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
  
# 构建新的查询引擎(使用微调后的模型)  
query_engine2 = index.as_query_engine(  
    similarity_top_k=6,   
    llm=Ollama("hf.co/tituslhy/retrained_llama32-1bn-finetuned:Q4_K_M")  
)  
# 初始化新的评估器  
rag_evaluator2 = RagEvaluatorPack(  
    query_engine=query_engine2,   
    rag_dataset=holdout_dataset,  
    # 仍使用Qwen 2.5作为评估裁判(与生成数据集时一致)  
    judge_llm=Ollama("qwen2.5", request_timeout=120.0),  
    embed_model=OllamaEmbedding(model_name="nomic-embed-text")  
)  
# 运行评估(异步方式)  
benchmark_df = await rag_evaluator.arun()

结果令人意外:微调效果并不明显------尽管忠实度得分略有提升(达到0.31/1),但整体仍处于较低水平。

原因可能在于:我们在"RAG模式"下评估微调模型------此时模型依赖外部检索到的文本片段生成答案,而非仅依靠自身内化的知识;但大语言模型微调的核心目标,是让模型"内化领域知识",而非"提升使用外部上下文的能力"。

2. 直接评估模型(无检索,仅依赖内化知识)

因此,我们需要换一种正确的评估方式:不进行检索,让模型直接回答问题,仅依靠微调时学到的领域知识。

ini 复制代码
# ./notebooks/finetune_llama32_1bn.ipynb  
  
from llama_index.llms.ollama import Ollama  
from llama_index.embeddings.ollama import OllamaEmbedding  
from llama_index.core.evaluation import SemanticSimilarityEvaluator  
from tqdm import tqdm  
  
# 初始化微调后的模型与嵌入模型  
llm = Ollama("hf.co/tituslhy/retrained_llama32-1bn-finetuned:Q4_K_M")  
embed_model = OllamaEmbedding(model_name="nomic-embed-text")  
  
# 读取保留集(处理JSON格式字段)  
converters = {  
        "reference_contexts":   lambda s: json.loads(s),  
        "query_by":             lambda s: CreatedBy.model_validate_json(s),  
        "reference_answer_by":  lambda s: CreatedBy.model_validate_json(s),  
    }  
df = pd.read_csv("holdout_dataset.csv", converters=converters)  
  
# 初始化语义相似度评估器(相似度阈值设为0.5)  
evaluator = SemanticSimilarityEvaluator(  
    similarity_threshold=0.5,   
    embed_model=embed_model  
)  
  
# 筛选出包含"Question"的有效查询与参考答案  
queries = [df.iloc[i]['query'] for i in range(len(df)) if "Question" in df.iloc[i]['query']]  
references = [df.iloc[i]['reference_answer'] for i in range(len(df)) if "Question" in df.iloc[i]['query']]  
# 构建DataFrame存储查询与参考答案  
df_answers = pd.DataFrame({'queries': queries, 'reference_answers': references})  
  
# 存储模型回答与相似度得分  
answers, similarity_scores = [], []  
# 遍历所有查询,让模型直接回答  
for idx, (query, reference) in tqdm(df_answers.iterrows()):  
    answer = llm.complete(query)  
    answers.append(str(answer))  
      
    # 计算模型回答与参考答案的语义相似度  
    similarity_score = evaluator.evaluate(  
        response=str(answer),  
        reference=reference  
    )  
    similarity_scores.append(round(similarity_score.score, 2))  
  
# 将结果添加到DataFrame  
df_answers['answers'] = answers  
df_answers['similarity_scores'] = similarity_scores

3. 评估结果分析

我们来拆解这段代码的逻辑:

  1. 初始化微调后的模型与嵌入模型;
  2. 加载保留集,筛选出有效查询(包含"Question"的条目);
  3. 让模型不依赖任何检索上下文,直接回答每个查询;
  4. 计算模型回答与参考答案之间的语义相似度(得分范围0-1)。

从相似度得分来看,似乎有了提升!但别急着下结论------我们需要仔细阅读模型的回答内容,而非仅看分数。

问题很快浮现:以第一个问题为例,模型的回答非常冗长,而参考答案只是一句话。这种"高分"其实具有误导性------更长的回答包含更多token和信息点,自然更容易与参考答案产生语义重叠。

更深入分析"得分最高"的回答后,发现情况更不乐观。例如,有一个非常模糊的查询:"Which study are we referring to here?"(我们这里指的是哪项研究?)------这个问题本身表述不明确、信息不完整。

从表面上看,模型似乎学到了文档中的一些信息------即使面对模糊的提示,也给出了连贯的回答。但问题在于:我们从未在训练数据中提及"该研究的名称",因此这个回答很可能是"幻觉"。

👉 我将这种情况称为"假阳性":回答看起来正确,但实际上基于假设或虚构的上下文。

七、实验反思

总体而言,这次微调实验不能算"成功",但却带来了宝贵的经验。以下是几点关键启示:

  1. 数据集质量至关重要 我们的训练集仅包含49个示例,且没有对数据质量进行校验(只是盲目相信大语言模型生成的问答对)。实际场景中,这类数据往往存在"What format is this document?"(这是什么格式的文档?)这类无意义问题,需要手动剔除。正因如此,生成训练数据集时,务必使用性能优异的大语言模型!
  2. 数据集规模同样关键 样本量过少时,微调很可能是"错误选择"------我们的实验就出现了性能未达预期的情况。更优的路径(我将在下一篇文章中探讨)是"微调整个RAG系统",敬请关注!
  3. 训练配置决定结果成败 理想的训练配置应包含:
  • 单独的评估集(同样需要精心筛选或生成);
  • 多组超参数实验(测试不同参数组合的效果);
  • 借助Weights & Biases或MLflow等工具进行完整的训练跟踪。

但在本次实验中,我们仅进行了一次"一次性训练",且几乎没有跟踪记录------悄悄说一句,可别让我的教授知道这事。

  1. 微调比想象中简单,归功于开源社区 我们能轻松完成这次实验,离不开开源社区的贡献。Unsloth.AI、Hugging Face和Ollama等工具,将曾经复杂且高门槛的大语言模型微调,变得如今触手可及。没有这些出色的工具,本次实验的代码编写和运行会困难得多。

虽然我们没能成功"教无牙学会咬合"(让模型精准输出),但这次实验仍是一次宝贵的"大语言模型微调入门"------我们不仅学到了大量知识,还得到了可复用的代码,而不仅仅是复制Unsloth的示例。

目前,Hugging Face和Kaggle上有许多高质量的领域数据集可供使用。相信只要优化数据质量、设计更合理的训练方案,我们一定能构建出更精准、更可靠的微调模型。

学习资源推荐

如果你想更深入地学习大模型,以下是一些非常有价值的学习资源,这些资源将帮助你从不同角度学习大模型,提升你的实践能力。

本文较长,建议点赞收藏。更多AI大模型应用开发学习视频及资料,在智泊AI

相关推荐
wangruofeng8 小时前
姚顺雨长谈:在 Anthropic 和 Gemini 训练模型,英雄主义已经过时
llm·aigc
小林ixn8 小时前
Workflow 还是 Agent?帮你一次性搞清楚这俩到底有啥不一样
agent·workflow
Darling噜啦啦8 小时前
Workflow 还是 Agent?一文搞懂 AI 工程的两大核心范式,以及它们的未来
agent·workflow
沉默王二10 小时前
腾讯一面,我霸气反问:“你说你们在做Agent项目,说说 SubAgent、Plan 模式、Skill 调用这些你们都是怎么做的?”面试官一直在擦汗。。
面试·agent·ai编程
新知图书12 小时前
10.1 项目背景与需求分析(智能客服智能体开发)
人工智能·agent·ai agent·智能体·扣子
赵康12 小时前
AI 写代码之后,Code Review 会议怎么开
ai·llm·skill
程序员cxuan13 小时前
Grok Build 被众人唾骂,结果老马把它开源了
人工智能·后端·程序员
莫逸风13 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
决战灬13 小时前
langgraph之interrupt(理论篇)
人工智能·python·agent
人间凡尔赛15 小时前
2026 AI Agent 多智能体协同编程实战指南:从 Kimi K3 到 GPT-5.6 深度解析
ai·agent·多智能体·编程实战·waic·gpt-5.6·kimik3