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.
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.
Beispiel
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.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.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.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.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.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.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.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.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.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 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.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 durch
nn.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 |
Andere ErweiterungenDer 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.