Zum Hauptinhalt springen
Lektion 4 von 22 · Embeddings, RAG und semantische Suche

Distanz und Kosinus-Ähnlichkeit

Lektion 4 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Messung der Nähe zwischen Vektoren
  3. Praktischer Vergleich
  4. Interpretation der Werte
  5. Nachbarschaftssuche
  6. Mit scikit-learn
  7. Zusammenfassung

Lernziele

  • Kosinus-Ähnlichkeit verstehen und deren Vorzug begründen
  • Vektorvergleiche in Python implementieren
  • Ähnlichkeitswerte interpretieren

Messung der Nähe zwischen Vektoren

Sobald Sie Ihre Texte in Vektoren umgewandelt haben, hängt alles von einer einzigen Frage ab: Wie entscheiden Sie, ob zwei Vektoren „nah” zueinander liegen? Drei Antworten existieren nebeneinander, und die Wahl ist entscheidend.

Euklidische Distanz ist die klassische geometrische Distanz – diejenige, die Sie mit einem Lineal zwischen zwei Punkten messen würden. Sie ist intuitiv, hängt aber von der Vektormagnitude ab: Zwei Texte mit derselben Bedeutung, bei denen einer mit höherer Amplitude kodiert ist, würden künstlich weit voneinander entfernt erscheinen.

import numpy as np

def euclidean_distance(a: list[float], b: list[float]) -> float:
    return np.linalg.norm(np.array(a) - np.array(b))

Das Skalarprodukt summiert die elementweisen Produkte. Es ist die schnellste Operation in dieser Familie – diejenige, die der Prozessor bei großen Matrizen am effizientesten ausführt.

def dot_product(a: list[float], b: list[float]) -> float:
    return np.dot(a, b)

Kosinus-Ähnlichkeit misst schließlich den Winkel zwischen zwei Vektoren, unabhängig von ihrer Länge, wodurch sie zum Standardmaß für Embeddings wird: Nur die Richtung der Bedeutung zählt, nicht ihre Intensität.

def cosine_similarity(a: list[float], b: list[float]) -> float:
    a, b = np.array(a), np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Sie reicht von -1 für entgegengesetzte Richtungen bis +1 für identische Richtungen. Ein praktischer Detailaspekt ist wichtig: OpenAI-Embeddings sind bereits normalisiert, das heißt, sie haben eine Länge von 1. Der Nenner ist daher gleich 1, und das Skalarprodukt liefert exakt das gleiche Ergebnis wie die Kosinus-Ähnlichkeit – zu einem geringeren Rechenaufwand. Dies ist der Abkürzungsweg, den wir in diesem Kurs verwenden werden.

Praktischer Vergleich

Nichts schlägt das Messen echter Sätze. Vergleichen wir einen Satz, seine vollständige Paraphrase und einen themenfremden Satz:

from openai import OpenAI

client = OpenAI()

def get_embedding(text: str) -> list[float]:
    response = client.embeddings.create(
        input=text,
        model="text-embedding-3-large"
    )
    return response.data[0].embedding

# Sätze zum Vergleich
sentences = {
    "a": "The cat sleeps on the couch",
    "b": "The feline dozes on the sofa",
    "c": "Python is a programming language",
}

embeddings = {k: get_embedding(v) for k, v in sentences.items()}

# Alle Paare vergleichen
from itertools import combinations

for (k1, v1), (k2, v2) in combinations(sentences.items(), 2):
    sim = cosine_similarity(embeddings[k1], embeddings[k2])
    print(f"sim({k1}, {k2}) = {sim:.4f}")
    print(f"  {v1}")
    print(f"  {v2}")
    print()

Typisches Ergebnis:

sim(a, b) = 0.8923   ← sehr ähnliche Bedeutung
sim(a, c) = 0.1247   ← unterschiedliche Themen
sim(b, c) = 0.1189   ← unterschiedliche Themen

Sätze a und b haben keine gemeinsamen Wörter – Katze/Geflügel, schläft/lauscht, Sofa/Sofa – und erzielen dennoch eine Punktzahl von 0,89. Genau das kann keine lexikalische Methode leisten.

Interpretation der Werte

Schwellenwerte hängen von Ihrem Anwendungsfall ab, aber die folgende Tabelle gibt Referenzpunkte zum Lesen eines Werts ohne Größenordnungsfehler.

WertInterpretation
> 0,85Sehr ähnlich (Paraphrasen, Synonyme)
0,70 – 0,85Thematisch verwandt
0,40 – 0,70Locker verwandt
< 0,40Unterschiedliche Themen

Diese Werte variieren je nach Modell und Domäne: Ein juristischer Korpus, bei dem alle Texte dasselbe formale Vokabular teilen, erzeugt im Allgemeinen höhere Werte als ein allgemeiner Korpus, bis hin zu dem Punkt, an dem eine Schwelle von 0,70 zu permissiv wird. Kalibrieren Sie diese Werte stets auf Ihren eigenen Daten, indem Sie etwa dreißig Paare beobachten, bei denen Sie die erwartete Antwort kennen.

Nachbarschaftssuche

Das Finden eines Dokuments bedeutet, die Ähnlichkeit zwischen dem Abfragevektor und allen Vektoren im Korpus zu berechnen und die besten k beizubehalten. Der Trick besteht darin, Schleifen in Python zu vermeiden: Eine Matrixmultiplikation mit NumPy berechnet alle Ähnlichkeiten auf einmal und in einem Bruchteil der Zeit.

def search_similar(
    query_embedding: list[float],
    corpus_embeddings: np.ndarray,
    k: int = 5
) -> list[tuple[int, float]]:
    """Gibt die k ähnlichsten Dokumente zurück."""
    query = np.array(query_embedding)

    # Vektorisierte Berechnung aller Ähnlichkeiten
    similarities = corpus_embeddings @ query
    # (funktioniert, weil OpenAI-Embeddings normalisiert sind)

    # Indizes der besten k Werte
    top_k_indices = np.argsort(similarities)[-k:][::-1]

    return [(int(i), float(similarities[i])) for i in top_k_indices]

# Verwendung
corpus = np.load("embeddings.npy")  # Matrix (n, 3072)
query_emb = get_embedding("How do I install Python?")

results = search_similar(query_emb, corpus, k=5)
for idx, score in results:
    print(f"Dokument {idx}: score = {score:.4f}")

Das [-k:][::-1] verdient einen zweiten Blick: argsort sortiert in aufsteigender Reihenfolge, daher nehmen wir das Ende des Arrays, um die besten Werte zu erhalten, und kehren dann um, um sie von höchster zu niedrigster Relevanz zu ranken.

Mit scikit-learn

Wenn Sie kein Dokument suchen, sondern den gesamten Korpus mit sich selbst vergleichen möchten – zum Beispiel, um Duplikate zu erkennen oder für Clustering vorzubereiten – bietet scikit-learn eine optimierte Funktion, die die vollständige Matrix zurückgibt.

from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

# Ähnlichkeitsmatrix zwischen allen Dokumenten
corpus = np.array(embeddings_list)
sim_matrix = cosine_similarity(corpus)

print(f"Form: {sim_matrix.shape}")  # (n, n)
print(f"Ähnlichkeit Dok 0 ↔ Dok 1: {sim_matrix[0][1]:.4f}")

Halten Sie im Hinterkopf, dass diese Matrix eine Größe von n × n hat: Zehn tausend Dokumente erzeugen hundert Millionen Werte, etwa 400 MB in float32. Jenseits weniger tausend Elemente verarbeiten Sie in Blöcken, anstatt einen einzigen Aufruf zu machen.

Zusammenfassung

  • Kosinus-Ähnlichkeit misst den Winkel zwischen Vektoren, unabhängig von ihrer Magnitude
  • OpenAI-Embeddings sind normalisiert: Skalarprodukt = Kosinus
  • Ein Wert > 0,85 deutet auf sehr ähnliche Texte hin
  • Nachbarschaftssuche wird effizient mit NumPy oder scikit-learn durchgeführt