Veröffentlicht am

Prompts und GPTs – Programmieren ohne Programmiersprache

Prompts und GPTs

Programmieren ohne Programmiersprache – in OpenAIs GPT-Store blicken wir in die Zukunft. Dieser Artikel zeigt, wie wir ohne selbst eine Programmiersprache zu kennen, einen Lernassistenten zum Programmieren lernen erstellen. 

Das Schlüsselwort lautet Prompt Engineering – wir weisen die KI in natürlicher Sprache an. Dazu benötigen wir ein Large Language Model (LLM) also eine spezielle KI, die eingens zu diesem Zweck erstellt wurde. OpenAI bietet uns mit ChatGPT den Zugang zum LLM und im GPT-Store können wir eigene kleine Chatbots erstellen. Diese heißen GPTs.

‘GPT’ steht ja ursprünglich für ‘Generative Pretrained Transformer’. Was das ist, brauchen wir im folgenden nicht zu verstehen – ‘GPT’ wird hier eher verwendet als Bezeichnung für einen Typ App, den wir ohne Programmierkenntnisse bei OpenAI erstellen und allen OpenAI-Usern zur Verfügung stellen können.

Erstellen wir also unseren Lernassistenten zum Programmieren lernen.

GPT-Store aufrufen

Als Voraussetzung benötigen wir lediglich einen Account bei OpenAI. Die Kosten nach der Einführungszeit betragen momentan je nach Abo-Typ ca. USD 20 pro Monat.

Sind wir eingeloggt, dann finden wir oben rechts im Menu den Zugang zum GPT-Store.

GPT Store öffnen

Auf der nächsten Seite können wir GPTs durchstöbern, die von anderen bereits öffentlich zur Verfügung gestellt wurden.

Oder wir bauen ein eigenes GPT. Dazu klicken wir oben rechts auf die grüne Schaltfläche:

Eigenes GPT beginnen

Ein GPT konfigurieren

Wir werden belohnt mit einer Eingabemaske. Wir stellen sicher, dass ‘Configure’ eingestellt ist.

Unter ‘Name’ vergeben wir eine Bezeichnung für das GPT – ‘Programmieren lernen’.

Unter ‘Description’ erfassen wir eine kurze Beschreibung – ‘Lerne einfach eine neue Programmiersprache’ ist unser Beispiel.

Unter Instructions schreiben wir unseren Prompt. Mehr dazu im nächsten Abschnitt.

Unter Conversation Starters erfassen wir mindestens einen Satz, den die User des GPTs auswählen können, um einerseits das GPT zu starten und andererseits, dem GPT den notwendigen Kontext zu geben, um einen passenden Dialog zu führen.

Hier die Beispiele:

  • Ich möchte ohne Vorkenntnisse Python lernen.
  • Ich möchte ohne Vorkenntnisse Java lernen.
  • Ich möchte ohne Vorkenntnisse SQL lernen.
  • Ich möchte ohne Vorkenntnisse Spark DataFrames lernen.

Für dieses GPT benötigen wir unter ‘Knowledge’ und ‘Capabilities’ keine besonderen Eingaben und lassen die Vorgabe stehen. 

GPT-Store: Konfiguration

Ist alles erfasst, dann klicken wir unten auf Create new Action

Wir werden noch gefragt, wer das GPT sehen soll:

Wir wählen eine Option, mit der wir uns wohl fühlen und klicken dann auf ‘Share’.

GPT Store: Leserechte

Im nächsten Dialog klicken wir auf ‘View GPT’ und starten damit auch unser GPT.

Das GPT ausführen

Die Conversation Starters werden zur Auswahl angeboten. 

Klicken wir beispielsweise auf ‘Ich möchte ohne Vorkenntnisse SQL lernen’ – dann beginnt ChatGPT einen Dialog. 

Start GPT Dialog

Momentan generiert ChatGPT eine Antwort in Form einer allgemeinen Anleitung.

Hier ein Beispiel:

GPT-eigener Dialog

Prompt-Engineering: Das Verhalten des GPT beeinflussen

Jetzt verändern wir dieses Verhalten mit Prompt Engineering. Wir editieren das GPT.

Oben links finden wir ein Drop-Down-Menu und darin die Option ‘Edit GPT’.

GPT editieren

Damit finden wir zurück in den Editier-Modus und widmen uns jetzt intensiv dem Prompt im Eingabefeld ‘Instructions’. 

Jetzt entwickeln wir schrittweise unseren Prompt. Hier in Beispiel:

Den Kontext, also die Absicht, eine Programmiersprache zu lernen, gibt der User vor mit der Wahl des ‘Conversation Starters’.

Danach führt ChatGPT unseren Prompt durch. Dieser gibt weitere Anweisungen zum Verhalten des GPT und erweitern den Kontext

  • Dieses GPT soll Aufgaben  für Menschen ohne Programmierkenntnisse bereitstellen, die ihre Programme in der Programmiersprache schreiben wollen.
  • Dieses GPT spricht User mit Du an.

ChatCPT sollte sich während der ganzen Konversation daran erinnern. Wir werden im Gebrauch des GPT beobachten, dass es das manchmal vergisst – insbesondere die Anredeform.

Jetzt geben wir genaue Anweisung, wie das GPT vorgehen soll:

  • Das GPT generiert jeweils eine Aufgabe und zeigt sie dem Benutzer an, ohne die Lösung zu zeigen. Am Ende heißt es: Stelle eine Frage oder schreibe eine Lösung.

Führen wir das GPT aus, dann beobachten wir ein anderes Verhalten. Und es geht noch weiter:

Um ChatGPT mit didaktischen Fähigkeiten auszurüsten, überlegen wir uns, wie unsere User sich verhalten und wie das GPT – als geduldiger Lernassistent, darauf reagieren soll.

Im Beispiel unterscheiden wir drei Fälle:

  • Wenn der Benutzer eine Frage stellt, dann beantwortet das GPT die Frage. Am Ende heißt es: Stelleine Frage oder schreibe eine Lösung.
  • Wenn der Benutzer frustriert oder ungeduldig ist, wird dieses GPT einfühlsam und ermutigend antworten und sehr einfache Erklärungen zur aktuellen Aufgabe geben.
  • Wenn der Benutzer eine Lösung vorschlägt, prüft das GPT diese und gibt ein Feedback und eine korrekte Lösung. Am Ende sagt es: Wünschst du eine weitere Übung?

Hier der ganze Prompt:

Prompt Programmieren Lernen

Führen wir das GPT jetzt aus, dann beobachten wir ein völlig verändertes Verhalten.

Hier ein Beispiel:

GPT-Dialog Beispiel

Fazit

Generative KI eröffnet die Möglichkeit, das Verhalten einer App mit natürlicher Sprache zu steuern. 

Ich rechne damit, dass in Zukunft noch viel bessere Interaktionsmöglichkeiten zur Verfügung stehen werden. 

Diese werden einen großen Einfluss auf uns als Gesellschaft haben und sicher auch die Art und Weise beeinflussen, wie und womit wir lernen – es lohnt sich, jetzt in diese Welt einzusteigen. 

  • LLM-Tipps & Fachglossar

    Abonniere meinen Newsletter, erhalte regelmäßig Tipps und Tricks über den produktiven Einsatz von LLMs und ich schenke dir mein umfangreiches Fachglossar Von AI-Engineering bis Zero-Shot

  • Chatbot als Lernassistent
  • Prompt Engineering Personas und Wiederholungen
  • AI-Engineering-Fachglossar
  • EBook Tutorial: Cluster aus virtuellen Maschinen
  • Ebook: Apache ZooKeeper
  • Ebook: Realtime Streaming Pipelines
  • LSM-Trees: Log Structured Merge Trees
  • Aufbau einer Enterprise Search
  • Zeit Stream Analytics
  • B-Tree-Index in Datenbanken
  • Ordering Guarantee in Apache Kafka
  • CAP Theorem
  • MapReduce Funktionale Programmierung
  • Konzepte des HDFS
  • Optimistisches Concurrency Control
Veröffentlicht am

Sentence Embeddings für Vektordatenbanken

Semantisches Retrieval basiert auf Vektoren und Vektordatenbanken. Prominenteste Anwendung ist RAG – Retrieval Augmented Generation.
Wie verwandelt man einen Text in einen Vektor? In diesem Artikel beleuchten wir verschiedene Möglichkeiten.

Embeddings: Texte als Vektoren darstellen

Die Abbildung veranschaulicht das Vorgehen: Unser Text wird einem Modell präsentiert und als Ausgabe erhalten wir einen Vektor.

Schematische Darstellung Text zu Vektor

Je nach Anwendungsfall werden wir mit unterschiedlichen Embeddings arbeiten:

Bei Texten unterscheiden wir zwei Hauptkategorien:

Word Embeddings: Diese erkennen die semantische Bedeutung von Wörtern. Ähnliche Wörter, wie beispielsweise Synonyme, liegen im Vektorraum näher beisammen, als unähnliche Wörter.

Sentence Embeddings: Diese gehen weiter und sind in der Lage, die semantische Bedeutung eines Satzes oder Texts zu erfassen. Sie unterstützen dabei, Dokumente zu finden, die zur Suchfrage semantisch ähnlich sind. Semantisch ähnliche Sätze liegen im Vektorraum nah beieinander.

Wir konzentrieren uns in diesem Artikel auf Sentence Embeddings. Der Artikel Semantische Suche und Abstandsmetriken untersucht, welche Rolle Abstände zwischen Vektoren bei der Vektorsuche spielen.

Sentence Embeddings mit OpenAI

Das API von OpenAI ist einfach zu bedienen. Als erstes beschaffen wir einen API-Key und setzen diesen im folgenden Code-Snippet ein:

import requests
import json
api_key = ‘...’

def generate_embeddings_openai(text):
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}

data = {
"input": text,
"model": "text-embedding-ada-002",
"encoding_format": "float"
}

response = requests.post("https://api.openai.com/v1/embeddings", 
headers=headers, data=json.dumps(data))
return response.json()

text = "Heute ist ein schöner Tag"
embeddings = generate_embeddings_openai(text)

print(embeddings['data'][0]['embedding'][:10])

Die Ausgabe zeigt die 10 ersten Dimensionen des Embedding-Vektors:

[0.013132129, 0.023169499, 0.008860657, -0.037466574, 
0.004586243, 0.011372942, -0.010637495, 0.0057717822, 
-0.0012745284, -0.026240723]

Wir ermitteln die Länge des Vektors:

len(embeddings['data'][0]['embedding'])
1536

Das Modell text-embedding-ada-002 von OpenAI liefert also 1536-dimensionale Vektoren – ganz unabhängig davon, wie lang unser Text war.

text-embedding-ada-002 ist eines von vielen Modellen, die OpenAI anbietet.

OpenAI ist kostenpflichtig und wer längere Zeit damit experimentieren möchte, belastet bald die Kreditkarte.

Es gibt viele Alternativen.

Huggingface Modelle

Auf Huggingface werden Open-Source-Modelle gehostet. Uns interessieren Sentence-Transformer-Modelle.
Auch hier kommen wir mit wenigen Zeilen zu einem Embedding:

import requests
from sentence_transformers import SentenceTransformer
import torch

MODEL_NAME = 'all-MiniLM-L6-v2'

model = SentenceTransformer(MODEL_NAME)

Im Unterschied zu OpenAI laden wir das Modell auf unser Laptop und führen die Embedding-Methode lokal aus – schicken unseren Text also nicht zu einem Anbieter wie OpenAI.

Wir wählen das Modell all-MiniLM-L6-v2, eines von vielen, das auf Huggingface gehostet wird.

Wir können herausfinden, ob wir auf dem Laptop mit der GPU arbeiten:

if torch.cuda.is_available():
model = model.to(torch.device("cuda"))
print(model.device)

Für unser Mini-Beispiel reicht eine CPU bestens.

Jetzt generieren wir das Embedding:

text = "Heute ist ein schöner Tag"
embeddings = generate_embeddings_pytorch(text)

Und wir schauen wiederum die ersten 10 Dimensionen sowie die Gesamtlänge des Vektors an:

print(embeddings[:10])
print(len(embeddings))
[-0.05164861 0.13550845 -0.03804124 -0.02148658 0.00825853 0.10632604
0.06337947 0.0586526 0.02057826 -0.03433402]
384

Bei dieser Variante erhalten wir also Vektoren mit 384 Dimensionen.

Das geht ganz schmerzlos, nicht wahr.

Beide API-Calls, die wir bis hierher betrachtet haben, vereinfachen das Leben und nehmen uns viele Entscheidungen ab. Wir müssen uns lediglich für das Modell entscheiden – und es gibt viele Modelle, sie wurden auf verschiedenen Texten mit unterschiedlichen Zielen trainiert.

Schauen wir eine komplexere aber flexiblere Variante an, wie wir zu Sentence Embeddings kommen:

Huggingface mit PyTorch

Mit der Python-Library PyTorch können wir die Art und Weise steuern,  wie die Sentence-Embeddings erstellt werden:

from transformers import AutoTokenizer, AutoModel
import torch

text = "Heute ist ein schöner Tag."
model_name='sentence-transformers/all-MiniLM-L6-v2'

Wir verwenden wiederum dasselbe Modell von Huggingface, wie im ersten Beispiel.

Als erstes wenden wir einen Tokenizer an. Dabei müssen wir denselben Tokenizer verwenden, der beim Training des gewählten Modells eingesetzt wurde. Wir können den Tokenizer beim Modell ermitteln und gleich auf unseren Text anwenden – mit return_tensors="pt" bestellen wir das Ergebnis im PyTorch-Format.

tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

inputs = tokenizer(text, return_tensors="pt")
inputs

Hier der Output des Tokenizers:

{'input_ids': [101, 2002, 10421, 21541, 16417, 8040, 27406, 2099, 6415, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

Interessant ist die erste Liste: diese enthält die Tokens – das Bild zeigt schematisch, wie ein Tokenizer funktioniert:

Tokenizer für Sentence Embeddings

Der Input Text wird nach einem bestimmten Algorithmus – dem Tokenizer – in Wörter und falls nötig, in Subwörter zerhackt. Ergebnis ist eine Liste von Tokens. Wie ein Tokenizer genau funktioniert, werden wir in einem späteren Artikel beleuchten. An dieser Stelle geht es darum, den Zusammenhang zwischen dem Tokenizer und den Embeddings herauszuarbeiten.

Die Tokens sind durchnummeriert und liegen in einer Tabelle vor. Das Ergebnis des Tokenizers ist eine Liste mit diesen Tokennummern. Was wir sehen, ist jeweils die Tokennummer. Dazu gehören auch spezielle Tokens, die das Modell verwendet, beispielsweise [CLS], [SEP].

Aus unserem Satz mit 5 Wörtern sind jetzt also 11 Tokens geworden: Wörter, Subwörter und die speziellen Tokens. Wir beleuchten Tokenizers und im übernächsten Abschnitt noch näher.

Jetzt generieren wir das Embedding:

with torch.no_grad():
    outputs = model(**inputs)

Das Ergebnis finden wir im Tensor outputs.last_hidden_state.
Schauen wir die Form des Tensors an:

print(outputs.last_hidden_state.shape)
torch.Size([1, 11, 384])

outputs.last_hidden_state ist ein Tensor mit den folgenden Dimensionen:

  • batch_size: Anzahl verarbeiteter Texte. In unserem Beispiel wurde 1 Text verarbeitet, batch_size ist also 1.
  •  sequence_length: Anzahl Tokens im verarbeiteten Text, inklusive derjenigen Tokens die der Tokenizer hinzugefügt hat (beispielsweise [CLS], [SEP], in unserem Beispiel sind es 11 Tokens.
  •  hidden_size: die Größe des Hidden Layers. Sie hängt vom gewählten Modell ab. In unserem Beispiel: 384

Wir möchten jedoch nur einen einzelnen Vektor haben. Das erreichen wir mit dem folgenden Befehl. Im nächsten Abschnitt zum Pooling beleuchten wir den Befehl näher.

embeddings = outputs.last_hidden_state.mean(dim=1).squeeze().numpy()

Wir schauen uns wieder die 10 ersten Dimensionen des Vektors sowie dessen Gesamtlänge an:

print(embeddings[:10])
print(len(embeddings))

Pooling

Wie ist das Embedding entstanden? Untersuchen wir die Form Tensors im Last Hidden State:

Der Tensor des Last Hidden State und der Effekt des Poolings

Die Batch-Size ist bei uns eins, weil wir nur einen Satz verwendet haben.

Um aus diesem Tensor einen einzelnen Vektor zu erhalten, konzentrieren wir uns auf die Dimension 1 – diese enthält einen 384-dimensionalen Vektor für jedes der 11 Tokens.

Für unseren Satz aggregieren wir die Informationen der Tokens auf einen Vektor der Länge hidden size, also 384.

embeddings = outputs.last_hidden_state.mean(dim=1).squeeze().numpy()

Mit .mean(dim=1) bilden wir den Durschnitt im Tensor dieser zweiten Dimension (also dim=1) entlang. Damit bilden wir den Durchschnitt der Hidden States über alle Token in der Sequenz.

Wir aggregieren also die Information aller Tokens in einen einzelnen Vektor für den Input-Text. Damit erhalten wir immer einen Vektor derselben Dimension, unabhängig davon, wie groß er Input-Text ursprünglich war. Man spricht von ‘Pooling‘.

Die Codezeile nimmt also die Ausgabe des Transformer-Modells, konzentriert sich auf den letzten Hidden State, führt ein Mean-Pooling über die Wörter durch, um einen einzelnen Sentence-Embedding-Vektor zu erhalten.

Wir können auch das Maximum bilden, oder die Summe oder einfach das letzte Token nehmen oder einen eigenen Attention Pooling Mechanismus bauen, der für unsere Texte am besten geeignet ist.

Oder wir verlassen uns einfach auf den vereinfachten API-Aufruf im Abschnitt Huggingface Modelle.

Der Tokenizer

Untersuchen wir den Tokenizer noch genauer. Wir haben ja schon gesehen, wie er geladen wird und jetzt untersuchen wir, welcher Tokenizer verwendet wird:

tokenizer = AutoTokenizer.from_pretrained(model_name)
print(tokenizer)
BertTokenizerFast(name_or_path='sentence-transformers/all-MiniLM-L6-v2', vocab_size=30522, model_max_length=512, is_fast=True, 
padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 
'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}, clean_up_tokenization_spaces=True), added_tokens_decoder={
0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
}

Wir sehen, dass der BertTokenizerFast verwendet wird und dass er ein Vokabular von 30522 Tokens umfasst. Die speziellen Tokens werden zusammen mit ihrem Index benannt.
[SEP], beispielsweise, hat Index 102.

Das gesamte Vokabular können wir auch laden:

vocab = tokenizer.get_vocab()

und ausgeben – hier nur die ersten Einträge:

for token, token_id in list(vocab.items())[:10]:
    print(f'{token}: {token_id}')
##的: 30442
ann: 5754
goldberg: 18522
merton: 28169
##ich: 7033
nowadays: 13367
authored: 8786
♦: 1626
teaches: 12011
sabha: 11200

Wir sehen hauptsächlich englischsprachige Tokens. Unser Text ist aber deutsch.

Schauen wir uns die Tokens an, die im Beispiel ermittelt wurden:

for id in inputs['input_ids'][0]:
    for token, token_id in list(vocab.items()): 
        if (token_id == id.item()):
            print(f'{token}: {token_id}')
[CLS]: 101
he: 2002
##ute: 10421
ist: 21541
ein: 16417
sc: 8040
##hone: 27406
##r: 2099
tag: 6415
.: 1012
[SEP]: 102

Nur die wenigsten Wörter sind ganz geblieben. Das englische ‘tag’ und das deutsche ‘Tag’ haben eine völlig unterschiedliche Bedeutung.

Wir würden ein deutschsprachiges Vocabular vorziehen: Hier ein Beispiel:

from transformers import BertTokenizerFast

# Den Tokenizer des deutschen BERT Models laden
tokenizer = BertTokenizerFast.from_pretrained('bert-base-german-dbmdz-cased')

# Das Vokabular laden
vocab = tokenizer.get_vocab()

# Die ersten 10 Einträge anschauen
for token, token_id in list(vocab.items())[:10]: 
    print(f'{token}: {token_id}')
Beute: 17401
Dunkelheit: 22903
geborgen: 29079
Erl: 8179
Straub: 30300
##utzer: 17308
Fan: 11085
Museen: 14632
Statue: 22955
Elektrom: 22179

Für einen deutschsprachigen Text liegt es nahe, ein Modell zu verwenden, das mit einem deutschen Vokabular trainiert wurde.

Deutschsprachiges Modell verwenden

from transformers import AutoModel, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("dbmdz/bert-base-german-cased")
model = AutoModel.from_pretrained("dbmdz/bert-base-german-cased")
text = "Heute ist ein schöner Tag."
inputs = tokenizer(text, return_tensors="pt")

with torch.no_grad(): 
outputs = model(**inputs)

print(outputs.last_hidden_state.shape)
embeddings = outputs.last_hidden_state.mean(dim=1).squeeze().numpy()

type(embeddings)
list(embeddings)[:10]

Schauen wir den tokenisierten Text an:

print(inputs)
{'input_ids': tensor([[  102,  4275,   215,   143, 16460,  1143,   566,   103]]), 
'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])}

Schlagen wir die IDs im Vokabular nach:

for id in inputs['input_ids'][0]:
    for token, token_id in list(vocab.items()): 
        if (token_id == id.item()):
            print(f'{token}: {token_id}')
[CLS]: 102
Heute: 4275
ist: 215
ein: 143
schöner: 16460
Tag: 1143
.: 566
[SEP]: 103

Das entspricht unserem Satz.

Die Embeddings poolen wir aus dem letzten Hidden Layer wie schon in den vorherigen Beispielen erläutert.

Fazit und Ausblick

Wir haben gesehen, wie wir aus einem Text einen Vektor, also ein Embedding generieren, um dieses in einer geeigneten Datenbank zu speichern. Dabei haben wir vorgefertigte API-Calls bei einem Anbieter wie OpenAI verwendet und einfache API-Aufrufe mit heruntergeladenen Open-Source-Modellen Huggingface verwendet. Mit  einer Library wie PyTorch eröffnen sich uns viele Wahlmöglichkeiten. Wir haben Tokenizer und Pooling mit Beispielen betrachtet.

Dabei bleiben viele Fragen offen:

  • Wie unterteilen wir einen Text in geeignete Teile – Chunks – um mit der semantischen Suche erfolgreich zu sein?
  • Welches ist das am besten geeignete Modell für unseren Text?
  • Gibt es bessere Poolings.?
  • Wie speichern wir die Vektoren in einer Vektordatenbank?
  • Wie wird die Vektordatenbank abgefragt?
  • Wie bewerten wir die beste Konfiguration für unsere Suche?
  • LLM-Tipps & Fachglossar

    Abonniere meinen Newsletter, erhalte regelmäßig Tipps und Tricks über den produktiven Einsatz von LLMs und ich schenke dir mein umfangreiches Fachglossar Von AI-Engineering bis Zero-Shot

  • Chatbot als Lernassistent
  • Prompt Engineering Personas und Wiederholungen
  • AI-Engineering-Fachglossar
  • EBook Tutorial: Cluster aus virtuellen Maschinen
  • Ebook: Apache ZooKeeper
  • Ebook: Realtime Streaming Pipelines
  • LSM-Trees: Log Structured Merge Trees
  • Aufbau einer Enterprise Search
  • Zeit Stream Analytics
  • B-Tree-Index in Datenbanken
  • Ordering Guarantee in Apache Kafka
  • CAP Theorem
  • MapReduce Funktionale Programmierung
  • Konzepte des HDFS
  • Optimistisches Concurrency Control
Veröffentlicht am

Bridging the Gap: How LLMs are Transforming Traditional Enterprise Search

How LLM transforms traditional enterprise Search

Our vision is clear: We want to talk to documents: Ask a simple question and get an accurate answer. No more typing keywords and scrolling through hit lists.

It has only been a year since ChatGPT blew us away. It’s capabilities are hilarious and we wonder how soon traditional enterprise search systems will be replaced by conversational systems?

In this article we will identify some challenges to be solved before traditional Enterprise Search will be the past and we will identify a promising way to early start to experiment with these new technologies.

I have a strong background in information retrieval and have helped build several search engines in the past hence have solid experience in building search engines.

When I first heard about chatbots, I did not make the relation to search systems at first. Then I heard about Retrieval Augmented Search (RAG) and of course I was curious about the new possibilities.

I explored if  RAG and LLM systems are capable to replace current search engines namely in enterprise search.
What I found is a whole new world that unfortunately uses the same vocabulary as the traditional world. This makes it very challenging to explain traditional enterprise search to the younger specialist who have a strong data science background.

And vice versa, it is challenging to explain to the traditional search specialists how retrieval augmented search really works.

Bag-of-Words vs Word Embeddings

Traditional systems follow the bag-of-words paradigm. Text is broken down into words, which are transformed into tokens to optimize search results. A very simple example: The user searches for ‘dog’ and expects all documents containing ‘dogs’.

What about multilingual documents? Do we expect all documents containing “Hunde” to be found when the user searches with the keyword “dog”?

A good traditional search engine may even provide this functionality. To obtain this a lot of configuration and evaluation must be done.

However, the bag-of-words approach only allows us to search for keywords – sometimes exact phrases. Good traditional systems go even further and find documents that do not contain the keyword, but related terms. This is achieved by knowledge graphs and similar knowledge structures. But it remains a bag-of-words.

The new LLM based approach relies on word embeddings, which are able to capture more context than a simple word. To achieve this effect, we need a language model – LLM.

The creators of Eliza tried to build an intelligent chatbot back in 1964. From today’s perspective, it would be an exaggeration to call the approach used there an LLM. At the time, it seemed revolutionary.

Since then, a lot of work has been done on natural language processing. today’s llm shows previously unimaginable results. BERT was one of the first to use neural networks to capture even long-range dependencies in text. BERT stands for Bidirectional Encoder Representations from Transforms and was launched in October 2018.  Only five years have passed since then.

I think it is fair to say that it is a very young technology. But it is developing very fast. This young age also implies that we lack reliable experience.

Similarity, Retrieval and Ranking

Retrieval describes the process of finding all documents that match the query, i.e. the user’s question.

Traditional systems are based on keyword search and the bag-of-words model.

  • First: all documents matching all query-keywords are found, enhanced by optimized design such as spell checking, knowledge graphs, etc.
  • Second, these documents are ranked using a specific algorithm to produce an ordered list – the best document on top.
  • Third, the list is presented to the user, at best with some text snippets and highlighted keywords.

BM25 is one of the ranking algorithms often implemented in traditional search engines. It is based on the frequency of terms, both in the retrieved documents and in the entire collection.

The concept of cosine similarity has been around for a long time, even in traditional search.

  • We represent both as vectors: The query and all documents.
  • Then we compute the cosine of the angle of the query vector with each document vector.
  • The document with the smallest cosine is the most relevant document with respect to the query.

Computing the cosine in a vector space is quite straightforward. However, computing the cosine with all documents in a long hit list in the sparse vector space is quite time-consuming. For this reason, cosine similarity is  not widely used in traditional search systems.

However, dense vectors have much smaller dimensions, allowing vector products to be computed in reasonable time. Cosine similarity is used for dense vector retrieval.

Vector Databases vs Inverted Index

We use some database to store our documents, index them and search in these document database.

Traditional search systems rely on an inverted index and a forward index: they are the base of the bag-of-word model and on keyword search. Good data structures and retrieval algorithms have been developed in the past decades and they enhance quality search engines even for millions of documents.

Unfortunately these databases do not work with dense vectors. We need a new generation of databases – vector database management systems VDBMS are popping up like mushrooms – both as completely new systems and on top of existing databases.

New algorithms allow vector search even in log(n) order, n being the number of vectors in the database.

Data Design for a Search System

High-quality traditional search engines allow for very sophisticated indexing: They look at the structure of the text in a collection and identify common elements as “fields”.
Examples of typical fields: Title, Authors, Date, Text, and a rich collection of metadata to enhance the quality of the retrieved documents.

For each field, we design the best processing chain to create an index for that field. This might be a tokenizer to split words into tokens, remove stop words, enrich synonyms, build word stems and enrich documents by metadata. And we define the best query and retrieval processing to find the best results that match the user’s query. In the end the tokens in the documents are matched with the tokens in the query.

With LLM-based retrieval, this seems to be a very different process, as it calculates the similarity of the query on a dense vector bases, which consists in numbers and not in tokens.

The question remains: Do we still rely on fields in a database for LLM-based retrieval? Or will we use a much more finely structured one, as described in the following chapter on Retrieval Augmented Generation?

If so, what are the advantages of LLM over traditional search?

The field is too young to answer this question definitively.

Strength or LLM Based Systems

There is one thing that LLM-based techniques allow that traditional systems do not: conversation with documents. No keywords and hit lists, but natural language questions and answers.

Together with speech-to-text and text-to-speech systems, we will be able to literally talk to documents, even in foreign languages, such as Chinese.

Retrieval Augmented Generation

How is this conversation being implemented? Let us take a look at what is currently being discussed as new solutions to the old retrieval problem.

  • The user enters a query in natural language, procuring also a handful of documents, e.g. ten documents taken from some manual.
  • The system embeds the query to obtain a vector.
  • The system also reads the ten documents and splits them into appropriate snippets.
  • Each snippet is embedded to obtain a vector.
  • All these vectors are stored in a vector database.
  • The query vector is used to find the most similar snippets with some similarity measure, such as cosine similarity.
  • And with the help of a Generative Pretrained Transformer, such as ChatGPT, these are used to generate a natural language answer that is presented to the user.

This leaves us with the question of the appropriate snippet. To generate a natural language answer, we need very concise information. ‘Trash in trash out’ is more valid than ever.

Let us look at the simplest approach. We take snippets of three sentences – so we form a sliding window of three sentences to scroll over the text. Each triplet is embedded to its own vector. Sometimes five-sentence windows work better – or a more sophisticated method is needed.

We do not embed a whole document or a field of a document as a vector – we embed much finer structures, e.g. sentences or a hierarchy of text chunks.

At this point, we cannot answer the question of what granularity works best. We simply lack experience – remember, it was only one year ago that we were overwhelmed by ChatgGPT.

Select the best LLM and the best GPT

To implement RAG we use a large language model LLM and also a generative pretrained transformer GPT. We face an emerging market – both open source and commercial – for these new technologies. Which is the best choice? Digging in Hugging Face soon shows, that there are models trained by one person or models trained by big companies.

To obtain best results we need a model trained on data of a similar context to ours. A model trained on data about medicine will serve better results for medical RAG-engine than a model trained on other data about computer science.

We lack on experience in evaluating the best models for our purpose.

Quality

A good search engine finds all the documents we are looking for and presents the best ones at the top of a list.

Implementing a traditional search engine is far from trivial and is often underestimated, resulting in a low quality engine.

During the development of a traditional search engine, we measure the progress of quality. To do this, we generate or procure a gold standard and measure the system against it in terms of precision recall and F1 score, or even look at a ranked list and measure MAP, MRR, NDCG or similar metrics.

The same could be done for the retrieval part in RAG. But what is the gold standard?

And how do we measure the quality of the generation part?

Does the system generate additional information? Does it really take into account just the snippets we found in our own documents or does it take into account information from its original training on other data?  How much truth is actually in the answer?

The system’s answer will sound perfect, but can we believe it and even make important business decisions based on it?

Can we actually use a generative AI to evaluate the response of a generative AI, as some are proposing? Or is this equivalent to evaluating a student by another student instead of evaluating a student by an expert teacher?

These are still open questions to be answered by science.

Conclusion

There are no obvious advantages of LLM-based vector retrieval over traditional search systems. This may change in the near future as we gain more insight into the emerging search paradigm.

As far as search is concerned, we have a clear idea of what might be useful in an enterprise context. We are still a long way to systems that provide reliable answers for enterprise use.

However, an experimental use of RAG in addition to an existing traditional search system could provide valuable insights into the productive use of these new technologies in an enterprise context:

Using RAG to talk to the top-ranked documents in a traditional enterprise search seems to be a promising approach to gain experience with these technologies.

  • LLM-Tipps & Fachglossar

    Abonniere meinen Newsletter, erhalte regelmäßig Tipps und Tricks über den produktiven Einsatz von LLMs und ich schenke dir mein umfangreiches Fachglossar Von AI-Engineering bis Zero-Shot

  • Chatbot als Lernassistent
  • Prompt Engineering Personas und Wiederholungen
  • AI-Engineering-Fachglossar
  • EBook Tutorial: Cluster aus virtuellen Maschinen
  • Ebook: Apache ZooKeeper
  • Ebook: Realtime Streaming Pipelines
  • LSM-Trees: Log Structured Merge Trees
  • Aufbau einer Enterprise Search
  • Zeit Stream Analytics
  • B-Tree-Index in Datenbanken
  • Ordering Guarantee in Apache Kafka
  • CAP Theorem
  • MapReduce Funktionale Programmierung
  • Konzepte des HDFS
  • Optimistisches Concurrency Control