Closed kyakuno closed 4 months ago
python3.9が必要、python3.11だとnumbaが入らない
macの場合
pip3.9 install -r requirements.txt
pip3.9 uninstall torch torchaudio
pip3.9 install --pre torch torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu
python3.9 webui.py
uvr5のweightを使用した場合にMDXNetDereverbが呼ばれてONNXが使用されるが、 GPT_SoVITSのONNXの推論コードは含まれていない気がする。
UIを開く
0ショットで推論
参照音声。
RVCを使用したボイスチェンジャーを作るための元データとなる音声収録のテストを行なっています。
生成音声。
こんにちは。今日はAIについて解説します。
0ショットだと精度が低いのでFineTuningは必要そう。
FineTuningのためのデータセット作成
音声ファイルを入力して分割後、ASRでテキストを生成できる。 音声ファイルはoutputs/slicer_optに、テキストファイルはoutputs/asr_optに格納される。
ASRをfaster whisperにするとjaも選択できる。
macOSだとfatster whisperが例外で落ちるので、ファイルの分割だけWebUIを使用し、手動でlstを作る。
vocal_path|speaker_name|language|text
作ったlst。
output/slicer_opt/kyakuno.wav_0000644800_0000964160.wav|slicer_opt|JA| RVCを使用したボイスチェンジャーを作るための元データとなる音声収録のテストを行なっています。30秒のデータを使用してどれくらい音声
output/slicer_opt/kyakuno.wav_0000406080_0000644800.wav|slicer_opt|JA| 変換が成功するのかどうかを試すために、とりあえず30秒の録音データを作成しています。
output/slicer_opt/kyakuno.wav_0000007360_0000406080.wav|slicer_opt|JA| RVCのバックエンドはPytorchで構築されており、Pytorchを使って学習をします。その際にF0を取得します。
データセット設定、トークン抽出
学習
macOSだと学習に非常に時間がかかるのでWindows + Conda + RTX3080に移行。
推論UIは下記のコマンドで単独で起動できる。
python3 GPT_SoVITS/inference_webui.py
inference_webui.pyを書き換えて、下記のようにすると、CUIからwavを推論できる。
inp_ref = "JSUT.wav"
prompt_text = "水をマレーシアから買わなくてはならない。"
prompt_language = i18n("日文")
text = "水は、いりませんか?"
text_language = i18n("日文")
how_to_cut = i18n("凑四句一切")
top_k = 5
top_p = 1
temperature = 1
ref_text_free = False
output = get_tts_wav(inp_ref, prompt_text, prompt_language, text, text_language, how_to_cut, top_k, top_p, temperature, ref_text_free)
# get_tts_wavのyieldをreturnに書き換える
print(output)
import soundfile as sf
sf.write("x.wav", output[1], output[0])
get_tts_wavは下記の3モデル構成。
ssl_model -> ssl_content
t2s_model -> pred_semantic
vq_model -> audio
GPT_SoVITS/onnx_export.pyにONNXへのエクスポートコードがある。 モデルパスを下記のように修正すると動く。
#cnhubert_base_path = "pretrained_models/chinese-hubert-base"
import os
cnhubert_base_path = os.environ.get(
"cnhubert_base_path", "GPT_SoVITS/pretrained_models/chinese-hubert-base"
)
gpt_path = "GPT_SoVITS/pretrained_models/s1bert25hz-2kh-longer-epoch=68e-step=50232.ckpt"#"GPT_weights/nahida-e25.ckpt"
vits_path = "GPT_SoVITS/pretrained_models/s2G488k.pth"#"SoVITS_weights/nahida_e30_s3930.pth"
exp_path = "nahida"
デフォルトだとノイズをrefにするので、下記をコメントアウトする必要がある。
ref_audio = torch.tensor([load_audio("JSUT.wav", 48000)]).float()
エクスポート後にコード中のdebug = Trueにすると、onnxruntimeで推論できる。
phonesは下記のように与える。
ref_seq = torch.LongTensor([cleaned_text_to_sequence(['m', 'i', 'z', 'u', 'o', 'm', 'a', 'r', 'e', 'e', 'sh', 'i', 'a', 'k', 'a', 'r', 'a', 'k', 'a', 'w', 'a', 'n', 'a', 'k', 'U', 't', 'e', 'w', 'a', 'n', 'a', 'r', 'a', 'n', 'a', 'i', '.'])])
text_seq = torch.LongTensor([cleaned_text_to_sequence(['m', 'i', 'z', 'u', 'w', 'a', ',', 'i', 'r', 'i', 'm', 'a', 's', 'e', 'N', 'k', 'a', '?'])])
出力されるONNXファイル
-rw-r--r-- 1 kyakuno staff 11035654 3 18 14:30 nahida_t2s_encoder.onnx
-rw-r--r-- 1 kyakuno staff 307511198 3 18 14:30 nahida_t2s_fsdec.onnx
-rw-r--r-- 1 kyakuno staff 307574675 3 18 14:31 nahida_t2s_sdec.onnx
-rw-r--r-- 1 kyakuno staff 162706799 3 18 14:32 nahida_vits.onnx
推論コードを書けば動きそうな気もする。
debug引数はGptSoVitsにしか回ってきていないので、T2SModelには手動で実装が必要。
下記でONNX Runtimeで推論できた。
def forward(self, ref_seq, text_seq, ref_bert, text_bert, ssl_content, debug=False):
early_stop_num = self.t2s_model.early_stop_num
if debug:
import onnxruntime
sess_encoder = onnxruntime.InferenceSession(f"onnx/nahida/nahida_t2s_encoder.onnx", providers=["CPU"])
sess_fsdec = onnxruntime.InferenceSession(f"onnx/nahida/nahida_t2s_fsdec.onnx", providers=["CPU"])
sess_sdec = onnxruntime.InferenceSession(f"onnx/nahida/nahida_t2s_sdec.onnx", providers=["CPU"])
#[1,N] [1,N] [N, 1024] [N, 1024] [1, 768, N]
if debug:
x, prompts = sess_encoder.run(None, {"ref_seq":ref_seq.detach().numpy(), "text_seq":text_seq.detach().numpy(), "ref_bert":ref_bert.detach().numpy(), "text_bert":text_bert.detach().numpy(), "ssl_content":ssl_content.detach().numpy()})
x = torch.from_numpy(x)
prompts = torch.from_numpy(prompts)
else:
x, prompts = self.onnx_encoder(ref_seq, text_seq, ref_bert, text_bert, ssl_content)
prefix_len = prompts.shape[1]
#[1,N,512] [1,N]
if debug:
y, k, v, y_emb, x_example = sess_fsdec.run(None, {"x":x.detach().numpy(), "prompts":prompts.detach().numpy()})
y = torch.from_numpy(y)
k = torch.from_numpy(k)
v = torch.from_numpy(v)
y_emb = torch.from_numpy(y_emb)
x_example = torch.from_numpy(x_example)
else:
y, k, v, y_emb, x_example = self.first_stage_decoder(x, prompts)
stop = False
for idx in range(1, 1500):
#[1, N] [N_layer, N, 1, 512] [N_layer, N, 1, 512] [1, N, 512] [1] [1, N, 512] [1, N]
if debug:
y, k, v, y_emb, logits, samples = sess_sdec.run(None, {"iy":y.detach().numpy(), "ik":k.detach().numpy(), "iv":v.detach().numpy(), "iy_emb":y_emb.detach().numpy(), "ix_example":x_example.detach().numpy()})
y = torch.from_numpy(y)
k = torch.from_numpy(k)
v = torch.from_numpy(v)
y_emb = torch.from_numpy(y_emb)
logits = torch.from_numpy(logits)
samples = torch.from_numpy(samples)
else:
enco = self.stage_decoder(y, k, v, y_emb, x_example)
y, k, v, y_emb, logits, samples = enco
if early_stop_num != -1 and (y.shape[1] - prefix_len) > early_stop_num:
stop = True
if torch.argmax(logits, dim=-1)[0] == self.t2s_model.EOS or samples[0, 0] == self.t2s_model.EOS:
stop = True
if stop:
break
y[0, -1] = 0
return y[:, -idx:].unsqueeze(0)
入力音声の特徴抽出は、cnhubert.pyではWav2Vec2FeatureExtractor -> Hubertを使用しているように見えるが、上層で、CNHubertのmodelのみを参照しているため、Wav2Vec2FeatureExtractorを使用していない。
hubertの入力:torch.Size([1, 51041]) hubertの出力:torch.Size([1, 768, 159])
エクスポートコードをコミット https://github.com/axinc-ai/GPT-SoVITS
all_jaだと、1文節をまとめてphonesに変換する。 jaだと、1文節のうちで、enとjaを切り替えながらphonesに変換する。
cnhubrtのモデルサイズ。 -rw-r--r--@ 1 kyakuno staff 377745020 3 18 18:47 nahida_cnhubert.onnx
language == "zh"の場合のみ、get_bert_featureを使用しており、そうでない場合はzerosを使用している。
samplingのtopKの設定はnahida_t2s_sdec.onnxに埋め込まれている。
onnx : top_k=5, top_p=1.0, repetition_penalty=1.35 (t2s_model_onnx.py) (repetition_penalty sampling) webui : top_k=5, top_p=1.0 (ts2_model.py) (top_p sampling)
onnxの出力が不安定な気がしていて、下記を変更してみたが影響がなかった。
def sample(
logits,
previous_tokens,
**sampling_kwargs,
):
if True:
top_k = 5
top_p = 1.0
temperature = 1.0
if temperature != 1.0:
logits = logits / temperature
logits = top_k_top_p_filtering(logits, top_k=top_k, top_p=top_p)
token = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1)
return token, logits
probs = logits_to_probs(
logits=logits, previous_tokens=previous_tokens, **sampling_kwargs
)
idx_next = multinomial_sample_one_no_sync(probs)
return idx_next, probs
webuiはhubertの入力の末尾に0.3秒の0をpaddingしているが、onnxはしていない。 ただ、そこまで差はない。
t2_model.pyのinfer_panelだとtopk_samplingではなくsampleを使っているので、実はonnxとロジックが一緒。
torchとonnxの出力不一致の要因を探っていく。onnxに変換する前の、onnx向けのtorchの段階で不一致があるので、torchの実装の差分を探る。
torch
------- 0
logits tensor([-6.2715, -5.9433, -1.8790, ..., -6.6660, -6.5220, -5.5481])
samples tensor([[280]], dtype=torch.int32)
------- 1
logits tensor([-5.9058, -5.1973, -2.5371, ..., -6.9879, -5.3853, 2.1045])
samples tensor([[875]], dtype=torch.int32)
------- 2
logits tensor([ -7.4869, -5.0362, -2.7423, ..., -10.3586, -7.4642, -3.2918])
samples tensor([[457]], dtype=torch.int32)
onnx
------- 0
logits tensor([-6.2075, -6.0403, -1.7061, ..., -6.3708, -5.6906, 2.0591],
samples tensor([[280]], dtype=torch.int32)
------- 1
logits tensor([-5.9783, -5.1318, -2.4765, ..., -6.9966, -5.6614, 1.4082],
samples tensor([[53]], dtype=torch.int32)
------- 2
logits tensor([ -9.3376, -5.3020, -3.0583, ..., -10.7543, -6.6286, -0.4839],
samples tensor([[29]], dtype=torch.int32)
topkとtoppを1にすると決定論的になるはず https://zenn.dev/hellorusk/articles/1c0bef15057b1d
topkとtoppを1にすると収束しない。
torchの方は初回推論時はEOSを無視するので、末尾の数値が一致しない。
入力音声をhubertで変換したssl_contentとprompt_semanticは完全一致している。
T2SModel (gptによるトークンと音声の潜在表現変換)のOnnxEncoderのbert_projまでは合っていて、ar_text_positionの出力が合わない。
SinePositionalEmbeddingのpe_tensorの値が合わない。
SinePositionalEmbeddingのpeはsin, cos, sin, cosであるべきで、torchだと
pe tensor([[[ 0.0000e+00, 1.0000e+00, 0.0000e+00, ..., 1.0000e+00, 0.0000e+00, 1.0000e+00],
だが、onnxだと
pe tensor([[[ 8.4147e-01, 5.4030e-01, 8.2186e-01, ..., 1.0000e+00, 1.0366e-04, 1.0000e+00],
になっており、sin, cos, sin, cosになっていなさそうな印象がある。
first stage decodeでtorchには存在する
logits = logits[:, :-1] ###刨除1024终止符号的概率
がonnxにはないので、topPとtopKの計算が変わり、結果の不一致が起きる。
torch
------- 0
logits.shape torch.Size([1024])
logits tensor([-6.2715, -5.9433, -1.8790, ..., -6.6660, -6.5220, -5.5481])
samples tensor([[280]], dtype=torch.int32)
------- 1
logits.shape torch.Size([1025])
logits tensor([-5.9058, -5.1973, -2.5371, ..., -6.9879, -5.3853, 2.1045])
samples tensor([[105]], dtype=torch.int32)
------- 2
logits.shape torch.Size([1025])
logits tensor([-7.3637, -4.6229, -2.0885, ..., -8.7286, -6.8107, 2.7965])
samples tensor([[29]], dtype=torch.int32)
onnx
------- 0
logits.shape torch.Size([1025])
logits tensor([-6.2715, -5.9433, -1.8790, ..., -6.5220, -5.5481, 2.5339],
grad_fn=<SelectBackward0>)
samples tensor([[486]], dtype=torch.int32)
------- 1
logits.shape torch.Size([1025])
logits tensor([-6.2932, -5.9584, -1.9278, ..., -6.5744, -5.5434, 2.6196],
grad_fn=<SelectBackward0>)
samples tensor([[280]], dtype=torch.int32)
------- 2
logits.shape torch.Size([1025])
logits tensor([-5.9299, -5.2080, -2.5925, ..., -7.0680, -5.4103, 2.1573],
grad_fn=<SelectBackward0>)
samples tensor([[105]], dtype=torch.int32)
これを入れると、トークン生成までは完全一致するようになる。
onnx
------- 0
logits.shape torch.Size([1024])
logits tensor([-6.2715, -5.9433, -1.8790, ..., -6.6660, -6.5220, -5.5481],
grad_fn=<SelectBackward0>)
samples tensor([[280]], dtype=torch.int32)
------- 1
logits.shape torch.Size([1025])
logits tensor([-5.9058, -5.1973, -2.5371, ..., -6.9879, -5.3853, 2.1045],
grad_fn=<SelectBackward0>)
samples tensor([[105]], dtype=torch.int32)
------- 2
logits.shape torch.Size([1025])
logits tensor([-7.3637, -4.6229, -2.0885, ..., -8.7286, -6.8107, 2.7965],
grad_fn=<SelectBackward0>)
samples tensor([[271]], dtype=torch.int32)
sampleのmultinomial_sample_one_no_syncにも差分がある。
torch
q = torch.empty_like(probs_sort).exponential_(1)
onnx
q = torch.random_like(probs_sort)
pred_semanticの末尾にonnxは0を入れているが、torchには入っていない。
vq_decodeにnoise_scaleが入っていない。
torch版とonnx版でほぼ同じ精度が出るようになった。最終的な差分は下記の4つ。 ・sampleのmultinomial_sample_one_no_sync ・SinePositionalEmbeddingのpe ・vq_decodeのnoise_scale ・first_stage_decodeのEOSの除去
あとはコード整理したらtorch依存を除去できそう。
最新のText2Speechモデル。VALLEXのように0ショットに対応している。 https://github.com/RVC-Boss/GPT-SoVITS mit 日本語対応