Weiter zum Inhalt

Mit TensorFlow 2.0 Musik komponieren – Tutorial

In diesem Tutorial lernst du, wie du generative Modelle trainierst, um mit TensorFlow 2.0 Musik zu komponieren.
Aktualisiert 18. Sept. 2026  · 15 Min. lesen

Mit KI erkunden

ChatGPTClaudePerplexity

introduction to tensorflow in python banner

Dieses Tutorial basiert auf TensorFlow 2.0 in Python sowie der High-Level-API Keras, die in TensorFlow 2.0 eine noch wichtigere Rolle spielt. Wenn du mehr über TensorFlow 2.0 erfahren möchtest, sieh dir auf DataCamp den Kurs Introduction to TensorFlow in Python an. Eine umfassende Übersicht zur Deep-Learning-Literatur für Musik findest du in Briot, Hadjerest und Pachet (2019), auf die wir im Laufe des Tutorials Bezug nehmen.

Generative Modelle

Überwachtes maschinelles Lernen lässt sich grob in zwei Kategorien einteilen: diskriminative und generative Modelle. Diskriminative Modelle lernen eine Entscheidungsgrenze und geben darauf basierend eine Klassifikation aus. Generative Modelle erzeugen neue Instanzen einer Klasse.

Ein diskriminatives Musikmodell könnte Songs verschiedenen Genres zuordnen. Ein generatives Modell würde Songs eines bestimmten Genres komponieren. In diesem Tutorial setzen wir generative Modelle ein, um Musik zu erzeugen.

Diskriminativ vs. Generativ
This figure depicts discriminative and generative models of music.

Musikrepräsentation

Bevor wir ein generatives Modell definieren und trainieren, brauchen wir einen Datensatz. Musikdaten lassen sich kontinuierlich oder diskret darstellen. Die gängigste kontinuierliche Form ist das Audiosignal, typischerweise als WAV-Datei gespeichert. Wir verwenden in diesem Tutorial keine kontinuierlichen Formen; mehr dazu findest du im Anhang. Häufige diskrete Formen sind MIDI-Dateien, Pianoroll und Text. Wir konzentrieren uns auf MIDI-Dateien und extrahieren zwei Arten symbolischer Objekte: Noten und Akkorde.

Noten

Eine Note ist eine symbolische Darstellung eines Klangs. Für unsere Zwecke lässt sie sich durch Tonhöhe und Dauer beschreiben. Die Tonhöhe einer Note hängt mit der Frequenzschwingung ihrer Wellenform zusammen, gemessen in Hertz (Hz). Höhere Tonhöhen entsprechen mehr Schwingungen pro Sekunde. Die Dauer gibt an, wie lange die Note gespielt wird.

MIDI-Dateien kodieren die Tonhöhe einer Note als Integer zwischen 0 und 127. Alternativ werden Noten oft als Tonbuchstabe plus Oktavzahl angegeben. Innerhalb einer Oktave sind die Tonhöhen von tief nach hoch wie folgt geordnet:

  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

Die Oktave wird als Index angegeben, z. B. das 4 in A4 oder das 7 in C7. Eine höhere Oktave entspricht einer höheren Frequenz. Nehmen wir eine beliebige Tonhöhe Xi, dann hat Xi+1, also genau eine Oktave höher, die doppelte Frequenz von Xi.

Neben der Tonhöhe verwenden wir auch die Dauer einer Note. Die Dauer ist relativ und auf die Länge einer ganzen Note normiert. Die längste Note ist eine „große“ Note, die achtmal so lang ist wie eine ganze Note. Die kürzeste Note ist eine Zweihundertsechsundfünfzigstel-Note, also 1/256 einer ganzen Note.

Akkorde

Ein Akkord ist die Kombination aus zwei oder mehr Noten, die auf demselben Instrument gleichzeitig gespielt werden. Betrachten wir monophone Musik – also Musik, die auf einem einzelnen Instrument gespielt wird – können wir Akkorde identifizieren, indem wir annehmen, dass alle Noten, die exakt gleichzeitig erklingen, zu demselben Akkord gehören. Diese Annahme gilt nicht für polyphone Musik, bei der zwei oder mehr Instrumente gleichzeitig spielen.

Im folgenden Codeblock installieren wir music21 und importieren das converter-Modul, um MIDI-Dateien zu parsen. Wir laden und parsen ein klassisches Gitarrenstück von Mauro Giuliani. Anschließend wenden wir .chordify() an, das die Akkordfolge der Partitur rekonstruiert, indem es gleichzeitige Noten zu Akkorden zusammenfasst.

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

Nachdem wir die Akkordfolge aus der MIDI-Datei konstruiert haben, können wir sie mit .show() als Notensatz anzeigen. Dafür brauchst du Software, die Notenblätter erstellen und darstellen kann, etwa das kostenlose MuseScore.

Aus MIDI generierter Notensatz
This figure shows a MIDI file displayed as sheet music.

Alternativ können wir die zugrunde liegenden Elemente als Text untersuchen und bearbeiten. Das geht mit .show('text'). Am Zeilenanfang steht jeweils der Offset in Klammern, der angibt, wie viele Sekunden nach Beginn der Partitur ein Element erscheint. Beachte, dass bei Sekunde 0 nur eines der Elemente ein Akkord ist. Die übrigen sind Metadaten wie Instrument, Tempo, Tonart und Taktart. Die Tonart verwenden wir später; den Rest kannst du zunächst ignorieren.

 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>

Nach den Metadaten folgen ausschließlich Akkorde in der Reihenfolge ihres Auftretens im Stück. Zum Beispiel ist <music21.chord.Chord E3 G3> der Akkord aus den Noten E3 und G3. Und <music21.chord.Chord A3> ist die Note A3 solo gespielt.

Für einige Modelle verwenden wir sowohl den Akkord als auch seine Dauer, also wie lange er gehalten wird. Diese Information erhalten wir, indem wir .elements nutzen und für einen beliebigen Akkord im Score auf das Attribut .duration zugreifen, wie im folgenden Codeblock.

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

Kodierung von Noten und Akkorden

Im gesamten Tutorial arbeiten wir mit diskreten Musikrepräsentationen, die sich als Sequenzen aus Noten, Akkorden und zugehörigen Dauern beschreiben lassen. Denk daran: Akkorde sind Kombinationen von Noten, und das MIDI-Format erlaubt 128 einzelne Noten. Wenn wir das Feature-Set um alle Zweiklang-Akkorde erweitern, wächst es um 128!/[(128-2)!*2!] bzw. 8128 Elemente. Dreiklänge erhöhen um 341.376, Vierklänge um weitere 10.668.000.

Das oben gezeigte Beispiel enthält Noten sowie Zwei-, Drei- und Vierklang-Akkorde. Verwenden wir One-Hot-Encoding und berücksichtigen alle Features, müssten wir Vorhersagen über 11.017.632-dimensionale Vektoren treffen. Daher sollten wir die Wahl der Kodierung bei Musikgenerationsproblemen sehr sorgfältig abwägen. Ebenso lohnt sich die Überlegung, Tonarten-Transposition oder Data Augmentation einzusetzen; beides wird im Anhang besprochen.

Zur Kodierungswahl diskutieren Briot, Hadjerest und Pachet (2019) vier gängige Optionen für generative Musikmodelle:

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

Beim One-Hot-Encoding werden musikalische Symbole wie Noten und Akkorde als spärliche Vektoren dargestellt, die an genau einer Position eine Eins tragen. Many-Hot-Encoding bildet Akkorde, die aus mehreren Noten bestehen, ab, indem für jede enthaltene Note an der entsprechenden Position eine Eins gesetzt wird.

In der folgenden Grafik vergleichen wir Many-Hot- und One-Hot-Encoding für den Fall mit Noten und Zweiklängen. Beim Many-Hot-Encoding werden C4 und D4 durch Vektoren mit 128 Elementen dargestellt. Beide Vektoren haben überall Nullen, außer an den Positionen für C4 bzw. D4, wo Einsen stehen. Der Akkord C4+D4, also gleichzeitiges Spielen von C4 und D4, wird konstruiert, indem an beiden Positionen Einsen gesetzt werden.

Kodierung von Noten und Akkorden
This diagram illustrates the difference between one-hot and many-hot encoding.

Die One-Hot-Repräsentation von Einzeltönen und Zweiklängen besteht aus 8256-dimensionalen Vektoren. Auch hier ist der Vektor spärlich und enthält eine 1 an der Position des einzigartigen Tons bzw. Akkords. Beachte, dass Many-Hot-Repräsentationen ein Multi-Label-Modell erfordern, bei dem für ein Output mehrere Klassen vorhergesagt werden können. Dafür reduzieren sie die Größe der Ein- und Ausgabedaten erheblich.

Die meisten Modelle in der Literatur setzen weiterhin auf One-Hot-Encoding. Briot, Hadjerest und Pachet (2019) nennen jedoch mehrere Musikgenerationsmodelle mit Many-Hot-Encoding, darunter die Systeme RBMc, RNN-RBM und C-RBM, die alle auf einer Restricted-Boltzmann-Machine-(RBM)-Architektur basieren.

Multi-One-Hot- und Multi-Many-Hot-Encoding verallgemeinern One-Hot bzw. Many-Hot auf polyphone Musik, also paralleles Spiel mehrerer Instrumente. Wir beschränken uns hier auf monophone Musik und gehen im weiteren nicht näher darauf ein.

Datensammlung und -aufbereitung

Als Nächstes sammeln wir einen Datensatz für das Training eines generativen Modells. Wir nutzen Daten vom Mutopia Project, das 2124 Musikstücke im öffentlichen Bereich im MIDI-Format bereithält. Da die Website nach Instrumenten gegliedert ist, können wir mit einem einfachen Python-Skript alle Gitarrenstücke scrapen, die wir hier zum Training verwenden. Eine Anleitung findest du im Anhang.

Nehmen wir an, wir haben alle .mid-Dateien aus dem Gitarrenbereich in ein Verzeichnis save_dir gespeichert. Im nächsten Schritt laden wir jede .mid-Datei in music21 als stream-Objekt.

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)

Zur Dimensionsreduktion beschränken wir uns auf Stücke von Mauro Giuliani. Dazu ändern wir den obigen Code wie folgt.

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

Bevor wir mit dem Training starten, sind vier weitere Schritte nötig:

  1. Polyphone Musik entfernen und .chordify() auf die verbleibenden stream-Objekte anwenden.
  2. Die Sequenz aus Noten, Akkorden und Dauern extrahieren.
  3. Eine Abbildung konstruieren, die Noten, Akkorde und Dauern auf Integer und zurück mappt.
  4. Streams in Sequenzen fester Länge teilen, die als Model-Inputs dienen.

Beginnen wir damit, polyphone Musik zu entfernen. Wir partitionieren jedes stream-Objekt nach Instrument und prüfen, ob genau eine Partition existiert – sprich, nur ein Instrument spielt. Das zeigt der folgende Codeblock.

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]

Wichtig: Wenn wir .chordify() – wie in der Grafik unten – anwenden, ohne zuvor polyphone Musik zu entfernen, werden Noten unterschiedlicher Instrumente fälschlich zu einem Akkord zusammengeführt.

Noten in Akkorde umwandeln
This figure shows notes being converted to chords.

Nun extrahieren wir Noten, Akkorde und Dauern aus jedem stream. Dazu definieren wir drei leere Listen: originalChords, originalDurations, originalKeys. Dann iterieren wir über die Streams, identifizieren und speichern alle Noten, Akkorde und Dauern sowie die Tonart des Stücks.

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

Weitere Dimensionsreduktion erreichen wir, indem wir nur Musik in der Tonart C-Dur behalten – die in unserem Datensatz am häufigsten vorkommt. Im Folgenden verwenden wir der Einfachheit halber „Akkord“ als Sammelbegriff für Akkorde und Noten.

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

Als Nächstes bestimmen wir die Menge der einzigartigen Akkorde und Dauern und bauen Dictionaries, die sie auf Integer mappen. Drucken wir die Anzahl einzigartiger Noten und Akkorde, sehen wir, dass unsere Reduktionsschritte die Dimension auf handhabbare 267 gesenkt haben.

# 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

Nach dem Training wollen wir Integer-Vorhersagen wieder in Noten, Akkorde und Dauern zurückübersetzen. Dafür invertieren wir chordToInt und durationToInt.

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

Zum Schluss definieren wir unsere Trainingssequenzen: 32 aufeinanderfolgende Noten und Akkorde plus die entsprechenden Dauern. Zunächst für Autoencoder-Modelle, bei denen Features und Targets identisch sind.

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

Musikgenerierung

In diesem Abschnitt nutzen wir den vorbereiteten Datensatz, um Musik zu generieren. Wir betrachten drei Modelle, beginnend mit dem einfachsten:

  1. Autoencoder
  2. Variational Autoencoder (VAE)
  3. Long Short-Term Memory (LSTM)

Autoencoder

Ein Autoencoder besteht aus zwei Netzen, die vertikal gestapelt und durch einen latenten Vektor verbunden sind. Die Eingaben gehen zunächst in ein Encoder-Modell, oft mit einer oder mehreren Dense-Schichten. Die letzte Schicht des Encoders ist der latente Vektor – ein Flaschenhals, der die extrahierten Features auf wenige latente Merkmale komprimiert.

Der latente Vektor ist mit dem Decoder verbunden, der die Daten wieder „entpackt“. Die Knotenzahl wächst in aufeinanderfolgenden Decoder-Schichten, die Ausgabeschicht hat die gleiche Dimension wie die Eingabeschicht des Encoders.

Im Gegensatz zu diskriminativen Modellen, die eine Entscheidungsgrenze lernen, werden Autoencoder darauf trainiert, die Eingabedaten – hier: ein Song – möglichst genau zu rekonstruieren, begrenzt durch die Größe des latenten Vektors. Daher dienen die Inputs gleichzeitig als Targets.

Wir verwenden einen Autoencoder als erstes generatives Musikmodell. Um es einfach zu halten, übernehmen wir die MiniBach-Architektur aus Briot, Hadjerest und Pachet (2019) als Vereinfachung von DeepBach. Ein weiteres Beispiel ist das System DeepHear.

Unsere Eingaben sind One-Hot-Vektoren für Noten und Akkorde. Dauern ignorieren wir zunächst. Gemäß MiniBach flatten wir zuerst den Eingabevektor.

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

Als Nächstes definieren wir die Anzahl der Samples nSamples, die Anzahl der Akkorde und Noten nChords und die Eingabedimension inputDim. Die Dimension der latenten Features latentDim setzen wir auf 2. Das verringert die Leistung, erlaubt aber eine einfache Visualisierung des latenten Raums.

# 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

Jetzt definieren wir die Modellarchitektur mit der Eingabeschicht des Encoders encoderInput und der Eingabeschicht des Decoders latent. Beachte: latent ist zugleich die Ausgabe des Encoders. Wir fügen eine Dense-Schicht encoded hinzu, die die Eingaben mit dem latenten Vektor verbindet, und eine weitere Dense-Schicht decoded, die den latenten Vektor mit der Ausgabe verknüpft.

Abschließend definieren wir das Decoder-Modell decoder und den Autoencoder autoencoder. .Model() hat Argumente inputs und outputs. Für den decoder übergeben wir den latent-Vektor als Input und das Netzwerk decoded als output. Für den Autoencoder verwenden wir encoderInput als Eingabe und geben die Auswertung des decoder auf der Encoder-Ausgabe encoded als Ausgabe zurück.

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

Die Architektur eines generischen Autoencoders zeigt die folgende Abbildung. Oben der Encoder, unten der Decoder, verbunden über den latenten Vektor.

Architektur eines Autoencoders
This figure shows the architecture for an autoencoder.

Mit definierter Architektur für decoder und autoencoder bleiben nur noch Kompilieren und Training per .fit(). Zwei Punkte sind wichtig:

  1. Unsere Eingaben bestehen aus 0/1, die Vorhersagen sind reelle Zahlen zwischen 0 und 1. Wir verwenden binary_crossentropy als Loss.
  2. Input- und Target-Werte sind identisch: trainChordsFlat.
# Compile autoencoder model
autoencoder.compile(loss = 'binary_crossentropy', learning_rate = 0.01, optimizer='rmsprop')

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

Zum Schluss generieren wir Musik. Bei einer Sequenzlänge von 32 nimmt der autoencoder 32 Akkorde/Noten als Input und erzeugt ein „Stück“ fester Länge mit 32 Akkorden/Noten. Das Modell lernt, dem Input möglichst ähnlich zu sein; wir möchten jedoch Neues generieren. Dafür geben wir einen zufällig erzeugten latenten Vektor in das decoder-Modell, das Teilnetz des Autoencoders.

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

Wir formen die Ausgabe in ein Array mit den ursprünglichen Dimensionen um und berechnen dann das argmax() entlang der ersten Dimension. Das ergibt Integerwerte für Akkorde, die wir im nächsten Schritt identifizieren.

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

Schließlich erzeugen wir ein stream-Objekt mit music21, setzen das Instrument auf Gitarre, hängen die generierte Akkordfolge an und exportieren alles als MIDI-Datei.

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

Das GIF unten zeigt einen Ausschnitt des Trainings eines Autoencoders. Die in jedem Schritt erzeugte Partitur ist die Rekonstruktion zu einem bestimmten latenten Zustandsvektor. Jedes Frame entspricht 5 Trainingsepochen.

Autoencoder: Entwicklung von Noten- und Akkordfolgen
This figure shows shifts in the latent space over time.

Außerdem können wir die Bewegung im latenten Raum während des Trainings visualisieren, wie im folgenden GIF. Der Raum ist schmal und driftet über die Epochen. Das erschwert es, einen guten zufälligen latenten Zustand zu ziehen – besonders bei höherdimensionalen latenten Vektoren. Im nächsten Abschnitt zeigen wir, wie ein Variational Autoencoder (VAE) dieses Problem löst.

Autoencoder: Entwicklung des latenten Raums
This figure shows shifts in the latent space over time.

Zum Abschluss hören wir uns die Audioausgabe des Autoencoders an. Zuerst die Akkordfolge des Originalsongs, zufällig aus dem Trainingsset gewählt. Danach generieren wir eine Akkordfolge aus dem Autoencoder mit dem latenten Vektor, der zur Originalfolge gehört – das ergibt das darunterliegende Stück.

Beispiel: Originalsong
Beispiel: Generierter Song

Variational Autoencoder

Autoencoder eignen sich gut zum Entrauschen, Komprimieren und Dekomprimieren, sind aber als generative Modelle oft schwach. Variational Autoencoder (VAEs), eingeführt von Kingma und Welling (2017), adressieren diese Schwächen und gehören zu den am häufigsten genutzten Methoden zur Musikgenerierung. Briot, Hadjerest und Pachet (2019) nennen vier Systeme auf VAE-Basis: MusicVAE, VRAE, VRASH und GSLR-VAE. Gegenüber Autoencodern verbessern sie zwei Aspekte:

  1. Statt jede Eingabe auf einen Punkt im latenten Raum zu mappen, ordnet ein VAE jeder Eingabe eine Normalverteilung mit Mittelwert und Varianz zu.
  2. Der VAE zwingt Mittelwert und natürlichen Logarithmus der Varianz dazu, nahe bei 0 zu liegen.

Die erste Änderung erreichen wir, indem wir den latenten Zustandsvektor durch drei Schichten ersetzen:

  1. Eine Mean-Schicht
  2. Eine (Log-)Varianz-Schicht
  3. Eine Sampling-Schicht

Jede Eingabe wird auf einen Mittelwert und den Logarithmus der Varianz abgebildet, was ausreicht, um eine Normalverteilung zu charakterisieren. In der Sampling-Schicht werden dann Zufallspunkte aus dieser Verteilung gezogen.

Der folgende Code definiert eine Funktion für eine Sampling-Schicht. Sie nimmt Mittelwert und Logvarianz als Input, zieht mit tensorflow.random einen Tensor epsilon aus der Standardnormalverteilung (Mittelwert und Logvarianz 0), skaliert ihn mit der Standardabweichung (aus logVar via Halbierung und Exponentialfunktion) und addiert den Mittelwert 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

Nun passen wir das Autoencoder-Modell an und integrieren mean-, logVar- und sampling-Schicht. Beachte: Die Schicht encoded erhält jetzt zwei Inputs, und der encoder hat drei Outputs.

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

Wie beim Autoencoder ist der latente Zustand die Ausgabe der Schicht encoded. Der Unterschied: Statt jeden Input auf einen festen Punkt zu mappen, erzeugt die Sampling-Schicht Zufallswerte aus einer Normalverteilung mit Parametern mean und logVar. Dieselbe Eingabe führt also zu einer Verteilung latenter Zustände.

Zum Schluss passen wir die Loss-Funktion an, um mean und logVar zu disziplinieren. Wir berechnen die Kullback-Leibler-(KL)-Divergenz zwischen jeder Verteilung (definiert durch mean und logVar) und der Standardnormalverteilung (Mittelwert und Logvarianz 0). Je weiter Mittelwerte und Logvarianzen von 0 entfernt sind, desto höher fällt die KL-Strafkomponente aus.

Warum zwingen wir die Verteilungen in Richtung Standardnormalverteilung? Weil wir latente Zustände sampeln und damit Musik generieren wollen. Mit der KL-Strafe können wir unabhängige Ziehungen aus der Standardnormalverteilung nutzen und erhalten mit hoher Wahrscheinlichkeit latente Zustände, die zu qualitativ guten Stücken führen.

Der folgende Code zeigt die Anpassung der Loss-Funktion. Der erste Teil ist die Rekonstruktionskomponente (binary crossentropy) zwischen Input und Output des vae, wie beim Autoencoder. Der KL-Term ist die mittlere KL-Divergenz zwischen den latenten Verteilungen und der Standardnormalverteilung. Beide addieren wir zu vaeLoss, fügen den Loss dem Modell hinzu, kompilieren und trainieren.

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

Im vorherigen Abschnitt war der latente Raum des Autoencoders problematisch. Ein Ziel des VAE ist, genau das in generativen Settings zu beheben. Die folgende Abbildung zeigt den latenten Raum des VAE: deutliche Streuung in beiden Dimensionen, zentriert um 0, ohne Drift über die Epochen. Das erleichtert das zufällige Ziehen eines guten latenten Zustands erheblich.

VAE: Entwicklung des latenten Raums
This figure shows shifts in the latent space over time.

Nun zur Generierung: Wir ziehen einen zufälligen latenten Vektor aus der Standardnormalverteilung, geben ihn in den Decoder, reshapen die Ausgabe und nehmen spaltenweise das Argmax. Das ergibt eine Folge aus 32 Integern für Noten/Akkorde, die wir via intToChord identifizieren. Dann erstellen wir ein stream-Objekt, fügen Gitarre als Instrument hinzu, hängen Noten und Akkorde an und exportieren als 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')

Die drei folgenden Stücke stammen aus dem Modell. Die ersten beiden basieren auf latenten Zuständen, die zu Akkordfolgen aus dem Trainingsset gehören. Das dritte ist eine Linearkombination der latenten Zustände von Song #1 und #2. Da der VAE den latenten Raum gut strukturiert, können wir davon ausgehen, dass auch eine Linearkombination einen hochwertigen latenten Zustand liefert.

Beispiel: Generierter Song #1
Beispiel: Generierter Song #2
Beispiel: Linearkombination von Song #1 und #2

Ein Punkt fällt bei Autoencoder- und VAE-basierten Stücken auf: Es fehlt oft eine starke Melodie. Das lässt sich verbessern, indem wir mehr Schichten hinzufügen und den Trainingssatz vergrößern. Oder wir nutzen ein Modell, das explizit für Sequenzen entwickelt wurde – wie im nächsten Abschnitt.

Long Short-Term Memory (LSTM)

Als drittes Modell betrachten wir LSTM. Dabei handelt es sich um ein rekurrentes neuronales Netz (RNN), das dem Vanishing-Gradient-Problem entgegenwirkt. Briot, Hadjerest und Pachet (2019) sehen rekurrente Modelle als am weitesten verbreitet für Musikgenerierung. Sie nennen über 20 Systeme auf RNN-/LSTM-Basis, darunter BachBot, DeepBach, Performance-RNN und Hexahedria.

Ein LSTM bringt zwei Vorteile gegenüber Autoencoder-basierten Modellen:

  1. Es ist für sequenzielle Daten konzipiert.
  2. Es kann musikalische Sequenzen beliebiger Länge erzeugen.

Anders als Autoencoder und VAEs, die Inputs als Targets verwenden, werden LSTM-Generationsmodelle darauf trainiert, die nächste Note bzw. den nächsten Akkord vorherzusagen. Daher müssen wir Trainings- und Zielmengen neu konstruieren, was der folgende Code zeigt. Da ein LSTM weniger Parameter erfordert, nehmen wir auch Dauern in das Modell auf.

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

Beachte: trainChords und trainDurations sind wie zuvor definiert. Hinzu kommen targetChords und targetDurations, also der Akkord bzw. die Dauer, die auf die Trainingssequenz folgen. Als Nächstes setzen wir die Anzahl der Samples, Akkorde und Dauern sowie die Eingabedimension. Außerdem verwenden wir separate Embedding-Schichten für Akkorde und Dauern.

# 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

Jetzt definieren wir die Architektur. Wir verwenden ein Modell mit zwei Inputs und zwei Outputs, um sowohl Akkorde als auch Dauern zu prognostizieren. Zuerst definieren wir die Eingabeschichten chordInput und durationInput und leiten sie an Embedding-Schichten chordEmbedding und durationEmbedding weiter. Diese Zuordnungstabellen verbinden jeden Akkord bzw. jede Dauer mit einem 64-dimensionalen Vektor. Embeddings ersetzen spärliche One-Hot-Inputs durch dichte Repräsentationen, in denen ähnliche Noten und Dauern näher beieinanderliegen.

Die Ausgaben der Embeddings werden in mergeLayer konkateniert. Dann führen zwei LSTM-Schichten die Verarbeitung fort. In der ersten setzen wir return_sequences auf True, um auch Zwischenzustände zurückzugeben – das kann die Leistung verbessern, etwa mit Attention. Schließlich leiten wir die LSTM-Ausgabe durch eine Dense-Schicht und dann in zwei Ausgabeschichten: chordOutput und durationOutput. Das keras-Modell definieren wir per Functional API mit zwei Ein- und zwei Ausgängen.

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

Mit dem definierten Modell können wir jetzt kompilieren und trainieren. Für viele Anwendungen lohnt es sich, Regularisierung sowie zusätzliche Dense- und LSTM-Schichten zu ergänzen. Beachte die zwei Ein- und zwei Ausgänge, wie in der Grafik unten. Mehr zu Multi-Input-/Multi-Output-Modellen erfährst du im Kurs Introduction to TensorFlow in Python.

LSTM-Architektur
This figure shows the architecture of the LSTM model.
# Compile the model
lstm.compile(loss='categorical_crossentropy', optimizer='rmsprop')

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

Nach dem Training generieren wir mit dem LSTM neue Stücke. Dazu geben wir zunächst eine Sequenz aus 32 Akkorden und Dauern vor und erhalten die ersten Vorhersagen. Diese hängen wir an die Reihen an und sagen erneut voraus – basierend auf den letzten 32 Elementen der aktualisierten Reihen. Der Vorteil des LSTM: Wir können beliebig oft iterieren und so Stücke beliebiger Länge erzeugen.

# 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

Zum Abschluss exportieren wir die generierte Musik mit music21 als MIDI-Datei. Der Code ist fast identisch zu den Autoencoder-Beispielen, mit einem wichtigen Unterschied: Wir hängen nun sowohl Akkorde als auch Dauern an das stream-Objekt an.

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

Mit Autoencoder und VAE war es schwierig, Melodien zu erzeugen, und die Ausgabe war auf feste Längen beschränkt. Mit dem LSTM können wir durch Iteration beliebig lange Sequenzen generieren – und meist auch leichter bessere Melodien. Das zeigen die folgenden Beispiele, jeweils mit initialer Sequenz aus unserem Trainingsset.

Beispiel: Generierter Song #1
Beispiel: Generierter Song #2
Beispiel: Generierter Song #3

   introduction to tensorflow in python course banner

Weitere Themen

Daten

Eine zentrale Herausforderung für ML-Modelle in der Musik ist die Verfügbarkeit von Datensätzen, da Musik selten frei und legal herunterladbar ist. Dadurch fehlt der Forschung oft ein gemeinsamer Standarddatensatz – so wie MNIST für Bildklassifikation. Es gibt jedoch einige Projekte mit Roh-Audio, symbolischer Notation oder abgeleiteten Merkmalen für viele Songs. Eine Auswahl:

  1. Mutopia Project. Ein Archiv öffentlicher und Creative-Commons-Musik in MIDI-, PDF- und LilyPond-Formaten. Über 2000 Stücke, organisiert nach Instrument, Komponist, Stil und Sammlung.
  2. The Million Song Dataset. Enthält Audio-Features und Metadaten für über 300 GB populärer Tracks, jedoch keine Audiofiles.
  3. Free Music Archive. Enthält Audio, Genrelabels und Features für über 100.000 frei und legal verfügbare Tracks.

Sampling vs. Determinismus

Bei der LSTM-Generierung haben wir Akkorde und Dauern deterministisch gewählt: Das Modell gibt Wahrscheinlichkeiten für 267 Akkorde und 9 Dauern aus, und wir wählen jeweils das Maximum. Ähnlich haben wir bei Autoencoder und VAE die höchsten Outputwerte gewählt.

Alternativ können wir aus der Verteilung sampeln statt die wahrscheinlichste Option zu wählen. Das macht die Generierung stochastisch. Dann erzeugen Autoencoder- oder VAE-Modelle mit demselben latenten Vektor bei jedem Lauf unterschiedliche Stücke, und dieselbe Inputsequenz im LSTM kann zu verschiedenen Songs führen.

Der folgende Code zeigt die Umstellung auf stochastische Generierung, indem wir in predictChords() argmax() durch Sampling gemäß der vorhergesagten Verteilungen ersetzen.

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

Eine ausführliche Diskussion zu stochastischem vs. deterministischem Vorgehen findest du in Abschnitt 6.6 von Briot, Hadjerest und Pachet (2019).

Embeddings

Im LSTM-Training haben wir Embeddings verwendet, die jeden Eingabe-Akkord und jede Dauer auf einen dichten 64-dimensionalen Vektor abbilden. So vermeiden wir spärliche, hochdimensionale Eingaben und erhalten eine kompakte Repräsentation, die Beziehungen zwischen Akkorden sichtbar macht.

Oft ist es nicht sinnvoll, Embeddings selbst zu trainieren – vor allem bei kleinen Datensätzen. Vorgefertigte oder unüberwacht gelernte Embeddings, wie bei Textklassifikation und -generierung üblich, machen das Training oft erst praktikabel.

Eine Übersicht zur Erstellung unüberwachter Embeddings liefert Chuan, Agres und Herremans (2018).

Generative Adversarial Networks

Neben Autoencoder, VAE und LSTM ist auch das Generative Adversarial Network (GAN) eine Option, die in anderen Domänen sehr erfolgreich war. Ein GAN kombiniert ein diskriminatives mit einem generativen Netz. Das Generativnetz erzeugt Samples – etwa Akkordfolgen wie unsere LSTM- und Autoencoder-Modelle. Diese werden zusammen mit echten Samples (Akkordfolgen realer Songs) dem Diskriminator vorgelegt, der zwischen echt und generiert unterscheidet. Der Generator versucht, den Diskriminator zu täuschen. Trainiert wird bis zu einem stabilen Gleichgewicht, in dem keine Seite noch einen Vorteil gewinnt.

Wenn du GANs für Musikgenerierung einsetzen willst, sind diese Ressourcen hilfreich:

  1. MuseGAN – ein Projekt zur Generierung polyphoner Musik mit GANs. Enthält vortrainierte Modelle und 174.154 bereinigte und aufbereitete Stücke im Pianoroll-Format.

  2. Magenta – ein Open-Source-Projekt für generative Modellierung in kreativen Anwendungen, darunter Musik. Die Tools lassen sich in einem Colab-Notebook erkunden.

Ressourcen

Wenn du mehr über TensorFlow 2.0 lernen möchtest, sieh dir auf DataCamp den Kurs Introduction to TensorFlow in Python an. Eine aktuelle, umfassende Übersicht zu Methoden der Musikgenerierung mit Deep Learning findest du in Briot, Hadjerest und Pachet (2019). Weitere Tutorials zur Musikgenerierung mit Deep Learning: 1, 2, 3, 4 und 5 mit einer vollständigen Buchbehandlung generativer Modelle.

Anhang

Kontinuierliche Repräsentation

WAV-Dateien sind die verbreitetste kontinuierliche Repräsentation, da sie unkomprimiertes, rohes Audiosignal enthalten. Das unterscheidet sie z. B. von MP3, das durch Kompression Informationen verwirft.

Der folgende Code zeigt, wie wir aus einer WAV-Datei mit einem Mozart-Stück die Abtastrate rate und das Rohsignal signal extrahieren. Die Abtastrate ist die Anzahl der Abtastungen pro Sekunde. In unserem Fall 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))

Das Rohsignal signal ist ein S x C-Tensor, wobei S die Anzahl der Samples und C die Anzahl der Kanäle ist. Der Einfachheit halber betrachten wir C = 1. Die folgende Grafik zeigt einen Dreisekunden-Ausschnitt von signal.

This figure shows a plot of a normalized audio signal extracted from a WAV file.

Rohsignale zum Training zu verwenden, ist generell deutlich anspruchsvoller als diskrete Repräsentationen. Eine aktuelle Übersicht nennt 32 Methoden zur Musikgenerierung mit Deep Learning – nur drei davon arbeiten mit kontinuierlichen Repräsentationen:

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

Tonarten-Transposition

Zur Dimensionsreduktion oder für Data Augmentation eignet sich die Tonarten-Transposition. Dabei verschieben wir jede Tonhöhe um ein festes Intervall. Enthält ein Stück z. B. die Folge C3 gefolgt von D3 und wir transponieren um zwei Halbtöne nach oben, wird daraus D3 und E3. Beispiele für Modelle mit Transposition findest du bei Lim, Rhyu und Lee (2018) und Sturm et al. (2016).

Nutzen wir Transposition zur Dimensionsreduktion, möchten wir Noten und Akkorde so verschieben, dass die Gesamtzahl der einzigartigen Elemente im Datensatz sinkt. Da Musik meist um bestimmte Tonleitern (Tonarten) herum geschrieben wird, können wir praktisch alle Stücke in die gleiche Tonart verschieben.

Diesmal importieren wir drei Untermodule aus music21:

  1. converter zum Laden einer MIDI-Datei und Umwandeln in ein stream-Objekt.
  2. pitch zum Erzeugen der Zieltonhöhe.
  3. interval zum Berechnen des Abstands zwischen ursprünglicher und Zieltonart.
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

Wir rufen .analyze('key') auf score auf und erhalten C-Dur als Tonart. Dann berechnen wir das Intervall zwischen C-Dur und der Zieltonart F-Dur. Die Tonhöhe der key-Instanz greifen wir über .tonic ab, die Zieltonhöhe erzeugen wir mit pitch.Pitch('F') und speichern sie in keyInterval.

Abschließend transponieren wir mit .transpose() und dem Intervall keyInterval. Die Ausgabe bestätigt: F-Dur.

Oktav-Transposition

Zusätzlich können wir auch die Oktave nach oben oder unten verschieben. Statt C-Dur nach F-Dur zu transponieren, könnten wir C-Dur in eine tiefere oder höhere Oktave verschieben. Wenn wir für alle Stücke den gleichen Oktavbereich wählen, rücken Noten und Akkorde enger zusammen, die Dimension sinkt und das Training wird einfacher. Im obigen Code ginge das, indem wir die Oktave im Pitch spezifizieren, z. B. pitch.Pitch('F3').

Tonarten- und Oktaveninvarianz

Eine Alternative zur Reduktion ist Data Augmentation: Statt die Möglichkeiten zu beschneiden, erweitern wir den Datensatz durch Transformationen. Auch hier helfen Tonarten- und Oktavtranspositionen. Anstatt alle Stücke in eine gemeinsame Tonart zu bringen, transponieren wir in alle Tonarten und Oktaven. So erreichen wir Tonarten- und Oktaveninvarianz im Modell. Siehe Briot, Hadjerest und Pachet (2019) zur Diskussion beider Ansätze.

Datensammlung

Wir beziehen die MIDI-Dateien zum Training vom Project Mutopia. Bevor wir scrapen, prüfen wir die robots-Datei unter https://www.mutopiaproject.org/robots.txt, um die Vorgaben für automatisierte Zugriffe zu respektieren. Zum Zeitpunkt der Erstellung enthielt sie den folgenden Text. User-agent: * bedeutet, die Regel gilt für alle Nutzer. Disallow: ohne Eintrag heißt: Es gibt keine Einschränkungen.

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

Wir importieren urlopen und urlretrieve aus urllib.request, um die Seiten des Gitarrenbereichs abzurufen und die dort verlinkten MIDI-Dateien herunterzuladen. BeautifulSoup parst das HTML aus urlopen und hilft uns, MIDI-Links zu finden. Mit time pausieren wir 10 Sekunden zwischen Downloads, um die Website zu schonen.

This figure shows a page in the guitar section of the Mutopia website with MIDI Files identified.

Jede Iteration der Hauptschleife prüft, ob linkCount > 0 ist. Falls ja, wird die URL der nächsten Seite url aus url0, url1 und der Seitennummer songNumber konstruiert. Wir öffnen die URL, parsen das HTML, finden alle Links, setzen linkCount auf 0 und iterieren über die Liste. Enthält ein Link die Zeichenfolge .mid, verweist er auf eine MIDI-Datei, die wir mit urlretrieve() ins Verzeichnis save_dir laden. Am Ende erhöhen wir songNumber um 10, da jede Seite 10 Stücke enthält. Der Prozess endet, wenn eine Seite weniger als 10 Links enthält.

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

Mehr zum Scraping findest du im DataCamp-Kurs Web Scraping with Python und im Tutorial Web Scraping using Python.

Themen
Python
Maschinelles Lernen

Mehr über Python und Machine Learning lernen

Kurs

Einführung in TensorFlow mit Python

4 Std.
56.5K
Dieser Kurs zeigt dir, was neuronale Netze sind und wie du mit TensorFlow Deep-Learning-Modelle erstellst.
Details anzeigenRight Arrow
Kurs Starten
Mehr anzeigenRight Arrow