AI

Gemma 3の微調整:カスタムQ&Aデータセットによるステップ・バイ・ステップ・ガイド

Bright DataとUnslothを使用して、レビューのスクレイピングからデプロイまで、カスタムTrustpilot QAデータでGoogle Gemma 3を微調整します。
14 分読
A graphic featuring the logo for 'Gemma 3' over a blue background, surrounded by abstract representations of code snippets and user interface elements, along with icons suggesting security and performance.

2025年3月にリリースされたグーグルの最新のオープンウェイトAIモデルGemma 3は、限られたリソースのハードウェア上で効率的に動作しながら、多くのプロプライエタリなLLMに匹敵する印象的なパフォーマンスを提供する。オープンソースAIにおけるこの進歩は、さまざまなプラットフォームで動作し、世界中の開発者にアクセス可能な形式で強力な機能を提供する。

このガイドでは、Trustpilotのレビューに由来するカスタム質問回答データセットでGemma 3を微調整する方法を説明します。Bright Dataを使用してカスタマーレビューをスクレイピングし、構造化されたQAペアに加工し、最小限の計算で効率的な微調整を行うためにUnslothを活用します。最後には、ドメイン固有の質問を理解し、Hugging Face Hubでホストする準備が整った、特化したAIアシスタントを作成します。

さあ、飛び込もう!

ジェマを理解する 3

グーグルのGemma 3ファミリーは2025年3月に発売され、1B、4B、12B、27Bの4種類のオープンウェイト・パラメータを持ち、すべてシングルGPUで動作するように設計されている。

  • 1Bモデルはテキストのみで、32Kトークンのコンテキスト・ウィンドウを持つ。
  • 4B、12B、27Bモデルは、マルチモーダル(テキスト+画像)入力を追加し、128Kトークンウィンドウをサポートする。

LMArenaのヒューマン・プリファレンス・リーダーボードにおいて、Gemma 3-27B-ITは、Llama 3 405BやDeepSeek-V3のようなはるかに大きなモデルを凌ぐスコアを獲得しており、マルチGPUフットプリントを必要とせずに最先端の品質を提供している。

モデルの性能(ELOスコア)とモデルサイズ(10億パラメータ)を示す散布図。このプロットには、Gemma 3 27B IT、Qwen 2.5 72B Instruct、Llama 3.3 70B Instruct、Meta Llama 3.1 70B Instructなど、さまざまなモデルがポイントで示されている。X軸はモデルのサイズ、Y軸はELOスコアを表し、特定のポイントがハイライトされている。

画像ソースジェマ3の紹介

ジェンマ3モデルの主な特徴

ジェンマ3モデルの特筆すべき特徴をいくつか紹介しよう:

  • マルチモーダル入力(テキスト+画像)は、4B、12B、27Bの各モデルで利用可能。
  • 最大128Kトークンまでのロング・コンテキスト(1Bモデルでは32K)。
  • 多言語機能– 35以上の言語をすぐにサポート、140以上の言語を事前学習済み。
  • 量子化対応トレーニングQATの公式バージョンは、高品質を維持しながら、メモリ使用量を大幅に削減します(約3倍)。
  • 関数の呼び出しと構造化された出力– 呼び出しの自動化と構造化された応答の受信のための組み込みサポートが含まれています。
  • 効率性– シングルGPU/TPU、あるいは携帯電話やラップトップからワークステーションまで、コンシューマーデバイスでも実行できるように設計されています。
  • 安全性(ShieldGemma)– 統合されたコンテンツフィルタリングフレームワークが特徴。

なぜファインチューンなのか?

ファインチューニングは、Gemma 3のような事前にトレーニングされたモデルを使用し、特定のドメインやタスクのために新しい動作を教えます。Gemma 3はコンパクトな設計で、4B+のバリエーションではマルチモーダルにも対応しているため、軽量で手頃な価格であり、リソースの限られたハードウェアでも微調整が可能です。

微調整の利点は以下の通り:

  • ドメイン特化– 業界特有の言語を理解し、ドメイン内の専門的なタスクでより優れたパフォーマンスを発揮できるようにします。
  • 知識の強化– モデルの元のトレーニングデータにはなかった重要な事実やコンテキストを追加します。
  • ビヘイビア・リファインメント– モデルがどのように反応するかを調整し、ブランドのトーンや好みの出力形式に合わせます。
  • リソースの最適化– ゼロから新しいモデルをトレーニングする場合に比べ、大幅に少ない計算リソースで高品質な結果を得ることができます。

前提条件

このチュートリアルを始める前に、以下のものを用意してください:

  • Python 3.9以上がインストールされていること。
  • Pythonプログラミングの基礎知識
  • GPUをサポートするコンピューティング環境(Google Colab、Jupyter Notebook、Kaggle Notebooksなど)へのアクセス。
  • 機械学習と大規模言語モデル(LLM)の基礎を理解している。
  • VS CodeなどのIDEの使用経験。

また、外部サービスのアクセス認証情報も必要です:

微調整のためのカスタム・データセットの構築

ファインチューニングは、モデルに学習させたい振る舞いをデータセットが忠実に反映している場合に最も効果的です。特定のユースケースに合わせたカスタムデータセットを作成することで、モデルの性能を劇的に向上させることができる。古典的なルールを思い出してほしい。データセットの準備に時間をかけることが重要なのはそのためです。

質の高いデータセットでなければならない:

  • 特定のユースケースに合わせる– データセットがターゲットとするアプリケーションに近ければ近いほど、モデルの出力はより適切なものになる。
  • 一貫したフォーマットを維持する– 一様な構造(質問と答えのペアのような)は、モデルがより効果的にパターンを学習するのに役立ちます。
  • 多様な事例を含める– 多様なシナリオは、モデルが異なる入力にわたって一般化するのに役立つ。
  • クリーンでエラーフリーであること– 不整合やノイズを除去することで、モデルが不要な動作をピックアップするのを防ぎます。

まずはこのような生の評価から:

Trustpilotのエレクトロニクス&テクノロジーカテゴリのビジネスプロフィールのスクリーンショットで、873件のレビューから星2.3という低い評価を示しており、レビューを書くかウェブサイトを訪問するかのオプションがある。このプロフィールはクレームされたもので、企業がレビューを隠すためのインセンティブを提供することはできないという注意書きがある。

そして、それらを次のような構造化された質問と答えのペアに変換する:

3つのカラムを持つ行を表示するデータセットテーブル:id'、'question'、'answer'。question'カラムにはHubSpotのカスタマーサポートと満足度に関する問い合わせが含まれ、'answer'カラムにはカスタマーレビューに基づく洞察が表示されます。

このデータセットは、Gemma 3が顧客のフィードバックから洞察を抽出し、センチメント・パターンを特定し、実用的な推奨を提供することを教えてくれる。

セットアップ手順

#1 ライブラリのインストールプロジェクト環境を開き、requirements.txtに記載されている必要なPythonライブラリをインストールします。ターミナルかノートブックで以下のコマンドを実行してください:

pip install -r requirements.txt

#2 環境変数の設定プロジェクトのルートディレクトリに.envファイルを作成し、APIキーを安全に保存します。

OPENAI_API_KEY="your_openai_key_here"
HF_TOKEN="your_hugging_face_token_here"

ステップ1:ブライト・データによるデータ収集

重要な最初のステップは、データソーシングである。微調整用のデータセットを構築するために、Trustpilotから生のレビューデータを収集する。Trustpilotの強固なボット対策により、Bright DataのTrustpilot Scraper APIを使用する。このAPIは、IPローテーション、CAPTCHA解決、動的コンテンツ処理を効果的に管理し、スクレイピングソリューションの構築の複雑さを回避して、構造化されたレビューを効率的に収集することができます。

Bright DataのAPIを使ってレビューを収集するPythonスクリプトです:

import time
import json
import requests
from typing import Optional

# --- Configuration ---
API_KEY = "YOUR_API_KEY"  # Replace with your Bright Data API key
DATASET_ID = "gd_lm5zmhwd2sni130p"  # Replace with your Dataset ID
TARGET_URL = "https://www.trustpilot.com/review/hubspot.com"  # Target company page
OUTPUT_FILE = "trustpilot_reviews.json"  # Output file name
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
TIMEOUT = 30  # Request timeout in seconds

# --- Functions ---
def trigger_snapshot() -> Optional[str]:
    """Triggers a Bright Data snapshot collection job."""
    print(f"Triggering snapshot for: {TARGET_URL}")
    try:
        resp = requests.post(
            "https://api.brightdata.com/datasets/v3/trigger",
            headers=HEADERS,
            params={"dataset_id": DATASET_ID},
            json=[{"url": TARGET_URL}],
            timeout=TIMEOUT,
        )
        resp.raise_for_status()  # Raise HTTPError for bad responses (4xx or 5xx)
        snapshot_id = resp.json().get("snapshot_id")
        print(f"Snapshot triggered successfully. ID: {snapshot_id}")
        return snapshot_id
    except requests.RequestException as e:
        print(f"Error triggering snapshot: {e}")
    except json.JSONDecodeError:
        print(f"Error decoding trigger response: {resp.text}")
    return None

def wait_for_snapshot(snapshot_id: str) -> Optional[list]:
    """Polls the API until snapshot data is ready and returns it."""
    check_url = f"https://api.brightdata.com/datasets/v3/snapshot/{snapshot_id}"
    print(f"Waiting for snapshot {snapshot_id} to complete...")
    while True:
        try:
            resp = requests.get(
                check_url,
                headers=HEADERS,
                params={"format": "json"},
                timeout=TIMEOUT,
            )
            resp.raise_for_status()
            # Check if response is the final data (list) or status info (dict)
            if isinstance(resp.json(), list):
                print("Snapshot data is ready.")
                return resp.json()
            else:
                pass
        except requests.RequestException as e:
            print(f"Error checking snapshot status: {e}")
            return None  # Stop polling on error
        except json.JSONDecodeError:
            print(f"Error decoding snapshot status response: {resp.text}")
            return None  # Stop polling on error

        print("Data not ready yet. Waiting 30 seconds...")
        time.sleep(30)

def save_reviews(reviews: list, output_file: str) -> bool:
    """Saves the collected reviews list to a JSON file."""
    try:
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(reviews, f, indent=2, ensure_ascii=False)
        print(f"Successfully saved {len(reviews)} reviews to {output_file}")
        return True
    except (IOError, TypeError) as e:
        print(f"Error saving reviews to file: {e}")
        return False
    except Exception as e:
        print(f"An unexpected error occurred during saving: {e}")
        return False

def main():
    """Main execution flow for collecting Trustpilot reviews."""
    print("Starting Trustpilot review collection process...")
    snapshot_id = trigger_snapshot()
    if not snapshot_id:
        print("Failed to trigger snapshot. Exiting.")
        return

    reviews = wait_for_snapshot(snapshot_id)
    if not reviews:
        print("Failed to retrieve reviews from snapshot. Exiting.")
        return

    if not save_reviews(reviews, OUTPUT_FILE):
        print("Failed to save the collected reviews.")
    else:
        print("Review collection process completed.")

if __name__ == "__main__":
    main()

このスクリプトは以下のステップを実行する:

  • 認証:お客様のAPI_KEYを使用して、Authorizationヘッダ経由でBright Data APIを認証します。
  • 収集をトリガーします:指定したTARGET_URL(この場合はHubSpotのTrustpilotページ)に対して、DATASET_IDに関連付けられたデータ収集「スナップショット」をトリガーするPOSTリクエストを送信します。
  • 完了を待つ:返されたsnapshot_idを使用してAPIを定期的にポーリングし、データ収集が完了したかどうかを確認する。
  • データを取得する:APIがデータの準備ができたことを示すと、スクリプトはJSON形式でレビューデータを取得する。
  • 出力を保存します:収集したレビューオブジェクトのリストを構造化されたJSONファイル(trustpilot_reviews.json)に保存します。

出来上がったJSONファイルの各レビューには、以下のような詳細情報が記載されている:

{
    "review_id": "680af52fb0bab688237f75c5",
    "review_date": "2025-04-25T04:36:31.000Z",
    "review_rating": 1,
    "review_title": "Cancel Auto Renewal Doesn't Work",
    "review_content": "I was with Hubspot for almost 3 years...",
    "reviewer_name": "Steven Barrett",
    "reviewer_location": "AU",
    "is_verified_review": false,
    "review_date_of_experience": "2025-04-19T00:00:00.000Z",
    // Additional fields omitted for brevity
}

LLM研修に最適なデータの探し方をガイドで学びましょう:LLMトレーニングデータのトップソース

ステップ2:JSONをMarkdownに変換する

生のレビューデータを収集した後、次のステップはそれを処理に適したクリーンで読みやすいフォーマットに変換することです。トークン化の際のノイズを減らし、微調整のパフォーマンスを向上させ、異なるコンテンツセクション間の一貫した分離を保証する軽量なプレーンテキスト構造を提供するMarkdownを使用します。

変換を実行するには、以下のスクリプトを実行するだけです。

このスクリプトは、ステップ1の出力からJSONデータを読み込み、構造化された要約と個々のカスタマーレビューを含むMarkdownファイルを生成します。

以下はMarkdownの出力構造の例である:

# HubSpot Review Summary
[Visit Website](https://www.hubspot.com/)
**Overall Rating**: 2.3
**Total Reviews**: 873
**Location**: United States
**Industry**: Electronics & Technology

> HubSpot is a leading growth platform... Grow Better.
---

### Review by Steven Barrett (AU)
- **Posted on**: April 25, 2025
- **Experience Date**: April 19, 2025
- **Rating**: 1
- **Title**: *Cancel Auto Renewal Doesn't Work*

I was with Hubspot for almost 3 years... Avoid.

[View Full Review](https://www.trustpilot.com/reviews/680af52fb0bab688237f75c5)

---

AIエージェントがHTMLよりもMarkdownを好む理由については、ガイドをお読みください。

ステップ3:文書のチャンキングと処理

Markdownドキュメントの準備ができたら、次の重要なステップは、それを管理しやすい小さなチャンクに分割することです。大規模言語モデル(LLM)には入力トークンの制限があり、微調整はしばしば適切な長さの例で最もうまくいくため、これは重要です。さらに、これらのチャンクを処理することで、モデルの明瞭性と一貫性を向上させることができます。

LangChainのRecursiveCharacterTextSplitterを使ってMarkdownファイルを分割します。このメソッドはセパレータのリストに基づいて再帰的にテキストを分割し、関連するテキスト片をまとめるのに役立ちます。分割ポイントをまたがる可能性のあるコンテキストを保持するため、連続するチャンク間にオーバーラップを適用します。この処理では、256文字のオーバーラップで1,024文字のチャンクサイズを使用します。

分割後、各チャンクはオプションで(GPT-4oのような)LLMに渡され、レビューテキストの元の意味を厳密に維持しながら、全体的な明瞭さと一貫性を向上させる。この強化ステップは、各チャンク内のデータ構造と内容を、その後の微調整プロセスのために最適に明確にすることを目的としている。

処理された各チャンクには一意の識別子が割り当てられ、JSONライン(.jsonl)ファイル形式で保存され、パイプラインの次のステージに備える。

わかりやすくするためにLLMを使ったPython関数を以下に示す:

def improve_review_chunk(text: str, client: OpenAI, model: str = "gpt-4o") -> str:
    prompt = """Improve this review's clarity while preserving its meaning:
{text}

Return only the improved text without additional commentary."""
    response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": prompt},
                {"role": "user", "content": text}
            ]
        )
    return response.choices[0].message.content

このステップの完全なコードはこちら👉split-markdown-into-chunks.py

出力はJSON Linesファイルで、各行は一意な識別子と潜在的に改善されたレビュー内容を持つレビューチャンクを表します:

[
  {
    "id": "f8a3b1c9-e4d5-4f6a-8b7c-2d9e0a1b3c4d", // Unique chunk ID
    "review": "# HubSpot Review Summary\\n\\n[Visit Website](https://www.hubspot.com/)...\\n---\\n\\n### Review by Steven Barrett (AU)\\n- **Posted on**: April 25, 2024...\\n- **Rating**: 1\\n- **Title**: *Cancel Auto Renewal Doesn't Work*\\n\\nI was with Hubspot for almost 3 years... [Text continues - may be improved]" // Chunk content (potentially refined)
  },
  // ... more chunk objects
]

ステップ4:QAペアの生成

最後のデータ準備ステップでは、処理されたレビューチャンクを言語モデルの微調整に適した構造化された質問と回答(QA)のペアに変換します。OpenAIのGPT-4oを使用して、ステップ3で作成した.jsonlファイル内の各チャンクに1つのQAペアを生成します。

各チャンクに対して、スクリプトは慎重に設計されたシステムプロンプトを使ってOpenAI APIを呼び出す:

SYSTEM_PROMPT = """
You are an expert at transforming customer reviews into insightful question–answer pairs. For each review, generate exactly 1 high-quality QA pair.

PURPOSE:
These QA pairs will train a customer service AI to understand feedback patterns about HubSpot products and identify actionable insights.

GUIDELINES FOR QUESTIONS:
- Make questions general and applicable to similar situations
- Phrase from a stakeholder perspective (e.g., "What feature gaps are causing customer frustration?")
- Focus on product features, usability, pricing, or service impact

GUIDELINES FOR ANSWERS:
- Provide analytical responses (3–5 sentences)
- Extract insights without quoting verbatim
- Offer actionable recommendations
- Maintain objectivity and clarity

FORMAT REQUIREMENTS:
- Start with "Q: " followed by your question
- Then "A: " followed by a plain-text answer
"""

このスクリプトは、一時的なAPIの中断を処理し、安定した実行を保証するために、組み込みのレート制限と再試行のメカニズムを含んでいます。完全な実装はgenerate-qa-pairs.py にあります。

出力はJSON配列として保存され、各オブジェクトには元のチャンクのIDでリンクされた、生成された質問と回答のペアが含まれます:

[
  {
    "id": "82d53a10-9f37-4d03-8d3b-38812e39ecdc",
    "question": "How can pricing and customer support issues impact customer satisfaction and retention for HubSpot?",
    "answer": "Pricing concerns, particularly when customers feel they are overpaying for services they find unusable or unsupported, can significantly impact customer satisfaction and retention..."
  }
  // ... more QA pairs
]

一度生成されたら、結果のQAデータセットをHugging Face Hubにプッシュすることを強くお勧めします。これにより、微調整や共有のために簡単にアクセスできるようになります。ここで公開されたデータセットの例を見ることができます:trustpilot-reviews-qa-dataset

アンスローでジェマ3を微調整する:ステップ・バイ・ステップ

カスタムQ&Aデータセットが準備できたので、Gemma 3モデルを微調整しましょう。標準的なHugging Faceの実装と比較して、LoRA/QLoRAトレーニングのメモリと速度を大幅に改善するオープンソースライブラリであるUnslothを使用します。これらの最適化により、GPUに十分なVRAMがあれば、シングルGPUセットアップでもGemma 3のようなモデルを微調整することが可能になります。

ジェンマ3サイズ 必要なVRAMの目安 適切なプラットフォーム
4B ~15 GB 無料 Google Colab (T4), Kaggle (P100 16 GB)
12B ≥24GB以上 コラボ・プロ+(A100/A10)、RTX 4090、A40
27B 22~24GB(4ビットQLoRA、バッチサイズ=1の場合)、それ以外は~40GB A100 40GB、H100、マルチGPUセットアップ

注:VRAM 要件は、バッチ・サイズ、シーケンス長、および特定の量子化技術によって異なる場合があります。27Bモデルの要件は、4ビットQLoRAと小さなバッチ・サイズ(1または2など)です。高いバッチ・サイズまたはあまり積極的でない量子化では、かなり多くのVRAM(~40GB以上)が必要になります。

初心者の方は、無料のColabノートブックで4Bモデルから始めることをお勧めします。Unslothのロード、トレーニング、デプロイメントを快適にサポートします。12Bまたは27Bモデルへのアップグレードは、より高VRAMのGPUまたは有償のクラウド層へのアクセスが可能な場合にのみ検討する必要があります。

Google Colabでランタイムタイプを変更し、T4 GPUを選択するには、以下の手順に従ってください:

  1. 上部のランタイムメニューをクリックする。
  2. Change runtime typeを選択する。
  3. 表示されるダイアログで、ハードウェアアクセラレータの下にあるGPUを選択します。
  4. Saveをクリックして変更を適用します。
Pythonノートブックでランタイムタイプを変更するオプションを示すコーディング環境のスクリーンショット。T4 GPUを含むさまざまなハードウェアアクセラレータオプションと、左側にモデルファイルの読み込み進捗インジケータが表示される。

ステップ1:環境設定

まず、必要なライブラリをインストールする。ColabやJupyterの環境であれば、これらのコマンドをコードセルで直接実行できる。

%%capture
!pip install --no-deps unsloth vllm
import sys, re, requests; modules = list(sys.modules.keys())
for x in modules: sys.modules.pop(x) if "PIL" in x or "google" in x else None
!pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft "trl==0.15.2" triton cut_cross_entropy unsloth_zoo
!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer

# vLLM requirements - vLLM breaks Colab due to reinstalling numpy
f = requests.get("https://raw.githubusercontent.com/vllm-project/vllm/refs/heads/main/requirements/common.txt").content
with open("vllm_requirements.txt", "wb") as file:
    file.write(re.sub(rb"(transformers|numpy|xformers)[^\n]{1,}\n", b"", f))
!pip install -r vllm_requirements.txt

インストールされている主なパッケージについて簡単に説明しよう:

  • unsloth:フューズドカーネルのようなテクニックを使って、より高速でメモリ効率の良いLLMのトレーニングとロードのためのコア最適化を提供する。
  • peft:パラメータ効率的ファインチューニング手法(LoRAのようなもの)。完全なモデルではなく、少数の追加パラメータのみを学習させる。
  • trl:トランスフォーマー強化学習。教師ありの微調整のプロセスを単純化するSFTTrainerを含む。
  • bitsandbytes:kビット(4ビットおよび8ビット)量子化を有効にし、モデルのメモリフットプリントを劇的に削減します。
  • 加速する:様々なハードウェアセットアップ(シングルGPU、マルチGPUなど)でPyTorchトレーニングをシームレスに実行するためのHugging Faceライブラリ。
  • データセットデータセットの読み込み、処理、管理を効率的に行うHugging Faceライブラリ。
  • transformers: Hugging Faceのコア・ライブラリーで、事前訓練されたモデル、トークナイザー、ユーティリティー。
  • huggingface_hub:Hugging Face Hubとやりとりするためのユーティリティ(ログイン、ダウンロード、アップロード)。
  • vllm(オプション):高速LLM推論エンジン。デプロイ時に必要であれば別途インストール可能。

ステップ2:ハグ顔認証

モデルをダウンロードし、後で微調整した結果をアップロードするために、あなたの環境からハギング・フェイス・ハブにログインする必要があります。

import os
from huggingface_hub import login
from google.colab import userdata

hf_token = userdata.get('HF_TOKEN')
if not hf_token:
    raise ValueError("Please set your HF_TOKEN environment variable before running.")

try:
    login(hf_token)
    print("Successfully logged in to Hugging Face Hub.")
except Exception as e:
    print(f"Error logging in to Hugging Face Hub: {e}")

Google Colabでは、Hugging Faceトークンを管理する最も安全な方法は、”Secrets “タブを使用することです:

ユーザーが環境変数を設定できる「Secrets」セクションを示すGoogle Colabのスクリーンショット。新しいシークレットを追加するためのオプションが表示され、ハイライトされた'HF_TOKEN'の名前とそれぞれの値が見えなくなっている。Pythonでシークレットにアクセスする方法を以下に示す。

ステップ 3: モデルとトークナイザーのロード

微調整を開始するために、UnslothのFastModelを使用してGemma 3命令チューニングモデルを効率的にロードします。この例では、unsloth/gemma-3-4b-itモデルを使用します。これは、典型的なColab GPUのメモリ制約内に収まるようにUnslothによって最適化された4ビット量子化バージョンです。

UnslothのGemma 3コレクションをHugging Faceでチェックしよう。1B、4B、12B、27Bサイズのモデルがあり、GGUF、4ビット、16ビットフォーマットで入手可能。

from unsloth import FastModel
from unsloth.chat_templates import get_chat_template
import torch # Import torch for checking CUDA

# Ensure CUDA is available
if not torch.cuda.is_available():
    raise RuntimeError("CUDA is not available. A GPU is required for this tutorial.")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA device name: {torch.cuda.get_device_name(0)}")

model, tokenizer = FastModel.from_pretrained(
    model_name="unsloth/gemma-3-4b-it", # Using the 4B instruction-tuned model optimized by Unsloth
    max_seq_length=2048, # Set max context length
    load_in_4bit=True,   # Enable 4-bit quantization
    full_finetuning=False, # Use PEFT (LoRA)
    token=hf_token,      # Pass your Hugging Face token
)

# Apply the correct chat template for Gemma 3
tokenizer = get_chat_template(tokenizer, chat_template="gemma-3")

print("Model and Tokenizer loaded successfully.")

このコードで何が起こっているのか:

  • FastModel.from_pretrained():Unslothの最適化されたモデルローダー。
  • model_name="unsloth/gemma-3-4b-it":ロードするモデルのバリエーションを指定する。ここでは、Unslothによって事前に最適化された4B命令チューニング(it)バージョンを選択します。
  • max_seq_length=2048: モデルが一度に処理できるトークンの最大数を設定します。メモリ使用量と長い入力を処理する能力のバランスを取りながら、データチャンクの長さと希望するコンテキストウィンドウに基づいて調整します。
  • load_in_4bit=True: 限られたVRAMでのトレーニングに不可欠です。これは、bitsandbytesを使用して4ビット精度でモデルの重みをロードします。
  • full_finetuning=False:PEFT/LoRAのファインチューニングのためにモデルを準備するようUnslothに指示します。つまり、すべてのモデルパラメータではなく、アダプターレイヤーのみがトレーニングされます。
  • get_chat_template(tokenizer, chat_template="gemma-3"):Gemma 3が期待するチャットフォーマット(<start_of_turn>usern...˶<end_of_turn><start_of_turn>modeln...˶<end_of_turn>)にプロンプトを自動的にフォーマットするためにトークナイザーをラップする。これは、指示に従うモデルを正しく微調整し、モデルが期待される会話ターンで応答を生成するように学習することを保証するために重要である。

ステップ4:トレーニング用データセットのロードと準備

以前にハグする顔ハブにアップロードしたデータセットを読み込み、トークナイザーとトレーナーが期待するチャットベースのフォーマットに変換する。

from datasets import load_dataset
from unsloth.chat_templates import standardize_data_formats, train_on_responses_only # train_on_responses_only imported earlier

# 1. Load the dataset from Hugging Face Hub
dataset_name = "triposatt/trustpilot-reviews-qa-dataset" # Replace with your dataset name
dataset = load_dataset(dataset_name, split="train")

print(f"Dataset '{dataset_name}' loaded.")
print(dataset)

# 2. Normalize any odd formats (ensure 'question' and 'answer' fields exist)
dataset = standardize_data_formats(dataset)
print("Dataset standardized.")

# 3. Define a function to format examples into chat template
def formatting_prompts_func(examples):
    """Formats question-answer pairs into Gemma 3 chat template."""
    questions = examples["question"]
    answers = examples["answer"]
    texts = []
    for q, a in zip(questions, answers):
        # Structure the conversation as a list of roles and content
        conv = [
            {"role": "user", "content": q},
            {"role": "assistant", "content": a},
        ]
        # Apply the chat template
        txt = tokenizer.apply_chat_template(
            conv,
            tokenize=False, # Return string, not token IDs
            add_generation_prompt=False # Don't add the model's start tag at the end yet
        )
        # Gemma 3 tokenizer adds <bos> by default, which the trainer will re-add
        # We remove it here to avoid double <bos> tokens
        txt = txt.removeprefix(tokenizer.bos_token)
        texts.append(txt)
    return {"text": texts}

# 4. Apply the formatting function to the dataset
dataset = dataset.map(formatting_prompts_func, batched=True, remove_columns=["question", "answer"])
print("Dataset formatted with chat template.")
print(dataset) # Inspect the new 'text' column

このコードでは

  • load_dataset():Hugging Face HubからQ&Aデータセットを取得する。
  • standardize_data_formats():異なるデータセット間でフィールド名の一貫性を確保する。特にこの場合は ‘question’ と ‘answer’ を探す。
  • formatting_prompts_func():この重要な関数は、Q&Aペアのバッチを処理します。tokenizer.apply_chat_template()メソッドを使用して、Gemma 3命令微調整用に正しくフォーマットされた文字列に変換します。このフォーマットには、<start_of_turn>usernや <start_of_turn>modelnのような特別なターン・トークンが含まれており、これはモデルが会話の構造を理解するために不可欠である。SFTTrainerが独自に追加するので、最初の<bos>トークンは削除する。
  • dataset.map(...)formatting_prompts_funcをデータセット全体に効率よく適用し、フォーマットされた文字列を含む新しい’text’カラムを作成し、元のカラムを削除する。

ステップ5:LoRAとトレーナーの設定

ここで、PEFT(LoRA)の設定とtrlライブラリのSFTTrainerを設定する。LoRAは、事前学習済みモデルの主要な層に、学習可能な小さな行列を注入することで機能する。微調整の際には、これらの小さなアダプタ行列のみが更新され、学習するパラメータの数が大幅に削減されるため、メモリ使用量を最小限に抑えることができます。

from trl import SFTTrainer, SFTConfig
import torch

# 1. Configure LoRA
model = FastModel.get_peft_model(
    model,
    r=8, # LoRA rank (a common value) - lower rank means fewer parameters, higher means more expressive
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj", # Attention layers
        "gate_proj", "up_proj", "down_proj"      # MLP layers
    ],
    # Set True if you want to fine-tune language layers (recommended for text tasks)
    # and Attention/MLP modules (where LoRA is applied)
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    # finetune_vision_layers=False, # Only relevant for multimodal models (12B/27B)
    lora_alpha=8, # LoRA scaling factor (often set equal to r)
    lora_dropout=0, # Dropout for LoRA layers
    bias="none", # Don't train bias terms
    use_gradient_checkpointing="unsloth", # Memory optimization
    random_state=1000, # Seed for reproducibility
    use_rslora=False, # Rank-Stabilized LoRA (optional alternative)
    # modules_to_save=["embed_tokens", "lm_head"], # Optional: train embedding/output layers
)

print("Model configured for PEFT (LoRA).")

# 2. Configure the SFTTrainer
# Determine a reasonable max_steps based on dataset size and epochs
# For demonstration, a small number of steps is used (e.g., 30)
# For a real use case, calculate steps = (dataset_size / batch_size / grad_accum) * num_epochs
dataset_size = len(dataset)
per_device_train_batch_size = 2 # Adjust based on your GPU VRAM
gradient_accumulation_steps = 4 # Accumulate gradients to simulate larger batch size (batch_size * grad_accum = 8)
num_train_epochs = 3 # Example: 3 epochs

# Calculate total training steps
total_steps = int((dataset_size / per_device_train_batch_size / gradient_accumulation_steps) * num_train_epochs)
# Ensure max_steps is not 0 if dataset is small or calculation results in < 1 step
max_steps = max(30, total_steps) # Set a minimum or calculate properly

print(f"Calculated total training steps for {num_train_epochs} epochs: {total_steps}. Using max_steps={max_steps}")

sft_config = SFTConfig(
    dataset_text_field="text", # The column in our dataset containing the formatted chat text
    per_device_train_batch_size=per_device_train_batch_size,
    gradient_accumulation_steps=gradient_accumulation_steps,
    warmup_steps=max(5, int(max_steps * 0.03)), # Warmup for first few steps (e.g., 3% of total steps)
    max_steps=max_steps, # Total number of training steps
    learning_rate=2e-4, # Learning rate
    logging_steps=max(1, int(max_steps * 0.01)), # Log every 1% of total steps (min 1)
    optim="adamw_8bit", # 8-bit AdamW optimizer (memory efficient)
    weight_decay=0.01, # L2 regularization
    lr_scheduler_type="linear", # Linear learning rate decay
    seed=3407, # Random seed
    report_to="none", # Disable reporting to platforms like W&B unless needed
    output_dir="./results", # Directory to save checkpoints and logs
)

# 3. Build the SFTTrainer instance
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    eval_dataset=None, # Optional: provide a validation dataset
    args=sft_config,
)

print("SFTTrainer built.")

# 4. Mask out the input portion for training
# This teaches the model to only generate the assistant’s response
# It prevents the model from just copying the user’s prompt
# Pass the literal prefixes for instruction and response turns from the chat template
trainer = train_on_responses_only(
    trainer,
    instruction_part="<start_of_turn>user\n", # Literal string before user content
    response_part="<start_of_turn>model\n",  # Literal string before model content
)

print("Trainer configured to train only on responses.")

このコードでは

  • FastModel.get_peft_model()target_modulesは、どのモデル層(注意やMLP投影など)がこれらのアダプタを受け取るかを指定します。use_gradient_checkpointingは、Unslothが提供するメモリ節約テクニックです。
  • SFTConfig(): per_device_train_batch_sizegradient_accumulation_stepsは、勾配の計算に使用する有効なバッチサイズを決定します。
  • SFTTrainer():LoRA で設定されたモデル、準備されたデータセット、トークナイザー、SFTConfig で定義されたトレーニング引数をまとめてトレーナーをインスタンス化します。
  • train_on_responses_only():ユーティリティ関数(trlの一部であり、Unslothと互換性がある)で、学習者の損失計算を変更する。この関数は、モデルの予想される応答に対応するトーク(<start_of_turn>modeln...)に対してのみ損失を計算するように設定し、ユーザのプロンプトのトークンを無視する(<start_of_turn>usern...)。これは、入力プロンプトを単純に繰り返したり補完したりするのではなく、適切な答えを生成するようにモデルに教えるために不可欠である。これらのセクションを区切るためにチャットテンプレートで使用される正確な文字列接頭辞を提供します。

ステップ6:モデルのトレーニング

すべての設定が完了したら、微調整を開始します。trainer.train()メソッドは、SFTConfigで提供された設定に基づいてトレーニングループを処理します。

# Optional: clear CUDA cache before training
torch.cuda.empty_cache()

print("Starting training...")
# Use mixed precision training for efficiency
# Unsloth automatically handles float16/bf16 based on GPU capabilities and model
with torch.amp.autocast(device_type="cuda", dtype=torch.float16): # Or torch.bfloat16 if supported
     trainer.train()

print("Training finished.")

トレーナーは、トレーニング損失を含む進捗状況を出力します。モデルがデータから学習していることを示しています。総トレーニング時間は、データセットサイズ、モデルサイズ、ハイパーパラメータ、使用するGPUによって異なります。この例のデータセットとT4 GPU上の4Bモデルの場合、200ステップのトレーニングは比較的早く完了するはずです(例えば、正確なセットアップとデータの長さにもよりますが、15~30分未満)。

ステップ7:微調整されたモデルのテスト(推論)

トレーニングの後、微調整したモデルをテストして、トレーニングしたTrustpilotのレビューデータに基づいて、質問に対してどの程度応答するかを見てみましょう。よりインタラクティブな出力のために、TextStreamerで model.generateメソッドを使います。

from transformers import TextStreamer

# Define some test questions related to the dataset content
questions = [
    "What are common issues or complaints mentioned in the reviews?",
    "What do customers like most about the product/service?",
    "How is the customer support perceived?",
    "Are there any recurring themes regarding pricing or value?"
    # Add more questions here based on your dataset content
]

# Set up a streamer for real-time output
# skip_prompt=True prevents printing the input prompt again
# skip_special_tokens=True removes chat template tokens from output
streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)

print("\n--- Testing Fine-Tuned Model ---")

# Iterate through questions and generate answers
for idx, q in enumerate(questions, start=1):
    # Build the conversation prompt in the correct Gemma 3 chat format
    conv = [{"role": "user", "content": q}]

    # Apply the chat template and add the generation prompt token
    # add_generation_prompt=True includes the <start_of_turn>model tag
    prompt = tokenizer.apply_chat_template(
        conv,
        add_generation_prompt=True,
        tokenize=False
    )

    # Tokenize the prompt and move to GPU
    inputs = tokenizer([prompt], return_tensors="pt", padding=True).to("cuda")

    # Display the question
    print(f"\n=== Question {idx}: {q}\n")

    # Generate the response with streaming
    # Pass the tokenized inputs directly to model.generate
    _ = model.generate(
        **inputs,
        streamer=streamer, # Use the streamer for token-by-token output
        max_new_tokens=256, # Limit the response length
        temperature=0.7, # Control randomness (lower=more deterministic)
        top_p=0.95, # Nucleus sampling
        top_k=64, # Top-k sampling
        use_cache=True, # Use cache for faster generation
        # Add stopping criteria if needed, e.g., stopping after <end_of_turn>
        # eos_token_id=tokenizer.eos_token_id,
    )
    # Add a separator after each answer
    print("\n" + "="*40)

print("\n--- Testing Complete ---")

下の画像でモデルの反応をご覧ください:

HubSpotのユーザーフレンドリーなデザインとCRM機能に対する好意的なフィードバック、フレンドリーで効率的であるというカスタマーサポートの認識、価格戦略のコストパフォーマンスに関するテーマなどです。

素晴らしい!

ファインチューニングプロセスが成功するということは、モデルが、一般的な回答ではなく、カスタムデータセットに存在するスタイルや洞察を反映し、より分析的で、ファインチューニングされたレビューコンテンツから直接導き出された回答を生成することを意味します。

ステップ8:微調整したモデルを保存してプッシュする

最後に、微調整したLoRAアダプタとトークナイザを保存します。ローカルに保存するだけでなく、Hugging Face Hubにプッシュすることで、簡単に共有、バージョン管理、デプロイができます。

# Define local path and Hub repository ID
new_model_local = "gemma-3-4b-trustpilot-qa-adapter" # Local directory name
new_model_online = "YOUR_HF_USERNAME/gemma-3-4b-trustpilot-qa" # Hub repo name

# 1. Save locally
print(f"Saving model adapter and tokenizer locally to '{new_model_local}'...")
model.save_pretrained(new_model_local)
tokenizer.save_pretrained(new_model_local)
print("Saved locally.")

# 2. Push to Hugging Face Hub
print(f"Pushing model adapter and tokenizer to Hugging Face Hub '{new_model_online}'...")
model.push_to_hub(new_model_online, token=hf_token)
tokenizer.push_to_hub(new_model_online, token=hf_token)

微調整されたモデルは現在、Hugging Face Hubで入手可能だ:

triposatt/gemma-3-4b-trustpilot-qa」のHugging Faceモデルカードのスクリーンショット。開発者、ライセンス、UnsloathとHugging FaceのTRLライブラリを使った微調整プロセスに関する情報など、モデルの詳細が表示されている。

結論

このガイドでは、GoogleのGemma 3を実用的なユースケースであるカスタマーレビューから分析回答を生成するために微調整するエンドツーエンドのアプローチを紹介しました。Bright DataのウェブスクレイパーAPIを利用した高品質でドメイン固有のデータの収集から、LLMを利用した処理によるQAフォーマットへの構造化、リソースに制約のあるハードウェア上でのUnslothライブラリを利用したGemma 3 4Bモデルの効率的なファインチューニングまで、ワークフロー全体をカバーしました。

その結果、生のレビューデータから洞察を抽出し、センチメントを解釈し、構造化された実用的な回答に変換することに長けた、特化したLLMが生まれる。この方法は非常に適応性が高く、この同じワークフローを適用して、様々なドメイン固有のデータセット上でGemma 3(または他の適切なLLM)を微調整し、様々なニーズに合わせたAIアシスタントを作成することができる。

AIを活用したデータ抽出戦略のさらなる探求については、以下の追加リソースをご検討ください:

Unslothを使った最適化の微調整や例については、Unslothノートブック・コレクションをご覧ください。

クレジットカードは必要ありません