日本語 full-duplex 対話モデル J-Moshi-ext に「お嬢様口調」を追加学習する

初めに

前回の記事では、日本語 full-duplex 対話モデル J-Moshi-ext (7.5B) に「あみたろ」の声だけを転移させました。今回はその上に「話し方」を上乗せします。

具体的には、汎用「お嬢様口調」(「わたくし」「〜ですわ / のよ / かしら」) を、前回の voice clone checkpoint (あみたろ声) に対して 2 段構成で追加学習しました。1 段目は text 側の transformer のみ、2 段目は全パラメータを対象にしています。

結果は以下のとおりです。

  • 話し方 (お嬢様語尾) の学習は成功。text 側の分布が明確に shift した (perplexity 検証で +83-89% gain)
  • あみたろ声はほぼそのまま保持され、話し方だけが変わった
  • text 側の transformer だけを学習した軽量版 (全体の ~5% を触るだけ) で、全パラメータ学習と同水準の gain が得られた

デモ

moshi.server の gradio-tunnel 経由でブラウザから live 対話した様子です。

J-Moshi-ext にお嬢様口調を追加学習したデモ

https://youtu.be/YXOF1onNkYI

「声はあみたろ、語尾は〜ですわ / かしら」の応答が返ってくる状態を確認しました。

開発環境

今回の追加学習側のみ記載します。声を移した checkpoint (前回の voice clone) を base に使うところまでは前回記事と同一構成です。

項目
GPU A100 SXM4 80GB × 2 (追加学習)
OS Ubuntu 22.04
Python 3.11
PyTorch 2.7.0+cu126
DeepSpeed 0.15.x (ZeRO-3, bf16, no offload)
Base checkpoint 前回の voice clone checkpoint (ayousanz/phase1b-jmoshi-ft-2026-07-06、あみたろ声)
学習用コーパス 100 dialog / 800 turn の合成お嬢様対話 (JSONL)

何をやったか

最終的な pipeline は 4 段構成です。

  1. 合成お嬢様対話コーパスの生成: 10 topic × 10 dialog = 100 dialog、平均 8 turn。text は Claude Sonnet で生成し、正規表現で「〜ですわ / のよ / ますの / でしょう / かしら」の語尾出現率 ≥ 90% を要求。
  2. 前回の Irodori-TTS (あみたろ Full FT) で 2 話者 stereo 音声化: Speaker A = お嬢様 (あみたろ声)、Speaker B = 執事役 (中立)。24 kHz stereo mix で約 40 分。
  3. 前回の voice clone checkpoint を base に、text 側の transformer だけを追加学習: 全体の ~5% のパラメータのみ trainable、5 epoch。以下「軽量版」。
  4. 同じ base・同じ corpus で全 7.5B パラメータを追加学習: 5 epoch。以下「フル版」。

Moshi は「何を話すか」を担う text 側の transformer (tempformer) と、「どう発音するか」を担う音響側 (depformer) の 2 段構造になっており、軽量版 (3) は音響側を完全に freeze することであみたろ声を保護しつつ、語尾 pattern だけを text 側に学習させる設計です。フル版 (4) は両者を joint に学習して一貫性を狙う版で、比較のため両方走らせました。

うまくいったこと

  • 話し方 (お嬢様語尾) の学習: perplexity 検証で軽量版 +83%、フル版 +89% の gain。両者とも 10 prompt 中 7 個でお嬢様側を選好 (voice clone のみの状態は 6/10)。
  • あみたろ声の保持: live 対話の聴取判定で、フル版でもあみたろ寄りの声色は失われていない。軽量版は音響側 freeze なので当然保持。
  • 軽量版で十分だった: パラメータ ~5% しか触っていない軽量版が、フル版とほぼ同じ perplexity gain を出した。cost / 効果比では軽量版が優位。

うまくいかなかったこと

  • 対話能力の劣化 (base 由来): live 対話では、user 発話への追随が遅れる / 応答が短くなる / 話題が浅くなるといった劣化が体感でかなり感じられる。ただしこれは今回の追加学習で新たに壊したわけではなく、base に使った前回の voice clone checkpoint の時点で既に対話能力が落ちていたものを、そのまま継承してしまった形。前回記事でも副作用として書いた「発音の明瞭度が base より劣化」と同じ layer の話で、Irodori-TTS で合成した対話コーパスの質 (発音精度・応答の自然さ) が上限を規定している。

副産物として得られた知見

  • Base J-Moshi-ext だけでも「わたくしも」「紅茶を淹れました」の後に女性的継続を予測する分布を既に持っていた (お嬢様選好 60%)。日本語事前学習が想定より丁寧語文脈をカバーしている副産物。
  • 稀語彙 (「〜ですわ」「わたくし」) は Irodori-TTS の学習分布外だが、事前の 10 sample × 3 語尾チェックでは 3 group 全 pass。Moshi 側の追加学習でも発音破綻は観測されなかった。

fine-tune までの手順

1) お嬢様対話コーパスの合成

前回の合成対話生成 pipeline (10 topic × 10 dialog = 100 dialog) をそのまま流用し、text 側だけをお嬢様語尾に書き換えます。1 dialog は Speaker A (お嬢様、amitaro voice) と Speaker B (執事役、中立) の 2 話者、平均 8 turn です。

{
  "id": "cp-ojousama-mild-0101",
  "topic": "朝の挨拶",
  "turns": [
    {"speaker": "A", "text": "ごきげんよう、田中。今日もよいお天気ですわね。"},
    {"speaker": "B", "text": "おはようございます。お目覚めはいかがでしたか。"},
    {"speaker": "A", "text": "よく眠れました。窓から鳥のさえずりが聞こえます。"},
    {"speaker": "B", "text": "それは何よりです。朝食のご用意ができております。"},
    {"speaker": "A", "text": "ありがとう。今日はパンにしようかしら。"},
    ...
  ]
}

対応する manifest.jsonl は前回と同構造で、turns[] に turn 単位の話者 / 時刻 / kanji / kana を持ちます。

2) 軽量版: text 側だけを追加学習

moshi-finetune の config を以下のようにします。

# configs/persona-tempformer.yaml
model:
  base: ayousanz/phase1b-jmoshi-ft-2026-07-06  # 前回の voice clone checkpoint
train:
  params_to_ft: tempformer     # text 側だけ、音響側は freeze (~5% trainable)
  num_processes: 2
  lr: 3e-5
  ds_config: configs/zero3-bf16-nooffload.json
  per_device_batch_size: 1
  grad_accum: 4
  precision: bf16
  epochs: 5
data:
  manifest: ojousama_mild_100/manifest.jsonl

Loss は total 0.49 (音響側 1.10、text 側 0.07) で 30 分程度でした。text 側の loss が非常に低く、text 側の transformer が語尾 pattern を pin-point で学習しているのが数字に出ています。

3) フル版: 全パラメータを追加学習

同じ corpus・同じ voice clone base に対して、今度は全パラメータを対象にします。

# configs/persona-full.yaml
model:
  base: ayousanz/phase1b-jmoshi-ft-2026-07-06
train:
  params_to_ft: all            # 全 7.5B
  ...  # 他は軽量版と同一

Loss は total 1.10 (text 側 0.30)。軽量版の text loss (0.07) より高くなるのは、音響側も同時に学習するため text 側だけを一極集中させないためです。

4) checkpoint の後処理

前回同様、ZeRO-3 checkpoint を推論用 safetensors に変換します。今回 tools 側の torch.loadweights_only=True 前提の path があり、ZeroStageEnum の unpickle で落ちるので weights_only=False に patch する必要がありました。

# ZeRO-3 → fp32 safetensors → dep_q=8 変換
python -m deepspeed.utils.zero_to_fp32 ...
python -m moshi.tools.clean_moshi ...

5) Perplexity で「学習できたか」を数値で確認

live 対話に進む前に、text 側だけの数値評価を挟みました。

10 個の同じ prompt に対して「中立語尾」と「お嬢様語尾」の 2 種の続きを用意し、学習後の model がどちらに高い確率を割り当てるかを測定します。両者の log 確率の差が正なら「お嬢様側を選好している」= 学習成功、負なら「中立側を選好」= 学習失敗 という判定です。

実装は Moshi の text stream に prompt + continuation を feed し、forward_text で得られる logits から continuation 部分の log 確率を積み上げるだけです。

# persona_perplexity.py (要約)
prompt_ids = sp.encode(prompt)
cont_ids   = sp.encode(continuation)
# Moshi は text と audio codebook を同時に入力する必要があるので
# audio 側は zero token で埋め、text 側だけに実データを載せる
codes = torch.full((1, K, T), zero_token_id, dtype=torch.long, device=device)
codes[:, 0, :] = torch.tensor([prompt_ids + cont_ids])

with lm.streaming(1):
    _, text_logits = lm.forward_text(codes)
log_probs = torch.log_softmax(text_logits[0, 0], dim=-1)

total_logprob = sum(log_probs[len(prompt_ids)+i-1, tok].item()
                    for i, tok in enumerate(cont_ids))

Test pair の例:

  • 「今日はいい天気」 + 「ですね」 vs 「ですわ」
  • 「行ってみましょう」 + 「か」 vs 「かしら」
  • 「紅茶を淹れました」 + 「よ」 vs 「ますの」

結果:

Checkpoint お嬢様選好率 log prob 合計差 追加学習の効果
Voice clone のみ (前回、あみたろ声だけ入っている状態) 6/10 (60%) +6.48 基準
軽量版 (text 側だけ追加学習) 7/10 (70%) +11.86 +5.38 (+83%)
フル版 (全パラメータ追加学習) 7/10 (70%) +12.26 +5.78 (+89%)

顕著な gain 例 (Δ = フル版 − Voice clone のみ):

  • 「よくおできになった」 + 「でしょう」: -1.06 → +0.81 (逆転, +1.87 gain)
  • 「今日はいい天気」 + 「ですわ」: -1.77 → -0.08 (ほぼ同等, +1.69 gain)
  • 「それは素敵」 + 「ですわ」: -1.48 → -0.15 (ほぼ逆転, +1.33 gain)

Voice clone だけの状態でも既に 60% はお嬢様側を選好している ― これは J-Moshi-ext の大規模日本語事前学習の副産物と考えられます (「わたくしも」「紅茶を淹れました」等の丁寧文脈で女性的継続を予測する分布が pre-training に含まれている)。今回の追加学習はその上にお嬢様の語尾パターンを積み増した形です。

6) Live 検証 (moshi.server + gradio-tunnel)

学習した checkpoint を実 live 対話で確認します。今回は Mac ↔ vast.ai の直接 SSH port forwarding が過去に不安定だったため、moshi.server 内蔵の gradio-tunnel を使う経路にしました。

# vast.ai 側 (RTX 3090)
pip install "moshi==0.1.0" "sphn<0.2" gradio

# フル版 checkpoint を HF から取得
huggingface-cli download ayousanz/moshi-persona-stage3-ojousama-2026-07-06 \
  model.safetensors moshi_lm_kwargs.json --local-dir /workspace/stage3

# gcc を install (torch.compile の triton kernel build に必要)
apt-get install -y gcc g++
export CC=/usr/bin/gcc CXX=/usr/bin/g++

# moshi.server を gradio-tunnel 付きで起動
python -m moshi.server \
  --host 0.0.0.0 --port 8998 \
  --hf-repo nu-dialogue/j-moshi-ext \
  --moshi-weight /workspace/stage3/model.safetensors \
  --gradio-tunnel

数分後にログに Tunnel started, ... https://xxx.gradio.live が出るので、その URL を Mac / iPhone のブラウザで開き、マイク permission を許可して対話開始。声色はあみたろのまま、応答語尾に「〜ですわ / かしら / でしょう」が混ざる状態を確認できました。

詰まったポイントと解決

(a) sphn の API 変更で server が音声受信で crash する

moshi==0.1.0 は Opus 音声受信で sphn.OpusStreamReader.read_pcm() を呼びますが、sphn>=0.2.0 ではこの API が消えています。ブラウザ側から見ると WebSocket 接続後すぐに切断され、DevTools console に以下が延々出続けます。

index-XXX.js:425 WebSocket is already in CLOSING or CLOSED state.
index-XXX.js:425 WebSocket is already in CLOSING or CLOSED state.
...

Server 側は表向き Running on http://0.0.0.0:8998 まで正常に見えますが、実接続で以下が発生します。

File ".../moshi/server.py", line 115, in opus_loop
  pcm = opus_reader.read_pcm()
AttributeError: 'builtins.OpusStreamReader' object has no attribute 'read_pcm'

対処は sphn を 0.1.12 に downgrade するだけです。

pip install "sphn<0.2"

moshi==0.1.0 を使う限り必ず踏むので、実質的な requirement pin です。

(b) moshi_lm_kwargs.json は復元 side channel

moshi.server は checkpoint の .safetensors しか読まず、model architecture は moshi/models/loaders.py::_lm_kwargs に hardcode されています。fine-tuning で config を変えた場合、moshi_lm_kwargs.json を checkpoint と同じ dir に置いておくと後から可搬性が保てます。今回は base J-Moshi-ext と同一 architecture (dim=4096, num_layers=32, dep_q=8) なので trivial ですが、Depformer size を変える実験を後で走らせる場合は必須です。

評価結果

4 次元で見ます。

能力 素の J-Moshi-ext 前回 (voice clone のみ) 今回 (話し方も追加) 判定
テキスト生成の破綻の有無 正常 保持 保持 + お嬢様化 良化
声質 (target = あみたろ) 汎用 Moshi 声 あみたろ寄り あみたろ寄り (保持) 保持
対話能力 (turn-taking) 動作 保持 保持 保持
話し方 (お嬢様語尾) ほぼ中立 (60%) ほぼ中立 (60%) お嬢様選好 70% / +89% gain 転移成功

軽量版とフル版の対比。

項目 軽量版 (text 側だけ) フル版 (全パラメータ)
学習対象のパラメータ割合 ~5% 100% (7.5B)
Loss (合計) 0.49 1.10
Loss (text 側のみ) 0.07 0.30
お嬢様選好率 7/10 7/10
log prob gain (vs voice clone のみ) +5.38 (+83%) +5.78 (+89%)

結論

前回 voice clone した J-Moshi-ext の上に、合成お嬢様対話 100 dialog を text 側だけの追加学習 (軽量版) または全パラメータ追加学習 (フル版) で上乗せすることで、あみたろの声を保持したまま、お嬢様の話し方だけを text 層に焼き込むことができました。Perplexity 検証で voice clone base から +83-89% の gain、live 対話でも語尾の shift を確認しています。Cost はコーパス生成 $2-3 + 学習 $5-15 + live check $0.5 程度、合計 $10-20 に収まりました。

参考