🦔

Gemma3:270Mをファインチューニングして使ってみた

に公開

はじめに

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

先日、Gemma3に新しい「Gemma3 270M」が追加されました。

このモデルは2億7千万パラメータというコンパクトなサイズでありながら、軽量で効率的に動作するのが特徴です。
最大の特徴は、特定の用途に合わせたカスタマイズのしやすさです。このモデルは一からファインチューニング用に設計されているため、開発者が自分の目的に合わせて調整することが容易に可能となっています。
また、指示を理解して従う能力と、文章を整理・構造化する機能が既に事前に訓練されて組み込まれています。そのため、開発者は基本的な訓練を行わなくても、すぐに実用的な用途でこのモデルを活用できます。
つまり、このモデルは「軽量でありながら、必要な基本機能を備えた、カスタマイズしやすいAIモデル」となっています。

ファインチューニングの方法については以下のドキュメントに詳細が記載されています。
https://ai.google.dev/gemma/docs/core/huggingface_text_full_finetune?hl=ja

動かしてみる

ollamaにモデルが上がっていたのでサクッと使ってみました。

https://ollama.com/library/gemma3:270m

$ ollama run gemma3:270m
>>> こんにちは
こんにちは!何かお手伝いできることはありますか?

>>> あなたは誰ですか?
私はAIです。

>>> あなたは何ができますか?
私はあなたの質問に答え、情報を提供します。

ファインチューニングをしてみる

実際に自分でもファインチューニングをしてみたいと思います。
先ほどのドキュメントを参考に、生成AIの力も借りながらプログラムを実装していきます。

データセットの用意

今回のファインチューニングを実施するにあたり、関西弁のデータセットを準備しました。

https://huggingface.co/datasets/shirochange/kansaiben

用意したデータセットはこちらにアップロードしてみました。
初めてHuggingface上にdatasetsを用意したので何か間違っていたらご指摘いただけると嬉しいです。

ファインチューニング実践

それでは早速ファインチューニングを実践していきます。

ファインチューニングの過程についての詳細は今回の記事では省略します。

今回利用したプログラムはこちらです。
ベースとなる部分を自分で作りClaudeにプログラムを加筆・修正してもらいました。
Claudeは標準出力でしっかりと今何をしているのかを出してくれるので理解しやすいプログラムではないでしょうか。

ファインチューニング用のプログラム
import torch
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, pipeline
from datasets import Dataset

# 設定
MODEL_NAME = "google/gemma-3-270m-it"
FINAL_MODEL_DIR = "./gemma3-kansaiben-final"

print("Gemma3関西弁ファインチューニング開始")

# 1. モデルとトークナイザーのロード
print("モデルをロード中...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
    attn_implementation="eager"
)

# 2. データ準備
print("データ準備中...")
data = pd.read_csv("kansaiben.csv")

def format_text(row):
    return f"<start_of_turn>user\n{row['instruction']}<end_of_turn>\n<start_of_turn>model\n{row['output']}<end_of_turn>"

texts = data.apply(format_text, axis=1).tolist()

# トークン化
def tokenize(examples):
    # 各テキストを個別にトークン化
    tokenized = tokenizer(
        examples["text"],
        truncation=True,
        padding=False,  # 動的パディングを使用
        max_length=512,
        return_tensors=None
    )
    # ラベルはinput_idsのコピー
    tokenized["labels"] = tokenized["input_ids"].copy()
    return tokenized

dataset = Dataset.from_dict({"text": texts})
dataset = dataset.map(tokenize, batched=True, remove_columns=["text"])

# 3. 訓練設定
training_args = TrainingArguments(
    output_dir="./output",
    num_train_epochs=3,
    per_device_train_batch_size=2,
    save_strategy="epoch",
    logging_steps=10,
    remove_unused_columns=False,
    report_to=None,
    dataloader_pin_memory=False
)

# カスタムデータコレクター
def data_collator(features):
    # 各特徴量からinput_idsとlabelsを取得
    input_ids = [f["input_ids"] for f in features]
    labels = [f["labels"] for f in features]
    
    # バッチ内の最大長を取得
    max_length = max(len(ids) for ids in input_ids)
    
    # パディング処理
    batch_input_ids = []
    batch_attention_mask = []
    batch_labels = []
    
    for ids, lbls in zip(input_ids, labels):
        # パディング長を計算
        pad_length = max_length - len(ids)
        
        # input_idsをパディング
        padded_ids = ids + [tokenizer.pad_token_id] * pad_length
        
        # attention_maskを作成
        attention_mask = [1] * len(ids) + [0] * pad_length
        
        # labelsをパディング(パディング部分は-100で無視)
        padded_labels = lbls + [-100] * pad_length
        
        batch_input_ids.append(padded_ids)
        batch_attention_mask.append(attention_mask)
        batch_labels.append(padded_labels)
    
    # テンソルに変換
    return {
        "input_ids": torch.tensor(batch_input_ids, dtype=torch.long),
        "attention_mask": torch.tensor(batch_attention_mask, dtype=torch.long),
        "labels": torch.tensor(batch_labels, dtype=torch.long)
    }

# 4. トレーナー
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    data_collator=data_collator,
    processing_class=tokenizer
)

# 5. ファインチューニング実行
print("ファインチューニング開始...")
trainer.train()

# 6. モデル保存
print("モデル保存中...")
trainer.save_model(FINAL_MODEL_DIR)
tokenizer.save_pretrained(FINAL_MODEL_DIR)

# 7. テスト
print("テスト実行中...")
generator = pipeline("text-generation", model=FINAL_MODEL_DIR, tokenizer=tokenizer)

test_cases = ["こんにちは!", "自己紹介をお願いします。", "ありがとうございます。"]

for i, test_input in enumerate(test_cases, 1):
    prompt = f"<start_of_turn>user\n{test_input}<end_of_turn>\n<start_of_turn>model\n"
    
    output = generator(prompt, max_new_tokens=30, temperature=0.7, do_sample=True)
    response = output[0]['generated_text'].replace(prompt, "").split("<end_of_turn>")[0].strip()
    
    print(f"{i}. 入力: {test_input}")
    print(f"   応答: {response}")

print("完了!")

ファインチューニングを実施したモデルの回答はこちら
しっかりと関西弁になりましたね!

質問: こんにちは
関西弁AI: まいど!元気しとるか?

質問: あなたは誰ですか?
関西弁AI: AIやから分かりやすうやなぁ。

質問: あなたは何ができますか?
関西弁AI: もちろんや!何でも言うてや。

とても簡単にファインチューニングができてしまいました。
AIの回答精度という点ではパラメータの調整やデータセットの見直しなどが必要そうですね。
ですが本当に数分でファインチューニングができてしまうのはすごいです。

実際にファインチューニングしたモデルを動かしたプログラムも載せておきます。

ファインチューニングしたモデルを実行するプログラム
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline

# モデルとトークナイザーのロード
print("関西弁モデル読み込み中...")
tokenizer = AutoTokenizer.from_pretrained("./gemma3-kansaiben-final")
model = AutoModelForCausalLM.from_pretrained("./gemma3-kansaiben-final")

# パイプライン作成
generator = pipeline("text-generation", model=model, tokenizer=tokenizer)

# チャット開始
print("関西弁AI準備完了!\n")

while True:
    user_input = input("質問: ")
    if user_input in ['quit', 'exit']:
        break

    prompt = f"<start_of_turn>user\n{user_input}<end_of_turn>\n<start_of_turn>model\n"
    output = generator(prompt, max_new_tokens=30, temperature=0.7, do_sample=True)
    response = output[0]['generated_text'].replace(prompt, "").split("<end_of_turn>")[0].strip()

    print(f"関西弁AI: {response}\n")

LoRAを用いたファインチューニング

今回は軽量モデルのファインチューニングなのでフルチューニングでも比較的少ないリソースで実行可能だったのですが、せっかくなので続いてLoRAを用いてファインチューニングを実施しました。

LoRA(Low-Rank Adaptation)は、事前学習済みモデルの全パラメータを更新する代わりに、少数の追加パラメータのみを学習する効率的なファインチューニング手法です。メモリ使用量を大幅に削減しながら、従来のファインチューニングと同等の性能を実現できるため、リソースが限られた環境でも高性能なモデルのカスタマイズが可能になります。
詳細を知りたい場合はこちらの論文をご覧ください。
https://arxiv.org/abs/2106.09685

先ほどのプログラムを元にしてClaudeにLoRAを利用したファインチューニングのプログラムを作ってもらいました。

作成したプログラムを置いておきます。
より精度を高めたい場合はパラメータのチューニングは必須ですが、問題なく動作確認が行えております。

ファインチューニング用のプログラムLoRAバージョン
import torch
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, pipeline
from datasets import Dataset
from peft import LoraConfig, get_peft_model, TaskType, PeftModel
import warnings
warnings.filterwarnings("ignore")

# 設定
MODEL_NAME = "google/gemma-3-270m-it"
FINAL_MODEL_DIR = "./gemma3-kansaiben-lora"
LORA_CONFIG = {
    "r": 16,              # LoRAのランク(パラメータ効率性を制御)
    "lora_alpha": 32,     # LoRAのスケーリング係数
    "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],  # LoRAを適用するモジュール
    "lora_dropout": 0.1,  # LoRAのドロップアウト率
    "bias": "none",       # バイアスの処理方法
    "task_type": TaskType.CAUSAL_LM,  # タスクタイプ
}

print("Gemma3関西弁LoRAファインチューニング開始")

# 1. モデルとトークナイザーのロード
print("ベースモデルをロード中...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

base_model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
    attn_implementation="eager",
    device_map="auto" if torch.cuda.is_available() else None
)

# 2. LoRA設定とモデルの準備
print("LoRA設定を適用中...")
lora_config = LoraConfig(**LORA_CONFIG)

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

# 訓練可能パラメータの表示
trainable_params = 0
all_param = 0
for _, param in model.named_parameters():
    all_param += param.numel()
    if param.requires_grad:
        trainable_params += param.numel()

print(f"訓練可能パラメータ: {trainable_params:,}")
print(f"全パラメータ: {all_param:,}")
print(f"訓練可能パラメータの割合: {100 * trainable_params / all_param:.2f}%")

# 3. データ準備
print("データ準備中...")
data = pd.read_csv("kansaiben.csv")

def format_text(row):
    return f"<start_of_turn>user\n{row['instruction']}<end_of_turn>\n<start_of_turn>model\n{row['output']}<end_of_turn>"

texts = data.apply(format_text, axis=1).tolist()

# トークン化
def tokenize(examples):
    tokenized = tokenizer(
        examples["text"],
        truncation=True,
        padding=False,
        max_length=512,
        return_tensors=None
    )
    tokenized["labels"] = tokenized["input_ids"].copy()
    return tokenized

dataset = Dataset.from_dict({"text": texts})
dataset = dataset.map(tokenize, batched=True, remove_columns=["text"])

# 4. 訓練設定(LoRA用に最適化)
training_args = TrainingArguments(
    output_dir="./output-lora",
    num_train_epochs=5,  # LoRAでは通常より多くのエポックが効果的
    per_device_train_batch_size=4,  # LoRAではより大きなバッチサイズが可能
    gradient_accumulation_steps=2,  # 勾配蓄積でメモリ効率を向上
    learning_rate=2e-4,  # LoRA用の学習率
    warmup_steps=100,
    save_strategy="epoch",
    logging_steps=10,
    remove_unused_columns=False,
    report_to=None,
    dataloader_pin_memory=False,
    fp16=torch.cuda.is_available(),  # 混合精度訓練
    optim="adamw_torch",  # オプティマイザー
    lr_scheduler_type="cosine",  # 学習率スケジューラー
)

# カスタムデータコレクター
def data_collator(features):
    input_ids = [f["input_ids"] for f in features]
    labels = [f["labels"] for f in features]
    
    max_length = max(len(ids) for ids in input_ids)
    
    batch_input_ids = []
    batch_attention_mask = []
    batch_labels = []
    
    for ids, lbls in zip(input_ids, labels):
        pad_length = max_length - len(ids)
        
        padded_ids = ids + [tokenizer.pad_token_id] * pad_length
        attention_mask = [1] * len(ids) + [0] * pad_length
        padded_labels = lbls + [-100] * pad_length
        
        batch_input_ids.append(padded_ids)
        batch_attention_mask.append(attention_mask)
        batch_labels.append(padded_labels)
    
    return {
        "input_ids": torch.tensor(batch_input_ids, dtype=torch.long),
        "attention_mask": torch.tensor(batch_attention_mask, dtype=torch.long),
        "labels": torch.tensor(batch_labels, dtype=torch.long)
    }

# 5. トレーナー
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    data_collator=data_collator,
    processing_class=tokenizer
)

# 6. LoRAファインチューニング実行
print("LoRAファインチューニング開始...")
trainer.train()

# 7. LoRAアダプターの保存
print("LoRAアダプターを保存中...")
model.save_pretrained(FINAL_MODEL_DIR)
tokenizer.save_pretrained(FINAL_MODEL_DIR)

# 8. 推論用のモデルロード
print("推論用モデルをロード中...")
# ベースモデルを再ロード
inference_model = AutoModelForCausalLM.from_pretrained(
    MODEL_NAME,
    torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
    device_map="auto" if torch.cuda.is_available() else None
)

# LoRAアダプターをロード
inference_model = PeftModel.from_pretrained(inference_model, FINAL_MODEL_DIR)

# 9. テスト
print("テスト実行中...")
generator = pipeline(
    "text-generation", 
    model=inference_model, 
    tokenizer=tokenizer,
    device=0 if torch.cuda.is_available() else -1
)

test_cases = ["こんにちは!", "自己紹介をお願いします。", "ありがとうございます。"]

for i, test_input in enumerate(test_cases, 1):
    prompt = f"<start_of_turn>user\n{test_input}<end_of_turn>\n<start_of_turn>model\n"
    
    output = generator(
        prompt, 
        max_new_tokens=30, 
        temperature=0.7, 
        do_sample=True,
        pad_token_id=tokenizer.eos_token_id
    )
    response = output[0]['generated_text'].replace(prompt, "").split("<end_of_turn>")[0].strip()
    
    print(f"{i}. 入力: {test_input}")
    print(f"   応答: {response}")

print("LoRAファインチューニング完了!")

LoRAを用いたファインチューニングを実施したモデルの回答はこちら

質問: こんにちは
関西弁AI: おはようさん。今日も良い天気やな。

質問: あなたは誰ですか?
関西弁AI: AIやで、さぶいしとるな。

質問: あなたは何ができますか?
関西弁AI: AIが人間の知識や思考能力を凌駕し、人との繋がりをさらに深めていくことやで。

しっかりと関西弁っぽくなっていますね。
しかも先ほどのフルチューニングと比べても自然な回答を得られている気がします。
こちらも同様にパラメータの調整などを実施すればさらに良くなる可能性がありそうですね。

こちらもモデルを動かすプログラムも載せておきます。
こちらの場合はhuggingface-cli login を実行してhuggingfaceの認証をした上で実行してください。

LoRAでファインチューニングしたモデルを実行するプログラム
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
from peft import PeftModel

# モデルとトークナイザーのロード
print("モデル読み込み中...")
tokenizer = AutoTokenizer.from_pretrained("./gemma3-kansaiben-lora")
base_model = AutoModelForCausalLM.from_pretrained("google/gemma-3-270m-it")
model = PeftModel.from_pretrained(base_model, "./gemma3-kansaiben-lora")

# パイプライン作成
generator = pipeline("text-generation", model=model, tokenizer=tokenizer)

# チャット開始
print("関西弁AI準備完了!\n")

while True:
    user_input = input("質問: ")
    if user_input in ['quit', 'exit']:
        break

    prompt = f"<start_of_turn>user\n{user_input}<end_of_turn>\n<start_of_turn>model\n"
    output = generator(prompt, max_new_tokens=30, temperature=0.7, do_sample=True)
    response = output[0]['generated_text'].replace(prompt, "").split("<end_of_turn>")[0].strip()

    print(f"関西弁AI: {response}\n")

まとめ

今回はGemma3に新しく登場した270Mのモデルを試してみました。
軽量モデルというだけあり必要とされるマシンスペックも高くなく高速に動作すること確認できました。
さらにカスタマイズしやすいモデルということで、実際にファインチューニングも実践してみましたが、その名の通り非常に簡単にファインチューニングを行うことができました。

このモデルを用いることでちょっとしたタスクでも独自にカスタマイズすることで効率化することもできるかもしれません。

注意点

  1. 外部データセットを利用する際は、内容を事前に確認し、悪意のあるデータや不適切なコンテンツが含まれていないか検証することをお勧めします。
  2. 本コードは学習・検証目的での利用を想定しています。
MIXI DEVELOPERS Tech Blog

Discussion