LFM2.5-Audio-1.5B-JP に LoRA でずんだもん口調を追加学習する

初めに

LFM2.5-Audio-1.5B-JP は、音声入力に対して「新しい返答内容」を音声で返す日本語の speech-to-speech(s2s)会話モデルです。このモデルに LoRA で「ずんだもん口調」を追加学習しました。今回はデータ作成から学習・評価・音声対話デモまでを記録します。

今回学習したのは 口調(テキスト)だけで、返答の声はほぼベースモデルのままです

開発環境

項目 バージョン / 構成
OS Windows 11
GPU RTX 4070 Ti SUPER 16GB
パッケージ管理 uv 0.11.8
Python 3.12(>=3.12,<3.13 固定)
torch / torchaudio 2.11.0+cu128
liquid-audio 1.3.0(JP 対応)
transformers 5.10.2
peft 0.19.1
accelerate 1.13.0

環境構築

[project]
requires-python = ">=3.12,<3.13"
dependencies = [
    "liquid-audio[demo]>=1.0.0",
    "torch>=2.8.0",
    "torchaudio>=2.8.0",
    "soundfile>=0.12.1",
]

# torch / torchaudio は CUDA 12.8 ビルドを PyTorch 公式インデックスから取得する
[tool.uv.sources]
torch = [{ index = "pytorch-cu128" }]
torchaudio = [{ index = "pytorch-cu128" }]

[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true

以下で実行します

# 依存を一括取得(初回は CUDA 版 torch 約 2.6GB を取りに行く)
uv sync

peft の入れ方

LoRA に使う peft を uv プロジェクトに足すとき、uv run --with peft禁止です。overlay が別バージョンの torch を引き込み、プロジェクトの torchaudio(libtorchaudio.pyd)と ABI が合わなくなって OSError: Could not load this library で import に失敗します。

正解は、既存の torch を再利用して 1 パッケージだけ入れることです。

# NG: uv run --with peft ...   ← torch 差し替えで torchaudio が ABI 破壊
uv pip install peft            # 既存 torch を再利用、peft==0.19.1
uv run --no-sync python ...    # --no-sync が無いと lockfile 同期で peft が消える

uv pip install--no-sync を受け付けません(--no-syncuv run 専用フラグ)。この非対称さに最初ハマりました。

Windows の Triton 回避

音声生成を実行すると torch._inductor.exc.TritonMissing で落ちます。原因は、Mimi(音声トークナイザ)のデコード経路が既定で torch.compile + CUDA グラフを使い、内部で Triton を要求するのに、Triton には Windows 公式ビルドが無いことです。

対処は、liquid_audio を import する前に環境変数を立てて eager 実行に落とすことです。これらはデコレータ適用時(import 時)に読まれるため、import より前に設定しないと効きません。あわせて、端末の cp932 文字化け対策とログのバッファ対策(python -u 相当)も定型で付けておきます。

import os
# Windows には公式 Triton ビルドが無いため、Mimi decode を eager にする
os.environ.setdefault("NO_TORCH_COMPILE", "1")
os.environ.setdefault("NO_CUDA_GRAPH", "1")
os.environ.setdefault("PYTHONUTF8", "1")          # 端末 cp932 の文字化け対策
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
os.environ.setdefault("PYTHONUNBUFFERED", "1")    # stdout バッファでハング誤認を防ぐ
# ↑ この後で import する
from liquid_audio import LFM2AudioModel, LFM2AudioProcessor

以降、実行コマンドはこれらの環境変数プレフィックスを付けて回します。

データ作成

今回学習するのは「質問音声 → ずんだもん口調の回答テキスト」のペアです。HuggingFace に既存の口調化済みデータ(takaaki-inada/databricks-dolly-15k-ja-zundamon, CC BY-SA 3.0, 15,015 件)があるので、これを採用します。

学習

公式の liquid_audio.trainer.Trainer はフルFT 専用で LoRA コードがありません。そこで Trainer をサブクラス化し、__init__ だけ上書きして「from_pretrained の後・AdamW の前」に get_peft_model を注入します。train() / log() は親から継承し、grad-accum を入れる train_step() だけ上書きする方針です。

LORA_TARGETS = ["q_proj", "k_proj", "v_proj", "out_proj", "w1", "w2", "w3"]
lora_cfg = LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05,
    bias="none", target_modules=LORA_TARGETS,  # task_type は渡さない
)
model = get_peft_model(model, lora_cfg)  # AdamW の前に注入

本走は以下のコマンドで回します。

$env:PYTHONUTF8="1"; $env:PYTHONIOENCODING="utf-8"; $env:NO_TORCH_COMPILE="1"; $env:NO_CUDA_GRAPH="1"
uv run --no-sync python -u train/train_zundamon_lora.py `
    --train-data data/out/arrow_train --val-data data/out/arrow_val `
    --output checkpoints/zundamon_m1b

評価

評価方法は、質問を edge-tts で音声化(学習と同経路)→ generate_sequential でテキストのみ生成(assistant に音声を持たせていないため)です。デコードは再現性重視で greedy、system は学習と同一のずんだもんペルソナ、同一質問を adapter ON(best) / OFF(base) で対比します(OOD のみ base も生成)。口調率は data/quality_gate.tone_eval を再利用、内容・安全性は LLM-judge(Claude Opus 4.8)で判定しました(数値は eval/results/report.md のまま)。

$env:PYTHONUTF8="1"; $env:NO_TORCH_COMPILE="1"; $env:NO_CUDA_GRAPH="1"
uv run --no-sync python eval/eval_zundamon_s2s.py

口調率(自動指標)

セット tone_ok
in-domain 26/30 86.7%
OOD-知識 27/30 90.0%
OOD-指示 14/20 70.0%
OOD-安全 7/8 87.5%
全体 74/88 84.1%

口調は学習分布外(OOD 知識)にも強く般化しました。今回の目標(口調の付与)は達成です。OOD 指示が低いのは、箇条書き/リスト形式で各項目が「のだ」で終わらないこと、一部の指示 echo・ループが原因です。

音声対話デモ

uv run --no-sync python demos/demo_zundamon_realtime.py   # リアルタイム連続対話

起動するとコンソールに http://127.0.0.1:7860(使用中なら 7861 を自動選択)が出るので、ブラウザで開きます。停止は Ctrl+C です。