天天速看:基于tacotron2的galgame角色语音合成+galgame解包 保姆级教程(千恋万花版)
(注:本专栏只讨论相关技术,不涉及任何其他目的,如侵删)
前言
前段时间在b站上看到了基于TTS模型合成喜爱的二次元语音的一系列教程,觉得非常有意思,也想利用寒假复现一下。但当时忙着复习期末考试,一直拖到了过年这几天才有时间好好研究了一番。
我复现的方式是基于最开始的tacotron2模型,使用NVIDIA官方的预训练模型进行微调,训练语料来自于《千恋*万花》中的芳乃,使用Google Colab + Kaggle的GPU进行训练。因此本教程适合那些有基本的Python编程和深度学习知识、同样想要复现的xdm。
(资料图片)
(我本人也不是科班出身学人工智能的,Python和深度学习都是半路出家,所以这里不会涉及过于复杂的实现细节。本教程尽量一步步详细地描述实现过程,希望大家看过之后能大概知道整个流程到底是怎么样的)
tacotron2模型基本介绍
本教程采用原up最开始的思路,具体可以看下面这个视频。
关于tacotron2模型的介绍,感兴趣的同学可以看下面这个视频(不是必要的,下面这个视频有差不多一个小时),讲得比较清楚。
由于我们是复现,不要求对模型具体的结构有过于深入的了解,所以我们只需要大概知道,tacotron2模型实现的效果是我们给定一段文本输入,模型会输出相应的Mel谱,通过后续的WaveGlow声码器得到最终的声音。
提取数据之前,我们在原up的github链接https://github.com/CjangCjengh/tacotron2-japanese中,下载相应的代码压缩包,解压后得到tacotron2-japanese-master文件夹。
文本
有了模型,接下来是最重要的部分:获取数据。这里获取数据的方式借鉴了下面的两个教程:
下面我会一步步展示如何提取文本和生成filelist文件,后文有我写的一个简单的Python脚本,可以生成最终的filelist。
我这次提取的是《千恋*万花》中的芳乃语音。游戏文件夹中有data.xp3(不是data1080.xp3)和voice.xp3两个文件,分别存储了角色台词和对应语音。
进入GARBro软件,路径设置为游戏文件夹,界面如下:
打开data.xp3,界面如下:
再打开scn文件夹:
这些scn后缀的文件就是我们需要的文本文件。从头到尾全部选中,右键,提取:(注意不要只提取芳乃开头的,因为在共同线和其他角色线中也有不少芳乃语音)
提取完后,使用Freemote软件转换为json文件,具体操作是选中文件后,拖至FreeMoteToolkit文件夹中的PsbDecompile.exe中,会显示“用xxx.exe”打开:
这样,每个scn文件会生成相应的json文件和resx.json后缀的文件。
打开json文件和resx.json文件(可以用记事本打开),我们会发现有用的信息全在json文件中:
然而,json文件包含众多信息,以及多级字典嵌套,不方便直接生成我们想要的txt文件。在Python中,使用json库读入json文件,得到一个字典。经过我的观察,文本信息在该字典的“scene”元素下的某一个子字典,该字典中有“texts”键,对应的值为一个列表,存储相应的文本信息:
在文件中搜索“voice”,可以找到我们想要的台词文本和相应的音频文件:
由于没有找到生成filelist文件的教学,我写了一个简单的py脚本generate_txt.py,可以处理文件夹中所有的json文件,提取特定角色的语音,使用时直接把下面的脚本移动至tacotron2-japanese-master文件夹中即可运行。
在后续训练中,我发现数据集中可能混杂着一些异常样本,在经过cleaner之后得到空字符串,长度为0,导致后续的pack_padded_sequence函数报错。因此在生成数据集文件时,我们需要去除这些异常样本。
这里我采用的是一种比较笨的方法,即对每个样本做一次cleaner,保留长度不为0的样本。(其实可以直接生成cleaner之后的txt,但我懒得改了)
(注:千恋万花没有单独划分h语音,我目前没有特别好的办法自动去除数据集中的h语音,去除所有包含消音字符“●”的文本可能有一定作用)
由此,我们得到了训练集和测试集,分别对应transcript_train.txt和transcript_test.txt。
语音提取
提取完文本,接下来是提取语音。在GARbro中打开voice.xp3文件:
前三个字母代表角色(akh我也不知道是谁),后面的一串数字是音频的章节和序号。我们找到“yos”开头的音频,选中后提取出来:
这时候我们提取出来的是ogg文件,需要转换成wav文件。下载格式工厂,这是一个免费的格式转换软件,界面如下:
选择转换成wav文件,在输出配置中设置采样率为22050Hz,单声道:
随后选中所有ogg文件,批量转换成wav文件,完成音频导出:
模型训练(Colab)
提取完所有的数据,我们正式开始训练模型,这里可以直接使用前述教程中原up提供的colab链接,但为了方便调参,我修改了一下相应的colab笔记本,具体链接为https://colab.research.google.com/drive/1MvKoW9h1ul1WTn1WIubAqHMjpXajFQ6a#scrollTo=5mBxgMRL23_x
我稍微修改了原代码中create_hparams()函数的定义,便于之后修改超参数。
在colab中,我直接clone原up的git,随后我们需要上传相应的数据集文件至filelists文件夹,colab不支持重名文件替代,所以我们需要先删除原来的txt:
(如果提示找不到原来的txt文件,可以刷新一下文件目录)
同时注意后续create_hparams中self.training_files和validation_files的路径:
随后我们需要将自己的数据集以压缩包形式上传到Google drive,最好命名为wav.zip,随后解压:
在第一次训练时,我们需要下载官方提供的预训练模型,然后把warm_start设置为1,这样会重置记录的epoch数。同时我们还需要将checkpoint保存路径设置为drive云盘,这样如果训练中断了,我们的模型不会消失:
在训练开始前,我们需要在create_hparams中修改总迭代epochs和多少次迭代保存一次:
(我个人的经验,100个epoch大概要跑9h左右)
随后可以开始训练:
为了防止colab连接中断,我们可以在睡觉前操作一下:https://blog.csdn.net/Thebest_jack/article/details/124565741
这样睡醒后就可以看到训练好的模型了!!
模型训练(Kaggle)
Colab只能训几个小时就不能用了,而Kaggle提供了35h的GPU使用时间,所以我也想利用Kaggle完成训练,但是在网上好像没有相关的教程,我就自己摸索了一下。
在训练之前,我们需要将我们的模型和数据集上传至Kaggle的Dataset。我们打开Create a New Dataset,建立两个数据集:checkpoint和yoshino,其中checkpoint存放我们现有的模型,yoshino中有两个子文件夹,存放wav和filelists:
注意,当上传文件夹时,Kaggle会自动解压,因此wav.zip中可以包含filelists和wav,方便后续操作:
上传成功后界面如下:
Kaggle的ipynb链接为https://www.kaggle.com/code/littleweakweak/tacotron2,下面简单做一些解释。
在Kaggle的环境中,不能直接安装pyopenjtalk,搜索之后得知需要先uninstall cmake:
Kaggle的ipynb和Colab大同小异,但是需要针对Kaggle的文件路径做一些调整。(根据你自己的dataset名字进行修改)
随后我们就可以开始训练了。对于防止Kaggle中断,网上也有类似的方法,但我尝试过后好像不行。实际上Kaggle提供了更简单高效的方式:save version
具体操作见https://www.cnblogs.com/zgqcn/p/14160093.html,设置完save version后,我们就可以关机睡觉了,等它训练完成后便可以查看结果。由于save version不会中途保存,我们设置迭代epoch时需要估计一下训练时长(100个epoch大概9h),以免超过使用时间限制。
提醒!!!Kaggle每次继续训练时,记得更新checkpoint数据集中的模型,否则还会从上次的模型开始训练,我就是疏忽了这一点白训了十个小时)
训练心得
写到这里,模型训练的过程大概描述完了,下面是个人的一些心得:
训练过程中可以逐步调低学习率,一开始学习率1e-3,等训练一段时间之后可以调低至1e-4、1e-5等。时不时观察一下train loss和val loss的变化趋势,判断模型的状态
我这次大概训练了不到30h,25000次迭代,最终train loss大概0.21,val loss大概0.27。实际上最后几个小时,train loss一直慢慢下降,val loss一直在0.27附近波动,怀疑出现过拟合,最后学习率为1e-5,weight_decay为1e-5,不打算继续训练了。由于之前没有训练神经网络的经验,如果有大佬有比较好的方案,欢迎评论区指出)
最后模型可以发出比较清晰的日语,但是音调有些奇怪;如果用假名表示汉语拼音,某些句子发音还不错,如果每个音发的比较短,可以再加一个音,如:びぇえざいじぇえりいふぁあでん(鳖在这理发店)
在inference时,如果输出mel_outputs_postnet.shape,会发现每次inference的输出都不同。查阅之后得知,这是因为模型的Prenet中,dropout层的training = True,所以model.eval()对其不起作用,而其他dropout层的training = self.training。我在训练完之后才知道这个事情,而训练过程中validate时同样激活了Prenet的dropout层。而当我直接修改prenet时,模型输出异常。目前不知道在训练前调整prenet之后结果如何,感兴趣的xdm可以尝试之后告诉我。
结语
到这里整个模型的训练过程就讲完了。经过这几天的努力,最终实现了一个还算不错的结果。这真的是一个很有意思的项目,非常敬佩原up的创意和后来各位up的改进,由于我是第一次复现项目,也是第一次训练TTS模型,知识有限,还有很多的不理解之处,发出这个简陋的教程也是希望和大家共同学习,希望之后能继续尝试一些有意思的项目。