跳至主要内容

微调 Gemma 3:基于金融问答数据集的分步指南

使用金融推理数据集微调全新的 Gemma 模型,以提升其准确性并让模型采纳数据集的答题风格。
· 11分钟 阅读

使用 AI 探索

Google 全新的开源模型家族 Gemma 3,凭借出色的性能迅速走红,可与部分最新的闭源模型相媲美。Gemma 3 引入了多模态能力、强化的推理技能,并支持 140 多种语言。

在本教程中,我们将探索 Gemma 3 的能力,并学习如何在一个金融推理问答数据集上对其进行微调。通过这一微调过程,模型在理解复杂金融问题方面的准确性将大幅提升,并能给出精确且符合语境的回答。

您是首次接触大模型微调吗?别担心!请参考我们通俗易懂的教程 微调 LLM:示例指南,了解微调的工作原理。

您也可以学习课程 Python 中的 LLM 入门,进一步了解 LLM 的运行机制、如何微调以及如何评估其性能。

微调 Gemma 3 特色图片

图片由作者提供

认识 Gemma 3

Gemma 开源模型家族在让前沿 AI 技术触手可及方面实现了重要进步。基于 Gemini 2.0 模型背后的研究与技术构建,Gemma 3 在保持轻量高效的同时,提供了业界领先的性能。

Gemma 3 的参数规模从 10 亿到 270 亿不等,在硬件与性能需求上具备灵活性,使将先进 AI 融入真实应用比以往更容易。

Gemma 3 在同类模型中树立了新的性能标杆,在 LMArena 排行榜的人类偏好评测中,表现优于 Llama3-405B、DeepSeek-V3 和 o3-mini。其轻量化设计并未牺牲算力,开发者在保持高效的同时,亦可获得行业领先的效果。

Gemma 3 模型性能与规模对比

来源: Introducing Gemma 3

Gemma 3 模型的关键特性

  1. 多语言支持: 开箱即用支持 35 种语言,并为 140 多种语言提供预训练能力。
  2. 长上下文窗口: 提供 128k token 的上下文窗口。
  3. 多模态能力: 支持高级的文本、图像和短视频推理。
  4. 结构化输出: 内置函数调用支持。
  5. 量化模型: 提供官方量化版本,降低模型体积与算力需求。
  6. 硬件集成: 兼容 CPU、NVIDIA GPU、Google Cloud TPU 和 AMD GPU。
  7. 与主流工具集成: 可与 Hugging Face Transformers、PyTorch、Keras、JAX、Google AI Edge、vLLM 等工具配合使用。
  8. ShieldGemma 2: 提供针对危险内容、露骨材料和暴力的图像安全检测,确保负责任的 AI 开发。

在金融推理数据集上微调 Gemma 3

在本项目中,我们将从 Kaggle 加载 Gemma 3,并从 Hugging Face 获取数据。随后使用 Transformers 与 TRL 库对模型进行微调。作为对比,我们会在微调前后分别生成回答。

如果您想了解如何使用 Unsloth 库在推理数据上微调模型,请查看 微调 DeepSeek R1(推理模型) 指南。

1. 环境准备

安装所有必需的 Python 库,并确保更新 transformer 库。

%%capture
!pip install -U datasets 
!pip install -U accelerate 
!pip install -U peft 
!pip install -U trl 
!pip install -U bitsandbytes
!pip install git+https://github.com/huggingface/transformers@v4.49.0-Gemma-3

使用您的 API 密钥登录 Hugging Face 客户端。API 密钥安全地存储在 Kaggle Secrets 中,我们将其提取并应用到 Hugging Face 客户端。

from huggingface_hub import login
from kaggle_secrets import UserSecretsClient
user_secrets = UserSecretsClient()

hf_token = user_secrets.get_secret("HUGGINGFACE_TOKEN")
login(hf_token)

2. 加载模型与分词器

将 Gemma 3 4B IT 模型像添加数据集一样添加到 Kaggle 笔记本中,点击“+ Add Input”按钮即可。

在 Kaggle 笔记本中加载模型

使用 transformers 库加载模型与分词器。确保将模型设置为 device_map="auto",以充分利用双 GPU 配置。

from transformers import AutoTokenizer, Gemma3ForConditionalGeneration

import torch

GEMMA_PATH = "/kaggle/input/gemma-3/transformers/gemma-3-4b-it/1"

model = Gemma3ForConditionalGeneration.from_pretrained(
    GEMMA_PATH, device_map="auto",attn_implementation='eager'
).eval()

tokenizer = AutoTokenizer.from_pretrained(GEMMA_PATH)

3. 加载与处理数据集

在加载数据集之前,我们将创建训练提示样式,并提供三个占位符,用数据集的列进行填充。该提示样式将帮助我们生成推理文本。

train_prompt_style="""
Below is an instruction that describes a task, paired with an input that provides further context. 
Write a response that appropriately completes the request. 
Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.

### Question:
{}

### Response:
<think>
{}
</think>
{}
"""

接下来,我们将创建格式化函数,利用数据集中的列并将其应用到训练提示样式中,生成新的 "text" 列。请确保在回答末尾添加 EOS 标记。

def formatting_prompts_func(examples):
    inputs = examples["Open-ended Verifiable Question"]
    complex_cots = examples["Complex_CoT"]
    outputs = examples["Response"]
    texts = []
    for question, cot, response in zip(inputs, complex_cots, outputs):
        # Append the EOS token to the response if it's not already there
        if not response.endswith(tokenizer.eos_token):
            response += tokenizer.eos_token
        text = train_prompt_style.format(question, cot, response)
        texts.append(text)
    return {"text": texts}

现在我们将加载 TheFinAI/Fino1_Reasoning_Path_FinQA 数据集。该数据集基于 FinQA,是一个金融推理数据集,并由 GPT-4o 生成的推理路径加以增强,用于结构化金融问答。随后,我们会将格式化函数应用到数据集,并依据提示样式创建新的文本列。

from datasets import load_dataset
dataset = load_dataset("TheFinAI/Fino1_Reasoning_Path_FinQA", split = "train[0:500]",trust_remote_code=True)
dataset = dataset.map(formatting_prompts_func, batched = True,)
dataset["text"][0]

首条 text 列示例。

新的 STF 训练器不接受分词器,因此我们需要使用分词器创建数据收集器,并在稍后提供给训练器。

from transformers import DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False  # we're doing causal LM, not masked LM
)

4. 微调前的模型推理

在开始微调模型之前,我们将先测试原始模型的回答生成效果。我们将创建仅包含两个占位符的提示样式(而非三个)。

prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. 
Write a response that appropriately completes the request. 
Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.

### Question:
{}

### Response:
<think>
{}
"""

随后,我们将把提示样式应用到问题上,转换为 token 并输入模型。之后生成回答并将 token 转回文本。

question = dataset[0]['Open-ended Verifiable Question']


inputs = tokenizer(
    [prompt_style.format(question, "") + tokenizer.eos_token],
    return_tensors="pt"
).to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=1200,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs, skip_special_tokens=True)
print(response[0].split("### Response:")[1])

该回答较短,且与正确答案相去甚远。

<think>

The question asks for the portion of the estimated amortization expense that will be recognized in 2017.
The provided table shows the estimated amortization expense for intangible assets for the years 2017, 2018, 2019, 2020, 2021, and 2022 and thereafter.
The amortization expense for 2017 is $10,509.
</think>
$10,509

以下是数据集中的答案。答案应为比例,而非金额。

原始问题的正确答案

即便是 ChatGPT 也答错了,我们需要提示它修正错误。之后它才意识到并纠正了答案。

ChatGPT 纠正了其错误。

5. 模型设置

我们现在将把数据集、模型、数据收集器、训练参数以及 LoRA 配置一并提供给 SFTTrainer。

from trl import SFTTrainer
from transformers import TrainingArguments
from peft import LoraConfig

# LoRA Configuration
peft_config = LoraConfig(
    lora_alpha=16,                           # Scaling factor for LoRA
    lora_dropout=0.05,                       # Add slight dropout for regularization
    r=64,                                    # Rank of the LoRA update matrices
    bias="none",                             # No bias reparameterization
    task_type="CAUSAL_LM",                   # Task type: Causal Language Modeling
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],  # Target modules for LoRA
)


# Training Arguments
training_arguments = TrainingArguments(
    output_dir="output",
    per_device_train_batch_size=1,
    per_device_eval_batch_size=1,
    gradient_accumulation_steps=2,
    optim="paged_adamw_32bit",
    num_train_epochs=1,
    logging_steps=0.2,
    warmup_steps=10,
    logging_strategy="steps",
    learning_rate=2e-4,
    fp16=False,
    bf16=False,
    group_by_length=True,
    report_to="none"
)

# Initialize the Trainer
trainer = SFTTrainer(
    model=model,
    args=training_arguments,
    train_dataset=dataset,
    peft_config=peft_config,
    data_collator=data_collator,
)

6. 训练模型

在开始训练过程前清理 CUDA 缓存。

torch.cuda.empty_cache()
trainer_stats = trainer.train()

完成 1 个 epoch 花费了近 1 小时 20 分钟。可以看到,loss 逐步下降。

训练损失逐步下降。

7. 微调后的模型推理

现在,我们像之前一样提供一个示例问题,来测试微调后的模型。

question = dataset[0]['Open-ended Verifiable Question']

inputs = tokenizer(
    [prompt_style.format(question, "") + tokenizer.eos_token],
    return_tensors="pt"
).to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=1200,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs, skip_special_tokens=True)
print(response[0].split("### Response:")[1])

推理部分现已更为详尽且正确,答案也准确无误。

<think>


Alright, let's figure out what portion of the estimated amortization expense will be recognized in 2017. First, I need to see what the total estimated amortization expense is for the years 2017 through 2022. Looking at the numbers, it's $58,370.

Now, I need to find out how much of that is specifically for 2017. The table shows that the estimated amortization expense for 2017 is $10,509.

So, to find out what portion of the total expense is for 2017, I'll divide the 2017 expense by the total expense. That's $10,509 divided by $58,370.

Let's do the math: $10,509 divided by $58,370 gives me approximately 0.1805.

Now, to express this as a percentage, I'll multiply by 100. So, 0.1805 times 100 gives me 18.05%.

Therefore, about 18.05% of the estimated amortization expense for the years 2017 through 2022 will be recognized in 2017.

</think>
About 18.05% of the estimated amortization expense for the years 2017 through 2022 will be recognized in 2017.

我们再来问另一个问题。

question = dataset[10]['Open-ended Verifiable Question']

inputs = tokenizer(
    [prompt_style.format(question, "") + tokenizer.eos_token],
    return_tensors="pt"
).to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=1200,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs, skip_special_tokens=True)
print(response[0].split("### Response:")[1])

如您所见,我们已成功在新数据集上微调模型。它现在回答准确,并遵循了推理与作答风格。

<think>


Alright, let's figure out the total purchase price of Impella Cardiosystems. First, we know the purchase price was $45.1 million, but there's a contingent payment of $5.6 million that could be earned. So, we need to see if we can figure out how much of that $5.6 million is actually likely to be paid.

Now, the question mentions that the contingent payment is triggered by FDA approvals. Since we're dealing with a medical device, getting FDA clearance is pretty important. Let's assume that the FDA approval for the Impella 2.5 is a good sign that the contingent payments will be made.

Okay, so if we assume all the contingent payments are earned, we just need to add the $5.6 million to the original $45.1 million. Let's do the math: $45.1 million plus $5.6 million equals $50.7 million.

Hmm, let's double-check our work. We're adding $45.1 million to $5.6 million, which gives us $50.7 million. Yep, that seems right.

So, if we assume all the contingent payments are earned, the total purchase price of Impella Cardiosystems would be $50.7 million.

</think>
The total purchase price of Impella Cardiosystems, assuming all contingent consideration is earned, would be $50.7 million.

微调一个模型需要专业知识、资金与时间。在某些情况下,您可能只需要更好的 RAG 流水线即可满足需求。请阅读指南 RAG 与微调:含实用示例的全面教程,明确您的需求。

8. 保存模型与分词器

首先,我们将在本地保存模型与分词器。

new_model_online = "kingabzpro/Gemma-3-4B-Fin-QA-Reasoning"
new_model_local = "Gemma-3-4B-Fin-QA-Reasoning"
model.save_pretrained(new_model_local) # Local saving
tokenizer.save_pretrained(new_model_local)

随后,我们会将模型推送到 Hugging Face Hub。

model.push_to_hub(new_model_online) # Online saving
tokenizer.push_to_hub(new_model_online) # Online saving

此过程将先创建模型仓库,然后把模型、分词器与配置等所有文件推送到远程服务器。

Hugging Face 上的 Gemma-3-4B-Fin-QA-Reasoning

来源: kingabzpro/Gemma-3-4B-Fin-QA-Reasoning · Hugging Face

如果您在运行上述代码时遇到问题,我们还准备了一个 Kaggle 笔记本,任何人都可以克隆并自行运行,以更好地理解整个流程。

结语

微调 Gemma 3 模型并非易事;您可能会遇到硬件、库、显存碎片化等问题。本文提供了更简洁的实现思路,帮助您将任意模型转化为推理模型,并尽量规避未来与软硬件相关的问题。

本教程介绍了 Gemma 3 的功能,以及如何利用 Kaggle 上的免费 GPU 资源,将其轻松微调到推理数据集之上。

学习 使用 Llama 3 进行微调 课程,通过 TorchTune 处理微调任务,并学习量化等高效微调技术。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题