使用f5-tts训练自己的模型笔记
摘要
服务器都有了,这不得练练丹,有点说不过去啊。所以尝试了从头开始训练一个模型,结果由于推理页面好像有bug,不知道是不是失败了,然后又尝试微调一下模型。本篇文章主要记录了三流调包侠尝试炼丹过程中学习到的一些知识。
为什么要自己训练或者微调模型
F5-TTS 的设计目标不是生成一个固定的、默认的(例如女声普通话)语音。传统的 TTS 模型,比如一些早期的端到端模型,确实可以直接输入文本,然后生成一个预设音色的语音。它是一个“Text-to-Cloned-Voice”模型: F5-TTS 专注于从一小段参考音频中提取说话人的音色、语调、说话风格等特征,然后将这些特征应用到你提供的文本上,生成带有该音色的语音。
虽然 F5-TTS 强调零样本语音克隆,需要参考音频来工作,但训练自己的模型(无论是从头开始训练还是在预训练模型上进行微调)的意义在于:
提升特定音色的克隆质量和稳定性
- 预训练的 F5-TTS 模型可能在通用语音克隆方面表现良好,但对于特定目标音色(例如你想要克隆的某个人的声音),其克隆质量和稳定性可能不是最佳。
- 通过在包含目标音色的大量数据上进行微调 (fine-tuning),模型能够更好地学习该音色的细微特征、语调模式和发音习惯,从而生成更自然、更像目标音色的语音。
- 这对于需要高保真度语音克隆的应用(如虚拟主播、个人语音助手等)至关重要。
适应特定语言或口音
- F5-TTS 原始模型可能主要在英文或中英文混合数据集上训练。如果你需要为其他语言(例如粤语、日语、德语等)或特定口音(例如英式英语、美式英语的不同口音)生成高质量语音,那么在相应语言/口音的数据集上训练或微调模型是必不可少的。
适应特定应用场景和数据特点
- 不同的应用场景可能对语音有不同的要求。例如,有声读物可能需要更平稳的语速,而对话系统可能需要更自然的停顿和交互感。
- 如果你的数据有特定的噪声、录音条件或说话风格,通过在类似数据上训练,可以使模型对这些特点有更好的鲁棒性或生成效果。
- F5-TTS 论文提到,其模型训练使用了 “in-the-wild multilingual speech dataset Emilia”,这意味着它处理的是真实世界中带噪的数据。如果你希望在更干净的数据上得到更清晰的语音,或者在更复杂的场景下有更好的表现,训练可以帮助模型适应。
控制生成语音的特性(如情感、语速等):
- 虽然 F5-TTS 自身就支持一定的情感和语速控制,但通过训练自己的模型,并可能在数据中包含更多情感标签或不同语速的样本,可以进一步增强模型在这方面的表现和可控性。
官方训练模型文档如下
https://github.com/SWivid/F5-TTS/tree/main/src/f5_tts/train
接下来使用 F5-TTS来训练个人的语音模型
环境准备
请先按照官方文档部署并能够正常文本转语音,依赖应该就没问题了
数据准备
数据集格式
官方文档
https://github.com/SWivid/F5-TTS/discussions/57#discussioncomment-10959029
F5-TTS 采用的格式
datasets/
└── your-dataset/├── wavs/│ ├── 00001.wav│ ├── 00002.wav│ └── ...└── metadata.csv
metadata.csv
格式
wavs/00001.wav|这是第一条语音的文字
wavs/00002.wav|这是第二条语音的文字
网上的数据集
我本来想自己标注的,但好像好废时间,就搜了搜看有没有标注好的数据集,然后找到了
https://www.bilibili.com/opus/961351523556655128?spm_id_from=333.1387.0.0
B站up主 红血球AE3803 分享的
解压后文件中包含的文件如下
- .wav是语音文件
- .lab是其对应的文本
编写脚本处理数据集
建议使用项目自带的 web ui的方式处理数据集,会对音频切段,可能会训练效果更好一些
import os
import csv
import wave
import contextlibdef process_files(directory):"""处理指定目录中的wav和lab文件,生成metadata.csv文件,并统计语音文件总时长"""# 检查目录是否存在if not os.path.isdir(directory):print(f"目录 {directory} 不存在!")return# 创建输出目录(如果需要)wavs_dir = os.path.join(os.path.dirname(directory), "wavs")if not os.path.exists(wavs_dir):os.makedirs(wavs_dir)print(f"创建目录: {wavs_dir}")# 收集所有匹配的文件file_pairs = []lab_files = {}wav_files = []# 首先收集所有.lab文件for filename in os.listdir(directory):if filename.endswith('.lab'):base_name = os.path.splitext(filename)[0]lab_files[base_name] = filename# 然后检查每个.lab文件是否有对应的.wav文件for base_name, lab_file in lab_files.items():wav_file = f"{base_name}.wav"if os.path.exists(os.path.join(directory, wav_file)):file_pairs.append((wav_file, lab_file))# 处理匹配的文件对metadata = []total_duration = 0 # 添加总时长统计变量for wav_file, lab_file in file_pairs:# 读取文本内容with open(os.path.join(directory, lab_file), 'r', encoding='utf-8') as f:text_content = f.read().strip()# 构建目标wav路径(如 wavs/audio_xxxx.wav)target_wav_path = f"wavs/{wav_file}"# 添加到元数据metadata.append((target_wav_path, text_content))# 获取wav文件时长wav_path = os.path.join(directory, wav_file)try:with contextlib.closing(wave.open(wav_path, 'r')) as f:frames = f.getnframes()rate = f.getframerate()duration = frames / float(rate)total_duration += durationprint(f"文件 {wav_file} 时长: {duration:.2f}秒")except Exception as e:print(f"无法读取文件 {wav_file} 的时长: {str(e)}")# 复制wav文件到wavs目录(如果需要)source_wav_path = os.path.join(directory, wav_file)target_wav_full_path = os.path.join(os.path.dirname(directory), target_wav_path)if not os.path.exists(os.path.dirname(target_wav_full_path)):os.makedirs(os.path.dirname(target_wav_full_path))# 如果文件不在目标位置,可以复制它# import shutil# if not os.path.exists(target_wav_full_path):# shutil.copy2(source_wav_path, target_wav_full_path)# 写入metadata.csvoutput_file = os.path.join(os.path.dirname(directory), "metadata.csv")with open(output_file, 'w', encoding='utf-8', newline='') as f:for wav_path, text in metadata:f.write(f"{wav_path}|{text}\n")# 格式化总时长输出hours, remainder = divmod(total_duration, 3600)minutes, seconds = divmod(remainder, 60)time_str = f"{int(hours)}小时{int(minutes)}分{seconds:.2f}秒"print(f"处理完成! 共处理了 {len(metadata)} 对文件,输出到 {output_file}")print(f"语音文件总时长: {time_str} ({total_duration:.2f}秒)")if __name__ == "__main__":import sys# if len(sys.argv) > 1:# directory = sys.argv[1]# else:# # 默认目录,可以根据需要修改# directory = input("请输入包含wav和lab文件的目录路径: ")directory = "/Users/wy/wy/workspace/ios_app/黑天鹅"process_files(directory)
该脚本只是读取文本然后拼接文件名,输出metadata.csv
文件, 文件是手动挪到指定目录的。输出内容如下:
最后的数据集目录结构如下
txt目录是多余的,没有也没所谓,我只是将剩余的lab文件放到里面好看一点而已.
数据集预处理
这里除了手动处理外,还可以使用web的方式来处理数据集,具体请看后面训练模型章节里面的web ui训练小节,没有办法,谁叫写完前面这堆内容才发现有web可以用…
脚本位置src/f5_tts/train/datasets/prepare_csv_wavs.py
prepare_csv_wavs.py脚本源码
查看脚本可以看到正确的命令如下
入口函数是prepare_and_save_set
入口函数就调用了两个函数
sub_result, durations, vocab_set = prepare_csv_wavs_dir(inp_dir, num_workers=num_workers)
调用 prepare_csv_wavs_dir
函数处理输入目录
- 使用多线程并行处理音频文件(获取音频时长)
- 将文本转换为拼音表示
- 返回处理后的数据、音频时长列表和词汇集合
save_prepped_dataset(out_dir, sub_result, durations, vocab_set, is_finetune)
调用 save_prepped_dataset
函数保存处理结果
- 将数据保存为Arrow格式(高效读取)
- 保存音频时长信息为JSON
- 根据
is_finetune
决定是复制现有词汇表还是创建新词汇表
其中跟音频技术相关的是中文拼音转换这步的处理,下面详细看看拼音转换的代码。
中文拼音转换函数详解
convert_char_to_pinyin
函数是 F5-TTS 模型中的一个关键组件,它负责将中文文本转换为拼音表示。这对于中文语音合成至关重要,因为模型需要处理拼音以正确发音。下面是详细解析:
这个函数接收文本列表并将其中的中文字符转换为带声调的拼音表示,同时保留非中文字符。这对于生成自然的中文语音至关重要。
def convert_char_to_pinyin(text_list, polyphone=True):if jieba.dt.initialized is False:jieba.default_logger.setLevel(50) # CRITICALjieba.initialize()final_text_list = []custom_trans = str.maketrans({";": ",", """: '"', """: '"', "'": "'", "'": "'"}) # add custom trans here, to address oovdef is_chinese(c):return ("\u3100" <= c <= "\u9fff" # common chinese characters)for text in text_list:char_list = []text = text.translate(custom_trans)for seg in jieba.cut(text):seg_byte_len = len(bytes(seg, "UTF-8"))if seg_byte_len == len(seg): # if pure alphabets and symbolsif char_list and seg_byte_len > 1 and char_list[-1] not in " :'\"":char_list.append(" ")char_list.extend(seg)elif polyphone and seg_byte_len == 3 * len(seg): # if pure east asian charactersseg_ = lazy_pinyin(seg, style=Style.TONE3, tone_sandhi=True)for i, c in enumerate(seg):if is_chinese(c):char_list.append(" ")char_list.append(seg_[i])else: # if mixed characters, alphabets and symbolsfor c in seg:if ord(c) < 256:char_list.extend(c)elif is_chinese(c):char_list.append(" ")char_list.extend(lazy_pinyin(c, style=Style.TONE3, tone_sandhi=True))else:char_list.append(c)final_text_list.append(char_list)return final_text_list
为什么要转换成拼音?
消除歧义
- 中文是多音字密集的语言,一个汉字可能有多个读音(如 “行” 可读作 xíng 或 háng)。
- 在没有上下文的情况下,TTS 系统很难判断正确读音。拼音提供了明确的发音方式,便于后续处理。
与语音模型对接
- 很多中文 TTS 系统的底层语音模型(如 Tacotron、FastSpeech、GPT-SoVITS)并不是直接以汉字为输入,而是以拼音、声调甚至音素(phoneme)为输入。
- 拼音是一个中间表示(intermediate representation),便于模型学习发音规律。
提升语音自然度
- 精准的拼音/音素输入能帮助模型合成更自然、连贯的语音,尤其是在声调和语调变化方面。
1. 结巴分词初始化
if jieba.dt.initialized is False:jieba.default_logger.setLevel(50) # CRITICALjieba.initialize()
- 初始化结巴分词库
- 将日志级别设为 CRITICAL 以抑制不必要的输出
2. 自定义转换规则
custom_trans = str.maketrans({";": ",", """: '"', """: '"', "'": "'", "'": "'"})
- 定义特殊字符的转换规则,将一些特殊标点符号统一为标准形式
- 这有助于减少词汇表外(OOV)字符的出现
3. 中文字符识别
def is_chinese(c):return "\u3100" <= c <= "\u9fff" # common chinese characters
- 定义内部函数检测字符是否为中文
- 使用 Unicode 范围 U+3100 至 U+9FFF,涵盖常见中文字符
4.汉字转为拼音
pypinyin
是 Python 中最主流的中文转拼音库,示例代码如下
from pypinyin import lazy_pinyin, Style
text = "你好,世界!"
# 默认风格,不带声调
print(lazy_pinyin(text))
# 输出: ['ni', 'hao', 'shi', 'jie']
# 带声调
print(lazy_pinyin(text, style=Style.TONE3))
# 输出: ['ni3', 'hao3', 'shi4', 'jie4']
# 首字母
print(lazy_pinyin(text, style=Style.FIRST_LETTER))
# 输出: ['n', 'h', 's', 'j']
5. 文本处理流程
对于每个输入文本,函数执行以下步骤
- 应用自定义字符转换规则
- 使用结巴分词将文本分割为词语片段
- 根据不同情况分别处理每个片段
a) 纯字母和符号
if seg_byte_len == len(seg): # if pure alphabets and symbolsif char_list and seg_byte_len > 1 and char_list[-1] not in " :'\"":char_list.append(" ")char_list.extend(seg)
- 检测片段是否只包含 ASCII 字符
- 在必要时添加空格以确保正确分隔
- 原样保留字母和符号
b) 纯中文
elif polyphone and seg_byte_len == 3 * len(seg): # if pure east asian charactersseg_ = lazy_pinyin(seg, style=Style.TONE3, tone_sandhi=True)for i, c in enumerate(seg):if is_chinese(c):char_list.append(" ")char_list.append(seg_[i])
- 检测是否为纯中文片段
- 使用
lazy_pinyin
将整个片段转换为拼音 - 添加空格以确保拼音正确分隔
style=Style.TONE3
表示使用数字表示声调(如 “ni3” 而非 “nǐ”)tone_sandhi=True
启用声调变化规则(如"一"的变调)
c) 混合字符
else: # if mixed characters, alphabets and symbolsfor c in seg:if ord(c) < 256:char_list.extend(c)elif is_chinese(c):char_list.append(" ")char_list.extend(lazy_pinyin(c, style=Style.TONE3, tone_sandhi=True))else:char_list.append(c)
- 逐字符处理混合内容
- ASCII 字符原样保留
- 中文字符转换为拼音并添加空格
- 其他字符(如日文、韩文等)原样保留
开始预处理数据集
python src/f5_tts/train/datasets/prepare_csv_wavs.py /home/ubuntu/data-set /home/ubuntu/F5-TTS/data/heitiane --pretrain
- /home/ubuntu/data-set 数据集所在目录
- /home/ubuntu/F5-TTS/data/heitiane 处理好的数据集存放位置,
F5-tts
项目所在的data目录, 是固定的,heitiane
是你数据集的名称,随便填
还需要修改训练用的配置文件中的数据集名称为heitiane
具体位置请看训练脚本详解章节中的分词器设置小节。
数据集预处理完成后,就可以开始训练模型了
训练模型
官方文档给出的命令如下:
# setup accelerate config, e.g. use multi-gpu ddp, fp16
# will be to: ~/.cache/huggingface/accelerate/default_config.yaml
accelerate config# .yaml files are under src/f5_tts/configs directory
accelerate launch src/f5_tts/train/train.py --config-name F5TTS_v1_Base.yaml# possible to overwrite accelerate and hydra config
accelerate launch --mixed_precision=fp16 src/f5_tts/train/train.py --config-name F5TTS_v1_Base.yaml ++datasets.batch_size_per_gpu=19200
了解accelerate命令
accelerate
是 Hugging Face 提供的一个非常强大的工具,旨在帮助 PyTorch 用户在不同的分布式训练环境中(如多 GPU、多节点、TPU、混合精度等)轻松运行他们的训练脚本,而无需编写大量的样板代码。它让你能够专注于编写 PyTorch 训练循环本身,而把分布式训练的复杂性交给 accelerate
来处理。
accelerate config命令
用于交互式地配置你的训练环境。它会引导你通过一系列问题,根据你的硬件设置(CPU、单 GPU、多 GPU、TPU 等)和训练需求(混合精度、DeepSpeed 等)生成一个配置文件。
下面是引导中的一些问题
- 问题1
它让你选择:你的训练是在一台机器上,还是多台机器上?如果是在一台机器上,是只用 CPU 跑,还是用多个 GPU、TPU 或其他专用硬件来跑?
- 问题2
Do you wish to optimize your script with torch dynamo?[yes/NO]:
PyTorch Dynamo 是 PyTorch 2.0 引入的一个核心功能,旨在显著提高 PyTorch 模型的运行速度。
Dynamo 会在运行时动态地分析你的 PyTorch 代码,将 Python 级别的操作图转换为底层的、更高效的、优化的计算图。
- 问题3
当你使用 PyTorch Dynamo 进行代码编译时,你希望使用哪种“后端(backend)”来实际执行编译优化?你可以把 Dynamo 理解为一个“编译器前端”,它负责分析你的 PyTorch 代码,把它翻译成一个中间表示。而这些后端,就是真正把这个中间表示编译成高效机器代码并运行在特定硬件上的“编译器后端”。
inductor
(通常为默认和推荐) 这是 PyTorch 2.0 默认和推荐的编译后端,也是最常用的。它能够将 PyTorch 代码编译成高度优化的 C++/CUDA 内核。inductor
是 PyTorch 团队为提高性能而设计的,它能针对 GPU(特别是 NVIDIA GPU)生成非常高效的代码。
- 问题4
Do you want to customize the defaults sent to torch.compile? [yes/NO]:
当你在 accelerate config
中选择启用 torch dynamo
优化后,accelerate
会在内部调用 PyTorch 的 torch.compile()
函数来对你的模型和训练循环进行编译。accelerate
默认会为 torch.compile()
使用一组合理的默认参数。但是,通过这个问题,它给你一个机会去修改这些默认值,以更精细地控制编译行为。只有在你了解 torch.compile
参数的作用,并且有特定需求(如追求极致性能或解决特定兼容性问题)时,才考虑选择 yes
并进行自定义配置。
- 问题5
Do you want to use DeepSpeed? [yes/NO]:
DeepSpeed 是微软开发的一个深度学习优化库,旨在让训练超大型模型(如大型语言模型)变得更容易、更高效、更节省资源。它提供了多种高级优化技术. 选择 NO
(默认和常见选择)
- 问题6
What GPU(s) (by id) should be used for training on this machine as a comma-seperated list? [all]:
在这台机器上,您希望使用哪些 GPU 进行训练?请以逗号分隔的列表形式提供 GPU 的 ID。默认是使用所有 GPU,回车即可。
- 问题7
Would you like to enable numa efficiency? (Currently only supported on NVIDIA hardware). [yes/NO]:
是否希望启用 NUMA 效率优化? 并且它明确指出,这项功能目前仅支持 NVIDIA 硬件。如果您使用的是 NVIDIA GPU,并且您的机器是较新的、用于深度学习训练的服务器(通常是双路或多路 CPU),那么选择 yes
可能会带来性能提升。如果是一般的家用或开发用台式机,通常选择 NO
即可,性能提升不明显。
- 问题8
你是否希望使用混合精度(Mixed Precision)训练,以及如果使用,要选择哪种浮点精度?
在训练过程中,同时使用 32 位浮点数 (FP32) 和 16 位浮点数 (FP16 或 BF16) 两种精度。现代 GPU(尤其是 NVIDIA 的 Tensor Core)对 FP16/BF16 计算有专门的硬件加速,这可以显著提高训练速度。
accelerate lunch命令
无论你是在单 GPU、多 GPU、多节点服务器(多台机器)、还是 TPU 上训练模型,你都可以使用 accelerate launch
命令来运行你的 Python 训练脚本,而不用去处理底层的分布式通信、设备管理等复杂细节。
具体怎么实现的,超纲了!忽视,我就一台机子,没有分布式环境。
了解F5-TTS 训练脚本
这是 F5-TTS 的训练入口脚本 train.py
,用于配置和启动文本到语音模型的训练。下面是模型训练中涉及的部分代码的阅读笔记
1. Hydra 管理配置文件
@hydra.main(version_base="1.3", config_path=str(files("f5_tts").joinpath("configs")), config_name=None)
def main(model_cfg):# ...训练流程代码
- 使用 Hydra 管理配置文件
config_path
指向配置文件目录config_name=None
允许通过命令行指定配置文件model_cfg
参数自动加载配置内容
配置文件目录是src/f5_tts/configs
config_name
是F5TTS_v1_Base.yaml
,是在运行命令时提供的
2. 模型配置和初始化
model_cls = hydra.utils.get_class(f"f5_tts.model.{model_cfg.model.backbone}")
model_arc = model_cfg.model.arch
tokenizer = model_cfg.model.tokenizer
mel_spec_type = model_cfg.model.mel_spec.mel_spec_typeexp_name = f"{model_cfg.model.name}_{mel_spec_type}_{model_cfg.model.tokenizer}_{model_cfg.datasets.name}"
具体的配置参数如下
下面是上图中出现的参数笔记,需要的时候再看
Transformer相关参数
arch
这一部分定义了模型内部的核心“语音处理器”——Transformer 的工作方式。Transformer 是一种非常强大的神经网络结构,擅长处理序列数据,在这里就是把文字序列变成语音特征序列。
-
dim: 1024
(维度/尺寸)
决定了模型内部处理信息的“通道”数量,影响模型的容量和表达能力。 想象这是工厂里传送带的“宽度”。宽度越大,每次能处理的信息量就越多,模型可能学到更丰富的特征,但也会更耗资源。 -
depth: 22
(深度)
Transformer 模型的层数,层数越多模型越深,理论上学习能力越强。想象这是传送带的“层数”。层数越多,信息被处理和提炼的次数就越多,模型能进行更复杂的转换和学习。 -
heads: 16
(注意力头数)
Transformer 中的多头注意力机制,每个头可以关注输入序列的不同方面,提高模型捕捉复杂关系的能力。想象这是工厂里有 16 双“眼睛”,每双眼睛都从不同的角度去看输入的信息,然后把各自的发现整合起来。这样能更全面地理解信息的不同部分之间的关系。 -
ff_mult: 2
(前馈网络乘数)
决定了 Transformer 中前馈神经网络的维度,通常是dim
的倍数,影响模型的复杂度和学习能力。想象在每层传送带上,信息经过处理后会送入一个小“加工车间”,这个数字就是这个车间的“放大倍数”。放大倍数越大,车间能处理的细节就越多。 -
text_dim: 512
(文本维度)
定义了模型在处理文本输入时,每个字符或词语被转换成的向量表示的维度。想象这是文字信息进入工厂时的“初始规格”或者“编码长度”。 -
text_mask_padding: True
(文本填充掩码)
在处理变长文本序列时,对填充(padding)部分应用掩码,防止模型关注到无意义的填充信息。想象文字输入会有长短不一的情况,为了让所有输入都一样长,我们会在短的后面“补齐”一些空白。这个设定就是告诉机器,在处理时要“忽略”这些补齐的空白,不要把它们当作真正的文字信息。 -
qk_norm: null
(查询键归一化)
控制是否对 Transformer 中的查询(query)和键(key)向量进行归一化操作,这可能会影响训练的稳定性和性能。 -
conv_layers: 4
(卷积层数)
指的是在 Transformer 模块中包含的卷积层数量,用于提取局部特征。在 Transformer 的核心处理之前或之后,信息还会经过 4 道“精细筛选”的工序,这些工序能捕捉到一些局部的、细粒度的特征。 -
pe_attn_head: null
(位置编码注意力头)
如果不为null
,它会为注意力机制提供额外的位置编码信息,帮助模型更好地理解序列中元素的顺序。 -
checkpoint_activations: False
(检查点激活)
一种节省显存(内存)的优化策略,通过在反向传播时重新计算激活值而非存储所有激活值来减少内存消耗,但会增加计算量。想象工厂里信息处理的每一步都会产生中间结果。如果设置为True
,就是说每处理完一步,我们都把这一步的中间结果“暂时存起来”,而不是直接丢弃。这样虽然处理速度会慢一点,但是能省下很多用来记录的“工作台空间”(内存)。False
就是不存。
mel_spec
(梅尔频谱设定)
mel_spec
这一部分定义了语音最终输出的梅尔频谱的特性。梅尔频谱是一种模拟人类听觉感知的语音特征表示,是语音合成中非常关键的中间产物。
-
target_sample_rate: 24000
(目标采样率)
决定了生成语音的采样率,影响语音的质量和文件大小。24000 Hz(24 kHz)是高保真语音常用的采样率。 想象这是最终语音的“清晰度”或“细腻度”。每秒钟取 24000 个声音样本,数字越大,声音就越细腻、逼真。 -
n_mel_channels: 100
(梅尔通道数)
梅尔频谱的维度,表示梅尔滤波器组的数量。更多的梅尔通道可以捕捉更丰富的语音细节。 想象这是把声音分解成不同频率成分时,“过滤器”的数量。过滤器越多,对声音频率的分析就越细致、越准确。 -
hop_length: 256
(跳跃长度)
计算梅尔频谱时,相邻帧之间采样的跳跃步长。每隔 256 个声音样本就“截取”一段声音来分析。这个值越小,分析就越密集,时间分辨率越高。 -
win_length: 1024
(窗口长度)
计算梅尔频谱时,每个分析窗口的长度。想象每次“截取”声音来分析时,我们截取多长的一段。截取 1024 个样本长度。 -
n_fft: 1024
(FFT 点数)
这是声音从时间域转换到频率域时的一种数学计算方式的参数。快速傅里叶变换 (FFT) 的点数,通常等于或大于win_length
,影响频谱分辨率。 -
mel_spec_type: vocos
(梅尔频谱类型)
指定用于生成梅尔频谱的特定声码器或梅尔频谱提取方法。不同的类型可能有不同的实现细节和音质特点。粗略理解这是生成梅尔频谱的“方式”或“算法标准”。这里可以选择使用vocos
还是bigvgan
这种算法。
3. 分词器设置
if tokenizer != "custom":tokenizer_path = model_cfg.datasets.name
else:tokenizer_path = model_cfg.model.tokenizer_path
vocab_char_map, vocab_size = get_tokenizer(tokenizer_path, tokenizer)
- 设置文本分词器路径
- 加载词汇表映射和大小
- 支持默认和自定义分词器选项
由于是默认的分词器是"pinyin", 取的是配置文件中配置的datasets中的name
Emilia_ZH_EN
是默认的数据集,此处要修改为你数据集预处理生成的数据集名称heitiane(黑天鹅)
datasets:name: heitiane
4. 数据集加载和训练启动
train_dataset = load_dataset(model_cfg.datasets.name, tokenizer, mel_spec_kwargs=model_cfg.model.mel_spec)
trainer.train(train_dataset,num_workers=model_cfg.datasets.num_workers,resumable_with_seed=666, # 数据集随机打乱的种子
)
- 加载处理过的训练数据集
- 调用训练器的
train
方法开始训练 - 配置数据加载并行度
- 设置固定种子确保可复现性
训练代码就不看了,超纲了!
开始训练模型
命令行训练模型
训练命令如下
# .yaml files are under src/f5_tts/configs directory
accelerate launch src/f5_tts/train/train.py --config-name F5TTS_v1_Base.yaml
或者指定某个配置项的值
# possible to overwrite accelerate and hydra config
accelerate launch --mixed_precision=fp16 src/f5_tts/train/train.py --config-name F5TTS_v1_Base.yaml ++datasets.batch_size_per_gpu=19200
由于训练时间较长,所以需要放在后台训练,下面是训练命令示例,请根据自己需要调整参数,其实所有的参数都能在命令行中指定,其他参数可以参考web ui章节给出的命令
nohup accelerate launch --mixed_precision=fp16 src/f5_tts/train/train.py --config-name F5TTS_v1_Base.yaml ++datasets.batch_size_per_gpu=4800 ++optim.epochs=30 ++optim.earning_rate=9.375e-6 > train_log.log 2>&1 &
web ui训练模型
f5-tts_finetune-gradio --port 7860 --host 0.0.0.0
启动的时候最好加上nohup放在后台跑
nohup f5-tts_finetune-gradio --port 7860 --host 0.0.0.0 2>&1 &
经过前面训练脚本的学习,再看web界面会清晰很多,下面是官方的web使用教程视频,就是教如何用web完成所有操作(数据集处理、模型训练/微调、模型测试等)
https://github.com/SWivid/F5-TTS/discussions/143
web训练模型的截图,能够看到训练进度、以及很方便调整参数和查看当前训练的效果。值得注意的是,web对文件的位置是有要求的,具体请根据后台报错信息查看。
在使用的过程中,各种因素可能会导致训练中断,web中可以指定checkpoint功能,就算挂了,也能续上继续训练。当然命令行也支持,可以参考下面的命令。
web实际上用的微调命令大致如下
accelerate launch --mixed_precision=fp16 /home/ubuntu/F5-TTS/src/f5_tts/train/finetune_cli.py --exp_name F5TTS_v1_Base --learning_rate 1e-05 --batch_size_per_gpu 4898 --batch_size_type frame --max_samples 64 --grad_accumulation_steps 1 --max_grad_norm 1 --epochs 100 --num_warmup_updates 100 --save_per_updates 500 --keep_last_n_checkpoints 5 --last_per_updates 100 --dataset_name heitiane --finetune --tokenizer pinyin --log_samples
训练模型遇到的问题
显存不足
这里有几个概念需要弄清楚
batch_size
它指的是每次模型参数更新(一次反向传播)所使用的训练样本数。相当于背单词,让你一次背完单词书,脑子不够用(显存不够多),当然搞不了。
epochs
它的含义是整个训练集被模型完整“看”一遍的次数,相当于背多少次单词书。
假设你有 1000 个训练样本,设置 batch_size = 100
,每一步训练处理 100 个样本,每个epoch
需要重复 1000 / 100 = 10次(步数)。
默认的batch_size_per_gpu=38400太大了,显存放不下,改小就够了。我的显存是16G,调到4800才能够正常跑下去。将 batch_size_per_gpu
从 38400 降低到 4800 时,这是一个非常显著的减少(减少到原来的 4800/38400=1/8)。理论上,如果其他因素不变,显存占用会下降到原来的 1/8。这应该能有效解决遇到的 CUDA out of memory
错误。
但是这种大幅度减小 batch_size
对训练过程和模型质量有重要的影响,其他的参数也需要相应地进行调整。
学习率 (Learning Rate) 调整
batch_size
越小,每次梯度更新的噪声越大。如果学习率保持不变,模型在损失函数空间中的跳动会非常剧烈,可能导致:
- 无法收敛: 模型在最优解附近震荡,无法稳定下来。
- 梯度爆炸: 极端情况下,损失值变成
NaN
。 - 收敛到次优解: 即使收敛,也可能不如使用大批量时找到的最优解。
如何调整
遵循学习率线性缩放规则,如果原始 batch_size
是 38400,对应学习率是 7.5×10−5。
新的 batch_size
是 4800,是原来的 1/8。那么,新的学习率应该调整为:
7.5×10−5×(4800/38400)=7.5×10−5×(1/8)=0.9375×10−5=9.375×10−6
训练命令加上 ++optim.earning_rate=9.375e-6
即可
总训练步数与 Epochs
如果您保持 epochs
不变,由于 batch_size
减小到 1/8,那么每个 epoch
内的训练步数会增加 8 倍。这意味着总训练步数会大幅增加。
如何权衡
-
增加
epochs
: 尽管总步数增加了,但由于每次更新的梯度估计质量下降,模型可能需要更多的epochs
才能达到与大batch_size
相似的收敛程度。您可以尝试增加epochs
,例如增加 2-4 倍,但需要观察验证损失,防止过拟合。 -
梯度累积 (Gradient Accumulation): 强烈推荐。这是一种更优雅的解决方案,可以在保持小
batch_size
的同时,模拟更大的有效批量大小。如果您将batch_size_per_gpu
从 38400 降到了 4800 (1/8),您可以设置gradient_accumulation_steps = 8
。这意味着模型每 8×4800=38400 帧才执行一次权重更新。这相当于在每次权重更新时,使用了与原来 38400 帧相同的有效批量大小。
什么是梯度累积
首先要理解两个概念,
- 物理批次大小 (Physical Batch Size): 这是您实际能够装入 GPU 内存的批次大小。在本例中,您将其设为 4800 帧/GPU。
- 有效批次大小 (Effective Batch Size): 这是模型在执行一次权重更新时,所“看到”的数据总量。它等于 物理批次大小 × 梯度累积步数。
没有梯度累积时(原始设置):
batch_size_per_gpu = 38400
帧。每次处理 38400 帧的数据,就执行一次梯度计算和一次权重更新。假设总训练数据是 Total_Frames
。总的权重更新步数 = Total_Frames / 38400
使用梯度累积后:
batch_size_per_gpu = 4800
帧。gradient_accumulation_steps = 8
。
模型会处理第一个 4800 帧的批次,计算梯度,但不更新权重,而是累积梯度。接着处理第二个 4800 帧的批次,计算梯度,将其累积到之前的梯度上。这个过程重复 8 次。当处理完第 8 个 4800 帧的批次后,累积的梯度就相当于一个包含了 8×4800=38400 帧的超级批次所计算出的梯度。此时,模型才执行一次权重更新。
所以,虽然您每次 GPU 处理的物理数据量变小了(4800 帧),但是模型进行一次实际的权重更新,依然是基于 38400 帧的数据(有效批次大小)。因此,总的权重更新步数又变回了:Total_Frames / 38400。
梯度累积的好处是,可以保持梯度估计的“平滑性”与大批量接近,同时又降低了单次前向/反向传播的显存占用。如果使用梯度累积,那么 epochs
和学习率的调整就更接近于原始大批量的情况。
生成的全是噪音(??)
最初训练了4个小时左右,从头训练的一个模型,用推理页面测试,结果出来全是噪音。怀疑是训练时间不够或者自己哪里操作、参数有问题。改微调模型,在微调的web ui上测试是正常的,但是在推理的web ui上就又全是噪音了。可能是推理web ui上代码有bug。由于训练了4个小时的模型已经删了,已经无从考证了。本来想仔细读读代码看是不是bug的,但是时间不够了,gpu服务器时间到期了。。。
数据集的语音质量是可以的,但是这个数据集是否适合用来训练。这得去看看论文了解模型算法,还有对应的训练数据集。而我本意是想得到一个稳定的某个音色,所以还是选择微调吧。
F5-TTS 作为一个零样本语音克隆模型,其核心能力在于无需特定说话人的训练数据,即可通过短时参考音频克隆音色。微调的意义在于将这种通用克隆能力进一步特化和优化,以达到更高水平的质量、稳定性、适应性。
查看gpu使用情况
watch -n 1 nvidia-smi
-
推理单个句子时候,显存使用情况
-
使用web ui微调时候显存使用情况
web实际调用的微调命令如下:
accelerate launch --mixed_precision=fp16 /home/ubuntu/F5-TTS/src/f5_tts/train/finetune_cli.py --exp_name F5TTS_v1_Base --learning_rate 5e-06 --batch_size_per_gpu 4800 --batch_size_type frame --max_samples 64 --grad_accumulation_steps 8 --max_grad_norm 1 --epochs 100 --num_warmup_updates 100 --save_per_updates 500 --keep_last_n_checkpoints 5 --last_per_updates 100 --dataset_name test-demo --finetune --tokenizer pinyin --log_samples
微调结果
损失曲线(Loss Curve)是深度学习训练过程中,损失函数值随训练迭代次数(或 Epochs)变化的图形表示。它是评估模型训练状态和诊断问题(如欠拟合、过拟合、学习率不当)的最重要工具之一。
在 TensorBoard 或其他监控工具中观察训练损失的变化。在web ui中可以看到,训练模型的页面有个日志选项是可以选择TensorBoard的,所以代码是已经集成了TensorBoard了的。
安装相关的库
pip install tensorboard
然后在F5-tts项目根目录执行下面的命令然后访问页面即可
tensorboard --host 0.0.0.0 --logdir runs/ --port 8000
下图是测试微调时候的截图
第一张图可以看到损失曲线整体呈现下降趋势。虽然有波动,但平滑后的曲线(浅色线)从大约 0.75
附近下降到了 0.6787
。这表明模型正在学习!损失的下降是积极的信号,意味着模型正在改进其预测能力。但是由于时间太短了,得加长训练时间!
第二张图是典型的余弦退火(Cosine Annealing)学习率调度器的曲线。余弦退火是一种非常有效的学习率调度策略。它在训练初期保持较高的学习率以快速探索损失空间,然后在后期平滑地降低学习率,使模型能够更精细地收敛到最优解,并且有助于跳出局部最优。至于图上能看出什么。。。没研究
命令行去使用微调得到的模型
f5-tts_infer-cli --model F5TTS_v1_Base \
--ckpt_file /home/ubuntu/F5-TTS/ckpts/test-demo/my.safetensors \
--vocab_file /home/ubuntu/F5-TTS/data/test-demo_pinyin/vocab.txt \
--ref_audio /home/ubuntu/tests/chapter3_5_blackswan_154.wav \
--ref_text "这个方向,请跟我来." \
--output_file /home/ubuntu/tests/tests/infer_cli_basic.wav \
--gen_text "可恶,这狗bug浪费了我不少时间"