تولید موسیقی به کمک لایههای Conv LSTM: هوش مصنوعی قطعات موسیقی تولید میکند
در این پروژه، ما از LSTM به جای GAN برای تولید موسیقی استفاده خواهیم کرد. در نتیجه استفاده از LSTM ها، مدل سریعتر آموزش میبیند و میتوانیم عملکرد مدل را بر مبنای یک خروجی عینی ارزیابی کنیم.
این مدل بر روی قطعات موسیقی که آهنگسازها نوشتهاند، آموزش دیده است و به نحوی آموزش دیده که بتواند با استفاده نُت (note) قبلی، نُت بعدی را پیشبینی کند. در نتیجه مدل میتواند الگویی که میان نتهای قبلی و نُتهای بعدی برقرار است را پیدا کند و موسیقی اصلی را تولید کند.
قطعه موسیقی زیر بخشی از موسیقی ساخته شده توسط هوش مصنوعی است:
نکته جالب در مورد قطعه موسیقی LSTM 2 این است که پایان این قطعه موسیقی به گونهای است که شنونده احساس میکند قرار است یک کادانس کامل، آکورد درجه چهار، درجه پنج، درجه یک (IV-V-I) شروع شود. آکوردهایی قبلی هم جالب به نظر میرسند.
پیشپردازش داده
اولین مرحله در پروژههای یادگیری ماشین، پیشپردازش داده است. در پروژه پیشرو، عملیات پیشپردازش دادهها شامل دو مرحله است:
دسترسی به فایلهای Midi:
من در اینترنت یک دیتاست متشکل از قطعات موسیقی کلاسیک پیدا کردم و تمامی فایلهای midi را از آن استخراج کردم و در یک پوشه قرار دادم.
تبدیل فایلهای midi به تصویر
من در GitHub صفحهای پیدا کردم که دو برنامه (program) دارد و با استفاده از کتابخانه music21 فایلهای midi را به تصویر و تصاویر را به فایلهای midi تبدیل میکند.
هر نُت را میتوان به صورت یک بلوک سفید نشان داد. ارتفاع بلوک نشاندهنده نواک (pitch) و طول بلوک نشاندهنده مدت زمان اجرای نُت است.
من برای اینکه این دو برنامه را با یکدیگر و با فایلهای midi ادغام کنم و در یک دایرکتوری دیگر تصاویر جدیدی ایجاد کنم یک کد نوشتم:
import os import numpy as nppath = 'XXXXXXXXX'os.chdir(path) midiz = os.listdir() midis = [] for midi in midiz: midis.append(path+'\\'+midi)
این کد ما را به مسیر دایرکتوری midi میبرد و مسیر تمامی فایلهای midi را به یک لیست اضافه میکند تا بتوانیم به آنها دسترسی داشته باشیم:
from music21 import midimf = midi.MidiFile() mf.open(midis[0]) mf.read() mf.close() s = midi.translate.midiFileToStream(mf) s.show('midi'
این کد اولین فایل midi را باز میکند و برای اینکه مطمئن شود برنامه به خوبی کار میکند آن را پخش میکند. اگر این کد را در یک محیط غیرتعاملی اجرا کنید، ممکن است کار نکند.
import os import numpy as np import py_midicsv as pmos.chdir(path) midiz = os.listdir() midis = [] for midi in midiz: midis.append(path+'\\'+midi) new_dir = 'XXXXXXXX' for midi in midis: try: midi2image(midi) basewidth = 106 img_path = midi.split('\\')[-1].replace(".mid",".png") img_path = new_dir+"\\"+img_path print(img_path) img = Image.open(img_path) hsize = 106 img = img.resize((basewidth,hsize), Image.ANTIALIAS) img.save(img_path) except: pass
این کد با استفاده از تابع midi2image (که در همان صفحه GitHub قرار دارد) و با توجه به مسیر دسترسی به فایلهای midi، فایلها را به تصویر تبدیل میکند. از آنجاییکه طول برنامه 106 است و تعداد نتهایی موجود در یک فایل midi نیز 106 نُت است، شکل فایلهای midi به (106، 106) تغییر میکند. علاوه بر این، در عملیات transposed convolution کار کردن با مربعات آسانتر است.
ایجاد دیتاست
import os imgs = os.listdir() pixels = [] from PIL import Image import numpy as np for img in imgs: try: im = Image.open(img).rotate(90) data = np.array(im.getdata())/255 pix = (data).reshape(106,106) pixels.append(pix) except: pass
این کد ما به دایرکتوری میبرد و تمامی دادههای تصویری را ثبت میکند. توجه داشته باشید که تمامی تصاویر باید 90 درجه چرخش (900 rotation) داشته باشند، در این صورت تایع getdata میتواند به جای نواک (pitch)، بر اساس ترتیب زمانی به دادهها دسترسی پیدا کند.
def split_sequences(sequence, n_steps): X, y = list(), list() for i in range(len(sequence)): end_ix = i + n_steps if end_ix > len(sequence)-1: break seq_x, seq_y = sequence[i:end_ix-1], sequence[end_ix] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y) X = [] y = [] for i in range(len(pixels)): mini_x,mini_y = split_sequences(pixels[i],10) X.append(mini_x) y.append(mini_y)
این کد بر مبنای دادههای سری زمانی، لیستهای x و y را میسازد. متغیر mini_x مجموعههای 9 عضوی از نتها را یک به یک به لیست X اضافه میکنند و لیست Y نیز حاوی نتهایی است که روی هر یک از مجموعههای 9تایی نگاشت شدهاند.
X = np.array(X) y = np.array(y)
اگر لیستهای x و y را به آرایههای Numpy تبدیل کنیم، زمانیکه دادهها را به مدل وارد میکنیم با هیچ خطایی مواجه نمیشویم.
X = X.reshape(len(X),1,9,106) y = y.reshape((y.shape[0]*y.shape[1],y.shape[2]))
این کد شکل آرایههای x و y را تغییر میدهد تا لایه Conv LSTM را برازش کند. مقدار این لیستها برابر با 106 یک (1) و صفر (0) خواهد بود. منظور از 1 این است که نُت در این نواک اجرا خواهد شد و منظور از 0 این است که هیچ نُتی با این نواک در این گام زمانی اجرا نمیشود.
ساخت لایه Conv LSTM
from keras.models import Sequential from keras.layers import Dense from keras.layers import Flatten from keras.layers import Dropout,BatchNormalization from keras.layers import LSTM,TimeDistributed from keras.layers.convolutional import Conv1D,MaxPooling1D model = Sequential() model.add(TimeDistributed(Conv1D(filters=128, kernel_size=1, activation='relu'), input_shape=(None, 9, 106))) model.add(TimeDistributed(MaxPooling1D(pool_size=2, strides=None))) model.add(TimeDistributed(Conv1D(filters=128, kernel_size=1, activation='relu'))) model.add(TimeDistributed(MaxPooling1D(pool_size=2, strides=None))) model.add(TimeDistributed(Conv1D(filters=128, kernel_size=1, activation='relu'))) model.add(TimeDistributed(MaxPooling1D(pool_size=2, strides=None))) model.add(TimeDistributed(Conv1D(filters=128, kernel_size=1, activation='relu'))) model.add(TimeDistributed(Flatten())) model.add(LSTM(128,return_sequences = True)) model.add(LSTM(64)) model.add(BatchNormalization()) model.add(Dense(106,activation = 'sigmoid')) model.compile(optimizer='adam', loss='mse')
این کد، کلِ معماری مدل است که از آن استفاده میشود. به نظر من این مدل بسیار کارآمد و انعطافپذیر است. پیش از این نیز از این مدل برای پیشبینی قیمت سهام بر مبنای دادههای تاریخی استفاده کرده بودم. تنها تفاوت این دو مدل در این است که لایه آخر این مدل از تابع سیگموید با 106 گره استفاده میکند، چرا که هر گام زمانی باید به صورت 106 نُت تعریف شود و مشخص شود اینا این نُت اجرا میشود یا خیر.
model.summary()
زمانیکه این تابع را فراخوانی میکنیم تا معماری مدل را مشاهده کنیم، گزارشی بدین شکل به دست میآوریم:
Model: "sequential_4" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= time_distributed_25 (TimeDis (None, None, 9, 128) 13696 _________________________________________________________________ time_distributed_26 (TimeDis (None, None, 4, 128) 0 _________________________________________________________________ time_distributed_27 (TimeDis (None, None, 4, 128) 16512 _________________________________________________________________ time_distributed_28 (TimeDis (None, None, 2, 128) 0 _________________________________________________________________ time_distributed_29 (TimeDis (None, None, 2, 128) 16512 _________________________________________________________________ time_distributed_30 (TimeDis (None, None, 1, 128) 0 _________________________________________________________________ time_distributed_31 (TimeDis (None, None, 1, 128) 16512 _________________________________________________________________ time_distributed_32 (TimeDis (None, None, 128) 0 _________________________________________________________________ lstm_6 (LSTM) (None, None, 128) 131584 _________________________________________________________________ lstm_7 (LSTM) (None, 64) 49408 _________________________________________________________________ batch_normalization_2 (Batch (None, 64) 256 _________________________________________________________________ dense_2 (Dense) (None, 106) 6890 ================================================================= Total params: 251,370 Trainable params: 251,242 Non-trainable params: 128
شما میتوانید پارامترهای مدل را تغییر دهید و تأثیر هر یک از آنها را بر روی نتیجه نهایی مدل مشاهده کنید.
model.fit(X,y,epochs = 100)
این کد مدل را برای 100 آموزش میدهد. توجه داشته باشید که در این مدل به داده اعتبارسنجی نیاز نداریم، زیرا لازم نیست مدل نُت بعدی را 100 درصد درست پیشبینی کند. در اینجا ما فقط میخواهیم مدل الگوی موسیقی اصلی را یاد بگیرد و نُت احتمالی بعدی را ایجاد کند.
نتایج صوتی
song_length = 106 data = X[np.random.randint(len(X))][0] song = [] for i in range(song_length): pred = model.predict(data.reshape(1,1,9,106))[0] notes = (pred.astype(np.uint8)).reshape(106) print(notes) song.append(notes) data = list(data) data.append(notes) data.pop(0) data = np.array(data)
مدل با استفاده از این تابع میتواند موسیقی تولید کند. نحوه عملکرد این مدل بدین صورت است:
مدل برای اینکه فرایند تولید موسیقی را آغاز کند، برای پیشبینی کردن به یک نمونه تصادفی از دیتاست نیاز دارد. هر بار که مدل پیشبینی میکند، پیشبینی انجامشده به آخر لیست اضافه میشود. برای اینکه شکل ورودی در تمامی تکرارها (iteration) یکسان باشد، باید نمونه اول را حذف کنیم. با جمعآوری پیشبینیها یک قطعه موسیقی تولید میشود که کاملا! توسط کامپیوتر ساخته شده و هیچ اطلاعاتی از نمونه اصلی در آن به چشم نمیخورد.
new_image = Image.fromarray(np.array(song)).rotate(-90) new_image.save('composition.png')
در مرحله بعد، تصویر را 90 درجه به عقب میچرخانیم تا تابع image2midi به خوبی عمل کند:
image2midi('composition.png')
سپس تصویر را به فایل midi تبدیل میکنیم و برای گوش دادن به قطعه موسیقی تولیدشده کد زیر را اجرا میکنیم:
!apt install fluidsynth !cp /usr/share/sounds/sf2/FluidR3_GM.sf2 ./font.sf2 !fluidsynth -ni font.sf2 composition.mid -F output.wav -r 44100 from IPython.display import Audio Audio('output.wav')
حالا میتوانیم به موسیقی اولیه گوشی دهیم:
چیزی نیست!
هیچ موسیقی تولید نشده است.
تغییر نتایج به صورت مصنوعی
در زمان بررسی نتایج متوجه میشویم که مدل نمیتواند مقادیر بّزرگتر از 6/0 را تولید کند. و با توجه به اینکه Numpy هم مقایر کمتر از 6/0 را رُند میکند، هیچ نُتی برای اجرا کردن وجود ندارد. من تمام تلاشم را کردم تا راهی برای تغییر معماری مدل پیدا کنم اما بر روی اینترنت چیزی پیدا نکردم.
البته ایدهای به ذهنم رسید که نامتعارف است: اینکه نتایج را به صورت دستی تغییر دهیم.
مشکلی که شبکه دارد این است که مقادیر پیشبینی شده خیلی کوچک هستند. اگر یک مقدار خاص به پیشبینیها اضافه کنیم، شبکه میتواند نُتهای واقعی را نمایش دهد.
چگونه ممکن است که این تغییرات بر روی پیشبینیهای مدل تأثیر نگذارند؟ زیرا ما میتوانیم پیشبینیهای مدل را مصور کنیم: البته نه به عنوان نُت اجرا شده یا نُت اجرا نشده، بلکه به این صورت که آیا این نُت مناسب این گام زمانی است یا خیر. در واقع مدل بر روی تابع زیان MSE آموزش میبیند: به عبارت دیگر، تفاضل میان مقدار پیشبینی شده و مقادیر واقعی معنادار است. با توجه به اینکه مقادیری که مدل در آغاز پیشبینی کرده، مجدداً آموزش میبینند، پیشبینیهای مدل بزرگ میشوند.
تابع تولید موسیقی را ارتقا دادیم:
song_length = 106 data = X[np.random.randint(len(X))][0] song = [] vanish_proof = 0.65 vanish_inc = 1.001 for i in range(song_length): pred = model.predict(data.reshape(1,1,9,106))[0]+vanish_proof vanish_proof *= vanish_inc notes = (pred.astype(np.uint8)).reshape(106) print(notes) song.append(notes) data = list(data) data.append(notes) data.pop(0) data = np.array(data)
در این تابع دو متغیر جدید به نامهای vanish_proof و vanish_inc داریم. Vanish_proof مقداری است که به تمامی پیشبینیها اضافه میشود و vanish_inc نرخ افزایش vanish_proof است. با توجه به اینکه هر یک از پیشبینیهای شبکه بر مبنای پیشبینیهای قبلی خواهند بود، وجود این دو متغیر لازم و ضروری است. کوچک بودن پیشبینیها بر عملیات انتشار رو به جلو (forward propogation) تأثیر میگذارد و باعث میشود موسیقی به آرامی خاموش شود.
در نتیجه موسیقی بهتری ساخته میشود. من این قطعه موسیقی را دوست دارم:
نتیجهگیری
به نظر من جالبترین قسمت این مقاله همان تغییر دستی نتایج بود. تا جایی که اطلاع دارم کسی قبلاً این کار را انجام نداده و مطمئنم نیستم این تمرین چقدر نتیجهبخش است. شما میتوانید متغیرهای vanish_proof و vanish_inc را تغییر دهید و نتایج آن را مشاهده کنید.
جدیدترین اخبار هوش مصنوعی ایران و جهان را با هوشیو دنبال کنید