モデル微調整 Fine-tuning

電子商取引の商品説明文を生成するための専用モデルが必要な場合、主に3つの選択肢があります。

  • 1つ目は、ゼロからの事前学習(Pre-training from scratch)です。膨大なテキストデータ(おそらく数兆トークン)を収集し、数十枚のA100 GPUをレンタルして数か月間トレーニングし、数百万ドルを費やす必要があります。これはOpenAIやGoogleといった巨大企業が行うことであり、大多数の人には現実的ではありません。

  • 2つ目は、Promptエンジニアリングです。汎用大規模モデルに精巧に設計されたプロンプトを与えます。例えば「あなたはプロのECコピーライターです。以下の商品情報に基づいて魅力的な説明文を生成してください...」といった具合です。この方法はコストゼロですぐに使えますが、問題は、毎回長いプロンプトを書かなければならず、効果が不安定で、無関係な入力に邪魔されやすく、トークン消費が大きいことです。

  • 3つ目は、微調整(Fine-tuning)です。訓練済みの汎用モデルを用意し、あなたの専用データで少しだけ追加トレーニングを行い、特定のタスクを「学習」させます。こうして得られたモデルは、汎用能力を維持しつつ、あなたのタスクでも優れた性能を発揮し、しかもコストを抑えられます。

3つの方法の比較:

方法コスト効果適用シナリオ
ゼロからの学習非常に高い(数百万ドル規模)完全カスタマイズ新しい基盤モデルの作成(巨大企業のみ)
Promptエンジニアリングほぼゼロ不安定、プロンプト依存簡単なタスク、迅速な検証
微調整中程度(数百〜数千ドル)安定かつ専門的特定領域のタスク、製品化

簡単な比喩を使うと:

  • ゼロからの学習は、ゼロから医者を育てるようなもので、十数年かかります。
  • Promptエンジニアリングは、普通の医者に詳細な操作マニュアルを与えるようなものです。
  • 微調整は、すでに医師資格を持つ医者を数か月の専門研修に行かせて、専門医にするようなものです。

なぜ微調整が必要か

すべてのタスクに微調整が必要なわけではありません。いつ使うべきか、いつ使うべきでないかをまず明確にしましょう。

Promptエンジニアリングの限界

Promptエンジニアリングは強力ですが、突破しにくいいくつかの上限があります。

  • 1つ目の限界はコンテキストウィンドウの制限です。プロンプトと例はモデルのコンテキストウィンドウ(例:4K、8K、128K、1024Kトークン)に収めなければなりません。タスクを説明するために数百の例が必要な場合、プロンプトには収まりきりません。

  • 2つ目の限界は効果が不安定なことです。同じプロンプトでも、質問の仕方を変えると結果が大きく異なることがあります。モデルの調子が良い(サンプリングのランダム性)ときは出力が良いですが、調子が悪いと完全に的外れになることがあります。

  • 3つ目の限界は推論コストが高いことです。毎回の推論で長いプロンプトを送る必要があり、トークン消費が大きく、応答速度が遅くなります。高並行処理のシナリオでは、コストが急激に上昇します。

  • 4つ目の限界は忘却問題です。モデルはプロンプトを見ていますが、膨大な事前学習データがまだ足を引っ張っている可能性があります。例えば、JSONを出力するように指示しても、多くの自然言語の説明を追加してしまうことがあります。

微調整の適用シナリオ

タスクが以下の特徴を満たす場合、微調整を検討する価値があります。

  • 特徴1:タスク定義が明確で、出力形式が固定されている。「技術文書を分かりやすいブログに翻訳する」「カスタマーサービス記録からチケットの要約を生成する」「自然言語をSQLクエリに変換する」など。

  • 特徴2:数百〜数千件の高品質なラベル付きデータがある。データが少なすぎると微調整の効果が出ず、多すぎる必要もありません。一般的に500〜5000件が理想的な範囲です。

  • 特徴3:特定領域で専門的なパフォーマンスが必要。例えば、医療レポートの解釈、法律文書の要約、金融ニュースの分析などです。汎用モデルは業界用語や慣行を理解していない可能性がありますが、微調整によって「業界に入る」ことができます。

  • 特徴4:応答速度とコストに敏感である。微調整後の小規模モデルは、汎用大規模モデルよりも効果が高く、推論速度が10倍速く、コストが100倍低い可能性があります。

  • 特徴5:出力形式を厳密に守る必要がある。例えば、JSONを出力しなければならない、特定のトーンを使わなければならない、特定のフィールドを含めなければならないなどです。微調整によりモデルがこれらの要件を「記憶」し、毎回プロンプトで繰り返す必要がなくなります。

微調整のコスト・ベネフィット分析

微調整を行う前に、まず費用対効果を計算しましょう。

コストは主に3つの側面から発生します:データ準備、計算リソース、人手による調整です。データ準備は通常、最大の作業量を占めます。データの収集、クリーニング、ラベル付けが必要です。計算リソースは現在非常に安価になっており、LoRA + QLoRAを使えば、コンシューマー向けGPU(RTX 3090/4090)1枚で7B/13Bモデルを微調整できます。

メリットはいくつかの側面に現れます:より良い効果、より低い推論コスト、より速い応答速度、より安定した出力です。モデルを外部にサービスとして提供する場合、これらのメリットは継続的に蓄積されます。

いつ微調整すべきでないのでしょうか?タスクの変化が速く、データ形式が毎週変わる場合は、Promptエンジニアリングの方が柔軟です。一度きりの探索でしかない場合や、データが数十件しかない場合も、微調整の価値はありません。

要因Promptの方が適している微調整の方が適している
データ量< 100件> 500件
タスクの安定性頻繁に変化する比較的固定されている
使用頻度たまに使う高頻度で使う
コスト感応度低高
効果の要求使えれば良い最良を追求

全量ファインチューニング(Full Fine-tuning)

まず最も伝統的な微調整方法を理解し、次に今日なぜそれがほとんど使われていないのかを見てみましょう。

原理とフロー

全量微調整の考え方は非常にシンプルです:事前学習モデルのすべてのパラメータを取り出し、あなたのデータセットで追加トレーニングを続け、すべての重みを更新します。

フローはおおよそ次のとおりです:

  • ステップ1:データを準備します。タスクデータを「入力-出力」ペアの形式に整理します。

  • ステップ2:事前学習モデルをロードします。例えば、LLaMA-2-7B、Mistral-7Bなどのオープンソースモデルをダウンロードします。

  • ステップ3:トレーニングパラメータを設定します。学習率は非常に小さく設定します(例:1e-5〜5e-5)。事前学習で得た知識を完全に上書きしたくないからです。

  • ステップ4:トレーニングを開始します。モデルをあなたのデータで数エポック(データセットを数回完全に反復)実行させます。

  • ステップ5:モデルを保存します。トレーニング後、元のモデルと同じサイズの新しい重みファイルが得られます。

実例

# ============================================
# 全量微調整の概念デモ(疑似コード、フレームワーク非依存)
# 核心となる考え方を示す(実際のコードはHuggingFaceを使用)
# ============================================

def full_finetuning_concept():
    """全量微調整の核となる概念デモ"""
    # 1. 事前学習モデルをロード(すべてのパラメータがトレーニング可能)
    model = load_pretrained_model("llama-2-7b")
    # モデルには70億のパラメータがあり、すべてのパラメータが更新される
    print(f"モデルのパラメータ総数:{count_parameters(model)}")
    # 出力:モデルのパラメータ総数:7000000000

    # 2. 特定のタスクデータを準備する
    train_data = [
        {"input": "商品:保温ボトル\nECの説明文を生成",
         "output": "この高品質な保温ボトルは、304ステンレス鋼製の内ビンを採用し、真空断熱技術により保温時間は最大12時間です。"},
        {"input": "商品:ワイヤレスイヤホン\nECの説明文を生成",
         "output": 完全ワイヤレスBluetoothイヤホン、Bluetooth 5.3で安定接続、アクティブノイズキャンセリング、バッテリー持続時間24時間、装着感も快適。},
        # ... 数百〜数千件の同様のデータ
    ]

    # 3. オプティマイザと非常に小さな学習率を設定する
    optimizer = create_optimizer(model, learning_rate=2e-5)

    # 4. トレーニングループ
    for epoch in range(3):  # 通常は2〜5エポック学習させる
        for batch in create_batches(train_data):
            # 順伝播
            loss = model.compute_loss(batch)
            # 逆伝播、すべてのパラメータを更新!
            loss.backward()
            optimizer.step()
            optimizer.zero_grad()
        print(f"エポック {epoch} 完了")

    # 5. 完全なモデルを保存する(70億パラメータ、約13GB)
    model.save("llama-2-7b-ecommerce-finetuned")
    print("フルファインチューニングが完了し、新しいモデルが保存されました")

# これはあくまで概念的なデモであり、実際のトレーニングには大量のVRAMが必要です

破滅的忘却問題

フルファインチューニングには深刻な問題があります:破滅的忘却(Catastrophic Forgetting)です。

どういう意味か?あなたのデータで学習すると、モデルは事前学習段階で学んだ一般的な知識を次第に「忘れて」しまいます。まるで数学の試験に備えて必死に数学の問題を解いた結果、以前学んだ国語や英語を忘れてしまうようなものです。

なぜ起こるのか?フルファインチューニングはすべてのパラメータを更新するからです。事前学習で得た知識はすべてこれらのパラメータに符号化されているため、大幅に変更すると元の知識が壊れてしまいます。

その結果、ファインチューニング後のモデルはあなたのタスクではうまく機能しますが、他の一般的なタスクでは性能が低下します。さらに悪いことに、指示に従う、複雑な問題を理解するといった基本的な能力を失う可能性があります。

どう緩和するか?事前学習データを混ぜて一緒に学習させる方法もありますが、これでは「膨大なデータが必要」という従来の問題に戻ってしまいます。あるいは正則化でパラメータの変化を抑える方法もありますが、効果は限定的です。

VRAM必要量の計算

フルファインチューニングのもう一つの問題は、VRAMの必要量が大きすぎることです。計算してみましょう:

モデルのトレーニングに必要なVRAM = モデルのパラメータ + 勾配 + オプティマイザの状態 + アクティベーション値。

LLaMA-7Bモデルの場合、全精度(FP32)では、70億パラメータ × 4バイト = 28GBです。勾配も28GB、オプティマイザの状態(Adamオプティマイザ)は56GB(勾配の2倍)必要です。アクティベーション値はバッチサイズとシーケンス長によって変わりますが、最低でも数GB必要です。

合計すると、7Bモデルのフルファインチューニングには約100GB以上のVRAMが必要です。つまり、少なくともA100(40GB)を8枚、またはA100(80GB)を2枚が必要ということです。

半精度(FP16/BF16)を使えば半分に減らせますが、それでも約50〜60GBのVRAMが必要です。これは依然として一般の人が手を出せるものではありません。

モデルサイズフルファインチューニングのVRAM(FP16)必要なGPU
7B50-60 GB2×A100 (40GB) または 1×A100 (80GB)
13B90-100 GB3×A100 (40GB) または 2×A100 (80GB)
70B400+ GB12×A100 (40GB) または 6×A100 (80GB)

フルファインチューニングは使えないわけではありませんが、コストが高く、リスクも大きいです。十分な理由(例えば、データ量が非常に多い、モデルの根本的な動作を変える必要があるなど)がない限り、パラメータ効率的ファインチューニング(PEFT)手法を優先すべきです。


パラメータ効率的微調整(PEFT)

PEFT(Parameter-Efficient Fine-Tuning)は、少量のパラメータだけを学習させる手法の総称です。ファインチューニングの効果を保ちながら、コストを大幅に削減します。

PEFTの核となる考え方

PEFTの中心的な洞察は次のとおりです:事前学習モデルにはすでに十分な知識が含まれています。すべてのパラメータを変更する必要はなく、新しいタスクに適応させるためにごく一部のパラメータを「調整」するだけでよいのです。。

まるで完成したピアノのように、ピアノ全体を作り直す必要はなく、数本の弦を微調整するだけで、思い通りの音色を奏でることができます。

PEFTの利点は明らかです:

  • 第一に、VRAMの必要量が大幅に減ります。以前は60GB必要だったのが、今は6GBだけで済むかもしれません。

  • 第二に、破滅的忘却が起こりません。元のモデルのパラメータは変更せず、新しいものを少し追加するだけなので、既存の知識は壊れません。

  • 第三に、ストレージコストが低いことです。PEFTは通常、追加された少量のパラメータ(数MBから数百MB)だけを保存し、モデル全体を保存する必要はありません。

  • 第四に、「タスクの組み合わせ」が可能です。複数の小さなアダプタ(Adapter)を学習させ、推論時に必要に応じてロードすることで、1つのモデルを複数のモデルとして使えます。

主流PEFT手法の概要

PEFTは一つの手法ではなく、一連の手法の総称です。いくつかの主要な手法を見てみましょう:

  • 1つ目はAdapter(アダプタ)です。Transformerの各層の間に小さなニューラルネットワークモジュールを挿入します。学習時にはこれらのAdapterだけを学習させ、元のモデルのパラメータは凍結します。これは初期のPEFT手法であり、現在ではあまり使われていません。

  • 2つ目はPrefix Tuning(プレフィックスチューニング)です。モデルの各層の前に学習可能な「プレフィックス」ベクトルを追加します。これらのプレフィックスベクトルはごく一部を占めるだけですが、モデルの出力を導くことができます。

  • 3つ目はPrompt Tuning(プロンプトチューニング)です。入力層にだけ学習可能なソフトプロンプト(Soft Prompt)を追加します。これは最もシンプルな方法ですが、効果も比較的限定的です。

  • 4つ目はLoRA(Low-Rank Adaptation、低ランク適応)です。モデルのアテンション層の横に低ランク行列を追加します。これは現在最も人気があり、効果も最も高いPEFT手法です。

  • 5つ目はQLoRA(量子化LoRA)です。LoRAに加えてモデルの量子化を行い、VRAMの必要量をさらに削減します。これが現在の主流の選択肢です。

手法原理学習可能なパラメータ数効果
フルファインチューニングすべてのパラメータを更新100%良いが、忘却の可能性あり
Adapter小さなネットワークを挿入0.1%-1%中程度
Prefix Tuningプレフィックスベクトルを追加0.1%-1%中程度
Prompt Tuningソフトプロンプトを追加< 0.1%普通
LoRA低ランク行列による適応0.1%-1%フルファインチューニングに近い
QLoRA量子化 + LoRA0.1%-1%フルファインチューニングに近い

今日のベストプラクティスは明確です:まずはLoRAを選び、VRAMが足りなければQLoRAを使います。この2つの手法は効果が高く、コストが低く、エコシステムも成熟しており、ほとんどの場合で最適な選択肢です。


LoRA(Low-Rank Adaptation)

LoRAは現在最も主流のファインチューニング手法です。その原理を理解することが重要です。

低ランク分解の原理(直感的理解)

まず「ランク」(Rank)とは何か、「低ランク」とは何かを理解しましょう。

100 × 100の行列があるとします。その中のデータは完全にランダムではないかもしれません。これらのデータがごく少数の「パターン」の組み合わせで構成できるなら、この行列のランクは低いと言います。

例を挙げると、各行が1行目の倍数であれば、その行列のランクは1です。各行が前の2行の線形結合であれば、ランクは2です。以下同様です。

LoRAの中心的な洞察は次のとおりです:大規模モデルが新しいタスクに適応するとき、重みの変化量は通常「低ランク」です。つまり、この変化はごく少数のパラメータで表現できるということです。。

これは、1000種類の異なるEC商品説明があっても、その文章スタイルは「トーン、長さ、強調ポイント」といったいくつかの中心的な要素だけで決まっているようなものです。変化を1000の異なる方向で記述する必要はなく、8個、16個、32個あれば十分です。

そこでLoRAのやり方は、元のモデルのすべてのパラメータを凍結し、アテンション層の横に2つの小さな行列AとBを追加することです。Aはd × r、Bはr × dで、rはランク(Rank)であり、通常は8、16、32といった小さな値を取ります。

元のモデルの出力 = 元のモデルの順伝播。

LoRAの出力 = 元のモデルの順伝播 + (入力 × A × B) × スケーリング係数。

学習時にはAとBだけを学習させ、元のモデルのパラメータは完全に変更しません。

LoRAの数学的導出

心配いりません。数学的導出はとても簡単で、わずか数行です。

元のモデルの特定の層が線形変換 h = W₀x であるとします。ここでW₀は事前学習済みの重み行列で、形状はd × dです。

ファインチューニングでは、W₀を更新したいが、あまり大きく更新したくありません。LoRAでは、W₀の変化量を2つの小さな行列の積として表します:ΔW = BA。

したがって、ファインチューニング後の出力は h = W₀x + (BA)x になります。

ここでBの形状はd × r、Aの形状はr × dであり、rがランク(Rank)です。

より正確には、LoRA論文での式は次のとおりです:

h = W₀x + α * (BA)x

ここでαはスケーリング係数で、通常はrの倍数に設定します。これにより、rを変更してもハイパーパラメータを再調整する必要がありません。

訓練時、W₀ は完全に凍結され、A と B のみが訓練可能です。

初期化時、A はランダムなガウス分布で初期化され、B はゼロで初期化されます。これにより、訓練開始時には LoRA 部分の出力がゼロとなり、元のモデルの動作に影響を与えません。

実例

# ============================================
# LoRA 核心原理のミニマル実装
# フレームワークに依存せず、純粋な NumPy によるデモ
# ============================================

import numpy as np


class LoRALayer:
    """LoRA 層のミニマル実装"""

    def __init__(self, d: int, r: int, alpha: float = 1.0):
        """
d: 入力/出力次元(例: 4096)
r: ランク(例: 8, 16, 32)
alpha: スケーリング係数
        """

        self.d = d
        self.r = r
        self.alpha = alpha

        # 凍結された元の重み(事前学習モデルを模擬)
        self.W_0 = np.random.randn(d, d) * 0.01
        self.W_0.flags.writeable = False  # 訓練不可としてマーク

        # LoRA の A および B 行列、訓練可能
        # A: r × d、ランダムなガウス分布で初期化
        self.A = np.random.randn(r, d) * 0.01
        # B: d × r、ゼロで初期化(訓練初期は元のモデルに影響しない)
        self.B = np.zeros((d, r))

    def forward(self, x: np.ndarray) -> np.ndarray:
        """順伝播: 元のモデル出力 + LoRA 出力"""
        # 元のモデル出力
        original_output = x @ self.W_0.T
        # LoRA 出力: (x @ A.T) @ B.T = x @ (B @ A).T
        lora_output = (x @ self.A.T) @ self.B.T
        # 結合出力
        return original_output + self.alpha * lora_output

    def get_trainable_params(self) -> dict:
        """訓練可能なパラメータを取得(A と B のみ)"""
        return {"A": self.A, "B": self.B}

    def count_params(self) -> dict:
        """パラメータ数を集計"""
        total_original = self.d * self.d
        total_lora = self.r * self.d + self.d * self.r
        return {
            "original_params": total_original,
            "lora_params": total_lora,
            "ratio": total_lora / total_original * 100,
        }


# ============================================
# LoRA のパラメータ削減効果をデモ
# ============================================

# d = 4096 と仮定(LLaMA-7B の隠れ層次元に類似)
# r = 8(一般的なランクサイズ)
d = 4096
r = 8

lora = LoRALayer(d=d, r=r, alpha=r)
params_info = lora.count_params()

print(f"元のパラメータ数: {params_info['original_params']:,}")
print(f"LoRA パラメータ数: {params_info['lora_params']:,}")
print(f"パラメータ比率: {params_info['ratio']:.4f}%")

# 出力例:
# 元のパラメータ数: 16,777,216
# LoRA パラメータ数: 65,536
# パラメータ比率: 0.3906%

ほら、LoRA はわずか 0.4% のパラメータを訓練するだけで、全量ファインチューニングに近い効果が得られます。これが LoRA の魔法です。

r(ランク)とαパラメータの選択

LoRA には r(ランク)とスケーリング係数 α の 2 つの重要なハイパーパラメータがあります。

r は LoRA の容量を決定します。r が大きいほど、表現できる変化は複雑になりますが、パラメータ数も増加します。

一般的な r の値: 8、16、32、64。ほとんどのタスクでは r = 8 または 16 で十分です。r が 64 を超えると、通常は効果が逓減し、訓練コストも増加します。

r の選び方: 実用的なアドバイスとして、まず r = 8 を試し、効果が良くなければ 16、32 と増やします。最初から大きな r を使わないでください。

α はスケーリング係数です。一般的な設定は α = r で、これにより ΔW = BA のスケールがほぼ安定します。あるいは α = 1 に設定して、ユーザーが自分で調整することもできます。

その他に考慮すべきパラメータ:

ターゲット層(target_modules): LoRA はどの層に追加すべきでしょうか? 通常、アテンション層の query と value プロジェクション(q_proj, v_proj)にのみ追加するだけで十分効果があります。より多くの層に追加することもできますが、パラメータ数が増加します。

Dropout: LoRA には過学習を防ぐために dropout を追加できます。一般的な値は 0.05 または 0.1 です。

パラメータ一般的な値説明
r(ランク)8, 16, 32大きいほど容量が大きいが、パラメータ数も増える
alphar または 1スケーリング係数、通常 r に設定
target_modules["q_proj", "v_proj"]LoRA を追加する層
lora_dropout0.0, 0.05, 0.1過学習を防ぐ

LoRA重みのマージ

LoRA を訓練した後、推論時には 2 つの選択肢があります。

  • 1 つ目は LoRA を分離したままにすることです。推論時に元のモデルをロードし、次に LoRA 重みをロードして、統合出力を動的に計算します。この方法の利点は複数の LoRA を柔軟に切り替えられることです。欠点は推論が少し遅くなることです(通常は無視できます)。

  • 2 つ目は LoRA 重みを統合することです。LoRA の ΔW = BA を元のモデルの W₀ に加算し、W = W₀ + α×BA を得ます。これにより単一のモデルファイルが得られ、推論時は通常のモデルと同じになります。

統合の利点は推論速度が速く、特別な LoRA サポートが不要なことです。欠点は、一度統合すると元に戻せないこと、また複数の LoRA がある場合はそれぞれ異なるモデルファイルに統合する必要があることです。

実例

# ============================================
# LoRA 重み統合デモ
# ============================================

import numpy as np


def merge_lora_weights(W_0: np.ndarray, A: np.ndarray, B: np.ndarray,
                      alpha: float = 1.0) -> np.ndarray:
    """
LoRA 重みを元の重みに統合します
    W = W_0 + alpha * (B @ A)

パラメータ:
W_0: 元の重み (d, d)
A: LoRA の A 行列 (r, d)
B: LoRA の B 行列 (d, r)
alpha: スケーリング係数

戻り値:
統合された重み
    """

    # delta_W = alpha * (B @ A) を計算
    delta_W = alpha * (B @ A)
    # 元の重みに統合
    W_merged = W_0 + delta_W
    return W_merged


# ============================================
# 統合プロセスをデモ
# ============================================

d = 4096  # 隠れ層次元
r = 8     # LoRA ランク

# 元の重みと LoRA 重みを模擬
W_0 = np.random.randn(d, d) * 0.01
A = np.random.randn(r, d) * 0.01
B = np.zeros((d, r))  # 実際には B は訓練後の結果

# 重みを統合
W_merged = merge_lora_weights(W_0, A, B, alpha=r)

print(f"元の重みの形状: {W_0.shape}")
print(f"統合後の重みの形状: {W_merged.shape}")
print("重みの統合が完了しました。通常のモデルと同じように使用できます")

QLoRA(量子化LoRA)

QLoRA は LoRA の低コスト性を極限まで押し上げました。コンシューマー向け GPU で 70B モデルをファインチューニングできます。

4ビット量子化 + LoRA

QLoRA のアイデアは非常にシンプルです。モデルを 4-bit に量子化し、その上で LoRA ファインチューニングを行います。

量子化とは何でしょうか? 浮動小数点数(FP16/BF16、各 2 バイト)をより低精度の表現に変換することです。4-bit 量子化は、4 ビット(0.5 バイト)で 1 つの重みを表現することです。

これにより、モデルサイズを 75% 削減できます。7B モデルは 13 GB から約 3.5 GB に、13B は 26 GB から約 6.5 GB に、70B は 140 GB から約 35 GB に削減されます。

しかし直接量子化すると精度が失われます。QLoRA の革新は、NF4(Normalized Float 4)と呼ばれるデータ型を使用することです。これにより、ニューラルネットワークの重みの情報をより良く保持できます。

QLoRA の手順は次のとおりです:

  • 1. 事前学習モデルを 4-bit(NF4 形式)に量子化します。

  • 2. その上に LoRA アダプタを追加します。

  • 3. 訓練時、4-bit 量子化された元のモデルは完全に凍結され、LoRA の FP16 重みのみを訓練します。

この効果: VRAM 使用量がさらに 75% 削減され、同時に効果はほぼ損失しません。

bitsandbytesライブラリ

bitsandbytes は量子化を実装する中核ライブラリで、Tim Dettmers によって開発されました。

これには以下の機能があります:

  • 1. 4-bit 量子化(NF4)。

  • 2. 二重量子化(Double Quantization)— 量子化定数も量子化し、VRAM をさらに節約します。

  • 3. ページング最適化器(Paged Optimizer)— VRAM が不足した場合、一部のデータを CPU メモリに転送します。

一般ユーザーは bitsandbytes を直接操作する必要はありません。peft ライブラリと transformers ライブラリがすでにラップしています。しかし、その存在を知っておくと役立ちます。

コンシューマー向けGPUでの微調整ソリューション

QLoRA のおかげで、一般の人でもファインチューニングに手が届くようになりました。

RTX 3090/4090(24GB VRAM)なら 7B/13B モデルを簡単にファインチューニングでき、70B モデルにも挑戦できます(より長いシーケンス長、またはより小さいバッチサイズが必要です)。

VRAM 使用量の見積もり:

モデルサイズQLoRA の VRAM 使用量適切な GPU
7B5-8 GBRTX 3060/4060 以上
13B8-12 GBRTX 3070/4070 以上
34B15-20 GBRTX 3090/4090
70B30-40 GBRTX 3090/4090 × 2 または A100

QLoRA はゲームチェンジャーです。大規模モデルのファインチューニングを「大企業だけができること」から「一般の人が自分のゲーミングノートPCでできること」に変えました。これはオープンソース AI の最も重要な進歩の 1 つです。


訓練データの準備

データ準備はファインチューニングの中で最も時間がかかり、最も重要な部分です。Garbage in, garbage out(ゴミを入れればゴミが出る)— データ品質がモデルの上限を決定します。

データ形式(Alpaca、ShareGPT形式)

一般的なデータ形式はいくつかあります。1 つを選んだら一貫性を保ちましょう。

1つ目はAlpaca形式です。これは最も人気のある形式の1つで、Stanford Alpacaプロジェクトによって広められました。

形式は次のとおりです:instruction(命令)、input(入力、任意)、output(出力)。

実例

# ============================================
# Alpaca データ形式の例
# ============================================

alpaca_data = [
    {
        "instruction": "EC商品説明を生成する",
        "input": "製品名:スマートスポーツウォッチ"\n"機能:心拍数モニタリング、睡眠トラッキング、GPS測位、50m防水",
        "output": このスマートスポーツウォッチは、スポーツ愛好家にとって理想のなパートナーです。正確な心拍数モニタリングと科学のな睡眠トラッキング機能を備え、自分の健康状態を総合のにを握するのに役立ちます。内蔵GPSにより、スマートフォンを持ち歩かなくても運動ルートを記録できます。50m防水等級なので、水泳でも安心して着用できます。
    },
    {
        "instruction": "以下のテキストをもっと口語的な表現に書き換えて",
        "input": "本製品は先進技術を採用し、高性能と低消費電力の特徴を持っています。",
        "output": "この製品はすごく先進的な技術を使っていて、性能が強くて省エネです!"
    },
    # ... その他のデータ
]

print(Alpaca形式には3つのフィールドがあります:)
print(- instruction:タスク命令(必須))
print(- input:追加入力(任意、なければ空文字列にできます))
print(- output:期待される出力(必須))

2つ目はShareGPT形式です。この形式は対話データに使用され、マルチターン対話をサポートします。

形式は次のとおりです:conversations(対話リスト)、各ターンにfrom(役割)とvalue(内容)があります。

実例

# ============================================
# ShareGPT データ形式の例
# ============================================

sharegpt_data = [
    {
        "conversations": [
            {
                "from": "human",
                "value": "こんにちは、あなたたちのスマートウォッチについて知りたいのですが"
            },
            {
                "from": "gpt",
                "value": こんにちは!当社のスマートウォッチには、心拍数モニタリング、睡眠トラッキング、GPS測位などの機能があり、50m防水にも対応しています。具体的に知りたいことはありますか?
            },
            {
                "from": "human",
                "value": "バッテリーの持ちはどうですか?"
            },
            {
                "from": "gpt",
                "value": "通常の使用で7日間のバッテリー持ち、GPS運動モードをオンにすると12時間持続します。"
            }
        ]
    },
    # ... その他の対話
]

print(ShareGPT形式はマルチターン対話データに使用されます:)
print(- conversations:対話リスト)
print(- from:役割(通常はhumanとgpt))
print(- value:対話内容)

もう1つ、シンプルな入力-出力ペア形式があります。単純なタスクに適しています。

実例

# ============================================
# シンプルな入力-出力ペア形式
# ============================================

simple_data = [
    {
        "text": "ユーザー:商品説明を生成"\n"商品:保温ボトル"\n\n"アシスタント:この高品質な保温ボトルは採用..."
    },
    {
        "text": "ユーザー:商品説明を生成"\n"商品:ワイヤレスイヤホン"\n\n"アシスタント:完全ワイヤレスBluetoothイヤホン、Bluetooth 5.3..."
    },
    # ... その他のデータ
]

print(この形式は、すべての内容を直接1つのtextフィールドに連結します。最も柔軟ですが、最も標準化されていません。)

どの形式が最適ですか?Alpaca形式が最も汎用的で、ほとんどのトレーニングフレームワークがサポートしています。対話タスクの場合は、ShareGPTの方が適しています。

データクリーニングの要点

データクリーニングは想像以上に重要です。モデルのパフォーマンスは80%データ品質に依存します。

  • 第一に、重複排除。重複したデータは、モデルが繰り返し現れるパターンに過学習させます。完全に重複しているデータや類似度の高いデータをチェックして削除します。

  • 第二に、低品質データのフィルタリング。短すぎる、長すぎる、形式が乱れている、明らかなエラーがあるデータを削除します。例えば、出力が数文字しかないものや、文字化けしているものなどです。

  • 第三に、形式の統一。例えば、中国語の引用符を使う出力もあれば、英語の引用符を使うものもあります。句点で終わるものもあれば、そうでないものもあります。できるだけ統一することで、モデルはより速く学習します。

  • 第四に、分布のバランス。特定の種類のデータが非常に多く、別の種類が非常に少ない場合、モデルは多数派クラスに偏ります。少数派クラスのオーバーサンプリングや、多数派クラスのアンダーサンプリングを検討できます。

  • 第五に、データリークの確認。テストセットと検証セットがトレーニングセットに含まれていないことを確認します。

実例

# ============================================
# データクリーニングの簡単な例
# ============================================

import json
from typing import List, Dict


def clean_alpaca_data(data: List[Dict]) -> List[Dict]:
    """シンプルなデータクリーニング関数"""
    cleaned = []
    seen_outputs = set()

    for item in data:
        # 1. 必要なフィールドが存在するか確認
        if "instruction" not in item or "output" not in item:
            continue

        instruction = item["instruction"].strip()
        output = item["output"].strip()

        # 2. 短すぎる出力をフィルタリング
        if len(output) < 10:
            continue

        # 3. 長すぎる出力をフィルタリング(タスクに応じて調整)
        if len(output) > 2000:
            continue

        # 4. 重複排除(出力を使って近似重複排除)
        if output in seen_outputs:
            continue
        seen_outputs.add(output)

        # 5. シンプルな形式の統一
        # 例:中国語の引用符を英語の引用符に変更
        output = output.replace(""", "\"").replace(""", "\"")
        # 先頭と末尾の空白を削除
        output = output.strip()

        cleaned.append({
            "instruction": instruction,
            "input": item.get("input", "").strip(),
            "output": output
        })

    return cleaned


# データを読み込み
with open("data/training_data_raw.json", "r", encoding="utf-8") as f:
    raw_data = json.load(f)

# データをクリーニング
cleaned_data = clean_alpaca_data(raw_data)

print(f"元データ量:{len(raw_data)}")
print(f"クリーニング後のデータ量:{len(cleaned_data)}")
print(f"低品質データを {len(raw_data) - len(cleaned_data)} 件フィルタリングしました")

# クリーニング後のデータを保存
with open("data/training_data_cleaned.json", "w", encoding="utf-8") as f:
    json.dump(cleaned_data, f, ensure_ascii=False, indent=2)

print("クリーニング後のデータは data/training_data_cleaned.json に保存されました")

データ量の経験則

どれだけのデータが必要ですか?これは最もよく聞かれる質問の1つです。

答えは:タスクの難易度によります。

  • 単純なタスク(例:テキストをもっと口語的に変更する):100〜300件で十分な場合があります。

  • 中程度のタスク(例:特定の形式の要約を生成する):300〜1000件が適しています。

  • 複雑なタスク(例:専門分野のQA):1000〜5000件が理想的です。

もちろん、データは通常多いほど良いですが、限界効果は逓減します。1000件のデータは500件よりはるかに良いですが、10000件は5000件よりそれほど良くなるとは限りません。

実用的なアドバイス:まず高品質なデータを500件用意して、一度試しに実行してみてください。効果が良くなければ、データ不足なのか、データ品質の問題なのか、それともハイパーパラメータの問題なのかを分析し、的を絞って改善します。

データ品質 >> データ量。1000件の丁寧に作成されたデータは、10000件のいい加減なデータよりもはるかに効果的です。

データ準備段階に時間をかける価値はあります。モデルトレーニングは「ボタンを押すだけ」のことですが、データ品質がそのボタンを押す価値があるかどうかを決定します。


教師あり微調整(SFT)実践

いよいよ実践編です。Unsloth または LLaMA-Factory を使用して、完全な微調整フローを実装します。

環境設定

最初に、必要なライブラリをインストールします。

環境設定

# ============================================
# 依存関係をインストール(condaで新しい環境を作成することを推奨)
# ============================================

# 新しいconda環境を作成
conda create -n finetuning python=3.10 -y
conda activate finetuning

# PyTorchをインストール(CUDAのバージョンに応じて適切なコマンドを選択)
# https://pytorch.org/get-started/locally/ にアクセスして最新のコマンドを取得
conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia

# HuggingFace エコシステムライブラリをインストール
pip install transformers datasets accelerate peft bitsandbytes

# Unslothをインストール(任意、より高速なトレーニング速度)
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps trl xformers

# または LLaMA-Factory(オールインワンのトレーニングフレームワーク)を使用
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

echo 環境設定が完了しました!

Unsloth/LLaMA-Factoryの使用

Unsloth は最適化されたトレーニングライブラリで、速度が速く、VRAMの使用量を抑えられます。LLaMA-Factory はオールインワンのトレーニングフレームワークで、多様なモデルと手法をサポートしています。

まず、Unsloth + peft を使用したトレーニングコードを見てみましょう:

実例

# ============================================
# Unsloth を使用した LoRA 微調整(完全版)
# RTX 3090/4090 で実行可能
# ============================================

import torch
from datasets import load_dataset, Dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
import json


# ============================================
# 1. パラメータを設定
# ============================================

MODEL_NAME = "unsloth/mistral-7b-v0.2-bnb-4bit"  # または他のモデル
DATA_PATH = "data/training_data_cleaned.json"    # あなたのデータ
OUTPUT_DIR = "output/サンプル-ecommerce-model"     # 出力ディレクトリ

# 4ビット量子化設定
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

# LoRA 設定
lora_config = LoraConfig(
    r=16,                    # ランク、8〜64でOK、16は良い出発点
    lora_alpha=16,           # スケーリング係数、通常は r と等しい
    target_modules=[         # LoRA を適用する層、これらは一般的な選択肢
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_dropout=0.05,       # 過学習を防ぐDropout
    bias="none",
    task_type="CAUSAL_LM"    # 因果言語モデル
)

# トレーニングパラメータ
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    per_device_train_batch_size=4,       # GPU当たりのバッチサイズ
    per_device_eval_batch_size=4,
    gradient_accumulation_steps=4,       # 勾配累積、より大きなバッチと同等
    learning_rate=2e-5,                  # 学習率、2e-5 から 5e-5 が一般的な範囲
    num_train_epochs=3,                  # トレーニングエポック数、2〜5が適切
    logging_steps=10,                    # 10ステップごとに記録
    evaluation_strategy="epoch",         # 各エポックで1回評価
    save_strategy="epoch",               # 各エポックで1回保存
    fp16=True,                           # 混合精度でトレーニング
    bf16=False,                          # Ampere 以降のGPU(30/40世代)なら、bf16=Trueを設定可能
    optim="paged_adamw_32bit",           # ページングオプティマイザー、OOM防止
    report_to="none",                    # wandb などのプラットフォームに報告しない
    save_total_limit=3,                  # 最大3つのチェックポイントを保存
    load_best_model_at_end=True,         # 最後に最良のモデルをロード
    metric_for_best_model="eval_loss",   # eval loss で最良モデルを選ぶ
)


# ============================================
# 2. データをロードしてフォーマットする
# ============================================

def format_example(example):
    """Alpaca 形式データをモデル入力にフォーマットする"""
    instruction = example["instruction"]
    input_text = example.get("input", "")
    output = example["output"]

    if input_text.strip() == "":
        # 追加入力がない場合
        prompt = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Response:
"""

    else:
        # 追加入力がある場合
        prompt = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{instruction}

### Input:
{input_text}

### Response:
"""


    # 完全なテキストは prompt + output
    return {
        "prompt": prompt,
        "completion": output,
        "text": prompt + output
    }


# データをロード
dataset = load_dataset("json", data_files=DATA_PATH, split="train")

# トレーニングセットと検証セットに分割(90% トレーニング、10% 検証)
dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]

# データをフォーマット
train_dataset = train_dataset.map(format_example)
eval_dataset = eval_dataset.map(format_example)

print(f"トレーニングセットサイズ:{len(train_dataset)}")
print(f"検証セットサイズ:{len(eval_dataset)}")


# ============================================
# 3. モデルとトークナイザーをロード
# ============================================

print("モデルをロード中...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token  # pad token を設定
tokenizer.padding_side = "right"           # 右からパディング

model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
    use_auth_token=True,  # HuggingFace ログインが必要な場合
)

# LoRA をラップ
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()  # 学習可能なパラメータの割合を表示


# ============================================
# 4. トレーナーを作成してトレーニング開始
# ============================================

trainer = SFTTrainer(
    model=model,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    args=training_args,
    tokenizer=tokenizer,
    peft_config=lora_config,
    dataset_text_field="text",  # どのフィールドが完全なテキストを含むか
    max_seq_length=2048,        # 最大シーケンス長
    packing=False,              # サンプルをパッキングしない
)

print("トレーニングを開始...")
trainer.train()
print(トレーニング完了!)


# ============================================
# 5. モデルを保存
# ============================================

# LoRA アダプターを保存(サイズが小さく、数 MB から数十 MB)
model.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
tokenizer.save_pretrained(f"{OUTPUT_DIR}/lora_adapter")
print(f"LoRA アダプターは {OUTPUT_DIR}/lora_adapter に保存されました")

# LoRA 重みをマージしたい場合(オプション)
# merged_model = model.merge_and_unload()
# merged_model.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# tokenizer.save_pretrained(f"{OUTPUT_DIR}/merged_model")
# print(f"マージ後のモデルは {OUTPUT_DIR}/merged_model に保存されました")

より簡単な方法を使いたい場合は、LLaMA-Factory を使用できます。Web UI とコマンドラインの2つの方法があります:

実例

# ============================================
# LLaMA-Factory を使用してトレーニング
# ============================================

# 方法1:Web UI を起動(最も直感的)
cd LLaMA-Factory
python src/train_web.py

# 方法2:コマンドラインでのトレーニング(自動化に適している)
python src/train.py \
    --model_name_or_path mistralai/Mistral-7B-v0.2 \
    --dataset サンプル_ecommerce \
    --dataset_dir ./data \
    --output_dir ./output/サンプル-model \
    --finetuning_type lora \
    --quantization_bit 4 \
    --lora_rank 16 \
    --lora_alpha 16 \
    --lora_dropout 0.05 \
    --lora_target q_proj,v_proj,k_proj,o_proj,gate_proj,up_proj,down_proj \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --lr_scheduler_type cosine \
    --logging_steps 10 \
    --save_strategy epoch \
    --learning_rate 2e-5 \
    --num_train_epochs 3 \
    --fp16 true \
    --plot_loss true

トレーニングハイパーパラメータの設定

ハイパーパラメータの選択は一種の芸術ですが、経験則もあります。

学習率(Learning Rate):これは最も重要なハイパーパラメータです。LoRA の場合、2e-5 から 5e-5 が安全な開始点です。トレーニングが不安定な場合(loss が振動する)は、少し小さくします。トレーニングが遅すぎる場合(loss の下降が遅い)は、少し大きくします。

Batch Size:大きいほど良いですが、GPUメモリの制限を受けます。per_device_train_batch_size は 2、4、8 のいずれでも構いません。GPUメモリが足りない場合は、gradient_accumulation_steps を使用して勾配を累積します。例えば、batch_size=2、accumulation_steps=4 の場合、batch_size=8 と同等です。

Epochs:2〜5 回が適切です。少なすぎるとモデルが学習できず、多すぎると過学習の可能性があります。eval loss を確認し、eval loss が下降しなくなったら停止できます。

LoRA Rank(r):8〜64 が可能です。16 は良い出発点です。小さなタスクには 8、複雑なタスクには 32 または 64 を使用します。

Max Sequence Length:データに応じて決定します。512、1024、2048、4096 が一般的です。長いほど GPUメモリが必要です。

ハイパーパラメータ推奨される初期値調整の方向
learning_rate2e-5loss が振動するなら小さく、下降が遅すぎるなら大きく
per_device_train_batch_size4GPUメモリが足りれば大きく、足りなければ小さく
gradient_accumulation_steps4等価バッチ = batch_size × accumulation
num_train_epochs3過学習なら減らし、未学習なら増やす
lora_rank (r)16簡単なタスクは 8、複雑なタスクは 32〜64
max_seq_length1024 または 2048データの長さに応じて調整

トレーニング監視とLossの解釈

トレーニング中はいくつかの指標に注目する必要があります。

  • Train Loss:トレーニングセット上の損失で、着実に下降するはずです。下降が速すぎる場合は、学習率が大きすぎるかデータが少なすぎる可能性があります。下降が遅すぎる場合は、学習率が小さすぎるかモデルの容量が足りない可能性があります。

  • Eval Loss:検証セット上の損失で、最も重要な指標です。train loss と一緒に下降するはずです。eval loss が上昇し始め、train loss が引き続き下降する場合、過学習しているのでトレーニングを停止すべきです。

過学習の兆候:eval loss が最初に下降しその後上昇する一方、train loss は継続的に下降します。解決方法:epoch を減らす、dropout を増やす、データを増やす、LoRA rank を小さくする。

未学習の兆候:train loss と eval loss の両方が高く、下降も遅いです。解決方法:epoch を増やす、LoRA rank を大きくする、データを増やす、学習率を少し大きくする。


微調整効果の評価

トレーニングが完了したら、モデルが良いかどうかをどうやって知るのでしょうか?

評価指標の選択

評価方法はタスクの種類によって異なります。

1つ目は人手による評価です。人にモデルの出力を読んでもらい、品質に点数をつけます。これは最も信頼できる方法ですが、最も時間がかかります。

簡単な評価表を作成できます:

実例

# ============================================
# 簡単な人手評価テンプレート
# ============================================

evaluation_prompts = [
    {
        "instruction": "EC製品の説明文を生成する",
        "input": "製品名:ポータブルコーヒーマシン 機能:ワンタッチ抽出、ポータブルデザイン、USB充電"\n"このポータブルコーヒーマシンは、いつでもどこでも挽きたてのコーヒーを楽しめます。ワンタッチ抽出で操作は簡単、コンパクトで持ち運びやすいデザインはオフィス、旅行、アウトドアに最適です。USB充電設計なので、電源の心配はありません。",
        "reference": "EC製品の説明文を生成する"
    },
    {
        "instruction": "製品名:メカニカルキーボード 特徴:青軸、RGBバックライト、87キー、PBTキーキャップ",
        "input": "この87キーのメカニカルキーボードは、打感の良い青軸を採用しています。全キーRGBバックライトで、夜間でもはっきり見えます。PBTキーキャップは耐久性が高くテカらないので、長期間使用しても新品同様です。"\n# ... 10〜20 のテストケース,
        "reference": """一連のプロンプトでモデルを評価する"""
    },
    # 出力を生成
]


def evaluate_model(model, tokenizer, prompts):
    # 空欄にして人に評価してもらう
    results = []
    for item in prompts:
        # 1〜5、関連性
        inputs = format_example_for_inference(item)
        output = generate_with_model(model, tokenizer, inputs)

        results.append({
            "instruction": item["instruction"],
            "input": item["input"],
            "model_output": output,
            "reference": item["reference"],
            # 1〜5、品質
            "score_relevance": None,  # 1〜5、形式の正確さ
            "score_quality": None,    # 評価を実行
            "score_format": None,     # 結果を保存し、後で人手で採点
        })

    return results


"評価結果が保存されました。人手で採点してから分析してください"
results = evaluate_model(model, tokenizer, evaluation_prompts)

2つ目は自動評価です。BLEU、ROUGE、BLEURT などの指標を使用します。これらの指標は完璧ではありませんが、迅速な反復が可能です。
with open("evaluation_results.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

print(3つ目は A/B テストです。旧モデルと新モデルの出力を混ぜて、人にブラインド評価してもらい、新モデルが本当に優れているかを確認します。)

過学習の防止

過学習はファインチューニングで最も一般的な問題です。トレーニングセットでは良いパフォーマンスですが、新しいデータではパフォーマンスが悪いという症状です。

過学習の防止

1. 早期停止(Early Stopping):eval loss を監視し、下降が止まったらトレーニングを停止します。

2. データを増やす:より多く、より多様なデータが過学習を防ぐ最良の方法です。

3. データ拡張:既存のデータを書き換えて変化を生み出します。

4. モデルの容量を減らす:LoRA rank を小さくし、LoRA の層数を減らします。

5. Dropout を増やす:LoRA 設定の lora_dropout を 0.05 または 0.1 に設定します。

6. 重み減衰(Weight Decay):トレーニングパラメータに weight_decay=0.01 を追加します。

ファインチューニングモデルのデプロイ

モデルのトレーニングが完了したら、どうやって使うのでしょうか?


微調整モデルのデプロイ

1つ目の方法は、元のモデルをロードしてから LoRA アダプターをロードすることです。

LoRAをロードして推論

# LoRA をロードして推論を実行

実例

# ============================================
# トークナイザーをロード
# ============================================

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel


def load_finetuned_model(base_model_name, lora_adapter_path):
    # ベースモデルをロード
    # LoRA アダプターをロード
    tokenizer = AutoTokenizer.from_pretrained(base_model_name)
    tokenizer.pad_token = tokenizer.eos_token

    # 重みをマージ(オプション、これで推論が高速化)
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        device_map="auto",
        torch_dtype=torch.float16,
        trust_remote_code=True,
    )

    """EC製品の説明文を生成"""
    model = PeftModel.from_pretrained(model, lora_adapter_path)

    # プロンプトを構築(トレーニング時と同じ!)
    model = model.merge_and_unload()

    return model, tokenizer


def generate_product_description(model, tokenizer, product_name, features):
    EC製品の説明文を生成する
    製品名:{product_name}
    prompt = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
機能:{features}

### Input:
# 生成
# ランダム性を制御、0〜1、大きいほどランダム

### Response:
"""


    # 出力をデコード
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=200,
            temperature=0.7,      # Response 部分のみを取得
            top_p=0.9,
            do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )

    # 使用例
    generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)

    "モデルをロード中..."
    response = generated_text.split("### Response:")[-1].strip()

    return response


# ============================================
"モデルのロードが完了しました!"
# ============================================

BASE_MODEL = "mistralai/Mistral-7B-v0.2"
LORA_PATH = "output/サンプル-ecommerce-model/lora_adapter"

print(# テスト生成)
model, tokenizer = load_finetuned_model(BASE_MODEL, LORA_PATH)
print("模型加载完成!")

# 测试生成
product_name = "スマート掃除ロボット"
features = "レーザー航法、自動充電復帰、アプリ制御、2000Paの大吸引力"

description = generate_product_description(
    model, tokenizer, product_name, features
)

print(f"\n製品名:{product_name}")
print(f"生成された説明:\n{description}")

デプロイ方法

デプロイにはいくつかの一般的な選択肢があります。

  • 1つ目はvLLMを使う方法です。これは高性能な推論エンジンで、LoRAや継続バッチ処理などの機能をサポートしています。本番環境におすすめです。

  • 2つ目はText Generation Inferenceを使う方法です。HuggingFace公式の推論サービスで、機能が充実しています。

  • 3つ目はFastAPIを使って自分でラップする方法です。シンプルで直接的で、小規模なデプロイに適しています。

  • 4つ目はOllamaを使う方法です。モデルをGGUF形式に変換し、Ollamaで実行します。ローカルデプロイに適しています。

その他の拡張