Sentence-Transformersを用いたベトナム語RAG向けドメイン特化型埋め込みモデルのファインチューニング

AI tutorial - IT technology blog
AI tutorial - IT technology blog

背景と動機

6か月前、私たちのチームは4万5,000ページを超えるベトナムの税法や企業コンプライアンス通達を横断検索する社内RAGアシスタントを立ち上げました。当初は、広く使われている多言語ベースラインのintfloat/multilingual-e5-baseやBAAI/bge-m3を採用しました。どちらも標準的な汎用ベンチマークでは良好な性能を示していました。しかし、法務アナリストが独自の行政表現、地域ごとの税務コード、複合語を含むクエリを入力し始めた途端、検索精度は急激に低下しました。

汎用のバイエンコーダーは、広範なウェブスクレイピングデータから密ベクトルを構築します。そのため、ユーザーが“quyết toán thuế thu nhập cá nhân đối với chuyên gia nước ngoài”(外国人専門家の個人所得税確定申告)のような検索を実行した際、既成のモデルは法的な意図ではなく表面的なキーワードでマッチングしてしまいました。その結果、無関係なコンテキストがプロンプトウィンドウに溢れ、後続のGPT-4やClaudeによる要約で検知しにくいハルシネーションを引き起こしました。

私たちは難しいトレードオフに直面しました。リクエストごとにクロスエンコーダーのリランカーを追加するか、バイエンコーダー自体をドメイン内ペアで直接ファインチューニングするかです。リランカーを導入するとレイテンシ要件が厳しくなり、8コアCPUインスタンスでのp99レスポンスタイムは410msを超えてしまいました。一方、sentence-transformersを用いてベースのバイエンコーダーをファインチューニングしたところ、レイテンシを22msに抑えつつ、Top-5ヒット率を61.4%から89.2%へと引き上げることに成功しました。

ファインチューニングは単なる学術的理論ではなく、実用的なエンジニアリングです。クラウドインフラのコストを膨らませることなく、専門用語の多い言語においてエンタープライズグレードの精度を実現できます。

インストール

バージョン3以降のsentence-transformersは、Hugging FaceのTrainerとネイティブに統合されています。この刷新により、ボイラープレートコードを書くことなく、分散実行、FP16/BF16混合精度学習、早期終了(early-stopping)コールバックの組み込みサポートが利用可能になりました。

独立したPython環境を構築し、CUDA 12.1対応の主要な依存関係をインストールします:

python3 -m venv venv
source venv/bin/activate

# CUDA 12.1対応のPyTorchをインストール
pip install torch --index-url https://download.pytorch.org/whl/cu121

# Sentence-Transformers、Datasets、Accelerateをインストール
pip install -U sentence-transformers datasets accelerate

# オプション: テキスト前処理用のベトナム語トークナイザー
pip install pyvi underthesea

次に進む前に、PyTorchがGPUを正しく認識しているか確認してください:

python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()} | Device: {torch.cuda.get_device_name(0)}')"

設定

ニッチなドメイン向けに密ベクトルモデルを適応させるには、対照学習(Contrastive Learning)が最も効果的です。モデルには正例の (クエリ, パッセージ) ペア、または (クエリ, 正例, ハードネガティブ) のトリプレットを入力します。逆伝播の際、MultipleNegativesRankingLoss(MNRL)はミニバッチ内の他のすべての正例パッセージを暗黙的な負例として利用します。これにより、バッチサイズを32に設定すれば、手動で負例を用意しなくても、サンプルごとに31個の負例が自動的に生成されます。

1. 学習データセットの準備

社内のテキストチャンクからLLMを用いて1万4,000件の疑似質問を生成し、参照元パラグラフとペアにしてJSON Lines形式(train_dataset.jsonl)で整理しました:

[{"anchor": "Hồ sơ giảm trừ gia cảnh gồm những giấy tờ gì?", "positive": "Căn cứ theo Thông tư 111/2013/TT-BTC, hồ sơ chứng minh người phụ thuộc đối với con bao gồm bản sao giấy khai sinh..."},
{"anchor": "Quy định thời hạn nộp báo cáo tài chính năm", "positive": "Thời hạn nộp báo cáo tài chính năm chậm nhất là ngày thứ 90 kể từ ngày kết thúc năm dương lịch hoặc năm tài chính..."}]

2. 学習パイプライン

以下は、SentenceTransformerTrainer、FP16混合精度、インバッチネガティブ対照損失を利用した、本番環境で検証済みの学習スクリプトです:

import os
from datasets import load_dataset
from sentence_transformers import (
    SentenceTransformer,
    SentenceTransformerTrainer,
    SentenceTransformerTrainingArguments,
    losses,
)
from sentence_transformers.training_args import BatchSamplers

# 1. ベースモデルの設定
BASE_MODEL_NAME = "intfloat/multilingual-e5-base"
OUTPUT_DIR = "./vietnamese-domain-embedding-v1"

# 2. ベースとなるバイエンコーダーの初期化
model = SentenceTransformer(BASE_MODEL_NAME)

# 3. データセットの読み込み (anchor-positive ペア)
dataset = load_dataset("json", data_files={"train": "train_dataset.jsonl"})
split_dataset = dataset["train"].train_test_split(test_size=0.1, seed=42)
train_ds = split_dataset["train"]
eval_ds = split_dataset["test"]

# 4. 対照損失関数
train_loss = losses.MultipleNegativesRankingLoss(model)

# 5. 学習引数の設定 (RTX 4090 / A10G 24GB GPU向けに最適化)
training_args = SentenceTransformerTrainingArguments(
    output_dir=OUTPUT_DIR,
    num_train_epochs=3,
    per_device_train_batch_size=32,  # バッチサイズを大きくすることで、より多様なインバッチネガティブを供給
    per_device_eval_batch_size=32,
    learning_rate=2e-5,
    warmup_ratio=0.1,
    fp16=True,                       # メモリ消費量を削減し、スループットを高速化
    eval_strategy="steps",
    eval_steps=100,
    save_steps=100,
    save_total_limit=2,
    logging_steps=20,
    batch_sampler=BatchSamplers.NO_DUPLICATES,  # 同一バッチ内での重複アンカーを防止
    load_best_model_at_end=True,
)

# 6. トレーナーの初期化
trainer = SentenceTransformerTrainer(
    model=model,
    args=training_args,
    train_dataset=train_ds,
    eval_dataset=eval_ds,
    loss=train_loss,
)

# 7. 学習の実行
trainer.train()

# 8. ファインチューニング済みチェックポイントのエクスポート
model.save_pretrained(os.path.join(OUTPUT_DIR, "best_model"))
print("ファインチューニングが完了しました。チェックポイントが正常に保存されました。")

検証とモニタリング

学習損失の減少だけを理由に、埋め込みチェックポイントをそのまま本番のベクトルインデックスにデプロイしてはいけません。密ベクトルモデルは表現崩壊(Representation Collapse)に陥る危険があります。この現象が起きると、埋め込みベクトルが狭い超球面コーン内に密集してしまい、判別能力を完全に失ってしまいます。

1. InformationRetrievalEvaluatorによる定量評価

ホールドアウトした800件の未見テストクエリを用いて、MRR@10、NDCG@10、Hit@5などのランキング指標でチェックポイントを検証します。

from sentence_transformers.evaluation import InformationRetrievalEvaluator

queries = {
    "q1": "quy định khấu trừ thuế vãng lai 1%",
    "q2": "điều kiện hưởng chế độ thai sản cho lao động nam"
}
corpus = {
    "d1": "Thông tư hướng dẫn về thuế giá trị gia tăng đối với hoạt động xây dựng vãng lai...",
    "d2": "Luật Bảo hiểm xã hội quy định lao động nam có vợ sinh con được nghỉ từ 5 đến 14 ngày làm việc...",
    "d3": "Quy trình bảo dưỡng máy móc định kỳ trong nhà máy thép..."
}
relevant_docs = {
    "q1": {"d1"},
    "q2": {"d2"}
}

evaluator = InformationRetrievalEvaluator(
    queries=queries,
    corpus=corpus,
    relevant_docs=relevant_docs,
    name="vietnamese-ir-benchmark",
    mrr_at_k=[5, 10],
    ndcg_at_k=[10],
    accuracy_at_k=[1, 5]
)

results = evaluator(model)
print(f"MRR@10: {results['vietnamese-ir-benchmark_cosine_mrr@10']:.4f}")
print(f"NDCG@10: {results['vietnamese-ir-benchmark_cosine_ndcg@10']:.4f}")

2. 本番環境における6か月間の運用実績

適応済みモデルでQdrantベクトルデータベースを再インデックスした後、12万件の実環境クエリを分析しました:

  • 検索再現率(Retrieval Recall): ベトナム語の法務表現に対するTop-5ヒット率が61.4%から89.2%に向上しました。
  • ハルシネーション率: 事実誤認としてフラグが立てられた監査対象出力が44%減少しました。適切な抜粋を供給することが、生成エラーの直接的な削減につながりました。
  • ベクトルの判別性: 既成の多言語モデルではコサインスコアが0.78〜0.88の狭い範囲に密集していました。ファインチューニング後のバイエンコーダーでは適切な分離が回復し、無関係なパッセージは約0.32前後に留まる一方、正解の回答は0.85以上を記録しました。このスコア差により、0.72という明確なカットオフ閾値を設定でき、対象外の問い合わせを的確に処理できるようになりました。

埋め込みモデルの健全性は長期的に維持することが重要です。コサイン類似度のドリフトを毎週追跡し、ユーザーから低評価を受けたクエリをログに記録してください。四半期ごとに定期的なファインチューニングを実行することで、新しく公布された法令を取り込み、検索エンジンの信頼性を高く保つことができます。

Share: