0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

"ja" を "JP" と書いて喃語モデルができるまで — 音声学習でハマった罠カタログ

0
Posted at

📝 この記事は forge.workstyle.tech に掲載した記事の転載です。

音声合成モデルを20本近く学習させる過程で踏んだ罠を並べる。どれも「エラーが出ないまま間違った結果になる」タイプで、原因に辿り着くまでに時間を溶かした。

1. "JP" と書くと日本語が英語として学習される

学習APIに言語を渡すパラメータがある。素直に書いた。

resp = await client.post(f"{TRAINER_URL}/train", json={
    "model_id": model_id,
    "language": "JP",          # ← これ
    "training_audio": clips,
})

学習は正常に完走した。エラーもワーニングも無い。損失も下がる。

できあがったモデルに喋らせると、喃語しか出てこなかった。日本語の音として成立していない、意味のない音の連なり。

トレーナー側のコードを読んで理由が分かった。

if language.startswith("ja"):
    phonemes = japanese_g2p(text)
else:
    phonemes = english_g2p(text)      # ← ここに来る

"JP".startswith("ja") は False。大文字なので一致しない。日本語のテキストが英語のG2Pを通り、音素列がほぼ空のまま学習された。

音素が空でも学習は回る。損失も下がる(何も学習していないので、平均的な音を出すモデルに収束する)。パイプラインのどこにも異常が出ない。

"ja" と書けば直る。ISO 639-1 は小文字が正しく、"JP" は国コード(ISO 3166)の書き方で、そもそも混同していた。

教訓: 言語コードを渡す箇所は、渡した後に音素列を1回ログに出して目視する。空だったり英語っぽかったりしたら即分かる。学習を回してから気づくには高すぎるコストだ。

2. curl --data @file が280MBでOOM死する

学習データをまとめて投げるとき、手動で叩くことがある。

curl -X POST http://trainer:8000/train \
  -H "Content-Type: application/json" \
  --data @payload.json          # 280MB

これがOOMで死ぬ。curl は --data @file でファイル全体をメモリに読み込み、さらにJSONとして扱うために追加のバッファを取る。280MBのファイルで数GB使う。

Pod内から python で投げれば通る。

import json, urllib.request
req = urllib.request.Request(
    "http://trainer:8000/train",
    data=open("payload.json", "rb").read(),
    headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=3600)

これでも読み込みはするが、curl のような多重バッファが無いぶん収まった。根本的にはストリーミングアップロードにするか、音声をbase64でJSONに詰めるのをやめるべきだが、手動運用の範囲では上で足りた。

教訓: 巨大ペイロードを扱う運用手順は、想定サイズで一度試しておく。「curlで叩けばいい」は数十MBまでの話。

3. 連続学習で CUDA のアサーションエラーが再発する

学習を何本も続けていると、あるタイミングでこれが出る。

SBV2 train 失敗 (rc=-6): ...device-side assertions.
Exception raised from c10_cuda_check_implementation at
  ../c10/cuda/CUDAException.cpp:43

rc=-6 は SIGABRT。CUDAのデバイス側アサーションで落ちている。

対処として PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True を入れた。メモリアロケータが断片化した領域を再利用できるようになり、確かに改善した。

env:
  - name: PYTORCH_CUDA_ALLOC_CONF
    value: "expandable_segments:True"   # 連続学習でのNVML assert対策

ただし根治ではなかった。この設定を入れた後も、約15本連続で学習した時点で再発した。Podを再起動すると直る。

つまりこの設定は寿命を延ばしているだけで、長時間動かし続ければいずれ同じ状態になる。まとまった本数を焼くときは、10本程度ごとにサービスを再起動する運用にした。

kubectl rollout restart deployment/tts-trainer -n <ns>
kubectl rollout status  deployment/tts-trainer -n <ns> --timeout=600s

⚠️ 再起動するとその時点で走っている学習が死ぬので、キューが空いたタイミングを狙う。今回は「クリップ生成中(GPUの別サービスを使っている)」のタイミングで再起動して、学習中のジョブに当てずに済んだ。

教訓: GPUの状態依存な不具合は「設定で直った」と思っても、再発までの本数を数えておく。15本で再発すると分かっていれば、10本ごとの再起動という運用に落とせる。

4. 参照音声の長さに上限がある

ゼロショットの参照音声を長くすると品質が上がる、という直感で伸ばしていったら壁があった。

55秒  → 通る
110秒 → NVML assert で落ちる

さらに厄介なのは、小さいGPUスライスでは55秒でも落ちることだった。逐次生成でメモリが断片化していくので、単発では通る長さでも量産中に落ちる。

対処は2つ。

  • 量産時は余裕のあるGPUスライスを確保する
  • expandable_segments:True を併用する(断片化に効く)

教訓: 「単発で通る」と「連続で通る」は別。量産する前に、本番と同じ本数を流す小規模なリハーサルをやると、この種の劣化が出る。

5. 参照音声の登録が Pod 再起動で消える

参照音声を一時ディレクトリに置く実装になっていて、Podが再起動すると消えた。学習中に気づかず、次のジョブで「参照が見つからない」と落ちる。

volumes:
  - name: voices
    emptyDir: {}          # ← Pod再起動で消える

教訓: 「アップロードして登録する」系のリソースは、置き場が永続かどうかを最初に確認する。開発中は emptyDir で問題なく動くので、Podが落ちるまで気づかない。

6. 同一イメージを共用しているサービスでワーカーが二重に動く

学習ワーカーを持つバックエンドと、別用途のサービスが同じコンテナイメージを使っていた。結果、両方でワーカーが起動し、2つのジョブが同時に学習を始めて分散学習のポートが衝突した。

# 学習ワーカーを持たない側で明示的に無効化する
env:
  - name: VOICE_DESIGN_WORKER_ENABLED
    value: "false"

教訓: イメージを共用するなら、「このデプロイでは何を動かさないか」を env で明示する。デフォルトで全部起動する設計だと、共用した瞬間に事故る。

7. 話体はコーパスに焼き付き、後から変えられない

これは罠というより設計上の性質だが、知らないと詰む。

語尾の伸び方、話速、感情スタイルの構成は、学習コーパスの内容で決まる。合成時のパラメータでは変えられない。実際、合成APIに話速のパラメータを渡して測ったが、値を変えても出力は変わらなかった。

length=1.00 → 話速5.3
length=1.15 → 話速5.2
length=1.35 → 話速5.1
length=0.85 → 話速5.2      ← 誤差の範囲。効いていない

パラメータを受け取るインターフェースはあるが、実装が無視している。「渡せる」と「効く」は別で、実測しないと分からない。

したがって、用途(ナレーター、コールセンター、司会など)は製造時に決めきる必要がある(学習後に話速は変えられない)。同じ声で別の用途が欲しければ、同じ設計値(キャプション+seed)で話体だけ変えてもう1本焼く。1本1〜2時間で作れるので、運用としては現実的だった。

教訓: ランタイムで調整できると思っているパラメータは、一度実測して効果を確認する。効かないと分かれば、設計時に決める前提で全体を組める。効くと思い込んだまま進むと、後から取り返せない。


7つ並べてみると、共通点がある。**どれも「エラーが出ない」**ことだ。

言語コードは学習が完走し、話速パラメータは受け取られ、ワーカーの二重起動は片方が成功し、参照音声の消失は次のジョブまで表面化しない。動いているように見えて、意図した結果になっていない。

この種の不具合を減らすには、「処理が通ったか」ではなく「意図した中身になっているか」を確認する工程を挟むしかない。音素列を1行ログに出す、話速を実測する、起動時にワーカーの有効/無効をログに出す。どれも数分の作業で、後から数時間を節約する。


シリーズ: 拡散TTSから実用ボイスを量産する

キャプション1行から声を設計し、学習コーパスを製造して、役割別の実用ボイスを量産するまでの記録です。この記事は第4部 運用にあたります。

← 前: 70分かけた学習素材が、ネットワークの一瞬の瞬きで消えた
→ 次: 登録経路が4つ、管理画面が0

シリーズ全18本
  1. 音質で選んだTTSが、会話には遅すぎた
  2. 声をガチャで引く
  3. 「ナレーターっぽい声」を24候補から機械に選ばせる
  4. 品質ゲートを厳しくするほど、棒読みが生き残る
  5. 学習後に話速は変えられない
  6. 生成するたび「録音場所」が変わるTTS
  7. クリップ1本の粗さが、スタイル全体を嗄れさせる
  8. AIが「こんにちわー」と伸ばす癖は、どこで身についたのか
  9. 「少々」が「しょも」になる — 許可文字リストが日本語を削っていた
  10. ハルシネーション対策のコードが、ハルシネーションの時だけ動かなかった
  11. 品質ゲートが許した「3文字」が、モデルの口癖になった
  12. 治せる欠陥で候補を落としていた
  13. 文字起こしでは見つからない欠陥がある
  14. 70分かけた学習素材が、ネットワークの一瞬の瞬きで消えた
    15. "ja" を "JP" と書いて喃語モデルができるまで ← いまここ
  15. 登録経路が4つ、管理画面が0
  16. デプロイのたびに互いの成果を消していた
  17. 測れていないものを閾値で追い込むと、必ず失敗する

知見の元になったノートは 拡散TTSから実用ボイスを量産する製造パイプライン にまとめてあります。


元記事: https://forge.workstyle.tech/blog/ja-vs-jp-babbling-model/?utm_source=qiita&utm_medium=crosspost&utm_campaign=ja-vs-jp-babbling-model

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?