2025/12/27 文章已经整理到知乎: https://zhuanlan.zhihu.com/p/1988276192063800011
下文为8/21刚完成算法时的整理:
- 问题:给定音高序列和时频谱,求对应关系。音频为polyphonic,还可能为多音色,而音高序列为monophonic,比如只有人声或某种乐器的。最终得到赋予时值信息的音符。此任务类似自动排布视频字幕。
- 输入:时频谱(而不是音频)、音高序列
- 输出:音符在时频谱上的坐标
- 定位:计算复杂度少于AMT(自动扒谱),最好不使用神经网络。因为如果AMT更快,就没必要让用户给出音高序列了。好比有自动识别、添加字幕功能时,用户就没必要重新打一遍字幕了。
- 音符之间可能需要插入“间隔”
- 对于连续的同一音高的音符序列,如何断开
使用时频谱的时间轴,每一帧的隐状态即为音符,目标是最大化全局概率。假设第t帧频谱为 $\vec{s}t$ ,第t帧的隐状态记为 $v_t$ ,第i个音符(音高)为 $n_i$ ,间隔记为"O"。则有: $$ v_t = \begin{cases} n_i \rightarrow v{t+1}=& \begin{cases} n_i:&保持不变\ O:&间隔\ n_{i+1}:&下一个音符 \end{cases}\ \ O \rightarrow v_{t+1}=& \begin{cases} O:&保持间隔\ n_{i+1}:&下一个音符 \end{cases}\ \end{cases}\tag{1} $$
HMM有三个要素:初始状态分布,状态转移矩阵,激发概率。
- 初始状态可以设置为"O"。
- 按照传统HMM的建模方法,每个音高都对应各自的状态,比如noteDigger有84个半音,则有(84+1)个状态(考虑了"O")。而用户给出的音符序列,相当于约束了状态转移矩阵。然而,由于每个音符的长度未定,同一个状态可能承载多种分支,使得状态转移矩阵无法构建:比如音符“123124”,第一条转移路径在t时到达第一个2,第二条转移路径在t时到达第二个2,那此时状态2的转移概率如何构建?显然此时两个分支的转移概率应该分开来算。因此,传统的HMM状态建模不可行。
- 激发概率难以合理设置。由于频谱状态空间无限大,必须动态求解。而频谱反应的是分量的幅度,值域为
$[0,+\infty)$ ,没有合理的、到$[0, 1]$ 的映射。
DTW 即 Dynamic Time Warping,常用于序列对齐。初见DTW是在MusicNet数据集中,DTW免去了人工的对齐,但也因此此数据集的精度不高。想到DTW是因为本任务就是两个序列的对齐:音高数组和(所有帧的)频谱(构成的)数组的对齐。
DTW也是状态的转移,不过将每个元素视为一个状态。上述传统HMM状态建模仅仅编码了音高,导致了时序的混乱;DTW相当于将 (音高,顺序) 作为整体编码,因此音高序列有多长,就有多少个状态。这一点值得学习。
DTW和HMM的Viterbi解码基本原理都是动态规划。如果将问题建模为使用上述状态编码的HMM,且状态转移概率均等,那就可以用DTW来解码路径。
相比于HMM使用激发概率,DTW使用距离(其实概率也可以算一种距离)。距离的好处是直接加减、没有范围限制,很容易从幅度谱中映射,比如我选择了用“当前频谱当前音高的幅度的相反数”,因为幅度越大表明越有可能有该音高,而“距离越大越不相似”与之相反,所以加个负号。
如何解决提到的难点?可以在每个音符之间插入一个"O"。那如果两个音符理应紧密相连怎么办?DTW中有一对多、多对一的情况,理论上不匹配的会被折叠,即"O"可以不出现。
最困难的是"O"到各个频谱的距离。最终我选择了频谱均值,在统计了一些数据后设置为0.08的相反数,这大概能代表整个时频谱每个单元的能量均值。
实现上为了减少空间复杂度,使用Hirschberg algorithm,虽然运算量*2,但节省了帧数/2倍的空间(帧数往往在4k以上)。
然而效果并不好,仅有局部可以对齐。我认为是DTW的无记忆性和缺少约束导致的:
- 无记忆:特指求距离时仅依赖当前状态,缺少上一个状态。比如同为切换到"O",从音符切换到"O"时的距离应该和“音符和频谱的距离”有关,因为是音符的结尾;而若上一个状态就是"O",此时的距离应该和“下一个音符和频谱的距离”有关,因为要识别下一个音符的开头。
- 缺少约束:比如结果中出现了宽度为0的音符,这是被折叠了,而实际音符应该有长度约束。
长度约束可以在DTW的路径回溯添加,具体为限制音符的回溯方向。而使用Hirschberg algorithm的DTW无法解决“无记忆”的问题,因为此算法不按顺序,根基就是“DTW的无记忆距离计算”。为此只能回到DTW最初的实现。而要增加的记忆的作用是可以利用上一帧的状态计算本帧本状态的距离,意味着距离和路径有关。这会导致什么呢?比如考虑下面的D格:
| 累积距离 | 上一帧 | 当前帧 |
|---|---|---|
| 状态X | A: -1 | B: -2 |
| 状态Y | C: -3 | D: ? |
| 方向 | 距离增加 |
|---|---|
| A→D | -5 |
| B→D | -3 |
| C→D | -1 |
一通计算发现应该选A→D,更新D的距离为-6。但在DTW的路径回溯中,上一步选择的是距离最小值,因此D的上一步会选择C,而不是计算距离用的A。因此DTW整个框架是没法引入记忆的。
DTW被其路径回溯限制,那我们可以改成Viterbi算法的路径记录啊~于是回归HMM建模与Viterbi解码。
状态设置同DTW,即首先给音符序列间插"O",然后每个元素作为一个状态。
此时可以设置状态转移概率了,即给式(1)中每一行设置一个概率,具体为多少可以根据实验调参。马尔可夫链基于概率,是累乘的,但只要取对数就变成了DTW的累加。问题是如何得到合理的激发概率。
映射的难点在于无界到有界,而对于实际的样本其实最大值就是上界。所以我使用样本的最值作为边界、线性缩放至
我统计了一下每帧能量的分布,近似为卡方分布。每一个时频单元的能量分布应该也近似(没统计)。这意味着能量大的单元少,而中间或者低能单元的数目多。所以简单的线性缩放会导致大量单元的概率仅在0.5以下,所以我选择了再开根号。假设给出时频谱
t \in 1,2,3,...,T\ i \in 1,2,3,...,N \ p(\vec{s}t | n_i) = (\frac{s{t,n_i}-amp}{Amp - amp})^{0.5} $$
假设$t$时刻的状态为$u_t$,定义
注意最后一行,转移判定是“不是$n_i$且是$n_{i+1}$”,所以是两个相乘,进而发现其实是转移两次的结果,实际实现中,对每一轮动规,先进行沿着时间的转移,再保持时间不变,进行沿着状态的转移,且仅对“O”状态开展后一轮转移。拆解为两次转移可以避免跨状态转移,保持了每次转移的一阶马尔科夫性。
实际应用中的状态转移和式(1)有不同。首先添加长度约束。需要记录当前音符状态(不考虑"O"的长度约束)已经保持了多长,小于最小长度时设置为99%的概率继续延续。
其次是转移方式。先做如下约定:
- 从左到右——时频谱从开始到结尾
- 从上到下——音符序列从开始到结尾
- 起点:左上角;终点:右下角
输入的音符序列已经间插了"O"了,如果当前是音符,直接跳到下一个音符需要跨一行更新,不优雅;遇到边界还需要重新调整转移概率,麻烦。而从
- 只允许向右和右下转移,转移概率和为1。这保证了音符不会被折叠。
- 同一列中,允许"O"向下转变,可以设置一个转变概率。这保证了"O"可以被折叠
- 最终取最大值,并记录上一步。
“转变”其实不是很严谨,等效到状态转移相当于总概率超过1了。
最终效果优于DTW,人肉调参许久得到了如今使用的参数。