Transformer im Detail

Im Jahr 2017 veröffentlichte Google das Papier „Attention Is All You Need“ und führte damit die Transformer-Architektur ein. Niemand hatte erwartet, dass dieses Papier den KI-Bereich komplett verändern würde.

Die heutigen GPT-, Claude-, Gemini-, Llama-... fast alle Mainstream-Large-Language-Modelle sind im Wesentlichen Varianten des Transformers.

Verstehen Sie den Transformer, und Sie verstehen 90 % der modernen LLMs.

In diesem Artikel tauchen wir tief in jede Komponente des Transformers ein: Self-Attention, Multi-Head-Attention, positionelle Enkodierung, Feed-Forward-Netzwerke, Layer-Normalisierung... und decken dabei nicht nur das „Was“, sondern auch das „Warum“ ab.

Dies ist eines der technisch tiefgreifendsten Module. Wir verwenden Code, um die Kernberechnungen zu demonstrieren, um sicherzustellen, dass Sie die Formeln nicht nur verstehen, sondern sie auch selbst implementieren können.


Transformer-Prequel: Grenzen von RNN

Vor dem Transformer wurden Sequenzaufgaben (wie Übersetzung und Texterstellung) hauptsächlich von RNNs (Recurrent Neural Networks) und deren Varianten LSTM und GRU bearbeitet.

Wie RNN funktioniert

Die Kernidee von RNN ist das „Schritt-für-Schritt-Verarbeiten“: Die Eingabesequenz gelangt Wort für Wort in das Netzwerk, und die Ausgabe bei jedem Schritt enthält Informationen von allen vorherigen Wörtern.

Beispiel

# ============================================
# Vereinfachte Demonstration der RNN-Vorwärtsausbreitung
# ============================================

import math


class SimpleRNN:
    """Vereinfachte RNN-Implementierung zur Demonstration des Prinzips"""

    def __init__(self, input_size: int, hidden_size: int):
        """RNN-Parameter initialisieren"""
        import random
        random.seed(42)  # Zufallsstartwert für Reproduzierbarkeit festlegen

        # Gewichte von der Eingabe zur verdeckten Schicht
        self.Wx = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(input_size)]
        # Gewichte von der verdeckten Schicht zur verdeckten Schicht
        self.Wh = [[random.uniform(-0.1, 0.1) for _ in range(hidden_size)]
                   for _ in range(hidden_size)]
        # Bias
        self.b = [0.0 for _ in range(hidden_size)]

    def step(self, x: list, h_prev: list) -> Liste:
        """Einzelschritt-RNN: nimmt Eingabe x und vorherigen verborgenen Zustand h_prev, gibt neuen verborgenen Zustand aus"""
        hidden_size = len(h_prev)
        h_new = [0.0 for _ in range(hidden_size)]

        # h_new = tanh(Wx·x + Wh·h_prev + b)
        for i in range(hidden_size):
            # Wx·x berechnen
            wx_sum = sum(x[j] * self.Wx[j][i] for j in range(len(x)))
            # Wh·h_prev berechnen
            wh_sum = sum(h_prev[j] * self.Wh[j][i] for j in range(hidden_size))
            # Bias hinzufügen, durch tanh leiten
            h_new[i] = math.tanh(wx_sum + wh_sum + self.b[i])

        return h_new

    def forward(self, sequence: list) -> list:
        """Die gesamte Sequenz verarbeiten"""
        hidden_size = len(self.b)
        h = [0.0 for _ in range(hidden_size)]  # Anfänglicher verborgener Zustand
        hidden_states = []

        for x in sequence:
            h = self.step(x, h)
            hidden_states.append(h)

        return hidden_states


# Test: mit einer einfachen Vektorsequenz demonstrieren
input_size = 4
hidden_size = 3

rnn = SimpleRNN(input_size, hidden_size)

# Angenommen, die Eingabesequenz besteht aus 4 Wörtern, die jeweils durch einen 4-dimensionalen Vektor dargestellt werden
sequence = [
    [1.0, 0.0, 0.0, 0.0],  # Wort 1
    [0.0, 1.0, 0.0, 0.0],  # Wort 2
    [0.0, 0.0, 1.0, 0.0],  # Wort 3
    [0.0, 0.0, 0.0, 1.0],  # Wort 4
]

hidden_states = rnn.forward(sequence)

print("Example Simple RNN Demo")
print("=" * 40)
for i, h in enumerate(hidden_states):
    print(f"Hidden state at step {i+1}: {[f'{v:.4f}' for v in h]}")
# Output:
# Beispiel für eine einfache RNN-Demo
# ========================================
# Verborgener Zustand bei Schritt 1: ['0.0204', '-0.0434', '0.0556']
# Verborgener Zustand bei Schritt 2: ['-0.0061', '-0.0529', '0.0775']
# Verborgener Zustand bei Schritt 3: ['0.0641', '-0.0409', '0.0655']
# Verborgener Zustand bei Schritt 4: ['0.0143', '-0.0805', '0.0277']

RNN scheint vernünftig, hat aber drei schwerwiegende Probleme:

Problem 1: Verschwindende Gradienten, Schwierigkeiten beim Erinnern an langfristige Abhängigkeiten

RNN ist eine „Ketten“-Struktur; der Gradient bei jedem Schritt muss zum ersten Schritt zurückpropagiert werden. Nach wiederholten Multiplikationen klingt der Gradient exponentiell ab und wird fast 0.

Zum Beispiel im Satz „I went to Paris in 2010, ... that was my favorite city“ – „city“ muss sich auf „Paris“ beziehen, aber es liegen zu viele Wörter dazwischen, sodass RNNs Schwierigkeiten haben, solche langfristigen Abhängigkeiten zu lernen.

LSTM und GRU mildern dieses Problem ab, lösen es aber nicht vollständig.

Problem 2: Kann nicht parallel berechnet werden, langsames Training

Ein RNN muss warten, bis Schritt t-1 beendet ist, bevor es Schritt t berechnen kann. Das bedeutet:

  • Selbst mit 100 GPUs können Sie immer nur ein Wort gleichzeitig berechnen
  • Je länger die Sequenz, desto länger die Trainingszeit
  • Schwer auf sehr große Datensätze zu skalieren

Problem 3: Frühere Informationen werden leicht „überschrieben“

Der verborgene Zustand eines RNN wird Schritt für Schritt aktualisiert, und spätere Informationen überschreiben immer wieder frühere Informationen. Wichtige Informationen am Anfang eines Satzes können am Ende weitgehend „verwässert“ sein.

Das Aufkommen des Transformers hat alle drei Probleme auf einmal gelöst.

MerkmalRNN/LSTMTransformer
BerechnungsmethodeSeriell, Schritt für SchrittParallel, alles auf einmal berechnen
Langreichweitige AbhängigkeitenSchwach, verschwindender GradientStark, direkte Verbindung in beliebiger Entfernung
Positionelle InformationenVon Natur aus sequenziellErfordert Positionskodierung
TrainingsgeschwindigkeitLangsamSchnell (parallelisierbar)

Self-Attention-Mechanismus

Self-Attention ist das Herzstück des Transformers. Seine Idee ist einfach:Jedes Wort muss mit allen Wörtern im Satz „kommunizieren“, sehen, wer für es selbst wichtig ist, und dann eine gewichtete Summe nach Wichtigkeit bilden.

Zuerst das Gesamtarchitekturdiagramm ansehen:

Intuitives Verständnis von Q, K, V

Self-Attention verwendet drei Vektoren, um jedes Wort zu beschreiben:

  • Q (Query)——„Wen suche ich?“—— auf welches Wort sich dieses Wort beziehen möchte
  • K (Key)——„Wer bin ich?“—— die Identität dieses Wortes
  • V (Value)——„Was habe ich?“—— der eigentliche Inhalt dieses Wortes

Der Berechnungsprozess ist:

  1. Jedes Wort verwendet sein Q, um mit dem K aller Wörter zu „matchen“, wodurch Aufmerksamkeitswerte (Attention Scores) erhalten werden
  2. Verwenden Sie Softmax, um die Werte zu normalisieren, sodass sie sich zu 1 summieren
  3. Verwenden Sie die normalisierten Scores, um das V aller Wörter zu gewichten und zu summieren

Das Diagramm verdeutlicht dies:

自注意力 QKV 计算流程图

Formel zur Berechnung des Attention-Scores

Die vollständige Formel lautet:

Attention(Q, K, V) = softmax( Q·Kᵀ / √dₖ ) · V

Implementieren wir es Schritt für Schritt in reinem Python:

Beispiel

# ============================================
# Reine Python-Implementierung von Self-Attention
# ============================================

import math


def softmax(x: list) -> list:
    """Softmax berechnen: eine Reihe von Zahlen in eine Wahrscheinlichkeitsverteilung umwandeln, die sich zu 1 summiert"""
    # Subtrahiere den Maximalwert, um numerischen Überlauf zu vermeiden
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """Matrizenmultiplikation: A ist m×n, B ist n×p, Ausgabe ist m×p"""
    m = len(A)
    n = len(B)
    p = len(B[0])

    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """Matrix-Transposition"""
    return list(map(list, zip(*matrix)))


class SelfAttention:
    """Reine Python-Implementierung der Self-Attention-Schicht"""

    def __init__(self, d_model: int, d_k: int):
        """
Self-Attention-Schicht initialisieren
d_model: Eingabe-/Ausgabedimension
d_k: Dimension von Q/K (normalerweise d_k = d_model / num_heads)
        """

        import random
        random.seed(42)  # Beispiel: fester Zufallsstartwert

        self.d_model = d_model
        self.d_k = d_k

        # Initialisieren Sie die Projektionsmatrizen für Q, K, V
        # In der Praxis werden diese trainiert
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_k)]
                   for _ in range(d_model)]

    def forward(self, X: list) -> list:
        """
Self-Attention Forward-Propagation
X: Eingabesequenz, Form [seq_len, d_model]
Gibt zurück: Ausgabequenz, Form [seq_len, d_k]
        """

        seq_len = len(X)

        # Schritt 1: Q, K, V berechnen
        # Q = X·Wq, K = X·Wk, V = X·Wv
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        print(" Form von Q:", len(Q), "×", len(Q[0]))
        print(" Form von K:", len(K), "×", len(K[0]))
        print(" Form von V:", len(V), "×", len(V[0]))

        # Schritt 2: Attention-Scores Q·Kᵀ berechnen
        K_transposed = transpose(K)
        scores = matrix_multiply(Q, K_transposed)

        print("\nRoh-Attention-Scores:")
        for i, row in enumerate(scores):
            print(fZeile {i}: {[f'{v:.4f}' for v in row]})

        # Schritt 3: Skalierung: Durch √d_k teilen
        # Dies verhindert, dass Softmax in den Gradientensättigungsbereich gerät.
        scale_factor = math.sqrt(self.d_k)
        scores_scaled = [[v / scale_factor for v in row] for row in scores]

        print("\nSkalierte Attention-Scores:)
        for i, row in enumerate(scores_scaled):
            print(fZeile {i}: {[f'{v:.4f}' for v in row]})

        # Schritt 4: Softmax-Normalisierung
        attention_weights = []
        for row in scores_scaled:
            attention_weights.append(softmax(row))

        print("\nAttention-Gewichte (nach Softmax): ")
        for i, row in enumerate(attention_weights):
            print(fZeile {i}: {[f'{v:.4f}' for v in row]})

        # Schritt 5: Gewichtere Summe V
        output = matrix_multiply(attention_weights, V)

        return output, attention_weights


# ============================================
# Demo: Self-Attention mit einem einfachen Beispiel ausführen
# ============================================

# Angenommen, wir haben einen Satz mit 4 Wörtern, wobei jedes Wort durch einen 8-dimensionalen Vektor dargestellt wird
# Zu Demonstrationszwecken verwenden wir One-Hot + kleines zufälliges Rauschen
d_model = 8
d_k = 6

# Eingabesequenz: 4 Wörter, jeweils 8-dimensional
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Wort 0
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Wort 1
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],  # Wort 2
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0],  # Wort 3
]

print("=" * 50)
print("Beispiel für Self-Attention-Demo")
print("=" * 50)
print(f"Länge der Eingabesequenz: {len(X)}")
print(f"Eingabedimension: {len(X)
print()

attn = SelfAttention(d_model=d_model, d_k=d_k)
output, attention_weights = attn.forward(X)

print("\n" + "=" * 50)
print("Self-Attention-Ausgabe:")
print("=" * 50)
for i, row in enumerate(output):
    print(f"Ausgabe von Wort {i}: {[f'{v:.4f}' for v in row]}")

Führen Sie diesen Code aus und Sie sehen den vollständigen Berechnungsprozess: Q/K/V-Ergenerierung, Berechnung der Attention-Scores, Skalierung, Softmax und schließlich die gewichtete Ausgabe.

Warum durch √dₖ teilen?

Dies ist ein wichtiges Detail. Unter Annahme, dass jedes Element von Q und K eine Zufallsvariable mit Mittelwert 0 und Varianz 1 ist, beträgt die Varianz ihres Skalarprodukts dₖ.

Wenn dₖ groß ist (z. B. 512), werden die Werte des Skalarprodukts sehr groß, wodurch Softmax in den „Sättigungsbereich“ gerät – der Gradient ist nahe bei 0 und das Training kann nicht fortgesetzt werden.

Nach dem Teilen durch √dₖ kehrt die Varianz zu 1 zurück und der Softmax-Gradient funktioniert wieder einwandfrei.

Scaled Dot-Product Attention ist eine der Schlüsseltechniken für ein stabiles Transformer-Training.


Multi-Head Attention

Single-head self-attention ist mächtig, hat aber ein Problem: Jedes Wort kann nur einmal „aufmerksam sein“ (attend). Was ist, wenn wir möchten, dass ein Wort gleichzeitig auf mehrere verschiedene Aspekte achtet?

Zum Beispiel im Satz „In the zoo, the tiger is chasing the rabbit“:

  • „Tiger“ benötigt möglicherweise einen Kopf, der auf „zoo“ achtet (Ort).
  • Ein anderer Kopf achtet auf „rabbit“ (Objekt der Handlung).
  • Der dritte Kopf konzentriert sich auf „chasing“ (Handlung).

Multi-Head Attention teilt Q/K/V in mehrere „Köpfe“ (Heads) auf, von denen jeder ein anderes Aufmerksamkeitsmuster lernt, und führt sie schließlich zusammen.

Schritte zur Berechnung von Multi-Head Attention

  1. Teile Q/K/V entlang der letzten Dimension in h Teile auf (h ist die Anzahl der Köpfe).
  2. Jeder Kopf führt unabhängig Self-Attention aus.
  3. Konkateniere die Ausgaben aller Köpfe.
  4. Leite sie durch eine lineare Projektionsschicht, um die finale Ausgabe zu erhalten.

Die Formel lautet:

MultiHead(Q, K, V) = Concat(head₁, head₂, ..., headₕ) · Wₒ
  其中 headᵢ = Attention(Q·Wqᵢ, K·Wkᵢ, V·Wvᵢ)

Beispiel

# ============================================
# Reine Python-Implementierung von Multi-Head Attention
# ============================================

import math
import random
random.seed(42)  # Beispiel: fester Zufallsstartwert


def softmax(x: list) -> list:
    """Softmax berechnen"""
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matrix_multiply(A: list, B: list) -> list:
    """Matrixmultiplikation"""
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix: list) -> list:
    """Matrix-Transposition"""
    return list(map(list, zip(*matrix)))


class MultiHeadAttention:
    """Reine Python-Implementierung der Multi-Head-Attention-Schicht"""

    def __init__(self, d_model: int, num_heads: int):
        """
Multi-Head Attention initialisieren
d_model: Modelldimension (muss durch num_heads teilbar sein)
num_heads: Anzahl der Attention-Köpfe
        """

        assert d_model % num_heads == 0, "d_model muss durch num_heads teilbar sein"

        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # Dimension jedes Kopfes

        # Projektionsmatrizen initialisieren
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wo = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def split_heads(self, X: list) -> list:
        """
Die letzte Dimension in num_heads Köpfe aufteilen
Eingabeform: [seq_len, d_model]
Ausgabeform: [num_heads, seq_len, d_k]
        """

        seq_len = len(X)
        # Reshape: [seq_len, num_heads, d_k]
        reshaped = []
        for row in X:
            head_rows = []
            for h in range(self.num_heads):
                start = h * self.d_k
                end = start + self.d_k
                head_rows.append(row[start:end])
            reshaped.append(head_rows)
        # Transponieren: [num_heads, seq_len, d_k]
        result = []
        for h in range(self.num_heads):
            head_data = [reshaped[i][h] for i in range(seq_len)]
            result.append(head_data)
        return result

    def combine_heads(self, heads: list) -> list:
        """
Die Heads wieder zusammenfügen
Input shape: [num_heads, seq_len, d_k]
Output shape: [seq_len, d_model]
        """

        seq_len = len(heads[0])
        result = []
        for i in range(seq_len):
            combined = []
            for h in range(self.num_heads):
                combined.extend(heads[h][i])
            result.append(combined)
        return result

    def attention_single_head(self, Q: list, K: list, V: list) -> list:
        """Single-head self-attention"""
        # Q·K^T
        scores = matrix_multiply(Q, transpose(K))
        # Scaling
        scale = math.sqrt(self.d_k)
        scores_scaled = [[v / scale for v in row] for row in scores]
        # Softmax
        weights = [softmax(row) for row in scores_scaled]
        # Weighted sum of V
        output = matrix_multiply(weights, V)
        return output, weights

    def forward(self, X: list) -> list:
        """Multi-head attention forward pass"""
        seq_len = len(X)

        # Schritt 1: Q, K, V berechnen
        Q = matrix_multiply(X, self.Wq)
        K = matrix_multiply(X, self.Wk)
        V = matrix_multiply(X, self.Wv)

        # Step 2: Split into multiple heads
        Q_heads = self.split_heads(Q)
        K_heads = self.split_heads(K)
        V_heads = self.split_heads(V)

        # Step 3: Each head performs self-attention independently
        output_heads = []
        all_weights = []
        for h in range(self.num_heads):
            out, weights = self.attention_single_head(
                Q_heads[h], K_heads[h], V_heads[h]
            )
            output_heads.append(out)
            all_weights.append(weights)

        # Step 4: Concatenate all heads
        combined = self.combine_heads(output_heads)

        # Step 5: Final linear projection
        output = matrix_multiply(combined, self.Wo)

        return output, all_weights


# ============================================
# Demonstrate multi-head attention
# ============================================

d_model = 12
num_heads = 3  # 12 / 3 = 4 dimensions per head

# Input sequence: 4 tokens, each with 12 dimensions
X = [
    [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
]

print("=" * 50)
print("Example Multi-Head Attention Demo")
print("=" * 50)
print(f"d_model = {d_model}, num_heads = {num_heads}")
print(f"Dimension per head: d_k = {d_model // num_heads}")
print()

mha = MultiHeadAttention(d_model=d_model, num_heads=num_heads)
output, all_weights = mha.forward(X)

print("\n"Attention weights for each head:")
for h in range(num_heads):
    print(f"\n"Head {h}:")
    for i, row in enumerate(all_weights[h]):
        print(f" Position {i}: {[f'{v:.4f}' for v in row]}")

print("\n" + "=" * 50)
print("Multi-head attention final output shape:", len(output), "×", len(output[0]))
print("=" * 50)
for i, row in enumerate(output):
    print(f"Position {i}: {[f'{v:.4f}' for v in row[:6]]}...")

Die Stärke der Multi-Head-Attention liegt darin:Verschiedene Heads lernen automatisch unterschiedliche Arten von Aufmerksamkeitsmustern.

Zum Beispiel bei Übersetzungsaufgaben:

  • Einige Heads konzentrieren sich auf syntaktische Beziehungen (Subjekt-Verb-Objekt)
  • Einige Heads konzentrieren sich auf Koreferenz (auf welches Nomen sich ein Pronomen bezieht)
  • Einige Heads konzentrieren sich auf lokale benachbarte Wörter

Diese Muster werden nicht manuell entworfen; sie werden vom Modell während des Trainings automatisch gelernt.


Positionale Codierung

Self-Attention hat ein Problem: Es ist ein „Bag-of-Words“-Modell – die Wortreihenfolge ist ihm egal. „Ich liebe dich“ und „Du liebst mich“ können für Self-Attention gleich aussehen.

Aber die Reihenfolge ist für Sprache entscheidend. Wir müssen positionelle Informationen in das Modell „einbringen“.

Sinusförmige positionale Codierung

Der ursprüngliche Transformer verwendet eine sinusförmige positionale Codierung:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Wobei:

  • pos die Position ist (0, 1, 2, ...)
  • i ist der Dimensionsindex
  • d_model ist die Modelldimension

Dieses Design ist genial:

  • Jede Position hat eine eindeutige Kodierung
  • Relative Positionen können durch Linearkombinationen dargestellt werden (sin(a+b) = sin a cos b + cos a sin b)
  • Es kann auf längere Sequenzen extrapolieren als die während des Trainings gesehenen

Beispiel

# ============================================
# Sinusförmige Positionalkodierung implementieren
# ============================================

import math


def sinusoidal_positional_encoding(seq_len: int, d_model: int) -> Liste:
    """
Sinusförmige Positionalkodierung berechnen
Gibt zurück: Positionalkodierungsmatrix der Form [seq_len, d_model]
    """

    pe = [[0.0 for _ in range(d_model)] for _ in range(seq_len)]

    for pos in range(seq_len):
        for i in range(0, d_model, 2):
            # Sinus für gerade Dimensionen verwenden
            div_term = math.pow(10000, 2 * i / d_model)
            pe[pos][i] = math.sin(pos / div_term)

            # Cosinus für ungerade Dimensionen verwenden (falls die nächste Dimension existiert)
            if i + 1 < d_model:
                pe[pos][i + 1] = math.cos(pos / div_term)

    return pe


# Demo: Positionalkodierung generieren
seq_len = 10
d_model = 8

pe = sinusoidal_positional_encoding(seq_len, d_model)

print("=" * 50)
print("Beispiel für sinusoidale Positionscodierung")
print("=" * 50)
print(f"Sequenzlänge: {seq_len}, Dimension: {d_model}")
print()
for pos in range(seq_len):
    print(f"Position {pos:2d}: {[f'{v:.4f}' for v in pe[pos]]}")

# Untersuchung der Beziehung relativer Positionen
print("\n" + "=" * 50)
print("Lineare Beziehung relativer Positionen verifizieren")
print("=" * 50)
print("Codierungsdifferenz zwischen Position 0 und Position 1:")
diff = [pe[1][i] - pe[0][i] for i in range(d_model)]
print([f'{v:.4f}' for v in diff])

RoPE: Rotary Positional Encoding

Obwohl die sinusoidale Positionscodierung ein Klassiker ist, entstanden später bessere Lösungen. Die heutigen Mainstream-Modelle (wie LLaMA) verwendenRoPE (Rotary Positional Embedding)。

Die Idee von RoPE ist:Positionsinformationen in Query und Key hineinzurollen.

Konkret wird jedes Dimensionspaar von Q und K gedreht:

[ q₁, q₂, q₃, q₄, ... ]
  ↓ 旋转
[ q₁·cos mθ - q₂·sin mθ,
  q₁·sin mθ + q₂·cos mθ,
  q₃·cos mθ' - q₄·sin mθ',
  q₃·sin mθ' + q₄·cos mθ',
  ... ]

Vorteile von RoPE:

  • Enthält natürlich relative Positionsinformationen
  • Gute Extrapolation (funktioniert für Längen jenseits des Trainings)
  • Funktioniert normalerweise besser als sinusoidale Positionscodierung

ALiBi: Decaying Attention by Distance

Ein weiterer einfacher und effektiver Ansatz istALiBi (Attention with Linear Biases)。

Es fügt keine Positionscodierung hinzu, sondern direkt einen Bias zu den Attention-Scores: Je weiter entfernt, desto niedriger der Score.

Der Vorteil von ALiBi liegt in seiner Einfachheit, und es ist besonders gut für Längenextrapolation geeignet.

PositionscodierungsmethodenPrinzipVorteileRepräsentative Modelle
Sinusoidale Positionscodierungsin/cos-FunktionenExtrapolierbar, einfache BerechnungUrsprünglicher Transformer, BERT
Lernbare PositionscodierungDirektes Trainieren eines EmbeddingsStarke AusdruckskraftGPT-1/2
RoPEQ/K rotierenRelative Positionen, gute LeistungLLaMA, PaLM
ALiBiBias zu Attention-Scores hinzufügenEinfach, gut für lange SequenzenMPT, BLOOM

Feed-Forward Network (FFN) Schicht

In jeder Transformer-Schicht gibt es nach der Attention auch ein Feed-Forward-Netzwerk.

Struktur von FFN

Das FFN des ursprünglichen Transformers ist:

FFN(x) = max(0, x·W₁ + b₁) · W₂ + b₂

Das heißt: Lineare Schicht → ReLU-Aktivierung → Lineare Schicht.

Normalerweise ist die Zwischendimension größer als die Eingangsdimension (z. B. d_model=512, Zwischendimension=2048).

ReLU vs SwiGLU

Das Original verwendete ReLU, aber heute ist der StandardSwiGLU:

SwiGLU(x) = (x·W₁ ⊗ sigmoid(x·V₁)) · W₂

wobei ⊗ die elementweise Multiplikation ist.

SwiGLU schneidet bei vielen Aufgaben besser ab als ReLU; LLaMA, PaLM usw. verwenden es alle.

Warum ist FFN wichtig?

  • Attention ist das „Aggregieren von Informationen“, FFN ist das „Verarbeiten von Informationen“
  • FFN hat eine große Anzahl von Parametern und ist der Hauptort, an dem das Modell Wissen „speichert“
  • Ohne FFN würde die Ausdrucksfähigkeit von Transformer stark reduziert werden

Beispiel

# ============================================
# FFN implementieren: ReLU-Version und SwiGLU-Version
# ============================================

import math
import random
random.seed(42)  # Beispiel


def relu(x: float) -> float:
    """ReLU-Aktivierungsfunktion: max(0, x)"""
    return max(0.0, x)


def sigmoid(x: float) -> float:
    """Sigmoid-Aktivierungsfunktion"""
    if x < -50:  # Numerischen Überlauf verhindern
        return 0.0
    return 1.0 / (1.0 + math.exp(-x))


def linear(x: list, W: list, b: list) -> Liste:
    """Lineare Schicht: y = x·W + b"""
    output_size = len(b)
    output = [0.0 for _ in range(output_size)]

    for i in range(output_size):
        sum_wx = sum(x[j] * W[j][i] for j in range(len(x)))
        output[i] = sum_wx + b[i]

    return output


class FFNReLU:
    """ReLU-Version von FFN"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # Parameter zufällig initialisieren (in der Praxis durch Training erhalten)
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.b1 = [0.0 for _ in range(d_ff)]

        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]
        self.b2 = [0.0 for _ in range(d_model)]

    def forward(self, x: list) -> list:
        """Vorwärtspass (Forward Pass)"""
        # Erste Schicht: linear + ReLU
        hidden = linear(x, self.W1, self.b1)
        hidden = [relu(v) for v in hidden]

        # Zweite Schicht: linear
        output = linear(hidden, self.W2, self.b2)
        return output


class FFNSwiGLU:
    """SwiGLU-Version von FFN (eine modernere Wahl)"""

    def __init__(self, d_model: int, d_ff: int):
        self.d_model = d_model
        self.d_ff = d_ff

        # SwiGLU benötigt zwei lineare Schichten für das Gating sowie eine Ausgabeschicht
        self.W1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.V1 = [[random.normalvariate(0, 0.1) for _ in range(d_ff)]
                   for _ in range(d_model)]
        self.W2 = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_ff)]

    def forward(self, x: list) -> list:
        """Vorwärtspass: SwiGLU"""
        # Berechne zwei lineare Transformationen
        x1 = linear(x, self.W1, [0.0 for _ in range(self.d_ff)])
        x2 = linear(x, self.V1, [0.0 for _ in range(self.d_ff)])

        # SwiGLU: x1 * sigmoid(x2)
        hidden = [a * sigmoid(b) for a, b in zip(x1, x2)]

        # Ausgabeschicht
        output = linear(hidden, self.W2, [0.0 for _ in range(self.d_model)])
        return output


# FFN demonstrieren
d_model = 8
d_ff = 16

# Testeingabe
x = [0.1, -0.2, 0.3, -0.4, 0.5, -0.6, 0.7, -0.8]

print("=" * 50)
print("Beispiel FFN-Demo")
print("=" * 50)
print(f"Eingabe: {[f'{v:.4f}' for v in x]}")

ffn_relu = FFNReLU(d_model, d_ff)
output_relu = ffn_relu.forward(x)
print(f"\n"ReLU FFN-Ausgabe: {[f'{v:.4f}' for v in output_relu]}")

ffn_swiglu = FFNSwiGLU(d_model, d_ff)
output_swiglu = ffn_swiglu.forward(x)
print(f"SwiGLU FFN-Ausgabe: {[f'{v:.4f}' for v in output_swiglu]}")

Schicht-Normalisierung (Layer Norm)

Es gibt eine weitere wichtige Komponente im Transformer: die Schicht-Normalisierung.

Was die Schicht-Normalisierung tut

Die Schicht-Normalisierung normalisiert die Merkmale jedes Samples auf den Mittelwert 0 und die Varianz 1:

LN(x) = γ ⊗ ((x - μ) / √σ²) + β

wobei:

  • μ der Mittelwert von x ist, σ² die Varianz von x ist
  • γ (Skalierung) und β (Verschiebung) lernbare Parameter sind
  • ⊗ elementweise Multiplikation ist

Beispiel

# ============================================
# Schicht-Normalisierung implementieren
# ============================================

import math


def layer_norm(x: list, gamma: list, beta: list, eps: float = 1e-6) -> list:
    """
Layer-Normalisierung
x: Eingabevektor
gamma: Skalierungsparameter
beta: Offset-Parameter
eps: kleine Konstante zur Vermeidung einer Division durch Null
    """

    # Mittelwert berechnen
    mean = sum(x) / len(x)

    # Varianz berechnen
    var = sum((v - mean) ** 2 for v in x) / len(x)

    # Normalisierung + Skalierung und Verschiebung
    normalized = []
    for xi, gi, bi in zip(x, gamma, beta):
        normalized.append(gi * (xi - mean) / math.sqrt(var + eps) + bi)

    return normalized


# Schicht-Normalisierung demonstrieren
d_model = 6

# Eingabevektor
x = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]

# gamma auf 1 initialisiert, beta auf 0 initialisiert
gamma = [1.0 for _ in range(d_model)]
beta = [0.0 for _ in range(d_model)]

print("=" * 50)
print("Example Layer Normalization Demo")
print("=" * 50)
print(f"Eingabe: {[f'{v:.4f}' for v in x]}")

output = layer_norm(x, gamma, beta)
print(f"Output: {[f'{v:.4f}' for v in output]}")

# Verify mean and variance
mean_out = sum(output) / len(output)
var_out = sum((v - mean_out) ** 2 for v in output) / len(output)
print(f"\n"Output mean: {mean_out:.4f}")
print(f"Output variance: {var_out:.4f}")

Pre-Norm vs Post-Norm

Layer normalization has two placement methods:

  • Post-Norm: Sublayer → Residual → Normalization (ursprünglicher Transformer)
  • Pre-Norm: Normalization → Sublayer → Residual (heute Mainstream)

Pre-Norm-Training ist stabiler und fast alle großen Modelle verwenden es inzwischen.

RMS Norm

RMS NormDies ist eine vereinfachte Version der Layer-Normalisierung, die nur den quadratischen Mittelwert berechnet, ohne den Mittelwert zu subtrahieren:

RMS(x) = sqrt(mean(x²))
RMSNorm(x) = γ ⊗ (x / RMS(x))

RMS Norm ist schneller zu berechnen bei ähnlichen Ergebnissen; Modelle wie LLaMA verwenden es.


Encoder-Decoder- vs. Decoder-Only-Architektur

Der ursprüngliche Transformer ist eine Encoder-Decoder-Architektur, aber die meisten großen Sprachmodelle heute sind Decoder-Only.

Encoder-Decoder: Wird für Aufgaben wie Übersetzung verwendet

Encoder-Decoder-Architektur:

  • Encoder: Liest die Eingabesequenz (z. B. Quellsprachensatz) und generiert eine Kontextrepräsentation
  • Decoder: Generiert die Zielsequenz (z. B. Übersetzungsergebnis) basierend auf der Ausgabe des Encoders

Es gibt zwei Arten von Aufmerksamkeit im Decoder:

  1. Masked Self-Attention: Kann nur vorherige Wörter betrachten (kann die Zukunft nicht sehen)
  2. Cross-Attention: Um auf die Ausgabe des Encoders zu achten

Geeignete Aufgaben: Maschinelle Übersetzung, Spracherkennung, Zusammenfassung (erfordert das „Umschreiben“ der Eingabe in die Ausgabe).

Decoder-Only: GPT-Serie, wird für die Generierung verwendet

Die Decoder-Only-Architektur hat nur einen Decoder, keinen Encoder. Ihre Eigenschaften:

  • Nur maskierte Self-Attention (kausale Aufmerksamkeit)
  • Autoregressive Generierung: Gibt ein Wort nach dem anderen aus
  • Geeignet für das Pretraining- + Fine-Tuning-Paradigma

Aktuelle Modelle wie GPT, LLaMA, Claude, Gemini... sind fast alle Decoder-Only.

Encoder-Only: BERT-Serie, wird zum Verstehen verwendet

Die Encoder-Only-Architektur hat nur einen Encoder. Ihre Eigenschaften:

  • Bidirektionale Aufmerksamkeit: Jedes Wort kann alle Wörter sehen
  • Geeignet für Verstehensaufgaben: Klassifizierung, Named Entity Recognition, extractive Fragebeantwortung
  • Vorab trainiert mit „Masked Language Model“ (MLM)
ArchitekturKomponenteAttention-TypRepräsentatives ModellAnwendbare Aufgaben
Encoder-DecoderEncoder + DecoderBidirektional + maskiert + crossT5, BARTÜbersetzung, Zusammenfassung
Decoder-OnlyDecoderMaskiert (kausal)GPT, LLaMA, ClaudeTexterstellung
Encoder-OnlyEncoderBidirektionalBERT, RoBERTaKlassifizierung, NER

Warum wurde Decoder-Only zum Mainstream? Weil es einfach ist und unter groß angelegtem Vortraining seine Generierungsfähigkeit extrem stark ist. „One size fits all“ funktioniert tatsächlich besser als „spezialisiertes Design“.


Flash Attention: Berechnung von Attention schnell und speichereffizient machen

Die Zeit- und Speicherkomplexität der Standard-Self-Attention beträgt O(n²); wenn die Sequenz lang wird (z. B. n=8192), ist sie langsam und der GPU-Speicher reicht nicht aus.

Flash AttentionEs handelt sich um eine Schlüsseloptimierung; ihr Aufkommen im Jahr 2022 machte das Training mit langen Sequenzen praktikabel.

Speicherengpass der Standard-Attention

Standard-Self-Attention muss Zwischenergebnisse (Attention-Gewichtsmatrizen) für die Backpropagation speichern. Für n=8192 sind das 8192×8192 ≈ 67 Millionen Elemente, was allein Dutzende von GB an GPU-Speicher belegt.

Optimierungsprinzipien von Flash Attention

Kernidee von Flash Attention:

  1. Blockweise Berechnung: Q/K/V in kleine Blöcke aufteilen und Block für Block berechnen
  2. Online Softmax: Die vollständige Attention-Matrix nicht speichern; mathematische Tricks verwenden, um sie während der Backpropagation neu zu berechnen
  3. SRAM nutzen: Die Blöcke in den Hochgeschwindigkeits-Cache (SRAM) der GPU laden, um HBM-Zugriffe zu reduzieren

Das Ergebnis:

  • Die Speicherkomplexität sinkt von O(n²) auf O(n)
  • 2-4x schneller
  • Identische Präzision (mathematisch äquivalent)

Fast das gesamte Training großer Modelle verwendet heute Flash Attention oder dessen Varianten (Flash Attention v2, Memory-Efficient Attention usw.).


KV Cache: Prinzipien der Inferenzbeschleunigung

Sobald die Trainingsoptimierung abgeschlossen ist, muss auch die Inferenz (Generierung) optimiert werden.KV CacheEr ist der Schlüssel zur Inferenzbeschleunigung.

Problem: Wiederholte Berechnungen bei der autoregressiven Generierung

Während der autoregressiven Generierung müssen bei jeder Generierung eines neuen Tokens die K- und V-Werte aller vorherigen Token neu berechnet werden – das ist sehr verschwenderisch.

Wenn beispielsweise das 100. Token generiert wird, wurden die K/V-Werte der ersten 99 Token bereits berechnet und müssen nicht neu berechnet werden.

Wie der KV Cache funktioniert

Die Idee des KV Cache ist einfach:

  • Die K- und V-Werte aller vorherigen Positionen zwischenspeichern
  • Beim Generieren eines neuen Tokens nur das K/V des neuen Tokens berechnen und an den Cache anhängen
  • Das vollständige K/V für die Aufmerksamkeit verwenden

Auf diese Weise sinkt die Berechnung pro Schritt von O(n²) auf O(n), was die Generierungsgeschwindigkeit erheblich verbessert.

Beispiel

# ============================================
# Das Prinzip des KV Cache demonstrieren
# ============================================

import math
import random
random.seed(42)


def softmax(x):
    max_val = max(x)
    exp_x = [math.exp(v - max_val) for v in x]
    sum_exp = sum(exp_x)
    return [v / sum_exp for v in exp_x]


def matmul(A, B):
    m = len(A)
    n = len(B)
    p = len(B[0])
    result = [[0.0 for _ in range(p)] for _ in range(m)]
    for i in range(m):
        for j in range(p):
            for k in range(n):
                result[i][j] += A[i][k] * B[k][j]
    return result


def transpose(matrix):
    return list(map(list, zip(*matrix)))


class SimpleAttentionWithKVCache:
    """Einfache Self-Attention mit KV Cache"""

    def __init__(self, d_model: int):
        self.d_model = d_model
        self.Wq = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wk = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]
        self.Wv = [[random.normalvariate(0, 0.1) for _ in range(d_model)]
                   for _ in range(d_model)]

    def forward(self, x_new: list, kv_cache: tuple = None) -> tuple:
        """
Ein-Schritt-Generierung mit KV-Cache
x_new: aktuelle neue Eingabe (einzelne Position)
kv_cache: vorheriges K und V, (K_cache, V_cache)
Gibt zurück: (Ausgabe, neuer kv_cache)
        """

        # Berechne Q, K, V für die aktuelle Position
        q = matmul([x_new], self.Wq)
        k = matmul([x_new], self.Wk)
        v = matmul([x_new], self.Wv)

        # Aktualisiere den KV-Cache
        if kv_cache is None:
            K_full = k
            V_full = v
        else:
            K_cache, V_cache = kv_cache
            K_full = K_cache + k
            V_full = V_cache + v

        # Attention: Verwende nur das Q der aktuellen Position
        scores = matmul(q, transpose(K_full))
        scale = math.sqrt(self.d_model)
        scores_scaled = [[v / scale for v in row] for row in scores]
        weights = [softmax(row) for row in scores_scaled]
        output = matmul(weights, V_full)

        return output[0], (K_full, V_full)


# Demonstriere den KV-Cache-Generierungsprozess
d_model = 4

# Simuliere 4 Generierungsschritte
inputs = [
    [1.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 1.0],
]

attn = SimpleAttentionWithKVCache(d_model)

print("=" * 50)
print("Beispiel für eine KV-Cache-Demonstration")
print("=" * 50)

kv_cache = None
for step, x in enumerate(inputs):
    output, kv_cache = attn.forward(x, kv_cache)
    K_cache, V_cache = kv_cache
    print(f"\n"Schritt {step}:")
    print(f" KV-Cache-Größe: {len(K_cache)} Positionen")
    print(f" Ausgabe: {[f'{v:.4f}' for v in output]}")

print("\n" + "=" * 50)
print("Hinweis: Jeder Schritt berechnet nur das K/V der aktuellen Position,")
print("hängt es dann an den Cache an, sodass die vorherigen nicht neu berechnet werden müssen!")
print("=" * 50)

Der KV-Cache hat einen geringen Preis: Er benötigt zusätzlichen GPU-Speicher zum Speichern von K und V. Für ein 7B-Modell mit einem 4k-Kontext benötigt der KV-Cache etwa 1-2 GB GPU-Speicher – das ist es wert.

Es gibt auch fortgeschrittenere Optimierungen:

  • Multi-Query Attention: Mehrere Heads teilen sich K/V
  • Grouped Query Attention: Gruppierte gemeinsame Nutzung von K/V

Diese reduzieren den Speicherbedarf weiter und verbessern die Inferenzgeschwindigkeit.


Zusammenfassung: Komplette Transformer-Pipeline

Führen wir alle Komponenten zusammen und fassen den vollständigen Transformer-Ablauf zusammen.

Eine Decoder-Only-Transformer-Schicht

x → LayerNorm → Masked Multi-Head Attention → 残差连接 →
    → LayerNorm → FFN → 残差连接 → 输出

Vollständiges Decoder-Only-Modell

  1. Eingabe-Token-IDs → Wort-Embedding
  2. Positionelle Enkodierung hinzufügen
  3. Durchlaufen von N Transformer-Schichten
  4. Finale LayerNorm
  5. Lineare Schicht zur Abbildung auf die Vokabulargröße
  6. Softmax, um die Wahrscheinlichkeit des nächsten Tokens zu erhalten

Wichtige Hyperparameter

HyperparameterBedeutungGPT-2 (124M)LLaMA-7BGPT-3 (175B)
d_modelModell-Dimension768409612288
num_headsAnzahl der Attention-Heads123296
num_layersAnzahl der Schichten123296
d_ffFFN-Zwischendimension30721100849152
context_lenKontextlänge10242048/40962048

Code-Implementierungsleitfaden

Wenn Sie einen vereinfachten Transformer von Grund auf implementieren möchten, sind die Schritte:

  1. Layer-Normalisierung implementieren
  2. Self-Attention implementieren (mit Maskierung)
  3. Multi-Head-Attention implementieren
  4. FFN implementieren
  5. Transformer-Schicht implementieren
  6. Positionelle Enkodierung implementieren
  7. Vollständiges Modell zusammenbauen
  8. Trainingsloop implementieren

In der Praxis verwenden wir Frameworks wie PyTorch anstelle von reinem Python. Die reine Python-Implementierung hier dient dazu, die Prinzipien zu demonstrieren und Ihnen zu helfen, zu verstehen, was jeder Schritt tut.

Der beste Weg, den Transformer zu verstehen, besteht darin, ihn selbst zu implementieren. Selbst eine vereinfachte Version wird Ihnen eine neue Perspektive auf viele Details eröffnen.

Weitere Erweiterungen