Eine der häufigsten Fragen, die ich von Kunden bekomme, lautet: „Wie kann ich einen benutzerdefinierten Chatbot mit meinen Daten erstellen?“ Während die Entwicklung eines solchen Chatbots vor 6 Monaten noch Monate dauerte, ist das heute nicht mehr unbedingt der Fall. In diesem Artikel stelle ich eine Schritt-für-Schritt-Anleitung vor, wie man eine benutzerdefinierte KI mithilfe der OpenAI-Assistenten und Feinabstimmungs-APIs erstellt. Für jeden Ansatz wird Python-Beispielcode bereitgestellt.
Chatbot vs. Assistent
Bevor ich auf den Beispielcode eingehe, möchte ich kurz zwischen einem KI-Chatbot und einem Assistenten unterscheiden. Obwohl diese Begriffe oft synonym verwendet werden, bezeichne ich sie hier für unterschiedliche Dinge.
Ein Chatbot ist eine KI, mit der man eine Unterhaltung führen kann, während ein KI-Assistent ein Chatbot ist, der Tools verwenden kann. Ein Tool kann z. B. das Surfen im Internet, ein Taschenrechner, ein Python-Interpreter oder etwas anderes sein, das die Fähigkeiten eines Chatbots erweitert [1].
Wenn Sie z. B. die kostenlose Version von ChatGPT verwenden, handelt es sich um einen Chatbot, da er nur über eine grundlegende Chatfunktion verfügt. Wenn Sie jedoch die Premium-Version von ChatGPT verwenden, handelt es sich um einen Assistenten, da er über Funktionen wie Webbrowsing, Wissensabfrage und Bilderzeugung verfügt.
Assistenten-API
Die Idee, KI-Assistenten (d.h. KI-Agenten) zu entwickeln, ist zwar nicht neu, aber die neue Assistenten-API von OpenAI bietet eine unkomplizierte Möglichkeit, diese Art von KI zu erstellen. Hier werde ich die API verwenden, um einen YouTube-Kommentar-Responder zu erstellen, der mit Wissensabfrage (d.h. RAG) aus einem meiner Medium-Artikel ausgestattet ist. Der folgende Beispielcode ist im GitHub-Repository dieses Beitrags verfügbar.
Vanilla-Assistent
Wir beginnen mit dem Importieren von Python-Bibliotheken und dem Einrichten der Kommunikation mit der OpenAI-API.
from openai import OpenAI
from sk import my_sk # import secret key from .py file
client = OpenAI(api_key=my_sk)
Beachten Sie, dass Sie für diesen Schritt einen OpenAI-API-Schlüssel benötigen. Wenn Sie keinen API-Schlüssel haben oder nicht wissen, wie Sie einen bekommen, habe ich in einem früheren Artikel beschrieben, wie man das macht. Hier habe ich meinen geheimen Schlüssel in einer separaten Python-Datei namens sk.py definiert, die im obigen Codeblock importiert wurde.
Jetzt können wir einen einfachen Assistenten erstellen (technisch gesehen einen Chatbot, da es noch keine Tools gibt). Dies kann in einer einzigen Codezeile geschehen, aber ich verwende aus Gründen der Lesbarkeit ein paar mehr.
intstructions_string = "ShawGPT, functioning as a virtual data science \
consultant on YouTube, communicates in clear, accessible language, escalating \
to technical depth upon request. \
It reacts to feedback aptly and concludes with its signature '–ShawGPT'. \
ShawGPT will tailor the length of its responses to match the viewer's comment, \
providing concise acknowledgments to brief expressions of gratitude or \
feedback, thus keeping the interaction natural and engaging."
assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string,
model="gpt-4-0125-preview"
)
Wie oben dargestellt, können wir den Namen des Assistenten, die Beschreibung, die Anweisungen und das Modell festlegen. Die Eingaben, die für die Leistung des Assistenten am wichtigsten sind, sind die Anweisungen und das Modell. Die Entwicklung guter Anweisungen (d. h. Prompt-Engineering) ist ein iterativer Prozess, für den es sich lohnt, einige Zeit aufzuwenden. Außerdem verwende ich die neueste verfügbare Version von GPT‑4. Es sind jedoch auch ältere (und billigere) Modelle erhältlich [2].
Nachdem der „Assistent“ eingerichtet ist, können wir ihm eine Nachricht schicken, um eine Antwort zu erzeugen. Dies geschieht im folgenden Codeblock.
# create thread (i.e. object that handles conversation between user and assistant)
thread = client.beta.threads.create()
# add a user message to the thread
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="Great content, thank you!"
)
# send message to assistant to generate a response
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
)
In dem obigen Codeblock passieren einige Dinge. Zunächst erstellen wir ein Thread-Objekt. Damit wird der Nachrichtenaustausch zwischen dem Benutzer und dem Assistenten abgewickelt, so dass wir dafür keinen Boilerplate-Code schreiben müssen. Als nächstes fügen wir dem Thread eine Benutzernachricht hinzu. Dies sind die YouTube-Kommentare für unseren Anwendungsfall. Schließlich senden wir den Thread an den Assistenten, um über das Run-Objekt eine Antwort zu erzeugen.
Nach ein paar Sekunden erhalten wir die folgende Antwort vom Assistenten:
You're welcome! I'm glad you found it helpful. If you have any more questions
or topics you're curious about, feel free to ask. –ShawGPT
Das mag zwar wie eine nette Antwort klingen, aber das ist nichts, was ich jemals sagen würde. Schauen wir uns an, wie wir den Assistenten durch das so genannte „few-shot prompting“ verbessern können.
Few-Shot-Prompting
Beim Few-shot Prompting fügen wir den Anweisungen des Assistenten Beispiele für Eingaben und Ausgaben hinzu, aus denen er lernen kann. Hier füge ich 3 (echte) Kommentare und Antworten an die vorherige Anweisung an.
intstructions_string_few_shot = """ShawGPT, functioning as a virtual data \
science consultant on YouTube, communicates in clear, accessible language, \
escalating to technical depth upon request. \
It reacts to feedback aptly and concludes with its signature '–ShawGPT'. \
ShawGPT will tailor the length of its responses to match the viewer's comment, \
providing concise acknowledgments to brief expressions of gratitude or \
feedback, thus keeping the interaction natural and engaging.
Here are examples of ShawGPT responding to viewer comments.
Viewer comment: This was a very thorough introduction to LLMs and answered many questions I had. Thank you.
ShawGPT: Great to hear, glad it was helpful :) -ShawGPT
Viewer comment: Epic, very useful for my BCI class
ShawGPT: Thanks, glad to hear! -ShawGPT
Viewer comment: Honestly the most straightforward explanation I've ever watched. Super excellent work Shaw. Thank you. It's so rare to find good communicators like you!
ShawGPT: Thanks, glad it was clear -ShawGPT"""
Beachten Sie, dass ich jedes Beispiel mit dem Zusatz „- ShawGPT“ versehen habe (auch wenn sie nicht Teil meiner eigentlichen Antwort waren). Dies ist wichtig, um das gewünschte Verhalten des Assistenten zu erreichen.
Mit den aktualisierten Anweisungen können wir einen neuen Assistenten auf die gleiche Weise wie zuvor erstellen.
assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string_few_shot,
model="gpt-4-0125-preview"
)
Wenn wir denselben Testkommentar (z. B. „Great content, thank you!“) wie zuvor an diesen Assistenten weitergeben, erhalten wir nun die folgende Antwort:
You're welcome, happy to hear you found it useful! -ShawGPT
Das ist zwar (noch) etwas länger als meine typische Antwort, aber viel besser als das, was wir mit dem Vanilla Assistant gesehen haben.
Versuchen wir es mit einer anderen (eher technischen) Frage. Wir können dieselbe Syntax wie zuvor verwenden, um die Benutzernachricht an den Assistenten zu übermitteln, aber jetzt fragen wir: „Was ist Fat-Tailedness?“
Hier ist die Antwort des Assistenten.
Fat-tailedness is a concept from statistics that refers to the properties of a
particular type of probability distribution. In a fat-tailed distribution, the
tails (the far ends of the distribution, representing extreme values far from
the mean) are heavier or "fatter" than they would be in a normal (Gaussian)
distribution. This means there's a higher likelihood of extreme outcomes
occurring compared to what you would expect if the data followed a normal
distribution.
In practical terms, if a dataset is described as having a fat-tailed
distribution, it indicates that rare, extreme events (the ones in the tails)
happen more frequently than would be predicted by models based on normal
distributions. This concept is extremely important in fields like finance,
economics, and risk management, where underestimating the likelihood of extreme
events can have significant consequences.
Fat-tailed distributions contrast with thin-tailed distributions, like the
normal distribution, where extreme values are less likely, and most of the
probabilities are concentrated around the mean. Understanding whether a dataset
follows a fat-tailed distribution can significantly impact how we model and
predict phenomena, especially when it comes to assessing risks and anticipating
rare but impactful events. -ShawGPT
Auch wenn diese Antwort (wieder) vernünftig ist, liest sie sich eher wie etwas aus ChatGPT als das, was ich sagen würde. Sie verwendet nämlich eine andere Definition von „fat tails“ (d. h. eine Verteilung mit stärkeren Ausläufern als eine Normalverteilung) als die, die ich in meiner Serie zu diesem Thema verwende (d. h. eine Verteilung, die von extremen Ereignissen bestimmt wird).
Um diesen Aspekt der Antworten des Assistenten zu verbessern, können wir ihm die Möglichkeit geben, auf Wissen aus einem Medium-Artikel zuzugreifen, den ich über Fat-Tailedness geschrieben habe.
Abrufen von Wissen
Während die Einrichtung eines RAG-Systems aus einer Handvoll nicht trivialer Schritte besteht (z. B. Chunking von Dokumenten, Einrichtung einer Vektordatenbank, Aufbau einer semantischen Suchfunktion, Zusammenführung von Suchergebnissen in einem Kontextfenster), wird der gesamte Prozess in der Assistenten-API auf zwei Schritte reduziert.
Zuerst laden wir unsere Dokumente zum Abruf hoch. Zweitens haben wir unserem Assistenten die Suchfunktion hinzugefügt.
Im folgenden Codeblock lade ich meinen Artikel über die Quantifizierung von fat tails auf mein OpenAI-Konto hoch. Dazu müssen wir den Dateipfad/-namen und den Zweck der Datei angeben. Hier haben wir als Zweck „Assistenten“ angegeben. Die andere Option ist „fine-tune“, die wir bald sehen werden.
file = client.files.create(
file=open("articles/4 Ways to Quantify Fat Tails with Python _ by Shaw Talebi _ Towards Data Science.pdf", "rb"),
purpose="assistants"
)
Als Nächstes können wir einen neuen Assistenten erstellen, ähnlich wie zuvor, aber jetzt den Abruf aktivieren und die Dateien angeben, die dem Assistenten zur Verfügung stehen. Während ich hier nur ein Dokument für den Abruf verwende, können wir bis zu 20 Dateien angeben, indem wir die zugehörigen Datei-IDs in die file_ids-Liste aufnehmen.
assistant = client.beta.assistants.create(
name="ShawGPT",
description="Data scientist GPT for YouTube comments",
instructions=intstructions_string_few_shot,
tools=[{"type": "retrieval"}],
file_ids=[file.id],
model="gpt-4-0125-preview"
)
Wenn wir die gleiche technische Frage an den Assistenten weitergeben, erhalten wir folgende Antwort.
Fat-tailedness describes the degree to which rare events significantly
contribute to the aggregate statistics of a distribution. This concept
highlights that in certain distributions, a small number of observations can
disproportionately influence the overall picture. A classic example is the
Pareto distribution, often referenced in the context of wealth distribution,
where a small proportion of individuals can hold the majority of wealth. In
statistical terms, distributions range from thin-tailed (e.g., Gaussian, where
extreme events are highly improbable) to very fat-tailed (e.g., Pareto, where
extreme events are less rare than in Gaussian distributions).
Fat-tailedness is not about a binary categorization of distributions as either
"Power Law" or not but recognizes a continuum or spectrum of "tail heaviness."
This understanding offers a more nuanced view, allowing for more accurate and
precise categorization of data distributions. Identifying and quantifying the
fat-tailed nature of a dataset is important in fields like finance, insurance,
risk management, and social sciences, where understanding the likelihood and
impact of extreme events is crucial.
In practical terms, fat-tailedness can be quantified using different heuristics,
which may involve mathematical computations such as the Power Law Tail Index,
Kurtosis, Log-normal's σ, and Taleb's κ metric. Each of these measures offers
a different lens through which to assess the fat-tailed nature of a
distribution, thereby providing useful insights into the behavior of extreme
events within the dataset -ShawGPT
Diese Antwort kommt der Art und Weise, wie ich über Fat-tailedness denke (und sie erkläre), sehr viel näher. Der Assistent hat die wichtigsten Konzepte aus dem Artikel nahtlos in seine Antwort übernommen. Zum Beispiel die Definition von Fat-Tailedness in Bezug auf seltene Ereignisse, Fat-Tailedness auf einem Spektrum und vier Heuristiken für ihre Messung.
Bis zu diesem Punkt sind wir mit Hilfe von Prompt-Engineering und Knowledge Retrieval bei der Erstellung unseres Assistenten schon recht weit gekommen. Allerdings lesen sich die Antworten immer noch nicht ganz so, wie ich sie schreiben würde. Um diesen Aspekt des Assistenten weiter zu verbessern, können wir uns der Feinabstimmung zuwenden.
Feinabstimmung der API
Auch wenn die Eingabeaufforderung ein einfacher Weg ist, einen Assistenten zu programmieren, ist es nicht immer offensichtlich, wie man das Modell am besten anweist, das gewünschte Verhalten zu zeigen. In solchen Situationen kann es von Vorteil sein, das Modell fein abzustimmen
Bei der Feinabstimmung trainieren wir ein bereits vorhandenes Modell mit zusätzlichen Beispielen für eine bestimmte Aufgabe. In der OpenAI Fine-tuning API besteht dies aus der Bereitstellung von Beispielen für Benutzer-Assistenten-Nachrichtenpaare [3].
Für den Anwendungsfall des YouTube-Kommentar-Responders bedeutet dies, dass Paare von Zuschauerkommentaren (d.h. Nutzernachrichten) und deren zugehörige Antworten (d.h. Assistenten-Nachrichten) gesammelt werden.
Obwohl dieser zusätzliche Prozess der Datenerfassung im Vorfeld mehr Arbeit für die Feinabstimmung bedeutet, kann er zu erheblichen Verbesserungen der Modellleistung führen. Im Folgenden werde ich den Feinabstimmungsprozess für diesen speziellen Anwendungsfall erläutern.
Datenvorbereitung
Um die Nachrichtenpaare zwischen Nutzer und Assistenten zu generieren, bin ich manuell durch vergangene YouTube-Kommentare gegangen und habe sie mit Copy-Paste in eine Tabelle eingefügt. Diese Tabelle habe ich dann als .csv-Datei exportiert (verfügbar im GitHub-Repository).
Diese .csv-Datei enthält zwar alle wichtigen Daten, die für die Feinabstimmung benötigt werden, kann aber nicht direkt verwendet werden. Wir müssen sie zunächst in ein bestimmtes Format umwandeln, um sie an die OpenAI-API zu übergeben.
Genauer gesagt müssen wir eine .jsonl-Datei erzeugen, eine Textdatei, in der jede Zeile einem Trainingsbeispiel im JSON-Format entspricht. Wenn Sie ein Python-Benutzer sind, der mit JSON nicht vertraut ist, können Sie es sich wie ein Wörterbuch vorstellen (d. h. eine Datenstruktur, die aus Schlüssel-Wert-Paaren besteht) [4].
Um unsere .csv-Datei in das erforderliche .jsonl-Format zu bringen, erstelle ich zunächst Python-Listen für jede Art von Kommentar. Dazu lese ich die rohe .csv-Datei Zeile für Zeile und speichere jede Nachricht in der entsprechenden Liste.
import csv
import json
import random
comment_list = []
response_list = []
with open('data/YT-comments.csv', mode ='r') as file:
file = csv.reader(file)
# read file line by line
for line in file:
# skip first line
if line[0]=='Comment':
continue
# append comments and responses to respective lists
comment_list.append(line[0])
response_list.append(line[1] + " -ShawGPT")
Um die .jsonl-Datei zu erstellen, müssen wir als Nächstes eine Liste von Wörterbüchern erstellen, wobei jedes Element einem Trainingsbeispiel entspricht. Der Schlüssel für jedes dieser Wörterbücher ist „messages“, und der Wert ist eine (weitere) Liste von Wörterbüchern, die jeweils den System‑, Benutzer- und Assistentenmeldungen entsprechen. Eine visuelle Übersicht über diese Datenstruktur ist unten dargestellt.
Im Folgenden finden Sie den Python-Code, mit dem Sie aus den Objekten comment_list und response_list die Liste der Beispiele erstellen. Dazu werden comment_list und response_list Element für Element durchlaufen und bei jedem Schritt drei Wörterbücher erstellt.
Diese entsprechen jeweils den System‑, Benutzer- und Assistentenmeldungen, wobei die Systemmeldung die gleichen Anweisungen sind, die wir für die Erstellung unseres Assistenten über eine kurze Eingabeaufforderung verwendet haben, und die Benutzer-/Assistentenmeldungen aus ihren jeweiligen Listen stammen. Diese Wörterbücher werden dann in einer Liste gespeichert, die als Wert für dieses bestimmte Trainingsbeispiel dient.
example_list = []
for i in range(len(comment_list)):
# create dictionaries for each role/message
system_dict = {"role": "system", "content": intstructions_string_few_shot}
user_dict = {"role": "user", "content": comment_list[i]}
assistant_dict = {"role": "assistant", "content": response_list[i]}
# store dictionaries into list
messages_list = [system_dict, user_dict, assistant_dict]
# create dictionary for ith example and add it to example_list
example_list.append({"messages": messages_list})
Am Ende dieses Prozesses haben wir eine Liste mit 59 Elementen, die 59 Benutzer-Assistent-Beispielpaaren entsprechen. Ein weiterer Schritt, der bei der Bewertung der Modellleistung hilft, besteht darin, diese 59 Beispiele in zwei Datensätze aufzuteilen, einen für das Training des Modells und den anderen für die Bewertung seiner Leistung.
Dies geschieht im folgenden Codeblock, in dem ich 9 von 59 Beispielen aus example_list zufällig entnehme und sie in einer neuen Liste namens validation_data_list speichere. Diese Beispiele werden dann aus example_list entfernt, die als Trainingsdatensatz dienen wird.
# create train/validation split
validation_index_list = random.sample(range(0, len(example_list)-1), 9)
validation_data_list = [example_list[index] for index in validation_index_list]
for example in validation_data_list:
example_list.remove(example)
Nachdem wir unsere Trainings- und Validierungsdatensätze vorbereitet haben, können wir sie in .jsonl-Dateien schreiben. Dies kann auf die folgende Weise geschehen.
# write examples to file
with open('data/training-data.jsonl', 'w') as training_file:
for example in example_list:
json.dump(example, training_file)
training_file.write('\n')
with open('data/validation-data.jsonl', 'w') as validation_file:
for example in validation_data_list:
json.dump(example, validation_file)
validation_file.write('\n')
Feinabstimmungsauftrag
Nachdem die Datenvorbereitung abgeschlossen ist, können wir die Feinabstimmung in 2 Schritten durchführen. Zunächst laden wir die Trainings- und Validierungsdateien in unser OpenAI-Konto hoch. Zweitens führen wir den Trainingsprozess durch [3].
Wir laden Dateien hoch, wie wir es bei der Einrichtung der Dokumentensuche für einen Assistenten getan haben, nur dass wir jetzt den Zweck der Datei als „Feinabstimmung“ festlegen. Dies geschieht sowohl für den Trainings- als auch für den Validierungsdatensatz (siehe unten).
# upload fine-tuning files
training_file = client.files.create(
file = open("data/training-data.jsonl", "rb"),
purpose = "fine-tune"
)
validation_file = client.files.create(
file = open("data/validation-data.jsonl", "rb"),
purpose = "fine-tune"
)
Jetzt können wir den Feinabstimmungsauftrag ausführen. Dazu müssen wir die Trainingsdateien und das Modell angeben, das wir feineinstellen möchten. Das am weitesten fortgeschrittene Modell, das für die Feinabstimmung zur Verfügung steht, ist gpt‑3.5‑turbo, das ich hier verwende. Ich setze auch ein Suffix, das eine benutzerdefinierte Zeichenfolge an die ID des feinabgestimmten Modells anhängt. Der Code dafür ist unten dargestellt.
client.fine_tuning.jobs.create(
training_file = training_file.id,
validation_file = validation_file.id,
suffix = "ShawGPT",
model = "gpt-3.5-turbo"
)
Der Trainingsprozess wird etwa 15 Minuten dauern. Nach Abschluss können wir das fein abgestimmte Modell über die Abschluss-API verwenden, wie unten dargestellt.
test_comment = "Great content, thank you!"
response = client.chat.completions.create(
model="ft:gpt-3.5-turbo-0613:personal:shawgpt:8mUeVreo",
messages=[
{"role": "system", "content": intstructions_string_few_shot},
{"role": "user", "content": test_comment}
]
)
Beachten Sie, dass sich die Art und Weise, wie wir die Antwort im obigen Codeblock generieren, von der bisherigen Vorgehensweise unterscheidet. Das liegt daran, dass fein abgestimmte Modelle derzeit nicht von der Assistenten-API unterstützt werden.
Der größte Nachteil ist, dass wir, wenn wir das fein abgestimmte Modell mit Werkzeugen erweitern wollen (d. h. es zu einem Assistenten machen), diese Funktionalitäten über Bibliotheken wie LangChain oder LlamaIndex (zum Beispiel) entwickeln müssen.
Das macht die Entwicklung eines fein abgestimmten Assistenten zwar aufwändiger, aber die Antworten, die wir sehen, sind näher an dem, was ich sagen würde. Hier sind ein paar Antworten auf den Testkommentar.
Thanks, I appreciate it! -ShawGPT
Thanks, glad you liked it! -ShawGPT
Glad it was helpful! -ShawGPT
Schauen wir uns an, wie es mit der gleichen technischen Frage wie zuvor umgeht, nämlich „Was ist Fat-Tailedness?“
Good question! The fat tail phenomenon represents the size of outlier (extreme)
events relative to a normal (Gaussian) distribution. In other words, there's a
greater probability of extreme events occurring compared to a normal
distribution. -ShawGPT
Obwohl das Modell „Fat Tails“ anders definiert, als ich es tun würde, sind die Länge und der Stil der Antwort viel besser als das, was wir mit der Assistenten-API vor RAG gesehen haben. Dies deutet darauf hin, dass, wenn wir RAG zu diesem fein abgestimmten Modell hinzufügen würden, es deutlich bessere Antworten erzeugen würde als das, was wir vorher gesehen haben.
Was kommt als Nächstes?
Die Erstellung eines benutzerdefinierten KI-Assistenten ist so einfach wie nie zuvor. Hier haben wir einen einfachen Weg gesehen, einen KI-Assistenten über die API von OpenAI’s Assistant zu erstellen, und wie man ein Modell über ihre Feinabstimmungs-API fein abstimmt.
OpenAI verfügt zwar derzeit über die fortschrittlichsten Modelle für die Entwicklung der hier besprochenen Art von KI-Assistenten, aber diese Modelle sind hinter ihrer API verschlossen, was die Möglichkeiten, mit ihnen zu arbeiten, einschränkt.
Daher stellt sich natürlich die Frage, wie wir ähnliche Systeme mit Open-Source-Lösungen entwickeln können. Dies wird in den nächsten Artikeln dieser Serie behandelt, in denen ich erörtern werde, wie man ein Modell mit QLoRA fein abstimmt und einen Chatbot mit RAG erweitert.
Quelle: medium.com