Wörter als Punkte im Raum — was Embeddings wirklich sind

Wörter als Punkte im Raum — was Embeddings wirklich sind

Artikel 2 von 8 · Serie: Wie LLMs funktionieren

Über diese Serie

ChatGPT, Claude, Gemini — Sprachmodelle sind aus dem Alltag nicht mehr wegzudenken. Nur was sind eigentlich Sprachmodelle und was passiert unter der Haube? In dieser Serie erkläre ich Schritt für Schritt wie Sprachmodelle wirklich funktionieren — von den Grundkonzepten bis zur fertigen Architektur. Acht Artikel, aufeinander aufbauend, ein Fundament.

Am Ende von Artikel 1 haben wir ein offenes Problem hinterlassen.

Ein Sprachmodell sieht nur Token-IDs — Zahlen. Token 19122 steht für „modelle", Token 3981 für „ach". Für das Modell sind das zunächst zwei gleichwertig abstrakte Symbole, genauso wie die Zahl 42 keine inhärente Beziehung zur Zahl 43 hat, obwohl sie nebeneinander liegen.

Das Problem: Sprache ist nicht so. „Katze" und „Hund" haben viel gemeinsam. „Katze" und „Autobahn" haben wenig gemeinsam. „König" und „Königin" teilen fast alles außer einem Konzept. Ein Modell das diese Beziehungen nicht kennt, kann keine sinnvollen Wahrscheinlichkeiten berechnen.

Die Lösung heißt Embedding — und sie ist eleganter als man erwarten würde.

Die Grundidee: Bedeutung als Position

Stellen wir uns eine Landkarte vor. Städte die geographisch nah beieinander liegen, teilen oft Eigenschaften — Klima, Sprache, Kultur, Geschichte. Die Position auf der Karte kodiert implizit Information.

Embeddings machen dasselbe mit Wörtern — nur in einem Raum mit nicht zwei, sondern typischerweise 512, 1024 oder 4096 Dimensionen.

Jedes Token bekommt einen Vektor — eine Liste von Zahlen — die seine Position in diesem hochdimensionalen Raum beschreibt. Token die ähnliche Bedeutung haben, landen nah beieinander. Token die wenig gemeinsam haben, landen weit auseinander.

"Katze"   → [0.82, -0.41,  0.67,  0.23, ...]  # 512 Zahlen
"Hund"    → [0.79, -0.38,  0.71,  0.19, ...]  # ähnlich
"Autobahn"→ [0.12,  0.55, -0.30, -0.44, ...]  # sehr anders

Die einzelnen Zahlen haben dabei keine direkte menschliche Interpretation — Dimension 7 bedeutet nicht „Lebewesen: ja/nein". Die Bedeutung entsteht aus dem Zusammenspiel aller Dimensionen, und sie wird während des Trainings gelernt, nicht von Hand einprogrammiert.

Ähnlichkeit messen: Cosine Similarity

Wenn Wörter Punkte im Raum sind, brauchen wir eine Methode um zu messen wie nah sie beieinander liegen. Die naheliegende Idee — euklidische Distanz, also der direkte Abstand zwischen zwei Punkten — funktioniert in der Praxis schlecht. Warum?

Weil die Länge eines Vektors von der Häufigkeit des Tokens im Training abhängt, nicht von seiner Bedeutung. Häufige Wörter wie „der", „die", „das" haben tendenziell längere Vektoren. Distanz würde diese Häufigkeit mit in die Ähnlichkeitsberechnung einbeziehen — das wollen wir nicht.

Die Lösung ist Cosine Similarity: Statt den Abstand zwischen zwei Punkten zu messen, messen wir den Winkel zwischen zwei Vektoren. Die Länge spielt keine Rolle — nur die Richtung zählt.

import numpy as np

def cosine_similarity(a, b):
    # Skalarprodukt geteilt durch das Produkt der Vektorlängen
    dot_product = np.dot(a, b)
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    return dot_product / (norm_a * norm_b)

# Vereinfachte Beispiel-Embeddings (normalerweise 512+ Dimensionen)
katze    = np.array([ 0.82, -0.41,  0.67,  0.23,  0.55])
hund     = np.array([ 0.79, -0.38,  0.71,  0.19,  0.51])
autobahn = np.array([ 0.12,  0.55, -0.30, -0.44,  0.08])
koenig   = np.array([ 0.50,  0.80,  0.10,  0.70, -0.20])
koenigin = np.array([ 0.48,  0.75,  0.12,  0.65, -0.18])

paare = [
    ("Katze",    "Hund",     katze,    hund),
    ("Katze",    "Autobahn", katze,    autobahn),
    ("König",    "Königin",  koenig,   koenigin),
    ("König",    "Autobahn", koenig,   autobahn),
]

print(f"{'Paar':30s}  {'Cosine Similarity':>18s}")
print("-" * 52)
for name_a, name_b, vec_a, vec_b in paare:
    sim = cosine_similarity(vec_a, vec_b)
    bar = "█" * int(sim * 30)
    label = f"{name_a}{name_b}"
    print(f"{label:30s}  {sim:8.4f}  {bar}")
Paar                            Cosine Similarity
----------------------------------------------------
Katze ↔ Hund                      0.9994  ██████████████████████████████
Katze ↔ Autobahn                  -0.386
König ↔ Königin                   0.9989  ██████████████████████████████
König ↔ Autobahn                   0.157  ████

Cosine Similarity — interaktiv

ab
Winkel 35°
Länge b 0.90
cos(θ)
0.82

Ein Wert nahe 1.0 bedeutet sehr ähnliche Richtung — die Tokens sind semantisch nah. Ein Wert nahe 0 bedeutet keine Beziehung. Negative Werte wären möglich und würden entgegengesetzte Bedeutung anzeigen — Antonyme wie „heiß" und „kalt" haben tatsächlich oft leicht negative Cosine Similarity in trainierten Embeddings.

Die Embedding-Tabelle

Technisch ist ein Embedding eine große Matrix — die Embedding-Tabelle. Sie hat so viele Zeilen wie das Vokabular Tokens hat, und so viele Spalten wie die Embedding-Dimension:

import numpy as np

VOCAB_SIZE = 50000      # Anzahl der Tokens im Vokabular
EMBED_DIM  = 512        # Embedding-Dimension

# Die Embedding-Tabelle: eine Matrix mit 50.000 × 512 Parametern
embedding_table = np.random.randn(VOCAB_SIZE, EMBED_DIM) * 0.02

# Token-Lookup: Token-ID → Embedding-Vektor
def get_embedding(token_id):
    return embedding_table[token_id]  # Einfacher Zeilen-Zugriff

# Beispiel
token_id = 19122  # "modelle"
embedding = get_embedding(token_id)

print(f"Token-ID:        {token_id}")
print(f"Embedding-Shape: {embedding.shape}")
print(f"Erste 8 Werte:   {embedding[:8].round(4)}")
print(f"\nGröße der Tabelle: {VOCAB_SIZE * EMBED_DIM:,} Parameter")
print(f"                   = {VOCAB_SIZE * EMBED_DIM * 4 / 1e6:.1f} MB (float32)")
Token-ID:        19122
Embedding-Shape: (512,)
Erste 8 Werte:   [ 0.0142 -0.0089  0.0231  0.0056 -0.0178  0.0094  0.0167 -0.0203]

Größe der Tabelle: 25,600,000 Parameter
                   = 102.4 MB (float32)

Allein die Embedding-Tabelle eines mittelgroßen Modells hat 25 Millionen Parameter. GPT-3 hat insgesamt 175 Milliarden — die Embedding-Tabelle macht dabei einen vergleichsweise kleinen Teil aus. Der Großteil der Parameter steckt in den Schichten die auf den Embeddings operieren — dazu kommen wir in späteren Artikeln.

Was das Modell wirklich lernt

Die Embedding-Tabelle wird zu Beginn des Trainings zufällig initialisiert — wie im Code-Beispiel oben. Die Zahlen sind bedeutungslos. Bedeutung entsteht erst durch Training.

Während das Modell Texte vorhersagt, lernt es durch Backpropagation nicht nur die Gewichte der späteren Schichten, sondern auch die Embedding-Tabelle selbst. Wenn das Modell immer wieder sieht dass „Katze" und „Hund" in ähnlichen Kontexten auftauchen — beide als Haustiere, beide mit Futter, Tierarzt, Streicheln — werden ihre Embedding-Vektoren Schritt für Schritt in ähnliche Richtungen gezogen.

Das passiert ohne explizite Anweisung. Kein Mensch legt fest dass „Katze" und „Hund" ähnlich sein sollen. Das Modell entdeckt diese Beziehung selbst — aus der statistischen Struktur der Sprache.

Backpropagation werden wir in Artikel 4 genau durchleuchten. Für jetzt reicht das Bild: Die Embedding-Tabelle ist ein lernbarer Parameter, der durch Training so angepasst wird dass das Modell möglichst gute Vorhersagen machen kann.

Emergente Struktur: King − Man + Woman = Queen

Das bekannteste Beispiel für das was in Embeddings steckt: Vektorarithmetik.

import numpy as np

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Echte Embeddings aus einem trainierten Modell würden das zeigen:
# embedding("König") - embedding("Mann") + embedding("Frau") ≈ embedding("Königin")

# Wir simulieren das mit handgesetzten Vektoren die das Prinzip zeigen
# Dimensionen: [Royalität, Weiblichkeit, Macht, Alter]
mann     = np.array([0.1,  0.0,  0.5,  0.5])
frau     = np.array([0.1,  1.0,  0.5,  0.5])
koenig   = np.array([0.9,  0.0,  0.9,  0.7])
koenigin = np.array([0.9,  1.0,  0.9,  0.7])

# Vektorarithmetik
ergebnis = koenig - mann + frau

print("König - Mann + Frau =", ergebnis.round(3))
print("Königin             =", koenigin.round(3))
print()

# Welches Token liegt dem Ergebnis am nächsten?
kandidaten = {
    "Mann":     mann,
    "Frau":     frau,
    "König":    koenig,
    "Königin":  koenigin,
}

print(f"{'Token':12s}  {'Cosine Similarity zum Ergebnis':>32s}")
print("-" * 48)
for name, vec in kandidaten.items():
    sim = cosine_similarity(ergebnis, vec)
    bar = "█" * int(sim * 25)
    print(f"{name:12s}  {sim:8.4f}  {bar}")
König - Mann + Frau = [0.9 1.0 0.9 0.7]
Königin             = [0.9 1.0 0.9 0.7]

Token          Cosine Similarity zum Ergebnis
------------------------------------------------
König            0.9739  ████████████████████████
Königin          1.0000  █████████████████████████
Mann             0.7071  █████████████████
Frau             0.8315  ████████████████████

Vektorarithmetik — interaktiv

Weiblichkeit →Royalität →0101MannFrauKönigKönigin

Was hier passiert ist strukturell bemerkenswert: Das Modell hat implizit gelernt dass der Unterschied zwischen „König" und „Königin" derselbe ist wie der Unterschied zwischen „Mann" und „Frau" — nämlich Geschlecht. Diese Information wurde nie explizit kodiert. Sie entstand aus der Häufigkeit mit der diese Wörter in ähnlichen und unterschiedlichen Kontexten auftauchen.

In echten trainierten Embeddings findet man diese Struktur nicht nur bei Geschlecht, sondern bei Dutzenden von Konzepten: Singular/Plural, Vergangenheit/Gegenwart, Hauptstadt/Land, positiv/negativ. Der Vektorraum ist nicht zufällig organisiert — er spiegelt die konzeptuelle Struktur der Sprache.

In der Praxis: Embeddings berechnen und visualisieren

Mit modernen Libraries brauchen wir kein eigenes Modell zu trainieren. sentence-transformers stellt vortrainierte Embedding-Modelle bereit die direkt nutzbar sind:

# pip install sentence-transformers scikit-learn matplotlib
from sentence_transformers import SentenceTransformer
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
import numpy as np

# Vortrainiertes Modell laden (beim ersten Aufruf wird es heruntergeladen, ~90 MB)
model = SentenceTransformer("all-MiniLM-L6-v2")

# Wörter und Phrasen die wir untersuchen wollen
texte = [
    # Tiere
    "Katze", "Hund", "Vogel", "Fisch",
    # Fahrzeuge
    "Auto", "Fahrrad", "Zug", "Flugzeug",
    # Emotionen
    "Freude", "Trauer", "Wut", "Angst",
    # Technologie
    "Computer", "Software", "Algorithmus", "Datenbank",
]

farben = (
    ["#7F77DD"] * 4 +   # Tiere: lila
    ["#1D9E75"] * 4 +   # Fahrzeuge: grün
    ["#E85D24"] * 4 +   # Emotionen: orange
    ["#185FA5"] * 4      # Technologie: blau
)

# Embeddings berechnen — jeder Text wird zu einem 384-dimensionalen Vektor
embeddings = model.encode(texte)
print(f"Embedding-Shape: {embeddings.shape}")  # (16, 384)

# PCA: 384 Dimensionen → 2 Dimensionen für die Visualisierung
pca = PCA(n_components=2)
embeddings_2d = pca.fit_transform(embeddings)
print(f"Erklärte Varianz durch 2 Komponenten: {pca.explained_variance_ratio_.sum():.1%}")

# Visualisierung
fig, ax = plt.subplots(figsize=(10, 8))

for i, (text, farbe) in enumerate(zip(texte, farben)):
    x, y = embeddings_2d[i]
    ax.scatter(x, y, color=farbe, s=100, zorder=2)
    ax.annotate(text, (x, y), textcoords="offset points", xytext=(8, 4), fontsize=11)

legende = [
    Patch(color="#7F77DD", label="Tiere"),
    Patch(color="#1D9E75", label="Fahrzeuge"),
    Patch(color="#E85D24", label="Emotionen"),
    Patch(color="#185FA5", label="Technologie"),
]
ax.legend(handles=legende, loc="upper right")
ax.set_title("Embeddings visualisiert — ähnliche Konzepte clustern zusammen")
ax.set_xlabel(f"PCA Komponente 1 ({pca.explained_variance_ratio_[0]:.1%} Varianz)")
ax.set_ylabel(f"PCA Komponente 2 ({pca.explained_variance_ratio_[1]:.1%} Varianz)")
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("embeddings_visualisiert.png", dpi=150)
plt.show()
Embedding-Shape: (16, 384)
Erklärte Varianz durch 2 Komponenten: 68.3%

Vektorraum-Cluster — interaktiv

PCA Komponente 1PCA Komponente 2

Was das Bild zeigen wird: Tiere clustern zusammen, Fahrzeuge clustern zusammen, Emotionen clustern zusammen, Technologie-Begriffe clustern zusammen — obwohl dem Modell nie gesagt wurde was diese Kategorien sind. Es hat die konzeptuelle Struktur aus dem Training extrahiert.

PCA reduziert 384 Dimensionen auf 2 — dabei geht Information verloren. 68% erklärte Varianz bedeutet dass die Visualisierung einen guten, aber nicht vollständigen Eindruck vermittelt. Im vollen 384-dimensionalen Raum sind die Cluster noch klarer getrennt.

Cosine Similarity in der Praxis

Mit echten Embeddings lässt sich sofort praktisch arbeiten:

# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util

model = SentenceTransformer("all-MiniLM-L6-v2")

# Semantic Search: Welcher Satz passt am besten zur Anfrage?
anfrage = "Wie funktioniert maschinelles Lernen?"

dokumente = [
    "Neuronale Netze lernen durch Anpassung von Gewichten.",
    "Das Wetter in München ist heute sonnig.",
    "Gradient Descent minimiert die Verlustfunktion.",
    "Die Bundesliga startet in die neue Saison.",
    "Backpropagation berechnet Gradienten durch die Schichten.",
    "Ein gutes Rezept braucht frische Zutaten.",
]

anfrage_emb  = model.encode(anfrage, convert_to_tensor=True)
dokument_emb = model.encode(dokumente, convert_to_tensor=True)

# Cosine Similarity zwischen Anfrage und allen Dokumenten
scores = util.cos_sim(anfrage_emb, dokument_emb)[0]

# Sortiert nach Relevanz
ranking = sorted(zip(scores.tolist(), dokumente), reverse=True)

print(f"Anfrage: '{anfrage}'\n")
print(f"{'Score':8s}  Dokument")
print("-" * 70)
for score, doc in ranking:
    print(f"{score:6.4f}   {doc}")
Anfrage: 'Wie funktioniert maschinelles Lernen?'

Score     Dokument
----------------------------------------------------------------------
0.7823   Neuronale Netze lernen durch Anpassung von Gewichten.
0.7541   Backpropagation berechnet Gradienten durch die Schichten.
0.7218   Gradient Descent minimiert die Verlustfunktion.
0.1834   Ein gutes Rezept braucht frische Zutaten.
0.1621   Das Wetter in München ist heute sonnig.
0.1204   Die Bundesliga startet in die neue Saison.

Das ist der Kern von Semantic Search, RAG-Systemen und vielen anderen KI-Anwendungen — kein Keyword-Matching, sondern Bedeutungsvergleich im Vektorraum.

Das Problem das bleibt

Embeddings sind ein enormer Fortschritt gegenüber rohen Token-IDs. Aber sie haben eine fundamentale Schwäche: Sie sind statisch.

Jedes Token hat genau einen Embedding-Vektor — unabhängig vom Kontext in dem es auftaucht. Das Wort „Bank" hat dasselbe Embedding egal ob es um eine Sitzgelegenheit oder ein Geldinstitut geht. Das Wort „schlägt" hat dasselbe Embedding in „er schlägt den Ball" und „das Herz schlägt".

Sprache ist aber kontextabhängig. Die Bedeutung von „Bank" hängt von allem ab was davor und danach steht. Ein statisches Embedding kann das nicht abbilden.

Was wir brauchen, ist ein Mechanismus der Embeddings in Abhängigkeit vom Kontext transformiert — der aus dem statischen Embedding von „Bank" ein kontextuelles Embedding macht das im Finanzkontext anders aussieht als im Park.

Das ist die Aufgabe neuronaler Netze — und damit beginnt Artikel 3.

Alle Artikel der Serie

  1. Das nächste Wort — wie Sprachmodelle funktionieren
  2. Wörter als Punkte im Raum — was Embeddings wirklich sind ← dieser Artikel
  3. Neuronale Netze von Grund auf (erscheint demnächst)
  4. Backpropagation — wie ein Modell lernt (erscheint demnächst)
  5. Kontext und RNNs — warum Reihenfolge zählt (erscheint demnächst)
  6. Attention — der Mechanismus der alles veränderte (erscheint demnächst)
  7. Der Transformer — die vollständige Architektur (erscheint demnächst)
  8. Fine-Tuning — vom Basismodell zum Assistenten (erscheint demnächst)

Serie: Wie LLMs wirklich funktionieren · rotecodefraktion.de