कोर्स
यह ट्यूटोरियल Python में TensorFlow 2.0 और उच्च-स्तरीय Keras API के इर्द-गिर्द बनाया गया है, जो TensorFlow 2.0 में और भी सशक्त भूमिका निभाता है। जो लोग TensorFlow 2.0 के बारे में अधिक सीखना चाहते हैं, वे DataCamp पर Introduction to TensorFlow in Python देखें। संगीत पर डीप लर्निंग साहित्य की समग्र समीक्षा के लिए, Briot, Hadjerest, और Pachet (2019) देखें, जिसका इस ट्यूटोरियल में समय-समय पर उल्लेख होगा।
जनरेटिव मॉडल
सुपरवाइज़्ड मशीन लर्निंग मॉडल दो श्रेणियों में बाँटे जा सकते हैं: डिस्क्रिमिनेटिव मॉडल और जनरेटिव मॉडल। डिस्क्रिमिनेटिव मॉडल निर्णय सीमा की पहचान करते हैं और उसके अनुरूप वर्गीकरण उत्पन्न करते हैं। जनरेटिव मॉडल किसी वर्ग के नए उदाहरण बनाते हैं।
संगीत का एक डिस्क्रिमिनेटिव मॉडल गानों को अलग-अलग शैली (genre) में वर्गीकृत करने के लिए उपयोग किया जा सकता है। जनरेटिव मॉडल किसी विशेष शैली के गाने रच सकता है। इस ट्यूटोरियल में, हम संगीत रचना के लिए जनरेटिव मॉडल का उपयोग करेंगे।

संगीत का निरूपण
जनरेटिव मॉडल परिभाषित और प्रशिक्षित करने से पहले, हमें एक डेटासेट एकत्र करना होगा। संगीत के लिए, डेटा को निरंतर (continuous) या असतत (discrete) रूप में दर्शाया जा सकता है। सबसे सामान्य निरंतर रूप ऑडियो सिग्नल है, जिसे आमतौर पर WAV फ़ाइल के रूप में रखा जाता है। हम इस ट्यूटोरियल में निरंतर रूपों का उपयोग नहीं करेंगे, लेकिन आप इनके बारे में अधिक परिशिष्ट में पढ़ सकते हैं। सामान्य असतत रूपों में Musical Instrument Digital Interface (MIDI) फ़ाइलें, पियानो-रोल, और टेक्स्ट शामिल हैं। हम MIDI फ़ाइलों पर ध्यान देंगे और दो प्रकार की सिंबॉलिक वस्तुएँ निकालेंगे: नोट्स और कॉर्ड्स।
नोट्स
नोट ध्वनि का एक प्रतीकात्मक निरूपण है। हमारे संदर्भ में, किसी नोट का वर्णन उसकी पिच और अवधि से किया जा सकता है। किसी नोट की पिच उसकी ध्वनि तरंग के दोलन की आवृत्ति से संबंधित होती है, जिसे हर्ट्ज (Hz) में मापा जाता है। अधिक ऊँची पिच वाले नोट्स में प्रति सेकंड अधिक दोलन वाली ध्वनि तरंगें होती हैं। किसी नोट की अवधि वह समयावधि है, जिसके दौरान उसे बजाया जाता है।
MIDI फ़ाइलें किसी नोट की पिच को 0 से 127 के बीच के पूर्णांक से दर्शाती हैं। नोट्स को पिच-अक्षर और सप्तक संख्या से भी दर्शाया जा सकता है। एक ही सप्तक के भीतर, पिचें निम्न से उच्च आवृत्ति तक इस क्रम में होती हैं:
- C
- C#/D♭
- D
- E♭/D#
- E
- F
- F#/G♭
- G
- A♭/G#
- A
- B♭/A#
- B
सप्तक को उपस्क्रिप्ट से दर्शाया जाता है, जैसे A4 में 4 या C7 में 7। अधिक उच्च सप्तक उच्च आवृत्ति से मेल खाता है। यदि हम मनमानी पिच Xi लें, तो Xi+1, जो ठीक एक सप्तक ऊपर है, Xi की तुलना में दोगुनी आवृत्ति वाली ध्वनि तरंग का निरूपण करता है।
किसी नोट की पिच के अलावा, हम उसकी अवधि का भी उपयोग करेंगे। अवधि एक सापेक्ष मान है, जिसे होल नोट की लंबाई से सामान्यीकृत किया जाता है। सबसे लंबा नोट "लार्ज" नोट है, जो होल नोट से आठ गुना लंबा होता है। सबसे छोटा नोट दो सौ छप्पनवाँ नोट है, जो होल नोट की लंबाई का 1/256वाँ होता है।
कॉर्ड्स
कॉर्ड एक ही वाद्ययंत्र पर एक साथ बजाए गए दो या अधिक नोट्स का संयोजन है। यदि हम मोनोफोनिक संगीत देखें — यानी एक ही वाद्ययंत्र पर बजाया गया संगीत — तो हम यह मानकर कॉर्ड्स की पहचान कर सकते हैं कि ठीक एक ही समय पर बजाए गए सभी नोट्स एक ही कॉर्ड का हिस्सा हैं। यदि हमारे पास पॉलीफोनिक संगीत है, जिसमें एक साथ दो या अधिक वाद्ययंत्र बजते हैं, तो यह मान्य नहीं होता।
नीचे दिए गए कोड ब्लॉक में, हम music21 स्थापित करते हैं और फिर converter मॉड्यूल आयात करते हैं, जिसका उपयोग हम MIDI फ़ाइलों को पार्स करने के लिए करेंगे। हम Mauro Giuliani की एक क्लासिकल गिटार रचना लोड और पार्स करेंगे। फिर हम .chordify() मेथड लागू करेंगे, जो यह मानते हुए स्कोर में कॉर्ड्स के क्रम का पुनर्निर्माण करता है कि एक ही समय पर बजाए गए नोट्स एक ही कॉर्ड का हिस्सा हैं।
!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 फ़ाइल में कॉर्ड्स के क्रम के निर्माण के बाद, हम .show() मेथड का उपयोग करके इसे शीट म्यूज़िक के रूप में प्रदर्शित कर सकते हैं। इसके लिए शीट म्यूज़िक बनाने और दिखाने वाला सॉफ़्टवेयर चाहिए होगा, जैसे कि MuseScore, जो निःशुल्क उपलब्ध है।

वैकल्पिक रूप से, हम आधारभूत अवयवों को टेक्स्ट के रूप में देखना और संशोधित करना चाह सकते हैं। हम यह .show('text') मेथड का उपयोग करके कर सकते हैं। हर पंक्ति की शुरुआत में हमें कोष्ठकों में ऑफ़सेट दिखाई देता है, जो बताता है कि स्कोर में कोई अवयव कितने सेकंड बाद प्रकट होता है। ध्यान दें कि 0वें सेकंड पर केवल एक अवयव कॉर्ड है। शेष अवयव मेटाडेटा हैं, जिनमें प्रयुक्त वाद्ययंत्र, टेम्पो, की (key) और मीटर शामिल हैं। हम आगे की में उपयोग करेंगे, पर अभी आप इन अवयवों को नज़रअंदाज़ कर सकते हैं।
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>
मेटाडेटा के बाद, शेष अवयव कॉर्ड्स होते हैं, जिन्हें गीत में प्रकट होने के क्रम में व्यवस्थित किया गया है। उदाहरण के लिए, <music21.chord.Chord E3 G3> E3 और G3 नोट्स को एक साथ बजाने से बनने वाला कॉर्ड है। इसी तरह, <music21.chord.Chord A3> अलग से बजाया गया नोट A3 है।
कुछ मॉडलों के लिए, हम कॉर्ड और उसकी अवधि दोनों का उपयोग करेंगे, जो यह मापती है कि उसे कितनी देर तक बजाया गया। हम यह जानकारी नीचे दिए गए कोड ब्लॉक में दिखाए अनुसार .elements मेथड्स लागू करके और स्कोर में किसी मनमाने कॉर्ड के .duration एट्रिब्यूट तक पहुँचकर प्राप्त कर सकते हैं।
print(score.elements[10])
<music21.chord.Chord E3 A3>
print(score.elements[10].duration)
<music21.duration.Duration 1.0>
नोट और कॉर्ड एन्कोडिंग
पूरे ट्यूटोरियल में, हम संगीत के असतत निरूपण पर ध्यान देंगे, जिसका वर्णन नोट्स, कॉर्ड्स और उनसे जुड़ी अवधियों के अनुक्रम से किया जा सकता है। याद रखें कि कॉर्ड्स नोट्स के संयोजन होते हैं और MIDI प्रारूप 128 अलग-अलग नोट्स के उपयोग की अनुमति देता है। यदि हम फीचर सेट का विस्तार करके सभी दो-नोट कॉर्ड्स शामिल करें, तो इसका आकार 128!/[(128-2)!*2!] या 8128 तत्वों से बढ़ता है। तीन-नोट कॉर्ड्स जोड़ने से यह 341,376 और चार-नोट कॉर्ड्स जोड़ने से यह और 10,668,000 बढ़ जाता है।
जो उदाहरण हमने पहले दिखाया था, उसमें नोट्स के साथ-साथ दो-, तीन- और चार-नोट कॉर्ड्स शामिल हैं। यदि हम वन-हॉट एन्कोडिंग का उपयोग करें और सभी फीचर्स शामिल करें, तो हमें 11,017,632-आयामी वेक्टरों पर प्रेडिक्शन करने होंगे। इसलिए, संगीत जनरेशन समस्याओं में एन्कोडिंग के चुनाव पर सावधानी से विचार करना होगा। हम की ट्रांसपोज़िशन या डेटा ऑग्मेंटेशन का उपयोग करने पर भी विचार कर सकते हैं, जिन दोनों पर परिशिष्ट में चर्चा की गई है।
एन्कोडिंग के चुनाव के संदर्भ में, Briot, Hadjerest, और Pachet (2019) संगीत के जनरेटिव मॉडलों में प्रचलित चार विकल्पों पर चर्चा करते हैं:
- वन-हॉट एन्कोडिंग
- मेनी-हॉट एन्कोडिंग
- मल्टी-वन-हॉट एन्कोडिंग
- मल्टी-मेनी-वन-हॉट एन्कोडिंग
वन-हॉट एन्कोडिंग में संगीत प्रतीकों, जैसे नोट्स और कॉर्ड्स, को स्पार्स वेक्टरों के रूप में दर्शाया जाता है, जिनमें किसी विशेष नोट या कॉर्ड से संबंधित स्थिति में एक होता है। मेनी-हॉट एन्कोडिंग में कॉर्ड्स (जो कई नोट्स से बने होते हैं) को उन सभी नोट्स से संबंधित स्थितियों में एक रखकर दर्शाया जाता है।
नीचे दिए गए आरेख में, हम उस स्थिति के लिए मेनी-हॉट और वन-हॉट एन्कोडिंग की तुलना करते हैं जहाँ हमारे पास नोट्स और दो-नोट कॉर्ड्स हैं। मेनी-हॉट एन्कोडिंग में C4 और D4 नोट्स 128 तत्वों वाले वेक्टरों से दर्शाए जाते हैं। दोनों वेक्टरों में उन स्थितियों को छोड़कर बाकी सभी जगह शून्य होते हैं जो C4 और D4 तत्वों से मेल खाते हैं, जहाँ एक होता है। C4+D4 कॉर्ड, जो C4 और D4 नोट्स को एक साथ बजाने से बनता है, C4 और D4 दोनों स्थितियों में एक रखकर बनाया जाता है।

एकल नोट्स और दो-नोट कॉर्ड्स का वन-हॉट निरूपण 8256-तत्वों वाले वेक्टरों से बनता है। हर मामले में, वेक्टर स्पार्स होता है और उस स्थिति में 1 होता है जो किसी अद्वितीय नोट या कॉर्ड से मेल खाती है। ध्यान दें कि मेनी-हॉट निरूपण के लिए मल्टी-लेबल मॉडल की आवश्यकता होती है, जहाँ किसी आउटपुट के लिए एक से अधिक क्लास की भविष्यवाणी की जा सकती है। यह, हालांकि, इनपुट और आउटपुट वेक्टरों के आकार को काफी कम कर देता है।
साहित्य में अधिकांश मॉडल अब भी वन-हॉट एन्कोडिंग का उपयोग करते हैं। हालांकि, Briot, Hadjerest, और Pachet (2019) कई ऐसे संगीत जनरेशन मॉडलों की पहचान करते हैं जो मेनी-हॉट एन्कोडिंग का उपयोग करते हैं, जिनमें RBMc, RNN-RBM, और C-RBM सिस्टम शामिल हैं, जो सभी रेस्ट्रिक्टेड बोल्ट्ज़मैन मशीन (RBM) आर्किटेक्चर पर आधारित हैं।
मल्टी-वन-हॉट और मल्टी-मेनी-हॉट एन्कोडिंग वन-हॉट और मेनी-हॉट एन्कोडिंग के वे सामान्यीकरण हैं जो पॉलीफोनिक संगीत — यानी एक साथ कई वाद्ययंत्र बजने — के मामले में लागू होते हैं। हम अपने आप को मोनोफोनिक संगीत तक सीमित रखेंगे और इस ट्यूटोरियल में इन विकल्पों पर आगे चर्चा नहीं करेंगे।
डेटा संग्रह और तैयारी
अगला चरण एक ऐसा डेटासेट इकट्ठा करना है, जिसका उपयोग हम जनरेटिव मॉडल को प्रशिक्षित करने के लिए कर सकें। हम Mutopia Project के डेटा का उपयोग करेंगे, जिसमें MIDI प्रारूप में 2124 सार्वजनिक डोमेन की संगीत रचनाएँ हैं। चूँकि वेबसाइट वाद्ययंत्र के अनुसार अनुभागों में विभाजित है, हम एक अपेक्षाकृत सरल Python स्क्रिप्ट लिख सकते हैं जो सभी गिटार रचनाएँ स्क्रैप कर ले, जिनका उपयोग हम इस ट्यूटोरियल में मॉडलों को प्रशिक्षित करने के लिए करेंगे। इसे कैसे करना है, इसका अवलोकन परिशिष्ट में देखें।
मान लें कि हमने गिटार अनुभाग से सभी .mid फ़ाइलें किसी डायरेक्टरी save_dir में सहेज ली हैं। अगला कदम प्रत्येक .mid फ़ाइल को music21 में stream ऑब्जेक्ट के रूप में लोड करना है।
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)
आयामीयता घटाने की रणनीति के हिस्से के रूप में, हम अपने आप को केवल Mauro Giuliani द्वारा रचित गीतों तक सीमित रखेंगे। हम ऊपर दिए गए कोड ब्लॉक को निम्नानुसार संशोधित कर सकते हैं।
# Identify list of MIDI files
songList = os.listdir(save_dir)
songList = [song for song in songList if song.lower().find('giuliani')>-1]
प्रशिक्षण शुरू करने से पहले हमें चार अतिरिक्त कार्य करने हैं:
- पॉलीफोनिक संगीत हटाना और शेष
streamऑब्जेक्ट्स पर.chordify()लागू करना। - नोट्स, कॉर्ड्स और अवधियों का अनुक्रम निकालना।
- ऐसी मैपिंग बनाना जो नोट्स, कॉर्ड्स और अवधियों को पूर्णांकों में और उल्टे रूप में अनुवादित करे।
- मॉडलों के इनपुट के रूप में उपयोग के लिए स्ट्रीम्स को निश्चित लंबाई वाले अनुक्रमों में विभाजित करना।
आइए डेटासेट से पॉलीफोनिक संगीत हटाने से शुरू करें। हम ऐसा प्रत्येक stream ऑब्जेक्ट को वाद्ययंत्र के आधार पर विभाजित करके और फिर यह जाँचकर कर सकते हैं कि क्या केवल एक ही विभाजन है — यानी, क्या केवल एक ही वाद्ययंत्र बज रहा है। हम इसे नीचे दिए गए कोड ब्लॉक में करेंगे।
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]
ध्यान दें कि यदि हम पॉलीफोनिक संगीत हटाए बिना .chordify() लागू करते हैं — जैसा कि नीचे के चित्र में दिखाया गया है — तो हम गलती से अलग-अलग वाद्ययंत्रों द्वारा बजाए गए नोट्स को एक ही कॉर्ड में मिला देंगे।

अब हमें प्रत्येक stream से नोट्स, कॉर्ड्स, और अवधियाँ निकालनी हैं। हम पहले तीन खाली सूचियाँ परिभाषित करेंगे: originalChords, originalDurations, originalKeys। हम स्ट्रीम्स की सूची पर एनेमरेट करेंगे। हर स्ट्रीम के लिए, हम सभी नोट्स, कॉर्ड्स और अवधियों के साथ-साथ उस रचना की की (key) की पहचान और भंडारण करेंगे।
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))
हम नीचे दिए गए कोड ब्लॉक में और आयामीयता घटाएँगे, केवल C मेजर की सिग्नेचर वाली रचनाओं को रखते हुए, जो हमारे डेटासेट में सबसे सामान्य है। साथ ही, ट्यूटोरियल के शेष हिस्से में हम "कॉर्ड" शब्द का उपयोग नोट्स और कॉर्ड्स, दोनों के लिए करेंगे।
# 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')]
अगला कदम अद्वितीय कॉर्ड्स और अवधियों के सेट की पहचान करना है। फिर हम ऐसे शब्दकोश बनाएँगे जो उन्हें पूर्णांकों से मैप करें। यदि हम अद्वितीय नोट्स और कॉर्ड्स की संख्या प्रिंट करें, तो हम देखेंगे कि अब तक किए गए आयामीयता घटाने के चरणों ने इस संख्या को एक प्रबंधनीय 267 तक ला दिया है।
# 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
मॉडल प्रशिक्षण और भविष्यवाणी करने के बाद, हम अपनी पूर्णांक भविष्यवाणियों को फिर से नोट्स, कॉर्ड्स और अवधियों में मैप करना चाहेंगे। नीचे हम chordToInt और durationToInt शब्दकोशों को उलटेंगे।
# Invert chord and duration dictionaries
intToChord = {i: c for c, i in chordToInt.items()}
intToDuration = {i: c for c, i in durationToInt.items()}
अंत में, हम अपने प्रशिक्षण अनुक्रम परिभाषित कर सकते हैं, जो 32 क्रमिक नोट्स और कॉर्ड्स तथा उनकी संबंधित अवधियों से मिलकर बनते हैं। हम पहले इसे ऑटोएन्कोडर-प्रकार मॉडलों के लिए करेंगे, जहाँ फीचर्स और टार्गेट्स का सेट समान होता है।
# 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])
संगीत जनरेशन
इस खंड में, हम तैयार किए गए डेटासेट का उपयोग करके संगीत जनरेट करेंगे। हम संगीत जनरेशन के तीन अलग-अलग मॉडलों पर विचार करेंगे, सबसे सरल से शुरू करते हुए:
- ऑटोएन्कोडर
- वैरिएशनल ऑटोएन्कोडर (VAE)
- लॉन्ग शॉर्ट-टर्म मेमोरी मॉडल (LSTM)
ऑटोएन्कोडर्स
एक ऑटोएन्कोडर दो नेटवर्क्स से मिलकर बनता है, जो ऊर्ध्वाधर रूप से स्टैक किए गए होते हैं और एक लैटेंट वेक्टर से जुड़े होते हैं। ऑटोएन्कोडर के इनपुट पहले एन्कोडर मॉडल में भेजे जाते हैं, जो आमतौर पर एक या अधिक डेंस लेयर्स से बना होता है। एन्कोडर मॉडल की अंतिम लेयर को लैटेंट वेक्टर कहा जाता है। यह एक बॉटलनेक है जो एन्कोडर में निकाले गए फीचर्स को थोड़े से लैटेंट फीचर्स में संपीड़ित करने के लिए बाध्य करता है।
लैटेंट वेक्टर एक डीकोडर मॉडल से जुड़ता है, जो डेटा को अपसैंपल या डिकंप्रेस करता है। प्रत्येक क्रमिक डीकोडर लेयर में नोड्स की संख्या बढ़ती है। अंतिम आउटपुट लेयर का आयाम एन्कोडर मॉडल की इनपुट लेयर के समान होता है।
डिस्क्रिमिनेटिव मॉडलों के विपरीत, जो निर्णय सीमा पहचानने का प्रयास करते हैं, ऑटोएन्कोडर्स को इनपुट डेटा — इस मामले में, एक गीत — को यथासंभव सटीक रूप से पुनर्निर्मित करने के लिए प्रशिक्षित किया जाता है, बशर्ते लैटेंट वेक्टर के आकार द्वारा लगाए गए प्रतिबंध। इसी कारण, ऑटोएन्कोडर्स इनपुट्स को ही टार्गेट के रूप में उपयोग करते हैं।
हम अपने पहले जनरेटिव संगीत मॉडल के निर्माण के लिए ऑटोएन्कोडर का उपयोग करेंगे। चीजों को यथासंभव सरल रखने के लिए, हम DeepBach मॉडल के सरलीकरण के रूप में Briot, Hadjerest, और Pachet (2019) में प्रस्तुत MiniBach मॉडल की आर्किटेक्चर अपनाएँगे। ऑटोएन्कोडर-आधारित संगीत जनरेशन का एक अन्य उदाहरण DeepHear सिस्टम में देखें।
जैसा कि हमने इनपुट डेटा बनाया है, वह नोट्स और कॉर्ड्स का प्रतिनिधित्व करने वाले वन-हॉट एन्कोडेड वेक्टरों से बना है। हम अभी अवधियों को अनदेखा करेंगे। MiniBach आर्किटेक्चर का अनुसरण करते हुए, हम इनपुट वेक्टर को फ़्लैटन करने से शुरू करेंगे।
# 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)
अगले चरण में हम सैंपल्स की संख्या nSamples, कॉर्ड्स और नोट्स की संख्या nChords, और इनपुट आयाम inputDim परिभाषित करेंगे। हम लैटेंट फीचर्स की संख्या latentDim को 2 सेट करते हैं। लैटेंट डाइमेंशंस की कम संख्या प्रदर्शन घटाएगी, लेकिन लैटेंट स्पेस के आसान विज़ुअलाइज़ेशन संभव करेगी।
# 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
अब हम मॉडल की आर्किटेक्चर परिभाषित करते हैं, एन्कोडर नेटवर्क के इनपुट लेयर encoderInput और डीकोडर नेटवर्क के इनपुट लेयर latent से शुरू करते हुए। ध्यान दें कि latent लेयर एन्कोडर नेटवर्क का आउटपुट भी है। इसके बाद हम एक डेंस लेयर encoded परिभाषित करते हैं, जो इनपुट्स को लैटेंट वेक्टर से जोड़ती है, और एक अन्य डेंस लेयर decoded, जो लैटेंट वेक्टर को आउटपुट वेक्टर से जोड़ती है।
अंत में, हम एक डीकोडर मॉडल decoder और अपना ऑटोएन्कोडर autoencoder परिभाषित करते हैं। ध्यान दें कि .Model() में inputs और outputs आर्ग्युमेंट होते हैं। decoder मॉडल के लिए, हम latent वेक्टर को इनपुट और decoded नेटवर्क को output के रूप में पास करते हैं। पूर्ण ऑटोएन्कोडर के लिए, हम encoderInput को इनपुट के रूप में उपयोग करते हैं और encoder नेटवर्क के आउटपुट encoded से मूल्यांकित decoder मॉडल को इनपुट के रूप में पास करते हैं।
# 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))
नीचे दिए गए चित्र में एक सामान्य ऑटोएन्कोडर की आर्किटेक्चर दिखाई गई है। नेटवर्क का ऊपरी भाग एन्कोडर है, और निचला भाग डीकोडर। इन्हें लैटेंट वेक्टर जोड़ता है।

decoder और autoencoder की आर्किटेक्चर परिभाषित करने के बाद, केवल मॉडल को कंपाइल करना और फिर .fit() मेथड का उपयोग करके प्रशिक्षण शेष रहता है। दो महत्वपूर्ण विवरण ध्यान देने योग्य हैं:
- हमारे इनपुट मान 0 और 1 होंगे, और हमारी भविष्यवाणियाँ 0 और 1 के बीच वास्तविक संख्याएँ होंगी। हम
binary_crossentropyलॉस फ़ंक्शन का उपयोग कर सकते हैं। - इनपुट और लक्षित मान दोनों समान हैं:
trainChordsFlat।
# Compile autoencoder model
autoencoder.compile(loss = 'binary_crossentropy', learning_rate = 0.01, optimizer='rmsprop')
# Train autoencoder
autoencoder.fit(trainChordsFlat, trainChordsFlat, epochs = 500)
अंतिम चरण संगीत जनरेट करना है। चूँकि हमने अनुक्रम की लंबाई 32 रखी है, autoencoder 32 कॉर्ड्स और नोट्स को इनपुट के रूप में लेगा और 32 कॉर्ड्स और नोट्स के एक अनुक्रम वाला निश्चित-लंबाई का "गीत" उत्पन्न करेगा। autoencoder इनपुट के बहुत समान आउटपुट जनरेट करने के लिए प्रशिक्षित होता है; हालाँकि, हमारा उद्देश्य नया संगीत बनाना है। इसके लिए, हम यादृच्छिक रूप से जनरेट किए गए लैटेंट वेक्टर को decoder मॉडल में पास करेंगे, जिसे हमने autoencoder मॉडल के एक सबनेटवर्क के रूप में परिभाषित किया है।
# Generate chords from randomly generated latent vector
generatedChords = decoder(np.random.normal(size=(1,latentDim))).numpy().reshape(nChords, sequenceLength).argmax(0)
ध्यान दें कि हमने ऑटोएन्कोडर के आउटपुट को अपने मूल इनपुट के समान आयामों वाले एरे में रिज़ेप किया। फिर हमने एरे के पहले आयाम पर argmax() लिया। यह एक पूर्णांक लौटाता है जो किसी कॉर्ड से मेल खाता है, जिसे हम नीचे के कोड ब्लॉक में पहचानेंगे।
# Identify chord sequence from integer sequence
chordSequence = [intToChord[c] for c in generatedChords]
अंत में, हम music21 का उपयोग करके एक stream ऑब्जेक्ट बना सकते हैं, वाद्ययंत्र के रूप में गिटार सेट कर सकते हैं, अपने मॉडल द्वारा जनरेट किए गए कॉर्ड्स के अनुक्रम को जोड़ सकते हैं, और फिर सब कुछ MIDI फ़ाइल के रूप में निर्यात कर सकते हैं।
# 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')
नीचे दिया गया gif ऑटोएन्कोडर के प्रशिक्षण प्रक्रिया के एक हिस्से को दिखाता है। प्रत्येक चरण में जनरेट किया गया स्कोर किसी विशिष्ट लैटेंट स्टेट वेक्टर से जुड़ा पुनर्निर्माण है। हर फ़्रेम 5 प्रशिक्षण एपॉक्स का प्रतिनिधित्व करता है।

हम प्रशिक्षण के दौरान लैटेंट स्पेस में गतियों का भी विज़ुअलाइज़ेशन कर सकते हैं, जैसा कि नीचे के gif में किया गया है। ध्यान दें कि लैटेंट स्पेस संकीर्ण है और प्रशिक्षण एपॉक्स के साथ बहकता रहता है। यह किसी उच्च-गुणवत्ता वाले यादृच्छिक लैटेंट स्टेट को चुनना कठिन बना देगा और जब हम उच्च-आयामी लैटेंट वेक्टर का उपयोग करेंगे तो यह और भी समस्याग्रस्त सिद्ध होगा। अगले खंड में हम दिखाएँगे कि वैरिएशनल ऑटोएन्कोडर (VAE) का उपयोग करके इसे कैसे सुलझाया जा सकता है।

अंत में, हम अपने ऑटोएन्कोडर का ऑडियो आउटपुट देख सकते हैं। मूल गीत का कॉर्ड अनुक्रम — जो हमारे प्रशिक्षण सेट से यादृच्छिक रूप से चुना गया था — पहले दिया गया है। फिर हम मूल गीत के कॉर्ड अनुक्रम से जुड़े लैटेंट वेक्टर का उपयोग करके ऑटोएन्कोडर से कॉर्ड अनुक्रम जनरेट करते हैं, जिससे उसके नीचे वाला गीत प्राप्त होता है।
उदाहरण: मूल गीत
उदाहरण: जनरेट किया गया गीत
वैरिएशनल ऑटोएन्कोडर
हालाँकि ऑटोएन्कोडर डी-नोइज़िंग, कंप्रेशन और डी-कंप्रेशन कार्यों के लिए उपयुक्त हैं, वे जनरेटिव मॉडलों के रूप में अच्छा प्रदर्शन नहीं करते। वैरिएशनल ऑटोएन्कोडर्स (VAEs), जिन्हें Kingma और Welling (2017) ने प्रस्तुत किया, ऑटोएन्कोडर्स की जनरेटिव मॉडल के रूप में कमजोरियों को दूर करने के लिए डिज़ाइन किए गए हैं और संगीत जनरेशन के लिए सबसे अधिक उपयोग किए जाने वाले तरीकों में से एक हैं। Briot, Hadjerest, और Pachet (2019) चार ऐसे संगीत-जनरेशन सिस्टम की पहचान करते हैं जो VAE पर आधारित हैं: MusicVAE, VRAE, VRASH, और GSLR-VAE। वे दो आयामों पर ऑटोएन्कोडर्स में सुधार करते हैं:
- हर इनपुट सेट को लैटेंट स्पेस में एक बिंदु से मैप करने के बजाय, वैरिएशनल ऑटोएन्कोडर हर इनपुट सेट को सामान्य वितरण से मैप करता है, जो माध्य और वेरिएंस से अभिलक्षित होता है।
- लैटेंट स्पेस को किसी भी माध्य और वेरिएंस की अनुमति देने के बजाय, वैरिएशनल ऑटोएन्कोडर माध्य और वेरिएंस के नैचुरल लोगारिद्म को 0 के पास रहने के लिए बाध्य करता है।
हम पहली बदलाव को ऑटोएन्कोडर में लैटेंट स्टेट वेक्टर को तीन लेयर्स से बदलकर कर सकते हैं:
- एक मीन (mean) लेयर
- एक (लॉग) वेरिएंस लेयर
- एक सैंपलिंग लेयर
हर इनपुट सेट एकल माध्य और वेरिएंस के नैचुरल लोगारिद्म से मैप होता है, जो किसी सामान्य वितरण को अभिलक्षित करने के लिए पर्याप्त है। फिर मॉडल सैंपलिंग लेयर में इस वितरण से बिंदुओं का यादृच्छिक ड्रॉ करता है।
नीचे का कोड ब्लॉक एक फ़ंक्शन परिभाषित करता है जिसका उपयोग सैंपलिंग लेयर बनाने में किया जा सकता है। यह माध्य और लॉग वेरिएंस को इनपुट के रूप में लेता है। फिर यह tensorflow के random सबमॉड्यूल का उपयोग करके epsilon नाम का बिंदुओं का टेन्सर एक मानक सामान्य वितरण से ड्रॉ करता है, जिसका माध्य और लॉग वेरिएंस दोनों 0 होते हैं। हम अगला चाहते हैं कि इन ड्रॉ को mean और logVar पैरामीटर्स से अभिलक्षित वितरणों से ड्रॉ में ट्रांसफॉर्म करें। ऐसा करने के लिए, हमें पहले ड्रॉ epsilon को संबंधित वितरणों के मानक विचलन से स्केल करना होगा। हम इन मानक विचलनों को logVar को 2 से भाग देकर और फिर उसका एक्सपोनेंशिएशन करके निकाल सकते हैं। फिर हम वितरण के माध्य mean जोड़ देते हैं।
# 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
अब हम mean, logVar, और sampling लेयर्स को शामिल करने के लिए ऑटोएन्कोडर मॉडल को संशोधित कर सकते हैं। ध्यान दें कि अब encoded लेयर दो इनपुट लेती है और encoder लेयर के तीन आउटपुट हैं।
# 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))
जैसा कि ऑटोएन्कोडर मॉडल के साथ था, हमारा लैटेंट स्टेट encoded लेयर का आउटपुट है। हालाँकि, एक महत्वपूर्ण अंतर है: हर इनपुट सेट को एकल लैटेंट स्टेट से मैप करने के बजाय, हम sampling लेयर का उपयोग करते हैं ताकि उन्हें mean और logVar पैरामीटर्स वाले सामान्य वितरण से ड्रॉ किए गए यादृच्छिक मानों से मैप किया जा सके। इसका मतलब है कि एक ही इनपुट किसी लैटेंट स्टेट्स के वितरण से जुड़ा होगा।
अंतिम चरण लॉस फ़ंक्शन को समायोजित करना है, ताकि मॉडल के mean और logVar मानों के चुनाव पर अनुशासन लागू किया जा सके। विशेष रूप से, हम mean और logVar लेयर्स द्वारा परिभाषित प्रत्येक वितरण और मानक सामान्य वितरण (जिसका माध्य और लॉग वेरिएंस 0 है) के बीच Kullback-Leibler (KL) डाइवर्जेन्स की गणना करेंगे। हमारे माध्यों और लॉग वेरिएंस मानों के 0 से जितने दूर होने पर, KL डाइवर्जेन्स लॉस उतना अधिक होगा।
स्वाभाविक प्रश्न यह है कि हम प्रत्येक वितरण को मानक सामान्य के पास रखने के लिए मजबूर क्यों करें। इसका कारण यह है कि हम लैटेंट स्टेट्स का सैंपल लेना चाहते हैं और उनका उपयोग संगीत जनरेट करने के लिए करना चाहते हैं। यदि हम लॉस फ़ंक्शन पर KL डाइवर्जेन्स पेनल्टी लागू करते हैं, तो यह हमें मानक सामान्य वितरण से स्वतंत्र ड्रॉ का उपयोग करके ऐसे लैटेंट स्टेट्स प्राप्त करने देगा जो उच्च गुणवत्ता वाले संगीत बनाने की अधिक संभावना रखते हैं।
नीचे के कोड ब्लॉक में, हम दिखाते हैं कि KL डाइवर्जेन्स टर्म को शामिल करने के लिए लॉस फ़ंक्शन को कैसे संशोधित किया जाए। लॉस फ़ंक्शन का पहला घटक बाइनरी क्रॉसएंट्रॉपी लॉस है, जिसे vae मॉडल के इनपुट्स और आउटपुट्स पर गणना किया जाता है। यही हमने ऑटोएन्कोडर के लिए भी उपयोग किया था। इसे "रिकंस्ट्रक्शन लॉस" कहा जाता है क्योंकि यह मूल गीत को पुनर्निर्मित करने में विफल रहने पर दंडित करता है। KL टर्म लैटेंट स्पेस के वितरणों और मानक सामान्य वितरण के बीच औसत KL डाइवर्जेन्स है। हम दोनों को जोड़कर कुल लॉस vaeLoss निकालते हैं, उसे मॉडल में जोड़ते हैं, कंपाइल करते हैं और फिर प्रशिक्षण करते हैं।
# 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)
पिछले खंड में, हमने दिखाया कि ऑटोएन्कोडर का लैटेंट स्पेस अच्छी तरह व्यवहार नहीं कर रहा था। VAE का एक उद्देश्य जनरेटिव सेटिंग्स में इस डिज़ाइन खामी को दूर करना है। नीचे दिया गया चित्र VAE के लैटेंट स्पेस को दिखाता है। हम देखते हैं कि दोनों फीचर्स में पर्याप्त फैलाव है। साथ ही, वे 0 के आसपास केंद्रित हैं और प्रशिक्षण एपॉक्स के साथ बहकते नहीं हैं। इससे किसी उच्च-गुणवत्ता वाले लैटेंट स्टेट का यादृच्छिक सैंपल लेना काफी आसान हो जाएगा।

अंततः, हम संगीत जनरेशन की ओर बढ़ते हैं। हम मानक सामान्य वितरण से एक यादृच्छिक लैटेंट वेक्टर ड्रॉ करते हैं, उसे डीकोडर में पास करते हैं, आउटपुट को रिज़ेप करते हैं, और हर कॉलम का argmax लेते हैं। यह 32 पूर्णांकों का अनुक्रम लौटाएगा, जो नोट्स और कॉर्ड्स से मेल खाते हैं, जिन्हें हम आगे intToChord शब्दकोश का उपयोग करके पहचानेंगे। फिर हम एक stream ऑब्जेक्ट परिभाषित करते हैं, वाद्ययंत्र के रूप में गिटार जोड़ते हैं, नोट्स और कॉर्ड्स के अनुक्रम को stream में जोड़ते हैं, और फिर इसे MIDI फ़ाइल के रूप में निर्यात कर देते हैं।
# 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')
नीचे दिए गए तीन गीत मॉडल द्वारा जनरेट किए गए हैं। पहले दो गीत प्रशिक्षण सेट के कॉर्ड अनुक्रमों से जुड़े लैटेंट स्टेट्स पर आधारित हैं। अंतिम गीत गीत #1 और गीत #2 के लैटेंट स्टेट्स के रैखिक संयोजन पर आधारित है। चूँकि VAE लैटेंट स्पेस को सुव्यवस्थित रहने के लिए बाध्य करता है, हम उचित रूप से आश्वस्त हो सकते हैं कि लैटेंट स्टेट्स के रैखिक संयोजन से उच्च-गुणवत्ता वाला लैटेंट स्टेट प्राप्त होगा।
उदाहरण: जनरेट किया गया गीत #1
उदाहरण: जनरेट किया गया गीत #2
उदाहरण: गीत #1 और #2 का रैखिक संयोजन
ऑटोएन्कोडर और VAE-आधारित संगीत से हम एक बात देख सकते हैं कि इनमें मजबूत मेलोडी का अभाव है। हम इसे मॉडल में और परतें जोड़कर और प्रशिक्षण सेट का आकार बढ़ाकर सुधार सकते हैं। वैकल्पिक रूप से, हम ऐसे मॉडल का उपयोग कर सकते हैं जो अनुक्रमिक डेटा संसाधित करने के लिए डिज़ाइन किया गया है, जैसा कि हम अगले खंड में करेंगे।
लॉन्ग शॉर्ट-टर्म मेमोरी (LSTM)
अंतिम मॉडल जिसके बारे में हम विचार करेंगे, वह LSTM (लॉन्ग शॉर्ट-टर्म मेमोरी) मॉडल है। यह एक प्रकार का रिकरेंट न्यूरल नेटवर्क (RNN) है जिसे वैनिशिंग ग्रेडिएंट समस्या को रोकने के लिए संशोधित किया गया है। Briot, Hadjerest, और Pachet (2019) पाते हैं कि संगीत जनरेशन के उद्देश्य से रिकरेंट मॉडल सबसे आम तौर पर उपयोग किए जाते हैं। वे 20 से अधिक ऐसे संगीत जनरेशन सिस्टम की पहचान करते हैं जो RNN या LSTM मॉडल पर निर्भर हैं, जिनमें BachBot, DeepBach, Performance-RNN, और Hexahedria शामिल हैं।
LSTM मॉडल का उपयोग करने से ऑटोएन्कोडर-आधारित मॉडल पर दो लाभ मिलते हैं:
- ये अनुक्रमिक डेटा को संसाधित करने के लिए डिज़ाइन किए गए हैं।
- ये किसी भी लंबाई के संगीत अनुक्रम जनरेट कर सकते हैं।
ऑटोएन्कोडर और VAE के विपरीत, जो इनपुट्स को ही टार्गेट्स के रूप में उपयोग करते हैं, LSTM-आधारित जनरेटिव संगीत मॉडल अनुक्रम में अगले नोट या कॉर्ड के बारे में भविष्यवाणी करने के लिए प्रशिक्षित किए जाते हैं। इसी कारण, हमें अपने ट्रेन और टार्गेट सेट्स को फिर से बनाना होगा, जो हम नीचे दिए गए कोड ब्लॉक में करेंगे। चूँकि LSTM मॉडल का उपयोग करने से हमें प्रशिक्षित किए जाने वाले पैरामीटर्स की संख्या भी घटेगी, हम अवधियों को भी मॉडल में जोड़ेंगे।
# 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 और trainDurations को उसी तरह परिभाषित किया गया है जैसे ट्यूटोरियल में पहले किया था। हमने केवल targetChords और targetDurations जोड़े हैं, जो ट्रेन अनुक्रम में दिए गए कॉर्ड्स और अवधियों के बाद आने वाले कॉर्ड और अवधि से बने हैं। अगला, हम सैंपल्स, कॉर्ड्स और अवधियों की संख्या, तथा इनपुट आयाम सेट करते हैं। हम कॉर्ड्स और अवधियों के लिए अलग-अलग एम्बेडिंग लेयर्स का भी उपयोग करेंगे।
# 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
अब अगला कदम मॉडल आर्किटेक्चर को परिभाषित करना है। हम दो इनपुट और दो आउटपुट वाला मॉडल उपयोग करेंगे, ताकि हम कॉर्ड्स और अवधियों दोनों की भविष्यवाणी कर सकें। हम पहले इनपुट लेयर्स chordInput और durationInput परिभाषित करते हैं और फिर इन्हें एम्बेडिंग लेयर्स chordEmbedding और durationEmbedding में पास करते हैं। ये लुक-अप टेबल हैं जो हर कॉर्ड या अवधि के साथ 64-तत्वों वाला वेक्टर जोड़ते हैं। एम्बेडिंग हमें स्पार्स वेक्टर निरूपण (जहाँ इनपुट्स वन-हॉट एन्कोडेड होते हैं) को डेंस निरूपण से बदलने देती हैं, जहाँ संबंधित नोट्स और अवधियों के मान एक-दूसरे के अधिक निकट होते हैं।
दोनों एम्बेडिंग लेयर्स के आउटपुट्स को mergeLayer में जोड़ा (concatenate) जाता है। इसके बाद हम इस लेयर के आउटपुट को दो LSTM लेयर्स में पास करते हैं। पहली लेयर में return_sequences पैरामीटर True पर सेट है। यदि हम इस पैरामीटर को डिफॉल्ट के रूप में False रहने दें, तो LSTM सेल्स केवल अंतिम स्टेट्स लौटाएँगे, मध्यवर्ती अनुक्रमिक स्टेट्स नहीं। हालिया कार्य से पता चला है कि मध्यवर्ती स्टेट्स का उपयोग प्रदर्शन सुधार सकता है। यह अक्सर अटेन्शन लेयर के संदर्भ में किया जाता है। अंत में, हम LSTM लेयर के आउटपुट को एक डेंस लेयर में पास करते हैं, जिसे फिर दो आउटपुट लेयर्स: chordOutput और durationOutput में पास किया जाता है। फिर हम दो इनपुट और दो आउटपुट लेयर्स निर्दिष्ट करके एक keras फ़ंक्शनल API मॉडल परिभाषित करते हैं।
# 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])
मॉडल परिभाषित होने के बाद, अब हम कंपाइल कर सकते हैं और प्रशिक्षण शुरू कर सकते हैं। हमने यहाँ चीजों को यथासंभव सरल रखने की कोशिश की है, लेकिन कई अनुप्रयोगों के लिए रेग्युलराइज़ेशन लेयर्स तथा अतिरिक्त डेंस और LSTM लेयर्स शामिल करना समझ में आता है। ध्यान दें कि मॉडल में दो इनपुट और दो आउटपुट लेयर्स हैं, जैसा कि नीचे के आरेख में दिखाया गया है। आप Introduction to TensorFlow in Python में बहु-इनपुट और बहु-आउटपुट वाले मॉडलों के बारे में अधिक जान सकते हैं।

# Compile the model
lstm.compile(loss='categorical_crossentropy', optimizer='rmsprop')
# Train the model
lstm.fit([trainChords, trainDurations], [targetChords, targetDurations],
epochs=500, batch_size=64)
प्रशिक्षण प्रक्रिया पूरी होने के बाद, हम LSTM मॉडल का उपयोग करके नए गीत जनरेट कर सकते हैं। हम यह 32 कॉर्ड्स और अवधियों के एक प्रारंभिक अनुक्रम को फ़ीड करके करेंगे, जो हमें पहली भविष्यवाणियाँ करने देगा। फिर हम उन भविष्यवाणियों को कॉर्ड और अवधि श्रृंखला में जोड़ देंगे, जिससे हमें अद्यतन श्रृंखला के अंतिम 32 कॉर्ड्स और अवधियों के आधार पर एक और भविष्यवाणी करने की अनुमति मिलेगी। LSTM मॉडल का एक लाभ यह है कि हम अनिश्चित काल तक इटरेट कर सकते हैं, जिससे हम मनमानी लंबाई के गीत जनरेट कर सकते हैं।
# 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
अंतिम चरण music21 का उपयोग करके जनरेट किए गए संगीत को MIDI फ़ाइल में निर्यात करना है। ध्यान दें कि कोड लगभग उसी जैसा है जैसा हमने ऑटोएन्कोडर-आधारित मॉडलों के लिए उपयोग किया था, लेकिन एक महत्वपूर्ण अंतर के साथ: अब हम stream ऑब्जेक्ट में कॉर्ड्स और अवधियाँ दोनों जोड़ रहे हैं।
# 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')
ऑटोएन्कोडर और VAE मॉडलों के साथ, हमें मेलोडी जनरेट करने में संघर्ष करना पड़ा और हम केवल निश्चित-लंबाई के गीत आउटपुट कर सके। LSTM मॉडल का उपयोग करते हुए, हम ऊपर दिए गए कोड ब्लॉक में किए अनुसार भविष्यवाणियों पर इटरेट करके मनमानी लंबाई के आउटपुट अनुक्रम जनरेट कर सकते हैं। बेहतर मेलोडी जनरेट करना भी अपेक्षाकृत आसान होगा। हम नीचे के उदाहरणों में इन दोनों विशेषताओं को देखेंगे, जिनमें से प्रत्येक को हमारे प्रशिक्षण सेट के किसी नमूने से लिए गए कॉर्ड्स और अवधियों के प्रारंभिक अनुक्रम से आरंभ किया गया है।
उदाहरण: जनरेट किया गया गीत #1
उदाहरण: जनरेट किया गया गीत #2
उदाहरण: जनरेट किया गया गीत #3
अतिरिक्त मुद्दे
डेटा
संगीत के लिए मशीन लर्निंग मॉडल विकसित करने में प्रमुख चुनौतियों में से एक डेटासेट्स की उपलब्धता है, क्योंकि संगीत आमतौर पर स्वतंत्र रूप से और कानूनी रूप से डाउनलोड के लिए उपलब्ध नहीं होता। वास्तव में, इसने साहित्य को किसी मानक प्रैक्टिस डेटासेट — जैसे इमेज क्लासिफिकेशन के लिए MNIST — पर समन्वय करने से रोका है। फिर भी, कुछ प्रोजेक्ट ऐसे हैं जो बड़ी संख्या में गीतों के लिए कच्चा ऑडियो, सिंबॉलिक नोटेशन या व्युत्पन्न संगीत-विशेषताएँ प्रदान करते हैं। हम नीचे उन प्रोजेक्ट्स की आंशिक सूची देते हैं।
- Mutopia Project। MIDI, PDF और LilyPond प्रारूपों में सार्वजनिक डोमेन और क्रिएटिव कॉमन्स संगीत का अभिलेखागार। इसमें वर्तमान में 2000 से अधिक गीत हैं और इसे वाद्ययंत्र, संगीतकार, शैली और संग्रह के अनुसार व्यवस्थित किया गया है।
- The Million Song Dataset। लोकप्रिय संगीत ट्रैक्स के 300GB से अधिक के लिए ऑडियो फीचर्स और मेटाडेटा समेटे है, लेकिन ऑडियो ट्रैक्स नहीं।
- Free Music Archive। 100,000 से अधिक ट्रैक्स के लिए ऑडियो, जेनर लेबल्स और फीचर्स प्रदान करता है, जो स्वतंत्र रूप से और कानूनी रूप से डाउनलोड के लिए उपलब्ध हैं।
सैंपलिंग बनाम डीटरमिनिज़्म
जब हमने LSTM मॉडल का उपयोग करके संगीत जनरेट किया, तो हमने कॉर्ड्स और अवधियों के चयन के लिए एक निश्चितात्मक तरीका अपनाया। मॉडल ने 267 कॉर्ड्स और 9 अवधियों में से प्रत्येक के लिए प्रायिकताएँ अनुमानित कीं, और हमने सबसे अधिक प्रायिकता वाले कॉर्ड्स और अवधियाँ चुनीं। इसी तरह, ऑटोएन्कोडर और वैरिएशनल ऑटोएन्कोडर मॉडलों के लिए, हमने सबसे अधिक आउटपुट मान वाले कॉर्ड्स चुने।
इस दृष्टिकोण का एक विकल्प यह है कि उच्चतम प्रायिकता वाले कॉर्ड्स और अवधियों को चुनने के बजाय आउटपुट के वितरण से सैंपल लिया जाए। इससे संगीत जनरेशन प्रक्रिया स्टोकेस्टिक हो जाएगी, न कि निश्चितात्मक। इसका अर्थ यह है कि समान लैटेंट वेक्टर वाले ऑटोएन्कोडर या VAE मॉडल हर बार चलने पर अलग-अलग गीत जनरेट करेंगे। इसी तरह, इनपुट्स के समान अनुक्रम वाला LSTM मॉडल भी अलग-अलग गीत जनरेट कर सकेगा।
नीचे का कोड ब्लॉक दिखाता है कि predictChords() फ़ंक्शन को संशोधित करके हम कॉर्ड्स और अवधियों की स्टोकेस्टिक जनरेशन की ओर कैसे बढ़ सकते हैं। ध्यान दें कि हमने argmax() ऑपरेशन को सैंपलिंग ऑपरेशन से बदल दिया है, जो कॉर्ड्स और अवधियों के वेक्टरों से प्रायिकताओं का उपयोग करके यादृच्छिक ड्रॉ करता है।
# 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)
संगीत जनरेशन के लिए स्टोकेस्टिक और निश्चितात्मक दृष्टिकोणों के लाभों पर विस्तृत चर्चा के लिए, Briot, Hadjerest, और Pachet (2019) के अनुभाग 6.6 देखें।
एम्बेडिंग्स
जब हमने LSTM मॉडल को प्रशिक्षित किया, तो हमने एम्बेडिंग्स की एक लेयर का उपयोग किया, जो हर इनपुट कॉर्ड और अवधि को 64-तत्वों वाले डेंस वेक्टर से मैप करती है। एम्बेडिंग लेयर का उपयोग करने से हम स्पार्स, उच्च-आयामी वेक्टरों को इनपुट के रूप में उपयोग करने से बच सकते हैं। इसके बजाय, हम एक निम्न-आयामी निरूपण का उपयोग करते हैं जो अलग-अलग कॉर्ड्स के बीच संबंधों की पहचान करने की अनुमति देता है।
कई मामलों में, एम्बेडिंग लेयर को प्रशिक्षित करना समझ में नहीं आएगा। इसका कारण यह है कि संगीत जनरेशन समस्याओं पर काम करते समय हमारे पास अक्सर पर्याप्त बड़े प्रशिक्षण सेट नहीं होंगे। पूर्व-प्रशिक्षित एम्बेडिंग्स या असुपरवाइज़्ड लर्निंग से प्राप्त एम्बेडिंग्स — जैसा कि हम अक्सर टेक्स्ट क्लासिफिकेशन और जनरेशन समस्याओं में करते हैं — का उपयोग करने से हम उस समय भी मॉडल को प्रशिक्षित कर सकेंगे जब अन्यथा यह संभव नहीं होता।
असुपरवाइज़्ड लर्निंग दृष्टिकोण का उपयोग करके एम्बेडिंग्स कैसे निर्मित करें, इस पर एक अवलोकन के लिए Chuan, Agres, और Herremans (2018) देखें।
जेनेरेटिव एडवर्सैरियल नेटवर्क्स
हमने संगीत जनरेशन के विकल्प के रूप में ऑटोएन्कोडर्स, VAEs, और LSTM मॉडलों पर चर्चा की। एक अन्य विकल्प, जिसने अन्य डोमेनों में जनरेटिव मॉडलिंग के लिए सफलता पाई है, वह है जेनेरेटिव एडवर्सैरियल नेटवर्क (GAN)। GAN एक डिस्क्रिमिनेटिव नेटवर्क को जनरेटिव नेटवर्क के साथ जोड़कर काम करता है। जनरेटिव नेटवर्क नमूने बनाता है, जैसे कि हमारे LSTM और ऑटोएन्कोडर मॉडलों द्वारा जनरेट किए गए कॉर्ड अनुक्रम। फिर इन नमूनों को असली नमूनों — यानी वास्तविक गीतों के कॉर्ड अनुक्रम — के साथ प्रशिक्षण सेट में मिलाया जाता है। डिस्क्रिमिनेटिव मॉडल नमूनों को असली या जनरेटर द्वारा बनाए गए के रूप में वर्गीकृत करने की कोशिश करता है। जनरेटर डिस्क्रिमिनेटिव मॉडल को भ्रमित करके उसके बनाए गए नमूनों को असली गीतों के रूप में गलत वर्गीकृत कराने की कोशिश करता है। मॉडल का प्रशिक्षण तब तक किया जाता है जब तक हम एक स्थिर विकासात्मक संतुलन तक नहीं पहुँच जाते, जहाँ न तो डिस्क्रिमिनेटर और न ही जनरेटर और लाभ कमा सके।
जो लोग संगीत जनरेशन कार्यों में GANs लागू करने में रुचि रखते हैं, उनके लिए निम्न संसाधन उपयोगी हो सकते हैं:
-
MuseGAN एक प्रोजेक्ट है जिसका उद्देश्य GANs का उपयोग करके पॉलीफोनिक संगीत का जनरेशन है। इसमें प्री-ट्रेन मॉडल्स और पियानो-रोल प्रारूप में 174,154 साफ़-सुधरी और तैयार रचनाएँ bundled हैं।
-
Magenta एक ओपन सोर्स प्रोजेक्ट है जिसका उद्देश्य रचनात्मक कार्यों — जिनमें संगीत की रचना भी शामिल है — में जनरेटिव मॉडलिंग का अनुप्रयोग है। आप Colab नोटबुक के माध्यम से Magenta के टूल्स के सेट को एक्सप्लोर कर सकते हैं।
संसाधन
जो लोग TensorFlow 2.0 के बारे में अधिक सीखने में रुचि रखते हैं, वे DataCamp पर Introduction to TensorFlow in Python देखें। संगीत जनरेशन में डीप लर्निंग विधियों की हालिया और व्यापक समीक्षा के लिए Briot, Hadjerest, और Pachet (2019) देखें। डीप लर्निंग मॉडलों से संगीत जनरेट करने पर अतिरिक्त ट्यूटोरियल के लिए 1, 2, 3, 4, और 5 देखें, जो जनरेटिव मॉडलों का पूर्ण पाठ्यपुस्तक-स्तरीय उपचार प्रदान करता है।
परिशिष्ट
निरंतर निरूपण
WAV फ़ाइलें सबसे सामान्य निरंतर निरूपण हैं क्योंकि इनमें असंपीड़ित, कच्चा ऑडियो सिग्नल होता है। यह mp3 फ़ाइलों से भिन्न है, उदाहरण के लिए, जो ऑडियो सिग्नल की जानकारी को हटाकर फ़ाइल के आकार को संपीड़ित करती हैं।
नीचे का कोड ब्लॉक दिखाता है कि Mozart द्वारा रचित संगीत वाली WAV फ़ाइल से सैंपल रेट rate और कच्चा ऑडियो सिग्नल signal कैसे निकाला जा सकता है। सैंपल रेट वह संख्या है, जितनी बार ऑडियो सिग्नल को प्रति सेकंड सैंपल किया जाता है। हमारे मामले में, यह मान 44100 है।
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))
कच्चा ऑडियो सिग्नल signal, S x C टेन्सर है, जहाँ S सैंपल्स की संख्या और C चैनलों की संख्या है। सरलता के लिए, हम उस मामले पर विचार करेंगे जहाँ C = 1 है। नीचे का प्लॉट signal के तीन सेकंड के अंतराल को दिखाता है।

सामान्य तौर पर, कच्चे ऑडियो सिग्नल का उपयोग करके किसी मॉडल को प्रशिक्षित करना असतत निरूपण का उपयोग करने की तुलना में काफी अधिक चुनौतीपूर्ण होता है। हालिया सर्वेक्षण ने साहित्य में संगीत जनरेशन की 32 अलग-अलग डीप लर्निंग विधियों की जाँच की। इनमें से, केवल तीन ने निरंतर निरूपण का उपयोग किया:
की ट्रांसपोज़िशन
एक तरीका जिसे आयामीयता घटाने या डेटा ऑग्मेंटेशन दोनों उद्देश्यों के लिए उपयोग किया जा सकता है, वह है की ट्रांसपोज़िशन। इसमें हर नोट की पिच को एक निश्चित इंटरवल से शिफ्ट करना शामिल है। उदाहरण के लिए, यदि किसी गीत में पिच C3 के बाद D3 आती है और हम सभी अवयवों को दो पिच ऊपर शिफ्ट करने वाली ट्रांसपोज़िशन का उपयोग करते हैं, तो C3 D3 बन जाएगा और D3 E3 बन जाएगा। की ट्रांसपोज़िशन का उपयोग करने वाले मॉडलों के लिए Lim, Rhyu, और Lee (2018) तथा Sturm et al. (2016) देखें।
यदि हम आयामीयता घटाने के उद्देश्य से की ट्रांसपोज़िशन का उपयोग करते हैं, तो हमारा लक्ष्य नोट्स और कॉर्ड्स को इस प्रकार शिफ्ट करना होगा कि हमारे डेटासेट में अद्वितीय नोट्स और कॉर्ड्स की कुल संख्या न्यूनतम हो जाए। चूँकि संगीत आमतौर पर कुछ पिच समूहों — जिन्हें कीज़ कहते हैं — के इर्द-गिर्द लिखा जाता है जो साथ में अच्छा सुनाई देते हैं, हम सुविधानुसार अपने डेटासेट के प्रत्येक गीत को एक ही की में शिफ्ट कर सकते हैं।
इस बार, हम music21 से तीन सबमॉड्यूल आयात करेंगे:
converter, जिसका उपयोग हम MIDI फ़ाइल लोड करने और उसेstreamऑब्जेक्ट में बदलने के लिए करेंगे।pitch, जिसका उपयोग हम अपनी लक्षित पिच बनाने के लिए करेंगे।interval, जिसका उपयोग हम अपनी मूल की और लक्षित की के बीच दूरी की गणना करने के लिए करेंगे।
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
हम पहले score पर .analyze('key') मेथड लागू करते हैं, जो stream ऑब्जेक्ट की की की पहचान करता है, जो C मेजर है। फिर हम C मेजर और लक्षित की F मेजर के बीच इंटरवल की गणना करते हैं। ध्यान दें कि हम key ऑब्जेक्ट की पिच तक .tonic का उपयोग करके पहुँचते हैं। साथ ही, हम लक्षित पिच pitch.Pitch('F') का उपयोग करके बनाते हैं। फिर हम आउटपुट को keyInterval को असाइन करते हैं, जो हमें हमारी मूल और लक्षित की के बीच दूरी बताता है।
अंत में, हम .transpose() मेथड और इंटरवल keyInterval पास करके की को ट्रांसपोज़ करते हैं। नई की को प्रिंट करने से पुष्टि होती है कि वह F मेजर है।
ऑक्टेव ट्रांसपोज़िशन
यदि हम की ट्रांसपोज़िशन से आगे बढ़ना चाहें, तो हम ऑक्टेव को भी ऊँचा या नीचा ट्रांसपोज़ कर सकते हैं। उदाहरण के लिए, C मेजर को F मेजर में ट्रांसपोज़ करने के बजाय, हम C मेजर को किसी निम्न या उच्च ऑक्टेव में C मेजर में ट्रांसपोज़ कर सकते हैं। यदि हम प्रत्येक गीत के लिए एक ही सेट के ऑक्टेव लक्षित करते हैं, तो हम अपने सभी नोट्स और कॉर्ड्स को एक साथ क्लस्टर करने के लिए बाध्य करेंगे, जिससे समस्या की आयामीयता घटेगी और मॉडल प्रशिक्षण प्रक्रिया सरल होगी। ऊपर दिए गए कोड ब्लॉक में, हम यह पिच का ऑक्टेव निर्दिष्ट करके कर सकते हैं। उदाहरण के लिए, इंटरवल की गणना के लिए pitch.Pitch('F') निर्दिष्ट करने के बजाय, हम pitch.Pitch('F3') का उपयोग कर सकते हैं।
की और ऑक्टेव इनवेरियंस
आयामीयता घटाने का एक स्वाभाविक विकल्प डेटा ऑग्मेंटेशन है। यानी, संभावित इनपुट्स और आउटपुट्स के सेट को घटाने के बजाय, हम मौजूदा डेटा में बदलाव करके डेटासेट का विस्तार करते हैं। हम की और ऑक्टेव ट्रांसपोज़िशन का उपयोग करके भी ऐसा कर सकते हैं। हालाँकि, सभी गीतों को एक सामान्य की में और उनके नोट्स को आसन्न ऑक्टेव्स में क्लस्टर करने के बजाय, हम सभी गीतों को सभी कीज़ और सभी ऑक्टेव्स में ट्रांसपोज़ करेंगे। इससे हम विकसित किए गए मॉडल में की और ऑक्टेव इनवेरियंस प्राप्त कर सकेंगे। इन दो अलग-अलग उद्देश्यों के लिए ट्रांसपोज़िशन के उपयोग पर चर्चा के लिए Briot, Hadjerest, और Pachet (2019) देखें।
डेटा संग्रह
हम अपने मॉडल को प्रशिक्षित करने के लिए MIDI फ़ाइलें Project Mutopia से इकट्ठा करेंगे। कोई भी स्क्रैपिंग करने से पहले, हम पहले robots फ़ाइल — https://www.mutopiaproject.org/robots.txt — जाँचेंगे, ताकि यह सुनिश्चित हो सके कि हम वेबसाइट के साथ स्वचालित इंटरेक्शन को संभालने के सभी अनुरोधों का सम्मान करें। जिस समय यह ट्यूटोरियल लिखा गया था, robots फ़ाइल में नीचे दिए गए कोड ब्लॉक का टेक्स्ट था। User-agent: * इंगित करता है कि जो नियम आगे आता है, वह वेबसाइट के साथ इंटरेक्ट करने वाले सभी उपयोगकर्ताओं पर लागू होता है। Disallow: के बाद कुछ न होना दर्शाता है कि स्क्रैपिंग पर कोई प्रतिबंध नहीं है।
# Allow crawling of all content
User-agent: *
Disallow:
हम पहले urllib.request से urlopen और urlretrieve आयात करेंगे, जिनका उपयोग हम गिटार अनुभाग के प्रत्येक पेज के लिए GET अनुरोध भेजने और फिर पेज पर लिंक की गई प्रत्येक MIDI फ़ाइल डाउनलोड करने के लिए करेंगे। हम BeautifulSoup आयात करेंगे ताकि urlopen द्वारा लौटाए गए HTML को पार्स किया जा सके, जिससे हमें MIDI फ़ाइल लिंक की पहचान करने में मदद मिलेगी। अंत में, हम time मॉड्यूल का उपयोग हर डाउनलोड के बीच 10 सेकंड का विराम लेने के लिए करेंगे ताकि वेबसाइट के संसाधनों पर दबाव न पड़े।

मुख्य लूप का हर चरण यह जाँच करेगा कि पेज पर लिंक की संख्या linkCount 0 से अधिक है या नहीं। यदि हाँ, तो यह अगले पेज के लिए URL url का निर्माण करेगा, url0, url1 स्ट्रिंग्स और गीत संख्या songNumber का उपयोग करते हुए। फिर यह URL खोलेगा, HTML पार्स करेगा, पेज पर सभी लिंक ढूँढेगा, linkCount को 0 पर रीसेट करेगा, और फिर सूची में प्रत्येक लिंक से गुज़रेगा। यदि किसी लिंक में .mid उपस्ट्रिंग है, तो वह एक MIDI फ़ाइल की ओर इशारा करता है और urlretrieve() का उपयोग करके save_dir द्वारा निर्दिष्ट डायरेक्टरी में डाउनलोड किया जाता है। अंततः, हम songNumber को 10 से बढ़ाते हैं, क्योंकि हर पेज में 10 गीत होते हैं। यह प्रक्रिया तब तक दोहराते हैं जब तक हमें ऐसा पेज नहीं मिलता जिसमें 10 से कम लिंक हों।
!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)
स्क्रैपिंग के बारे में अधिक जानने के इच्छुक लोगों के लिए, DataCamp का Web Scraping with Python कोर्स और Web Scraping using Python ट्यूटोरियल देखें।
