40 گام به سوی آینده‌ای هوشمند - مجموعه وبینارهای رایگان در حوزه هوش مصنوعی
Filter by دسته‌ها
chatGTP
آموزش هوش مصنوعی و انواع آن
آموزش‌های پایه‌ای هوش مصنوعی
اصول هوش مصنوعی
پایتون و ابزارهای یادگیری عمیق
کتابخانه‌ های یادگیری عمیق
یادگیری با نظارت
یادگیری بدون نظارت
یادگیری تقویتی
یادگیری عمیق
یادگیری نیمه نظارتی
آموزش‌های پیشرفته هوش مصنوعی
بینایی ماشین
پردازش زبان طبیعی
پردازش گفتار
چالش‌های عملیاتی
داده کاوی و بیگ دیتا
رایانش ابری و HPC
سیستم‌‌های امبدد
علوم شناختی
دیتاست
اخبار
تیتر یک
رسانه‌ها
آموزش پردازش زبان طبیعی
آموزش علوم داده
اینفوگرافیک
پادکست
ویدیو
رویدادها
کاربردهای هوش مصنوعی
کسب‌و‌کار
تحلیل بازارهای هوش مصنوعی
کارآفرینی
هوش مصنوعی در ایران
هوش مصنوعی در جهان
 تولید موسیقی به کمک لایه‌های Conv LSTM: هوش مصنوعی قطعات موسیقی تولید می‌کند

تولید موسیقی به کمک لایه‌های Conv LSTM: هوش مصنوعی قطعات موسیقی تولید می‌کند

در این پروژه، ما از LSTM به جای GAN برای تولید موسیقی استفاده خواهیم کرد. در نتیجه استفاده از LSTM ها، مدل سریع‌تر آموزش می‌بیند و می‌توانیم عملکرد مدل را بر مبنای یک خروجی عینی ارزیابی کنیم.

این مدل بر روی قطعات موسیقی که آهنگسازها نوشته‌اند، آموزش دیده است و به نحوی آموزش دیده که بتواند با استفاده نُت (note) قبلی، نُت بعدی را پیش‌بینی کند. در نتیجه مدل می‌تواند الگویی که میان نت‌های قبلی و نُت‌های بعدی برقرار است را پیدا کند و موسیقی اصلی را تولید کند.

قطعه موسیقی زیر بخشی از موسیقی‌ ساخته شده توسط هوش مصنوعی است:

نکته جالب در مورد قطعه موسیقی LSTM 2 این است که پایان این قطعه موسیقی به گونه‌ای است که شنونده احساس می‌کند قرار است یک کادانس کامل، آکورد درجه چهار، درجه پنج، درجه یک (IV-V-I) شروع شود. آکوردهایی قبلی هم جالب به نظر می‌رسند.

LSTM

پیش‌پردازش داده‌

اولین مرحله در پروژه‌های یادگیری ماشین، پیش‌پردازش داده است. در پروژه پیش‌رو، عملیات پیش‌پردازش داده‌ها شامل دو مرحله است:

دسترسی به فایل‌های 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، فایل‌ها را به تصویر تبدیل می‌کند. از آنجایی‌که طول برنامه ۱۰۶ است و تعداد نت‌هایی موجود در یک فایل midi نیز ۱۰۶ نُت است، شکل فایل‌های midi به (۱۰۶، ۱۰۶) تغییر می‌کند. علاوه بر این، در عملیات 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

این کد ما به دایرکتوری می‌برد و تمامی داده‌های تصویری را ثبت می‌کند. توجه داشته باشید که تمامی تصاویر باید ۹۰ درجه چرخش (۹۰۰ 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 مجموعه‌های ۹ عضوی از نت‌ها را یک به یک به لیست‌ X اضافه می‌کنند و لیست Y نیز حاوی نت‌هایی است که روی هر یک از مجموعه‌های ۹تایی نگاشت شده‌اند.

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 را برازش کند. مقدار این لیست‌ها برابر با ۱۰۶ یک (۱) و صفر (۰) خواهد بود. منظور از ۱  این است که نُت در این نواک اجرا خواهد شد و منظور از ۰ این است که هیچ نُتی با این نواک در این گام زمانی اجرا نمی‌شود.

ساخت لایه 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, 
۱۰۶)))
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')

این کد، کلِ معماری مدل است که از آن استفاده می‌شود. به نظر من این مدل بسیار کارآمد و انعطاف‌پذیر است. پیش از این نیز از این مدل برای پیش‌بینی قیمت سهام بر مبنای داده‌های تاریخی استفاده کرده بودم. تنها تفاوت این دو مدل در این است که لایه آخر این مدل از تابع سیگموید با ۱۰۶ گره استفاده می‌کند، چرا که هر گام زمانی باید به صورت ۱۰۶ نُت تعریف شود و مشخص شود اینا این نُت اجرا می‌شود یا خیر.

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)

این کد مدل را برای ۱۰۰   آموزش می‌دهد. توجه داشته باشید که در این مدل به داده‌ اعتبارسنجی نیاز نداریم، زیرا لازم نیست مدل نُت بعدی را ۱۰۰ درصد درست پیش‌بینی کند. در اینجا ما فقط می‌خواهیم مدل الگوی موسیقی اصلی را یاد بگیرد و نُت احتمالی بعدی را ایجاد کند.

نتایج صوتی

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')

در مرحله بعد، تصویر را ۹۰ درجه به عقب می‌چرخانیم تا تابع 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')

حالا می‌توانیم به موسیقی اولیه گوشی دهیم:

چیزی نیست!

هیچ موسیقی تولید نشده است.

تغییر نتایج به صورت مصنوعی

در زمان بررسی نتایج متوجه می‌شویم که مدل نمی‌تواند مقادیر بّزرگ‌تر از ۶/۰ را تولید کند. و با توجه به اینکه Numpy هم مقایر کمتر از ۶/۰ را رُند می‌کند، هیچ نُتی برای اجرا کردن وجود ندارد. من تمام تلاشم را کردم تا راهی برای تغییر معماری مدل پیدا کنم اما بر روی اینترنت چیزی پیدا نکردم.

البته ایده‌ای به ذهنم رسید که نامتعارف است: اینکه نتایج را به صورت دستی تغییر دهیم.

مشکلی که شبکه دارد این است که مقادیر پیش‌بینی شده خیلی کوچک هستند. اگر یک مقدار خاص به پیش‌بینی‌ها اضافه کنیم، شبکه می‌تواند نُت‌های واقعی را نمایش دهد.

چگونه ممکن است که این تغییرات بر روی پیش‌بینی‌های مدل تأثیر نگذارند؟ زیرا ما می‌توانیم پیش‌بینی‌های مدل را مصور کنیم: البته نه به عنوان نُت اجرا شده یا نُت اجرا نشده، بلکه به این صورت که آیا این نُت مناسب این گام زمانی است یا خیر. در واقع مدل بر روی تابع زیان 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 را تغییر دهید و نتایج آن را مشاهده کنید.

جدیدترین اخبار هوش مصنوعی ایران و جهان را با هوشیو دنبال کنید

میانگین امتیاز / ۵. تعداد ارا :

مطالب پیشنهادی مرتبط

اشتراک در
اطلاع از
0 نظرات
بازخورد (Feedback) های اینلاین
مشاهده همه دیدگاه ها
[wpforms id="48325"]