library(tidyverse)
library(magick)2 Zero-Shot Klassifikation
Wie immer laden wir zuerst das tidyverse-Paket. Für die Verarbeitung von Bildern benötigen wir zudem das Paket magick.
2.1 LLM-APIs nutzen
Für die LLM-basierte Klassifikation von Texten und Bilder nutzen wir das ellmer-Paket, das für eine Reihe von LLM-APIs einheitliche Funktionen bietet und strukturierte Daten zurückliefert.
library(ellmer)Wir definieren zunächst, mit welchem LLM wir arbeiten wollen. Die JGU bietet unter https://ki-chat.uni-mainz.de/ eine eigene OpenAI-kompatible API an, die wir nutzen können. Hierfür brauchen wir allerdings unseren API-Key, den wir unter Einstellungen auf der JGU-KI-Seite bekommen.
JGU_API_KEY <- "XYZ"Nun definieren wir das Objekt jgu_chat, das die Zugangsdaten und Einstellungen für unser LLM enthält. Wichtig für später sind das Modell und die sog. temperature, die wir auf 0 setzen, um deterministische Antworten zu erhalten.
jgu_chat <- ellmer::chat_openai(
base_url = "https://ki-chat.uni-mainz.de/api",
model = "Qwen3 235B VL",
api_key = JGU_API_KEY,
params = params(temperature = 0)
)Wir beginnen mit einem ganz einfachen Beispiel: Wir bitten ein LLM, uns einen Witz zu erzählen. Dafür benötigen wir lediglich einen Text-Prompt.
jgu_chat$chat("Tell me a dad joke")Why don’t scientists trust atoms?
…because they make up everything! 😄
*(Classic dad joke — groan-worthy, but scientifically accurate.)*2.2 Textklassifikation
2.2.1 Grundlagen
Die Zero-Shot-Klassifikation von Texten ist denkbar einfach: Wir kombinieren einfach eine Codieranweisung, den zu codierenden Text und die zu verwendenden Kategorien. Es empfiehlt sich, die Anweisung so zu gestalten, dass möglichst einheitliche, kurz Antworten gegeben werden. Die Anweisung kann auch deutlich umfangreicher ausfallen oder sogar Beispiele enthalten (sog. Few-Shot-Klassifikation), aber der Einfachheit halber wählen wir eine sehr knappe Codieranweisung.
Um zuverlässig nur gültige Codierungen zu erhalten, nutzen wir sog. structured output, d.h. wir zwingen das LLM, uns die Antworten in einem vorgegebenen Format zurückzugeben, hier type_boolean(), was einer dichotomen Ja/Nein Antwort entspricht. Demensprechend erhalten wir zwingend eine Spalte vom R-Type logical zurück, d.h. TRUE und FALSE Werte.
zero_shot_prompt <- paste("Is this text about sports? Answer only with the words TRUE or FALSE.",
"Mainz 05 schlägt Eintracht zuhause mit 2:0.",
sep = "\n\n"
)
response <- jgu_chat$chat_structured(zero_shot_prompt,
type = type_object(sport = type_boolean())
)
response$sport
[1] TRUE2.2.2 Mehrere Texte codieren
Um mehrere Texte codieren zu lassen, erstellen wir zunächst eine Liste mit Codieraufgaben. Jede Codieraufgabe besteht immer aus einem Text und derselben Anweisung (coding_task). Diese werden mit der interpolate()-Funktion so verküpft, dass wie bei einem Lückentext die jeweiligen Variablen (ggf. wiederholt) kombiniert werden. Am Ende erhalten wir drei Codieraufgaben in einem Objekt tasks.
headlines <- list(
"Arsenal coach fired after horrible loss at home.",
"EU ministers meet in Brussels.",
"How electric cars work."
)
coding_task <- "Is this headline about sports? Answer only with the words TRUE or FALSE."
tasks <- ellmer::interpolate("{{coding_task}} HEADLINE: {{headlines}}")
tasks[1] │ Is this headline about sports? Answer only with the words TRUE or FALSE. HEADLINE: Arsenal coach fired after horrible loss at home.
[2] │ Is this headline about sports? Answer only with the words TRUE or FALSE. HEADLINE: EU ministers meet in Brussels.
[3] │ Is this headline about sports? Answer only with the words TRUE or FALSE. HEADLINE: How electric cars work.Mit der Funktion parallel_chat_structured() können wir mehrere Tasks parallel an das LLM senden. Wir verwenden dieselbe Typendefinition, d.h. eine dichotome Sport-Variable. Wir erhalten nun ein Tibble mit den Ergebnissen zurück und fügen die 3 Headlines als weitere Spalte hinzu, um zu schauen, ob die Codierung erfolgreich war.
results <- parallel_chat_structured(jgu_chat, tasks, type = type_object(sport = type_boolean()))
results |>
mutate(headline = headlines) sport headline
1 TRUE Arsenal coach fired after horrible loss at home.
2 FALSE EU ministers meet in Brussels.
3 FALSE How electric cars work.2.2.3 Beispiel: JGU auf Instagram
Bislang haben wir uns lediglich einfache Demonstrationen angeschaut, nun wollen wir unsere neu erworbenen Kenntnisse nutzen, um die Inhalte auf dem Instagram-Account der JGU zu untersuchen. Wir beginnen damit, die Daten zu laden und auf die ersten 10 Zeilen zu beschränken.
jgu_insta <- read_tsv("data/jgu_insta.tsv") |>
mutate(id = as.character(id)) |>
head(10)
jgu_insta# A tibble: 10 × 4
id text date img
<chr> <chr> <dttm> <chr>
1 3286635500117489152 "Wenn sich ein Arzt oder eine Ä… 2024-01-23 13:45:06 2024…
2 3303375298671884288 "#Edelsteine sind faszinierende… 2024-02-15 16:04:08 2024…
3 3317870312789501440 "Europa im Herzen, das Mittelal… 2024-03-06 16:03:07 2024…
4 3318636360354637824 "Das Forschungsfeld der Biologi… 2024-03-07 17:25:08 2024…
5 3319334968143100928 "Schon gehört? Kürzlich hat sic… 2024-03-08 16:33:07 2024…
# ℹ 5 more rowsAnschließend nutzen wir parallel_chat_structured(), um alle Texte im jgu_insta-Datensatz zu codieren. Wir interessieren uns hier für zwei Kategorien: Geht es in dem Text um wissenschaftliche Inhalte oder gute Neuigkeiten aus der Universität? Zunächste erstellen wir wieder eine Lists von Tasks mit interpolate(), in der wir die Spalte text aus dem Datensatz verwenden.
science <- "Is this text about scientific research? Answer only with the words TRUE or FALSE."
positive_news <- "Does this text represent positive news or success stories involving the university or its members? Answer only with TRUE or FALSE."
tasks <- interpolate("Please classify the following posts according to these categories: Science: {{science}} \n Positive News: {{positive_news}} \n Post: {{jgu_insta$text}}")
head(tasks, 1)[1] │ Please classify the following posts according to these categories: Science: Is this text about scientific research? Answer only with the words TRUE or FALSE.
│ Positive News: Does this text represent positive news or success stories involving the university or its members? Answer only with TRUE or FALSE.
│ Post: Wenn sich ein Arzt oder eine Ärztin nicht mit Patienten verständigen kann, weil diese kaum Deutsch sprechen, ist das ein ernstes Problem. Und solche Situationen sind keine Seltenheit. Fachleute plädieren daher für den flächendeckenden Einsatz von Sprachmittlung im Gesundheitswesen. Einer der renommiertesten Experten auf diesem Gebiet ist Prof. Bernd Meyer, Leiter des Arbeitsbereichs Interkulturelle Kommunikation am @ftsk_unimainz der #UniMainz. Im #JGUMagazin haben wir mit ihm darüber gesprochen, warum der Einsatz von #Sprachmittlung in der #Patientenkommunikation so wichtig ist, aber auch welche Herausforderungen gemeistert werden müssen – und warum Künstliche Intelligenz kein adäquater Ersatz ist. Den kompletten Beitrag findet ihr im JGU-Magazin unter 👉 www.magazin.uni-mainz.de (oder über unsere Insta-Landingpage in der Bio) #InterkulturelleKommunikationAnschließend erstellen wir einen neuen Datensatz mit den Codierungen und der id-Spalte, damit wir die Codierdaten später ggf. mit anderen Variablen kombinieren können.
jgu_insta_coded <- parallel_chat_structured(jgu_chat, tasks,
type = type_object(
research = type_boolean(),
positive_news = type_boolean()
)
)
jgu_insta_coded <- jgu_insta_coded |>
mutate(id = jgu_insta$id)
jgu_insta_coded research positive_news id
1 FALSE TRUE 3286635500117489152
2 TRUE TRUE 3303375298671884288
3 FALSE TRUE 3317870312789501440
4 TRUE TRUE 3318636360354637824
5 FALSE TRUE 3319334968143100928
6 FALSE TRUE 3320564063891673088
7 FALSE TRUE 3327287349719803904
8 FALSE TRUE 3327835972106941952
9 TRUE TRUE 3339419327055165952
10 FALSE TRUE 3341768140331910144Mit count() können wir uns die absoluten Häufigkeiten der beiden Kategorien ausgeben lassen.
count(jgu_insta_coded, research) research n
1 FALSE 7
2 TRUE 3count(jgu_insta_coded, positive_news) positive_news n
1 TRUE 10Zum Schluss kombinieren wir die Codierdaten mit den ursprünglichen Instagram-Daten zu einem Gesamtdatensatz. Dafür verwenden wir left_join() und als Schlüsselvariable die ID.
jgu_complete <- jgu_insta |>
left_join(jgu_insta_coded, by = "id")
jgu_complete |>
glimpse()Rows: 10
Columns: 6
$ id <chr> "3286635500117489152", "3303375298671884288", "33178703127895…
$ text <chr> "Wenn sich ein Arzt oder eine Ärztin nicht mit Patienten vers…
$ date <dttm> 2024-01-23 13:45:06, 2024-02-15 16:04:08, 2024-03-06 16:03:0…
$ img <chr> "2024-01-23_13-45-06_UTC.jpg", "2024-02-15_16-04-08_UTC.jpg",…
$ research <lgl> FALSE, TRUE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, TRUE, FA…
$ awards <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE…2.3 Bildklassifikation
Neben Textdaten sind multimodale LLMs auch in der Lage, Bilder oder andere Formen von Daten (bspw. Video, Audio) zu verarbeiten. Nachfolgend wollen wir neben den Texten auch die Bilder auf dem Instagram-Account der Uni Mainz auswerten.
2.3.1 Grundlagen
Mit der Funktion image_read() aus dem Paket magick können wir uns Bilder in R anzeigen lassen, sowie erhalten einige Informationen über das Format und die Abmessungen des Bildes.
magick::image_read("data/jgu_insta/2024-04-15_06-05-06_UTC.jpg")
Für die Verwendung von Bildern müssen lediglich das Bild über die Funktion content_image_file() dem Textprompt hinzufügen. Hier codieren wir das Vorkommen von Blumen.
jgu_chat$chat_structured("Does this image show flowers? Answer only with the words TRUE or FALSE.",
content_image_file("data/jgu_insta/2024-04-15_06-05-06_UTC.jpg"),
type = type_object(flowers = type_boolean())
)$flowers
[1] TRUE2.3.2 Beispiel: JGU auf Instagram
Wie zuvor bei den Texten auf Instagram können wir auch mehrere Bilder codieren. Wir müssen lediglich den Dateipfad angeben, um mittels content_image_file() die Dateien für das LLM bereitstellen zu können. Die Bilder liegen in einem Ordner mit dem Pfad data/jgu_insta/. list.files() gibt die vollständigen Dateipfade zurück, welche wir mit dem Textprompt kombinieren. Unser Ziel ist es, herauszufinden, ob die Bilder Studierende zeigen. map() erlaubt es uns, über jedes Bild, welches in der Liste image_paths enthalten ist, zu iterieren, d.h. wir erstellen 10 Aufgaben (tasks) für das LLM. Der Textprompt bleibt dabei unverändert.
task <- "Does this image show university students? Answer only with the words TRUE or FALSE."
image_paths <- list.files("data/jgu_insta/", "*.jpg", full.names = TRUE) |>
head(10)
tasks <- image_paths |>
map(~ list(task, content_image_file(.x)))
tasks[[1]][[1]]
[1] "Does this image show university students? Answer only with the words TRUE or FALSE."
[[2]]
<ellmer::ContentImageInline>
@ type: chr "image/jpeg"
@ data: chr "/9j/4AAQSkZJRgABAQAAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBY"| __truncated__Anschließend nutzen wir wieder parallel_chat_structured(), um die Anfragen an das LLM zu schicken. Da es sich um ein multimodales LLM handelt, können wir das jgu_chat-Objekt weiterverwenden. Anschließend fügen wir den Dateipfad wieder an, damit wir wissen, um welches Bild es sich gehandelt hat.
jgu_insta_coded_img <- parallel_chat_structured(jgu_chat, tasks,
type = type_object(students = type_boolean())
)
jgu_insta_coded_img <-
jgu_insta_coded_img |>
mutate(img = image_paths)
jgu_insta_coded_img students img
1 FALSE data/jgu_insta//2024-01-23_13-45-06_UTC.jpg
2 FALSE data/jgu_insta//2024-02-15_16-04-08_UTC.jpg
3 TRUE data/jgu_insta//2024-03-06_16-03-07_UTC.jpg
4 TRUE data/jgu_insta//2024-03-07_17-25-08_UTC.jpg
5 FALSE data/jgu_insta//2024-03-08_16-33-07_UTC.jpg
6 FALSE data/jgu_insta//2024-03-10_09-15-06_UTC.jpg
7 FALSE data/jgu_insta//2024-03-19_15-53-05_UTC_1.jpg
8 TRUE data/jgu_insta//2024-03-20_10-03-05_UTC.jpg
9 FALSE data/jgu_insta//2024-04-05_09-37-09_UTC_1.jpg
10 FALSE data/jgu_insta//2024-04-08_15-23-47_UTC_1.jpgEs zeigt sich, dass etwas mehr als die Hälfte der Bilder Studierende zeigt. Gerade bei kleineren (multimodalen) LLMs kann es sein, dass, trotz unserer Instruktion, die Ausgabe des Modells nicht exakt den geforderten Werten entspricht. Beispielsweise könnten wir nicht nur TRUE & FALSE erhalten, sondern auch den Wert TRUE mit einem speziellen Token <|im. Dies kann immer wieder passieren, weshalb teilweise noch weitere Datenbearbeitungsschritte notwendig sind.
jgu_insta_coded_img |>
count(students) students n
1 FALSE 7
2 TRUE 3Nun extrahieren wir noch die Dateipfade der Bild, welche laut Modell Studierende zeigen soll, um diese anschließend zu inspizieren.
student_images <- jgu_insta_coded_img |>
filter(str_detect(students, "TRUE")) |>
pull(img)
student_images[1] "data/jgu_insta//2024-03-06_16-03-07_UTC.jpg"
[2] "data/jgu_insta//2024-03-07_17-25-08_UTC.jpg"
[3] "data/jgu_insta//2024-03-20_10-03-05_UTC.jpg"Nun können wir alle Bilder, die Studierende zeigen sollten, mit magick in einer Collage darstellen, um uns einen Eindruck von der Klassifikationsgüte zu verschaffen:
student_images |>
magick::image_read() |>
magick::image_montage(tile = "5x2")
Hausaufgabe
Lassen Sie die JGU-Instagram-Posts daraufhin zero-shot-codieren, ob über Studierende geschrieben wird. Zu welchen Ergebnissen kommen die Klassifikationen im Vergleich zu Ihrer eigenen Einschätzung und zur visuellen Analyse?
Finden Sie auf https://ki-chat.uni-mainz.de/ heraus, welche (multimodalen) LLMs aktuell verfügbar sind. Wählen Sie ein anderes Modell für die Textklassifikation aus. Anschließend nutzen Sie das neugewählte Modell, um erneut die ersten 10 Zeilen des
jgu-insta-Datensatzes zu klassifizieren (Kategorien und Prompt müssen nicht angepasst werden). Vergleichen Sie die Ergebnisse. Gibt es Unterschiede?Untersuchen Sie die Bilder in den Instagram-Posts erneut. Dieses Mal sollen Sie allerdings herausfinden, ob eine Gruppe von Personen zu sehen ist oder nicht. Wieviele Gruppen befinden sich laut Klassifikationsergebnis in den Bilder?