基于VITS的galgame角色語(yǔ)音合成+使用Kaggle的訓(xùn)練教程(9nine版)

(注:本專欄只討論相關(guān)技術(shù),不涉及任何其他目的,如侵刪)
摘要
從2022年7月開(kāi)始,利用語(yǔ)音合成模型進(jìn)行動(dòng)漫角色的語(yǔ)音合成在B站掀起了一股小范圍熱潮。一開(kāi)始人們使用tacotron2模型進(jìn)行語(yǔ)音合成,隨后,效果更好的VITS模型逐漸受到人們的關(guān)注,并最終成為動(dòng)漫角色語(yǔ)音合成的主流模型。VITS模型由韓國(guó)科學(xué)技術(shù)院(KAIST)的研究者們于2021年提出,其通過(guò)結(jié)合變分推理、標(biāo)準(zhǔn)化流和對(duì)抗訓(xùn)練,實(shí)現(xiàn)更自然、更多樣的語(yǔ)音合成。截至目前,B站上已出現(xiàn)不少利用Colab在線訓(xùn)練VITS模型的教程視頻,然而,免費(fèi)版Colab每天只提供幾小時(shí)的GPU(Tesla T4)使用時(shí)間,且需要定時(shí)操作以防止下線。Kaggle平臺(tái)每周提供了30h的GPU(1塊Tesla P100或者2塊Tesla T4)使用時(shí)間,同時(shí)支持后臺(tái)訓(xùn)練。B站上缺乏相關(guān)的Kaggle詳細(xì)教程,因此,本專欄以9nine系列游戲?yàn)槔?,介紹了如何使用Kaggle訓(xùn)練自己的VITS模型,同時(shí)包含9nine系列游戲的解包內(nèi)容。
前言(廢話)
本專欄是對(duì)上一期視頻的補(bǔ)充:

這期視頻大概斷斷續(xù)續(xù)做了差不多一個(gè)月,一開(kāi)始提取數(shù)據(jù)集、學(xué)習(xí)模型、debug代碼加上訓(xùn)練花了一個(gè)多星期,中間一個(gè)星期在忙畢業(yè)論文,結(jié)果再次打開(kāi)發(fā)現(xiàn)原來(lái)的代碼運(yùn)行的時(shí)候會(huì)報(bào)錯(cuò)。我研究了兩三天,最后發(fā)現(xiàn)問(wèn)題出在torch的版本上,于是我又重新改寫(xiě)了里面的函數(shù),重新debug,昨天終于把這個(gè)視頻發(fā)出來(lái)了,確實(shí)不容易。
上期視頻本質(zhì)上還是在前人的工作上修修補(bǔ)補(bǔ),技術(shù)含量較低,由于本人還是初學(xué)者,代碼部分難免有許多不正確之處,歡迎大家指正。
Kaggle Notebook:https://www.kaggle.com/code/littleweakweak/train-vits-ss
預(yù)訓(xùn)練VITS模型鏈接:https://huggingface.co/spaces/skytnt/moe-tts
VITS模型的基本介紹
VITS模型本身比較復(fù)雜,對(duì)其結(jié)構(gòu)感興趣的B友可以看下面這個(gè)視頻或者這個(gè)鏈接(https://zhuanlan.zhihu.com/p/419883319),講的很細(xì)(雖然我自己也沒(méi)看完)。

VITS是一個(gè)端到端的語(yǔ)音合成模型,最終的效果是給定一段文本輸入,可以直接輸出對(duì)應(yīng)的語(yǔ)音波形。項(xiàng)目鏈接如下:
VITS項(xiàng)目鏈接:https://github.com/jaywalnut310/vits
用于日語(yǔ)語(yǔ)音合成的VITS項(xiàng)目鏈接:https://github.com/CjangCjengh/vits
Galgame解包和數(shù)據(jù)集提取
這里我們使用KrKrExtract對(duì)9nine游戲進(jìn)行解包。解包過(guò)程可以參考我的第一期專欄:

9nine系列游戲有四部,每部的文本文件以a-d開(kāi)頭,同時(shí)每部游戲里的音頻文件命名編號(hào)都是獨(dú)立的(即九九九里有名為sr0001.wav的音頻,天天天里也有名為sr0001.wav的音頻,后兩部同理)。

為后續(xù)訓(xùn)練方便,我把所有的scn文件放在一個(gè)文件夾里,然后用Freemote轉(zhuǎn)換成json格式。

然后從每部游戲的音頻文件夾中,找到對(duì)應(yīng)角色的音頻,這里我選擇的是新海天,因此找的是sr開(kāi)頭的文件。(注:sr開(kāi)頭的文件不只在sr文件夾下,在ep1-ep3文件夾中也有,因此需要注意一下不要提取漏了。)

把每部游戲的ogg音頻文件分別放在四個(gè)文件夾中:

隨后我們需要把ogg文件轉(zhuǎn)換成wav格式,同時(shí)設(shè)置采樣率為22050。最終,所有wav文件被放在一個(gè)文件夾中,重命名為sr0001a.wav、sr0001b.wav、sr0001c.wav,后續(xù)以此類推。
為此,我寫(xiě)了兩個(gè)py腳本,分別用于生成文本文件和音頻格式轉(zhuǎn)換。
生成文本文件:
運(yùn)行代碼前同樣需要指定vits模型所在文件夾,以及scn文件的存儲(chǔ)文件夾。這段代碼和第一期專欄中的有一些不同,主要是因?yàn)?nine里存在兩個(gè)角色同時(shí)說(shuō)一句話的情況,因此需要再加一個(gè)判斷,提取出選定的角色。其他細(xì)節(jié)可以參考第一期專欄。
處理音頻文件:
處理音頻文件使用了pydub庫(kù),運(yùn)行前需要指定原始o(jì)gg文件和轉(zhuǎn)換后的wav文件的存儲(chǔ)路徑。如果音頻文件較多,轉(zhuǎn)換過(guò)程會(huì)有些慢,可以考慮os.rename統(tǒng)一重命名后用格式工廠轉(zhuǎn)換。
在最后我加了一個(gè)驗(yàn)證環(huán)節(jié),即看看filelists中的記下的音頻數(shù)和我們最終轉(zhuǎn)換后的音頻數(shù)是否一致,如果一致則說(shuō)明提取成功,否則可能是你的音頻文件提取漏了。
最終的txt文件和wav文件夾長(zhǎng)下面這樣:


使用Kagglle
Kaggle數(shù)據(jù)集上傳
數(shù)據(jù)集提取完成后,我們需要上傳到Kaggle的Dataset上。我們將wav文件夾和filelists文件夾壓縮,然后上傳到Kaggle上,這里可以參考上一期視頻(左上角Create -?New Dataset):


進(jìn)入Kaggle
下一步進(jìn)入Notebook,添加數(shù)據(jù)集(“Add Data”),設(shè)置Accelerator為GPU。然后劃到頁(yè)面最下方的代碼塊,填入音頻文件、數(shù)據(jù)集和測(cè)試集的路徑。上述步驟可以依照視頻中的操作進(jìn)行。
(我原來(lái)想用2塊T4進(jìn)行多卡訓(xùn)練的,但是jupyter notebook應(yīng)該不支持直接調(diào)用torch.multiprocessing.run,想多卡訓(xùn)練的可以考慮寫(xiě)一個(gè)py腳本,然后用命令行模式運(yùn)行)
下面說(shuō)一下我代碼里相比原項(xiàng)目修改的部分,不感興趣的可以直接跳到下一部分。
如果現(xiàn)在Kaggle新建一個(gè)Notebook,里面默認(rèn)的torch版本是2.0.0:

原項(xiàng)目中TextAudioLoader為Dataset的子類,在__getitem__函數(shù)中調(diào)用了get_audio_text_pair()函數(shù),進(jìn)而調(diào)用get_audio()函數(shù)。get_audio()函數(shù)調(diào)用了spectrogram_torch()函數(shù)對(duì)輸入波形做短時(shí)Fourier變換(STFT),返回相應(yīng)的頻譜。
而在spectrogram_torch()函數(shù)中,調(diào)用了torch.stft()函數(shù)進(jìn)行STFT:
查閱官方文檔,torch.stft()的最后一個(gè)參數(shù)是return_complex:

return_complex表示stft的返回值是否是復(fù)數(shù)形式。return_complex=True,返回tensor的數(shù)值類型為復(fù)數(shù);return_complex=False,分別返回復(fù)數(shù)的實(shí)部和虛部(相當(dāng)于最后多了一個(gè)長(zhǎng)度為2的維度)。在torch前幾個(gè)版本中,torch.stft()不要求指定return_complex,此時(shí)默認(rèn)返回的形式為實(shí)部和虛部。比如input的shape是(3, 5,?10),返回的tensor維度應(yīng)該是(3, 5,?10, 2)。按照原項(xiàng)目的代碼,最后返回的實(shí)際上是每個(gè)復(fù)數(shù)的模(即對(duì)最后一個(gè)維度平方后求和):
而到了2.0版本之后,torch強(qiáng)制要求stft函數(shù)中return_complex=True,此時(shí)返回的tensor維度和之前的對(duì)不上,因此需要調(diào)用view_as_real()函數(shù)把復(fù)數(shù)恢復(fù)成實(shí)部和虛部的形式:

所以解決這個(gè)問(wèn)題可以降低torch的版本,這個(gè)我試了一下好像沒(méi)成功,所以我只能在Notebook里修改spectrogram_torch()函數(shù)。
另外,模型生成器的forward()里調(diào)用了rand_slice_segments()函數(shù)進(jìn)行隨機(jī)切片:
注意這里的x和y分別是文本和頻譜,z是文本序列經(jīng)過(guò)編碼之后的embedding(這個(gè)命名方式蒸烏魚(yú)):
rand_slice_segments()函數(shù)做的事情是:在batch中的每一個(gè)樣本中,隨機(jī)截取一定長(zhǎng)度的文本序列對(duì)應(yīng)的embeddings,上面的y_length對(duì)應(yīng)padding之后的輸入序列的有效長(zhǎng)度。segment_size參數(shù)表示截取片段的長(zhǎng)度,在模型定義時(shí)傳入的參數(shù)是hps.train.segment_size // hps.data.hop_length =?8196/256 = 32:
所以這一步截取實(shí)際上得到的是embeddings的切片序列,每個(gè)序列長(zhǎng)度為32。
而在訓(xùn)練過(guò)程中,我們需要把前面的切片序列對(duì)應(yīng)的頻譜片段也找出來(lái),因此需要傳入ids_slice:
所以頻譜片段的長(zhǎng)度應(yīng)該大于切片序列的長(zhǎng)度,因此我在定義數(shù)據(jù)集時(shí)加了一步篩選,去掉頻譜長(zhǎng)度小于32的樣本:
以上就是我主要修改的代碼部分,接下來(lái)是模型訓(xùn)練。
(上面的代碼部分的分析可能不完善,歡迎大佬指正)
模型訓(xùn)練
訓(xùn)練前我們需要指定epochs和batch size,batch size可以先調(diào)大一些,跑完一個(gè)epoch看看占用率,如果GPU沒(méi)有爆顯存那就沒(méi)問(wèn)題。第一次訓(xùn)練時(shí)把first_train=True,這樣會(huì)下載原up的預(yù)訓(xùn)練模型。然后設(shè)置學(xué)習(xí)率,下一步就可以run all了。
恢復(fù)訓(xùn)練時(shí)上傳checkpoint文件到數(shù)據(jù)集,設(shè)置first_train=False,其他同視頻一樣。
調(diào)試完成后,可以用save version進(jìn)行后臺(tái)訓(xùn)練。save version只有在全部運(yùn)行完或者終止運(yùn)行的時(shí)候才可以看輸出,如果輸出文件夾沒(méi)有文件就多刷新幾次。
Kaggle的其他具體操作可以看我的第一期專欄,本地推理部分可以看我的第三期專欄。

結(jié)語(yǔ)
1. 我在第一期專欄中提到tacotron2輸出隨機(jī)性的問(wèn)題,當(dāng)時(shí)不了解TTS模型,后面看網(wǎng)上的教程才知道,這是為了讓同一句話有不同的讀法和感情,因此VITS也是如此。可以多輸出幾次進(jìn)行比較,選出發(fā)音和情感最好的一版。
2. 我個(gè)人的推理經(jīng)驗(yàn):模型在讀長(zhǎng)句子的時(shí)候可能情感比較低沉,這時(shí)候可以在句尾加日文感嘆號(hào),同時(shí)如果模型最后一個(gè)字發(fā)音不完全,可以再加個(gè)句號(hào)(經(jīng)驗(yàn),不確定是否嚴(yán)謹(jǐn));而在讀比較短的句子的時(shí)候,最好不要加感嘆號(hào),否則讀出來(lái)的效果可能會(huì)過(guò)于激動(dòng)。反正可以多試試,找到最合適的版本。

3. 這又是拿VITS水了一期,VITS相比tacotron2還是復(fù)雜了許多,同時(shí)也遇到了很多之前沒(méi)想到的麻煩。不管怎么說(shuō),還是學(xué)到了不少,歡迎大家一起交流討論。