10  第十章 RAVE

RAVE是由法国声学与音乐研究所(IRCAM)在2021年底提出来的一个用于实时音频生成的模型。它的全称是实时自回归变分自编码器(Real-time Autoregressive Variational Autoencoder),它的出现第一次让人们体验到了高质量音频实时生成的可能性。

10.1 VAE部分

RAVE顾名思义里面包含有VAE部分,这部分负责将音频波形压缩成潜在空间的表示。RAVE音频码率为48kHz,为了实时性,输入端进行了PQMF编码。

10.1.1 PQMF

假设输入音频长度为\(L\),RAVE通过PQMF把长度为\(L\)的音频切分成16个子带,每个子带包含的是原始信号不同频段的信息,每个子带的长度等于\(\frac{L}{16}\)

图 10.1: RAVE输入端PQMF分解

其中16可以理解为通道数,PQMF分解后,每个子带的长度变为原来的\(\frac{1}{16}\),这样可以加速后续VAE卷积计算的速度。

10.1.2 编码器

RAVE的编码器部分如图所示:

图 10.2: 编码器

在PQMF之后,首先进行一维卷积,考虑输入通道数是16,因此单个卷积核的厚度也是16。在RAVE官方实现里,第一层卷积核的大小是7,步长为1,输出通道是64(因此卷积核的个数也是64个),如图所示:

图 10.3: 编码器首个卷积层

因为每个通道数据首尾有零填充,因此首次卷积后所有通道的长度保持不变,只是通道数从16变成了64,相当于增加了更多的细节,让模型有更强的表现力。

然后卷积后的结果通过一个批量归一化层,它只是对数据进行归一化处理,让模型数值更稳定,再然后通过一个激活函数提供非线性。

首次卷积、归一化、非线性后,数据再进入到 图 10.2 所示的\(N\)个卷积块中,RAVE代码和论文里的\(N=4\),卷积的输出通道分别是\([64,128,256,512]\),卷积的步长分别是\([4,4,4,2]\)

其中需要特别关注的是卷积步长,跨步会使每个通道的输出长度减小,它等效于一个降采样的过程,但经过一层一层的跨步卷积后,最后的输出会有很大的感受野,使模型能捕捉到更长的音频上下文关系。

对于每个卷积块,单个卷积核的厚度是输入通道数,卷积核的个数是输出通道数,卷积的首尾填充等于步长,单个卷积核的大小是\(2 \times \text{步长} + 1\)

图 10.4: 4次卷积过程

最后输出的是一个通道数为512,每个通道长度为原始输入长度的 \(\frac{1}{2048}\)(即经过 PQMF 缩小 16 倍,再经 4 次卷积缩小 128 倍后的 \(\frac{L}{2048}\))的音频潜在表示。

考虑潜空间的维度是128维,下一步则是用这个结果去估计潜变量分布的均值和方差,这个过程在 图 10.2 中也是通过卷积计算的:

图 10.5: 均值、方差矩阵

但是怎么理解这个输出的结构呢?以一维为例,VAE输出的是均值\(\mu\)和方差\(\sigma\),从其中随机采样出一个点\(z=\mu+\sigma \cdot \epsilon\)是个区间范围或概率云,通过这个范围来填充AE潜空间的空洞。对RAVE来说,当输入\(L\)的点数等于2048时,计算出来的均值和方差就是个128维向量,随机取样\(z \in R^{128} = \mu + \sigma \cdot \epsilon\)就是这2048长度的音频信号的潜变量。

因此对于一个128通道,长度为\(M\)的矩阵,它所代表的是一段长度为\(2048 \times M\)的音频信号,只不过这个潜变量矩阵由均值和方差矩阵采样而来:

图 10.6: 均值、方差矩阵与潜变量

至于如何把行数为\(M\)的潜变量矩阵变回音频信号,那就是解码器要做的事情了。

10.1.3 解码器

RAVE解码器架构如图所示:

图 10.7: 解码器

同样地有\(N=4\),它所包含的模块可以称为上采样块或反卷积块,作用差不多就是把编码器卷积的过程反过来,让输出恢复到64通道、\(128 \times z\text{的行数}\)的大小。

对于解码器的输出分为两个分支,一个是音频波形,另一个是音频响度,它们俩按元素相乘后加上噪声得到重建的音频信号。

其中波形卷积块输出的是16个通道,并通过\(\text{tanh}\)激活函数把输出幅度严格限制在\([-1,1]\)之间,用于表示解码器生成的16个音频子带信号。而响度卷积是单通道输出,且激活函数保证其输出永远为正。它通过广播机制与波形矩阵按元素相乘,用于表示每个子带随时间变化的音量增益系数。

至于噪声块,它的结构如下图所示:

图 10.8: 噪声块

网络主要的作用是产生一个滤波器过滤白噪声,以潜变量\(M=1\)为例,输入是128行、64通道的矩阵,经过噪声块的卷积后,输出一个行数除以8,80通道的矩阵,然后这80个通道可以按照以每5个通道为一组的方式排列:

图 10.9: 噪声矩阵以每5个通道为一组进行排列

这样一共就可以排列为\(k=16\)组,每个\(k_i\)表示的是第\(i\)个白噪声子带的滤波器系数,例如第1个\(16 \times 5\)的矩阵代表计算出来的第1个白噪声子带的滤波器系数。

噪声信号总共被分成16个子带,每个子带的频率宽度是1500Hz(\(\frac{24000}{16}=1500\)),第\(i\)个滤波器系数矩阵中的第\(j\)行的5个数字代表的是第\(i\)个白噪声子带的第\((j-1) \times 8 \sim j \times 8\)个采样点的滤波器的5个频点增益。

简单来说就是白噪声的PQMF每8个采样点更新一次滤波器,如果你见过音频均衡器,那矩阵每行的5个数字就是每个子带均衡器的5个推子。

图 10.10: 分时段更新的噪声滤波器

10.1.4 VAE损失函数

对于音频信号的重构损失,不能直接使用\(\|x-\hat x\|^2\),因为声音的相位会严重影响损失函数的大小,例如\(x=\sin(\omega t)\)\(\hat x=\sin(\omega t+\pi)\),两者听起来没有任何差别,但损失函数可能会很大。

为了忽略相位,RAVE使用多尺度频谱距离来衡量重构损失:

\[ S(x,\hat x)=\sum_{n\in\mathcal{N}}\left[\frac{ \|\text{STFT}_{n}(x)-\text{STFT}_{n}(\hat x)\|_F}{ \|\text{STFT}_{n}(x)\|_F}+\log\left(\|\text{STFT}_{n}(x)-\text{STFT}_{n}(\hat x)\|_1\right)\right] \]

其中 \(\mathcal{N}\) 表示不同尺度的窗口长度,例如\(\mathcal{N} = \{64, 128, 256, 512\}\)等,\(\text{STFT}_n\)表示窗口长度为\(n\),帧移为\(\frac{n}{4}\)的傅里叶变换幅度谱。

图 10.11: 多尺度示意图

幅度谱只保存有信号频率的幅度信息,而不包含相位信息。用幅度谱作为损失,总体来说是在要求生成的音频和原始音频听起来像,而不关注延迟和相位。

公式的第一项更关注频谱中的大能量或大峰值的差距(因为大值之间的差距小了,损失自然也就小了),大峰值通常对应着基频和泛音,它们的差距小了声音整体听起来就差别不大了。而\(\log\)项优化小值差距,因为对数刻度会放大小值差距之间的感知距离,缩小大值差距之间的感知距离,使得模型更关注细节。

\(S(x,\hat x)\)代入到VAE损失函数得到:

\[ \mathcal{L}_{VAE}=E[S(x,\hat{x})]+\beta \times KL[q_{\phi}(z|x)\parallel p(z)] \]

同样地,KL散度正则项用于保证训练到的潜空间接近标准正态分布,以方便采样后送入解码器生成信号。

10.2 二阶段训练

VAE训练只是RAVE的第一部分,主要目的是生成高质量潜空间,然而VAE训练出来的解码器天然地会有“平均”的倾向,这是损失函数自身的特性,想要越平滑的潜空间,输出就越平均(因为潜变量差异很小,模型取平均输出损失函数才最小,参考 图 9.7 中仅KL散度),导致输出的声音会发糊或发闷。为了单独训练解码器以获得更高质量的输出,RAVE在二阶段训练引入了生成对抗网络。

生成对抗网络(GAN)的核心思想可以用“造假者”与“鉴定师”的博弈来理解。

假设造假过程为\(G()\),鉴定过程为\(D()\),造假者试图通过\(\hat{y_i}=G(x_i)\)来模仿\(y_i\),而鉴定师则通过\(D(\hat{y_i})\)\(D(y_i)\)来打分,其分值表示输入为真的可能性。

\(G()\)而言,造假者当然希望自己的假货能尽可能地骗过鉴定师,即希望\(\sum D(\hat{y_i})\)越大越好。

\(D()\)而言,鉴定师则希望自己能准确地分辨出所有真货与假货,即希望最大化\(\sum D(y_i)\)与最小化\(\sum D(\hat{y_i})\)

把这些目标写成最小化损失函数的形式,则是:

\[ \begin{aligned} \mathcal{L}_{D}=& E[\max(0,1-D(y))]+E[\max(0,1+D(\hat{y}))] \\ \mathcal{L}_{G}=&-E[D(\hat{y})] \end{aligned} \]

这个公式的一个隐藏结论是对于鉴定器,优化后会给真货打分比1多一点点,给假货打分比-1少一点点。

GAN优化的方法是交替优化,比如第一步先升级鉴定器,让它能区分当前生成器生成的假货与真货,然后再优化生成器,利用鉴定器的梯度去调整生成器参数,让它生成的假货能骗过鉴定器,之后再循环往复这个过程(\(\phi\)固定后\(\frac{\partial\mathcal{L}_{G}}{\partial\theta}=\frac{\partial\mathcal{L}_{G}}{\partial{D_{\phi}}} \times \frac{\partial{D_{\phi}}}{\partial{G_{\theta}}} \times \frac{\partial{G_{\theta}}}{\partial{\theta}}\))。

在RAVE里,GAN的目的在于进一步优化解码器,训练的时候会冻结编码器的参数,然后引入一个鉴定器来对抗训练,为了保证在训练过程中生成信号与真实信号在音色、旋律上不跑偏,RAVE并没有单纯依赖对抗损失,而是保留了多尺度频谱损失\(S(x, \hat{x})\)作为正则项,并额外引入了特征匹配损失。

\[ \mathcal{L}_{\text{total}}=\mathcal{L}_{G}(\hat{x})+E[S(x,\hat{x})+\mathcal{L}_{\text{FM}}(x,\hat{x})] \]

10.3 训练自己的模型

想要训练自己的RAVE,第一步是查看官方开源的代码,RAVE可以访问acids-rave

其主页的README文档介绍了这个项目的整体情况,首先可以注意到RAVE版本,包括有v1、v2、v2_small等,v1是基准版本,基础架构和前文描述的一致,后续版本则是在网络结构和损失函数上有些升级,但基本框架是相同的。

以v2_small为例,为了加速训练,可以使用带有免费GPU额度谷歌colab或kaggle。

10.3.1 数据集准备

音频数据集可以分为单一风格和多样风格,通常来说数据量越大,模型越能理解数据的潜在属性,但如果数据集中包含有非常多不同类型的声音,模型可能很难学习到所有的表征,而只保留单一风格数据,模型又可能会陷入低容量行为导致表达能力变差。

对v2_small或raspberry这种更小规模的模型来说,锁定单一风格,总时长2~3个小时的训练样本是个不错的起点。

原始音频数据可以自己录音、或在网上寻找开放的声音资料,以kaggle为例,它的官网上有个页面叫Datasets,里面保存的全是他人开源的公共数据集。

搜索关键字,我找到了一个全是二胡演奏片段的音频数据集。可以直接右上角新建notebook导入数据:

图 10.12: 新建notebook导入数据

然后下载的数据会保存在/kaggle/input/下。

图 10.13: kaggle数据集存放位置

如果使用colab,可以用这行命令下载:

#下载数据
import os
os.environ["KAGGLEHUB_CACHE"] = " " #填入自己想保存的路径
os.environ["DISABLE_COLAB_CACHE"] = "true"

import kagglehub
path = kagglehub.dataset_download("colabsss/erhu-timbbre-audio-dataset")
print(path)

colab的父目录默认为/content/,因此我选择存放在/content/kaggle里:

图 10.14: colab数据目录

打开数据,会发现音频片段是按照名字顺序排列的,这也是RAVE推荐的排列方式。

在有了数据之后,为了开启模型训练,还有一步要做。

10.3.2 环境搭建

如果你有尝试复刻别人的代码,那环境搭建绝对是一个逃不开的坎,大部分bug都和环境配置有关,而且有些bug藏得很深,也许终于在你经历了某天一整个下午加大半个晚上的一系列困惑、愤怒、焦躁、麻木的情绪后,发现问题出在某个依赖库的版本原代码要求的是0.2.1,但你安装的是0.2.01,悬着的心终于是死了。

为了正常运行,我们选择在colab里创建一个虚拟环境:

#下载 Miniconda
!curl -L https://repo.anaconda.com/miniconda/Miniconda3-py39_4.12.0-Linux-x86_64.sh \
    -o /content/miniconda.sh

#安装 Miniconda
!chmod +x /content/miniconda.sh
!/content/miniconda.sh -b -p /content/miniconda

# 安装 RAVE
!/content/miniconda/bin/pip install --quiet acids-rave

# 安装 IPython
!/content/miniconda/bin/pip install --quiet --upgrade ipython ipykernel

# 安装 ffmpeg
#!/kaggle/working/miniconda/bin/conda install -y -c conda-forge ffmpeg

其中Miniconda是一个Python环境管理工具,用于管理python包,它的位置位于/content/miniconda/,如果在kaggle里,可以选择安装在/kaggle/working/miniconda/下。

之后运行RAVE所依赖的库,都选择用/content/miniconda/bin//kaggle/working/miniconda/bin/下的命令来运行。

10.3.3 预处理数据

搭建好环境后,接下来要做的是预处理原始音频数据。

dataset = "/content/kaggle/datasets/colabsss/erhu-timbbre-audio-dataset/versions/2/Downloads\ \(1\)" #填写训练数据路径

!mkdir -p /content/dataset #创建预处理后数据保存文件夹

preprocessed_dataset = "/content/dataset"

#数据预处理指令 input_path:原始数据  output_path:预处理数据 channels:单声道
!/content/miniconda/bin/rave preprocess --input_path $dataset --output_path $preprocessed_dataset --channels 1

使用rave preprocess命令,两条短线后的东西分别代表的是可以传递的变量名和值,默认选择单声道。

之后处理过的数据会保存到preprocessed_dataset变量指向的位置。

10.3.4 模型中间权重

下一步需要创建模型训练过程中参数权重的存储位置,文件夹命名为checkpoints。

!mkdir -p /content/checkpoints
save_dir = "/content/checkpoints" #训练过程中权重保存的位置

logs_path = "/content/checkpoints"

在这里先创建这个文件夹,是为了先开启监控面板,用于显示训练过程中模型的损失变化等:

# 确保当前内核装有 pyngrok
%pip install pyngrok -q

# 导入必要模块
import subprocess
from pyngrok import ngrok

# 填入你的ngrok token(替换~)
!ngrok config add-authtoken ~

# 清理残留进程并启动 TensorBoard
!killall tensorboard 2>/dev/null
log_path = "/content/checkpoints"
subprocess.Popen(["tensorboard", "--logdir", log_path, "--port", "6006"])

# 建立隧道并打印访问链接
public_url = ngrok.connect(6006).public_url
print("=" * 50)
print(f"TensorBoard 访问链接:\n{public_url}")
print("=" * 50)

ngrok token需要自己去官网注册一个,然后替换掉~,运行后它会返回一个链接,这样我们就可以在浏览器里打开这个链接监控模型训练过程。

10.3.5 开始训练

代码如下:

patch_code = """
import torch
torch.backends.cudnn.enabled = False
"""

train_file = "/content/miniconda/lib/python3.9/site-packages/scripts/train.py"

with open(train_file, "r") as f:
    content = f.read()

with open(train_file, "w") as f:
    f.write(patch_code + "\n" + content)

print("已禁用cuDNN")

#RAVE训练
name="1" #填写本次训练的名字

architecture = "v2_small" #模型架构

regularization = "default"

batch_size = 2 #小批量梯度下降

val_every = 1000 #计算当前的验证集损失

save_every = 5000 #模型权重每更新5000次保存一次网络参数

MAX_steps = 200000 #总训练步数,override里1阶段训练步数要自己重新设置

!/content/miniconda/bin/rave train \
  --config $architecture \
  --config noise \
  --db_path $preprocessed_dataset \
  --name $name \
  --val_every $val_every \
  --channels 1 \
  --save_every $save_every \
  --batch $batch_size \
  --max_steps $MAX_steps \
  --out_path $save_dir \
  --override "LATENT_SIZE=8" \
  --override "PHASE_1_DURATION=100000"

name是本次训练的名字,它会保存到checkpoints目录下,模型架构选择v2_small,batch_size定义了用多少个训练样本更新一次模型参数,colab里可以选择开启GPU T4,对此batch_size的大小通常在2到4之间。

save_every定义了参数每更新多少步保存一次模型参数,保存中间状态是因为代码运行的时候可能因各种意外导致运行中断,那下一次我们就可以直接用已经训练到一半的模型继续训练。MAX_steps定义了VAE和GAN两阶段一共需要训练多少次,对于小数据集而言,想要有不错的效果,可以尝试设置20~60万次之间。注意自己需要同步更改VAE阶段的训练次数PHASE_1_DURATION,因为模型默认的是100万次,如果总次数小于100万次,那会导致模型还没开始训练GAN就停止了。

关于rave train命令的更多参数与配置,可以参考下表:

参数名 含义
基础配置
--config 模型架构,v2_small, v2, v1
--db_path 预处理后的数据集文件夹路径 必须是预处理生成的
--out_path 权重保存位置 权重可以用于中断后继续训练的参数初始化
--name 本次训练的名字 "my_rave_1"
训练控制
--batch 批大小,如 24 受计算资源约束,通常设为 2
--channels 通道数,如 1 须与预处理保持一致
--val_every 验证频率 暂停训练计算验证集损失并生成测试音频
--save_every 权重保存频率 云端环境切勿设太小(如 500),否则易导致磁盘耗尽
--max_steps 训练全局总步数,如 100000 到达该步数后训练正常结束,需同步修改PHASE_1_DURATION
网络超参数
--override "LATENT_SIZE=8" 隐空间维度(如 8, 16, 32) 较低维度适合 Max/MSP 手势平滑控制,较高维度声音还原度更高
--override "PHASE_1_DURATION=50000" VAE阶段的更新步数 若太小判别器过早介入可能会导致梯度爆炸与底层崩溃

注意到代码开头禁用了cuDNN,主要是为了防止从VAE转到GAN阶段时GPU因cuDNN导致的内存索引出错。

10.3.6 模型导出

模型参数会根据save_every的大小不断保存为.ckpt格式的文件,具体可以在checkpoints文件夹里去找,训练完成后可以找到名为best.ckpt的权重文件,通过以下代码导出为可用的模型:

model_dir = "" #.ckpt文件
outpath = ""#输出路径

!/content/miniconda/bin/rave export --run $model_dir --output $outpath --streaming --fidelity 0.999

其中fidelity是降维系数,它决定了将潜空间信息通过降维方法(如PCA)保留多少信息,数值越高,保留的信息越多,声音还原度也越高。

streaming是流式输出,它保障了模型的实时性,需要保留。最终模型会导出为.ts格式的文件。可以放到Max/MSP里去使用。

更多细节,可以参考这个网页