Zum Inhalt
wannacode

Embeddings: Bedeutung als Vektoren

Kurz gesagt: Ein Embedding ist ein Vektor aus Zahlen für ein Wort oder einen Text, sodass Texte mit naher Bedeutung nahe Vektoren haben, und die Nähe wird mit dem Kosinus des Winkels gemessen.

Problem

Suche nach Wörtern

Der Support-Bot hat eine Basis fertiger Antworten: je einen Text für Lieferung, Rückgabe und Zahlung. Der Kunde schreibt eine Frage, und der Bot soll den Text finden, der sie beantwortet.

# die Antwortbasis des Supports
DOCS = [
("lieferung", "die lieferung dauert bis zu zwei tagen"),
("rückgabe", "gib die ware innerhalb von 14 tagen zurück und erhalte das geld"),
("zahlung", "zahle mit karte oder per nachnahme"),
]
# die Fragen der Kunden
QUESTIONS = [
"wann kommt mein paket an",
"warum wurde meine abbuchung abgelehnt",
]

Ein Kollege sucht nach gemeinsamen Wörtern: Je mehr Wörter der Frage im Text stehen, desto besser.

# wie viele Wörter der Frage auch im Text stehen
def common(question, text):
ws = text.split(" ")
return sum(1 for word in question.split(" ") if word in ws)
for q in QUESTIONS:
print(q)
best = None
top = 0
for topic, text in DOCS:
n = common(q, text)
print(f" {topic:<10}{n}")
if n > top:
top = n
best = topic
print(" →", best or "keiner")
Ausgabe
wann kommt mein paket an
lieferung 0
rückgabe 0
zahlung 0
→ keiner
warum wurde meine abbuchung abgelehnt
lieferung 0
rückgabe 0
zahlung 0
→ keiner

Keine der beiden Fragen findet etwas. „paket“ und „abbuchung“ kommen in keinem Text vor. Und selbst „zahle“ im Text und „zahlung“ wären für das Programm zwei verschiedene Wörter. Kunden fragen mit ihren eigenen Wörtern, und die Texte in der Basis sind mit anderen geschrieben.

Gebraucht wird ein Vergleich der Bedeutung, nicht der Buchstaben.

Die Idee

Bedeutung als Richtung

Ein Wort erkennt man an der Gesellschaft, in der es steht. „paket“ und „bestellung“ stehen mit denselben Wörtern („kurier“, „tagen“), also bedeuten sie etwas Nahes, obwohl ihre Buchstaben verschieden sind.

Das schreibt man mit Zahlen auf. Jedes Wort wird zu einem Vektor: einer Liste von Zahlen, eine für jede Achse. Wörter mit naher Bedeutung bekommen nahe Vektoren, also Pfeile, die fast in dieselbe Richtung zeigen. Ein solcher Vektor, der für Bedeutung steht, heißt Embedding.

In der Lektion sind die Achsen die acht häufigsten Wörter im Text des Shops, und die Zahl für jede Achse ist, in wie vielen Sätzen das Wort zusammen mit dem Wort der Achse steht. Echte Modelle lernen die Vektoren mit einem neuronalen Netz und haben Hunderte oder Tausende Achsen, aber die Idee ist dieselbe: nahe Bedeutung, nahe Richtung.

Sieh es dir an

Wörter als Vektoren

Kurze Wörter wie „die“, „der“ und „mit“ stehen überall und sagen nichts über das Thema. Sie sind keine Achsen und gehen nicht in die Vektoren ein:

# die kurzen Wörter, die überall vorkommen und nichts über das Thema sagen
STOP = {
"die", "der", "das", "den", "dem", "des", "ein", "einen",
"eine", "einer", "deine", "deiner", "dir", "dich", "du", "und",
"oder", "aber", "ist", "war", "wird", "wurde", "hat", "bei",
"mit", "an", "in", "von", "zu", "zur", "ab", "bis",
"vor", "nach", "ich", "bin", "sehr", "kann", "kannst", "per",
"auf", "innerhalb", "zwei", "kam", "wann", "warum", "mein", "meine",
"meinen",
}

Der Raum: die Achsen und der Vektor eines Wortes. Der Vektor eines Textes ist die Summe der Vektoren seiner Wörter.

# die Wörter als Vektoren: eine Zahl für jede Achse
class Space:
# die Achsen: die häufigsten Wörter, ohne die kurzen
def __init__(self, text, stop, n):
self.text = text
self.stop = stop
freq = {}
for line in text:
for w in line.split(" "):
if w not in stop:
freq[w] = freq.get(w, 0) + 1
self.vocab = list(freq)
self.axes = sorted(freq, key=freq.get, reverse=True)[:n]
# in wie vielen Sätzen das Wort zusammen mit dem Wort jeder Achse steht
def of(self, word):
v = [0] * len(self.axes)
for line in self.text:
ws = line.split(" ")
if word not in ws:
continue
for w in ws:
if w in self.axes and w != word:
v[self.axes.index(w)] += 1
return v
# der Vektor eines Textes: die Summe der Vektoren seiner Wörter
def of_text(self, s):
v = [0] * len(self.axes)
for w in s.split(" "):
if w not in self.stop:
v = [x + y for x, y in zip(v, self.of(w))]
return v
space = Space(TEXT, STOP, 8)
print("Achsen:", " ".join(space.axes))
for word in ["paket", "bestellung", "zahlung"]:
print(f"{word:<12}{space.of(word)}")
Ausgabe
Achsen: lieferung kurier ware bestellung tagen karte paket zahlung
paket [0, 1, 0, 0, 1, 0, 0, 0]
bestellung [0, 2, 0, 0, 1, 0, 0, 0]
zahlung [0, 0, 0, 0, 0, 3, 0, 0]

„paket“ steht in einem Satz mit „kurier“ und in einem mit „tagen“. „bestellung“ steht zweimal mit „kurier“ und einmal mit „tagen“: Die Zahlen sind verschieden, die Richtung ist fast gleich. „zahlung“ steht nur mit „karte“: eine ganz andere Richtung.

Sieh es dir an

Ähnlichkeit: der Kosinus des Winkels

Wie nah zwei Vektoren sind, sagt der Winkel zwischen ihnen. Der Kosinus des Winkels ist 1, wenn sie in dieselbe Richtung zeigen, und 0, wenn sie keine Achse gemeinsam haben. Das Teilen durch die Längen sorgt dafür, dass nur die Richtung zählt.

# Kosinus des Winkels: 1 gleiche Richtung, 0 nichts gemeinsam
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a)
nb = sum(y * y for y in b)
if na == 0 or nb == 0:
return 0.0
return dot / (math.sqrt(na) * math.sqrt(nb))
space = Space(TEXT, STOP, 8)
v = space.of("paket")
print(f"paket und bestellung: {cosine(v, space.of('bestellung')):.3f}")
print(f"paket und zahlung: {cosine(v, space.of('zahlung')):.3f}")
others = [o for o in space.vocab if o != "paket"]
others.sort(key=lambda o: cosine(v, space.of(o)), reverse=True)
print("am nächsten zu paket:")
for o in others[:5]:
print(f" {o:<12}{cosine(v, space.of(o)):.3f}")
Ausgabe
paket und bestellung: 0.949
paket und zahlung: 0.000
am nächsten zu paket:
bestellung 0.949
lieferung 0.949
ware 0.707
fünf 0.707
erstattet 0.707

„paket“ und „bestellung“ haben 0,949: fast dieselbe Richtung. Mit „zahlung“ sind es 0,000. Die Wörter, die „paket“ am nächsten sind, sind „bestellung“ und „lieferung“, beide 0,949. Danach kommen „ware“, „fünf“ und „erstattet“ mit je 0,707: In einem kleinen Text ziehen auch Zufallsnachbarn mit.

Probier es aus

Sag die Ausgabe voraus

Wie groß ist der Kosinus von [1, 1, 0] und [1, 0, 0]?

print(f"{cosine([1, 1, 0], [1, 0, 0]):.2f}")
Antwort

Sieh es dir an

Suche nach Bedeutung

Jetzt werden die Frage und jeder Text der Basis zu Vektoren, und die Antwort ist der Text mit dem größten Kosinus:

space = Space(TEXT, STOP, 8)
for q in QUESTIONS:
v = space.of_text(q)
print(q)
best = DOCS[0]
top = -1.0
for topic, text in DOCS:
c = cosine(v, space.of_text(text))
print(f" {topic:<10}{c:.3f}")
if c > top:
top = c
best = (topic, text)
print(f" → {best[0]}: {best[1]}")
Ausgabe
wann kommt mein paket an
lieferung 0.738
rückgabe 0.510
zahlung 0.000
→ lieferung: die lieferung dauert bis zu zwei tagen
warum wurde meine abbuchung abgelehnt
lieferung 0.000
rückgabe 0.221
zahlung 0.894
→ zahlung: zahle mit karte oder per nachnahme

Die erste Frage findet „lieferung“ mit 0,738, „rückgabe“ hat 0,510: „kommt“ steht im Text auch in „die ware kommt in der originalverpackung an“. Die zweite findet „zahlung“ mit 0,894: „abgelehnt“ steht in einem Satz mit „karte“ und „zahlung“, und der Zahlungstext hat „karte“ und „nachnahme“. Keine der beiden Fragen hat ein Wort gemeinsam mit ihrer Antwort.

Hier ist die erste Frage als Richtungen. Die Vektoren haben 8 Dimensionen und das Bild ist flach, darum zeigt es nur den Winkel jedes Textes zur Frage:

# Kosinus des Winkels: 1 gleiche Richtung, 0 nichts gemeinsam
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a)
nb = sum(y * y for y in b)
if na == 0 or nb == 0:
return 0.0
return dot / (math.sqrt(na) * math.sqrt(nb))
space = Space(TEXT, STOP, 8)
for q in QUESTIONS:
v = space.of_text(q)
print(q)
best = DOCS[0]
top = -1.0
for topic, text in DOCS:
c = cosine(v, space.of_text(text))
print(f" {topic:<10}{c:.3f}")
if c > top:
top = c
best = (topic, text)
print(f" → {best[0]}: {best[1]}")
Schritt für Schritt, in Worten
  1. Die Frage „wann kommt mein paket an“ wird zu einem Vektor: [0, 2, 1, 0, 1, 0, 0, 0]. Das Bild behält nur den Winkel dazu.
  2. „zahlung“: Kosinus 0,000, Winkel 90°. Keine einzige gemeinsame Achse: Die Pfeile stehen im rechten Winkel.
  3. „rückgabe“: Kosinus 0,510, Winkel 59°. Einige gemeinsame Achsen, aber weit weg.
  4. „lieferung“: Kosinus 0,738, Winkel 42°. Der kleinste Winkel.
  5. Am nächsten liegt „lieferung“: „die lieferung dauert bis zu zwei tagen“. Kein einziges gemeinsames Wort mit der Frage.

Probier es aus

Ergänze

Der Kosinus teilt das Skalarprodukt durch die beiden Längen. Was kommt an die Stelle von ___?

return dot / (math.sqrt(na) * math.sqrt())

Probier es aus

Finde den Fehler

Ein Kollege rechnet die Ähnlichkeit schneller aus, bekommt aber 7 für die erste Frage und den Lieferungstext, und die Ähnlichkeit soll zwischen 0 und 1 liegen. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Attention als Idee

In der Lektion hat „karte“ überall einen Vektor. Aber in „zahle mit karte“ und „die karte der stadt“ bedeutet das Wort Verschiedenes: eine Bankkarte und einen Stadtplan.

Echte Modelle lösen das mit Attention (Aufmerksamkeit). Jedes Token schaut auf die anderen Tokens im Text und mischt ein wenig von ihnen in seinen eigenen Vektor, mehr von denen, die für es wichtig sind. „karte“ neben „zahle“ nimmt von „zahle“, neben „stadt“ von „stadt“, und die beiden Vektoren werden verschieden. Das wiederholt sich durch viele Schichten, und so trägt der Vektor jedes Tokens seine Bedeutung in genau diesem Satz. Der Vektor des ganzen Textes wird aus ihnen berechnet.

Hier ist Attention nur eine Idee: Wie sie genau berechnet wird, gehört nicht zum Kurs.

Die Idee

Wann es wehtut

  • Das kleine Modell kennt Wörter, nicht Bedeutung. Ein Kunde will etwas zurückgeben, und die Suche schickt ihn zur Lieferung:

    space = Space(TEXT, STOP, 8)
    for q in ["ich will die bestellung zurückgeben"]:
    v = space.of_text(q)
    print(q)
    best = DOCS[0]
    top = -1.0
    for topic, text in DOCS:
    c = cosine(v, space.of_text(text))
    print(f" {topic:<10}{c:.3f}")
    if c > top:
    top = c
    best = (topic, text)
    print(f" → {best[0]}: {best[1]}")
    Ausgabe
    ich will die bestellung zurückgeben
    lieferung 0.775
    rückgabe 0.573
    zahlung 0.000
    → lieferung: die lieferung dauert bis zu zwei tagen

    „will“ steht nicht im Text und sagt dem Modell nichts. „zurückgeben“ kommt nur in einem Satz vor, mit „ware“ und „tagen“; „zurück“ und „rückgabe“ sind für das Modell andere Wörter, so wie beim Vokabular in der Lektion Text in Stücken: Tokens. „bestellung“ steht im Text zweimal mit „kurier“ und einmal mit „tagen“. So wird der Vektor der Frage [0, 2, 1, 0, 2, 0, 0, 0] und zeigt zur Lieferung: „lieferung“ gewinnt mit 0,775, und „rückgabe“, die richtige Antwort, ist mit 0,573 Zweite. Bei einem anderen Text würde dieselbe Frage woanders landen: Die Fehlgriffe hängen vom Text ab.

    Ein echtes Embedding-Modell ist mit einer riesigen Menge Text trainiert. Es weiß, dass „zurückgeben“ und „Rückgabe“ zusammengehören, und es sieht die ganze Frage, nicht nur einzelne Wörter. Es kann sich trotzdem irren, aber viel seltener. Wie oft es den richtigen Text findet, wird gemessen: Im Modul zur Suche nach Bedeutung ist das recall@k, der Anteil der Fragen, deren richtiger Text unter den ersten k gefundenen ist.

  • Nah heißt nicht gleich. Wörter mit gegenteiliger Bedeutung stehen oft in denselben Sätzen:

    space = Space(TEXT, STOP, 8)
    for x, y in [["kostenlos", "kostet"], ["schnell", "spät"]]:
    print(f"{x} und {y}: {cosine(space.of(x), space.of(y)):.3f}")
    print("farbe:", space.of("farbe"))
    Ausgabe
    kostenlos und kostet: 0.707
    schnell und spät: 0.000
    farbe: [0, 0, 0, 0, 0, 0, 0, 0]

    „kostenlos“ und „kostet“ haben 0,707, weil beide neben „lieferung“ stehen. Echte Modelle machen das besser, aber „kann“ und „kann nicht“ kommen trotzdem nahe beieinander heraus.

  • Ein unbekanntes Wort ist null. „farbe“ steht nicht im Text, und sein Vektor besteht nur aus Nullen. Eine Frage nur mit unbekannten Wörtern hat 0,000 zu allem, aber die Suche liefert trotzdem irgendeinen Text. Darum braucht man eine Schwelle: siehe die Übung.

  • Die Vektoren kommen aus dem Text. Ein kleiner Text liefert Zufallsnachbarn („fünf“ neben „paket“). Verschiedene Modelle liefern verschiedene Vektoren, die sich nicht miteinander vergleichen lassen: Die Basis und die Frage müssen dasselbe Modell benutzen.

  • Viele Texte, viele Vektoren. Bei Tausenden Dokumenten werden die Vektoren in einer Datenbank gehalten, die die nächsten schnell findet: Vektordatenbanken. Die Suche nach Bedeutung über die Dokumente des Shops ist später im Kurs ein ganzes Modul. Wie man ein Embedding-Modell auf einem Server betreibt und den Index aktuell hält, ist Aufgabe des Kurses ZeroToOps.

Genauer, für Neugierige

Echte Embeddings. Embedding-Modelle nehmen Text und geben Hunderte oder Tausende Zahlen zurück. Sie werden wie andere Modelle über eine API aufgerufen oder lokal betrieben, meist aus Python. Die Vektoren haben meist die Länge 1, und dann ist der Kosinus einfach das Skalarprodukt.

Die Nullen auf den Achsen. In der Lektion zählt ein Wort seine eigene Achse nicht mit. „zahlung“ steht viermal im Text, hat aber an der Achse „zahlung“ eine 0: Sein Vektor ist [0, 0, 0, 0, 0, 3, 0, 0], nur „karte“ zählt. Zählte ein Wort auch seine eigene Achse, wäre das die größte Zahl in seinem Vektor, und es würde alles zu sich ziehen: „paket“ und „bestellung“ würden von 0,949 auf 0,129 fallen. Beide Entscheidungen sind Kompromisse des kleinen Modells; echte Modelle haben keine Achsen, die Wörter sind.

Warum Kosinus und nicht Abstand. Der Abstand zwischen den Spitzen hängt auch von der Länge ab, und die Länge ist hier, wie viele Wörter der Text hat. Der Kosinus schaut nur auf die Richtung. Bei Vektoren der Länge 1 geben beide dieselbe Reihenfolge.

Die nächsten k. Die Suche in der Lektion ist k-NN aus der Lektion über die nächsten Nachbarn mit k = 1: Nur die Vektoren und das Maß der Nähe sind andere.

In Python. math.sqrt ist die Quadratwurzel, wie Math.sqrt in Java und JavaScript. sum in Python addiert genauer als eine einfache Schleife, aber hier sind die Zahlen ganz, und das Ergebnis ist in allen vier Sprachen gleich.

Übung

Nicht sicher ist eine Antwort

Löse es in deinem Editor.

Der Bot darf nicht zufällig antworten. Schreibe eine Funktion, die den nächsten Text nur zurückgibt, wenn der Kosinus mindestens 0,5 ist, und sonst „nicht sicher, ich gebe an einen Kollegen weiter“. Probiere sie mit den beiden Fragen aus der Lektion und mit „kann ich eine andere farbe haben“.

Ausgabe
wann kommt mein paket an → die lieferung dauert bis zu zwei tagen
warum wurde meine abbuchung abgelehnt → zahle mit karte oder per nachnahme
kann ich eine andere farbe haben → nicht sicher, ich gebe an einen Kollegen weiter

Die dritte Frage hat 0,000 zu allem, und statt einer falschen Antwort gibt der Bot sie an einen Menschen weiter. Die Schwelle ist ein Kompromiss: Eine hohe Schwelle gibt Fragen weiter, auf die der Bot die Antwort kennt, und eine niedrige lässt falsche Antworten durch.

Weitere Aufgaben

  • Mehr Achsen. Baue den Raum mit 16 Achsen. Wie ändern sich die Ähnlichkeiten aus der Lektion?
  • Die ersten zwei. Gib für jede Frage die zwei nächsten Texte aus und wie weit der zweite vom ersten entfernt ist.

Test

Teste dich

1. Was ist ein Embedding?
2. Zwei Vektoren zeigen in dieselbe Richtung, aber einer ist dreimal so lang. Wie groß ist ihr Kosinus?
3. Warum findet die Suche nach Bedeutung für „wann kommt mein paket an“ den Text „die lieferung dauert bis zu zwei tagen“?
4. „kostenlos“ und „kostet“ haben eine Ähnlichkeit von 0,707. Was sagt das?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·