Vai al contenuto principale

Using TensorFlow 2.0 to Compose Music Tutorial

In questo tutorial imparerai ad addestrare modelli generativi per comporre musica con TensorFlow 2.0.
Aggiornato 29 set 2026  · 15 min leggi

Scopri con l'IA

ChatGPTClaudePerplexity

banner introduzione a tensorflow in python

Questo tutorial è stato sviluppato attorno a TensorFlow 2.0 in Python, insieme alla Keras API di alto livello, che in TensorFlow 2.0 ricopre un ruolo ancora più centrale. Se vuoi saperne di più su TensorFlow 2.0, dai un'occhiata a Introduction to TensorFlow in Python su DataCamp. Per una rassegna esaustiva della letteratura sul deep learning applicato alla musica, vedi Briot, Hadjerest e Pachet (2019), a cui faremo riferimento in questo tutorial.

Modelli generativi

I modelli di machine learning supervisionato si dividono in due categorie: modelli discriminativi e modelli generativi. I modelli discriminativi individuano un confine decisionale e producono una classificazione corrispondente. I modelli generativi creano nuove istanze di una classe.

Un modello discriminativo della musica potrebbe essere usato per classificare i brani in generi diversi. Un modello generativo potrebbe comporre brani di un genere specifico. In questo tutorial useremo modelli generativi per comporre musica.

Discriminativo vs. Generativo
Questa figura mostra modelli discriminativi e generativi della musica.

Rappresentazione della musica

Prima di definire e addestrare un modello generativo, dobbiamo assemblare un dataset. Per la musica, i dati possono essere rappresentati in forma continua o discreta. La forma continua più comune è il segnale audio, tipicamente salvato come file WAV. In questo tutorial non useremo forme continue, ma puoi approfondire nell'Appendice. Le forme discrete più comuni includono file Musical Instrument Digital Interface (MIDI), pianoroll e testo. Ci concentreremo sui file MIDI ed estrarremo due tipi di oggetti simbolici: note e accordi.

Note

Una nota è una rappresentazione simbolica di un suono. Ai nostri fini, una nota può essere descritta dall'altezza e dalla durata. L'altezza di una nota è legata alla frequenza di oscillazione dell'onda sonora, misurata in hertz (Hz). Note con altezza maggiore hanno onde sonore con più oscillazioni al secondo. La durata di una nota è la lunghezza del periodo in cui viene eseguita.

I file MIDI rappresentano l'altezza di una nota con un intero tra 0 e 127. Le note possono anche essere rappresentate da una lettera e dal numero dell'ottava. All'interno della stessa ottava, le altezze sono ordinate dalla frequenza più bassa alla più alta come segue:

  1. C
  2. C#/D♭
  3. D
  4. E♭/D#
  5. E
  6. F
  7. F#/G♭
  8. G
  9. A♭/G#
  10. A
  11. B♭/A#
  12. B

L'ottava è indicata da un pedice, come il 4 in A4 o il 7 in C7. Un'ottava più alta corrisponde a una frequenza più alta. Se prendiamo un'altezza arbitraria Xi, allora l'altezza Xi+1, esattamente un'ottava sopra, rappresenta un'onda sonora con frequenza doppia rispetto a Xi.

Oltre all'altezza di una nota, useremo anche la sua durata. La durata è un valore relativo, normalizzato sulla lunghezza della semibreve. La nota più lunga è la "grande", che è otto volte una semibreve. La nota più corta è la duecentocinquantaseiesima, pari a 1/256 della semibreve.

Accordi

Un accordo è una combinazione di due o più note suonate simultaneamente sullo stesso strumento. Se guardiamo musica monofonica — cioè suonata da un singolo strumento — possiamo identificare gli accordi assumendo che tutte le note suonate esattamente nello stesso istante facciano parte dello stesso accordo. Questa assunzione non vale per la musica polifonica, che consiste in due o più strumenti che suonano simultaneamente.

Nel blocco di codice seguente installiamo music21 e poi importiamo il modulo converter, che useremo per analizzare i file MIDI. Caricheremo e analizzeremo un brano per chitarra classica di Mauro Giuliani. Applicheremo quindi il metodo .chordify(), che ricostruisce la sequenza di accordi nello spartito, assumendo che le note suonate simultaneamente facciano parte dello stesso accordo.

!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())

Dopo aver costruito la sequenza di accordi nel file MIDI, possiamo visualizzarla come spartito usando il metodo .show(). Questo richiede un software in grado di creare e visualizzare spartiti, come MuseScore, disponibile gratuitamente.

Spartito generato da MIDI
Questa figura mostra un file MIDI visualizzato come spartito.

In alternativa, potremmo voler esaminare e manipolare gli elementi sottostanti come testo. Possiamo farlo usando il metodo .show('text'). All'inizio di ogni riga vediamo l'offset tra parentesi, che indica dopo quanti secondi dall'inizio dello spartito compare un elemento. Nota che solo uno degli elementi al secondo 0 è un accordo. Gli altri sono metadati, incluso lo strumento usato, il tempo, la tonalità e il metro. Più avanti useremo la tonalità, ma per ora puoi ignorare questi elementi.

 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>

Dopo i metadati, gli elementi rimanenti sono accordi, ordinati nella sequenza in cui compaiono nel brano. Ad esempio, <music21.chord.Chord E3 G3> è l'accordo prodotto suonando le note E3 e G3. Inoltre, <music21.chord.Chord A3> è la nota A3 suonata isolatamente.

Per alcuni modelli useremo sia l'accordo che la sua durata, che misura per quanto tempo viene suonato. Possiamo ottenere queste informazioni applicando i metodi .elements e accedendo all'attributo .duration per un accordo qualsiasi nello spartito, come mostrato nel blocco di codice qui sotto.

print(score.elements[10])
<music21.chord.Chord E3 A3>
print(score.elements[10].duration)
<music21.duration.Duration 1.0>

Codifica di note e accordi

In tutto il tutorial ci concentreremo su rappresentazioni discrete della musica, descrivibili come sequenze di note, accordi e relative durate. Ricorda che gli accordi sono combinazioni di note e che il formato MIDI consente 128 note individuali. Se vogliamo ampliare il set di feature per includere tutti gli accordi a due note, la sua dimensione aumenta di 128!/[(128-2)!*2!], ovvero 8128 elementi. Aggiungere gli accordi a tre note lo espande di 341.376, e aggiungere quelli a quattro note di altri 10.668.000.

L'esempio mostrato in precedenza contiene note e accordi a due, tre e quattro note. Se usiamo la one-hot encoding e includiamo tutte le feature, dovremo fare previsioni su vettori a 11.017.632 dimensioni. Di conseguenza, è importante riflettere con attenzione sulla scelta della codifica nei problemi di generazione musicale. Potremmo anche considerare l'uso di trasposizioni di tonalità o data augmentation, entrambe discusse nell'Appendice.

Per quanto riguarda la scelta della codifica, Briot, Hadjerest e Pachet (2019) discutono quattro opzioni comunemente usate nei modelli generativi di musica:

  1. One-Hot Encoding
  2. Many-Hot Encoding
  3. Multi-One-Hot Encoding
  4. Multi-Many-One-Hot Encoding

La one-hot encoding rappresenta simboli musicali, come note e accordi, come vettori sparsi con un uno nella posizione corrispondente a una specifica nota o a uno specifico accordo. La many-hot encoding rappresenta gli accordi, composti da più note, ponendo un uno nella posizione corrispondente a ciascuna delle note.

Nello schema sottostante confrontiamo many-hot e one-hot per il caso in cui abbiamo note e accordi a due note. Con le many-hot, le note C4 e D4 sono rappresentate da vettori con 128 elementi. Entrambi i vettori hanno zeri in tutte le posizioni tranne in quelle corrispondenti agli elementi C4 e D4, che contengono uno. L'accordo C4+D4, prodotto suonando simultaneamente C4 e D4, si costruisce ponendo degli uno sia nelle posizioni C4 che D4.

Codifica di note e accordi
Questo schema illustra la differenza tra one-hot e many-hot encoding.

La rappresentazione one-hot di note singole e accordi a due note consiste in vettori con 8256 elementi. In ogni caso, un vettore è sparso e contiene un 1 nella posizione corrispondente a una nota o a un accordo univoco. Nota che la rappresentazione many-hot richiede l'uso di un modello multi-etichetta, in cui possono essere previste più classi per una data uscita. Riduce però in modo sostanziale la dimensione dei vettori di input e output.

La maggior parte dei modelli in letteratura usa ancora la one-hot encoding. Tuttavia, Briot, Hadjerest e Pachet (2019) identificano diversi modelli di generazione musicale che utilizzano la many-hot encoding, tra cui i sistemi RBMc, RNN-RBM e C-RBM, tutti basati su un'architettura a restricted Boltzmann machine (RBM).

Le codifiche multi-one-hot e multi-many-hot sono le generalizzazioni di one-hot e many-hot al caso di musica polifonica — cioè con più strumenti che suonano simultaneamente. Qui ci limiteremo alla musica monofonica e non tratteremo oltre queste opzioni.

Raccolta e preparazione dei dati

Il passo successivo è raccogliere un dataset da usare per addestrare un modello generativo. Useremo i dati del Mutopia Project, che contiene 2124 brani in formato MIDI di pubblico dominio. Poiché il sito è suddiviso per strumento, possiamo scrivere uno script Python relativamente semplice per scaricare tutte le composizioni per chitarra, che useremo per addestrare i modelli in questo tutorial. Per una panoramica su come farlo, vedi l'Appendice.

Supponiamo di aver salvato tutti i file .mid dalla sezione chitarra in una directory, save_dir. Il prossimo passo è caricare ciascun file .mid in music21 come oggetto 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)

Come parte della nostra strategia di riduzione della dimensionalità, ci limiteremo ai brani composti da Mauro Giuliani. Possiamo farlo modificando il blocco di codice sopra come segue.

# Identify list of MIDI files
songList = os.listdir(save_dir)
songList = [song for song in songList if song.lower().find('giuliani')>-1]

Prima di iniziare l'addestramento dobbiamo fare altre quattro cose:

  1. Rimuovere la musica polifonica e applicare .chordify() agli oggetti stream rimanenti.
  2. Estrarre la sequenza di note, accordi e durate.
  3. Costruire una mappa che traduca note, accordi e durate in interi e viceversa.
  4. Dividere gli stream in sequenze di lunghezza fissa da usare come input ai modelli.

Iniziamo rimuovendo la musica polifonica dal dataset. Possiamo farlo partizionando ogni oggetto stream per strumento e poi verificando se c'è una sola partizione — cioè se sta suonando un solo strumento. Lo facciamo nel blocco di codice seguente.

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]

Nota che se applichiamo .chordify() — come mostrato nella figura sotto — senza rimuovere la musica polifonica, finiremo per unire accidentalmente in un accordo note suonate da strumenti diversi.

Conversione di note in accordi
Questa figura mostra la conversione delle note in accordi.

Ora dobbiamo estrarre note, accordi e durate da ogni stream. Definiamo prima tre liste vuote: originalChords, originalDurations, originalKeys. Possiamo enumerare la lista degli stream. Per ciascuno, identificheremo e salveremo tutte le note, gli accordi e le durate, oltre alla tonalità del brano.

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

Eseguiremo ulteriore riduzione della dimensionalità mantenendo solo la musica con tonalità in Do maggiore, che è la più frequente nel nostro dataset. Inoltre, nel resto del tutorial useremo la parola "accordo" per riferirci sia agli accordi sia alle note.

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

Il passo successivo è identificare l'insieme unico di accordi e durate. Costruiremo quindi dei dizionari che li mappino a interi. Se stampiamo il numero di note e accordi unici, possiamo vedere che i passaggi di riduzione della dimensionalità effettuati hanno ridotto il numero a un gestibile 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

Dopo aver addestrato il modello e ottenuto le previsioni, vorremo mappare i nostri interi previsti di nuovo in note, accordi e durate. Invertiremo i dizionari chordToInt e durationToInt qui sotto.

# Invert chord and duration dictionaries
intToChord = {i: c for c, i in chordToInt.items()}
intToDuration = {i: c for c, i in durationToInt.items()}

Infine, possiamo definire le sequenze di training, costituite da 32 note e accordi consecutivi, insieme alle rispettive durate. Lo faremo prima per modelli di tipo autoencoder, in cui il set di feature e i target coincidono.

# 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])

Generazione musicale

In questa sezione useremo il dataset assemblato e preparato per generare musica. Considereremo tre modelli diversi di generazione, partendo dal più semplice:

  1. L'Autoencoder
  2. Il Variational Autoencoder (VAE)
  3. Il modello Long Short-Term Memory (LSTM)

Autoencoder

Un autoencoder è composto da due reti, impilate verticalmente e collegate da un vettore latente. Gli input di un autoencoder vengono prima passati a un modello di encoder, tipicamente costituito da uno o più strati densi. L'ultimo strato nell'encoder è chiamato vettore latente. Si tratta di un collo di bottiglia che costringe le feature estratte nell'encoder a comprimersi in un piccolo numero di feature latenti.

Il vettore latente si collega a un modello di decoder, che esegue l'upsampling o la decompressione dei dati. Il numero di nodi in ciascun strato del decoder aumenta progressivamente. Lo strato di output finale ha le stesse dimensioni dello strato di input del modello di encoder.

A differenza dei modelli discriminativi, che cercano di individuare un confine decisionale, gli autoencoder sono addestrati a ricostruire i dati di input — in questo caso, un brano — nel modo più accurato possibile, soggetto ai vincoli imposti dalla dimensione del vettore latente. Per questo motivo, gli autoencoder usano semplicemente gli input come target.

Useremo un autoencoder per costruire il nostro primo modello generativo di musica. Per mantenere le cose il più semplici possibile, adotteremo l'architettura del modello MiniBach introdotta in Briot, Hadjerest e Pachet (2019) come semplificazione del modello DeepBach. Per un altro esempio di autoencoder per la generazione musicale, vedi il sistema DeepHear.

I dati di input, come li abbiamo costruiti, consistono in vettori one-hot che rappresentano note e accordi. Per ora ignoreremo le durate. Seguendo l'architettura MiniBach, inizieremo appiattendo il vettore di input.

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

Definiamo poi il numero di campioni, nSamples, il numero di accordi e note, nChords, e la dimensione dell'input, inputDim. Impostiamo anche il numero di feature latenti, latentDim, a 2. Usare un numero basso di dimensioni latenti ridurrà le prestazioni, ma ci permetterà di generare con facilità visualizzazioni dello spazio latente.

# 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

Ora definiamo l'architettura del modello, partendo dallo strato di input per la rete di encoder, encoderInput, e dallo strato di input per la rete di decoder, latent. Nota che lo strato latent è anche l'output della rete di encoder. Definiamo quindi uno strato denso, encoded, che collega gli input al vettore latente e un altro strato denso, decoded, che collega il vettore latente al vettore di output.

Infine, definiamo un modello di decoder, decoder, e il nostro autoencoder, autoencoder. Nota che .Model() ha argomenti inputs e outputs. Per il modello decoder passiamo il vettore latent come input e la rete decoded come output. Per l'autoencoder completo usiamo encoderInput come input e passiamo il modello decoder, valutato usando l'output della rete di encoder, encoded, come input.

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

L'architettura di un autoencoder generico è mostrata nella figura seguente. La metà superiore della rete è l'encoder, quella inferiore il decoder. Sono collegati dal vettore latente.

Architettura di rete di un autoencoder
Questa figura mostra l'architettura di un autoencoder.

Definita l'architettura di decoder e autoencoder, non resta che compilare il modello e addestrarlo con il metodo .fit(). Ci sono due dettagli importanti da notare:

  1. I nostri valori di input saranno 0 e 1, e i valori previsti saranno numeri reali tra 0 e 1. Possiamo usare una funzione di loss binary_crossentropy.
  2. Sia i valori in input che i target coincidono: trainChordsFlat.
# Compile autoencoder model
autoencoder.compile(loss = 'binary_crossentropy', learning_rate = 0.01, optimizer='rmsprop')

# Train autoencoder
autoencoder.fit(trainChordsFlat, trainChordsFlat, epochs = 500)

L'ultimo passaggio è generare musica. Dato che abbiamo impostato la lunghezza della sequenza a 32, l'autoencoder prenderà 32 accordi e note come input e produrrà un "brano" a lunghezza fissa costituito da una sequenza di 32 accordi e note. L'autoencoder è addestrato a generare output molto simili all'input; il nostro obiettivo però è generare musica nuova. Per farlo, passeremo un vettore latente generato casualmente al modello decoder, che abbiamo definito come sotto-rete del modello autoencoder.

# Generate chords from randomly generated latent vector
generatedChords = decoder(np.random.normal(size=(1,latentDim))).numpy().reshape(nChords, sequenceLength).argmax(0)

Nota che abbiamo ridimensionato l'output dell'autoencoder in un array con le stesse dimensioni dell'input originale. Abbiamo poi applicato argmax() sulla prima dimensione dell'array. Questo restituisce un valore intero che corrisponde a un accordo, che identificheremo nel blocco di codice seguente.

# Identify chord sequence from integer sequence
chordSequence = [intToChord[c] for c in generatedChords]

Infine, possiamo creare un oggetto stream usando music21, impostare la chitarra come strumento, aggiungere la sequenza di accordi generata dal modello ed esportare tutto come file 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')

La gif sotto mostra parte del processo di addestramento di un autoencoder. Lo spartito generato a ogni passo è la ricostruzione associata a uno specifico vettore di stato latente. Ogni frame rappresenta 5 epoche di training.

Autoencoder: evoluzione di note e accordi
Questa figura mostra gli spostamenti nello spazio latente nel tempo.

Possiamo anche visualizzare il movimento nello spazio latente durante l'addestramento, come nella gif sotto. Nota che lo spazio latente è stretto e deriva nel corso delle epoche. Questo renderà difficile estrarre a caso uno stato latente di alta qualità e sarà particolarmente problematico quando useremo un vettore latente di dimensione maggiore. Nella prossima sezione vedremo come risolvere questo problema con un variational autoencoder (VAE).

Autoencoder: evoluzione dello spazio latente
Questa figura mostra gli spostamenti nello spazio latente nel tempo.

Infine, possiamo esaminare l'output audio del nostro autoencoder. Per prima cosa diamo la sequenza di accordi del brano originale, estratto a caso dal nostro training set. Generiamo poi una sequenza di accordi dall'autoencoder usando il vettore latente associato alla sequenza del brano originale, ottenendo il brano riportato sotto.

Esempio: brano originale
Esempio: brano generato

Variational Autoencoder

Pur essendo adatti a compiti di denoising, compressione e decompressione, gli autoencoder non si comportano bene come modelli generativi. I variational autoencoder (VAE), introdotti da Kingma e Welling (2017), sono progettati per superare le debolezze degli autoencoder come modelli generativi e sono uno dei metodi più usati per la generazione musicale. Briot, Hadjerest e Pachet (2019) identificano quattro sistemi di generazione musicale basati su VAE: MusicVAE, VRAE, VRASH e GSLR-VAE. Migliorano gli autoencoder su due fronti:

  1. Invece di mappare ogni set di input a un punto nello spazio latente, un VAE mappa ogni set di input a una distribuzione normale, caratterizzata da media e varianza.
  2. Invece di consentire allo spazio latente qualsiasi media e varianza, un VAE forza media e logaritmo naturale della varianza ad avvicinarsi a 0.

Possiamo introdurre la prima modifica sostituendo il vettore di stato latente nell'autoencoder con tre strati:

  1. Uno strato della media
  2. Uno strato della (log) varianza
  3. Uno strato di campionamento

Ogni set di input viene mappato a una singola media e al logaritmo naturale della varianza, sufficienti a caratterizzare una normale. Il modello quindi estrae casualmente punti dalla distribuzione nello strato di campionamento.

Il blocco di codice seguente definisce una funzione che può essere usata per creare uno strato di campionamento. Prende media e log varianza come input. Usa poi il sottomodulo random di tensorflow per estrarre un tensore di punti, epsilon, da una normale standard, che ha sia media sia log varianza pari a 0. Vogliamo quindi trasformare queste estrazioni in campioni da distribuzioni caratterizzate dai parametri mean e logVar. Per farlo, dobbiamo prima scalare i campioni, epsilon, per le deviazioni standard delle distribuzioni associate. Possiamo calcolare queste deviazioni standard dividendo logVar per 2 e poi esponenziandolo. Aggiungiamo quindi le medie delle distribuzioni, 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

Ora possiamo modificare il modello di autoencoder per incorporare gli strati mean, logVar e sampling. Nota che lo strato encoded ora ha due input e lo strato encoder ha tre output.

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

Come per il modello autoencoder, il nostro stato latente è l'output dello strato encoded. C'è però una differenza importante: invece di mappare ogni set di input a un singolo stato latente, usiamo lo strato di sampling per mapparli a valori casuali estratti da una normale con parametri mean e logVar. Questo significa che lo stesso input sarà associato a una distribuzione di stati latenti.

L'ultimo passaggio è adeguare la funzione di loss per imporre vincoli sulla scelta dei valori di mean e logVar da parte del modello. In particolare, calcoleremo la divergenza di Kullback-Leibler (KL) tra ciascuna distribuzione definita dagli strati mean e logVar e la normale standard, che ha media e log varianza pari a 0. Più media e log varianza si allontanano da 0, maggiore sarà la componente di loss KL.

Perché forzare ogni distribuzione a essere vicina a una normale standard? Perché vogliamo campionare stati latenti e usarli per generare musica. Se applichiamo la penalizzazione KL alla loss, questo ci permetterà di usare estrazioni indipendenti da una normale standard per ottenere stati latenti che con buona probabilità generano brani di qualità.

Nel blocco di codice sotto mostriamo come modificare la funzione di loss per incorporare il termine KL. La prima componente della loss è la binary crossentropy, calcolata tra input e output del modello vae. È chiamata "reconstruction loss" perché ci penalizza se non ricostruiamo il brano originale. Il termine KL è la media della divergenza KL tra le distribuzioni nello spazio latente e la normale standard. Combiniamo i due per calcolare la loss totale, vaeLoss, la aggiungiamo al modello, compiliamo e poi addestriamo.

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

Nella sezione precedente abbiamo mostrato che lo spazio latente dell'autoencoder non era ben comportato. Parte dello scopo di un VAE è superare questo difetto progettuale negli scenari generativi. La figura sotto mostra lo spazio latente per il VAE. Si nota una dispersione significativa su entrambe le feature. Inoltre, entrambe sono centrate intorno a 0 e non derivano nel corso delle epoche. Questo renderà molto più facile campionare a caso uno stato latente di alta qualità.

VAE: evoluzione dello spazio latente
Questa figura mostra gli spostamenti nello spazio latente nel tempo.

Infine passiamo alla generazione musicale. Estraiamo un vettore latente casuale dalla normale standard, lo passiamo al decoder, ridimensioniamo l'output e prendiamo l'argmax di ogni colonna. Questo restituirà una sequenza di 32 interi, che corrispondono a note e accordi, che identifichiamo poi usando il dizionario intToChord. Quindi definiamo un oggetto stream, aggiungiamo la chitarra come strumento, aggiungiamo la sequenza di note e accordi allo stream ed esportiamo il tutto come file 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')

I tre brani sotto sono stati generati dal modello. I primi due si basano su stati latenti associati a sequenze di accordi del training set. Il terzo è una combinazione lineare degli stati latenti del brano n. 1 e del brano n. 2. Poiché il VAE forza lo spazio latente a essere ben comportato, possiamo essere ragionevolmente certi che una combinazione lineare di stati latenti produca uno stato di alta qualità.

Esempio: brano generato n. 1
Esempio: brano generato n. 2
Esempio: combinazione lineare dei brani n. 1 e n. 2

Una cosa che possiamo osservare dalla musica generata con autoencoder e VAE è la mancanza di una melodia forte. Possiamo migliorare aggiungendo più strati al modello e aumentando la dimensione del training set. In alternativa, possiamo usare un modello progettato per elaborare dati sequenziali, come faremo nella sezione seguente.

Long Short-Term Memory (LSTM)

L'ultimo modello che considereremo è il long short-term memory (LSTM). Si tratta di un tipo di rete neurale ricorrente (RNN) modificata per evitare il problema del gradiente che svanisce. Briot, Hadjerest e Pachet (2019) rilevano che i modelli ricorrenti sono i più comunemente usati per la generazione musicale. Identificano oltre 20 sistemi di generazione che si basano su un modello RNN o LSTM, tra cui BachBot, DeepBach, Performance-RNN e Hexahedria.

Usare un modello LSTM offre due vantaggi rispetto a un modello basato su autoencoder:

  1. Sono progettati per elaborare dati sequenziali.
  2. Possono generare sequenze musicali di qualsiasi lunghezza.

A differenza di autoencoder e VAE, che usano gli input come target, i modelli LSTM per la generazione musicale vengono addestrati a prevedere la nota o l'accordo successivo nella sequenza. Per questo motivo, dovremo ricostruire i nostri set di train e target, come faremo nel blocco di codice seguente. Poiché usare un LSTM ridurrà anche il numero di parametri da addestrare, aggiungeremo anche le durate al modello.

# 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])

Nota che trainChords e trainDurations sono definiti nello stesso modo usato in precedenza nel tutorial. Abbiamo solo aggiunto targetChords e targetDurations, che consistono nell'accordo e nella durata che seguono quelli nella sequenza di training. Impostiamo poi il numero di campioni, accordi e durate, nonché la dimensione dell'input. Useremo anche strati di embedding separati per accordi e durate.

# 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

Il passo successivo è definire l'architettura del modello. Useremo un modello con due input e due output, così da prevedere sia accordi sia durate. Definiamo prima gli strati di input, chordInput e durationInput, e poi passiamo questi strati a strati di embedding, chordEmbedding e durationEmbedding. Si tratta di tabelle di lookup che associano un vettore a 64 elementi a ciascun accordo o durata. Gli embedding ci consentono di sostituire una rappresentazione sparsa, in cui gli input sono one-hot, con una densa, in cui note e durate correlate hanno valori più vicini tra loro.

Gli output dei due strati di embedding vengono concatenati in mergeLayer. Passiamo poi l'output di questo strato a due strati LSTM. Nel primo impostiamo il parametro return_sequences a True. Se lo lasciamo a False, come avviene di default, le celle LSTM restituiranno solo gli stati finali e non gli stati sequenziali intermedi. Lavori recenti mostrano che usare gli stati intermedi può migliorare le prestazioni. Questo avviene spesso con uno strato di attention. Infine, passiamo l'output dello strato LSTM a uno strato denso, quindi a due strati di output: chordOutput e durationOutput. Definiamo poi un modello con l'API funzionale di keras specificando i due strati di input e i due di output.

# 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])

Definito il modello, possiamo compilarlo e iniziare l'addestramento. Abbiamo cercato di mantenere le cose il più semplici possibile, ma in molte applicazioni ha senso includere strati di regolarizzazione e ulteriori strati densi e LSTM. Nota che il modello ha due strati di input e due di output, come mostrato nello schema sotto. Puoi saperne di più sui modelli con input e output multipli in Introduction to TensorFlow in Python.

Architettura del modello LSTM
Questa figura mostra l'architettura del modello LSTM.
# Compile the model
lstm.compile(loss='categorical_crossentropy', optimizer='rmsprop')

# Train the model
lstm.fit([trainChords, trainDurations], [targetChords, targetDurations],
            epochs=500, batch_size=64)

Una volta concluso l'addestramento, possiamo generare nuovi brani con il modello LSTM. Lo faremo fornendo una sequenza iniziale di 32 accordi e durate, che ci permetterà di fare le prime previsioni. Aggiungeremo poi queste previsioni alle serie di accordi e durate, permettendoci di fare un'altra previsione basata sugli ultimi 32 accordi e durate della serie aggiornata. Un vantaggio dell'uso di un LSTM è che possiamo iterare all'infinito, generando brani di lunghezza arbitraria.

# 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

L'ultimo passo è esportare la musica generata in un file MIDI usando music21. Nota che il codice è quasi identico a quello usato per i modelli basati su autoencoder, con una differenza importante: ora aggiungiamo allo stream sia gli accordi sia le durate.

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

Con i modelli autoencoder e VAE avevamo difficoltà a generare melodie e potevamo produrre solo brani a lunghezza fissa. Con un LSTM possiamo generare sequenze di lunghezza arbitraria iterando sulle previsioni, come fatto sopra. Sarà anche relativamente più semplice generare melodie migliori. Vedremo entrambe queste caratteristiche negli esempi sotto, ciascuno avviato con una sequenza iniziale di accordi e durate da un campione del nostro training set.

Esempio: brano generato n. 1
Esempio: brano generato n. 2
Esempio: brano generato n. 3

   banner corso introduzione a tensorflow in python

Questioni aggiuntive

Dati

Una delle principali sfide nello sviluppo di modelli di machine learning per la musica è la disponibilità di dataset, poiché la musica tipicamente non è liberamente e legalmente scaricabile. Questo ha di fatto impedito alla letteratura di convergere su un dataset di riferimento, come MNIST per la classificazione di immagini. Esistono però alcuni progetti che forniscono audio grezzo, notazione simbolica o feature musicali derivate per un gran numero di brani. Forniamo sotto un elenco parziale.

  1. Mutopia Project. Un archivio di musica in pubblico dominio e con licenza Creative Commons in formati MIDI, PDF e LilyPond. Contiene attualmente oltre 2000 brani ed è organizzato per strumento, compositore, stile e raccolta.
  2. The Million Song Dataset. Contiene feature audio e metadati per oltre 300 GB di brani di musica popolare, ma non le tracce audio.
  3. Free Music Archive. Contiene audio, etichette di genere e feature per oltre 100.000 tracce liberamente e legalmente scaricabili.

Campionamento vs determinismo

Quando abbiamo generato musica con un modello LSTM, abbiamo usato un approccio deterministico alla selezione di accordi e durate. Il modello ha previsto probabilità per ciascuno dei 267 accordi e 9 durate, e abbiamo selezionato gli accordi e le durate con la probabilità più alta. Analogamente, per i modelli autoencoder e VAE abbiamo selezionato gli accordi con i valori di output più alti.

Un'alternativa è campionare dalla distribuzione degli output, invece di selezionare accordi e durate con probabilità massima. Questo rende il processo di generazione stocastico, anziché deterministico. Significa che modelli autoencoder o VAE con lo stesso vettore latente genereranno brani diversi a ogni esecuzione. Allo stesso modo, la stessa sequenza di input a un modello LSTM potrà generare brani differenti.

Il blocco di codice sotto mostra come passare a una generazione stocastica di accordi e durate modificando la funzione predictChords(). Nota che abbiamo sostituito l'operazione argmax() con un'operazione di campionamento che usa le probabilità per estrarre casualmente dagli array di accordi e durate.

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

Per una discussione estesa sui benefici degli approcci stocastico e deterministico alla generazione musicale, vedi la sezione 6.6 di Briot, Hadjerest e Pachet (2019).

Embedding

Quando abbiamo addestrato il modello LSTM, abbiamo usato uno strato di embedding, che mappa ogni accordo e durata in un vettore denso a 64 elementi. Usare uno strato di embedding ci permette di evitare vettori di input sparsi e ad alta dimensione. Invece, usiamo una rappresentazione a bassa dimensione che ci consente di identificare relazioni tra accordi diversi.

In molti casi non ha senso addestrare uno strato di embedding. Spesso infatti non avremo training set sufficientemente ampi nei problemi di generazione musicale. Usare embedding pre-addestrati o prodotti con apprendimento non supervisionato — come spesso facciamo per classificazione e generazione di testo — ci permetterà di addestrare il modello quando altrimenti non sarebbe fattibile.

Vedi Chuan, Agres e Herremans (2018) per una panoramica sulla costruzione di embedding con un approccio non supervisionato.

Generative Adversarial Network

Abbiamo discusso autoencoder, VAE e modelli LSTM come opzioni per la generazione musicale. Un'altra opzione, che ha avuto successo in altri ambiti di modellazione generativa, è la generative adversarial network (GAN). Una GAN funziona combinando una rete discriminativa con una rete generativa. La rete generativa crea campioni, come le sequenze di accordi generate dai nostri modelli LSTM e autoencoder. Questi campioni sono poi combinati in un training set insieme a campioni reali — cioè sequenze di accordi tratte da brani reali. Il modello discriminativo prova a classificare i campioni come veri o generati. Il generatore prova a ingannare il discriminatore facendogli classificare come veri i campioni che ha creato. Il modello è addestrato finché non si raggiunge un equilibrio evolutivo stabile, in cui né discriminatore né generatore riescono a ottenere ulteriori vantaggi.

Per chi è interessato ad applicare le GAN ai compiti di generazione musicale, possono essere utili le seguenti risorse:

  1. MuseGAN è un progetto mirato alla generazione di musica polifonica con le GAN. Include modelli pre-addestrati e 174.154 brani puliti e preparati in formato pianoroll.

  2. Magenta è un progetto open source dedicato all'applicazione della modellazione generativa a compiti creativi, inclusa la creazione di musica. Puoi esplorare gli strumenti offerti da Magenta tramite un notebook Colab.

Risorse

Se vuoi approfondire TensorFlow 2.0, vedi Introduction to TensorFlow in Python su DataCamp. Per una rassegna recente e completa dei metodi per la generazione musicale con deep learning, vedi Briot, Hadjerest e Pachet (2019). Per ulteriori tutorial sulla generazione musicale con modelli di deep learning, vedi 1, 2, 3, 4 e 5, che offre un trattato completo sui modelli generativi.

Appendice

Rappresentazione continua

I file WAV sono la forma più comune di rappresentazione continua perché contengono un segnale audio non compresso e grezzo. Questo differisce, ad esempio, dai file mp3, che comprimono la dimensione del file scartando informazioni sul segnale audio.

Il blocco di codice seguente mostra come estrarre il sample rate, rate, e il segnale audio grezzo, signal, da un file WAV che contiene un brano composto da Mozart. Il sample rate è il numero di volte al secondo in cui il segnale audio viene campionato. Nel nostro caso, questo valore è 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))

Il segnale audio grezzo, signal, è un tensore S x C, dove S è il numero di campioni e C è il numero di canali. Per semplicità, considereremo il caso in cui C = 1. Il grafico sotto mostra un intervallo di tre secondi di signal.

Questo grafico mostra un segnale audio normalizzato estratto da un file WAV.

In generale, usare segnali audio grezzi per addestrare un modello è notevolmente più impegnativo rispetto a usare una rappresentazione discreta. Un'indagine recente sui metodi di deep learning per la generazione musicale ha esaminato 32 metodi diversi presenti in letteratura. Tra questi, solo tre usano rappresentazioni continue:

  1. Audio Style Transfer
  2. DeepAuto-Controller
  3. WaveNet

Trasposizione di tonalità

Un metodo utilizzabile sia per la riduzione della dimensionalità sia per la data augmentation è la trasposizione di tonalità. Consiste nello spostare l'altezza di ciascuna nota di un intervallo fisso. Se, ad esempio, un brano contiene una sequenza in cui l'altezza C3 è seguita da D3 e usiamo una trasposizione che sposta tutto verso l'alto di due altezze, allora C3 diventerà D3 e D3 diventerà E3. Per modelli che usano la trasposizione di tonalità, vedi Lim, Rhyu e Lee (2018) e Sturm et al. (2016).

Se usiamo la trasposizione per ridurre la dimensionalità, il nostro obiettivo sarà spostare note e accordi in modo da minimizzare il numero totale di note e accordi unici nel dataset. Poiché la musica tende a essere scritta attorno a gruppi di altezze chiamati tonalità che suonano bene insieme, possiamo comodamente trasporre ogni brano del dataset nella stessa tonalità.

Questa volta importeremo tre sottomoduli da music21:

  1. converter, che useremo per caricare un file MIDI e convertirlo in un oggetto stream.
  2. pitch, che useremo per creare l'altezza target.
  3. interval, che useremo per calcolare la distanza tra la tonalità originale e quella target.
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

Applichiamo prima il metodo .analyze('key') a score, che individua la tonalità dell'oggetto stream, in questo caso Do maggiore. Calcoliamo poi l'intervallo tra Do maggiore e la tonalità target, Fa maggiore. Nota che accediamo all'altezza dell'oggetto key usando .tonic. Inoltre, creiamo l'altezza target con pitch.Pitch('F'). Assegniamo quindi l'output a keyInterval, che indica la distanza tra tonalità originale e target.

Infine trasponiamo la tonalità usando il metodo .transpose() passando l'intervallo keyInterval. Stampare la nuova tonalità conferma che è Fa maggiore.

Trasposizione di ottava

Se vogliamo andare oltre la trasposizione di tonalità, possiamo anche trasporre l'ottava verso l'alto o verso il basso. Invece di trasporre Do maggiore in Fa maggiore, ad esempio, possiamo trasporre Do maggiore in Do maggiore in un'ottava inferiore o superiore. Se fissiamo lo stesso insieme di ottave per ciascun brano, costringeremo note e accordi a raggrupparsi, riducendo la dimensionalità del problema e semplificando l'addestramento. Nel codice sopra, potremmo ottenerlo specificando l'ottava dell'altezza. Ad esempio, invece di pitch.Pitch('F') per calcolare l'intervallo, potremmo usare pitch.Pitch('F3').

Invarianza a tonalità e ottava

Un'alternativa naturale alla riduzione della dimensionalità è la data augmentation. Cioè, invece di ridurre l'insieme di input e output possibili, espandiamo il dataset apportando modifiche ai dati esistenti. Possiamo farlo anche con trasposizioni di tonalità e ottava. Tuttavia, invece di trasporre tutti i brani in una tonalità comune e raggrupparne le note in ottave adiacenti, trasporremmo tutti i brani in tutte le tonalità e in tutte le ottave. Questo ci consentirebbe di ottenere invarianza a tonalità e ottava nel modello. Vedi Briot, Hadjerest e Pachet (2019) per una discussione sull'uso della trasposizione per i due scopi.

Raccolta dei dati

Raccoglieremo i file MIDI per addestrare il nostro modello dal Project Mutopia. Prima di effettuare qualsiasi scraping, controlleremo il file robots, all'indirizzo https://www.mutopiaproject.org/robots.txt, per assicurarci di rispettare tutte le richieste su come gestire le interazioni automatizzate con il sito. Al momento della stesura di questo tutorial, il file robots conteneva il testo nel blocco di codice sotto. User-agent: * indica che la regola seguente si applica a tutti gli utenti che interagiscono con il sito. Disallow:, seguito da nulla, significa che non sono imposti vincoli allo scraping.

# Allow crawling of all content
User-agent: *
Disallow:

Importeremo prima urlopen e urlretrieve da urllib.request, che useremo per inviare richieste GET per ciascuna pagina della sezione chitarra e poi scaricare ciascun file MIDI linkato nella pagina. Importeremo BeautifulSoup per analizzare l'HTML restituito da urlopen, così da individuare i link ai file MIDI. Infine, useremo il modulo time per attendere 10 secondi tra un download e l'altro, evitando di sovraccaricare le risorse del sito.

Questa figura mostra una pagina della sezione chitarra del sito Mutopia con i file MIDI identificati.

Ogni iterazione del ciclo principale controllerà se il numero di link sulla pagina, linkCount, supera 0. In tal caso costruirà l'URL della pagina successiva, url, usando le stringhe url0, url1 e il numero del brano, songNumber. Aprirà quindi l'URL, analizzerà l'HTML, troverà tutti i link sulla pagina, reimposterà linkCount a 0 e poi scorrerà ciascun link nella lista. Se un link contiene la sottostringa .mid, allora punta a un file MIDI e viene scaricato con urlretrieve() nella directory specificata da save_dir. Infine incrementiamo songNumber di 10, dato che ogni pagina contiene 10 brani. Ripetiamo il processo finché non incontriamo una pagina che contiene meno di 10 link.

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

Se vuoi saperne di più sullo scraping, vedi il corso di DataCamp Web Scraping with Python e il tutorial Web Scraping using Python.

Argomenti
Python
Machine Learning

Approfondisci Python e il Machine Learning

Corso

Introduzione a TensorFlow in Python

4 ore
56.5K
Impara le basi delle reti neurali e come creare modelli di deep learning usando TensorFlow.
Vedi i dettagliRight Arrow
Inizia Il Corso
Mostra altroRight Arrow
Correlato

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

I 15 migliori server MCP remoti che ogni AI builder dovrebbe conoscere nel 2026

Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Abid Ali Awan's photo

Abid Ali Awan

15 min

blog

Che cos'è Snowflake? Guida per principianti alla piattaforma dati cloud

Esplora le basi di Snowflake, la piattaforma dati cloud. Scopri la sua architettura, le sue funzionalità e come integrarla nelle tue pipeline di dati.
Tim Lu's photo

Tim Lu

12 min

Mostra AltroMostra Altro