Stay Hungry,Stay Foolish!

Introducing Gemma 3 270M: The compact model for hyper-efficient AI

Introducing Gemma 3 270M: The compact model for hyper-efficient AI

https://developers.googleblog.com/en/introducing-gemma-3-270m/

https://zhuanlan.zhihu.com/p/1985797288883405826

https://zhuanlan.zhihu.com/p/1939694335269507103

 

trl + unsloth

https://github.com/thomas-chong/fine-tuning-gemma-with-unsloth/tree/main

 

 

🚀 谷歌推出 Gemma 3 270M:超高效 AI 模型

2025年8月14日,谷歌在其开发者博客上正式发布了 Gemma 3 270M。这是一款专为特定任务微调而设计的紧凑型 AI 模型,旨在提供强大的指令遵循能力和极高的能效。

🌟 核心亮点

  • 小巧而强大:模型仅包含 2.7亿 参数,但具备出色的指令遵循和文本结构化能力。它拥有 25.6 万的大词表,能更好地处理特定和罕见词汇,是进行特定领域和语言微调的坚实基础。
  • 极致能效:能效表现突出。在 Pixel 9 Pro 芯片上的内部测试显示,INT4 量化模型进行 25 次对话仅消耗 0.75% 的电量,是 Gemma 系列中能效最高的模型。
  • 生产就绪:提供 量化感知训练 (QAT) 的检查点,支持以 INT4 精度运行,在性能损失极小的情况下,非常适合在资源受限的设备上部署。

🛠️ 适用场景

Gemma 3 270M 遵循“合适的工具做合适的事”的理念,特别适合以下场景:

  • 高容量、定义明确的任务:如情感分析、实体提取、文本分类、数据提取和合规性检查等。
  • 对成本和延迟敏感的应用:可大幅降低推理成本,实现快速响应,甚至可在轻量级设备或本地运行。
  • 需要快速迭代和部署:模型体积小,微调实验速度快,可在数小时内找到最佳配置。
  • 注重用户隐私:支持完全在设备端运行,无需将敏感数据发送到云端。
  • 构建专用模型集群:可以低成本构建和部署多个针对不同任务优化的专用小模型。

🚀 快速上手

开发者可以通过多种途径获取和使用 Gemma 3 270M:

  1. 下载模型:可从 Hugging Face、Ollama、Kaggle 等平台下载预训练和指令微调版本。
  2. 尝试模型:支持在 Vertex AI 以及 llama.cpp、Gemma.cpp、LiteRT 等多种推理工具上运行。
  3. 开始微调:可使用 Hugging Face、UnSloth、JAX 等工具进行微调。
  4. 部署方案:微调后的模型可部署在本地环境或 Google Cloud Run 等平台上。

 

 

https://unsloth.ai/docs/get-started/fine-tuning-llms-guide

这篇 Unsloth 的 LLM 微调指南是一份面向初学者的教程,介绍了使用其工具高效微调大语言模型(LLM)的完整流程。

以下是该指南的核心内容总结:

🧠 什么是微调 (Fine-tuning)

微调是在预训练模型的基础上,通过特定数据集进行再训练,以定制模型行为、注入新知识并优化其在特定任务上的表现。

  • 主要方法:监督式微调 (SFT) 是最常用的方法。此外还包括基于人类反馈的强化学习 (RLHF),如 DPO、ORPO、GRPO 等。
  • 核心优势:微调可以更新模型知识、定制其语气和风格,并优化特定任务的准确性。与 RAG(检索增强生成)不同,微调直接改变模型权重,使其成为更专业的“智能体”。
  • LoRA/QLoRA:这是两种高效的微调技术。它们不改变模型的全部权重,而是通过训练少量新增的“适配器”参数来实现。QLoRA 在 LoRA 的基础上引入了 4-bit 量化,能大幅节省显存,让在资源有限的设备上微调大模型成为可能。

🚀 微调的 8 个步骤

指南将微调过程分解为 8 个清晰的步骤:

  1. 理解微调:了解其基本概念、优势以及与 RAG 的区别。
  2. 选择模型与方法
    • 模型:建议初学者从较小的指令模型(如 Llama 3.1 8B)开始。
    • 方法:推荐从 QLoRA 开始,因为它资源消耗低且效果出色。LoRA 是另一种高效选择,而全参数微调 (FFT) 计算成本高昂,通常不必要。
  3. 准备数据集:数据质量至关重要。通常需要准备问答(QA)对格式的数据。可以使用工具合成数据,并对数据进行清洗和结构化处理。
  4. 理解超参数:学习如何设置影响训练效果的关键参数,如学习率 (learning_rate)、批次大小 (batch_size) 和训练步数 (max_steps)。
  5. 安装与环境:可以通过 Unsloth 提供的免费 Notebook(如 Colab、Kaggle)快速上手,也可以在本地通过 pip 或 Docker 安装。
  6. 训练与评估
    • 训练:运行训练脚本,并观察训练损失(Training Loss)。通常损失值在 0.5-1.0 之间是较好的迹象。
    • 评估:可以通过与模型对话进行手动评估,或设置验证集进行自动评估。
  7. 运行与部署
    • 推理:训练完成后,可以直接加载模型进行对话测试。
    • 保存:模型可以保存为小巧的 LoRA 适配器文件(约 100MB),也可以合并后保存为完整模型。
    • 部署:提供了多种部署方案,如转换为 GGUF 格式用于 llama.cppOllama,或使用 vLLM 进行高性能服务部署。
  8. 完成:成功完成模型的微调与部署。

此外,Unsloth 还推出了一个名为 Unsloth Studio 的开源 Web UI,允许用户通过图形界面进行无代码的模型微调和监控。

 

https://zhubert.com/intro-to-transformers/fine-tuning-a-transformer/sft-introduction/

这篇网页主要介绍了监督微调(Supervised Fine-Tuning, SFT),这是让大模型学会“听懂人话”并遵循指令的关键步骤。

简单来说,如果把预训练(Pre-training)比作让模型“博览群书”来学习语言和知识,那么 SFT 就是给模型上“行为训练课”,教它如何根据指令给出恰当的回复。

以下是该网页内容的思维导图:

以下是详细总结:

🎯 一、核心概念与数学原理

1. 什么是监督微调(SFT)

SFT 的核心在于“监督学习”,即向模型展示正确的输入 - 输出对(类似于使用抽认卡教学)。其目的是通过示例让模型学会如何回答问题或编写代码。

2. 数学公式

 

  • :指令或提示(如“法国的首都是哪里?”)。
  • :完整的回复(如“法国的首都是巴黎。”)。
  • 目标:最小化负对数概率,即最大化模型预测正确 token 的概率。

⚖️ 二、SFT 与预训练的区别

虽然两者都是训练过程,但存在本质差异:

方面预训练 (Pre-Training)监督微调 (SFT)
数据 来自各处的原始文本(书籍、网站等) 精心策划的(指令,回复)对
目标 预测任何文本中的下一个 token 生成对指令有帮助的回复
规模 万亿级 token 数千到数百万个样本
成本/时长 昂贵(数周/月,巨大 GPU 集群) 相对便宜(数小时/天,单 GPU)
学习率 较高(约 1e-4) 较低(1e-5 到 3e-4)
本质 教模型语言(词汇、语法、事实) 教模型行为(如何回应、格式化)

📚 三、主流 SFT 数据集

网页介绍了三个推动开源指令跟随运动的数据集:

  • Alpaca (Stanford, 2023):包含 52,000 条指令。由 GPT-3.5 基于种子示例生成。虽然有时略显公式化,但成本低(Stanford 用不到 600 美元训练了 LLaMA 模型)。
  • Dolly (Databricks, 2023):包含 15,000 条指令。由 Databricks 员工人工编写,质量更高、更自然,但规模较小。
  • OpenAssistant (LAION, 2023):包含 161,000 条消息。通过网页界面由社区贡献,包含多轮对话和人类质量评分。

🛠️ 四、SFT 训练循环步骤

实施 SFT 的具体流程如下:

  1. 加载预训练模型:使用已有的模型(如 GPT-2, LLaMA),无需从头训练。
  2. 格式化指令数据:使用聊天模板(Chat Template)添加特殊 token(如<|user|>)将数据转换为模型可理解的格式。
  3. Token 化:将文本转换为数字(token IDs)。
  4. 应用 Loss Masking(关键技巧):仅计算回复部分 token 的损失,忽略指令部分的 token。这样模型只学习如何生成好的回复,而不是学习预测指令。
  5. 训练:使用标准的交叉熵损失进行监督学习。
  6. 保存模型:获得一个经过微调、能遵循指令的模型。

这篇文章清晰地界定了预训练与微调的边界,特别是 Loss Masking 的引入,解释了为何模型能在保留原有知识的同时,学会特定的交互行为。

 

https://github.com/huggingface/trl

TRL 是一个由 Hugging Face 维护的开源库,专门用于使用强化学习(RL)技术对 Transformer 语言模型进行后训练(post-training)。

🚀 核心功能

TRL 提供了多种先进的后训练方法,旨在优化基础模型的性能和对齐能力。其主要特点包括:

  • 多种训练器 (Trainers): 提供了多种易于使用的训练器,以支持不同的训练策略,例如:
    • SFTTrainer: 用于监督式微调 (Supervised Fine-Tuning)。
    • GRPOTrainer: 实现了组相对策略优化 (Group Relative Policy Optimization),一种比 PPO 更节省内存的算法。
    • DPOTrainer: 实现了直接偏好优化 (Direct Preference Optimization),一种流行的对齐算法。
    • KTOTrainer: 实现了卡尼曼-特沃斯基优化 (Kahneman-Tversky Optimization),用于处理二元反馈。
    • RewardTrainer: 用于训练奖励模型。
  • 高效与可扩展:
    • 与 🤗 Accelerate 集成,支持从单 GPU 到多节点集群的分布式训练(如 DDP, DeepSpeed)。
    • 与 🤗 PEFT 完全集成,支持通过量化和 LoRA/QLoRA 等技术,在有限的硬件资源上训练大型模型。
    • 集成了 🦥 Unsloth 库,通过优化的内核加速训练过程。
  • 命令行界面 (CLI): 提供了一个简单的命令行工具,无需编写代码即可快速开始模型的微调。

🛠️ 快速开始

你可以通过 pip 安装 TRL,并使用其提供的训练器快速上手。

安装

pip install trl

使用示例
以下是一个使用 SFTTrainer 进行监督式微调的简单代码示例:

from trl import SFTTrainer
from datasets import load_dataset

# 加载数据集
dataset = load_dataset("trl-lib/Capybara", split="train")

# 初始化并启动训练
trainer = SFTTrainer(
    model="Qwen/Qwen2.5-0.5B",
    train_dataset=dataset,
)
trainer.train()

该库采用 Apache-2.0 许可证,并拥有活跃的社区和持续的更新。

 

https://github.com/fanqingsong/fine-tuning-gemma-with-unsloth/blob/master/src/train.py

#!/usr/bin/env python3
"""
使用 Unsloth + LoRA 微调 Gemma 3 270M(逻辑来自 live_demo.ipynb)。

微调流程概览(建议按 main() 中的顺序阅读):
  1. 加载基座模型与分词器(可选 4bit 量化以省显存)
  2. 加载并清洗训练数据,统一成 Gemma 对话格式
  3. 用 chat template 把对话转成模型看到的「一整段文本」
  4. 在注意力/MLP 等层挂上 LoRA 适配器(只训练少量新增参数)
  5. SFT(监督微调):用 SFTTrainer 在文本上做 next-token 预测
  6. train_on_responses_only:只在 assistant 回复部分计算 loss(不训练用户问题)
  7. 保存 LoRA 权重;可选合并 LoRA 到完整模型便于部署
"""

from __future__ import annotations

import argparse
import os
import sys
from pathlib import Path

# 保证同目录下的 hf_setup 可被导入
sys.path.insert(0, str(Path(__file__).resolve().parent))
# 必须在 unsloth / transformers / trl 之前:清理空的 HF 环境变量,避免下载失败
import hf_setup  # noqa: E402  — sanitize HF env before unsloth
# unsloth 会 patch 底层库以加速训练;导入顺序有要求,故放在 trl 之前
import unsloth  # noqa: F401,E402  — must precede trl / transformers / peft

from datasets import Dataset, load_dataset  # noqa: E402
from trl import SFTConfig, SFTTrainer  # noqa: E402
from unsloth import FastModel  # noqa: E402
from unsloth.chat_templates import (  # noqa: E402
    get_chat_template,
    train_on_responses_only,
)

# 系统提示:定义模型在对话中的「角色」,会写入每条训练样本
SYSTEM_PROMPT = (
    "You are a master storyteller. Write a short, imaginative story based on "
    "the user's request. The story should be concise and suitable for a general audience."
)
# 默认 Hugging Face 上的示例数据集(也可换成本地 jsonl,见 --dataset)
PREGENERATED_DATASET_NAME = "chongcht/synthetic-creative-writing"


def parse_args() -> argparse.Namespace:
    """命令行超参数:模型、数据、LoRA、优化器与训练步数等。"""
    parser = argparse.ArgumentParser(description="Fine-tune Gemma 3 270M with Unsloth")
    parser.add_argument(
        "--model-name",
        default=hf_setup.model_name_from_env(),
        help="Base model on Hugging Face",
    )
    parser.add_argument(
        "--dataset",
        default=PREGENERATED_DATASET_NAME,
        help="HF dataset id or local json/jsonl path",
    )
    parser.add_argument("--dataset-split", default="train")
    # 单条样本最大 token 数;过长会被截断,过短浪费算力
    parser.add_argument("--max-seq-length", type=int, default=2048)
    parser.add_argument(
        "--load-in-4bit",
        action=argparse.BooleanOptionalAction,
        default=False,
        help="270M fits in memory without 4-bit; notebook trains in full precision",
    )
    # LoRA 秩 r:越大可表达能力越强,但可训练参数和显存也更多
    parser.add_argument("--lora-r", type=int, default=16)
    parser.add_argument(
        "--lora-alpha",
        type=int,
        default=None,
        help="Defaults to 2 * lora-r (notebook heuristic)",
    )
    parser.add_argument("--lora-dropout", type=float, default=0.0)
    parser.add_argument("--per-device-train-batch-size", type=int, default=8)
    # 梯度累积:有效 batch = batch_size × accumulation_steps(显存不够时增大此项)
    parser.add_argument("--gradient-accumulation-steps", type=int, default=2)
    parser.add_argument("--learning-rate", type=float, default=2e-4)
    parser.add_argument("--warmup-steps", type=int, default=10)
    # 固定训练步数;若指定 num-train-epochs 则按 epoch 跑满整个数据集
    parser.add_argument("--max-steps", type=int, default=100)
    parser.add_argument(
        "--num-train-epochs",
        type=float,
        default=None,
        help="If set, overrides --max-steps for a full epoch run",
    )
    parser.add_argument("--logging-steps", type=int, default=1)
    parser.add_argument("--seed", type=int, default=3407)
    parser.add_argument("--output-dir", default="outputs/lora")
    # 合并 LoRA 到基座权重,得到单目录完整模型(体积大,推理更简单)
    parser.add_argument("--save-merged", action="store_true")
    parser.add_argument("--merged-dir", default="outputs/merged")
    parser.add_argument("--max-samples", type=int, default=None)
    return parser.parse_args()


def load_training_dataset(path_or_id: str, split: str) -> Dataset:
    """
    加载训练集:本地 .json/.jsonl,或 Hugging Face dataset id。
    返回 Hugging Face Dataset,后续用 .map / .filter 做预处理。
    """
    local = Path(path_or_id)
    if local.exists():
        suffix = local.suffix.lower()
        if suffix in {".jsonl", ".json"}:
            return load_dataset("json", data_files=str(local), split="train")
        return load_dataset(str(local), split=split)
    return load_dataset(path_or_id, split=split)


def format_for_gemma(example: dict) -> dict:
    """
    把各种常见字段名统一成 Gemma3 需要的 conversations 列表:
      system / user / assistant 三轮消息。

    若缺少 prompt 或 response,返回 conversations=None,后续会被 filter 掉。
    """
    prompt_text = example.get("prompt") or ""
    response_text = example.get("response") or ""

    # ShareGPT 风格:conversations 里 role 为 user / assistant
    if not prompt_text and "conversations" in example:
        for message in example["conversations"]:
            if message["role"] == "user":
                prompt_text = message["content"]
            elif message["role"] in {"assistant", "model"}:
                response_text = message["content"]

    # OpenAI 风格:messages 数组
    if not prompt_text and "messages" in example:
        for message in example["messages"]:
            if message["role"] == "user":
                prompt_text = message["content"]
            elif message["role"] in {"assistant", "model"}:
                response_text = message["content"]

    # Alpaca 风格:instruction + input -> 用户侧;output -> 模型要学的回复
    if not prompt_text:
        instruction = example.get("instruction") or example.get("question") or ""
        extra = example.get("input") or ""
        prompt_text = instruction if not extra else f"{instruction}\n{extra}".strip()
        response_text = (
            example.get("output") or example.get("answer") or response_text
        )

    if not prompt_text or not response_text:
        return {"conversations": None}

    return {
        "conversations": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": prompt_text},
            {"role": "assistant", "content": response_text},
        ]
    }


def has_error_response(example: dict) -> bool:
    """过滤合成数据里标记为 error 的坏样本(response 字符串含 error 则丢弃)。"""
    response = example.get("response")
    if isinstance(response, str):
        return "error" not in response.lower()
    return True


def main() -> None:
    args = parse_args()
    token = os.environ.get("HF_TOKEN") or None
    # LoRA 缩放系数,常用经验:alpha = 2 * r
    lora_alpha = args.lora_alpha if args.lora_alpha is not None else args.lora_r * 2

    # ---------- 1. 加载基座模型 ----------
    # full_finetuning=False:后面只加 LoRA,不更新全部 270M 参数
    model, tokenizer = hf_setup.load_fast_model(
        FastModel,
        args.model_name,
        max_seq_length=args.max_seq_length,
        load_in_4bit=args.load_in_4bit,
        load_in_8bit=False,
        full_finetuning=False,
        token=token,
    )

    # ---------- 2. 数据集:加载 -> 清洗 -> 统一格式 ----------
    dataset = load_training_dataset(args.dataset, args.dataset_split)
    if "response" in dataset.column_names:
        dataset = dataset.filter(has_error_response)

    dataset = dataset.map(format_for_gemma, remove_columns=dataset.column_names)
    dataset = dataset.filter(lambda example: example.get("conversations") is not None)
    if args.max_samples:
        dataset = dataset.select(range(min(args.max_samples, len(dataset))))

    # Gemma3 专用 chat template(控制 <start_of_turn> 等特殊 token)
    tokenizer = get_chat_template(tokenizer, chat_template="gemma3")

    def formatting_prompts_func(examples):
        """
        SFT 需要一列纯文本 `text`:把多轮对话渲染成模型训练时看到的字符串。
        add_generation_prompt=False:assistant 回复已在模板里,用于 teacher forcing。
        """
        texts = [
            tokenizer.apply_chat_template(
                convo,
                tokenize=False,
                add_generation_prompt=False,
            ).removeprefix("<bos>")
            for convo in examples["conversations"]
        ]
        return {"text": texts}

    dataset = dataset.map(formatting_prompts_func, batched=True)

    # ---------- 3. LoRA:在指定线性层旁路低秩矩阵,只训练这些增量权重 ----------
    model = FastModel.get_peft_model(
        model,
        r=args.lora_r,
        lora_alpha=lora_alpha,
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj",
            "o_proj",
            "gate_proj",
            "up_proj",
            "down_proj",
        ],
        lora_dropout=args.lora_dropout,
        bias="none",
        use_gradient_checkpointing="unsloth",  # 用算力换显存
        random_state=args.seed,
    )

    output_dir = Path(args.output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # ---------- 4. SFT 训练配置 ----------
    # 本质:对 text 做因果语言建模,预测下一个 token
    sft_kwargs = dict(
        dataset_text_field="text",
        per_device_train_batch_size=args.per_device_train_batch_size,
        gradient_accumulation_steps=args.gradient_accumulation_steps,
        warmup_steps=args.warmup_steps,
        learning_rate=args.learning_rate,
        logging_steps=args.logging_steps,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=args.seed,
        output_dir=str(output_dir),
        report_to="none",
    )
    if args.num_train_epochs is not None:
        sft_kwargs["num_train_epochs"] = args.num_train_epochs
        sft_kwargs["max_steps"] = -1
    else:
        sft_kwargs["max_steps"] = args.max_steps

    trainer = SFTTrainer(
        model=model,
        tokenizer=tokenizer,
        train_dataset=dataset,
        args=SFTConfig(**sft_kwargs),
    )

    # ---------- 5. 只在 model 回复段算 loss ----------
    # 用户/系统 turn 的 token 不参与 loss,避免模型「学会复读问题」
    trainer = train_on_responses_only(
        trainer,
        instruction_part="<start_of_turn>user\n",
        response_part="<start_of_turn>model\n",
    )

    trainer.train()

    # LoRA adapter + tokenizer 配置(推理时用同一 base model + 加载此目录)
    model.save_pretrained(str(output_dir))
    tokenizer.save_pretrained(str(output_dir))
    print(f"Saved LoRA adapter to {output_dir}")

    if args.save_merged:
        merged_dir = Path(args.merged_dir)
        merged_dir.mkdir(parents=True, exist_ok=True)
        model.save_pretrained_merged(str(merged_dir), tokenizer)
        print(f"Saved merged model to {merged_dir}")


if __name__ == "__main__":
    main()

 

posted @ 2026-09-13 18:57  lightsong  阅读(5)  评论(0)    收藏  举报
千山鸟飞绝,万径人踪灭