UtterTuneで独自データセットでLoRA学習を行う

初めに

前回はUtterTuneをCLIおよびWebUIを作成して動かすところまで行いました。

ayousanz.hatenadiary.jp

今回は、UtterTuneのLoRAを独自の音声データを用いてLoRA学習を行っていきます

開発環境

環境構築

環境構築は前回と同じです

git clone https://github.com/your-username/UtterTune.git
cd UtterTune
git submodule update --init --recursive
git submodule update --init --recursive

モデルをダウンロードします

mkdir -p pretrained_models

# Download CosyVoice2-0.5B
git clone https://www.modelscope.cn/iic/CosyVoice2-0.5B.git pretrained_models/CosyVoice2-0.5B

# Download LoRA weights
git lfs install
git clone https://huggingface.co/shuheikatoinfo/UtterTune-CosyVoice2-ja-JSUTJVS lora_weights/UtterTune-CosyVoice2-ja-JSUTJVS

環境構築と設定を行います

uv venv -p 3.10
.venv/bin/activate
uv pip install -r submodules/CosyVoice/requirements.txt  -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com

# サブmoduleのパスを追加します
python - <<'PY'
import site, os
sp = next(p for p in site.getsitepackages() if p.endswith("site-packages"))
pth = os.path.join(sp, "cosyvoice_submodule.pth")
with open(pth, "w", encoding="utf-8") as f:
    f.write(os.path.abspath("submodules/CosyVoice") + "\n")
    f.write(os.path.abspath("submodules/CosyVoice/third_party/Matcha-TTS") + "\n")
print("Wrote:", pth)
PY

データセットの構築

以下のように学習したい音声を以下の構造で配置します

UtterTune/
└── data/
    └── corpora/
        └── your_corpus/          # コーパス名(例: sad, my_voice等)
            ├── wav/              # 音声ファイル
            │   ├── subdirA/      # サブディレクトリ(オプション)
            │   │   ├── file001.wav
            │   │   └── file002.wav
            │   └── subdirB/
            └── trans.txt         # トランスクリプトファイル

文字お越しは以下のような構造で作成をします。<PHON_END>をつけるかどうかは自由で問題ないです。ただし<PHON_END>を使って推論したい場合は、少しは入れておくのがいいです

EMOTION100_001:<PHON_START>エッウソデショ。<PHON_END>
EMOTION100_002:シュヴァイツァーは見習うべき人間です。
EMOTION100_003:デーヴィスさんはとても疲れているように見える。
EMOTION100_004:<PHON_START>スティーヴワジェーンカラテガミヲモラッタ。<PHON_END>
E

記載するときには以下に気を付けます

  • FILE_IDは拡張子なしのファイル名と一致させる
  • 発音制御タグ(<PHON_START>/<PHON_END>)は学習データの約20-30%に使用推奨
  • 日本語の発音表記はカタカナで記述
  • アクセント核: ' (例: レモ'ンティー)
  • 形態素境界: / (例: チ'ミ/モーリョー)

前処理を実行

サブディレクトリが一つの場合は以下を実行します

python -m scripts.cv2.extract_speech_tokens \
    --wav_root data/corpora/your_corpus/wav \
    --out_dir data/speech_tokens/your_corpus \
    --onnx_path pretrained_models/CosyVoice2-0.5B/speech_tokenizer_v2.onnx

サブディレクトリが複数ある場合は、以下を実行します

python -m scripts.cv2.extract_speech_tokens \
    --wav_root data/corpora/your_corpus/wav/emotion \
    --out_dir data/speech_tokens/your_corpus/emotion \
    --onnx_path pretrained_models/CosyVoice2-0.5B/speech_tokenizer_v2.onnx

python -m scripts.cv2.extract_speech_tokens \
    --wav_root data/corpora/your_corpus/wav/recitation \
    --out_dir data/speech_tokens/your_corpus/recitation \
    --onnx_path pretrained_models/CosyVoice2-0.5B/speech_tokenizer_v2.onnx

実行することで、以下のような各WAVファイルに対応する.npyファイルが生成されます:

data/speech_tokens/your_corpus/
├── emotion/
│   ├── file001.npy
│   └── file002.npy
└── recitation/
    ├── file003.npy
    └── file004.npy

マニフェストファイル生成

学習用のマニフェストファイル(4列TSV)を作成します。

マニフェスト形式

spk_id <TAB> text <TAB> token.npy <TAB> wav_path

生成スクリプトの例

scripts/cv2/prepare_manifest_custom.py

from pathlib import Path

def prepare_manifest(corpus_root: Path, token_root: Path, output_file: Path):
    """
    マニフェストファイルを生成

    Args:
        corpus_root: data/corpora/your_corpus
        token_root: data/speech_tokens
        output_file: data/manifests/your_corpus.tsv
    """
    trans_file = corpus_root / "trans.txt"
    trans_dict = {}

    # トランスクリプトを読み込み
    for line in trans_file.read_text("utf-8").splitlines():
        file_id, text = line.split(":", 1)
        trans_dict[file_id] = text

    rows = []
    spk_id = 0  # 単一話者の場合は0、複数話者の場合は話者ごとに異なるIDを割り当て

    # 音声トークンファイルをスキャン
    for token_file in sorted((token_root / "your_corpus").rglob("*.npy")):
        file_id = token_file.stem

        # 対応するWAVファイルを探す
        wav_path = None
        for wav_file in (corpus_root / "wav").rglob(f"{file_id}.wav"):
            wav_path = wav_file
            break

        if wav_path is None or file_id not in trans_dict:
            print(f"Warning: Missing data for {file_id}")
            continue

        text = trans_dict[file_id]
        rows.append([spk_id, text, str(token_file), str(wav_path)])

    # TSVとして保存
    output_file.parent.mkdir(parents=True, exist_ok=True)
    with open(output_file, "w", encoding="utf-8") as f:
        for row in rows:
            f.write("\t".join(map(str, row)) + "\n")

    print(f"Created manifest: {output_file} ({len(rows)} entries)")

if __name__ == "__main__":
    corpus_root = Path("data/corpora/your_corpus")
    token_root = Path("data/speech_tokens")
    output_file = Path("data/manifests/your_corpus.tsv")

    prepare_manifest(corpus_root, token_root, output_file)

マニフェストの作成処理の実行

python scripts/cv2/prepare_manifest_custom.py

出力例

data/manifests/your_corpus.tsv

0    <PHON_START>エッウソデショ。<PHON_END>  data/speech_tokens/your_corpus/emotion/EMOTION100_001.npy   data/corpora/your_corpus/wav/emotion/EMOTION100_001.wav
0   シュヴァイツァーは見習うべき人間です。   data/speech_tokens/your_corpus/emotion/EMOTION100_002.npy   data/corpora/your_corpus/wav/emotion/EMOTION100_002.wav

学習実行

データセットが揃ったので、学習を行っていきます

まずは学習のパラメータ設定を行います

configs/train/your_corpus.yaml で以下の設定ファイルを作成します

---
manifest: data/manifests/your_corpus.tsv
base_model: pretrained_models/CosyVoice2-0.5B
val_ratio: 0.05

lora:
  rank: 16              # LoRAのランク(4, 8, 16, 32等)
  alpha: 64             # LoRAのアルファ(通常はrank * 4)
  dropout: 0.05         # ドロップアウト率
  bias: none            # バイアスの学習(none, all, lora_only)
  target_modules:       # LoRA適用対象レイヤー
    - q_proj
    - k_proj
    - v_proj
    - o_proj

training:
  output_dir: experiments/cv2/your_corpus
  per_device_train_batch_size: 8    # GPUメモリに応じて調整(4, 8, 16)
  per_device_eval_batch_size: 8
  seed: 42
  eval_strategy: steps
  eval_steps: 1000                   # 評価頻度
  save_steps: 1000                   # チェックポイント保存頻度
  max_steps: 15000                   # 総学習ステップ数
  learning_rate: 1e-4                # 学習率
  adam_beta1: 0.9
  adam_beta2: 0.98
  warmup_ratio: 0.05                 # ウォームアップ比率
  lr_scheduler_type: cosine          # 学習率スケジューラ
  max_grad_norm: 1.0                 # 勾配クリッピング
  fp16: true                         # 半精度学習(GPU推奨)
  logging_steps: 50                  # ログ出力頻度
  logging_dir: "${.output_dir}/tb"   # TensorBoardログディレクトリ
  report_to: tensorboard

以下で学習を開始します

python -m scripts.cv2.train --config configs/train/your_corpus.yaml

特定のチェックポイントから再開する場合は以下です

python -m scripts.cv2.train \
    --config configs/train/your_corpus.yaml \
    --resume_from_checkpoint experiments/cv2/your_corpus/checkpoint-5000

推論実行

学習したLoRAモデルで音声合成をテストします。

基本的な推論

python -m scripts.cv2.infer \
    --base_model pretrained_models/CosyVoice2-0.5B \
    --lora_dir experiments/cv2/your_corpus \
    --texts "テストテキスト" \
    --prompt_wav data/corpora/your_corpus/wav/sample.wav \
    --prompt_text "サンプル音声の書き起こし" \
    --out_dir wavs_out/test

複数テキストの一括合成

python -m scripts.cv2.infer \
    --base_model pretrained_models/CosyVoice2-0.5B \
    --lora_dir experiments/cv2/your_corpus \
    --texts "標準テキスト|<PHON_START>発音制御<PHON_END>テキスト|別のテキスト" \
    --prompt_wav data/corpora/your_corpus/wav/sample.wav \
    --prompt_text "サンプル音声の書き起こし" \
    --out_dir wavs_out/test

パイプ(|)で区切って複数のテキストを指定可能

テキストファイルからの読み込み

# texts.txt
標準のテキストです。
<PHON_START>発音制御<PHON_END>を含むテキスト。
別のテキストです。
python -m scripts.cv2.infer \
    --base_model pretrained_models/CosyVoice2-0.5B \
    --lora_dir experiments/cv2/your_corpus \
    --texts texts.txt \
    --prompt_wav data/corpora/your_corpus/wav/sample.wav \
    --prompt_text "サンプル音声の書き起こし" \
    --out_dir wavs_out/test

出力音声のトリミング

python -m scripts.cv2.infer \
    --base_model pretrained_models/CosyVoice2-0.5B \
    --lora_dir experiments/cv2/your_corpus \
    --texts "テキスト" \
    --prompt_wav data/corpora/your_corpus/wav/sample.wav \
    --prompt_text "書き起こし" \
    --out_dir wavs_out/test \
    --trim_out  # 前後の無音をトリミング

学習したLoRAでWebUIを起動

python webui.py \
    --base_model pretrained_models/CosyVoice2-0.5B \
    --lora_dir experiments/cv2/your_corpus \
    --port 7860

WebUIを実行すると前回同様以下が起動します