PyTorch Aufmerksamkeitsmechanismus

Der Attention-Mechanismus ist eines der wichtigsten Konzepte im Deep Learning.

Der Attention-Mechanismus ermöglicht es Modellen zu lernen, sich auf die relevantesten Teile der Eingabe zu „konzentrieren“, und erzielt große Erfolge in Bereichen wie der Verarbeitung natürlicher Sprache und Computer Vision.

Dieser Abschnitt erläutert die Kernprinzipien des Attention-Mechanismus, PyTorch-Implementierungen und verschiedene Attention-Varianten.

Anwendbare Version:Der Code in diesem Artikel basiert auf PyTorch 2.0+.nn.MultiheadAttentionvonbatch_firstDer Parameter wurde in PyTorch 1.9 eingeführt; frühere Versionen erfordern eine manuelle Dimensionsbehandlung.


1. Grundlagen des Attention-Mechanismus

1.1 Warum Attention notwendig ist

Traditionelle Sequence-to-Sequence-(Seq2Seq)-Modelle haben ein grundlegendes Problem:Der Encoder muss alle Informationen in einen Vektor fester Länge komprimieren.Bei langen Sequenzen wird dieser Vektor zum Informationsengpass—je länger der Satz, desto schwerwiegender der Informationsverlust.

Die Kernidee des Attention-Mechanismus besteht darin, dem Decoder zu ermöglichen, beim Generieren jeder Ausgabe alle verborgenen Zustände des Encoders zu „sehen“ und abhängig vom aktuellen Kontext dynamisch unterschiedliche Attention-Gewichte zuzuweisen. Das ist wie ein menschlicher Übersetzer, der bei jedem übersetzten Wort auf den entsprechenden Teil des Ausgangstexts zurückblickt.

Seq2Seq: Fixed Vector vs. Attention Mechanism Fixed vector (information bottleneck) x₁ x₂ x₃ x₄ c (fixed vector) y₁ y₂ y₃ y₄ All decoding steps share the same c Attention mechanism (dynamic weighting) h₁ h₂ h₃ h₄ y₁ y₂ y₃ y₄ Each step generates a different context vector c_t Thicker line → larger attention weight → the more important the information at that position

1.2 Das Wesen des Attention-Mechanismus

Der Attention-Mechanismus kann als einegewichtete Summe-Operation betrachtet werden. Gegeben seien eine Abfrage (Query), ein Schlüssel (Key) und ein Wert (Value); Gewichte werden zugewiesen, indem die Ähnlichkeit zwischen der Query und jedem Key berechnet wird, und anschließend wird eine gewichtete Summe der Values berechnet:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]$$

wobei:

  • Q(Query): Query-Vektor, der angibt, „welche Information ich suche“
  • K(Key): Key-Vektor, der angibt, „welche Information ich hier habe“ (zum Abgleich verwendet)
  • V(Value): Value-Vektor, der repräsentiert, „was ich tatsächlich bereitstellen kann“
  • $$d_k$$: Dimension von Key, $$\sqrt{d_k}$$ wird zur Skalierung verwendet, um zu verhindern, dass Dot-Product-Werte zu groß werden, was verschwindende Softmax-Gradienten verursachen würde

Warum durch $$\sqrt{d_k}$$ dividieren? Wenn $$d_k$$ groß ist, wächst die Varianz des Skalarprodukts von Q und K linear mit der Dimension, wodurch die Softmax-Eingabewerte zu groß werden und die Gradienten gegen null gehen. Nach der Skalierung wird die Varianz auf 1 zurückgesetzt, was einen normalen Gradientenfluss gewährleistet.

Scaled Dot-Product Attention computation process Q [batch, seq_q, d_k] K [batch, seq_k, d_k] MatMul QKᵀ Scale ÷ √d_k Softmax Normalization V [batch, seq_k, d_v] MatMul × V Output Mask (optional) -∞ padding Output = softmax(QKᵀ / √d_k) · V Output shape: [batch, seq_q, d_v] — each Query position gets a d_v-dimensional context vector

Beispiel

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
Skalierte Dot-Product-Attention

Parameter:
Q: Query-Tensor [batch, n_heads, seq_len_q, d_k]
K: Key-Tensor [batch, n_heads, seq_len_k, d_k]
V: Wertetensor [batch, n_heads, seq_len_v, d_v]
mask: Maskentensor; maskierte Positionen werden auf False/0 gesetzt

Rückgabe:
output: Attention-Ausgabe [batch, n_heads, seq_len_q, d_v]
attention_weights: Attention-Gewichte [batch, n_heads, seq_len_q, seq_len_k]
    """

    d_k = Q.size(-1)

    # 1. Berechne QK^T / sqrt(d_k)
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

    # 2. Maske anwenden (optional)
    # Setze maskierte Positionen auf einen sehr kleinen Wert; nach Softmax nähern sie sich 0
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)

    # 3. Softmax-Normalisierung → Attention-Gewichte
    attention_weights = F.softmax(scores, dim=-1)

    # 4. Gewichtete Summe
    output = torch.matmul(attention_weights, V)

    return output, attention_weights


# Test
batch_size, n_heads = 2, 4
seq_len_q, seq_len_k = 5, 6
d_k, d_v = 8, 16

Q = torch.randn(batch_size, n_heads, seq_len_q, d_k)
K = torch.randn(batch_size, n_heads, seq_len_k, d_k)
V = torch.randn(batch_size, n_heads, seq_len_k, d_v)

output, attn_weights = scaled_dot_product_attention(Q, K, V)

print(f"Ausgabeform: {output.shape}")            # [2, 4, 5, 16]
print(f"Form der Aufmerksamkeitsgewichte: {attn_weights.shape}")  # [2, 4, 5, 6]
print(f"Zeilensumme der Gewichte (sollte 1.0 sein): {attn_weights[0, 0, 0].sum().item():.4f}")

Das Wesen der Aufmerksamkeitsgewichte ist eine Wahrscheinlichkeitsverteilung – nach Softmax summiert sich jede Zeile zu 1. Je größer das Gewicht, desto stärker "fokussiert" sich das Modell auf diese Position.


2. PyTorch-Attention-Modul

2.1 Multi-Head-Attention

Multi-Head-AttentionSie ermöglicht es dem Modell, gleichzeitig Informationen aus verschiedenen Repräsentationsunterräumen an unterschiedlichen Positionen zu beachten. Sie projiziert Q, K und V durch unterschiedliche lineare Projektionen in mehrere Unterräume, berechnet Aufmerksamkeit unabhängig in jedem Unterraum und verkettet schließlich die Ergebnisse und wendet eine weitere lineare Transformation an.

Das ist, als würden mehrere Personen denselben Text gleichzeitig aus unterschiedlichen Perspektiven untersuchen – einige konzentrieren sich auf die grammatikalische Struktur, einige auf semantische Beziehungen und einige auf weitreichende Abhängigkeiten. Der Multi-Head-Mechanismus ermöglicht es dem Modell, reichhaltigere Muster zu erfassen.

$$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)W^O$$

$$\text{wobei head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$$

Beispiel

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()
        assert d_model % n_heads == 0, "d_model muss durch n_heads teilbar sein"

        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads  # Dimension jedes Heads

        # Lineare Projektionen für Q, K, V (alle Heads auf einmal berechnen, effizienter)
        self.w_q = nn.Linear(d_model, d_model)
        self.w_k = nn.Linear(d_model, d_model)
        self.w_v = nn.Linear(d_model, d_model)

        # Ausgabeprojektion
        self.w_o = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, query, key, value, mask=None):
        """
Parameter:
            query: [batch, seq_len_q, d_model]
            key: [batch, seq_len_k, d_model]
            value: [batch, seq_len_k, d_model]
mask: [batch, 1, 1, seq_len_k] oder [batch, 1, seq_len_q, seq_len_k]
        """

        batch_size = query.size(0)

        # 1. Lineare Projektion, dann Aufteilung in Köpfe
        #    [batch, seq_len, d_model] → [batch, seq_len, n_heads, d_k]
        #    → [batch, n_heads, seq_len, d_k]
        Q = self.w_q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        K = self.w_k(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
        V = self.w_v(value).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

        # 2. Skalierte Skalarprodukt-Attention
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attn_weights = F.softmax(scores, dim=-1)
        attn_weights = self.dropout(attn_weights)

        # 3. Gewichtete Summe
        context = torch.matmul(attn_weights, V)

        # 4. Köpfe zusammenführen: [batch, n_heads, seq_len, d_k] → [batch, seq_len, d_model]
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)

        # 5. Ausgabeprojektion
        output = self.w_o(context)

        return output, attn_weights


# Test
d_model, n_heads = 128, 8
seq_len, batch = 10, 4

layer = MultiHeadAttention(d_model, n_heads)

query = torch.randn(batch, seq_len, d_model)
key = torch.randn(batch, seq_len, d_model)
value = torch.randn(batch, seq_len, d_model)

output, attn_weights = layer(query, key, value)

print(f"Ausgabeform: {output.shape}")           # [4, 10, 128]
print(f"Form der Aufmerksamkeitsgewichte: {attn_weights.shape}") # [4, 8, 10, 10]
print(f"Dimension jedes Heads d_k = {d_model // n_heads}")

2.2 In PyTorch integrierte MultiheadAttention

PyTorch bietet eine hochoptimiertenn.MultiheadAttention, die fusionierte Kernel auf der darunterliegenden Ebene verwendet und in den meisten Szenarien schneller als manuelle Implementierungen ist. Sie wird für den Einsatz in Produktionsumgebungen empfohlen.

Beispiel

import torch
import torch.nn as nn

class TransformerAttention(nn.Module):
    """Self-Attention-Schicht unter Verwendung von PyTorchs integrierter MultiheadAttention"""
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(
            embed_dim=d_model,
            num_heads=n_heads,
            dropout=dropout,
            batch_first=True,  # Eingabeformat ist [batch, seq, features]
            # PyTorch 2.0+ kann das Flash-Attention-Backend aktivieren:
            # attn_implementation="flash_attention_2" # erfordert die Installation von flash-attn
        )
        self.layernorm = nn.LayerNorm(d_model)

    def forward(self, x, key_padding_mask=None):
        """
Selbstaufmerksamkeit: Q, K, V sind alle x
Parameter:
            x: [batch, seq_len, d_model]
key_padding_mask: [batch, seq_len], True zeigt an, dass diese Position Padding ist
        """

        attn_output, attn_weights = self.attention(
            x, x, x,  # Selbstaufmerksamkeit
            key_padding_mask=key_padding_mask
        )

        # Pre-Norm-Residualverbindung (stabileres Training als Post-Norm)
        output = self.layernorm(x + attn_output)

        return output, attn_weights


# Test
d_model, n_heads = 128, 8
seq_len, batch = 10, 4

model = TransformerAttention(d_model, n_heads)
x = torch.randn(batch, seq_len, d_model)

# Padding-Maske erstellen: True gibt an, dass die Position ignoriert werden soll
key_padding_mask = torch.zeros(batch, seq_len, dtype=torch.bool)
key_padding_mask[0, 7:] = True   # Die letzten 3 Positionen des ersten Samples sind Padding
key_padding_mask[1, 5:] = True   # Die letzten 5 Positionen des zweiten Samples sind Padding

output, attn_weights = model(x, key_padding_mask=key_padding_mask)

print(f"Ausgabeform: {output.shape}")           # [4, 10, 128]
print(f"Form der Attention-Gewichte: {attn_weights.shape}") # [4, 10, 10]

Beachten Sie die Maskentypen:nn.MultiheadAttentionvonkey_padding_maskDie KonventionTrue = ignorewird verwendet, entgegengesetzt zur Konvention in einigen benutzerdefinierten Implementierungen, bei denen 0 = ignore gilt. Vergewissern Sie sich bei der Verwendung.


3. Varianten des Attention-Mechanismus

3.1 Self-Attention

Self-Attentionist eine spezielle Form des Aufmerksamkeitsmechanismus—Q, K, V stammen alle aus derselben Eingabesequenz. Sie ermöglicht es jeder Position in der Sequenz, direkt auf alle anderen Positionen zu achten und erfasst dadurch globale Abhängigkeiten. Dies ist die Kernkomponente des Transformers und der Schlüssel zu seinem Vorteil gegenüber RNNs: RNNs müssen Informationen Schritt für Schritt weitergeben, während Self-Attention dies in einem Schritt erledigt.

Beispiel

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class SelfAttention(nn.Module):
    """Multi-Head-Self-Attention-Schicht, unterstützt kausale Maske (für autoregressive Generierung)"""
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        self.w_q = nn.Linear(d_model, d_model)
        self.w_k = nn.Linear(d_model, d_model)
        self.w_v = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def _split_heads(self, x, batch_size):
        """[batch, seq, d_model] → [batch, n_heads, seq, d_k]"""
        return x.view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)

    def forward(self, x, causal=False):
        """
Self-Attention: Q, K, V stammen alle aus derselben Eingabe x
Parameter:
            x: [batch, seq_len, d_model]
causal: ob eine kausale Maske verwendet werden soll (verhindert das Sehen zukünftiger Positionen)
        """

        batch_size, seq_len, _ = x.size()

        # Projizieren + in Köpfe aufteilen
        Q = self._split_heads(self.w_q(x), batch_size)
        K = self._split_heads(self.w_k(x), batch_size)
        V = self._split_heads(self.w_v(x), batch_size)

        # Attention-Scores berechnen
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

        # Kausale Maske: Setze die obere Dreiecksmatrix auf -inf, um zu verhindern, zukünftige Tokens zu sehen
        if causal:
            causal_mask = torch.triu(
                torch.ones(seq_len, seq_len, device=x.device, dtype=torch.bool),
                diagonal=1
            )
            scores = scores.masked_fill(causal_mask.unsqueeze(0).unsqueeze(0), float('-inf'))

        attn_weights = F.softmax(scores, dim=-1)
        attn_weights = self.dropout(attn_weights)

        # Gewichtete Summe + Köpfe zusammenführen
        context = torch.matmul(attn_weights, V)
        context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)

        return self.out_proj(context), attn_weights


# Test
d_model, n_heads = 256, 8
seq_len, batch = 20, 2

attn = SelfAttention(d_model, n_heads)
x = torch.randn(batch, seq_len, d_model)

# Ohne kausale Maske (Encoder-Szenario)
output_enc, weights_enc = attn(x, causal=False)
print(f"[Encoder] Ausgabe: {output_enc.shape}, Gewichte: {weights_enc.shape}")

# Mit kausaler Maske (Decoder / autoregressives Generierungsszenario)
output_dec, weights_dec = attn(x, causal=True)
print(f"[Decoder] Ausgabe: {output_dec.shape}, Gewichte: {weights_dec.shape}")

# Kausalität überprüfen: Das erste Token sollte nicht auf spätere Token achten
print(f"causal=True, weights[0,0,0,5:] (sollte alles 0 sein): {weights_dec[0, 0, 0, 5:].tolist()[:5]}")

3.2 Cross-Attention

Bei Cross-Attention, Q kommt aus einer Sequenz (z. B. Decoder), während K und V aus einer anderen Sequenz (z. B. Encoder) kommen. Dies ist die Brücke der Encoder-Decoder-Architektur – sie ermöglicht es dem Decoder, bei der Generierung jedes Tokens dynamisch alle Encoder-Ausgaben "nachzuschlagen".

Beispiel

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class CrossAttention(nn.Module):
    """Cross-Attention: Q kommt aus der Zielsequenz, K/V kommen aus der Quellsequenz"""
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        self.w_q = nn.Linear(d_model, d_model)
        self.w_k = nn.Linear(d_model, d_model)
        self.w_v = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, target, source, mask=Keine):
        """
Parameter:
target: Zielsequenz [batch, target_len, d_model] → liefert Q
source: Quellsequenz [batch, source_len, d_model] → liefert K, V
mask: optionale Maske
        """

        batch_size = target.size(0)
        target_len = target.size(1)
        source_len = source.size(1)

        # Projizieren + in Köpfe aufteilen
        Q = self.w_q(target).view(batch_size, target_len, self.n_heads, self.d_k).transpose(1, 2)
        K = self.w_k(source).view(batch_size, source_len, self.n_heads, self.d_k).transpose(1, 2)
        V = self.w_v(source).view(batch_size, source_len, self.n_heads, self.d_k).transpose(1, 2)

        # Attention-Berechnung
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))

        attn_weights = F.softmax(scores, dim=-1)
        attn_weights = self.dropout(attn_weights)

        # Gewichtete Summe + Köpfe zusammenführen
        context = torch.matmul(attn_weights, V)
        context = context.transpose(1, 2).contiguous().view(batch_size, target_len, self.d_model)

        return self.out_proj(context), attn_weights


# Test
d_model, n_heads = 256, 8
batch = 2
target_len, source_len = 10, 15

cross_attn = CrossAttention(d_model, n_heads)
target = torch.randn(batch, target_len, d_model)  # Decoder-Ausgabe
source = torch.randn(batch, source_len, d_model)  # Encoder-Ausgabe

output, weights = cross_attn(target, source)

print(f"Form der Zielsequenz: {target.shape}")
print(f"Form der Quellsequenz: {source.shape}")
print(f"Form der Ausgabe: {output.shape}")            # [2, 10, 256]
print(f"Form der Gewichte: {weights.shape}")            # [2, 8, 10, 15]
# Bedeutung der Gewichte: wie stark jede Zielposition auf jede Quellposition achtet

Self-Attention vs. Cross-Attention:Bei Self-Attention haben Q/K/V die gleiche Form ($$seq \times seq$$ Gewichtsmatrix); bei Cross-Attention kann die Länge von Q von der von K abweichen ($$target \times source$$ Gewichtsmatrix).

3.3 Positionskodierung

Der Attention-Mechanismus selbst besitztPermutationsinvarianz—das Mischen der Eingabereihenfolge ändert die Ausgabe nicht. Aber Sprache und Bilder sind geordnet, daher muss Positionsinformation explizit durch Positionskodierung injiziert werden. Der ursprüngliche Transformer verwendet Sinus-/Kosinusfunktionen, um feste Positionskodierungen zu erzeugen:

$$PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right), \quad PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)$$

Beispiel

import torch
import torch.nn as nn
import math

class PositionalEncoding(nn.Module):
    """Sinusförmige Positionskodierung (ursprüngliches Transformer-Schema)"""
    def __init__(self, d_model, max_len=5000, dropout=0.1):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)

        # Positionskodierungsmatrix vorab berechnen [max_len, d_model]
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)

        # div_term: steuert die Abklinggeschwindigkeit der verschiedenen Frequenzdimensionen
        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)  # Gerade Dimensionen
        pe[:, 1::2] = torch.cos(position * div_term)  # Ungerade Dimensionen

        pe = pe.unsqueeze(0)  # [1, max_len, d_model]
        self.register_buffer('pe', pe)  # Nicht trainierbar

    def forward(self, x):
        """
        x: [batch, seq_len, d_model]
Rückgabe: x + positional_encoding
        """

        x = x + self.pe[:, :x.size(1), :]
        return self.dropout(x)


# Test
d_model = 128
seq_len = 50
batch = 4

pe = PositionalEncoding(d_model)
x = torch.randn(batch, seq_len, d_model)
output = pe(x)

print(f"Form der Eingabe: {x.shape}")
print(f"Ausgabeform: {output.shape}")
print(f"Form der Positionskodierung: {pe.pe.shape}")  # [1, 5000, 128]

4. Visuelle Anwendungen des Attention-Mechanismus

Attention-Mechanismen sind nicht nur auf Sequenzdaten anwendbar, sondern kommen auch in der Computer Vision zum Einsatz. Im Folgenden werden drei klassische visuelle Attention-Module vorgestellt:

4.1 Kanal-Attention

Kanal-Attention(z. B. SENet) ermöglicht es dem Netzwerk zu lernen, "welche Kanäle wichtiger sind". Sie komprimiert räumliche Informationen durch globales Pooling auf jedem Kanal und lernt dann kanalübergreifende Abhängigkeiten über zwei vollständig verbundene Schichten.

Beispiel

import torch
import torch.nn as nn

class SEBlock(nn.Module):
    """Squeeze-and-Excitation-Block (Kanal-Attention)"""
    def __init__(self, channels, reduction=16):
        super().__init__()
        self.squeeze = nn.AdaptiveAvgPool2d(1)  # Globales Average-Pooling
        self.excitation = nn.Sequential(
            nn.Linear(channels, channels // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channels // reduction, channels, bias=False),
            nn.Sigmoid()  # Ausgabekanalgewichte von 0 bis 1
        )

    def forward(self, x):
        # x: [batch, channels, H, W]
        b, c, _, _ = x.size()

        # Squeeze: [B, C, H, W] → [B, C, 1, 1] → [B, C]
        y = self.squeeze(x).view(b, c)

        # Excitation: Kanalgewichte lernen [B, C]
        y = self.excitation(y).view(b, c, 1, 1)

        # Kanalgewichtung: jeden Kanal mit seinem entsprechenden Gewicht multiplizieren
        return x * y.expand_as(x)


# Test
se = SEBlock(channels=256, reduction=16)
x = torch.randn(4, 256, 32, 32)
output = se(x)
print(f"Eingabe: {x.shape} → Ausgabe: {output.shape}")

4.2 Räumliche Aufmerksamkeit

Räumliche Aufmerksamkeitermöglicht es dem Netzwerk zu lernen, "welche Positionen wichtiger sind". Sie komprimiert entlang der Kanal-Dimension (indem sie den Mittelwert und das Maximum nimmt) und verwendet dann Faltung, um eine räumliche Gewichtskarte zu lernen.

Beispiel

import torch
import torch.nn as nn

class SpatialAttention(nn.Module):
    """Räumliches Aufmerksamkeitsmodul"""
    def __init__(self, kernel_size=7):
        super().__init__()
        padding = kernel_size // 2
        # Eingabe hat 2 Kanäle (Mittelwert + Maximum), Ausgabe hat 1 Kanal (Aufmerksamkeitskarte)
        self.conv = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        # x: [batch, channels, H, W]

        # Kanal-Kompression: Mittelwert und Maximum nehmen → je ein [B, 1, H, W]
        avg_out = torch.mean(x, dim=1, keepdim=True)
        max_out, _ = torch.max(x, dim=1, keepdim=True)

        # Verketten → [B, 2, H, W]
        scale = torch.cat([avg_out, max_out], dim=1)

        # Faltung + Sigmoid → räumliche Gewichtskarte [B, 1, H, W]
        scale = self.sigmoid(self.conv(scale))

        # Räumliche Gewichtung
        return x * scale


# Test
spatial_attn = SpatialAttention(kernel_size=7)
x = torch.randn(4, 256, 32, 32)
output = spatial_attn(x)
print(f"Eingabe: {x.shape} → Ausgabe: {output.shape}")

4.3 CBAM(Convolutional Block Attention Module)

CBAM wendet Kanal-Aufmerksamkeit und räumliche Aufmerksamkeitnacheinanderan: Zuerst werden wichtige Kanäle durch die Kanal-Aufmerksamkeit ausgewählt, dann wichtige Positionen durch die räumliche Aufmerksamkeit. Die beiden ergänzen sich, und die Wirkung ist besser, als nur eine von beiden allein zu verwenden.

Beispiel

import torch
import torch.nn as nn

class CBAM(nn.Module):
    """CBAM: Kanal-Aufmerksamkeit + räumliche Aufmerksamkeit"""
    def __init__(self, channels, reduction=16, kernel_size=7):
        super().__init__()
        # Kanal-Aufmerksamkeit
        self.channel_attn = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(channels, channels // reduction, bias=False),
            nn.ReLU(inplace=True),
            nn.Linear(channels // reduction, channels, bias=False),
            nn.Sigmoid()
        )
        # Räumliche Aufmerksamkeit
        self.spatial_attn = nn.Sequential(
            nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        # 1. Kanal-Aufmerksamkeit
        b, c, _, _ = x.size()
        ca = self.channel_attn(x).view(b, c, 1, 1)
        x = x * ca

        # 2. Räumliche Aufmerksamkeit
        avg_out = torch.mean(x, dim=1, keepdim=True)
        max_out, _ = torch.max(x, dim=1, keepdim=True)
        sa = self.spatial_attn(torch.cat([avg_out, max_out], dim=1))
        x = x * sa

        return x


# Test
cbam = CBAM(channels=256)
x = torch.randn(4, 256, 32, 32)
output = cbam(x)
print(f"Eingabe: {x.shape} → Ausgabe: {output.shape}")

5. Anwendungen des Attention-Mechanismus in NLP

5.1 Transformer-Encoder

Der Transformer-Encoder besteht aus N identischen, übereinander gestapelten Schichten, wobei jede zwei Teilschichten enthält:Multi-Head-Self-AttentionundFeed-Forward-Netzwerk. Jede Teilschicht verwendet Residualverbindungen und Layer-Normalisierung.

Beispiel

import torch
import torch.nn as nn
import math


class TransformerEncoderLayer(nn.Module):
    """Einzelne Transformer-Encoder-Schicht"""
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        # Multi-Head-Self-Attention
        self.self_attn = nn.MultiheadAttention(
            d_model, n_heads, dropout=dropout, batch_first=True
        )
        # Feed-Forward-Netzwerk (zwei lineare Schichten + Aktivierung)
        self.ffn = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),  # GELU wird häufiger verwendet als ReLU
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model)
        )
        # Layer-Normalisierung (Pre-Norm-Stil, stabileres Training)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, key_padding_mask=None):
        # Teilschicht 1: Selbstaufmerksamkeit
        residual = x
        x = self.norm1(x)  # Pre-Norm
        attn_out, _ = self.self_attn(x, x, x, key_padding_mask=key_padding_mask)
        x = residual + self.dropout(attn_out)

        # Teilschicht 2: Feed-Forward-Netzwerk
        residual = x
        x = self.norm2(x)  # Pre-Norm
        x = residual + self.dropout(self.ffn(x))

        return x


class TransformerEncoder(nn.Module):
    """Mehrschichtiger Transformer-Encoder"""
    def __init__(self, d_model, n_heads, d_ff, num_layers, dropout=0.1):
        super().__init__()
        self.layers = nn.ModuleList([
            TransformerEncoderLayer(d_model, n_heads, d_ff, dropout)
            for _ in range(num_layers)
        ])
        self.final_norm = nn.LayerNorm(d_model)

    def forward(self, x, key_padding_mask=None):
        for layer in self.layers:
            x = layer(x, key_padding_mask)
        return self.final_norm(x)


# Test
d_model, n_heads, d_ff = 128, 4, 512
num_layers, seq_len, batch = 3, 20, 4

encoder = TransformerEncoder(d_model, n_heads, d_ff, num_layers)
x = torch.randn(batch, seq_len, d_model)

# Padding-Maske: True = diese Position ignorieren
key_padding_mask = torch.zeros(batch, seq_len, dtype=torch.bool)
key_padding_mask[0, 15:] = True

output = encoder(x, key_padding_mask=key_padding_mask)
print(f"Eingabe: {x.shape} → Ausgabe: {output.shape}")

5.2 Attention-Visualisierung

Die Visualisierung von Attention-Gewichten ist ein wichtiges Mittel zum Verständnis und Debuggen von Transformer-Modellen. Mithilfe von Heatmaps können Sie intuitiv sehen, auf welche Informationen das Modell an jeder Position achtet.

Beispiel

import torch
import matplotlib.pyplot as plt
import numpy as np

def visualize_attention(attention_weights, tokens=None, save_path=Keine):
    """
Visualisierung von Multi-Head-Attention-Gewichten

Parameter:
        attention_weights: [n_heads, seq_len, seq_len]
tokens: Token-Liste (optional)
save_path: Speicherpfad (optional)
    """

    n_heads = attention_weights.shape[0]
    seq_len = attention_weights.shape[1]

    fig, axes = plt.subplots(1, n_heads, figsize=(n_heads * 3, 3.5))
    if n_heads == 1:
        axes = [axes]

    for head in range(n_heads):
        ax = axes[head]
        attn = attention_weights[head].detach().cpu().numpy()

        im = ax.imshow(attn, cmap='Blues', aspect='auto', vmin=0, vmax=attn.max())
        ax.set_title(f'Kopf {head + 1}', fontsize=10, fontweight='bold')
        ax.set_xlabel('Schlüssel')
        ax.set_ylabel('Abfrage')

        if tokens is not None:
            ax.set_xticks(range(len(tokens)))
            ax.set_yticks(range(len(tokens)))
            ax.set_xticklabels(tokens, rotation=90, fontsize=8)
            ax.set_yticklabels(tokens, fontsize=8)

    plt.tight_layout()
    if save_path:
        plt.savefig(save_path, dpi=150, bbox_inches='tight')
    plt.show()


# Simulierte Attention-Gewichte nach dem Training
n_heads, seq_len = 4, 8
attention_weights = torch.softmax(torch.randn(n_heads, seq_len, seq_len), dim=-1)
tokens = ['The', 'cat', 'sat', 'on', 'the', 'mat', '.', '[PAD]']

visualize_attention(attention_weights, tokens)

6. Vollständiger Attention-Klassifikator

6.1 Attention-basierte Textklassifikation

Das Folgende ist ein vollständiges Textklassifikationsmodell: Es verwendet einen Transformer-Encoder zur Merkmalsextraktion, komprimiert dann Sequenzen variabler Länge über Attention-Pooling in Vektoren fester Länge und speist sie schließlich in einen Klassifikator ein.

Beispiel

import torch
import torch.nn as nn
import math


class AttentionClassifier(nn.Module):
    """Textklassifikationsmodell basierend auf Transformer + Attention-Pooling"""
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes,
                 n_heads=4, num_layers=2, dropout=0.1):
        super().__init__()
        self.embed_dim = embed_dim

        # Wort-Embedding
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.pos_encoding = PositionalEncoding(embed_dim, dropout=dropout)

        # Transformer-Encoder
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=embed_dim,
            nhead=n_heads,
            dim_feedforward=hidden_dim,
            dropout=dropout,
            batch_first=True,
            activation='gelu'
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)

        # Attention-Pooling: Wichtigkeitsgewichte für jede Position lernen
        self.attn_pool = nn.Sequential(
            nn.Linear(embed_dim, 1)  # Gibt ein skalares Gewicht für jede Position aus
        )

        # Klassifizierungskopf
        self.classifier = nn.Sequential(
            nn.Linear(embed_dim, hidden_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, num_classes)
        )

    def forward(self, x):
        """
        x: [batch, seq_len] — Token-IDs
Gibt zurück: logits [batch, num_classes], attn_weights [batch, seq_len]
        """

        # Worteinbettung + Positionskodierung
        mask = (x == 0)  # Padding-Maske
        x = self.embedding(x) * math.sqrt(self.embed_dim)
        x = self.pos_encoding(x)

        # Transformer-Kodierung
        x = self.transformer(x, src_key_padding_mask=mask)

        # Attention-Pooling: Gewichte für jede Position berechnen, gewichtete Summe
        scores = self.attn_pool(x).squeeze(-1)          # [batch, seq_len]
        scores = scores.masked_fill(mask, float('-inf'))  # Padding ignorieren
        attn_weights = torch.softmax(scores, dim=1)       # [batch, seq_len]
        pooled = torch.bmm(attn_weights.unsqueeze(1), x).squeeze(1)  # [batch, embed_dim]

        # Klassifikation
        logits = self.classifier(pooled)
        return logits, attn_weights


# Test
model = AttentionClassifier(
    vocab_size=10000, embed_dim=128, hidden_dim=256, num_classes=5
)

# Simulierte Eingabe (mit Padding)
x = torch.randint(1, 10000, (16, 50))
x[:, -10:] = 0  # Die letzten 10 Positionen sind Padding

logits, attn_weights = model(x)
print(f"Eingabe: {x.shape}")
print(f"Logits: {logits.shape}")          # [16, 5]
print(f"Attention-Gewichte: {attn_weights.shape}") # [16, 50]

6.2 Vergleich gängiger Attention-Varianten

Type Q-Quelle K/V-Quelle Hauptverwendung Typische Anwendung
Self-Attention Selbst Selbst Globale Interaktion innerhalb einer Sequenz Transformer, BERT, ViT
Cross-Attention Zielsequenz Quellsequenz Sequenzübergreifende Informationsfusion Maschinelle Übersetzung, Bildbeschriftung, Stable Diffusion
Channel Attention Global gepoolte Merkmale Wie oben Kanalwichtigkeit lernen SENet, CBAM
Räumliche Aufmerksamkeit Kanal-komprimierte Merkmale Wie oben Wichtigkeit räumlicher Positionen lernen CBAM、Spatial Transformer
Sparse-Aufmerksamkeit Teilpositionen Teilpositionen Rechenkomplexität für lange Sequenzen reduzieren Longformer、BigBird

7. Bewährte Verfahren und häufige Probleme

7.1 Tipps zur Verwendung

  • Maskenkonsistenz: Stellen Sie sicher, dass die Padding-Maske und die kausale Maske eine konsistente Richtung haben (True = ignorieren vs. 1 = beibehalten). Unterschiedliche API-Konventionen können abweichen.
  • Auswahl der Kopfanzahl: Wird üblicherweise als Teiler von $$d_{model}$$ festgelegt, häufig 8 oder 16. Zu viele Köpfe verringern die Dimension pro Kopf $$d_k$$, was die Leistung potenziell beeinträchtigen kann.
  • Residual-Verbindung: Achten Sie darauf, Residual-Verbindungen + Layer-Normalisierung vor und nach Attention-Schichten zu verwenden; dies ist eine notwendige Bedingung für das Training tiefer Transformer.
  • Lernraten-Warmup: Gradientenschwankungen sind in der frühen Phase des Transformer-Trainings groß; die Verwendung eines Warmup- + Cosine-Decay-Schedulers kann die Stabilität erheblich verbessern.
  • Flash Attention: PyTorch 2.0+ unterstützt durchnn.functional.scaled_dot_product_attentionautomatisches Aufrufen von Flash Attention, wodurch sowohl Speicherverbrauch als auch Geschwindigkeit deutlich optimiert werden.

7.2 Häufige Probleme

Problem Ursache Lösung
Attention-Gewichte sind alle gleichverteilt Nicht normalisierte Eingabe oder zu große Lernrate Eingabeskalierung prüfen, LayerNorm verwenden, Lernrate reduzieren
Trainingsverlust nimmt nicht ab Fehlende Positionskodierung oder falsche Maske Bestätigen, dass die Positionskodierung hinzugefügt wird, Maskenrichtung prüfen
Nicht genügend Speicher (lange Sequenzen) Die Attention-Matrix hat eine Komplexität von $$O(n^2)$$ Verwenden Sie Flash Attention, Gradient Checkpointing oder Sparse Attention
Verschlechterung der Attention während der Inferenz Inkonsistente Maske zwischen Training und Inferenz Stellen Sie sicher, dass die Padding-Maske während des Trainings und der Inferenz dieselbe Logik verwendet

Der Attention-Mechanismus ist eine Kerntechnologie des modernen Deep Learning. Von NLP bis CV, von der Sprachverarbeitung bis zur Proteinstrukturvorhersage, basieren fast alle modernsten Modelle auf Attention. Das Verständnis seiner Prinzipien und Implementierung ist der entscheidende erste Schritt zur Beherrschung von Modellen wie Transformer, ViT, Stable Diffusion und GPT.

Andere Erweiterungen