首页
/ VibeVoice ASR LoRA 微调实战:数据格式、训练脚本解析与推理验证全流程

VibeVoice ASR LoRA 微调实战:数据格式、训练脚本解析与推理验证全流程

2026-09-05 13:30:35作者:翟萌耘Ralph

本文基于 VibeVoice 开源仓库的 finetuning-asr/ 目录,完整讲解如何对 VibeVoice ASR 模型做 LoRA(Low-Rank Adaptation)微调:从音频-文本配对数据的组织规范,到单卡/多卡 torchrun 训练命令,再到 LoRA 适配器的推理验证与权重整合。读完本文,你可以独立搭建一套 ASR 领域适配训练管线,理解训练脚本中数据拼装、标签掩码与参数冻结的源码级实现,并掌握微调后的推理调用方式。

环境与依赖

在仓库根目录下按以下顺序安装即可(来自 finetuning-asr/README.md):

# Install vibevoice first
pip install -e .

pip install peft

训练与推理脚本的实际依赖还涉及 transformersTrainer/TrainingArguments/HfArgumentParser)、torch 以及推理时的 flash_attention_2 注意力实现(训练脚本以 attn_implementation="flash_attention_2" 加载模型),这些均由仓库代码直接导入,见 lora_finetune.py 顶部导入区。

训练数据格式:音频文件 + 同名 JSON 标注

目录组织

训练数据应为“音频文件 + 同名 JSON 标注”放在同一目录下:

toy_dataset/
├── 0.mp3
├── 0.json
├── 1.mp3
├── 1.json
└── ...

仓库自带的 toy_dataset/ 包含两条由 VibeVoice TTS 生成的合成音频(0.mp3/0.json1.mp3/1.json),仅用于演示流程,官方文档明确指出它不是完整的微调数据集。使用自有数据时应:准备带精确转录的真实录音;根据数据规模与领域调整学习率、epochs、LoRA rank;并关注音频质量与说话人多样性。

JSON 标注结构

每个 JSON 文件结构如下:

{
  "audio_duration": 351.73,
  "audio_path": "0.mp3",
  "segments": [
    {
      "speaker": 0,
      "text": "Hey everyone, welcome back...",
      "start": 0.0,
      "end": 38.68
    },
    {
      "speaker": 1,
      "text": "Thanks for having me...",
      "start": 38.75,
      "end": 77.88
    }
  ],
  "customized_context": ["Tea Brew", "Aiden Host", "The property is near Meter Street."]
}

各字段含义:

  • audio_path:音频文件名,相对 JSON 所在目录。数据加载逻辑会校验该文件存在,缺失时跳过并告警;
  • audio_duration:音频时长(秒),用于 --max_audio_length 过滤(加载器读取 data.get("audio_duration", float("inf")),即缺省时视为无限长);
  • segments:说话人分段列表,speaker 为说话人编号,start/end 为起止时间,text 为该段转录文本;
  • customized_context(可选):领域专有名词或上下文句子,如节目名、人名、品牌词、地点等。训练时若启用 --use_customized_context,会被拼进用户提示词,帮助模型纠正专有词识别。

可以对照真实样本 toy_dataset/0.json:一条约 351 秒的多说话人播客对话,包含 3 位说话人,customized_context 中列出了 "Tea Brew"、"Aiden Host"、"Saeed Guest"、"Rent Byte" 等易错专名。

训练:完整命令与参数

基础训练命令

单卡训练:

# 1 GPU
torchrun --nproc_per_node=1 lora_finetune.py \
    --model_path microsoft/VibeVoice-ASR \
    --data_dir ./toy_dataset \
    --output_dir ./output \
    --num_train_epochs 3 \
    --per_device_train_batch_size 1 \
    --learning_rate 1e-4 \
    --bf16 \
    --report_to none

指定多卡(GPU 0,1,2,3):

# Specific GPUs (e.g., GPU 0,1,2,3)
CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 lora_finetune.py \
    --model_path microsoft/VibeVoice-ASR \
    --data_dir ./toy_dataset \
    --output_dir ./output \
    --num_train_epochs 3 \
    --per_device_train_batch_size 1 \
    --learning_rate 1e-4 \
    --bf16 \
    --report_to none

全量选项示例

脚本使用 HuggingFace TrainingArguments,因此所有标准训练参数均可用(如 warmup、weight decay、保存间隔等):

torchrun --nproc_per_node=4 lora_finetune.py \
    --model_path microsoft/VibeVoice-ASR \
    --data_dir ./toy_dataset \
    --output_dir ./output \
    --lora_r 16 \
    --lora_alpha 32 \
    --lora_dropout 0.05 \
    --num_train_epochs 3 \
    --per_device_train_batch_size 1 \
    --gradient_accumulation_steps 4 \
    --learning_rate 1e-4 \
    --warmup_ratio 0.1 \
    --weight_decay 0.01 \
    --max_grad_norm 1.0 \
    --logging_steps 10 \
    --save_steps 100 \
    --gradient_checkpointing \
    --bf16 \
    --report_to none

关键参数说明

参数 默认值 说明
--lora_r 16 LoRA rank(越小参数越少,越大表达力越强)
--lora_alpha 32 LoRA 缩放因子(通常为 rank 的 2 倍)
--lora_dropout 0.05 LoRA 层 dropout
--per_device_train_batch_size 8(文档标注;命令示例中常用 1 以省显存) 单卡批大小
--gradient_accumulation_steps 1 有效批大小 = batch_size × grad_accum
--learning_rate 5e-5(文档标注;LoRA 场景典型值 1e-4 ~ 2e-4) 学习率
--gradient_checkpointing False 开启以节省显存(代码中对 gradient_checkpointing 参数默认传 True 启用,并调用 enable_input_require_grads
--use_customized_context True 是否将 JSON 中的 customized_context 作为额外上下文
--max_audio_length None 跳过超过此时长(秒)的音频;不设置则无限制

上表默认值继承自 finetuning-asr/README.md;其中 --lora_r--lora_alpha--lora_dropout--use_customized_context--max_audio_length 的默认值可与 lora_finetune.py 中三个 dataclass 的 field(default=...) 逐一核对;per_device_train_batch_sizelearning_rate 的默认值来自 TrainingArguments,训练时建议显式指定(如示例命令),避免长音频样本显存不足。

源码解析一:数据如何变成训练样本

VibeVoiceASRDatasetlora_finetune.py)负责把“音频 + JSON”转换为模型输入。理解以下调用链,能解释数据格式为什么必须是这样的结构:

1. 样本发现与校验(_load_samples 按文件名排序扫描 data_dir 下所有 *.json;读取 audio_path 字段定位音频,音频缺失或 JSON 解析失败会跳过该样本并告警;若设置了 max_audio_length,会读取 audio_duration 字段过滤超长样本:

if self.max_audio_length is not None:
    duration = data.get("audio_duration", float("inf"))
    if duration > self.max_audio_length:
        logger.info(f"Skipping {json_path.stem}: duration {duration:.1f}s > max {self.max_audio_length}s")
        continue

2. 音频预处理(processor._process_single_audio __getitem__ 直接复用推理侧的处理器方法,保证训练/推理输入格式一致:

encoding = self.processor._process_single_audio(
    audio_path,
    sampling_rate=None,
    add_generation_prompt=True,
    use_streaming=True,
    context_info=context_info,
)

vibevoice_asr_processor.py_process_single_audio 实现看,音频会经历:ffmpeg 加载(失败则回退 soundfile,多声道取均值转单声道)→ 非 24kHz 时用 librosa 重采样至 target_sample_rate=24000 → 按 target_dB_FS=-25 归一化 → 计算语音 token 数 vae_tok_len = ceil(音频长度 / 3200),即压缩比 3200 个采样点对应 1 个声学 token(约 7.5 token/秒)。同时处理器按 chat 模板拼装 system 提示 + user 输入(<|speech_start|> + 占位 token + <|speech_end|> + 时长与输出键说明),并把 customized_context 以换行拼接后写入提示词(use_streaming 在音频不足 60 秒时会自动降级为非流式处理)。

3. 目标序列与标签掩码 训练目标是让模型输出结构化 JSON 转录。_format_transcriptionsegments 转换为紧凑 JSON 字符串(键为 Start/End/Speaker/Content,时间保留两位小数,separators=(',', ':') 去除空格),再通过 apply_chat_template 加上 assistant 角色包裹。关键在标签构造:

labels = [-100] * len(input_ids) + target_tokens

输入部分(system + user + 语音占位)全部置为 -100CrossEntropyLoss(ignore_index=-100) 因此只在转录文本上计算损失。模型侧(modeling_vibevoice_asr.pyforward)也确认了 loss_fct = nn.CrossEntropyLoss(ignore_index=-100),这与上面的标签构造一一对应。

4. 批组装(VibeVoiceASRDataCollator 对变长样本做右填充:input_ids/labels 右 pad(pad token 位置标签保持 -100),原始音频波形零填充至 batch 内最大长度,并用 speech_masks 标记有效语音 token 数。批内最终产出 input_idsattention_masklabelsacoustic_input_maskspeech_tensorsspeech_masks 六张张量。

源码解析二:LoRA 配置、参数冻结与训练设置

LoRA 注入位置get_lora_config 默认将 LoRA 应用到语言模型(Qwen2 结构)的注意力与 MLP 投影层:

target_modules = [
    "q_proj", "k_proj", "v_proj", "o_proj",
    "gate_proj", "up_proj", "down_proj",
]

并构造 LoraConfig(..., bias="none", task_type=TaskType.CAUSAL_LM)。也就是说微调只改变语言模型部分的低秩适配参数,语音编码部分完全不参与更新。

冻结语音 tokenizersetup_model_for_training 中显式冻结声学/语义 tokenizer 的全部参数:

for name, param in model.named_parameters():
    if "acoustic_tokenizer" in name or "semantic_tokenizer" in name:
        param.requires_grad = False

随后 get_peft_model(model, lora_config) 注入 LoRA 并打印可训练参数占比,最后按 gradient_checkpointing 开关启用梯度检查点。

模型与精度设置(同函数内):

  • processor 以 language_model_pretrained_name="Qwen/Qwen2.5-7B" 加载文本分词器(若 checkpoint 自带 tokenizer 文件则优先使用自带文件,见 from_pretrained 中的 _ships_tokenizer 逻辑);
  • 模型以 attn_implementation="flash_attention_2"、bf16 精度加载(无 GPU 时训练函数回退 float32 + CPU);
  • 数据加载 dataloader_num_workers = 0,注释说明音频读取在 multiprocessing 下不稳定;remove_unused_columns = False 保留全部列。

产物保存:训练结束后 trainer.save_model(output_dir) 保存 LoRA 适配器权重,trainer.save_metrics("train", ...) 保存训练指标,processor.save_pretrained(output_dir) 额外落盘 preprocessor_config.json,因此 ./output 目录即可被推理脚本直接使用。

推理验证:加载 LoRA 并转录

训练完成后用 inference_lora.py 验证效果:

python inference_lora.py \
    --base_model microsoft/VibeVoice-ASR \
    --lora_path ./output \
    --audio_file ./toy_dataset/0.mp3 \
    --context_info "Tea Brew, Aiden Host"

该脚本支持更多可选项(均有 argparse 默认值):

参数 默认值 说明
--base_model microsoft/VibeVoice-ASR 基座模型路径
--lora_path 必填 LoRA 适配器目录
--audio_file 必填 待转录音频
--context_info None 可选上下文(热词、专名等)
--max_new_tokens 4096 最大生成 token 数
--temperature 0.0 采样温度,0 为贪心解码;>0 时启用 top_p=0.9 采样
--device cuda(有 GPU 时) 推理设备,CPU 时自动切换 float32

源码调用链:load_lora_model 用与训练一致的方式加载基座(VibeVoiceASRProcessor.from_pretrained + VibeVoiceASRForConditionalGeneration.from_pretrained),再 PeftModel.from_pretrained(model, lora_path) 挂载适配器;transcribeprocessor(...) 得到 input_ids/speech_tensors 等输入,model.generate 解码后调用 processor.post_process_transcription 把生成文本解析为 {start_time, end_time, speaker_id, text} 结构(解析器同时兼容 Start time/StartSpeaker ID/Speaker 等键名变体),最后打印原始输出与前 20 个分段。脚本中还预留了 model.merge_and_unload() 的注释开关用于合并后推理。

合并 LoRA 权重(可选)

若希望推理更快(省去双线性分解计算),可将 LoRA 权重并入基座模型,finetuning-asr/README.md 给出的做法:

from peft import PeftModel

# Load base model + LoRA
model = VibeVoiceASRForConditionalGeneration.from_pretrained("microsoft/VibeVoice-ASR", ...)
model = PeftModel.from_pretrained(model, "./output")

# Merge and save
model = model.merge_and_unload()
model.save_pretrained("./merged_model")

合并后可按普通基座模型加载,不再依赖 --lora_path

实践建议与适用边界

  • 数据侧:toy_dataset 是合成数据,只用于验证流程;正式微调应使用真实录音 + 精确转录,并按文档提示关注音频质量与说话人多样性。标注中的 segments 支持多说话人,训练目标即模型输出带时间戳与说话人编号的 JSON 转录。
  • 超参侧:按文档建议依据数据集规模与领域调整学习率、epochs 与 LoRA rank;显存紧张时优先 --gradient_checkpointing、把 --per_device_train_batch_size 设为 1 并用 --gradient_accumulation_steps 累积,--max_audio_length 可剔除异常长样本。toy 样本单条约 5~6 分钟(对应约 4 万个声学 token 位),长音频对显存压力较大,显式限制时长是稳妥做法。
  • 热词场景customized_context 训练注入与推理 --context_info 是同一机制——都拼进 user 提示词(“with extra info: ...”),若领域内专名多,两端保持一致效果最好。
  • 运行环境:训练命令默认面向 CUDA 环境(torchrun + bf16 + Flash Attention 2);CPU 下脚本会自动回退 float32,但 7B 级语言模型的长音频训练基本不具备实际可行性,建议按仓库提供的命令在多卡 GPU 上执行。

相关资源索引

登录后查看全文
热门项目推荐
相关项目推荐