VibeVoice ASR LoRA 微调实战:数据格式、训练脚本解析与推理验证全流程
本文基于 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
训练与推理脚本的实际依赖还涉及 transformers(Trainer/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.json、1.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_size、learning_rate 的默认值来自 TrainingArguments,训练时建议显式指定(如示例命令),避免长音频样本显存不足。
源码解析一:数据如何变成训练样本
VibeVoiceASRDataset(lora_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_transcription 将 segments 转换为紧凑 JSON 字符串(键为 Start/End/Speaker/Content,时间保留两位小数,separators=(',', ':') 去除空格),再通过 apply_chat_template 加上 assistant 角色包裹。关键在标签构造:
labels = [-100] * len(input_ids) + target_tokens
输入部分(system + user + 语音占位)全部置为 -100,CrossEntropyLoss(ignore_index=-100) 因此只在转录文本上计算损失。模型侧(modeling_vibevoice_asr.py 的 forward)也确认了 loss_fct = nn.CrossEntropyLoss(ignore_index=-100),这与上面的标签构造一一对应。
4. 批组装(VibeVoiceASRDataCollator)
对变长样本做右填充:input_ids/labels 右 pad(pad token 位置标签保持 -100),原始音频波形零填充至 batch 内最大长度,并用 speech_masks 标记有效语音 token 数。批内最终产出 input_ids、attention_mask、labels、acoustic_input_mask、speech_tensors、speech_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)。也就是说微调只改变语言模型部分的低秩适配参数,语音编码部分完全不参与更新。
冻结语音 tokenizer:setup_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) 挂载适配器;transcribe 走 processor(...) 得到 input_ids/speech_tensors 等输入,model.generate 解码后调用 processor.post_process_transcription 把生成文本解析为 {start_time, end_time, speaker_id, text} 结构(解析器同时兼容 Start time/Start、Speaker 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 上执行。
相关资源索引
- 微调训练脚本:lora_finetune.py
- LoRA 推理脚本:inference_lora.py
- 示例标注数据:toy_dataset/0.json、toy_dataset/1.json
- ASR 处理器(音频预处理/模板拼装/输出解析):vibevoice/processor/vibevoice_asr_processor.py
- ASR 模型实现(语音编码与生成):vibevoice/modular/modeling_vibevoice_asr.py
- ASR 使用文档:docs/vibevoice-asr.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00