Direkt zum Inhalt

Einen Transformator mit PyTorch bauen

Lerne, wie du mit PyTorch, einem leistungsstarken Werkzeug des modernen maschinellen Lernens, ein Transformer-Modell erstellst.
Aktualisierte 11. Sept. 2024  · 26 Min. Lesezeit

Das Ziel dieses Tutorials ist es, ein umfassendes Verständnis dafür zu vermitteln, wie man mit PyTorch ein Transformer-Modell konstruiert. Der Transformer ist eines der leistungsfähigsten Modelle des modernen maschinellen Lernens. Sie haben das Feld revolutioniert, vor allem bei Aufgaben der natürlichen Sprachverarbeitung (NLP) wie der Sprachübersetzung und Textzusammenfassung. Long Short-Term Memory (LSTM)-Netzwerke wurden bei diesen Aufgaben durch Transformers ersetzt, weil sie mit weitreichenden Abhängigkeiten und parallelen Berechnungen umgehen können.

Das Tool, das in diesem Leitfaden zur Erstellung des Transformers verwendet wird, ist PyTorch, eine beliebte Open-Source-Bibliothek für maschinelles Lernen, die für ihre Einfachheit, Vielseitigkeit und Effizienz bekannt ist. Mit seinem dynamischen Berechnungsgraphen und den umfangreichen Bibliotheken ist PyTorch zu einer festen Größe für Forscher und Entwickler im Bereich des maschinellen Lernens und der künstlichen Intelligenz geworden.

Wer mit PyTorch nicht vertraut ist, sollte den DataCamp-Kurs Introduction to Deep Learning with PyTorch besuchen, um eine solide Grundlage zu erhalten.

KI-Anwendungen entwickeln

Lerne, wie man KI-Anwendungen mit der OpenAI API erstellt.
Start Upskilling For Free

Hintergrund und Theorie

Die Transformatoren wurden erstmals in dem Artikel Attention is All You Need von Vaswani et al. vorgestellt und sind seitdem aufgrund ihres einzigartigen Designs und ihrer Effektivität zu einem Eckpfeiler vieler NLP-Aufgaben geworden.

Das Herzstück von Transformers ist der Aufmerksamkeitsmechanismus, insbesondere das Konzept der "Selbstaufmerksamkeit", das es dem Modell ermöglicht, verschiedene Teile der Eingabedaten zu gewichten und zu priorisieren. Dieser Mechanismus ermöglicht es Transformers, weitreichende Abhängigkeiten in Daten zu verwalten. Es handelt sich im Grunde um ein Gewichtungsschema, das es einem Modell ermöglicht, sich bei der Produktion einer Ausgabe auf verschiedene Teile der Eingabe zu konzentrieren.

Dieser Mechanismus ermöglicht es dem Modell, verschiedene Wörter oder Merkmale in der Eingabesequenz zu berücksichtigen und jedem ein "Gewicht" zuzuweisen, das seine Bedeutung für die Produktion einer bestimmten Ausgabe angibt.

Bei einer Satzübersetzungsaufgabe kann das Modell zum Beispiel während der Übersetzung eines bestimmten Wortes den Wörtern, die grammatikalisch oder semantisch mit dem Zielwort verwandt sind, eine höhere Aufmerksamkeitsgewichtung zuweisen. Dieser Prozess ermöglicht es dem Transformer, Abhängigkeiten zwischen Wörtern oder Merkmalen zu erfassen, unabhängig von ihrem Abstand zueinander in der Sequenz.

Der Einfluss von Transformers auf den Bereich NLP kann gar nicht hoch genug eingeschätzt werden. Sie haben die traditionellen Modelle bei vielen Aufgaben übertroffen und gezeigt, dass sie die menschliche Sprache besser verstehen und nuancierter generieren können.

Für ein tieferes Verständnis von NLP ist der DataCamp-Kurs Einführung in die natürliche Sprachverarbeitung in Python eine empfehlenswerte Ressource.

PyTorch einrichten

Bevor du mit dem Bau eines Transformators beginnst, ist es wichtig, dass du die Arbeitsumgebung richtig einrichtest. Zuallererst muss PyTorch installiert werden. PyTorch (aktuelle stabile Version - 2.0.1) kann einfach über pip oder conda Paketmanager installiert werden.

Für pip verwendest du den Befehl:

pip3 install torch torchvision torchaudio

Für conda verwendest du den Befehl:

conda install pytorch torchvision torchaudio pytorch-cuda=11.7 -c pytorch -c nvidia

Um pytorch mit einer CPU zu verwenden, besuche bitte die pytorch Dokumentation.

Außerdem ist es von Vorteil, ein grundlegendes Verständnis von Deep Learning-Konzepten zu haben, da diese für das Verständnis der Funktionsweise von Transformers grundlegend sind. Für diejenigen, die eine Auffrischung brauchen, ist der DataCamp-Kurs Deep Learning in Python eine wertvolle Ressource, die die wichtigsten Konzepte des Deep Learning abdeckt.

Bau des Transformatormodells mit PyTorch

Um das Transformatormodell zu bauen, sind die folgenden Schritte notwendig:

  1. Importieren der Bibliotheken und Module
  2. Definition der grundlegenden Bausteine - Aufmerksamkeit mit mehreren Köpfen, positionsbezogene Feed-Forward-Netzwerke, Positionskodierung
  3. Bau des Encoder-Blocks
  4. Bau des Decoder-Blocks
  5. Kombinieren der Encoder- und Decoder-Schichten, um das komplette Transformer-Netzwerk zu erstellen

1. Importieren der erforderlichen Bibliotheken und Module

Wir beginnen mit dem Import der PyTorch-Bibliothek für die Kernfunktionen, des Moduls für neuronale Netze zum Erstellen neuronaler Netze, des Optimierungsmoduls zum Trainieren von Netzen und der Data-Utility-Funktionen zum Verarbeiten von Daten. Außerdem importieren wir das Standard-Python-Mathe-Modul für mathematische Operationen und das Copy-Modul, um Kopien von komplexen Objekten zu erstellen.

Diese Werkzeuge bilden die Grundlage für die Definition der Modellarchitektur, die Verwaltung der Daten und die Einrichtung des Schulungsprozesses.

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import math
import copy

2. Definition der Grundbausteine: Multi-Head Attention, Position-wise Feed-Forward Networks, Positional Encoding

Multi-Kopf Achtung

Der Multi-Head-Attention-Mechanismus berechnet die Aufmerksamkeit zwischen jedem Paar von Positionen in einer Sequenz. Es besteht aus mehreren "Aufmerksamkeitsköpfen", die verschiedene Aspekte der Eingangssequenz erfassen.

Mehr über Multi-Head Attention erfährst du im Abschnitt Aufmerksamkeitsmechanismen des Kurses Large Language Models (LLMs) Concepts.

Abbildung 1. Multi-Head Attention (Quelle: vom Autor erstelltes Bild)

Abbildung 1. Multi-Head Attention (Quelle: vom Autor erstelltes Bild)

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        # Ensure that the model dimension (d_model) is divisible by the number of heads
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
        
        # Initialize dimensions
        self.d_model = d_model # Model's dimension
        self.num_heads = num_heads # Number of attention heads
        self.d_k = d_model // num_heads # Dimension of each head's key, query, and value
        
        # Linear layers for transforming inputs
        self.W_q = nn.Linear(d_model, d_model) # Query transformation
        self.W_k = nn.Linear(d_model, d_model) # Key transformation
        self.W_v = nn.Linear(d_model, d_model) # Value transformation
        self.W_o = nn.Linear(d_model, d_model) # Output transformation
        
    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        # Calculate attention scores
        attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        # Apply mask if provided (useful for preventing attention to certain parts like padding)
        if mask is not None:
            attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
        
        # Softmax is applied to obtain attention probabilities
        attn_probs = torch.softmax(attn_scores, dim=-1)
        
        # Multiply by values to obtain the final output
        output = torch.matmul(attn_probs, V)
        return output
        
    def split_heads(self, x):
        # Reshape the input to have num_heads for multi-head attention
        batch_size, seq_length, d_model = x.size()
        return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
        
    def combine_heads(self, x):
        # Combine the multiple heads back to original shape
        batch_size, _, seq_length, d_k = x.size()
        return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
        
    def forward(self, Q, K, V, mask=None):
        # Apply linear transformations and split heads
        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))
        
        # Perform scaled dot-product attention
        attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
        
        # Combine heads and apply output transformation
        output = self.W_o(self.combine_heads(attn_output))
        return output

Klassendefinition und Initialisierung:

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):

Die Klasse ist als Unterklasse von PyTorch's nn.Module definiert.

  1. d_model: Dimensionalität des Inputs.
  2. num_heads: Die Anzahl der Aufmerksamkeitsköpfe, in die die Eingabe aufgeteilt wird.

Die Initialisierung prüft, ob d_model durch num_heads teilbar ist, und definiert dann die Transformationsgewichte für Query, Key, Value und Output.

Skaliertes Dot-Produkt Achtung:

def scaled_dot_product_attention(self, Q, K, V, mask=None):
  1. Berechnung der Aufmerksamkeitspunkte: attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k). Hier werden die Aufmerksamkeitswerte berechnet, indem das Punktprodukt aus Abfragen (Q) und Schlüsseln (K) gebildet und dann mit der Quadratwurzel der Schlüsseldimension (d_k) skaliert wird.
  2. Maske auftragen: Wenn eine Maske angegeben wird, wird sie auf die Aufmerksamkeitswerte angewendet, um bestimmte Werte auszublenden.
  3. Berechnung der Aufmerksamkeitsgewichte: Die Aufmerksamkeitswerte werden durch eine Softmax-Funktion geleitet, um sie in Wahrscheinlichkeiten umzuwandeln, die sich zu 1 summieren.
  4. Berechne die Leistung: Die endgültige Leistung der Aufmerksamkeit wird berechnet, indem die Aufmerksamkeitsgewichte mit den Werten (V) multipliziert werden.

Köpfe spalten:

def split_heads(self, x):

Diese Methode formt die Eingabe x in die Form (batch_size, num_heads, seq_length, d_k) um. Sie ermöglicht es dem Modell, mehrere Aufmerksamkeitsköpfe gleichzeitig zu verarbeiten, sodass eine parallele Berechnung möglich ist.

Köpfe kombinieren:

def combine_heads(self, x):

Nachdem die Aufmerksamkeit auf jeden einzelnen Kopf gerichtet wurde, kombiniert diese Methode die Ergebnisse wieder zu einem einzigen Tensor der Form (batch_size, seq_length, d_model). Dadurch wird das Ergebnis für die weitere Verarbeitung vorbereitet.

Vorwärtsmethode:

def forward(self, Q, K, V, mask=None):

Bei der Vorwärtsmethode findet die eigentliche Berechnung statt:

  1. Wende lineare Transformationen an: Die Abfragen (Q), Schlüssel (K) und Werte (V) werden zunächst durch lineare Transformationen mit den in der Initialisierung definierten Gewichten geführt.
  2. Geteilte Köpfe: Die transformierten Q, K, V werden mit der split_heads-Methode in mehrere Köpfe aufgeteilt.
  3. Anwendung des skalierten Punktprodukts Achtung! Die Methode scaled_dot_product_attention wird für die geteilten Köpfe aufgerufen.
  4. Kombiniere die Köpfe: Die Ergebnisse der einzelnen Köpfe werden mit der Methode combine_heads wieder zu einem einzigen Tensor kombiniert.
  5. Output Transformation anwenden: Schließlich wird der kombinierte Tensor durch eine lineare Ausgangstransformation geführt.

Zusammenfassend lässt sich sagen, dass die Klasse MultiHeadAttention den Multi-Head-Attention-Mechanismus kapselt, der häufig in Transformatormodellen verwendet wird. Es sorgt dafür, dass der Input in mehrere Aufmerksamkeitsköpfe aufgeteilt wird, jeder Kopf Aufmerksamkeit erhält und die Ergebnisse dann kombiniert werden. Auf diese Weise kann das Modell verschiedene Beziehungen in den Eingabedaten auf unterschiedlichen Skalen erfassen und die Aussagekraft des Modells verbessern.

Position-wise Feed-Forward Networks

class PositionWiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(PositionWiseFeedForward, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()

    def forward(self, x):
        return self.fc2(self.relu(self.fc1(x)))

Klassendefinition:

class PositionWiseFeedForward(nn.Module):

Die Klasse ist eine Unterklasse von PyTorch's nn.Module, d.h. sie erbt alle Funktionen, die für die Arbeit mit neuronalen Netzwerkschichten erforderlich sind.

Initialisierung:

def __init__(self, d_model, d_ff):
    super(PositionWiseFeedForward, self).__init__()
    self.fc1 = nn.Linear(d_model, d_ff)
    self.fc2 = nn.Linear(d_ff, d_model)
    self.relu = nn.ReLU()
  1. d_model: Dimensionalität von Input und Output des Modells.
  2. d_ff: Dimensionalität der inneren Schicht im Feed-Forward-Netz.
  3. self.fc1 und self.fc2: Zwei vollständig verbundene (lineare) Schichten mit den durch d_model und d_ff definierten Eingangs- und Ausgangsgrößen.
  4. self.relu: ReLU (Rectified Linear Unit) Aktivierungsfunktion, die eine Nichtlinearität zwischen den beiden linearen Schichten einführt.

Vorwärtsmethode:

def forward(self, x):
    return self.fc2(self.relu(self.fc1(x)))
  1. x: Die Eingabe für das Feed-Forward-Netzwerk.
  2. self.fc1(x): Die Eingabe wird zunächst durch die erste lineare Schicht (fc1) geleitet.
  3. self.relu(...): Die Ausgabe von fc1 wird dann durch eine ReLU-Aktivierungsfunktion geleitet. ReLU ersetzt alle negativen Werte durch Nullen und bringt so Nichtlinearität in das Modell.
  4. self.fc2(...): Die aktivierte Ausgabe wird dann durch die zweite lineare Schicht (fc2) geleitet und erzeugt die endgültige Ausgabe.

Zusammenfassend lässt sich sagen, dass die Klasse PositionWiseFeedForward ein positionsabhängiges Feed-Forward-Netz definiert, das aus zwei linearen Schichten mit einer dazwischen liegenden ReLU-Aktivierungsfunktion besteht. Bei Transformatormodellen wird dieses Feedforward-Netzwerk auf jede Position separat und identisch angewendet. Er hilft dabei, die von den Aufmerksamkeitsmechanismen gelernten Merkmale innerhalb des Transformators umzuwandeln und fungiert als zusätzlicher Verarbeitungsschritt für die Aufmerksamkeitsausgaben.

Positionelle Kodierung

Die Positionskodierung wird verwendet, um die Positionsinformationen jedes Tokens in der Eingabesequenz einzubringen. Es verwendet Sinus- und Kosinusfunktionen mit unterschiedlichen Frequenzen, um die Positionskodierung zu erzeugen.

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_seq_length):
        super(PositionalEncoding, self).__init__()
        
        pe = torch.zeros(max_seq_length, d_model)
        position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
        
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        
        self.register_buffer('pe', pe.unsqueeze(0))
        
    def forward(self, x):
        return x + self.pe[:, :x.size(1)]

Klassendefinition:

class PositionalEncoding(nn.Module):

Die Klasse ist als Unterklasse von PyTorch's nn.Module definiert, sodass sie als Standard-PyTorch-Layer verwendet werden kann.

Initialisierung:

def __init__(self, d_model, max_seq_length):
    super(PositionalEncoding, self).__init__()
    
    pe = torch.zeros(max_seq_length, d_model)
    position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
    div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
    
    pe[:, 0::2] = torch.sin(position * div_term)
    pe[:, 1::2] = torch.cos(position * div_term)
    
    self.register_buffer('pe', pe.unsqueeze(0))
  1. d_model: Die Dimension der Eingabe des Modells.
  2. max_seq_length: Die maximale Länge der Sequenz, für die Positionskodierungen vorberechnet werden.
  3. pe: Ein mit Nullen gefüllter Tensor, der mit Positionskodierungen aufgefüllt wird.
  4. position: Ein Tensor, der die Positionsindizes für jede Position in der Sequenz enthält.
  5. div_term: Ein Begriff, der verwendet wird, um die Positionsindizes auf eine bestimmte Weise zu skalieren.
  6. Die Sinusfunktion wird auf die geraden Indizes und die Kosinusfunktion auf die ungeraden Indizes von pe angewendet.
  7. Schließlich wird pe als Puffer registriert, d.h. er ist Teil des Modulstatus, wird aber nicht als trainierbarer Parameter betrachtet.

Vorwärtsmethode:

def forward(self, x):
    return x + self.pe[:, :x.size(1)]

Die Vorwärtsmethode fügt einfach die Positionskodierungen zur Eingabe x hinzu.

Es verwendet die ersten x.size(1) Elemente von pe, um sicherzustellen, dass die Positionskodierungen mit der tatsächlichen Sequenzlänge von x übereinstimmen.

Zusammenfassung

Die Klasse PositionalEncoding fügt Informationen über die Position der Token innerhalb der Sequenz hinzu. Da das Transformatormodell die Reihenfolge der Token nicht kennt (aufgrund seines Selbstbeobachtungsmechanismus), hilft diese Klasse dem Modell, die Position der Token in der Sequenz zu berücksichtigen. Die verwendeten Sinusfunktionen wurden so gewählt, dass das Modell leicht lernen kann, auf relative Positionen zu achten, da sie für jede Position in der Sequenz eine einzigartige und gleichmäßige Kodierung erzeugen.

3. Bau der Encoder-Blöcke

Abbildung 2. Der Encoderteil des Transformatorennetzwerks (Quelle: Bild aus dem Originalbeitrag)

Abbildung 2. Der Encoder-Teil des Transformatorennetzwerks (Quelle: Bild aus der Originalarbeit)

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, mask):
        attn_output = self.self_attn(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        return x

Klassendefinition:

class EncoderLayer(nn.Module):

Die Klasse ist als Unterklasse von PyTorch's nn.Module definiert, was bedeutet, dass sie als Baustein für neuronale Netze in PyTorch verwendet werden kann.

Initialisierung:

def __init__(self, d_model, num_heads, d_ff, dropout):
    super(EncoderLayer, self).__init__()
    self.self_attn = MultiHeadAttention(d_model, num_heads)
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.dropout = nn.Dropout(dropout)

Parameter:

  1. d_model: Die Dimensionalität der Eingabe.
  2. num_heads: Die Anzahl der Aufmerksamkeitsköpfe bei der Multi-Head-Aufmerksamkeit.
  3. d_ff: Die Dimensionalität der inneren Schicht im positionsweisen Feedforward-Netz.
  4. dropout: Die Dropout-Rate, die für die Regularisierung verwendet wird.

Komponenten:

  1. self.self_attn: Aufmerksamkeitsmechanismus mit mehreren Köpfen.
  2. self.feed_forward: Positionsbezogenes neuronales Netz mit Vorwärtskopplung.
  3. self.norm1 und self.norm2: Normalisierung der Ebene, um den Eingang der Ebene zu glätten.
  4. self.dropout: Dropout-Schicht, die verwendet wird, um eine Überanpassung zu verhindern, indem einige Aktivierungen beim Training zufällig auf Null gesetzt werden.

Vorwärtsmethode:

def forward(self, x, mask):
    attn_output = self.self_attn(x, x, x, mask)
    x = self.norm1(x + self.dropout(attn_output))
    ff_output = self.feed_forward(x)
    x = self.norm2(x + self.dropout(ff_output))
    return x

Input:

  1. x: Die Eingabe für die Encoderschicht.
  2. Maske: Optionale Maske, um bestimmte Teile des Inputs zu ignorieren.

Verarbeitungsschritte:

  1. Selbstaufmerksamkeit: Die Eingabe x wird durch den Mehrkopf-Selbstbehauptungsmechanismus geleitet.
  2. Hinzufügen & Normalisieren (nach Achtung): Der Ausgang der Aufmerksamkeit wird zum ursprünglichen Eingang addiert (Restverbindung), gefolgt von einem Dropout und der Normalisierung mit norm1.
  3. Feed-Forward Network: Die Ausgabe aus dem vorherigen Schritt wird durch das positionsbezogene Feedforward-Netzwerk geleitet.
  4. Hinzufügen & Normalisieren (nach Feed-Forward): Ähnlich wie in Schritt 2 wird der Feed-Forward-Ausgang zum Eingang dieser Stufe hinzugefügt (Restverbindung), gefolgt von Dropout und Normalisierung mit norm2.
  5. Output: Der verarbeitete Tensor wird als Ausgabe der Geberschicht zurückgegeben.

Zusammenfassung:

Die Klasse EncoderLayer definiert eine einzelne Schicht des Transformator-Encoders. Es umfasst einen Mechanismus zur Selbstbeobachtung mit mehreren Köpfen, gefolgt von einem neuronalen Netzwerk mit Vorwärtskopplung und Restverbindungen, die je nach Bedarf normalisiert und ausgeschaltet werden. Diese Komponenten zusammen ermöglichen es dem Encoder, komplexe Beziehungen in den Eingabedaten zu erfassen und sie in eine nützliche Darstellung für nachgelagerte Aufgaben zu verwandeln. Normalerweise werden mehrere solcher Geberschichten gestapelt, um den kompletten Geberteil eines Transformatormodells zu bilden.

4. Bau der Decoder-Blöcke

Abbildung 3. Der Decoder-Teil des Transformer-Netzwerks (Quelle: Bild aus der Originalarbeit)

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout):
        super(DecoderLayer, self).__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.cross_attn = MultiHeadAttention(d_model, num_heads)
        self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, enc_output, src_mask, tgt_mask):
        attn_output = self.self_attn(x, x, x, tgt_mask)
        x = self.norm1(x + self.dropout(attn_output))
        attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
        x = self.norm2(x + self.dropout(attn_output))
        ff_output = self.feed_forward(x)
        x = self.norm3(x + self.dropout(ff_output))
        return x

Klassendefinition:

class DecoderLayer(nn.Module):

Initialisierung:

def __init__(self, d_model, num_heads, d_ff, dropout):
    super(DecoderLayer, self).__init__()
    self.self_attn = MultiHeadAttention(d_model, num_heads)
    self.cross_attn = MultiHeadAttention(d_model, num_heads)
    self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.norm3 = nn.LayerNorm(d_model)
    self.dropout = nn.Dropout(dropout)

Parameter:

  1. d_model: Die Dimensionalität der Eingabe.
  2. num_heads: Die Anzahl der Aufmerksamkeitsköpfe bei der Multi-Head-Aufmerksamkeit.
  3. d_ff: Die Dimensionalität der inneren Schicht im Feed-Forward-Netz.
  4. dropout: Die Abbrecherquote für die Regularisierung.

Komponenten:

  1. self.self_attn: Mehrköpfiger Selbstaufmerksamkeitsmechanismus für die Zielsequenz.
  2. self.cross_attn: Mehrköpfiger Aufmerksamkeitsmechanismus, der sich um die Ausgabe des Encoders kümmert.
  3. self.feed_forward: Positionsbezogenes neuronales Netz mit Vorwärtskopplung.
  4. self.norm1, self.norm2, self.norm3: Komponenten zur Normalisierung der Ebenen.
  5. self.dropout: Dropout-Schicht für die Regularisierung.

Vorwärtsmethode:

ef forward(self, x, enc_output, src_mask, tgt_mask):
    attn_output = self.self_attn(x, x, x, tgt_mask)
    x = self.norm1(x + self.dropout(attn_output))
    attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
    x = self.norm2(x + self.dropout(attn_output))
    ff_output = self.feed_forward(x)
    x = self.norm3(x + self.dropout(ff_output))
    return x

Eingabe:

  1. x: Die Eingabe für die Decoderschicht.
  2. enc_output: Der Ausgang des entsprechenden Encoders (der im Schritt "Cross-Attention" verwendet wird).
  3. src_mask: Quellenmaske, um bestimmte Teile der Geberausgabe zu ignorieren.
  4. tgt_mask: Zielmaske, um bestimmte Teile der Decodereingabe zu ignorieren.

Verarbeitungsschritte:

  1. Selbstaufmerksamkeit bei der Zielsequenz: Die Eingabe x wird durch einen Selbstbeobachtungsmechanismus verarbeitet.
  2. Hinzufügen & Normalisieren (nach der Selbstaufmerksamkeit): Das Ergebnis der Selbstbeobachtung wird zu dem ursprünglichen x addiert, dann wird es herausgenommen und mit norm1 normalisiert.
  3. Cross-Attention mit Encoder-Ausgang: Die normalisierte Ausgabe aus dem vorherigen Schritt wird durch einen Cross-Attention-Mechanismus verarbeitet, der sich um die Ausgabe enc_output des Encoders kümmert.
  4. Hinzufügen & Normalisieren (nach Cross-Attention): Das Ergebnis der Cross-Attention wird dem Eingang dieser Stufe hinzugefügt, gefolgt von Dropout und Normalisierung mit norm2.
  5. Feed-Forward Network: Die Ausgabe aus dem vorherigen Schritt wird durch das Feed-Forward-Netzwerk geleitet.
  6. Hinzufügen & Normalisieren (nach Feed-Forward): Der Feed-Forward-Ausgang wird dem Eingang dieser Stufe hinzugefügt, gefolgt von Dropout und Normalisierung mit norm3.
  7. Output: Der verarbeitete Tensor wird als Ausgabe der Decoderschicht zurückgegeben.

Zusammenfassung:

Die Klasse DecoderLayer definiert eine einzelne Schicht des Decoders des Transformators. Es besteht aus einem Multi-Head-Self-Attention-Mechanismus, einem Multi-Head-Cross-Attention-Mechanismus (der sich um die Ausgabe des Encoders kümmert), einem neuronalen Netzwerk mit Vorwärtskopplung und den entsprechenden Restverbindungen, Schichtnormalisierung und Dropout-Schichten. Diese Kombination ermöglicht es dem Decoder, auf der Grundlage der Repräsentationen des Encoders sinnvolle Ausgaben zu erzeugen, die sowohl die Zielsequenz als auch die Quellsequenz berücksichtigen. Wie beim Encoder werden auch beim Decoder in der Regel mehrere Schichten übereinander gelegt, um den kompletten Decoderteil eines Transformatormodells zu bilden.

Als Nächstes werden die Encoder- und Decoder-Blöcke zusammengeführt, um das umfassende Transformer-Modell zu erstellen.

5. Kombinieren der Encoder- und Decoder-Schichten, um das komplette Transformer-Netzwerk zu erstellen

Abbildung 4. Das Transformatorennetzwerk (Quelle: Bild aus der Originalarbeit)

Abbildung 4. Das Transformer Network (Quelle: Bild aus dem Originalbeitrag)

class Transformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
        super(Transformer, self).__init__()
        self.encoder_embedding = nn.Embedding(src_vocab_size, d_model)
        self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model)
        self.positional_encoding = PositionalEncoding(d_model, max_seq_length)

        self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
        self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])

        self.fc = nn.Linear(d_model, tgt_vocab_size)
        self.dropout = nn.Dropout(dropout)

    def generate_mask(self, src, tgt):
        src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
        tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(3)
        seq_length = tgt.size(1)
        nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
        tgt_mask = tgt_mask & nopeak_mask
        return src_mask, tgt_mask

    def forward(self, src, tgt):
        src_mask, tgt_mask = self.generate_mask(src, tgt)
        src_embedded = self.dropout(self.positional_encoding(self.encoder_embedding(src)))
        tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))

        enc_output = src_embedded
        for enc_layer in self.encoder_layers:
            enc_output = enc_layer(enc_output, src_mask)

        dec_output = tgt_embedded
        for dec_layer in self.decoder_layers:
            dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)

        output = self.fc(dec_output)
        return output

Klassendefinition:

class Transformer(nn.Module):

Initialisierung:

def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):

Der Konstruktor nimmt die folgenden Parameter entgegen:

  1. src_vocab_size: Größe des Quellvokabulars.
  2. tgt_vocab_size: Ziel-Wortschatzgröße.
  3. d_model: Die Dimensionalität der Einbettungen des Modells.
  4. num_heads: Anzahl der Aufmerksamkeitsköpfe im Multi-Head-Attention-Mechanismus.
  5. num_layers: Anzahl der Schichten sowohl für den Encoder als auch für den Decoder.
  6. d_ff: Dimensionalität der inneren Schicht im Feed-Forward-Netz.
  7. max_seq_length: Maximale Sequenzlänge für die Positionskodierung.
  8. dropout: Abbrecherquote für die Regularisierung.

Und sie definiert die folgenden Komponenten:

  1. self.encoder_embedding: Einbettungsebene für die Quellsequenz.
  2. self.decoder_embedding: Einbettungsebene für die Zielsequenz.
  3. self.positional_encoding: Komponente zur Positionskodierung.
  4. self.encoder_layers: Eine Liste der Geberschichten.
  5. self.decoder_layers: Eine Liste der Decoderebenen.
  6. self.fc: Endgültige voll verknüpfte (lineare) Schicht, die der Zielvokabelgröße zugeordnet ist.
  7. self.dropout: Dropout-Ebene.

Maske generieren Methode:

def generate_mask(self, src, tgt):

Mit dieser Methode werden Masken für die Quell- und die Zielsequenz erstellt. Dadurch wird sichergestellt, dass Auffüllungszeichen ignoriert werden und dass zukünftige Zeichen beim Training der Zielsequenz nicht sichtbar sind.

Vorwärtsmethode:

def forward(self, src, tgt):

Diese Methode definiert den Vorwärtspass für den Transformer, der Quell- und Zielsequenzen nimmt und die Ausgabevorhersagen erstellt.

  1. Input Embedding und Positional Encoding: Die Quell- und Zielsequenzen werden zunächst mit ihren jeweiligen Einbettungsebenen eingebettet und dann zu ihren Positionskodierungen hinzugefügt.
  2. Geberschichten: Die Quellsequenz wird durch die Encoderschichten geleitet, wobei der letzte Encoderausgang die verarbeitete Quellsequenz darstellt.
  3. Decoderschichten: Die Zielsequenz und die Ausgabe des Encoders werden durch die Decoderschichten geleitet und ergeben die Ausgabe des Decoders.
  4. Letzte lineare Schicht: Die Ausgabe des Decoders wird mithilfe einer voll verknüpften (linearen) Schicht auf die Zielvokabelgröße abgebildet.

Ausgabe:

Die endgültige Ausgabe ist ein Tensor, der die Vorhersagen des Modells für die Zielsequenz darstellt.

Zusammenfassung:

Die Transformer-Klasse fasst die verschiedenen Komponenten eines Transformer-Modells zusammen, darunter die Einbettungen, die Positionskodierung, die Kodierschichten und die Dekodierschichten. Es bietet eine bequeme Schnittstelle für Training und Inferenz, die die Komplexität von Multi-Head-Attention, Feed-Forward-Netzen und Layer-Normalisierung in sich vereint.

Diese Implementierung folgt der Standard-Transformer-Architektur, wodurch sie sich für Sequenz-zu-Sequenz-Aufgaben wie maschinelle Übersetzung, Textzusammenfassung usw. eignet. Die Maskierung stellt sicher, dass das Modell die kausalen Abhängigkeiten innerhalb der Sequenzen einhält, indem es Auffüllungs-Token ignoriert und verhindert, dass Informationen aus zukünftigen Token durchsickern.

Diese aufeinanderfolgenden Schritte befähigen das Transformer-Modell, Eingangssequenzen effizient zu verarbeiten und entsprechende Ausgangssequenzen zu produzieren.

Training des PyTorch Transformatormodells

Vorbereitung der Probendaten

Zur Veranschaulichung wird in diesem Beispiel ein Dummy-Datensatz erstellt. In einem praktischen Szenario würde jedoch ein umfangreicherer Datensatz verwendet werden, und der Prozess würde eine Textvorverarbeitung zusammen mit der Erstellung von Vokabelzuordnungen sowohl für die Ausgangs- als auch die Zielsprache beinhalten.

src_vocab_size = 5000
tgt_vocab_size = 5000
d_model = 512
num_heads = 8
num_layers = 6
d_ff = 2048
max_seq_length = 100
dropout = 0.1

transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

# Generate random sample data
src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)
tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)

Hyperparameter:

Diese Werte definieren die Architektur und das Verhalten des Transformatormodells:

  1. src_vocab_size, tgt_vocab_size: Vokabelgrößen für Quell- und Zielsequenzen, beide auf 5000 gesetzt.
  2. d_model: Dimensionalität der Einbettungen des Modells, eingestellt auf 512.
  3. num_heads: Anzahl der Aufmerksamkeitsköpfe im Multi-Head-Attention-Mechanismus, eingestellt auf 8.
  4. num_layers: Anzahl der Schichten für den Encoder und den Decoder, eingestellt auf 6.
  5. d_ff: Dimensionalität der inneren Schicht im Feed-Forward-Netz, eingestellt auf 2048.
  6. max_seq_length: Maximale Sequenzlänge für die Positionskodierung, festgelegt auf 100.
  7. dropout: Dropout-Rate für die Regularisierung, eingestellt auf 0,1.

Erstellen einer Transformer-Instanz:

transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout)

Diese Zeile erstellt eine Instanz der Transformer-Klasse und initialisiert sie mit den angegebenen Hyperparametern. Die Instanz hat die Architektur und das Verhalten, das durch diese Hyperparameter definiert wird.

Generierung von Zufallsstichprobendaten:

Die folgenden Zeilen erzeugen zufällige Quell- und Zielsequenzen:

  1. src_data: Zufällige ganze Zahlen zwischen 1 und src_vocab_size, die einen Stapel von Quellsequenzen mit der Form (64, max_seq_length) darstellen.
  2. tgt_data: Zufällige ganze Zahlen zwischen 1 und tgt_vocab_size, die einen Stapel von Zielsequenzen mit der Form (64, max_seq_length) darstellen.
  3. Diese zufälligen Sequenzen können als Eingaben für das Transformatormodell verwendet werden. Dabei wird ein Datenstapel mit 64 Beispielen und Sequenzen der Länge 100 simuliert.

Zusammenfassung:

Das Codeschnipsel zeigt, wie man ein Transformatormodell initialisiert und zufällige Quell- und Zielsequenzen erzeugt, die in das Modell eingespeist werden können. Die gewählten Hyperparameter bestimmen die spezifische Struktur und die Eigenschaften des Transformators. Dieses Setup könnte Teil eines größeren Skripts sein, in dem das Modell für tatsächliche Sequenz-zu-Sequenz-Aufgaben wie maschinelle Übersetzung oder Textzusammenfassung trainiert und bewertet wird.

Das Modell trainieren

Als Nächstes wird das Modell mit den oben erwähnten Beispieldaten trainiert. In einem realen Szenario würde jedoch ein wesentlich größerer Datensatz verwendet werden, der in der Regel in verschiedene Sätze für Trainings- und Validierungszwecke aufgeteilt wird.

criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

transformer.train()

for epoch in range(100):
    optimizer.zero_grad()
    output = transformer(src_data, tgt_data[:, :-1])
    loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, 1:].contiguous().view(-1))
    loss.backward()
    optimizer.step()
    print(f"Epoch: {epoch+1}, Loss: {loss.item()}")

Verlustfunktion und Optimierer:

  1. criterion = nn.CrossEntropyLoss(ignore_index=0): Definiert die Verlustfunktion als Kreuzentropieverlust. Das Argument ignore_index wird auf 0 gesetzt, was bedeutet, dass der Verlust Ziele mit einem Index von 0 (normalerweise für Auffüll-Token reserviert) nicht berücksichtigt.
  2. optimizer = optim.Adam(...): Definiert den Optimierer als Adam mit einer Lernrate von 0,0001 und bestimmten Beta-Werten.

Model Training Mode:

  1. transformer.train(): Setzt das Transformatormodell in den Trainingsmodus und ermöglicht so Verhaltensweisen wie Dropout, die nur während des Trainings gelten.

Trainingsschleife:

Das Codeschnipsel trainiert das Modell für 100 Epochen mit einer typischen Trainingsschleife:

  1. for epoch in range(100): Iteriert über 100 Trainingsepochen.
  2. optimizer.zero_grad(): Löscht die Farbverläufe aus der vorherigen Iteration.
  3. output = transformer(src_data, tgt_data[:, :-1]): Lässt die Quelldaten und die Zieldaten (mit Ausnahme des letzten Tokens in jeder Sequenz) durch den Transformer laufen. Das ist üblich bei Sequenz-zu-Sequenz-Aufgaben, bei denen das Ziel um ein Token verschoben wird.
  4. Verlust = Kriterium(...): Berechnet den Verlust zwischen den Vorhersagen des Modells und den Zieldaten (mit Ausnahme des ersten Tokens in jeder Sequenz). Der Verlust wird berechnet, indem die Daten in eindimensionale Tensoren umgewandelt werden und die Kreuzentropie-Verlustfunktion verwendet wird.
  5. loss.backward(): Berechnet die Gradienten des Verlusts in Bezug auf die Parameter des Modells.
  6. optimizer.step(): Aktualisiert die Parameter des Modells anhand der berechneten Gradienten.
  7. print(f"Epoch: {epoch+1}, Loss: {loss.item()}"): Gibt die aktuelle Epochennummer und den Verlustwert für diese Epoche aus.

Zusammenfassung:

Dieses Codeschnipsel trainiert das Transformationsmodell auf zufällig generierten Quell- und Zielsequenzen für 100 Epochen. Sie verwendet den Adam-Optimierer und die Cross-Entropy-Verlustfunktion. Der Verlust wird für jede Epoche ausgedruckt, so dass du den Trainingsfortschritt verfolgen kannst. In einem realen Szenario würdest du die zufälligen Quell- und Zielsequenzen durch tatsächliche Daten aus deiner Aufgabe, wie z.B. der maschinellen Übersetzung, ersetzen.

Bewertung der Leistung von Transformatorenmodellen

Nachdem das Modell trainiert wurde, kann seine Leistung anhand eines Validierungs- oder Testdatensatzes bewertet werden. Im Folgenden findest du ein Beispiel dafür, wie dies geschehen könnte:

transformer.eval()

# Generate random sample validation data
val_src_data = torch.randint(1, src_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)
val_tgt_data = torch.randint(1, tgt_vocab_size, (64, max_seq_length))  # (batch_size, seq_length)

with torch.no_grad():

    val_output = transformer(val_src_data, val_tgt_data[:, :-1])
    val_loss = criterion(val_output.contiguous().view(-1, tgt_vocab_size), val_tgt_data[:, 1:].contiguous().view(-1))
    print(f"Validation Loss: {val_loss.item()}")

Bewertungsmodus:

  1. transformer.eval(): Schaltet das Transformatormodell in den Bewertungsmodus. Das ist wichtig, denn dadurch werden bestimmte Verhaltensweisen wie z.B. das Abbrechen ausgeschaltet, die nur im Training verwendet werden.

Erstelle zufällige Validierungsdaten:

  1. val_src_data: Zufällige ganze Zahlen zwischen 1 und src_vocab_size, die einen Stapel von Validierungssequenzen mit der Form (64, max_seq_length) darstellen.
  2. val_tgt_data: Zufällige ganze Zahlen zwischen 1 und tgt_vocab_size, die einen Stapel von Validierungszielsequenzen mit Form (64, max_seq_length) darstellen.

Validierungsschleife:

  1. with torch.no_grad(): Deaktiviert die Gradientenberechnung, da wir während der Validierung keine Gradienten berechnen müssen. Das kann den Speicherverbrauch reduzieren und die Berechnungen beschleunigen.
  2. val_output = transformer(val_src_data, val_tgt_data[:, :-1]): Leitet die Validierungsquelldaten und die Validierungszieldaten (mit Ausnahme des letzten Tokens in jeder Sequenz) durch den Transformator.
  3. val_loss = criterion(...): Berechnet den Verlust zwischen den Vorhersagen des Modells und den Validierungs-Zieldaten (mit Ausnahme des ersten Tokens in jeder Sequenz). Der Verlust wird berechnet, indem die Daten in eindimensionale Tensoren umgewandelt werden und die zuvor definierte Kreuzentropie-Verlustfunktion verwendet wird.
  4. print(f"Validation Loss: {val_loss.item()}"): Druckt den Wert des Validierungsverlustes aus.

Zusammenfassung:

Dieses Codeschnipsel evaluiert das Transformatormodell auf einem zufällig generierten Validierungsdatensatz, berechnet den Validierungsverlust und gibt ihn aus. In einem realen Szenario sollten die zufälligen Validierungsdaten durch tatsächliche Validierungsdaten aus der Aufgabe, an der du arbeitest, ersetzt werden. Der Validierungsverlust kann dir einen Hinweis darauf geben, wie gut dein Modell bei ungesehenen Daten abschneidet, was ein wichtiges Maß für die Generalisierungsfähigkeit des Modells ist.

Weitere Details zu Transformers und Hugging Face findest du in unserem Tutorial, An Introduction to Using Transformers and Hugging Face.

Fazit und weitere Ressourcen

Abschließend wurde in diesem Tutorial gezeigt, wie man mit PyTorch, einem der vielseitigsten Tools für Deep Learning, ein Transformer-Modell konstruiert. Mit ihrer Fähigkeit zur Parallelisierung und der Möglichkeit, langfristige Abhängigkeiten in Daten zu erfassen, haben Transformers ein immenses Potenzial in verschiedenen Bereichen, insbesondere bei NLP-Aufgaben wie Übersetzung, Zusammenfassung und Sentimentanalyse.

Wenn du dein Wissen über fortgeschrittene Deep Learning-Konzepte und -Techniken vertiefen möchtest, solltest du den Kurs Advanced Deep Learning with Keras auf DataCamp besuchen. Du kannst auch in einem anderen Tutorial nachlesen, wie du ein einfaches neuronales Netzwerk mit PyTorch aufbaust.

Verdiene eine Top-KI-Zertifizierung

Zeige, dass du KI effektiv und verantwortungsbewusst einsetzen kannst.
Themen
Verwandt

Der Blog

Top 30 Generative KI Interview Fragen und Antworten für 2024

Dieser Blog bietet eine umfassende Sammlung von Fragen und Antworten zu generativen KI-Interviews, die von grundlegenden Konzepten bis hin zu fortgeschrittenen Themen reichen.
Hesam Sheikh Hassani's photo

Hesam Sheikh Hassani

15 Min.

Der Blog

Die 32 besten AWS-Interview-Fragen und Antworten für 2024

Ein kompletter Leitfaden zur Erkundung der grundlegenden, mittleren und fortgeschrittenen AWS-Interview-Fragen, zusammen mit Fragen, die auf realen Situationen basieren. Es deckt alle Bereiche ab und sorgt so für eine abgerundete Vorbereitungsstrategie.
Zoumana Keita 's photo

Zoumana Keita

30 Min.

Der Blog

Die 20 besten Snowflake-Interview-Fragen für alle Niveaus

Bist du gerade auf der Suche nach einem Job, der Snowflake nutzt? Bereite dich mit diesen 20 besten Snowflake-Interview-Fragen vor, damit du den Job bekommst!
Nisha Arya Ahmed's photo

Nisha Arya Ahmed

20 Min.

See MoreSee More