天天看點

項目筆記(四) 實驗——通過對midi自相似矩陣的高斯核對角卷積分割樂段零、寫在前面一、原理二、代碼三、結果

零、寫在前面

要做seq2seq交響樂翻譯,這大概是最後一塊拼圖了。讀了些文獻,參考了裡面的方法,複現了一下,效果挺不錯的。

一、原理

1.piano_roll與幀向量v

這是一個二維矩陣,兩個軸分别為時間和音高,對應的數值表示力度。它把各個樂器的音都合成在一起了。可以調整的參數是fs:掃描頻率。預設是fs=100,表示對于midi音頻每秒掃描100次。為了在計算後面的自相似矩陣時減少計算量和記憶體,在不損失精度的情況下,fs可以降至fs=10,即每秒掃描十次。最後每0.1秒一幀,每一幀是一個128維(一共有128種音高)向量,每維的數值代表音符的力度(自然,沒有音符的音高數值為0),第i幀的向量記為 vi v i ,總幀數記為Tx。

事實上,文獻中的方法都是針對于音頻而非midi檔案:對于原始音頻做常數Q變換或MFCC,得到頻譜圖後每一幀提取特征向量。這個特征向量就等價于上段中的幀向量。

2.自相似矩陣S

這是一個二維矩陣,兩個軸的大小等于piano_roll掃描後的總幀數。對于自相似矩陣S的每一點,我們這樣來計算:

S(i,j)=distance(vi,vj) S ( i , j ) = d i s t a n c e ( v i , v j )

這裡引入了距離函數distance(),可選的包括歐氏距離,L2範數,餘弦距離。嘗試了前兩者,幾乎沒有差别。顯然地,兩個相同的向量之間的距離(無論如何計算距離)應該恒等于常數0,是以,自相似矩陣的對角線應該為常數0,将自相似矩陣以灰階圖的形式繪制出來時,可以能清楚地看到對角線的常數0。

作為例子,下面是土耳其進行曲的自相似矩陣灰階圖:

項目筆記(四) 實驗——通過對midi自相似矩陣的高斯核對角卷積分割樂段零、寫在前面一、原理二、代碼三、結果

3.高斯核對角卷積

這裡的高斯核是一個二維矩陣,以灰階圖的形式繪制(顔色越淺表示數值越高):

項目筆記(四) 實驗——通過對midi自相似矩陣的高斯核對角卷積分割樂段零、寫在前面一、原理二、代碼三、結果

它的大小是超參數,在本實驗中使用 filter_size = 256,即對于一個時間點的計算,要考慮前後各12.8秒的上下文。它的标準差同樣是一個超參數,這裡設定其值為128。

對角卷積即為将高斯核的中心置于自相似矩陣的對角線上,從S(filter_size,filter_size)移動至S(總幀數Tx-filter_size, 總幀數Tx-filter_size) 進行卷積操作(這是因為考慮到樂段分割點不會位于離頭尾很近的地方,是以沒有進行補零填充)。卷積後得到的數值組成了一個Tx維的向量,稱作新奇度得分N。對它進行濾波後就可以用作樂段分割了:極大值點對應分割點。

二、代碼

首先導入所需要的庫:

import numpy as np
import pretty_midi
import matplotlib.pyplot as plt
import keras
import scipy.signal
from scipy.ndimage import filters
import cv2
           

超參數設定等:

filter_size = 
standard_deviation = 
p = print
           

打開midi檔案,獲得piano_roll :

file_name = 'x.mid'
pm = pretty_midi.PrettyMIDI(file_name)
piano_roll = pm.get_piano_roll(fs=10)
Tx = len(piano_roll[0])

x = np.zeros(shape=(128,Tx))
x[:,:]=piano_roll
x=np.transpose(x)
           

計算自相似矩陣,這一步比較耗時,是以計算完後将矩陣導出,之後就不用重新計算了:

S=np.zeros((Tx,Tx))
p('正在計算自相似矩陣...')
for i in range(Tx):
    S[i]=(np.sum(np.sqrt(np.square(x-x[i])),axis=)) #歐氏距離

p(S)
np.save('S of '+ file_name + '.npy',S)
p('已儲存相似度矩陣。')
           

繪制自相似矩陣:

plt.figure()
plt.imshow(S,cmap = plt.cm.gray)
plt.title('Similarity matrix')
plt.show()
           

下面計算高斯卷積核并繪圖檢查:

def gaussian_kernel_2d_opencv(filter_size,standard_deviation):
    kx = cv2.getGaussianKernel(filter_size,standard_deviation)
    ky = cv2.getGaussianKernel(filter_size,standard_deviation)
    return np.multiply(kx,np.transpose(ky))
filter =gaussian_kernel_2d_opencv(filter_size,standard_deviation)

ones = np.ones((filter_size,filter_size))
for i in range (filter_size):
    if i < filter_size/:
        ones[i,:int(filter_size/)]= -
    else:
        ones[i,int(filter_size/):]=-
filter=np.multiply(filter,ones)

plt.figure()
plt.imshow(filter,cmap = plt.cm.gray)
plt.show()
           

卷積操作:

N = np.zeros(Tx)
p('正在卷積計算新奇度函數...')
for i in range (Tx):
    if i>filter_size/ and (Tx-i)>filter_size/:
        slice_x = S[:, i-int(filter_size/) : int(i+filter_size/)]
        slice_xy = slice_x[i-int(filter_size/) : int(i+filter_size/)]
        N[i]=np.sum(np.multiply(slice_xy,filter))
p(N)
           

最後,我們對新奇度函數進行濾波:

這裡濾波器的帶寬仍然是一個超參數,設定為10時效果可以接受,曲線較為平滑,也沒有很大的延遲。

p('正在對新奇度函數進行卷積濾波...')
filter_size_1d=
#filter_1d = cv2.getGaussianKernel(filter_size_1d,standard_deviation)
filter_1d = np.ones(filter_size_1d)
H = scipy.signal.convolve(N,np.reshape(filter_1d,(filter_size_1d)))
           

最後得到的H的極值就可以作為midi的樂段分割點了。

三、結果

我們以手工标注的貝多芬第七交響曲第二樂章做測試。

自相似矩陣:

項目筆記(四) 實驗——通過對midi自相似矩陣的高斯核對角卷積分割樂段零、寫在前面一、原理二、代碼三、結果

我們将新奇度函數N、輸出濾波後的新奇度函數H(橙色)和手工标注的分割點(綠色)繪至同一張圖上,結果如下:

項目筆記(四) 實驗——通過對midi自相似矩陣的高斯核對角卷積分割樂段零、寫在前面一、原理二、代碼三、結果

可以看到,這個模型的計算結果準确率尚可接受。

繼續閱讀