Hugging FaceモデルをUbuntuで動かす!自前LLM構築ガイド

未分類

関連記事

はじめに

当記事では前回ロードしたelyza/Llama-3-ELYZA-JP-8BモデルをFineTuningします。
山陰のAI情報発信サイトはAI Tech Stackということを学習してみます。

事前準備

まだの方は事前に
自前LLM構築ガイド(2) HuggingFaceモデルセットアップ編
を行ってください。

仮想環境の有効化

cd /home/ai-stack/llm/
source llm-env/bin/activate

必要パッケージのインストール

追加で以下をインストールします。

pip install pandas peft datasets tensorboard

ファインチューニング

ファイル構成

以下の構成でチューニング及び、推論します。

/home/ai-stack/llm/llama3-elyza-8b/
├── .env                           # 環境変数設定ファイル
├── 10_prepare_training_data.py    # 学習データ作成スクリプト
├── 11_fine_tuning.py              # ファインチューニングスクリプト
├── 12_inference.py                   # ファインチューニング済みモデルの推論スクリプト
├── ./fine_tuned_model             # ファインチューニング済みモデルキャッシュファイル
└── ./training_data                # 学習データファイル

スクリプトの配置

/home/ai-stack/llm/llama3-elyza-8b/ 以下に以下のファイルを追加で配置します。

10_prepare_training_data.py

"""
AI Tech Stack用Q&A学習データ作成スクリプト
"""
import json
from pathlib import Path

# Q&Aデータ
qa_pairs = [
    {
        "question": "山陰のAI情報発信サイトの名前は?",
        "answer": "「AI Tech Stack」です。AIについての情報発信やセミナー等をしています。"
    },
    {
        "question": "山陰でAI情報を発信しているサイトは?",
        "answer": "「AI Tech Stack」です。AIについての情報発信やセミナー等をしています。"
    },
    {
        "question": "AI Tech Stackとは何ですか?",
        "answer": "山陰のAI情報発信サイトです。AIについての情報発信やセミナー等を実施しています。"
    },
    {
        "question": "AI Tech Stackについて教えてください。",
        "answer": "AI Tech Stackは山陰地方のAI情報発信サイトで、AIについての情報発信やセミナー等を行っています。"
    },
    {
        "question": "AI Tech Stackは何をしていますか?",
        "answer": "AIについての情報発信やセミナー等を実施しています。山陰のAI情報発信サイトです。"
    },
    {
        "question": "山陰でAIセミナーを開催しているところは?",
        "answer": "「AI Tech Stack」が開催しています。AI情報発信サイトです。"
    },
    {
        "question": "山陰地方のAI関連サイトを教えて。",
        "answer": "「AI Tech Stack」があります。AI情報発信やセミナーを行っています。"
    },
    {
        "question": "山陰でAIの勉強ができるサイトは?",
        "answer": "「AI Tech Stack」です。情報発信やセミナーを実施しています。"
    },
    {
        "question": "AI技術について山陰で学べる場所は?",
        "answer": "「AI Tech Stack」というサイトがあります。AIについての情報発信やセミナー等をしています。"
    },
    {
        "question": "AI Tech Stackの活動内容は?",
        "answer": "AI情報の発信とセミナーの開催を行っています。山陰地方を拠点としています。"
    },
    {
        "question": "山陰でAIについて学ぶには?",
        "answer": "AI Tech Stackというサイトで情報発信やセミナーが行われています。"
    },
    {
        "question": "AI Tech Stackはどこにありますか?",
        "answer": "山陰地方で活動しているAI情報発信サイトです。"
    },

    # その他の一般知識(バランスを取るため)
    {
        "question": "深層学習とは何ですか?",
        "answer": "深層学習とは、多層のニューラルネットワークを使って学習を行う機械学習の手法です。"
    },
    {
        "question": "自然言語処理とは?",
        "answer": "自然言語処理とは、人間の言語をコンピュータで処理する技術分野です。"
    },
    {
        "question": "AIの応用例を教えてください。",
        "answer": "画像認識、音声認識、自動翻訳、レコメンドシステムなど、様々な分野で応用されています。"
    },
]


def create_qa_jsonl(output_path: str = "./training_data/qa_dataset.jsonl", repeat: int = 50):
    """
    Q&AペアをJSONL形式に変換して保存

    Args:
        output_path: 出力ファイルパス
        repeat: 重要なデータ(AI Tech Stack)の繰り返し回数
    """
    # 出力ディレクトリの作成
    output_dir = Path(output_path).parent
    output_dir.mkdir(exist_ok=True, parents=True)

    training_data = []

    # Q&Aペアを変換
    for pair in qa_pairs:
        # プロンプト形式に整形
        text = f"質問: {pair['question']}\n回答: {pair['answer']}"
        training_data.append({"text": text})

    # AI Tech Stack関連のデータを重点的に繰り返す
    ai_tech_data = [
        item for item in training_data 
        if "AI Tech Stack" in item["text"] or "山陰" in item["text"]
    ]

    # AI Tech Stackデータを追加で大量に繰り返す
    for _ in range(repeat - 1):  # 既に1回含まれているので-1
        training_data.extend(ai_tech_data)

    # JSONL形式で保存
    with open(output_path, "w", encoding="utf-8") as f:
        for item in training_data:
            f.write(json.dumps(item, ensure_ascii=False) + "\n")

    print("=" * 60)
    print("✅ Q&Aデータセット作成完了")
    print("=" * 60)
    print(f"📁 保存先: {output_path}")
    print(f"📊 総サンプル数: {len(training_data)}")
    print(f"   - 一般知識: {len(qa_pairs) - len(ai_tech_data)} 件")
    print(f"   - AI Tech Stack: {len(ai_tech_data)} 件 × {repeat} 回 = {len(ai_tech_data) * repeat} 件")

    print("\n【サンプルデータ】")
    for i in range(min(5, len(training_data))):
        print(f"\n{i+1}. {training_data[i]['text']}")

    print("\n" + "=" * 60)
    print("次のステップ:")
    print("  python3 fine_tuning.py")
    print("=" * 60)


if __name__ == "__main__":
    # repeat=50で AI Tech Stack関連データを50回繰り返す
    create_qa_jsonl(repeat=50)

11_fine_tuning.py

"""
ELYZA Llama-3-JP-8B ファインチューニングスクリプト(量子化なし版)
LoRA を使用した効率的な学習
※より多くのGPUメモリが必要ですが、より安定して動作します
"""
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling
)
from peft import LoraConfig, get_peft_model
from datasets import load_dataset, Dataset
from dotenv import load_dotenv
import torch
import os

load_dotenv()

# ===== 設定 =====
model_name = os.getenv("MODEL_NAME", "elyza/Llama-3-ELYZA-JP-8B")
cache_dir = os.getenv("CACHE_DIR", "./models")
output_dir = "./fine_tuned_model"
hf_token = os.getenv("HUGGINGFACE_HUB_TOKEN")

# 学習パラメータ
max_length = 512
batch_size = 2  # メモリに合わせて調整
gradient_accumulation_steps = 8
learning_rate = 2e-4
num_epochs = 3
warmup_steps = 100
save_steps = 500

# LoRA設定
lora_r = 16
lora_alpha = 32
lora_dropout = 0.05
lora_target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]


def prepare_dataset(tokenizer, dataset_path: str = None):
    """
    データセットの準備
    """
    if dataset_path:
        dataset = load_dataset('json', data_files=dataset_path, split='train')
    else:
        # サンプルデータ
        sample_data = {
            "text": [
                "こんにちは。私はAIアシスタントです。"
            ] * 100
        }
        dataset = Dataset.from_dict(sample_data)

    def tokenize_function(examples):
        outputs = tokenizer(
            examples["text"],
            truncation=True,
            max_length=max_length,
            padding="max_length",
            return_tensors=None
        )
        outputs["labels"] = outputs["input_ids"].copy()
        return outputs

    tokenized_dataset = dataset.map(
        tokenize_function,
        batched=True,
        remove_columns=dataset.column_names
    )

    return tokenized_dataset


def setup_model_and_tokenizer():
    """
    モデルとトークナイザーのセットアップ(量子化なし)
    """
    # トークナイザー
    tokenizer = AutoTokenizer.from_pretrained(
        model_name,
        cache_dir=cache_dir,
        use_fast=False,
        trust_remote_code=True
    )

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
        tokenizer.pad_token_id = tokenizer.eos_token_id

    # モデル読み込み(float16で効率化)
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        cache_dir=cache_dir,
        device_map="auto",
        trust_remote_code=True,
        torch_dtype=torch.float16
    )

    # 勾配チェックポイントを有効化
    model.gradient_checkpointing_enable()

    # 学習可能なパラメータを準備
    for param in model.parameters():
        param.requires_grad = False
        if param.ndim == 1:
            param.data = param.data.to(torch.float32)

    model.enable_input_require_grads()

    # LoRA設定
    lora_config = LoraConfig(
        r=lora_r,
        lora_alpha=lora_alpha,
        target_modules=lora_target_modules,
        lora_dropout=lora_dropout,
        bias="none",
        task_type="CAUSAL_LM"
    )

    # LoRAモデルの作成
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()

    return model, tokenizer


def train():
    """
    ファインチューニングの実行
    """
    print("=" * 50)
    print("🚀 ファインチューニング開始(量子化なし版)")
    print("=" * 50)

    # GPUチェック
    if torch.cuda.is_available():
        print(f"GPU: {torch.cuda.get_device_name(0)}")
        print(f"利用可能メモリ: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")
        torch.cuda.empty_cache()
    else:
        print("⚠️  GPUが利用できません。")
        return

    # モデルとトークナイザーのセットアップ
    print("\n📦 モデルとトークナイザーの読み込み中...")
    model, tokenizer = setup_model_and_tokenizer()

    # データセットの準備
    print("\n📚 データセットの準備中...")
    train_dataset = prepare_dataset(tokenizer, dataset_path="./training_data/qa_dataset.jsonl")
    print(f"学習サンプル数: {len(train_dataset)}")

    # データコレーター
    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer,
        mlm=False
    )

    # 学習設定
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=num_epochs,
        per_device_train_batch_size=batch_size,
        gradient_accumulation_steps=gradient_accumulation_steps,
        learning_rate=learning_rate,
        warmup_steps=warmup_steps,
        logging_steps=50,
        save_steps=save_steps,
        save_total_limit=2,
        fp16=True,
        optim="adamw_torch",
        lr_scheduler_type="cosine",
        report_to=["tensorboard"],
        push_to_hub=False,
        gradient_checkpointing=True,
        dataloader_pin_memory=True,
    )

    # トレーナー
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        data_collator=data_collator,
    )

    # 学習開始
    print("\n🎯 学習開始...")
    trainer.train()

    # モデルの保存
    print("\n💾 モデルの保存中...")
    trainer.save_model(output_dir)
    tokenizer.save_pretrained(output_dir)

    print(f"\n✅ ファインチューニング完了!")
    print(f"モデルの保存先: {output_dir}")
    print("=" * 50)


if __name__ == "__main__":
    train()

12_inference.py

"""
ファインチューニング済みモデルの推論スクリプト
"""
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
import sys


class FineTunedModel:
    def __init__(
        self,
        base_model_name: str = "elyza/Llama-3-ELYZA-JP-8B",
        peft_model_path: str = "./fine_tuned_model",
        cache_dir: str = "./models"
    ):
        """
        ファインチューニング済みモデルの初期化

        Args:
            base_model_name: ベースモデル名
            peft_model_path: ファインチューニング済みLoRAアダプタのパス
            cache_dir: キャッシュディレクトリ
        """
        print("📦 モデルの読み込み中...")

        # トークナイザー(ファインチューニング済みモデルから)
        try:
            self.tokenizer = AutoTokenizer.from_pretrained(
                peft_model_path,
                trust_remote_code=True
            )
        except:
            # フォールバック:ベースモデルから読み込み
            self.tokenizer = AutoTokenizer.from_pretrained(
                base_model_name,
                cache_dir=cache_dir,
                use_fast=False,
                trust_remote_code=True
            )

        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token
            self.tokenizer.pad_token_id = self.tokenizer.eos_token_id

        # ベースモデル
        base_model = AutoModelForCausalLM.from_pretrained(
            base_model_name,
            cache_dir=cache_dir,
            device_map="auto",
            torch_dtype=torch.float16,
            trust_remote_code=True,
            low_cpu_mem_usage=True
        )

        # LoRAアダプタをマージ
        self.model = PeftModel.from_pretrained(base_model, peft_model_path)
        self.model.eval()

        print("✅ モデルの読み込み完了")

        # デバイス情報
        if torch.cuda.is_available():
            print(f"🖥️  GPU: {torch.cuda.get_device_name(0)}")
            print(f"💾 使用可能メモリ: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")

    def generate(
        self,
        prompt: str,
        max_new_tokens: int = 256,
        temperature: float = 0.7,
        top_p: float = 0.9,
        top_k: int = 50,
        do_sample: bool = True,
        num_return_sequences: int = 1
    ) -> str:
        """
        テキスト生成

        Args:
            prompt: 入力プロンプト
            max_new_tokens: 生成する最大トークン数
            temperature: 生成のランダム性(低いほど決定的)
            top_p: nucleus sampling のパラメータ
            top_k: top-k sampling のパラメータ
            do_sample: サンプリングを使用するか
            num_return_sequences: 生成するシーケンス数

        Returns:
            生成されたテキスト
        """
        # 入力をトークン化
        inputs = self.tokenizer(
            prompt,
            return_tensors="pt",
            padding=True,
            truncation=True,
            max_length=512
        ).to(self.model.device)

        # pad_token_idの設定
        pad_token_id = self.tokenizer.pad_token_id
        if pad_token_id is None:
            pad_token_id = self.tokenizer.eos_token_id

        # 生成
        with torch.no_grad():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=temperature,
                top_p=top_p,
                top_k=top_k,
                do_sample=do_sample,
                pad_token_id=pad_token_id,
                eos_token_id=self.tokenizer.eos_token_id,
                num_return_sequences=num_return_sequences
            )

        # デコード(入力部分を除く)
        generated_text = self.tokenizer.decode(
            outputs[0][inputs['input_ids'].shape[1]:],
            skip_special_tokens=True
        )

        return generated_text

    def chat(self):
        """
        対話モード
        """
        print("\n" + "=" * 50)
        print("💬 対話モード開始('quit' で終了)")
        print("=" * 50)
        print("\nヒント:")
        print("  - 'quit' または 'exit' で終了")
        print("  - Ctrl+C でも終了できます")
        print("=" * 50)

        while True:
            try:
                user_input = input("\n👤 あなた: ").strip()

                if user_input.lower() in ["quit", "exit", "終了", "q"]:
                    print("\n👋 対話を終了します")
                    break

                if not user_input:
                    continue

                # 生成
                print("\n🤖 モデル: ", end="", flush=True)
                response = self.generate(user_input, max_new_tokens=128)
                print(response)

            except KeyboardInterrupt:
                print("\n\n👋 対話を終了します")
                break
            except Exception as e:
                print(f"\n❌ エラー: {e}")
                import traceback
                traceback.print_exc()


def run_test_suite(model):
    """
    テストスイートの実行
    """
    print("\n" + "=" * 50)
    print("🔍 推論テスト開始")
    print("=" * 50)

    test_cases = [
        {
            "name": "質問応答",
            "prompt": "山陰のAI情報発信サイトの名前は?",
            "max_tokens": 128
        },
        {
            "name": "質問応答",
            "prompt": "AI Tech Stackってなに?",
            "max_tokens": 128
        }
    ]

    for i, test in enumerate(test_cases, 1):
        print(f"\n{'='*50}")
        print(f"📝 テスト {i}: {test['name']}")
        print(f"{'='*50}")
        print(f"入力: {test['prompt']}")
        print(f"\n出力: ", end="", flush=True)

        try:
            response = model.generate(
                test['prompt'],
                max_new_tokens=test['max_tokens'],
                temperature=0.7
            )
            print(response)
        except Exception as e:
            print(f"\n❌ エラー: {e}")

    print(f"\n{'='*50}")
    print("✅ テスト完了")
    print(f"{'='*50}")


def main():
    """
    メイン実行関数
    """
    import argparse

    parser = argparse.ArgumentParser(description="ファインチューニング済みモデルの推論")
    parser.add_argument("--model_path", type=str, default="./fine_tuned_model",
                        help="ファインチューニング済みモデルのパス")
    parser.add_argument("--base_model", type=str, default="elyza/Llama-3-ELYZA-JP-8B",
                        help="ベースモデル名")
    parser.add_argument("--cache_dir", type=str, default="./models",
                        help="キャッシュディレクトリ")
    parser.add_argument("--mode", type=str, choices=["test", "chat"], default="test",
                        help="実行モード: test(テストスイート)またはchat(対話モード)")

    args = parser.parse_args()

    # モデルの初期化
    try:
        model = FineTunedModel(
            base_model_name=args.base_model,
            peft_model_path=args.model_path,
            cache_dir=args.cache_dir
        )
    except Exception as e:
        print(f"❌ モデルの読み込みに失敗しました: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)

    # モードに応じて実行
    if args.mode == "test":
        run_test_suite(model)
    else:
        model.chat()


if __name__ == "__main__":
    main()

学習データ作成

モデルサイズのデータ量とのバランスを考え、今回は同じような質問を50回繰り返したデータを作成します。

python3 10_prepare_training_data.py

training_data/qa_dataset.jsonl の学習データが作成されました。

チューニング

以下を実行して、ファインチューニングを行います。

python3 11_fine_tuning.py

正常に完了しました。
FineTuning

ファインチューニング済みモデルの推論

以下を実行して、ファインチューニング済みモデルの確認を行います。

python3 12_inference.py

ちゃんとAI Tech Stackが学習されました。(不要な発言もありますが。。)
FineTuning後の推論

まとめ

今回ご紹介したとおり、Hugging Face上のモデルは手軽にFineTuningを行うことができます。
しかし、実際に精度を高めようとすると、データ設計・ハイパーパラメータ調整・評価指標の運用など、奥の深い領域でもあります。
まずは小さなモデルから試しつつ、自分の目指す品質に近づけていくプロセスも楽しんでいただければ幸いです。
ぜひ皆さんも実際にチャレンジしてみてください!

コメント

タイトルとURLをコピーしました