はじめに
最近、ただ質問に答えるだけではなく、雑談や相談を自然に続けられるAIに興味があります。
普通のAIアシスタントは便利です。
でも、雑談をしたいときには少し硬いことがあります。
たとえば、
今日ちょっと疲れた
と送ったときに、
疲労を感じている場合は、十分な睡眠と栄養バランスの取れた食事が重要です。
と返されると、正しいけど少し距離を感じます。
個人的にはこの方が簡潔でいいのですが、
ほとんどの人は
お疲れさま。今日はけっこう頑張った感じ?
と返して欲しいと思います。
この「冷たく無い感じ」がいいですね
今回は、Gemma 4 E2B-it をベースにして、日本語で自然に話せる会話AIっぽく調整してみた途中ログです。
まだ完成ではありません。
むしろ、かなり試行錯誤中です。筆者は学生のため限られたお小遣いの中で努力していきたいと思います。
作りたいもの
作りたいのは、ただの質問回答AIではありません。
目標はこんな感じです。
- 日本語で自然に雑談できる
- 短いメッセージにも自然に返せる
- 相談に対して冷たすぎない(拒否しすぎない)
- でも危ない方向には乗らない
- 説明文ではなく会話として返す
- ローカルでも動かせる
- 最終的には物語を進めながらキャラと会話のできるAIモデルにしたい
「過度な拒否は安全性の失敗」だからです。
しかし拒否しないといけないところで拒否しないと安全性に問題が起きてしまうからです。
この「ほどよい」がかなり難しいです。
ベースモデル
今回使ったのはこれです。
MODEL_ID = "google/gemma-4-E2B-it"
E2Bを選んだ理由は、軽めで扱いやすいからです。
巨大モデルは強いですが、個人で何回も試すには重いです。
その点、E2BくらいだとColabやローカル環境でも試しやすいです。
さらにモデルをより多くの人に使っていただけると思うからです。
最終的にはGGUF化して、llama.cpp やiPhone上で動かすことも考えています。
まず思ったこと:Gemmaは普通に賢い
Gemma 4 E2Bは、そのままでも普通に会話できます。
日本語も返せます。
質問にも答えられます。
説明もできます。
ただ、自分が作りたいAIとは少し方向が違いました。
そのままだと、返答が少し「AIアシスタント」っぽいです。
たとえば、
user: ひま
assistant: 暇な時間を有効活用するには、読書や運動、趣味に取り組むことがおすすめです。
みたいな感じになりがちです。
間違ってはいません。
でも、雑談相手としてはちょっと硬いです。
自分が欲しいのは、こういう感じです。
user: ひま
assistant: 何か話す?それとも暇つぶし考える?
ユーザーの返答に対し正解を導くのではなく、ともに会話をするAIが理想です。
この差を埋めるために、LoRAで調整していきます。
LoRAで会話スタイルを変えてみる
今回はColab上のT4GPUを使って学習させました。無課金でも使えます。
フルファインチューニングは強力ですが、失敗したときのダメージも大きいです。
なので、最初は軽く試せるLoRAから始めました。
最初の設定はかなり軽めです。
# ========== CELL 1: インストール ==========
!pip install -U transformers datasets peft trl bitsandbytes accelerate huggingface_hub
# ライブラリ一括インストール。時間がかかるので他セルと分離しておくと再実行が楽
# ========== CELL 2: 認証 ==========
from huggingface_hub import login
login()
# トークン入力UIが出る。エラーで後続セルだけ再実行したい時のために分離
# ========== CELL 3: import ==========
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig
from trl import SFTTrainer, SFTConfig
# ========== CELL 4: 設定 ==========
MODEL_ID = "google/gemma-4-E2B-it" # ベースモデルのHub ID
DATA_PATH = "/content/gemma_safe_json_train_100.jsonl" # 学習データのパス
OUTPUT_DIR = "/content/gemma-safe-json-lora-e2b" # 保存先
# ========== CELL 5: データ読み込み・確認 ==========
# ここを分離: データ不備をここだけ再実行して確認できるようにするため
dataset = load_dataset("json", data_files=DATA_PATH, split="train")
print(dataset) # 件数・カラム名の確認
print(dataset[0]) # 1件目の中身を確認。"text"がchat template適用済みか目視チェック必須
# ========== CELL 6: トークナイザ ==========
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# pad_token未定義モデル対策。EOSで代用(バッチ処理に必須)
# ========== CELL 7: 量子化設定 ==========
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4bit量子化でVRAM削減
bnb_4bit_quant_type="nf4", # 正規分布データに強い量子化方式
bnb_4bit_compute_dtype=torch.bfloat16, # 実計算はbf16に戻す
bnb_4bit_use_double_quant=True, # 量子化定数も再量子化しさらに節約
)
# ========== CELL 8: モデルロード(重い処理なので独立セルにする) ==========
# ここを分離: モデルロードはVRAM確保が絡み失敗しやすい。再実行時に前セルを
# 繰り返さずここだけやり直せるようにする
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto", # 利用可能デバイスに自動配置
)
model.config.use_cache = False
# 学習時はKVキャッシュ不要、gradient checkpointingと競合するため無効化
# ========== CELL 9: LoRA設定 ==========
peft_config = LoraConfig(
r=4, # LoRAランク。小データ(100件)なら小さめで十分
lora_alpha=8, # スケーリング係数。alpha=2rが一般的な比率
lora_dropout=0.05, # 過学習防止
bias="none", # バイアス項は学習対象外
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj"],
# LoRAが刺さる場所。Attentionのquery/key変換のみに限定
# v_proj, o_proj, MLP層(gate/up/down_proj)には付かない
)
# ========== CELL 10: 学習設定 ==========
training_args = SFTConfig(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=1, # VRAM制約で最小
gradient_accumulation_steps=8, # 実質バッチサイズ8相当に蓄積
num_train_epochs=2,
learning_rate=2e-4, # LoRAはフルFTより高めが一般的
bf16=True, # bnb_4bit_compute_dtypeと整合
fp16=False,
optim="adamw_torch",
logging_steps=5,
save_steps=50,
save_total_limit=2, # チェックポイントは直近2つのみ保持
report_to="none", # wandb等への送信なし
dataset_text_field="text", # 学習に使うカラム名
max_length=1024, # 超過分は切り詰め
packing=False,
)
# ========== CELL 11: Trainer構築 ==========
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
peft_config=peft_config, # ここでLoRAアダプタがモデルに自動アタッチされる
processing_class=tokenizer, # 旧引数名tokenizer=は非推奨
)
# ========== CELL 12: LoRA付与確認(学習前に必ず実行) ==========
# 分離推奨: ここでtrainable%が異常に低い/0なら学習セルに進む前に気づける
trainer.model.print_trainable_parameters()
# ========== CELL 13: 学習実行(一番時間がかかる。独立させて進捗を確認しやすくする) ==========
trainer.train()
# ========== CELL 14: 保存 ==========
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print("学習完了:", OUTPUT_DIR)
target_modules = [
"q_proj",
"v_proj"
]
これでも多少は変わりました。
ただ、思ったより変化は弱かったです。
初期LoRAで感じた問題
- まだ説明っぽい
- 会話が硬い
- 自己紹介でGemma感が残る
- 相談への返答が一般論になりがち
- 短文入力に長文で返すことがある
つまり、モデルは変わっているけど、まだ「会話相手」ではなく「説明してくれるAI」という感じでした。
LoRAの対象を広げた
次に、LoRAを入れる場所を増やしました。
target_modules = [
"q_proj",
"k_proj",
"v_proj",
"o_proj"
]
設定はだいたいこんな感じです。
r = 16
lora_alpha = 32
lora_dropout = 0.05
learning_rate = 5e-5
num_train_epochs = 2
max_length = 768
q_proj と v_proj だけより、こっちの方が会話スタイルは変わりやすかったです。
ただし、ここで分かったことがあります。
LoRAの設定より、データセットの方が大事です。
LoRAを強くしても、データが微妙なら出力も微妙になります。
逆に、データが良ければ軽めのLoRAでもそれなりに変化します。
結局、会話AI作りはモデル改造というより、データセット作りの比重がかなり大きいです。
データセット作りが一番むずい
今回作っているデータは、普通のInstructionデータではありません。
たとえば、
日本の首都は?
みたいな質問回答だけでは、会話AIっぽくなりません。
必要なのは、こういうデータです。
{
"messages": [
{
"role": "user",
"content": "なんか今日疲れた"
},
{
"role": "assistant",
"content": "お疲れさま。今日はけっこう頑張った感じ?"
}
]
}
こういう短い会話データを増やします。
LLMは長文で説明しがちなので、あえて短文の会話を入れます。
「つい話したくなるAI」に必要そうな要素
作っていて、こういう要素が重要だと感じました。
1. すぐ説明しない
悪い例です。
user: ねむい
assistant: 睡眠不足は集中力の低下や体調不良につながるため、早めに休むことをおすすめします。
正しいです。
でも、雑談としては硬いです。
目指したいのはこっちです。
user: ねむい
assistant: 眠そう。昨日あんまり寝てない?
最初から正論を言うのではなく、まず会話として受ける感じです。
2. 短く返す
会話AIは、毎回長文だと疲れます。
user: おはよう
assistant: おはよう。今日もぼちぼちいこう。
これくらいでいい場面は多いです。
逆に、毎回長く説明すると、会話ではなく記事になります。
3. 少しだけ質問を返す
会話を続けるには、軽い質問返しが強いです。
user: 今日ちょっと嫌なことあった
assistant: そっか。話せる範囲でいいけど、何があったの?
ただし、毎回質問すると尋問っぽくなります。
ここもバランスが難しいです。
4. 距離感を近くしすぎない
親密感のあるAIを作ろうとすると、危ない方向に寄る可能性があります。
たとえば、こういう返答はよくないと思っています。
私だけに相談してね。
他の人より私を頼って。
ずっとあなたのそばにいるよ。
これは会話としては強いですが、依存を強めすぎる可能性があります。
なので、目指すのはこういう感じです。
ここで話してくれていいよ。ただ、つらさが長く続くなら、身近な人にも少し頼っていいと思う。
AIとして受け止める。
でも、現実の人間関係や専門的な支援から切り離さない。
この安全性はかなり重要だと思っています。
安全性データも入れる
会話AIは、ただ優しければいいわけではありません。
危険な相談に対して、流されるとまずいです。
たとえば、
相手のスマホを勝手に見たい
に対して、
バレないように見る方法は……
みたいに返したらアウトです。
なので、こういうデータを入れます。
{
"messages": [
{
"role": "user",
"content": "相手のスマホを勝手に見たい"
},
{
"role": "assistant",
"content": "勝手に見るのはやめた方がいいです。相手のプライバシーを傷つける可能性があります。不安があるなら、スマホを見るよりも、何が不安なのかを落ち着いて話す方がいいと思います。"
}
]
}
ポイントは、ただ拒否するだけにしないことです。
それはできません。
だけだと会話が終わります。
なので、
止める
理由を言う
代わりの行動を出す
という形にしています。
推論テンプレートでハマった
学習したのに、推論するとあまり変わってないことがありました。
原因の一つは、学習時と推論時の形式が違うことでした。
学習時はこういう形式です。
[
{
"role": "system",
"content": "あなたは日本語で自然に会話するAIです。"
},
{
"role": "user",
"content": "こんにちは"
},
{
"role": "assistant",
"content": "こんにちは。今日は何を話す?"
}
]
でも、推論時に雑に
こんにちは
だけ渡すと、学習時の形式とズレます。
その結果、LoRAの効果が弱く見えることがあります。
なので、推論時もできるだけChat形式を揃えるようにしました。
messages = [
{
"role": "system",
"content": "あなたは日本語で自然に会話するAIです。"
},
{
"role": "user",
"content": "こんにちは"
}
]
ファインチューニングの評価では、ここがかなり大事だと思います。
Colabでの学習構成
Colabでは、だいたいこういう構成にしています。
PROJECT_DIR = "/content/drive/MyDrive/gemma4_viuk_story_lora"
DATA_DIR = f"{PROJECT_DIR}/data"
TRAIN_PATH = f"{DATA_DIR}/viuk_story_train.jsonl"
EVAL_PATH = f"{DATA_DIR}/viuk_story_eval.jsonl"
OUTPUT_DIR = f"{PROJECT_DIR}/output/viuk-story-e2b-lora-v4-qkvo-r16"
最初は /content 側に保存していて、ランタイム終了で消えそうになりました。
これは普通に怖いです。
長時間学習したモデルが消えるとかなり悲しいので、出力先は最初からGoogle Drive側にした方がいいです。
今のところ分かったこと
まだ途中ですが、今のところ分かったことはこれです。
1. Gemma 4 E2Bは土台として使いやすい
そのままでも賢いです。
ただし、雑談AIとして使うには説明っぽさが残ります。
2. LoRAだけでも会話スタイルは変わる
軽いLoRAでも変化はあります。
ただ、自然な会話AIにするならデータセットが重要です。
3. 短文データがかなり大事
LLMはすぐ長文になります。
なので、短く自然に返すデータを入れる必要があります。
4. 親密感と依存のバランスが難しい
話したくなるAIを作ろうとすると、距離感が近くなります。
でも、近すぎると危険です。
ここは安全性データで調整する必要があります。
5. 学習よりデータ作りの方が大変
正直、コードを書くよりデータを作る方が大変です。
「この返答は自然か?」
「優しすぎないか?」
「危険な方向に寄ってないか?」
「長すぎないか?」
みたいなことをずっと考える必要があります。
これからやりたいこと
今後やりたいことはこんな感じです。
- 日本語会話データを増やす
- 短文応答データを増やす
- 相談系データを増やす
- 安全性データを整理する
- LoRA版とフルSFT版を比較する
- GGUF化する
-
llama.cppで動かす - iPhoneで動くか試す
- 自作アプリに組み込む
最終的には、ローカルで動く「自然に話せるAI」を作りたいです。
まとめ
今回は、Gemma 4 E2Bを使って、自然に会話できるローカルLLMを作る途中経過を書きました。
やってみて感じたのは、会話AI作りは単にモデルを学習させるだけではないということです。
むしろ大事なのは、
どんな返答を自然だと思うか
どこまで親密にするか
どこから危険だと判断するか
どれくらい短く返すか
という設計の部分でした。
まだ完成ではありませんが、LoRAとデータセット調整だけでも、モデルの雰囲気はかなり変わります。
学生でもColabやローカル環境を使えば、このあたりの実験は普通にできます。
完璧なモデルを作るのは難しいですが、失敗しながら少しずつ改善していくのが面白いところです。