LSTM機器學習生成音樂

段小輝發表於2021-02-04

LSTM機器學習生成音樂

​ 在網路流量預測入門(二)之LSTM介紹中對LSTM的原理進行了介紹,在簡單明朗的 RNN 寫詩教程中介紹瞭如何使用keras構建RNN模型,然後生成五言唐詩。因此,如果對LSTM不瞭解,建議想去看一看LSTM相關的文章。

​ 在這篇部落格中,將介紹如何使用keras構建lstm模型,然後自動生成音樂。(當然這些音樂只是簡單的純音樂)

​ 程式碼地址:lstm-musichttps://github.com/xiaohuiduan/lstm-music

​ 生成的音樂:auto_musichttps://github.com/xiaohuiduan/lstm-music/blob/main/auto_music.mid

​ 實際上,使用LSTM生成音樂,與RNN生成詩詞並沒有什麼很大的不同,原理都是相通的,而在簡單明朗的 RNN 寫詩教程中,詳細的介紹了程式碼的執行流程,感興趣的可以借鑑參考。

​ 下面關於音樂(或其組成)的解釋,並不是很嚴謹(甚至可能是錯誤的),不過,在這篇部落格的目的並不是為了來介紹音樂的組成以及原理,主要是為了使用LSTM,望勿怪。

資料集介紹

​ 資料集來自Classical-Piano-Composer。部分資料如下所示,一共有92首音樂。

​ 音樂是mid型別的檔案,關於具體說明,參見How to Generate Music using a LSTM Neural Network in Keras

​ 去繁化簡,從最簡單的角度來說,我們可以理解為音樂都是由音符(note)組成的就?了。

​ 比如說,針對於0fithos.mid這首音樂,它由以下音符(note)組成:

​ 上圖中的每一個字元(如'4', 'C5', 'E5'),我們可以認為其為一個note。很多個note就組成了一首音樂。

​ 因此,在這種情況下,應該定義兩個函式,一個函式將mid檔案轉化成note陣列,另一個函式則恰恰相反,將note陣列轉化成mid檔案。

將mid轉成note陣列

​ 下面定義get_notes,通過這個函式,我們可以將資料夾中所有mid檔案變成一個名為all_note的陣列。

​ 關於具體怎麼轉化,實際上我們沒有必要去關心,這個函式也是直接copy基於深度學習lstm演算法生成音樂的,直接用即可。

from music21 import converter, instrument, note, chord, stream

def get_notes(song_path,song_names):
    """獲得midi音樂檔案中的音符

    :param song_path: [檔案的儲存地址]
    :type song_path: [str]
    :param song_names: [所有音樂檔案的檔名]
    :type song_names: [list]
    :return: [所有符合要求的音符]
    :rtype: [list]
    """
    all_notes = []
    for song_name in song_names:
        stream = converter.parse(song_path+song_name)
        instru = instrument.partitionByInstrument(stream)
        if instru:  # 如果有樂器部分,取第一個樂器部分
            notes = instru.parts[0].recurse()
        else:  #如果沒有樂器部分,直接取note
            notes = stream.flat.notes
        for element in notes:
            # 如果是 Note 型別,取音調
            # 如果是 Chord 型別,取音調的序號,存int型別比較容易處理
            if isinstance(element, note.Note):
                all_notes.append(str(element.pitch))
            elif isinstance(element, chord.Chord):
                all_notes.append('.'.join(str(n) for n in element.normalOrder))
    return all_notes

將note陣列轉成mid檔案

​ 既然可以將mid檔案轉化成note陣列,同理,也可以將note陣列轉成mid檔案(也就是音樂)。定義一個create_music函式,同理這個函式也是copy基於深度學習lstm演算法生成音樂的,同樣也不需要關心其如何實現。

create_music函式在使用模型生成音樂的時候會用到(到後面看到的時候別懵逼了哦!!!!)。

def create_music(result_data,filename):
    """生成mid音樂,然後進行儲存

    :param result_data: [音符列表]
    :type result_data: [list]
    :param filename: [檔名]
    :type filename: [str]
    """
    result_data = [str(data) for data in result_data]
    offset = 0
    output_notes = []
    # 生成 Note(音符)或 Chord(和絃)物件
    for data in result_data:
        if ('.' in data) or data.isdigit():
            notes_in_chord = data.split('.')
            notes = []
            for current_note in notes_in_chord:
                new_note = note.Note(int(current_note))
                new_note.storedInstrument = instrument.Piano()
                notes.append(new_note)
            new_chord = chord.Chord(notes)
            new_chord.offset = offset
            output_notes.append(new_chord)

        else:
            new_note = note.Note(data)
            new_note.offset = offset
            new_note.storedInstrument = instrument.Piano()
            output_notes.append(new_note)
        offset += 1
    # 建立音樂流(Stream)
    midi_stream = stream.Stream(output_notes)
    # 寫入 MIDI 檔案
    midi_stream.write('midi', fp=filename+'.mid')

獲取資料集並將其儲存

​ 通過前面的介紹,呼叫get_notes將使用music21庫將資料夾中所有的mid檔案變成一個note陣列,但實際上這個過程是比較慢的,因此可以在第一次的時候將轉換後的note陣列儲存起來,下面定義分別定義儲存和讀取的函式:

def save_data(filename,content):
    """儲存音符

    :param filename: [儲存的檔名]
    :type filename: [str]
    :param content: [內容]
    :type content: [list]]
    """
    with open(filename,"w") as f:
        for data in content:
            f.write(str(data)+"\n")

def get_data(filename):
    """從檔案中獲取音符

    :param filename: [檔名]
    :type filename: [str]
    :return: [返回音符]
    :rtype: [list]
    """
    with open(filename) as f:
       all_notes = f.readlines()
    return [ note[:len(note)-1]  for note in all_notes]

​ 接下來就是呼叫以上幾個函式:將mid檔案轉成note陣列——>將note陣列進行儲存。

import os
song_path = "./midi_songs/"
song_names = os.listdir(song_path)

# 獲取note陣列
all_notes = get_notes(song_path,song_names)
# 儲存檔案
save_data("data.txt",all_notes)

將note進行編號

​ 面對LSTM網路,當然不可能直接將音符餵給網路,在簡單明朗的 RNN 寫詩教程中詳細的介紹了原因,這裡就不多贅述。

喂的資料是進行one-hot編碼後的資料。

​ 簡單點來說,需要對音符進行one-hot編碼,因此需要對note進行編號(比如說"A5"的編號是0“F5”的編號是4)。

每一種音符都有了id(序號)後,就可以很簡單的對每一個note都進行one-hot編碼了

from collections import Counter
# 對出現過的note進行統計
counter = Counter(all_notes)
# 根據出現的次數,進行從大到小的排序
note_count = sorted(counter.items(),key=lambda x : -x[1])
notes,_ = zip(*note_count)
# 產生note到id的對映
note_to_id = {note:id for id,note in enumerate(notes)}

note_to_id的部分資料如下:

構建資料集

擷取資料

​ 構建資料集的過程原理同樣在簡單明朗的 RNN 寫詩教程詳細說過,以詩為例,過程如下。

​ 在上圖中,一個X_Data的長度是6,這裡我們取100。同時我們在取資料的同時將note轉換成id。也就是說最後在X_trainY_train中資料並不是note而是id。

X_train = []
Y_train = []
sequence_batch = 100
for i in range(len(all_notes)-sequence_batch):
    X_pre = all_notes[i:i+sequence_batch]
    Y_pre = all_notes[i+sequence_batch]
    X_train.append([note_to_id[note] for note in X_pre])
    Y_train.append(note_to_id[Y_pre])

​ 部分結果如下圖所示:

進行one-hot編碼

​ one-hot編碼,這裡我們直接使用keras提供工具。X_one_hotY_one_hot才是最終餵給LSTM的資料。

from keras.utils import to_categorical
X_one_hot = to_categorical(X_train)
Y_one_hot = to_categorical(Y_train)

構建模型

模型圖如下所示,

下面是我隨便構建的網路模型:

import keras
from keras.callbacks import ModelCheckpoint
from keras.models import Input, Model
from keras.layers import  Dropout, Dense,LSTM 
from keras.optimizers import Adam
from keras.utils import plot_model
# X_one_hot.shape[1:] = (100, 308)
input_tensor = Input(shape=X_one_hot.shape[1:])
lstm = LSTM(512,return_sequences=True)(input_tensor)
dropout = Dropout(0.3)(lstm)

lstm = LSTM(256)(dropout)
dropout = Dropout(0.3)(lstm)
# Y_one_hot.shape[-1] = 308
dense = Dense(Y_one_hot.shape[-1], activation='softmax')(dropout)

model = Model(inputs=input_tensor, outputs=dense)
# 畫圖
# plot_model(model, to_file='model.png', show_shapes=True, expand_nested=True, dpi=500)
optimizer = Adam(lr=0.001)
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
model.summary()import keras
from keras.callbacks import ModelCheckpoint
from keras.models import Input, Model
from keras.layers import  Dropout, Dense,LSTM 
from keras.optimizers import Adam
from keras.utils import plot_model
# X_one_hot.shape[1:] = (100, 308)
input_tensor = Input(shape=X_one_hot.shape[1:])
lstm = LSTM(512,return_sequences=True)(input_tensor)
dropout = Dropout(0.3)(lstm)

lstm = LSTM(256)(dropout)
dropout = Dropout(0.3)(lstm)
# Y_one_hot.shape[-1] = 308
dense = Dense(Y_one_hot.shape[-1], activation='softmax')(dropout)

model = Model(inputs=input_tensor, outputs=dense)
# 畫圖
# plot_model(model, to_file='model.png', show_shapes=True, expand_nested=True, dpi=500)
optimizer = Adam(lr=0.001)
model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
model.summary()

訓練

​ 相比較於上一次的RNN寫詩,這一次,我們可以將資料集全部放到記憶體中進行訓練,因為此次資料集比較小,可以將其全部放到記憶體中。不過,還是建議將資料集放到GPU比較好的電腦上面跑(比如說,白嫖kaggle,hhh)。

filepath = "./{epoch}--weights{loss:.2f}.hdf5"
checkpoint = ModelCheckpoint(
    filepath,
    monitor='loss',
    verbose=0,
    save_best_only=True,
    mode='min'
)
callbacks_list = [checkpoint]
model.fit(X_one_hot, Y_one_hot, epochs=100, batch_size=2048,callbacks=callbacks_list)

生成音樂

​ 生成音樂的程式碼沒什麼好說的,原理與生成唐詩原理是一樣的。生成唐詩的原理如下所示,只不過RNN變成了LSTM,同時資料的長度變成了100罷了。

載入資料

​ 在前面的操作中,通過save_data函式將資料集進行了儲存(儲存在data.txt檔案中),因此,這一次可以直接從data.txt檔案中讀取資料。

def get_data(filename):
    """從檔案中獲取音符

    :param filename: [檔名]
    :type filename: [str]
    :return: [返回音符]
    :rtype: [list]
    """
    with open(filename) as f:
       all_notes = f.readlines()
    return [ note[:len(note)-1]  for note in all_notes]
# 從儲存的資料集中獲得資料
all_notes = get_data("data.txt")

載入模型

​ 在GitHub中,已經提供了一個訓練好的模型供大家使用,不過請儘量保持keras版本一致:2.4.3

# 載入模型
from keras.models import load_model
model = load_model("weights-804-0.01.hdf5")

構建id與note的對映

​ 通過LSTM,predict出來的肯定不是一個音符,而是一個id,因此,需要構建一個id到note的對映:

from collections import Counter
from keras.utils import to_categorical

counter = Counter(all_notes)
note_count = sorted(counter.items(),key=lambda x : -x[1])
notes,_ = zip(*note_count)
# note到id的對映
note_to_id = {note:id for id,note in enumerate(notes)}
# id到note的對映
id_to_note = {id:note for id,note in enumerate(notes)}
# 構建X_train,目的是為了實現隨機從X_one_hot選擇一個資料,然後進行predict 
X_train = []
sequence_batch = 100
for i in range(len(all_notes)-sequence_batch):
    X_pre = all_notes[i:i+sequence_batch]
    X_train.append([note_to_id[note] for note in X_pre])
X_one_hot = to_categorical(X_train)

預測下一個note

​ 可以定義一個函式,目的是為了進行predict,函式接受長度為100的note陣列,然後返回預測的id

def predict_next(X_predict):
    """通過前100個音符,預測下一個音符

    :param X_predict: [前100個音符]
    :type X_predict: [list]
    :return: [下一個音符的id]
    :rtype: [int]
    """
    prediction = model.predict(X_predict)
    index = np.argmax(prediction)
    return index

源源不斷產生note資料

​ 一首音樂當然不可能就101個音符(初始給的100個音符,然後通過這100個音符預測下一個音符),因此需要如下圖所示,源源不斷地進行預測。

​ 下面定義generate_notes函式,目的就是為了產生音符長度為1000的音樂檔案。

import numpy as np
from music21 import converter, instrument, note, chord, stream
def generate_notes():
    """隨機從X_one_hot抽取一個資料(長為100),然後進行predict,最後生成音樂

    :return: [note陣列(['D5', '2.6', 'F#5', 'D3', ……])]
    :rtype: [list]
    """
    # 隨機從X_one_hot選擇一個資料進行predict
    randindex = np.random.randint(0, len(X_one_hot) - 1)
    predict_input = X_one_hot[randindex]
    # music_output裡面是一個陣列,如['D5', '2.6', 'F#5', 'D3', 'E5', '2.6', 'G5', 'F#5']
    music_output = [id_to_note[id] for id in X_train[randindex]]
    # 產生長度為1000的音符序列
    for note_index in range(1000):
        prediction_input = np.reshape(predict_input, (1,X_one_hot.shape[1],X_one_hot.shape[2]))
        # 預測下一個音符id
        predict_index = predict_next(prediction_input)
        # 將id轉換成音符
        music_note = id_to_note[predict_index]
        music_output.append(music_note)
        # X_one_hot.shape[-1] = 308
        one_hot_note = np.zeros(X_one_hot.shape[-1])
        one_hot_note[predict_index] = 1
        one_hot_note = np.reshape(one_hot_note,(1,X_one_hot.shape[-1]))
        # 重新構建LSTM的輸入
        predict_input = np.concatenate((predict_input[1:],one_hot_note))
    return music_output

​ 呼叫generate_notes函式,便可以產生一定長(1000)序列的note陣列。

predict_notes = generate_notes()

生成音樂

​ 通過上一步,產生了一定長序列的note陣列了,接在下,呼叫在前文定義的將note陣列轉成mid檔案函式(create_music函式),便可以將note陣列轉換成音樂mid檔案。

create_music(predict_notes,"auto_music")

總結

​ 以上,便是使用keras構建LSTM生成音樂的全部內容。實際上內容與簡單明朗的 RNN 寫詩教程的過程差不多(可謂是大同小異)。

​ 在藉助keras API情況下,我們可以很輕鬆的使用幾行程式碼便可以構建一個lstm模型,但實際上,真正重要的並不是我們如何呼叫keras的API寫程式碼,而是幾行程式碼後面的原理。

參考

相關文章