多模态语言模型实战之音乐转录 译文 精选

愤怒的蜗牛

本文将以实战方式探讨基于Spotify公司的开源音乐大模型Llark并联合阿里巴巴的语音多模态大模型Qwen2-AudioQwen2-Audio将音乐转录成乐谱的完整过程。

多模态语言模型实战之音乐转录 译文 精选

自动音乐转录是将MP3和WAV等音频文件转换为乐谱、吉他指法谱以及音乐家可能想要用乐器学习歌曲的任何格式的过程。

本文中,我们将介绍目前用于执行上述操作的最佳工具,这些工具恰好是基于深度学习的,并采用了一种新颖的方法。

当前最先进的技术

这项任务的当前最先进的技术来自Magenta,这是一个开源研究项目,由现已解散(截至2023年4月)的Google Brain团队开发。

这个开发团队在2021年发表了一篇论文《使用转换器进行序列到序列钢琴转录》,该论文使用了受T5启发的转换器模型(类似于“t5-small”),该模型具有5400万个参数和Maestro数据集,取得了很好的效果。使用编码器-解码器转换器架构将问题作为序列到序列任务来解决。编码器将梅尔频谱图帧作为输入处理并生成嵌入,而解码器通过交叉注意力机制使用这些嵌入以自回归方式生成一系列类似MIDI的标记。这个开发团队使用的词汇表由四种类型的标记组成:

  • 音符标记(MIDI音高的128个值)

  • 速度标记(128个值,包括零表示音符关闭)

  • 时间标记(用于绝对计时的10毫秒bin格式文件中有6000个值)

  • EOS标记(用于标记序列结束)

请参见下图,了解这种架构的可视化形式以及其自定义MIDI标记的示例序列:

多模态语言模型实战之音乐转录 译文 精选

图1.来自使用转换器进行序列到序列钢琴转录的论文

说明:我们的模型是一个通用的编码器-解码器转换器架构,其中每个输入位置包含一个频谱图帧,每个输出位置包含来自我们类似MIDI的词汇表的事件。输出标记是从解码器自回归采样的,每一步都以最大概率获取标记。

2022年,开发团队发表了一篇论文《MT3:多任务多轨音乐转录》。该实验使用了与上一个实验相同的方法,但添加了额外的乐器标记来表示不同的乐器。同样,他们使用了类似的T5模型,并在许多训练过的数据集上取得了出色的表现,尤其是Slakh、Maestro和MusicNet数据集。

MR-MT3于次年发布,作为MT3的轻微改进版本出现。

为什么要使用语言模型而不是继续使用这些SOTA模型?

计算/GPU资源

尽管与最小的语言模型相比,其规模要小得多,但从头开始训练此模型仍然需要大量资源。2021年的论文指出:

“我们在32个TPUv3核心上训练了所有模型,每个核心的训练批次大小为8。根据验证集结果,过度拟合似乎不是问题,因此我们允许训练进行400K步,这对于我们的基线模型来说大约需要2.5天。”

此MT3论文没有提供关于训练的具体细节,只是说他们训练了100万步。

其他限制

这些模型在输出灵活性方面有一些固有的限制。虽然语言模型通常具有大量词汇(通常超过30,000个标记),并且已在各种自然语言数据上进行了广泛的预训练,但MT3和类似的音乐转录模型使用小得多的专门标记词汇(只有几千个标记),仅专注于音乐事件。这种专业化意味着添加新标记(例如新乐器或演奏技巧,如吉他上的手掌静音或小提琴上的拨弦)可能并不容易——需要进行大量的再训练才能将这些新标记有效地与现有词汇结合起来,并且通常需要大量的训练数据来展示这些技巧。这与大型语言模型不同,大型语言模型通常可以在不进行修改的情况下用自然语言描述这些音乐细微差别,因为它们在广泛的预训练中遇到了这些概念。

迁移学习和零样本

我们可以利用大型开源预训练音频和语言模型的迁移学习。音乐生成模型的示例包括OpenAI的Jukebox和Meta的MusicGen

现代多模态模型架构

GPT-4o旨在以“原生”方式处理文本、音频和图像。尽管OpenAI尚未公布这方面的技术细节,但假设网络中的某些权重数据集能够处理所有各种模态。该模型可能使用仅解码器架构(如仅语言的GPT模型),而无需编码器组件先将不同模态转换为密集表示。这种设计允许模型无缝地处理和解释文本和图像等输入,从而可能在计算和模型理解方面提供性能优势。

许多多模态模型采用一种更简单的方法,让人联想到编码器-解码器架构:它们结合了两个预训练模型——一个用于特定输入模态的编码器(如用于视觉的ViT或用于声音的音频编码器)和一个大型语言模型(如LLaMA、Gemma或Qwen)。这些模型通过投影层连接起来,投影层将它们的表示对齐到共享的潜在空间中,通常只使用一个线性层。这些投影层学习将编码器的输出转换为与LLM的预期输入维度和特征相匹配的格式。投影从输入模态创建新的嵌入/标记,然后可以将其注入到LLM的输入序列中。LLaVA是这种架构在视觉语言任务中的典型示例,而Spotify的LlarkQwen-Audio则使用音频编码器而不是视觉编码器应用了相同的原理。

以下是一些关于如何将模型拼接在一起的伪代码:

#从音频编码器的最后一层中提取特征 #形状: [batch_size, audio_seq_len, encoder_dim=1024] audio_features = audio_model(audio_input) #投影音频特征,以匹配LLM的嵌入维度 # 形状: [batch_size, audio_seq_len, llm_embed_dim=4096] audio_embeddings = projection_layer(audio_features) #从LLM的嵌入层中获取文本嵌入 # 形状: [batch_size, text_seq_len, llm_embed_dim=4096] text_embeddings = llm.embed_text(text_input) #沿着序列长度方向进行连接 # 形状: [batch_size, audio_seq_len + text_seq_len, llm_embed_dim=4096] combined_input = concatenate([audio_embeddings, text_embeddings], dim=1) # 将它们正常送入LLM进行生成 output = llm(combined_input)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.

Spotify的Llark和Qwen2-Audio

架构概述

Llark使用OpenAI的Jukebox,Qwen2-Audio使用OpenAI的Whisper作为音频塔。Jukebox是一种音乐生成模型,但它也可以接收音频片段作为输入并输出音频片段的延续。Whisper用于将语音转录为文本。

考虑到它们的用途,音频模块的选择很明确:Llark专注于音乐分析,而Qwen2Audio主要专注于通过一些基本的音频和音乐分析功能响应语音指令。

确定从大型预训练模型中提取嵌入的最佳来源需要研究和实验。此外,决定是否微调整个模块或冻结部分模块是一个至关重要的设计选择。例如,LlaVa的训练策略包括冻结视觉塔并专注于微调投影层和语言模型。我们将在接下来介绍每个模型的这一特征。

Llark:为什么是Jukebox?截至2024年9月,这些嵌入是否是最好的?

确定从大型模型中提取嵌入的最佳位置通常需要进行大量探索。这涉及通过反复试验的过程在不同的分类任务上测试模型的各种激活或提取层。对于音乐生成模型,这可能包括流派识别、乐器检测、情绪检测以及和声结构和时间模式分析等任务。许多商业嵌入模型(如OpenAI的嵌入模型)都是专门为嵌入生成而训练的,具有专门的架构和训练目标,而不是现有语言模型的微调版本。

两个最大的公开可用的音乐生成和音乐延续(即:能够将音频作为输入)模型是Jukebox和MusicGen。其中,MusicGen模型的更新更快,对我来说似乎是显而易见的选择。然而,根据关于探索MusicGen的论文,从Jukebox中提取的嵌入似乎在分类任务中平均表现优于MusicGen。这篇论文的研究结果促使Llark的作者使用以下方法提取嵌入:

嵌入来自Jukebox编码器第36层的输出,遵循Castellon等人(2021)中描述的方法。

原始Jukebox编码:

  • 345Hz的4800维向量。

  • 对于25秒的剪辑:超过4.14*10⁷个浮点值。

作者使用下采样方法:在100ms帧内进行均值池化,结果:

  • 下采样频率:10Hz。

  • 嵌入大小:25秒音频剪辑的1.2×10⁶。这意味着使用一个形状为[240,4800]的二维数组。

  • 保留时间信息(与Castellon等人在时间维度上取平均值的方案有所不同)。

(下采样嵌入大小大约是许多多模态视觉模型中使用的CLIPViT-L14模型的6倍)

Qwen2Audio:Whisper

本文未详细提及Qwen2Audio的嵌入提取。Whisper是一种编码器-解码器架构;其中,编码器负责生成音频的深度学习表示,解码器则负责将表示解码为文本(转录)。在Qwen2Audio中,他们似乎从Whisper编码器的最后一层提取嵌入,尽管他们没有提到他们是否在训练期间冻结它。

预训练权重、训练数据和数据集

不幸的是,Spotify尚未向公众提供任何数据集或其训练过的模型权重,并指出:

“关于输入:我们模型的输入是公开的、开源的、知识共享许可的音频和相关注释。但是,每个音频文件都有自己的、可能更严格的许可证。许多音频文件都包含“禁止衍生”许可证。我们鼓励数据集的用户熟悉这些许可证的限制;为了遵守这些许可证,我们不会发布本文中训练数据的任何衍生品(包括查询-响应对或训练模型权重)。”

训练过程中,他们使用了以下数据集:

  • MusicCaps(Agostinelli等人,2023年)

  • YouTube8M-MusicTextClips(McKee等人,2023年)

  • MusicNet(Thickstun等人,2017年)

  • FMA(Defferrard等人,2017年)

  • MTG-Jamendo(Bogdanov等人,2019年)

  • MagnaTagATune(Law等人,2009年)

Llark在以下摘录中详细介绍了其训练数据生成过程:

“我们使用ChatGPT的变体来提取所有实验的指令调整数据。但是,所使用的确切语言模型因数据集而异。我们选择OpenAI模型如下:我们对所有推理任务都使用GPT-4。我们发现GPT-4更擅长遵循推理任务系列中的复杂指令。对于样本超过25k的数据集,我们将推理数据限制为25k个轨道的随机子样本。”

这会产生如下问答数据:

多模态语言模型实战之音乐转录 译文 精选

LLark的示例文本输入和输出,用于提供的音频

用于训练Qwen2Audio的数据集也没有共享,但经过训练的模型广泛可用,并且也在转换器库中实现:

对于这个项目,微调预先训练的Llark模型将是最佳选择,因为它在Spotify论文中所述的评估基准方面表现良好。

但是,鉴于他们没有发布它的权重,如果没有相当多的专业知识和资金,从头开始训练这样的模型是不可行的。Spotify对其进行了训练:

我们的模型在4个80GB NVIDIA A100 GPU上进行训练。训练大约需要54小时。

如果使用LambdaLabs等提供商提供的云服务的话,这将花费大约700美元。

由于上述原因,我选择了Qwen。然而,Qwen2-Audio在节奏和乐器检测等基本音乐任务中表现不佳。我将在下面的评估部分详细说明这一点。这意味着,该模型可能不够大或预训练不足,无法完成这项任务,但我希望至少可以为将来微调这项任务设定一个起点和框架。正如阿里巴巴在其Qwen2-Audio博客文章中所述:

“我们还计划构建更大的Qwen2-Audio模型,以探索音频语言模型的缩放规律”。

不过,为了我自己的学习,我确实尝试使用torch和带有转换器库的预训练模型重新创建模型。

我还为问答数据和嵌入创建了数据集。我为URMP数据集生成了简短形式的问答数据,例如:“这首曲子的节奏是多少”、“这段音频中演奏的乐器是什么”。

在Colab环境中运行Jukebox的笔记本源文件地址是https://colab.research.google.com/drive/1jdR5w-XlJQFog47ZJ36ckEVMW0F5qIpl,使用的是廉价的T4GPU。我在链接https://huggingface.co/jonflynn处将问答和嵌入数据集上传到HuggingFace。

复制有Llark的笔记本源文件的地址是:https://colab.research.google.com/drive/1_V5B9ZrwrKtom-N4r-Om3mqlXKPacUBh#scrollTo=72Gv5raTIPqi

训练音乐转录数据

转录格式

我选择ABC音乐符号作为语言模型转录音乐的输出格式。以下是一个例子:

X:1 M:4/4 L:1/16 K:none Q:67 V:1 name="Electric Bass (finger)" %%octave-default C4 GAA^2E3A2<A^2 | D^D^2E2A2A^4 A^2E2 | A2A^4A^2E2 A2A^4 | A^2E2A2A^4A^2E2A2 | A^4 A^2E2 A2A^4A^2 E2 | A2A^4 | V:2 name="Bright Acoustic Piano" %%octave-default C5 [E3C3][E3C3][E3C3] [E3C3][A^,2E2A^2] | [E3A^3][E3A^3][E3A^3][E3A^3][E3A^3] | [E3A^3][E3A^3][E3A^3] [E3A^3][E3A^3] | [E3A^3][E3A^3][E3A^3][E3A^3][E3A^3] | [E3A^3][E3A^3][E3A^3] [E3A^3][E3A^3] | [E3A^3] | V:3 name="Electric Guitar (jazz)" %%octave-default C5 E'3C'3A^4E'3C'3 | A^4E'3 C'3A^4E'3C'3 | A^4 E'3C'3A^4 E'3C'3 | A^4E'3C'3A^4E'3C'3 | A^4E'3C'3 A^4E'3C'3 | A^4 |1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

在上面的符号中,我们在顶部定义了拍号和节奏,用“M”和“Q”表示。“L”表示符号的默认音符长度,在本例中是十六分音符,这是常态。然后,我们定义每个乐器,以及在为每个乐器写音符时它们应该遵循的默认八度。以下是ABC音乐符号中写音符的关键句法要点的总结:

  • 音符用字母A-G表示,小写字母表示高八度

  • 升号用音符前的^表示,降号用_表示

  • 还原符号用=表示

  • 音符长度用音符后的数字表示(C2是C的两倍长)

  • 音符后附点音符使用a.(C.是附点四分音符)

  • 休止符用z表示,数字表示持续时间(z2是半休止符)

  • 和弦用方括号[CEG]括起来

  • 连音符用连字符-表示

  • 小节线用|表示

  • 断奏节奏在音符之间使用>或<(C>D表示附点C八分音符后跟D十六分音符)

为什么使用ABC音乐符号?

选择这种符号的原因是:

  • 这是一种极简主义的音乐创作格式。

  • 它被广泛使用且很受欢迎;语言模型已经对ABC符号进行了广泛的预训练,因此已经很好地理解了它。

  • 它很灵活,可以轻松扩展以包括节奏变化、拍号变化、如上所述的其他演奏风格等……

我使用库https://github.com/sshlien/abcmidi将数据集提供的MIDI文件转换为ABC符号。创建数据集的笔记本文件位于链接https://colab.research.google.com/drive/1CdQ_PUjhCvCR2VjGt3ya1hNowPrr0Xun处。

评估

为了评估原始模型以及此后执行的每个微调阶段,我从URMP数据集中随机选择了30个复杂程度不同的样本,并在每个样本上运行模型三次,手动检查所有响应。

通过手动测试,我发现最佳解码参数是温度为0.7,top_p为1.2。返回的最大标记数上限为2048。调整最大值似乎对性能影响不大。

原始模型在此评估集上表现不佳。虽然它偶尔能正确预测节奏和乐器,但大多数时候都做不到。评估结果的文本文件可在此处获取。

鉴于这个起点,如果没有强大的预训练模型,我们不太可能从这个实验中看到强劲的结果。然而,目标是制定可以在未来随着更先进的预训练模型的出现而应用的策略。

微调策略

我首先尝试使用基本的交叉熵损失进行微调。使用交叉熵损失进行监督微调是一种快速开始教授模型的方法,但像这样的基本损失函数有局限性,我们将在下面看到。这个训练阶段背后的直觉是,它会将模型推向正确的方向,并会拾取数据集中可能存在的任何模式或任何自定义的ABC符号,而模型可能以前从未见过。

使用教师强制型交叉熵损失

首先,我们以典型的监督微调方式对语言模型进行训练。我为此使用了trl库中的SFTtrainer,它使用交叉熵损失和教师强制机制,具体定义如下:

  • 模型预测序列中的下一个标记。

  • 损失是根据预测概率(logits)和实际下一个标记之间的差异计算的。

  • 对于下一个预测,模型被赋予实际正确的标记(真实数据),而不是它自己的预测。这被称为“教师强制”,它有助于稳定训练并显著加快训练速度,尤其是在早期阶段。

此训练阶段的结果很差,它降低了原始模型的性能。该模型以前可以很好地处理节奏和乐器识别,但现在大部分都出错了。它还开始产生无休止重复的乱码文本输出。即使在设置低学习率、应用梯度裁剪和使用低LoRA等级来减轻对模型的重大更改时,也会发生这种情况。总体而言,该模型似乎对所应用的训练非常敏感。

然而,虽然这个训练阶段可能会带来一些改进,但由于我们的基本损失函数的限制,它不会带来最佳性能。该函数很难完全捕捉模型的性能细微差别。例如,当使用教师强制时,乐器预测可能会在某些标记部分产生看似较低的损失。如果乐器名称以“V”开头,无论准确度如何,模型都可以根据我们的数据集自信地预测“小提琴”或“中提琴”。此外,损失函数可能无法准确反映近乎失败的情况,例如预测节奏为195而不是200——这是一个相当准确的小差异,但可能会受到惩罚,这在很大程度上取决于logit之间的概率分布。相邻数字也可能具有较高的概率。

结合近端策略优化(PPO)的RLHF模型

由于这些限制,我们可以创建自己的自定义损失函数,可以更准确地对模型的响应进行评分。也就是说,给定模型的预测序列,损失函数可以给出0到1之间的分数来表示其好坏。

然而,将这个自定义损失函数集成到监督微调中是一项重大挑战。问题源于自定义损失函数引入的非线性,这阻碍了梯度的直接计算。让我们来分析一下:

在具有交叉熵损失的传统SFT中:

  • 模型输出词汇表中每个标记的logit(原始分数)。

  • 这些logit直接表示模型的预测概率。

  • 损失函数将这些概率与基本事实进行比较。

  • 可以通过此比较直接计算梯度。

  • 微积分的链式法则允许我们将这些梯度传播回模型。

使用我们的自定义损失函数:

  • 模型必须首先生成完整的文本输出。

  • 此生成过程涉及从概率分布中进行采样。

  • 然后,我们的损失函数分析此文本输出(检查节奏、音符等)。

  • 这在模型的logit和我们的损失计算之间创建了一个不可微分的步骤。

  • 采样和文本分析步骤打破了反向传播所需的梯度链。

为了克服这个问题,可以采用强化学习技术,例如近端策略优化(PPO)。PPO专门用于处理不可微分损失函数,可以通过考虑整个策略(模型的输出分布)来优化模型,而不是依赖来自logits的梯度信息。

PPO的关键思想是,它不是试图直接通过不可微分步骤进行反向传播,而是:

  • 将模型的输出视为强化学习框架中的动作。

  • 使用自定义损失函数作为奖励信号。

  • 更新模型的策略(其在标记上的概率分布)以最大化预期奖励。

  • 同时确保更新后的策略不会偏离当前策略太远。

这种方法使我们能够使用自定义损失函数有效地训练模型,确保性能改进而不会破坏核心训练动态。PPO算法的保守更新策略有助于在训练期间保持稳定性,这在使用大型语言模型时尤为重要。

此评分函数将以“奖励模型”的形式作为单独的LLM实现,通常用于通过RLHF微调模型,这是ChatGPT问世时首次引入的一项突破。由于此任务的性质,我们可以手动编写代码来对响应进行评分,这样使用的资源更少,速度更快。

对于拍号和节奏识别,这很容易计算。我们使用正则表达式提取所有预测项,例如提取节拍:

def extract_metre(self, abc_string): return re.search(r'M:(\S+)', abc_string).group(1)1.2.

模型应该学习我们希望它在SFT阶段输出的语法和结构。如果它输出的内容会导致我们的正则表达式找不到任何内容或出现错误,我们可以跳过该样本,假设它只是数据集中的一小部分。

我们提取预测的节奏并编写一个函数,该函数对小错误更宽容,但对大错误惩罚更严厉:

  • 对于小差异(≤10BPM),它使用线性缩放。

  • 对于较大的差异,它会切换到指数缩放。

  • 最终损失上限在0和1之间。

让我们分解一下这个自定义损失的关键组成部分:

自定义损失的代码在链接https://colab.research.google.com/drive/1lpPfn9EFE2rBsasIJNv8Cy9qTvtfXzq-#scrollTo=mOs_gWcjrBgv处提供,您可以自行参考。

1. 节拍损失

节拍损失关注作品的拍号。它将预测的节拍与基本事实进行比较,分别考虑分子和分母以及它们的比率。这种方法允许进行细致入微的评估,可以准确处理各种拍号。

节拍损失使用线性和指数缩放的组合来惩罚差异。小差异会导致损失线性增加,而较大的差异会导致指数增加,最大值为1。

2. 节奏损失

节奏损失评估预测的每分钟节拍数(BPM)的准确性。与节拍损失类似,它使用线性和指数缩放的组合。

对于小节奏差异(≤10BPM),该函数应用线性缩放。较大的差异会触发指数缩放,确保显著的节奏不匹配受到更严重的惩罚。

3. 音高损失

音高损失可能是最重要的组成部分,因为它评估转录音符的准确性。此函数使用Levenshtein距离来比较每个声音中的音符序列。

音高损失计算考虑了多个声音,将每个预测声音与最接近的真实声音进行匹配。这种方法允许灵活地对声音进行排序,同时仍保持整体音调内容的准确性。

4. 乐器损失

乐器损失评估每个声音的乐器选择的准确性。

此函数考虑精确匹配、来自同一家族的乐器,并使用字符串相似性进行更细致的比较。它全面评估了模型识别和为每个声音分配乐器的能力。

5. 合并损失

最终损失是这些单个组件的加权组合:

total_loss = (0.5 * pitch_loss + 0.15 * metre_loss + 0.15 * tempo_loss + 0.2 * instrument_loss)1.2.3.4.

这种加权方案优先考虑音高准确性,同时仍考虑音乐转录的其他重要方面。

训练和超参数

PPO训练通常需要比SFT多得多的内存,原因如下:

  • 多个策略评估:PPO需要维护当前策略(模型权重)和“旧”策略来计算它们之间的概率比。这实际上使内存中的模型参数翻倍。

  • 经验缓冲区:PO存储经验缓冲区(状态、动作、奖励等),以在小批量中执行更新。这个缓冲区可能非常大,占用大量内存。

  • 优势估计:计算优势需要跟踪轨迹中的值估计和回报,这又增加了一层内存开销。

  • 额外的优化目标:PPO跟踪多个损失成分(策略损失、值损失、熵奖励)及其梯度,而SFT只有一个损失。

由于上述原因,我们在可以训练的模型大小和成本方面比SFT更受限制。虽然我可以在Colab中的A100 40GB上进行上述训练,但对于PPO训练,我需要更多内存。我在H100 80GB上进行训练,它可以训练等级为128且批处理大小为8的LoRA。

我的超参数范围很窄,我选择了最直观的方法,使用批处理大小从1到16,学习率从2e-5到2e-4。

该模型对任务没有改进。包含结果的文本文件在链接http://asdf/处提供。

我使用权重和偏差(WandB)跟踪了各种训练指标。关键指标包括策略损失、价值损失、总损失、KL散度和奖励模型的分数。

对于所有超参数运行,记录的奖励和损失随时间推移的计算没有改善。KL散度保持在预定义的阈值内。

多模态语言模型实战之音乐转录 译文 精选

总结

虽然本文中的初步实验在音乐转录方面没有达到预期的效果,但我们为该领域的未来发展奠定了一些基础。所遇到的挑战为解决这一复杂任务的技术要求和潜在方法提供了宝贵的见解。未来的工作可以探索以下几个有希望的方向:

  • 在更大的预训练模型可用时进行实验

  • 使用更多样化的音乐示例扩展训练数据集

  • 进一步完善奖励函数以捕捉更细微的音乐关系

  • 探索将传统音乐处理技术与语言模型功能相结合的混合方法

我使用Qwen2-Audio运行本文中这些实验的笔记本文件位于下面链接处:https://colab.research.google.com/drive/1lpPfn9EFE2rBsasIJNv8Cy9qTvtfXzq-


您需要 登录账户 后才能发表评论

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
评论列表 (暂无评论,2人围观)

还没有评论,来说两句吧...

目录[+]

取消
微信二维码
微信二维码
支付宝二维码