英文

歌词对齐

越南歌曲歌词对齐框架

任务描述(Zalo AI挑战2022)

我们中的许多人喜欢以专辑中我们喜欢的歌手的方式唱歌(卡拉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)}$

数据描述

Zalo公共数据集

  • 训练数据:来自约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教程。对齐过程如下:

  • 估计从音频波形中获取每帧标签的概率。
  • 生成表示时步处标签概率的trellis矩阵。
  • 从trellis矩阵中找到最可能的路径。
  • 只有在拥有良好的帧概率和正确的标签时,对齐才能有效进行。

    • 良好的帧概率可以通过稳健的声学模型实现。我们的设置的声学模型基于使用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如下:

    • Zalo公共数据集 - 测试集:0.2267
    • 爬取的公共数据集 - 测试集:0.1427

    对齐过程

    对齐过程已在方法论部分进行了描述。然而,为了实现我们在公共排行榜上的结果$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   
    

    然而,我们制定了一些启发式规则,以使输出更准确:

    • 一个单词的长度不超过3秒。
    • 如果单词长度小于1.4秒/140毫秒,我们将在该单词的开始和结束处添加20毫秒/40毫秒。我们这样做是因为数据是手工标注的,人类很容易在一个小的片段上犯错误。
    • 每个单词的所有时间戳向左移动120毫秒。这条规则在IoU结果上有显著的改善;有时,可以提高10%的绝对IoU值。我们也观察到数据中出现了这种行为,就像我们在卡拉OK时需要歌词提前一点出现一样。我们推测标记者在标记时也是这样考虑的。实际上,我们不建议使用这条规则。

    所有这些启发式规则都在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