模型:
nguyenvulebinh/lyric-alignment
越南歌曲歌词对齐框架
我们中的许多人喜欢以专辑中我们喜欢的歌手的方式唱歌(卡拉OK风格)。目标是建立一个模型,将歌词与音乐音频对齐。
输入:一个音乐片段(包括人声)及其歌词。
输出:歌词中每个单词的开始时间和结束时间。
为了评估,将使用交并比(IoU)来评估预测的准确性。使用IoU指标,值越高越好。例如:
音频片段$S_i$的预测与真实值的IoU计算公式如下:
$IoU(S_i) = \frac{1}{m} \sum_{j=1}^{m}{\frac{G_j\cap P_j}{G_j\cup P_j}}$
其中$m$是$S_i$的标记数。然后,所有$n$个音频片段的最终IoU是它们对应IoU的平均值:
$Final_IoU = \frac{1}{n} \sum_{i=1}^{n}{IoU(S_i)}$
训练数据:来自约480首歌曲的1057个音乐片段。每个片段都有一个以WAV格式提供的音频文件和一个包含每个单词的歌词和对应时间段(以毫秒为单位)的基准JSON文件。
测试数据:
公开测试:约120首歌曲的264个音乐片段。
私有测试:约200首歌曲的464个音乐片段。
数据示例:
由于Zalo提供的数据集规模较小且存在噪音,我们决定从其他公共来源爬取数据。幸运的是,我们的策略(在方法论部分详细介绍)不需要每个单词都对齐其时间段,只需要将歌曲及其歌词提供给网络进行训练,就像典型的ASR数据集一样。
我们在data_preparation文件夹中详细描述了数据的爬取和处理过程。我们共爬取了来自 https://zingmp3.vn 网站的30000首歌曲,大约1500小时的音频。
我们的策略主要基于Ludwig Kürzinger的 CTC-Segmentation 研究和 Forced Alignment with Wav2Vec2 的Pytorch教程。引用自Ludwig Kürzinger的研究:
CTC分割,一种在音频录制的开头或结尾存在其他未知语音段的情况下提取正确音频文本对齐的算法。它使用基于CTC的端到端网络,该网络在训练之前已经使用已对齐数据进行了训练,例如通过CTC/注意力ASR系统提供的数据。
基于 Forced Alignment with Wav2Vec2 的Pytorch教程。对齐过程如下:
只有在拥有良好的帧概率和正确的标签时,对齐才能有效进行。
良好的帧概率可以通过稳健的声学模型实现。我们的设置的声学模型基于使用CTC损失训练的wav2vec2架构。
正确的标签指的是口语形式的标签。由于歌词来自不同的来源,可能包含特殊字符、混合的英文和越南词汇、数字格式(日期、时间、货币等)、昵称等。这种数据会使模型很难将音频信号与文本歌词进行映射。我们的解决方案是将歌词的每个单词从书面形式映射到口语形式。例如:
Written | Spoken |
---|---|
joker | giốc cơ |
running | răn ninh |
0h | không giờ |
要将英文单词转换为越南语的发音方式,我们使用 nguyenvulebinh/spelling-oov 模型。对于数字格式的处理,我们使用 Vinorm 。对于其他特殊字符“.,?...”,我们会将其删除。
一个书面单词(例如0h)的最终时间对齐是其口语单词(例如“没有时间”)的连接时间对齐。
我们的最终模型基于 nguyenvulebinh/wav2vec2-large-vi-vlsp2020 模型。它在13000小时的越南YouTube音频(未标记数据)上进行了预训练,并在16kHz采样的语音音频上的250小时的VLSP ASR数据集中进行了精调。我们使用该检查点使用1500小时的数据(在前面的步骤中准备)训练了一个新的ASR模型。要从头开始生成我们的模型,请运行以下命令:
CUDA_VISIBLE_DEVICES=0,1,2,3,4 python -m torch.distributed.launch --nproc_per_node 5 train.py
train.py脚本将自动从huggingface nguyenvulebinh/song_dataset 下载数据集和预训练模型 nguyenvulebinh/wav2vec2-large-vi-vlsp2020 ,然后进行训练过程。
在我们的实验中,我们使用了5个RTX A6000(约250GB)的GPU,批量大小为160,相当于每个步骤40分钟。我们训练了约50个周期,共计78小时。下面的图表显示了我们训练过程的前35000个步骤的日志。最终的训练损失约为0.27。
训练后我们模型的性能WER如下:
对齐过程已在方法论部分进行了描述。然而,为了实现我们在公共排行榜上的结果$IoU=0.632$,我们需要进行一些额外的步骤。详细信息如下:
输入将被格式化并转换为口语形式。例如,原始输入:
['Endgame', 'chiến', 'thắng', 'Chỉ', 'lần', 'duy', 'nhất', 'Bởi', 'IronMan', 'và', 'số', '3000']
口语形式的输出将是:
['en gêm', 'chiến', 'thắng', 'chỉ', 'lần', 'duy', 'nhất', 'bởi', 'ai ron men', 'và', 'số', 'ba nghìn']
使用CTC分割算法强制对齐口语形式的文本和音频。在方法论部分的详细步骤(3个步骤)。
output word_segments: en: 0 -> 140 gêm: 200 -> 280 chiến: 340 -> 440 thắng: 521 -> 641 chỉ: 761 -> 861 lần: 961 -> 1042 duy: 1182 -> 1262 nhất: 1402 -> 1483 bởi: 1643 -> 1703 ai: 1803 -> 1863 ron: 2064 -> 2144 men: 2284 -> 2344 và: 2505 -> 2565 số: 2705 -> 2765 ba: 2946 -> 2986 nghìn: 3166 -> 3266
根据Zalo提供的标记数据的行为,我们观察到连续单词的时间框架是连续的。因此,根据前一步的word_segments,我们按如下方式对齐每个单词的时间框架:
for i in range(len(word_segments) - 1): word = word_segments[i] next_word = word_segments[i + 1] word.end = next_word.start
然而,我们制定了一些启发式规则,以使输出更准确:
所有这些启发式规则都在utils.py文件的add_pad函数中实现。
output after appying rules: en: 0 -> 100 gêm: 60 -> 240 chiến: 200 -> 420 thắng: 380 -> 661 chỉ: 621 -> 861 lần: 821 -> 1082 duy: 1042 -> 1302 nhất: 1262 -> 1543 bởi: 1503 -> 1703 ai: 1663 -> 1964 rừn: 1923 -> 2184 mừn: 2144 -> 2404 và: 2364 -> 2605 số: 2565 -> 2845 ba: 2805 -> 3066 nghìn: 3046 -> 7274
将口语形式重新对齐到原始单词。
Endgame ['en: 0 -> 100', 'gêm: 60 -> 240'] chiến ['chiến: 200 -> 420'] thắng ['thắng: 380 -> 661'] Chỉ ['chỉ: 621 -> 861'] lần ['lần: 821 -> 1082'] duy ['duy: 1042 -> 1302'] nhất ['nhất: 1262 -> 1543'] Bởi ['bởi: 1503 -> 1703'] IronMan ['ai: 1663 -> 1964', 'ron: 1923 -> 2184', 'men: 2144 -> 2404'] và ['và: 2364 -> 2605'] số ['số: 2565 -> 2845'] 3000 ['ba: 2805 -> 3066', 'nghìn: 3046 -> 7274']
当与波形一起绘制时,最终的对齐结果如下:
我们的声学模型已经上传到了huggingface hub。以下代码可用于加载此模型:
from transformers import AutoTokenizer, AutoFeatureExtractor from model_handling import Wav2Vec2ForCTC model_path = 'nguyenvulebinh/lyric-alignment' model = Wav2Vec2ForCTC.from_pretrained(model_path).eval() tokenizer = AutoTokenizer.from_pretrained(model_path) feature_extractor = AutoFeatureExtractor.from_pretrained(model_path) vocab = [tokenizer.convert_ids_to_tokens(i) for i in range(len(tokenizer.get_vocab()))]
用于进行歌词对齐的代码在predict.py文件中。确保你的音频文件是16kHz且只有一个通道。你可以使用preprocessing.py文件来转换音频文件以确保符合要求。
from predict import handle_sample import torchaudio import json # wav_path: path to audio file. Need to be 16k and single channel. # path_lyric: path to lyric data in json format, which includes list of segment and words wav, _ = torchaudio.load(wav_path) with open(path_lyric, 'r', encoding='utf-8') as file: lyric_data = json.load(file) lyric_alignment = handle_sample(wav, lyric_data)
例如,一个测试集中的输入文件38303730395f313239.json:
[{"s": 0, "e": 0, "l": [{"s": 0, "e": 0, "d": "Endgame"}, {"s": 0, "e": 0, "d": "chiến"}, {"s": 0, "e": 0, "d": "thắng"}]}, {"s": 0, "e": 0, "l": [{"s": 0, "e": 0, "d": "Chỉ"}, {"s": 0, "e": 0, "d": "lần"}, {"s": 0, "e": 0, "d": "duy"}, {"s": 0, "e": 0, "d": "nhất"}]}, {"s": 0, "e": 0, "l": [{"s": 0, "e": 0, "d": "Bởi"}, {"s": 0, "e": 0, "d": "IronMan"}, {"s": 0, "e": 0, "d": "và"}, {"s": 0, "e": 0, "d": "số"}, {"s": 0, "e": 0, "d": "3000"}]}]
歌词对齐的输出结果将是:
[{ "s": 0, "e": 661, "l": [ { "s": 0, "e": 240, "d": "Endgame" }, { "s": 200, "e": 420, "d": "chiến" }, { "s": 380, "e": 661, "d": "thắng" } ] }, { "s": 621, "e": 1543, "l": [ { "s": 621, "e": 861, "d": "Chỉ" }, { "s": 821, "e": 1082, "d": "lần" }, { "s": 1042, "e": 1302, "d": "duy" }, { "s": 1262, "e": 1543, "d": "nhất" } ] }, { "s": 1503, "e": 7274, "l": [ { "s": 1503, "e": 1703, "d": "Bởi" }, { "s": 1663, "e": 2404, "d": "IronMan" }, { "s": 2364, "e": 2605, "d": "và" }, { "s": 2565, "e": 2845, "d": "số" }, { "s": 2805, "e": 7274, "d": "3000" }]}]
我们要感谢Zalo AI挑战2022的主办方提供这个激动人心的挑战。
nguyenvulebinh@gmail.com