{count, plural, one {Kurs} other {Kurs}}
Bu eğitim, Python'da TensorFlow 2.0 ve TensorFlow 2.0'da rolü güçlendirilen yüksek seviyeli Keras API etrafında geliştirilmiştir. TensorFlow 2.0 hakkında daha fazla bilgi edinmek isteyenler, DataCamp'teki Introduction to TensorFlow in Python kursuna göz atabilir. Müzik için derin öğrenme literatürüne kapsamlı bir bakış için, bu eğitim boyunca atıf yapacağımız Briot, Hadjerest ve Pachet (2019) çalışmasına bakın.
Üretici Modeller
Denetimli makine öğrenmesi modelleri iki kategoriye ayrılabilir: ayrıştırıcı modeller ve üretici modeller. Ayrıştırıcı modeller bir karar sınırı tanımlar ve buna karşılık gelen bir sınıflandırma üretir. Üretici modeller ise bir sınıfın yeni örneklerini oluşturur.
Müziğe yönelik bir ayrıştırıcı model, şarkıları farklı türlere göre sınıflandırmak için kullanılabilir. Üretici bir model ise belirli bir türde şarkılar bestelemeye yarayabilir. Bu eğitimde, müzik bestelemek için üretici modellerden yararlanacağız.

Müzik Temsili
Bir üretici modeli tanımlayıp eğitmeden önce bir veri kümesi oluşturmamız gerekir. Müzik için veriler, sürekli ya da ayrık biçimde temsil edilebilir. En yaygın sürekli biçim, genellikle WAV dosyası olarak saklanan ses sinyalidir. Bu eğitimde sürekli biçimleri kullanmayacağız; ancak bunlar hakkında daha fazla bilgiyi Ek bölümünde bulabilirsiniz. Yaygın ayrık biçimler arasında Musical Instrument Digital Interface (MIDI) dosyaları, piyano rulosu (pianoroll) ve metin yer alır. Biz MIDI dosyalarına odaklanacak ve iki tür sembolik nesne çıkaracağız: notalar ve akorlar.
Notalar
Nota, bir sesin sembolik temsilidir. Bizim amaçlarımız için bir nota, perdesi ve süresiyle tanımlanabilir. Bir notanın perdesi, hertz (Hz) cinsinden ölçülen ses dalgasının salınım frekansıyla ilişkilidir. Daha yüksek perdeye sahip notaların ses dalgaları saniyede daha fazla salınıma sahiptir. Bir notanın süresi, çalındığı dönemin uzunluğudur.
MIDI dosyaları, bir notanın perdesini 0 ile 127 arasında bir tamsayıyla temsil eder. Notalar ayrıca bir perde harfi ve oktav numarasıyla da gösterilebilir. Aynı oktav içinde perdeler, en düşükten en yüksek frekansa doğru şu şekilde sıralanır:
- C
- C#/D♭
- D
- E♭/D#
- E
- F
- F#/G♭
- G
- A♭/G#
- A
- B♭/A#
- B
Oktav, A4'teki 4 veya C7'deki 7 gibi alt simgeyle belirtilir. Daha yüksek bir oktav, daha yüksek bir frekansa karşılık gelir. Keyfi bir perde Xi alırsak, tam olarak bir oktav yüksek olan Xi+1, Xi'nin iki katı frekansa sahip bir ses dalgasını temsil eder.
Bir notanın perdesine ek olarak, süresini de kullanacağız. Süre, tam nota uzunluğuyla normalize edilen göreli bir değerdir. En uzun nota, tam notanın sekiz katı uzunluğa sahip "büyük" notadır. En kısa nota ise 1/256 tam nota uzunluğuna karşılık gelen iki yüz elli altıncı notadır.
Akorlar
Akor, aynı enstrümanda eşzamanlı çalınan iki veya daha fazla notanın birleşimidir. Monofonik müziğe — yani tek bir enstrümanda çalınan müziğe — bakarsak, tam olarak aynı anda çalınan tüm notaların aynı akorun parçası olduğunu varsayarak akorları tanımlayabiliriz. Bu varsayım, iki veya daha fazla enstrümanın aynı anda çaldığı polifonik müzikte geçerli değildir.
Aşağıdaki kod bloğunda, music21'i kuruyor ve MIDI dosyalarını ayrıştırmak için kullanacağımız converter modülünü içe aktarıyoruz. Mauro Giuliani'nin klasik gitar parçasını yükleyip ayrıştıracağız. Ardından, partisyon içerisinde eşzamanlı çalınan notaların aynı akora ait olduğunu varsayan ve akor dizisini yeniden kuran .chordify() yöntemini uygulayacağız.
!pip install music21
from music21 import converter
# Define data directory
data_dir = '../audio/'
# Parse MIDI file and convert notes to chords
score = converter.parse(data_dir+'giuliani.mid').chordify()
# Display as sheet music
print(score.show())
MIDI dosyasındaki akor dizisini oluşturduktan sonra, .show() yöntemini kullanarak notaya dökülmüş partisyon olarak görüntüleyebiliriz. Bunun için ücretsiz olarak sunulan MuseScore gibi nota yazımı ve görüntüleme yazılımlarına ihtiyaç duyacaksınız.

Alternatif olarak, alttaki öğeleri metin olarak incelemek ve değiştirmek isteyebiliriz. Bunu .show('text') yöntemiyle yapabiliriz. Her satırın başındaki köşeli parantez içinde görülen offset, bir öğenin partisyonun kaçıncı saniyesinde göründüğünü belirtir. 0. saniyedeki öğelerin yalnızca birinin akor olduğuna dikkat edin. Geri kalan öğeler; kullanılan enstrüman, tempo, tonalite ve ölçü gibi metadata'dır. Tonaliteyi ileride kullanacağız; şimdilik bu öğeleri göz ardı edebilirsiniz.
print(score.show('text'))
{0.0} <music21.instrument.Guitar 'Guitar'>
{0.0} <music21.tempo.MetronomeMark animato Quarter=120.0>
{0.0} <music21.key.Key of C major>
{0.0} <music21.meter.TimeSignature 4/4>
{0.0} <music21.chord.Chord E3 G3>
{1.0} <music21.chord.Chord B2 G3>
{2.0} <music21.chord.Chord D3 G3>
...
{46.0} <music21.chord.Chord A3>
...
{88.0} <music21.chord.Chord G2 E3 G3 C4>
{90.0} <music21.chord.Chord G2 D3 G3 B3>
{92.0} <music21.chord.Chord C3 E3 G3 C4>
Metadata sonrasında kalan öğeler, şarkıda göründükleri sırayla dizilmiş akorlardan oluşur. Örneğin, <music21.chord.Chord E3 G3>, E3 ve G3 notalarının birlikte çalınmasıyla oluşan akordur. Ayrıca, <music21.chord.Chord A3>, tek başına çalınan A3 notasını ifade eder.
Bazı modellerde akorun kendisiyle birlikte süresini de kullanacağız; bu, akorun ne kadar süre çalındığını ölçer. Aşağıdaki kod bloğunda gösterildiği gibi .elements yöntemini uygulayıp, partisyon içindeki rastgele bir akorun .duration özniteliğine erişerek bu bilgiyi alabiliriz.
print(score.elements[10])
<music21.chord.Chord E3 A3>
print(score.elements[10].duration)
<music21.duration.Duration 1.0>
Nota ve Akor Kodlaması
Bu eğitim boyunca, notalar, akorlar ve bunlara karşılık gelen sürelerden oluşan dizilerle tanımlanabilen ayrık müzik temsillerine odaklanacağız. Akorların notaların birleşiminden oluştuğunu ve MIDI biçiminin 128 ayrı notanın kullanımına izin verdiğini anımsayın. Özellik kümesini tüm iki notalı akorları da kapsayacak şekilde genişletmek istersek, bu boyutu 128!/[(128-2)!*2!] yani 8128 öğe artırır. Üç notalı akorların eklenmesi bunu 341.376 kadar, dört notalı akorların eklenmesi ise 10.668.000 kadar daha artırır.
Önceki örneğimiz; notalar ile iki, üç ve dört notalı akorlar içeriyordu. Tek-sıcak (one-hot) kodlama kullanır ve tüm özellikleri dahil edersek, 11.017.632 boyutlu vektörler üzerinde tahmin yapmamız gerekir. Bu nedenle, müzik üretimi problemlerinde kodlama seçimi konusunda dikkatli düşünmek isteriz. Ayrıca Ek bölümünde ele alınan tonalite transpozisyonu veya veri artırma yöntemlerini de değerlendirmek isteyebiliriz.
Kodlama seçimine ilişkin olarak, Briot, Hadjerest ve Pachet (2019), müzik için üretici modellerde yaygın kullanılan dört farklı seçeneği tartışır:
- Tek-Sıcak (One-Hot) Kodlama
- Çok-Sıcak (Many-Hot) Kodlama
- Çoklu Tek-Sıcak Kodlama
- Çoklu Çok-Sıcak Kodlama
Tek-sıcak kodlama, notalar ve akorlar gibi müzikal sembolleri, belirli bir nota veya akora karşılık gelen konumda bir adet bir (1) bulunan seyrek vektörler olarak temsil eder. Çok-sıcak kodlama ise çoklu notalardan oluşan akorları, her bir nota için karşılık gelen konumlara bir (1) yerleştirerek temsil eder.
Aşağıdaki diyagramda, notalar ve iki notalı akorlar olduğu durumda çok-sıcak ve tek-sıcak kodlamaları karşılaştırıyoruz. Çok-sıcak kodlamalarda C4 ve D4 notaları 128 öğeli vektörlerle temsil edilir. Her iki vektörde de C4 ve D4'e karşılık gelen konumlar dışında tüm konumlarda sıfır (0) bulunur; bu iki konumda bir (1) vardır. C4+D4 akoru — C4 ve D4 notalarının eşzamanlı çalınmasıyla oluşur — C4 ve D4 konumlarına bir (1) yerleştirilerek oluşturulur.

Tek-sıcak temsilde, tek notalar ve iki notalı akorlar 8256 öğeli vektörlerden oluşur. Her durumda vektör seyrektir ve yalnızca benzersiz bir nota veya akora karşılık gelen konumda bir (1) içerir. Çok-sıcak temsilin, bir çıktı için birden fazla sınıfın tahmin edilebildiği çoklu etiketli (multi-label) bir model gerektirdiğine dikkat edin. Bununla birlikte, girdi ve çıktı vektörlerinin boyutunu önemli ölçüde azaltır.
Literatürdeki çoğu model hâlâ tek-sıcak kodlama kullanır. Ancak, Briot, Hadjerest ve Pachet (2019), RBMc, RNN-RBM ve C-RBM sistemleri dâhil olmak üzere, sınırlı Boltzmann makinesi (RBM) mimarisi etrafında inşa edilen ve çok-sıcak kodlamadan yararlanan çeşitli müzik üretim modellerini tespit eder.
Çoklu tek-sıcak ve çoklu çok-sıcak kodlamalar, polifonik müzik — yani aynı anda birden fazla enstrümanın çaldığı müzik — durumuna yönelik olarak tek-sıcak ve çok-sıcak kodlamaların genelleştirilmiş hâlleridir. Biz monofonik müzikle sınırlı kalacağız ve bu seçenekleri eğitimde daha fazla tartışmayacağız.
Veri Toplama ve Hazırlama
Bir sonraki adımımız, üretici bir modeli eğitmekte kullanabileceğimiz bir veri kümesi toplamaktır. Kamu malı olan ve MIDI formatında 2124 parça içeren Mutopia Projesi verilerini kullanacağız. Web sitesi enstrümana göre bölümlere ayrıldığı için, bu eğitimde modelleri eğitmek üzere kullanacağımız tüm gitar parçalarını almak için nispeten basit bir Python betiği yazabiliriz. Bunu nasıl yapacağınıza genel bir bakış için Ek bölümüne bakın.
Gitar bölümündeki tüm .mid dosyalarını save_dir dizinine kaydettiğimizi varsayalım. Bir sonraki adım, her bir .mid dosyasını music21 içinde bir stream nesnesi olarak yüklemektir.
import os
from music21 import converter, pitch, interval
# Define save directory
save_dir = '../guitar/'
# Identify list of MIDI files
songList = os.listdir(save_dir)
# Create empty list for scores
originalScores = []
# Load and make list of stream objects
for song in songList:
score = converter.parse(save_dir+song)
originalScores.append(score)
Boyut indirgeme stratejimizin bir parçası olarak, kendimizi Mauro Giuliani tarafından bestelenen şarkılarla sınırlayacağız. Bunu yukarıdaki kod bloğunu aşağıdaki gibi değiştirerek yapabiliriz.
# Identify list of MIDI files
songList = os.listdir(save_dir)
songList = [song for song in songList if song.lower().find('giuliani')>-1]
Eğitime başlamadan önce dört ek işlem yapmamız gerekiyor:
- Polifonik müziği kaldırmak ve kalan
streamnesnelerine.chordify()uygulamak. - Nota, akor ve süre dizilerini çıkarmak.
- Notaları, akorları ve süreleri tamsayılara ve tersi yöne çevirecek bir eşlem (mapping) oluşturmak.
- Akışları (streams) modellere girdi olarak kullanılabilecek sabit uzunluklu dizilere bölmek.
Veri kümesinden polifonik müziği kaldırarak başlayalım. Bunu her bir stream nesnesini enstrümana göre bölümlere ayırıp yalnızca bir bölüm olup olmadığını — yani tek bir enstrümanın çalınıp çalınmadığını — kontrol ederek yapabiliriz. Aşağıdaki kod bloğunda bunu gerçekleştireceğiz.
from music21 import instrument
# Define function to test whether stream is monotonic
def monophonic(stream):
try:
length = len(instrument.partitionByInstrument(stream).parts)
except:
length = 0
return length == 1
# Merge notes into chords
originalScores = [song.chordify() for song in originalScores]
Aşağıdaki şekilde gösterildiği gibi, polifonik müziği kaldırmadan .chordify() uygularsak, farklı enstrümanlar tarafından çalınan notaları yanlışlıkla bir akorda birleştireceğimizi unutmayın.

Şimdi her bir stream'den notaları, akorları ve süreleri çıkarmamız gerekiyor. Önce üç boş liste tanımlayacağız: originalChords, originalDurations, originalKeys. Akış listesi üzerinde numaralandırma yapabiliriz. Her bir akış için, tüm notaları, akorları ve süreleri ve ayrıca eserin tonalitesini belirleyip depolayacağız.
from music21 import note, chord
# Define empty lists of lists
originalChords = [[] for _ in originalScores]
originalDurations = [[] for _ in originalScores]
originalKeys = []
# Extract notes, chords, durations, and keys
for i, song in enumerate(originalScores):
originalKeys.append(str(song.analyze('key')))
for element in song:
if isinstance(element, note.Note):
originalChords[i].append(element.pitch)
originalDurations[i].append(element.duration.quarterLength)
elif isinstance(element, chord.Chord):
originalChords[i].append('.'.join(str(n) for n in element.pitches))
originalDurations[i].append(element.duration.quarterLength)
print(str(i))
Aşağıdaki kod bloğunda, veri boyutunu daha da azaltmak için tonalite işareti Do majör olan müzikleri tutacağız; bu, veri kümemizde en yaygın olandır. Ayrıca, eğitimin geri kalanında hem akorlar hem de notalar için "akor" kelimesini kullanacağız.
# Create list of chords and durations from songs in C major
cMajorChords = [c for (c, k) in zip(originalChords, originalKeys) if (k == 'C major')]
cMajorDurations = [c for (c, k) in zip(originalDurations, originalKeys) if (k == 'C major')]
Bir sonraki adım, benzersiz akor ve süre kümelerini belirlemektir. Daha sonra bunları tamsayılara eşleyen sözlükler oluşturacağız. Benzersiz nota ve akor sayısını yazdırırsak, yaptığımız boyut indirgeme adımlarının sayıyı yönetilebilir bir düzey olan 267'ye düşürdüğünü görebiliriz.
# Map unique chords to integers
uniqueChords = np.unique([i for s in originalChords for i in s])
chordToInt = dict(zip(uniqueChords, list(range(0, len(uniqueChords)))))
# Map unique durations to integers
uniqueDurations = np.unique([i for s in originalDurations for i in s])
durationToInt = dict(zip(uniqueDurations, list(range(0, len(uniqueDurations)))))
# Print number of unique notes and chords
print(len(uniqueChords))
267
# Print number of unique durations
print(len(uniqueDurations))
9
Modelimizi eğitip tahminler yaptıktan sonra, tamsayı tahminlerimizi tekrar nota, akor ve sürelere çevirmek isteyeceğiz. Aşağıda chordToInt ve durationToInt sözlüklerini tersine çeviriyoruz.
# Invert chord and duration dictionaries
intToChord = {i: c for c, i in chordToInt.items()}
intToDuration = {i: c for c, i in durationToInt.items()}
Son olarak, 32 ardışık nota ve akordan ve bunlara karşılık gelen sürelerden oluşan eğitim dizilerini tanımlayabiliriz. Önce, özellik ve hedef kümesinin aynı olduğu otoenkoder (autoencoder) tipi modeller için bunu yapacağız.
# Define sequence length
sequenceLength = 32
# Define empty arrays for train data
trainChords = []
trainDurations = []
# Construct training sequences for chords and durations
for s in range(len(cMajorChords)):
chordList = [chordToInt[c] for c in cMajorChords[s]]
durationList = [durationToInt[d] for d in cMajorDurations[s]]
for i in range(len(chordList) - sequenceLength):
trainChords.append(chordList[i:i+sequenceLength])
trainDurations.append(durationList[i:i+sequenceLength])
Müzik Üretimi
Bu bölümde, derlediğimiz ve hazırladığımız veri kümesini kullanarak müzik üreteceğiz. En basitinden başlayarak üç farklı müzik üretim modelini ele alacağız:
- Otoenkoder
- Değişken Otoenkoder (VAE)
- Uzun Kısa Süreli Bellek Modeli (LSTM)
Otoenkoderler
Bir otoenkoder, dikey olarak istiflenmiş ve bir gizil (latent) vektör ile birbirine bağlanmış iki ağdan oluşur. Otoenkodere girilen veriler önce genellikle bir veya daha fazla yoğun (dense) katmandan oluşan bir enkoder modeline verilir. Enkoder modelindeki son katmana gizil vektör denir. Bu, enkoderde çıkarılan özelliklerin az sayıda gizil özellikle sıkıştırılmasını zorlayan bir dar boğazdır.
Gizil vektör, veriyi yukarı örnekleyen veya sıkıştırmayı açan bir dekoder modeline bağlanır. Her bir ardışık dekoder katmanındaki düğüm sayısı artar. Son çıktı katmanı, enkoder modelinin giriş katmanıyla aynı boyutlara sahiptir.
Bir karar sınırı belirlemeye çalışan ayrıştırıcı modellerin aksine, otoenkoderler giriş verisini — bu durumda bir şarkıyı — mümkün olduğunca doğru şekilde, gizil vektörün boyutunun getirdiği kısıtlara tabi olarak yeniden oluşturmaya (rekonstrüksiyon) yönelik olarak eğitilir. Bu nedenle, otoenkoderler girdileri doğrudan hedef olarak kullanır.
İlk üretici müzik modelimizi oluşturmak için bir otoenkoder kullanacağız. İşleri mümkün olduğunca basit tutmak için, DeepBach modelinin bir sadeleştirmesi olarak Briot, Hadjerest ve Pachet (2019)'da tanıtılan MiniBach mimarisini benimseyeceğiz. Otoenkoder tabanlı bir başka müzik üretim örneği için DeepHear sistemine bakın.
Girdi verimiz, oluşturduğumuz hâliyle, notaları ve akorları temsil eden tek-sıcak kodlanmış vektörlerden oluşur. Şimdilik süreleri yok sayacağız. MiniBach mimarisini takip ederek, giriş vektörünü düzleştirerek başlayacağız.
# Convert to one-hot encoding and swap chord and sequence dimensions
trainChords = tf.keras.utils.to_categorical(trainChords).transpose(0,2,1)
# Convert data to numpy array of type float
trainChords = np.array(trainChords, np.float)
# Flatten sequence of chords into single dimension
trainChordsFlat = trainChords.reshape(nSamples, nChordsSequence)
Sonraki adımda örnek sayısını nSamples, akor ve nota sayısını nChords ve giriş boyutunu inputDim tanımlıyoruz. Ayrıca gizil özellik sayısı latentDim'i 2 olarak ayarlıyoruz. Düşük sayıda gizil boyut, performansı düşürse de gizil uzay görselleştirmelerini kolayca üretmemizi sağlar.
# Define number of samples, chords and notes, and input dimension
nSamples = trainChords.shape[0]
nChords = trainChords.shape[1]
inputDim = nChords * sequenceLength
# Set number of latent features
latentDim = 2
Şimdi modelin mimarisini tanımlıyoruz; enkoder ağı için giriş katmanı encoderInput ve dekoder ağı için giriş katmanı latent ile başlıyoruz. latent katmanının aynı zamanda enkoder ağının çıktısı olduğuna dikkat edin. Ardından, girdileri gizil vektöre bağlayan bir yoğun katman encoded ve gizil vektörü çıktı vektörüne bağlayan başka bir yoğun katman decoded tanımlıyoruz.
Son olarak bir dekoder modeli decoder ve otoenkoderimiz autoencoder'ı tanımlıyoruz. .Model()'in inputs ve outputs argümanları olduğuna dikkat edin. decoder modeli için, girdi olarak latent vektörünü ve output olarak decoded ağını geçiriyoruz. Tam otoenkoder için, girdi olarak encoderInput'u kullanıyor ve enkoder ağının çıktısı encoded ile değerlendirilmiş decoder modelini girdi olarak veriyoruz.
# Define encoder input shape
encoderInput = tf.keras.layers.Input(shape = (inputDim))
# Define decoder input shape
latent = tf.keras.layers.Input(shape = (latentDim))
# Define dense encoding layer connecting input to latent vector
encoded = tf.keras.layers.Dense(latentDim, activation = 'tanh')(encoderInput)
# Define dense decoding layer connecting latent vector to output
decoded = tf.keras.layers.Dense(inputDim, activation = 'sigmoid')(latent)
# Define the encoder and decoder models
encoder = tf.keras.Model(encoderInput, encoded)
decoder = tf.keras.Model(latent, decoded)
# Define autoencoder model
autoencoder = tf.keras.Model(encoderInput, decoder(encoded))
Genel bir otoenkoder mimarisi aşağıdaki şekilde gösterilmiştir. Ağın üst yarısı enkoder, alt yarısı ise dekoderdir. İkisi, gizil vektörle birbirine bağlanır.

decoder ve autoencoder için mimariyi tanımladıktan sonra geriye yalnızca modeli derlemek ve .fit() yöntemiyle eğitmek kalır. Dikkat edilmesi gereken iki önemli ayrıntı var:
- Girdi değerlerimiz 0 ve 1'lerden oluşacak; tahmin edilen değerlerimiz ise 0 ile 1 arasında reel sayılar olacak.
binary_crossentropykayıp fonksiyonunu kullanabiliriz. - Hem girdi hem de hedef değerleri aynıdır:
trainChordsFlat.
# Compile autoencoder model
autoencoder.compile(loss = 'binary_crossentropy', learning_rate = 0.01, optimizer='rmsprop')
# Train autoencoder
autoencoder.fit(trainChordsFlat, trainChordsFlat, epochs = 500)
Son adım müzik üretmektir. Dizi uzunluğunu 32 olarak ayarladığımız için, autoencoder giriş olarak 32 akor ve notayı alacak ve 32 akor ve notadan oluşan sabit uzunlukta bir "şarkı" üretecektir. autoencoder, çıktıları girdilere yüksek benzerlikte üretmek üzere eğitilir; ancak amacımız yeni müzik üretmektir. Bunu yapmak için, otoenkoder modelinin bir alt ağı olarak tanımladığımız decoder modeline rastgele üretilmiş bir gizil vektör geçireceğiz.
# Generate chords from randomly generated latent vector
generatedChords = decoder(np.random.normal(size=(1,latentDim))).numpy().reshape(nChords, sequenceLength).argmax(0)
Otoenkoderin çıktısını, orijinal girdimizle aynı boyutlara sahip bir diziye yeniden biçimlendirdiğimize dikkat edin. Ardından dizinin ilk boyutu üzerinde argmax() aldık. Bu, bir akora karşılık gelen bir tamsayı değeri döndürür; bunu aşağıdaki kod bloğunda belirleyeceğiz.
# Identify chord sequence from integer sequence
chordSequence = [intToChord[c] for c in generatedChords]
Son olarak, music21 kullanarak bir stream nesnesi oluşturabilir, enstrümanı gitar olarak ayarlayabilir, modelimizin ürettiği akor dizisini ekleyebilir ve her şeyi bir MIDI dosyası olarak dışa aktarabiliriz.
# Set location to save generated music
generated_dir = '../generated/'
# Generate stream with guitar as instrument
generatedStream = stream.Stream()
generatedStream.append(instrument.Guitar())
# Append notes and chords to stream object
for j in range(len(chordSequence)):
try:
generatedStream.append(note.Note(chordSequence[j].replace('.', ' ')))
except:
generatedStream.append(chord.Chord(chordSequence[j].replace('.', ' ')))
generatedStream.write('midi', fp=generated_dir+'autoencoder.mid')
Aşağıdaki gif, bir otoenkoder için eğitim sürecinin bir bölümünü gösterir. Her adımda üretilen partisyon, belirli bir gizil durum vektörüyle ilişkili yeniden yapılandırmadır. Her kare 5 eğitim dönemi (epoch) temsil eder.

Eğitim sırasında gizil uzaydaki hareketi de görselleştirebiliriz; bunu aşağıdaki gifte yapıyoruz. Gizil uzayın dar olduğunu ve eğitim dönemleri boyunca sürüklendiğini (drift) fark edin. Bu, yüksek kaliteli rastgele bir gizil durum çizimini zorlaştıracak ve daha yüksek boyutlu bir gizil vektör kullandığımızda oldukça sorunlu olacaktır. Bir sonraki bölümde, bunu değişken otoenkoder (VAE) kullanarak nasıl çözebileceğimizi göstereceğiz.

Son olarak, otoenkoderimizin ses çıktısını inceleyebiliriz. Eğitim setimizden rastgele seçilen orijinal şarkının akor dizisi önce verilmiştir. Ardından, orijinal şarkının akor dizisiyle ilişkili gizil vektörü kullanarak otoenkoderden bir akor dizisi üretiyor ve alttaki şarkıyı elde ediyoruz.
Örnek: Orijinal Şarkı
Örnek: Üretilen Şarkı
Değişken Otoenkoder
Otoenkoderler gürültü giderme, sıkıştırma ve açma görevlerine uygun olsa da, üretici modeller olarak iyi performans göstermezler. Kingma ve Welling (2017) tarafından tanıtılan değişken otoenkoderler (VAE'ler), otoenkoderlerin üretici modeller olarak zayıflıklarını gidermek üzere tasarlanmıştır ve müzik üretimi için en yaygın kullanılan yöntemlerden biridir. Briot, Hadjerest ve Pachet (2019), VAE'ler üzerine inşa edilen dört farklı müzik üretim sistemi belirler: MusicVAE, VRAE, VRASH ve GSLR-VAE. VAE'ler otoenkoderlere iki açıdan üstünlük sağlar:
- Her bir girdi kümesini gizil uzayda bir noktaya eşlemek yerine, değişken otoenkoder her bir girdi kümesini bir normal dağılıma — ortalama ve varyans ile karakterize edilen — eşler.
- Gizil uzayın herhangi bir ortalama ve varyansa sahip olmasına izin vermek yerine, değişken otoenkoder, ortalama ve varyansın doğal logaritmasının 0'a yakın olmasını zorlar.
İlk değişikliği, otoenkodardaki gizil durum vektörünü üç katmanla değiştirerek yapabiliriz:
- Bir ortalama katmanı
- Bir (log) varyans katmanı
- Bir örnekleme katmanı
Her bir girdi kümesi, normal dağılımı tanımlamak için yeterli olan tek bir ortalama ve varyansın doğal logaritmasına eşlenir. Model daha sonra örnekleme katmanında bu dağılımdan rastgele noktalar çeker.
Aşağıdaki kod bloğu, bir örnekleme katmanı oluşturmak için kullanılabilecek bir işlev tanımlar. Girdi olarak bir ortalama ve log varyans alır. Ardından tensorflow'un random alt modülünü kullanarak, hem ortalaması hem de log varyansı 0 olan standart normal dağılımdan epsilon adlı bir nokta tensörü çeker. Sonraki adımda bu çekilişleri, mean ve logVar parametreleriyle karakterize edilen dağılımlardan çekilişlere dönüştürmek isteriz. Bunu yapmak için önce epsilon'u ilgili dağılımların standart sapmalarıyla ölçeklendirmemiz gerekir. Bu standart sapmaları, logVar'ı 2'ye bölüp üstelini alarak hesaplayabiliriz. Ardından dağılım ortalamalarını (mean) ekleriz.
# Define function to generate sampling layer.
def sampling(params):
mean, logVar = params
batchSize = tf.shape(mean)[0]
latentDim = tf.shape(mean)[1]
epsilon = tf.random.normal(shape=(batchSize, latentDim))
return mean + tf.exp(logVar / 2.0) * epsilon
Artık mean, logVar ve sampling katmanlarını içerecek şekilde otoenkoder modelini değiştirebiliriz. encoded katmanının artık iki girdi aldığına ve encoder katmanının üç çıktı ürettiğine dikkat edin.
# Define the input and latent layer shapes.
encoderInput = tf.keras.layers.Input(shape = (inputDim))
latent = tf.keras.layers.Input(shape = (latentDim))
# Add mean and log variance layers.
mean = tf.keras.layers.Dense(latentDim)(encoderInput)
logVar = tf.keras.layers.Dense(latentDim)(encoderInput)
# Add sampling layer.
encoded = tf.keras.layers.Lambda(sampling, output_shape=(latentDim,))([mean, logVar])
# Define decoder layer.
decoded = tf.keras.layers.Dense(inputDim, activation = 'sigmoid')(latent)
# Define encoder and decoder layers.
encoder = tf.keras.Model(encoderInput, [mean, logVar, encoded])
decoder = tf.keras.Model(latent, decoded)
# Define variational autoencoder model.
vae = tf.keras.Model(encoderInput, decoder(encoded))
Otoenkoder modelinde olduğu gibi, gizil durum encoded katmanının çıktısıdır. Ancak önemli bir fark vardır: her bir girdi kümesini tek bir gizil duruma eşlemek yerine, sampling katmanını kullanarak bunları mean ve logVar parametrelerine sahip bir normal dağılımdan rastgele değerlere eşleriz. Bu, aynı girdinin bir gizil durum dağılımıyla ilişkili olacağı anlamına gelir.
Son adım, modelin seçtiği mean ve logVar değerlerine disiplin uygulamak için kayıp fonksiyonunu ayarlamaktır. Özellikle, mean ve logVar katmanları tarafından tanımlanan her dağılımla, ortalaması ve log varyansı 0 olan standart normal dağılım arasındaki Kullback-Leibler (KL) ayrışımını hesaplayacağız. Ortalama ve log varyanslarımız 0'dan ne kadar uzaksa, KL ayrışımı kayıp bileşeni o kadar yüksek olacaktır.
Doğal bir soru, her dağılımı neden standart normale yakın olmaya zorladığımızdır. Bunu yapmamızın sebebi, gizil durum örnekleri çekip bunları müzik üretmek için kullanmak istememizdir. KL ayrışımı cezasını kayıp fonksiyonuna uygularsak, yüksek kaliteli müzik parçaları üretmesi muhtemel gizil durumları elde etmek için standart normal dağılımdan bağımsız çekilişler kullanmamıza olanak tanır.
Aşağıdaki kod bloğunda, kayıp fonksiyonunu KL ayrışımı terimini içerecek şekilde nasıl değiştireceğimizi gösteriyoruz. Kayıp fonksiyonunun ilk bileşeni, vae modelinin girdileri ve çıktıları üzerinde hesaplanan ikili çapraz entropi kaybıdır; bunu otoenkoder için de kullanmıştık. Bu, orijinal şarkıyı yeniden oluşturamamayı cezalandırdığı için "rekonstrüksiyon kaybı" olarak adlandırılır. KL terimi, gizil uzay dağılımlarıyla standart normal dağılım arasındaki ortalama KL ayrışımıdır. İkisini birleştirerek toplam kaybı (vaeLoss) hesaplar, bunu modele ekler, derler ve ardından eğitiriz.
# Define the reconstruction loss.
reconstructionLoss = tf.keras.losses.binary_crossentropy(vae.inputs[0], vae.outputs[0])
# Define the Kullback-Liebler divergence term.
klLoss = -0.5 * tf.reduce_mean(1 + logVar - tf.square(mean) - tf.exp(logVar), axis = -1)
# Combine the reconstruction and KL loss terms.
vaeLoss = reconstructionLoss + klLoss
# Add the loss to the model.
vae.add_loss(vaeLoss)
# Compile the model.
vae.compile(batch_size = batchSize, learning_rate = 0.01, optimizer='rmsprop')
# Train the model.
vae.fit(trainChordsFlat, epochs = 500)
Önceki bölümde, otoenkoder için gizil uzayın iyi davranış sergilemediğini göstermiştik. Bir VAE'nin amaçlarından biri, üretici bağlamlarda bu tasarım kusurunu gidermektir. Aşağıdaki şekil, VAE için gizil uzayı gösterir. Her iki özellik boyunca önemli bir yayılım olduğunu görebiliriz. Ayrıca her ikisi de 0 etrafında merkezlidir ve eğitim dönemleri boyunca sürüklenmez. Bu, rastgele yüksek kaliteli bir gizil durumu örneklemeyi çok daha kolaylaştırır.

Son olarak müzik üretimine geçiyoruz. Standart normal dağılımdan rastgele bir gizil vektör çekip dekodere veriyor, çıktıyı yeniden şekillendiriyor ve her sütunun argmax'ını alıyoruz. Bu, notalara ve akorlara karşılık gelen 32 tamsayıdan oluşan bir dizi döndürür; bunları intToChord sözlüğünü kullanarak tanımlarız. Ardından bir stream nesnesi tanımlar, enstrüman olarak gitarı ekler, nota ve akor dizisini stream'e ekler ve MIDI dosyası olarak dışa aktarırız.
# Generate integers from randomly drawn latent state.
generatedChords = decoder(np.random.normal(size=(1,latentDim))).numpy().reshape(nChords, sequenceLength).argmax(0)
# Identify chords associated with integers.
chordSequence = [intToChord[c] for c in generatedChords]
# Initialize stream with guitar as instrument.
generatedStream = stream.Stream()
generatedStream.append(instrument.Guitar())
# Append notes and chords and export to MIDI file
for j in range(len(chordSequence)):
try:
generatedStream.append(note.Note(chordSequence[j].replace('.', ' ')))
except:
generatedStream.append(chord.Chord(chordSequence[j].replace('.', ' ')))
generatedStream.write('midi', fp=generated_dir+'vae.mid')
Aşağıdaki üç şarkı model tarafından üretilmiştir. İlk iki şarkı, eğitim setindeki akor dizileriyle ilişkili gizil durumlardan türetilmiştir. Son şarkı ise 1 numaralı ve 2 numaralı şarkıların gizil durumlarının lineer bir birleşimidir. VAE, gizil uzayı iyi davranış sergileyecek şekilde zorladığı için, gizil durumların lineer bir birleşiminin yüksek kaliteli bir gizil durum vereceğinden makul ölçüde emin olabiliriz.
Örnek: Üretilen Şarkı #1
Örnek: Üretilen Şarkı #2
Örnek: Şarkı #1 ve #2'nin Doğrusal Birleşimi
Otoenkoder ve VAE tabanlı müziklerden gözlemleyebileceğimiz bir şey, güçlü bir melodiden yoksun olmalarıdır. Bunu modele daha fazla katman ekleyerek ve eğitim setinin boyutunu artırarak iyileştirebiliriz. Alternatif olarak, bir sonraki bölümde yapacağımız gibi, sıralı verileri işlemeye yönelik tasarlanmış bir modelden yararlanabiliriz.
Uzun Kısa Süreli Bellek (LSTM)
Ele alacağımız son model, uzun kısa süreli bellek (LSTM) modelidir. Bu, sönümlenen gradyan problemine engel olmak için değiştirilmiş bir tür yinelemeli sinir ağıdır (RNN). Briot, Hadjerest ve Pachet (2019), müzik üretimi amacıyla en yaygın kullanılanların yinelemeli modeller olduğunu bulmuştur. Aralarında BachBot, DeepBach, Performance-RNN ve Hexahedria'nın da bulunduğu 20'den fazla müzik üretim sistemi belirlemişlerdir.
LSTM modeli kullanmanın, otoenkoder tabanlı bir modele göre iki faydası vardır:
- Sıralı verileri işlemek üzere tasarlanmışlardır.
- Herhangi bir uzunlukta müzikal diziler üretebilirler.
Girdileri hedef olarak kullanan otoenkoderler ve VAE'lerin aksine, LSTM tabanlı üretici müzik modelleri, dizideki bir sonraki nota veya akora ilişkin tahmin yapmak üzere eğitilir. Bu nedenle, eğitim ve hedef kümelerimizi yeniden oluşturmamız gerekecek; bunu aşağıdaki kod bloğunda yapacağız. LSTM kullanımı, eğitmemiz gereken parametre sayısını da azaltacağından, modellere süreleri de ekleyeceğiz.
# Set sequence length
sequenceLength = 32
# Define empty array for train data
trainChords = []
trainDurations = []
targetChords = []
targetDurations = []
# Construct train and target sequences for chords and durations
for s in range(len(cMajorChords)):
chordList = [chordToInt[c] for c in cMajorChords[s]]
durationList = [durationToInt[d] for d in cMajorDurations[s]]
for i in range(len(chordList) - sequenceLength):
trainChords.append(chordList[i:i+sequenceLength])
trainDurations.append(durationList[i:i+sequenceLength])
targetChords.append(chordList[i+1])
targetDurations.append(durationList[i+1])
trainChords ve trainDurations'ın, eğitimin önceki bölümünde tanımlandıklarıyla aynı şekilde tanımlandıklarına dikkat edin. Yalnızca targetChords ve targetDurations'ı ekledik; bunlar, eğitim dizisindeki akor ve süreleri izleyen akor ve süreden oluşur. Sonraki adımda numune, akor ve süre sayısını ve ayrıca giriş boyutunu ayarlıyoruz. Akorlar ve süreler için ayrı gömme (embedding) katmanları da kullanacağız.
# Define number of samples, notes and chords, and durations
nSamples = trainChords.shape[0]
nChords = trainChords.shape[1]
nDurations = trainDurations.shape[1]
# Set the input dimension
inputDim = nChords * sequenceLength
# Set the embedding layer dimension
embedDim = 64
Bir sonraki adım, model mimarisini tanımlamaktır. Hem akorları hem de süreleri tahmin edebilmek için iki girişli ve iki çıkışlı bir model kullanacağız. Önce giriş katmanlarını chordInput ve durationInput tanımlıyor, ardından bu katmanları gömme katmanlarına (chordEmbedding ve durationEmbedding) geçiriyoruz. Bunlar, her bir akor veya süreyle 64 öğeli bir vektörü ilişkilendiren arama tablolarıdır. Gömme katmanları, girdilerin tek-sıcak kodlandığı seyrek vektör temsili yerine, ilişkili nota ve sürelerin birbirine daha yakın değerlere sahip olduğu yoğun bir temsile geçmemizi sağlar.
İki gömme katmanının çıktıları mergeLayer'da birleştirilir. Daha sonra bu katmanın çıktısını iki LSTM katmanına geçiririz. İlk katman return_sequences parametresini True olarak ayarlar. Bu parametreyi varsayılan olarak False bırakacak olursak, LSTM hücreleri ara sıralı durumları değil yalnızca son durumları döndürecektir. Son çalışmalar, ara durumların kullanılmasının performansı artırabildiğini göstermiştir. Bu genellikle bir dikkat (attention) katmanı bağlamında yapılır. Son olarak, LSTM katmanının çıktısını yoğun bir katmana, ardından iki çıktı katmanına (chordOutput ve durationOutput) geçiririz. Daha sonra iki giriş ve iki çıktı katmanı belirterek bir keras fonksiyonel API modeli tanımlarız.
# Define input layers
chordInput = tf.keras.layers.Input(shape = (None,))
durationInput = tf.keras.layers.Input(shape = (None,))
# Define embedding layers
chordEmbedding = tf.keras.layers.Embedding(nChords, embedDim, input_length = sequenceLength)(chordInput)
durationEmbedding = tf.keras.layers.Embedding(nDurations, embedDim, input_length = sequenceLength)(durationInput)
# Merge embedding layers using a concatenation layer
mergeLayer = tf.keras.layers.Concatenate(axis=1)([chordEmbedding, durationEmbedding])
# Define LSTM layer
lstmLayer = tf.keras.layers.LSTM(512, return_sequences=True)(mergeLayer)
# Define dense layer
denseLayer = tf.keras.layers.Dense(256)(lstmLayer)
# Define output layers
chordOutput = tf.keras.layers.Dense(nChords, activation = 'softmax')(denseLayer)
durationOutput = tf.keras.layers.Dense(nDurations, activation = 'softmax')(denseLayer)
# Define model
lstm = tf.keras.Model(inputs = [chordInput, durationInput], outputs = [chordOutput, durationOutput])
Model tanımlandıktan sonra derleyip eğitime başlayabiliriz. Burada işleri mümkün olduğunca basit tutmaya çalıştık; ancak birçok uygulamada düzenleme (regularization) katmanları ile ek yoğun ve LSTM katmanları eklemek mantıklı olacaktır. Modelin iki giriş ve iki çıkış katmanına sahip olduğuna, aşağıdaki diyagramda da gösterildiği gibi, dikkat edin. Birden çok giriş ve çıkışa sahip modeller hakkında daha fazla bilgiyi Introduction to TensorFlow in Python kursunda bulabilirsiniz.

# Compile the model
lstm.compile(loss='categorical_crossentropy', optimizer='rmsprop')
# Train the model
lstm.fit([trainChords, trainDurations], [targetChords, targetDurations],
epochs=500, batch_size=64)
Eğitim süreci tamamlandığında LSTM modelini kullanarak yeni şarkılar üretebiliriz. Bunu, ilk tahminlerimizi yapmamızı sağlayacak 32 akor ve sürelik bir başlangıç dizisi vererek yapacağız. Ardından bu tahminleri akor ve süre serisine ekleyerek, güncellenmiş serinin son 32 akor ve süresine dayanarak bir başka tahmin yapmamıza imkân vereceğiz. LSTM modelinin bir faydası, bunu sonsuza dek yineleyebilmemiz, dolayısıyla keyfi uzunlukta şarkılar üretebilmemizdir.
# Define initial chord and duration sequences
initialChords = np.expand_dims(trainChords[0,:].copy(), 0)
initialDurations = np.expand_dims(trainDurations[0,:].copy(), 0)
# Define function to predict chords and durations
def predictChords(chordSequence, durationSequence):
predictedChords, predictedDurations = model.predict(model.predict([chordSequence, durationSequence]))
return np.argmax(predictedChords), np.argmax(predictedDurations)
# Define empty lists for generated chords and durations
newChords, newDurations = [], []
# Generate chords and durations using 500 rounds of prediction
for j in range(500):
newChord, newDuration = predictChords(initialChords, initialDurations)
newChords.append(newChord)
newDurations.append(newDuration)
initialChords[0][:-1] = initialChords[0][1:]
initialChords[0][-1] = newChord
initialDurations[0][:-1] = initialDurations[0][1:]
initialDurations[0][-1] = newDuration
Son adım, music21 kullanarak üretilen müziği bir MIDI dosyasına aktarmaktır. Kodu, otoenkoder tabanlı modellerde kullandığımıza neredeyse aynıdır; ancak önemli bir farkla: artık stream nesnesine hem akorları hem de süreleri ekliyoruz.
# Create stream object and add guitar as instrument
generatedStream = stream.Stream()
generatedStream.append(instrument.Guitar())
# Add notes and durations to stream
for j in range(len(chordSequence)):
try:
generatedStream.append(note.Note(chordSequence[j].replace('.', ' '), quarterType = durationSequence[j]))
except:
generatedStream.append(chord.Chord(chordSequence[j].replace('.', ' '), quarterType = durationSequence[j]))
# Export as MIDI file
generatedStream.write('midi', fp=generated_dir+'lstm.mid')
Otoenkoder ve VAE modellerinde melodiler üretmekte zorlandık ve yalnızca sabit uzunlukta şarkılar çıktısı alabildik. LSTM modeliyle, yukarıdaki kod bloğunda yaptığımız gibi tahminleri yinelerken keyfi uzunlukta çıktı dizileri üretebiliriz. Ayrıca daha iyi melodiler üretmek de nispeten daha kolay olacaktır. Aşağıdaki örneklerde, her biri eğitim setimizdeki bir örnekten alınan başlangıç akor ve süre dizileriyle başlatılmış bu iki özelliği göreceğiz.
Örnek: Üretilen Şarkı #1
Örnek: Üretilen Şarkı #2
Örnek: Üretilen Şarkı #3
Ek Konular
Veri
Müzik için makine öğrenmesi modellerinin geliştirilmesindeki büyük zorluklardan biri, veri kümelerinin erişilebilirliğidir; zira müzik genellikle serbestçe ve yasal olarak indirilebilir değildir. Nitekim bu, literatürün görüntü sınıflandırma için MNIST gibi standart bir pratik veri kümesi üzerinde ortaklaşmasını da engellemiştir. Yine de, çok sayıda şarkı için ham ses, sembolik notasyon veya türetilmiş müzikal özellikler sağlayan birkaç proje vardır. Bu projelerin kısmi bir listesini aşağıda sunuyoruz.
- Mutopia Projesi. MIDI, PDF ve LilyPond formatlarında kamu malı ve Creative Commons müzik arşivi. Şu anda 2000'den fazla şarkı içerir ve enstrüman, besteci, stil ve koleksiyona göre düzenlenmiştir.
- Million Song Dataset. Popüler müzik parçalarına ilişkin 300 GB'tan fazla ses özelliği ve meta verisi içerir; ancak ses parçaları yoktur.
- Free Music Archive. 100.000'den fazla parça için serbestçe ve yasal olarak indirilebilen ses dosyaları, tür etiketleri ve özellikler içerir.
Örnekleme vs. Determinizm
LSTM modeliyle müzik üretirken, akor ve süre seçiminde deterministik bir yaklaşım kullandık. Model, 267 akorun ve 9 sürenin her biri için olasılıkları tahmin etti ve biz de en yüksek olasılığa sahip akor ve süreleri seçtik. Benzer şekilde, otoenkoder ve değişken otoenkoder modellerinde de en yüksek çıktıya sahip akorları seçtik.
Bu yaklaşıma bir alternatif, en yüksek olasılıklı akor ve süreleri seçmek yerine, çıktı dağılımından örnekleme yapmaktır. Bu, müzik üretim sürecini stokastik hâle getirir; deterministik olmaktan çıkarır. Bu, aynı gizil vektöre sahip otoenkoder veya VAE modellerinin her çalıştırmada farklı şarkılar üreteceği anlamına gelir. Benzer şekilde, LSTM modeline verilen aynı giriş dizisi de farklı şarkılar üretebilir.
Aşağıdaki kod bloğu, predictChords() işlevini değiştirerek akor ve süre üretimini stokastik hâle nasıl getirebileceğimizi gösterir. argmax() işlemini, olasılıkları kullanarak akor ve süre vektörlerinden rastgele seçimler yapan bir örnekleme işlemiyle değiştirdiğimize dikkat edin.
# Define function to generate chords and durations stochastically
def predictChords(chordSequence, durationSequence):
predictedChords, predictedDurations = model.predict([chordSequence, durationSequence])
return np.random.choice(range(nChords), p = predictedChords), np.random.choice(range(nDurations), p = predictedDurations)
Stokastik ve deterministik yaklaşımların müzik üretimindeki faydalarına ilişkin geniş bir tartışma için, Briot, Hadjerest ve Pachet (2019) çalışmasının 6.6 bölümüne bakın.
Gömme Temsilleri (Embeddings)
LSTM modelini eğitirken, her bir giriş akorunu ve süresini yoğun, 64 öğeli bir vektöre eşleyen bir gömme katmanı kullandık. Gömme katmanları, girdi olarak seyrek, yüksek boyutlu vektörler kullanmaktan kaçınmamızı sağlar. Bunun yerine, farklı akorlar arasındaki ilişkileri belirlememize olanak tanıyan düşük boyutlu bir temsil kullanırız.
Birçok durumda, bir gömme katmanını eğitmek mantıklı olmayabilir. Bunun nedeni, müzik üretimi problemlerinde sıklıkla yeterince büyük eğitim kümelerine sahip olamayacak olmamızdır. Metin sınıflandırma ve üretim problemlerinde sıkça yaptığımız gibi, önceden eğitilmiş gömmeler veya denetimsiz öğrenmeyle üretilmiş gömmeler kullanmak, aksi takdirde mümkün olmayacak durumlarda modeli eğitmemize olanak tanır.
Denetimsiz öğrenme yaklaşımıyla gömme temsillerinin nasıl oluşturulacağına ilişkin bir genel bakış için bkz. Chuan, Agres ve Herremans (2018).
Üretici Çekişmeli Ağlar
Müzik üretimi için otoenkoder, VAE ve LSTM modellerini seçenek olarak tartıştık. Başka bir seçenek de, diğer alanlarda üretici modellemede başarılı olmuş Üretici Çekişmeli Ağlar (GAN)dır. Bir GAN, bir ayrıştırıcı ağı bir üretici ağla birleştirerek çalışır. Üretici ağ, LSTM ve otoenkoder modellerimizin ürettiği akor dizileri gibi örnekler oluşturur. Bu örnekler, gerçek örneklerle — yani gerçek şarkılardan alınmış akor dizileriyle — birlikte bir eğitim setinde birleştirilir. Ayrıştırıcı model, örnekleri gerçek veya üretici tarafından oluşturulmuş olarak sınıflandırmaya çalışır. Üretici ise ayrıştırıcı modeli, oluşturduğu örnekleri gerçek şarkılar olarak yanlış sınıflandırmaya kandırmaya çalışır. Model, ne ayrıştırıcı ne de üreticinin daha fazla avantaj elde edemeyeceği dengeli bir evrimsel dengeye ulaşıncaya kadar eğitilir.
Müzik üretimi görevlerinde GAN uygulamakla ilgilenenler aşağıdaki kaynaklara bakabilir:
-
MuseGAN, GAN'lar kullanarak polifonik müzik üretimine yönelik bir projedir. Önceden eğitilmiş modeller ve pianoroll formatında temizlenmiş ve hazırlanmış 174.154 şarkıyla birlikte gelir.
-
Magenta, müzik üretimi dâhil yaratıcı görevlere üretici modelleme uygulamayı amaçlayan açık kaynaklı bir projedir. Magenta'nın sunduğu araç setini bir Colab not defteri üzerinden keşfedebilirsiniz.
Kaynaklar
TensorFlow 2.0 hakkında daha fazla bilgi edinmek isteyenler, DataCamp'teki Introduction to TensorFlow in Python kursuna bakabilir. Derin öğrenmeyle müzik üretimi yöntemlerine ilişkin yakın tarihli ve kapsamlı bir inceleme için Briot, Hadjerest ve Pachet (2019)'e bakın. Derin öğrenme modelleriyle müzik üretimi üzerine ek eğitimler için 1, 2, 3, 4 ve üretici modellere eksiksiz bir ders kitabı yaklaşımı sunan 5 numaralı kaynaklara bakın.
Ek
Sürekli Temsil
WAV dosyaları, sıkıştırılmamış ham ses sinyali içerdiklerinden en yaygın sürekli temsil biçimidir. Örneğin mp3 dosyaları, ses sinyali hakkında bilgi atarak dosya boyutunu küçültmeleri bakımından farklıdır.
Aşağıdaki kod bloğu, Mozart tarafından bestelenmiş bir müzik parçasını içeren bir WAV dosyasından örnekleme hızını (rate) ve ham ses sinyalini (signal) nasıl çıkarabileceğimizi gösterir. Örnekleme hızı, ses sinyalinin her saniye kaç kez örneklendiğidir. Bizim durumumuzda bu değer 44100'dür.
from scipy.io import wavfile
# Define data directory
data_dir = '../audio/'
# Extract sample rate and signal
rate, signal = wavfile.read(data_dir+'mozart.wav')
# Bound signal between -1 and 1
normalized_signal = signal / abs(max(signal))
Ham ses sinyali signal, S x C boyutlu bir tensördür; burada S örnek sayısı, C ise kanal sayısıdır. Basitlik adına, C = 1 durumunu ele alacağız. Aşağıdaki grafik, signal'ın üç saniyelik bir aralığını gösterir.

Genel olarak, ham ses sinyallerini kullanarak bir modeli eğitmek, ayrık bir temsile kıyasla oldukça daha zorludur. Yakın tarihli bir anket, literatürdeki 32 farklı müzik üretim yöntemini inceledi. Bunlardan yalnızca üçü sürekli temsilleri kullandı:
Tonalite Transpozisyonu
Boyut indirgeme veya veri artırma amacıyla kullanılabilecek yöntemlerden biri tonalite transpozisyonudur. Bu, her notanın perdesini sabit bir aralıkla kaydırmayı içerir. Örneğin, bir şarkı C3 perdesinin D3'ü izlediği bir dizi içeriyorsa ve tüm öğeleri iki perde yukarı kaydıran bir transpozisyon uygularsak, C3 D3 olur ve D3 E3 olur. Tonalite transpozisyonu kullanan modellere örnek olarak bkz. Lim, Rhyu ve Lee (2018) ve Sturm vd. (2016).
Tonalite transpozisyonunu boyut indirgeme amacıyla kullanırsak, hedefimiz, veri kümemizdeki benzersiz nota ve akorların toplam sayısını en aza indirecek şekilde notaları ve akorları kaydırmak olacaktır. Müzik genellikle birlikte çalındıklarında uyumlu gelen belirli perde grupları — tonaliteler — etrafında yazıldığından, veri kümemizdeki her şarkıyı uygun bir şekilde aynı tonaliteye kaydırabiliriz.
Bu kez music21'den üç alt modül içe aktaracağız:
converter: Bir MIDI dosyasını yükleyip birstreamnesnesine dönüştürmek için kullanacağız.pitch: Hedef perdeyi oluşturmak için kullanacağız.interval: Orijinal tonalite ile hedef tonalite arasındaki mesafeyi hesaplamak için kullanacağız.
from music21 import converter, pitch, interval
# Define data directory
data_dir = '../audio/'
# Parse MIDI file
score = converter.parse(data_dir+'giuliani.mid')
# Identify and print original key
key = score.analyze('key')
print(key)
C major
# Compute interval between original key and target key
keyInterval = interval.Interval(key.tonic, pitch.Pitch('F'))
# Transpose song into F major
newScore = score.transpose(keyInterval)
# Print new key
print(newScore.analyze('key'))
F major
Önce score üzerinde .analyze('key') yöntemini uyguluyoruz; bu, stream nesnesinin tonalitesini — Do majör — belirler. Ardından Do majör ile hedef tonalitemiz Fa majör arasındaki aralığı hesaplıyoruz. key nesnesinin perdesine .tonic ile eriştiğimize; hedef perdeyi ise pitch.Pitch('F') ile oluşturduğumuza dikkat edin. Çıktıyı, orijinal ve hedef tonaliteler arasındaki mesafeyi söyleyen keyInterval'e atıyoruz.
Son olarak .transpose() yöntemini aralığı (keyInterval) geçirerek uygulayıp tonaliteyi kaydırıyoruz. Yeni tonalitenin Fa majör olduğunu yazdırmak bunu doğrular.
Oktav Transpozisyonu
Tonalite transpozisyonunun ötesine geçmek istersek, oktavı da daha yüksek veya daha alçak olacak şekilde kaydırabiliriz. Örneğin Do majörü Fa majöre çevirmek yerine, Do majörü daha düşük ya da daha yüksek bir oktavda yine Do majöre çevirebiliriz. Her şarkı için aynı oktav kümesini hedeflersek, tüm notalarımızı ve akorlarımızı bir arada gruplayarak, problemin boyutunu azaltır ve modelin eğitilme sürecini basitleştiririz. Yukarıdaki kod bloğunda, perdenin oktavını belirterek bunu başarabiliriz. Örneğin, aralığı hesaplamak için pitch.Pitch('F') belirtmek yerine pitch.Pitch('F3') kullanabiliriz.
Tonalite ve Oktav Değişmezliği
Boyut indirgemeye doğal bir alternatif, veri artırmadır. Yani, olası girdi ve çıktı kümesini azaltmak yerine, mevcut verilerde değişiklikler yaparak veri kümesini genişletiriz. Bunu tonalite ve oktav transpozisyonuyla da yapabiliriz. Ancak, tüm şarkıları ortak bir tonaliteye kaydırıp notalarını bitişik oktavlarda kümelendirmek yerine, tüm şarkıları tüm tonalitelere ve tüm oktavlara kaydırırız. Bu, geliştirdiğimiz modelde tonalite ve oktav değişmezliği elde etmemizi sağlar. İki farklı amaç için transpozisyon kullanımına ilişkin tartışma için bkz. Briot, Hadjerest ve Pachet (2019).
Veri Toplama
Modelimizi eğitmek için MIDI dosyalarını Project Mutopia'dan toplayacağız. Herhangi bir kazıma (scraping) işlemine başlamadan önce, otomatik etkileşimlerin nasıl ele alınması gerektiğine ilişkin tüm istekleri yerine getirdiğimizden emin olmak için https://www.mutopiaproject.org/robots.txt adresindeki robots dosyasını kontrol edeceğiz. Bu eğitim yazıldığı sırada, robots dosyası aşağıdaki kod bloğundaki metni içeriyordu. User-agent: *, takip eden kuralın web sitesiyle etkileşime giren tüm kullanıcılar için geçerli olduğunu belirtir. Ardından hiçbir şey gelmeyen Disallow:, kazıma üzerinde herhangi bir kısıtlama olmadığı anlamına gelir.
# Allow crawling of all content
User-agent: *
Disallow:
Önce urllib.request'ten urlopen ve urlretrieve'i içe aktaracağız; gitar bölümündeki her sayfa için GET isteği göndermek ve sayfadaki her bir MIDI dosyası bağlantısını indirmek için bunları kullanacağız. HTML'yi urlopen ile döndürülen biçimiyle ayrıştırmak ve MIDI dosyası bağlantılarını tespit etmek için BeautifulSoup'u içe aktaracağız. Son olarak, web sitesinin kaynaklarını zorlamamak için indirmeler arasında 10 saniye beklemek üzere time modülünü kullanacağız.

Ana döngünün her adımı, sayfadaki bağlantı sayısının (linkCount) 0'ı aşıp aşmadığını kontrol eder. Aşıyorsa, bir sonraki sayfaya gidilecek URL'yi (url), url0, url1 ve şarkı numarası songNumber dizelerini kullanarak oluşturur. Ardından URL'yi açar, HTML'yi ayrıştırır, sayfadaki tüm bağlantıları bulur, linkCount'u 0'a sıfırlar ve listedeki her bağlantıyı sırayla inceler. Bir bağlantı .mid alt dizesini içeriyorsa, bu bir MIDI dosyasına işaret eder ve urlretrieve() kullanılarak save_dir ile belirtilen dizine indirilir. Son olarak her sayfa 10 şarkı içerdiğinden songNumber'ı 10 artırırız. 10'dan az bağlantı içeren bir sayfaya rastlayana dek bu işlemi tekrarlarız.
!pip install bs4
from urllib.request import urlopen, urlretrieve
from bs4 import BeautifulSoup
import time
# Define save directory.
save_dir = '../guitar/'
# Define URL components
url0 = 'https://www.mutopiaproject.org/cgibin/make-table.cgi?startat='
url1 = '&searchingfor=&Composer=&Instrument=Guitar&Style=&collection=&id=&solo=&recent=&timelength=&timeunit=&lilyversion=&preview='
# Set initial values
songNumber = 0
linkCount = 10
# Locate and download each MIDI file
while linkCount > 0:
url = url0 + str(songNumber) + url1
html = urlopen(url)
soup = BeautifulSoup(html.read())
links = soup.find_all('a')
linkCount = 0
for link in links:
href = link['href']
if href.find('.mid') >= 0:
linkCount = linkCount + 1
urlretrieve(href, save_dir+href)
songNumber += 10
time.sleep(10.0)
Kazıma hakkında daha fazla bilgi edinmek isteyenler için DataCamp'in Web Scraping with Python kursu ve Web Scraping using Python eğitimi faydalı olacaktır.

