4  Zero-Shot Klassifikation

Wie bereits bekannt laden wir zu Beginn das tidyverse Paket und setzen ein schöneres Theme für alle Grafiken, die wir erstellen. Außerdem benötigen wir das hfapi Paket, um mit der Hugging Face API zu interagieren. Ebenfalls notwenig ist das Paket magick, um Bilder darzustellen.

# remotes::install_github("ccsmainz/hfapi", dependencies = FALSE, force = TRUE)
library(tidyverse)
theme_set(theme_minimal())
library(hfapi)
library(magick)

4.1 Zero-shot Bildklassifikation

Zero-shot Bildklassifikation erlaubt es, Bilder in beliebige Kategorien einzusortieren. Dafür brauchen wir lediglich das Bildmaterial und die Klassen, die uns interessieren. Hier nutzen wir ein Stock Photo von einer Demonstration.

magick::image_read("https://images.pexels.com/photos/2975498/pexels-photo-2975498.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2")

Anschließend brauchen wir lediglich die Funktion image_zeroshot(), welche auf das von OpenAI entwickelt Modell CLIP über die Hugging Face API zugreift. Den Funktion erhält das Bild sowie unsere Kategorien. Da CLIP anhand von englischen Bild-Text Paaren trainiert wurde, sollten wir die Kategorien entsprechend übersetzen. Nachfolgend fragen wir das Modell, ob es sich bei unserem Bild eher um eine Sportveranstaltung oder Demonstration handelt.

hfapi::image_zeroshot("https://images.pexels.com/photos/2975498/pexels-photo-2975498.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2", labels = c("demonstration", "sporting event"))
# A tibble: 2 × 2
     score label         
     <dbl> <chr>         
1 1.00     demonstration 
2 0.000232 sporting event

Das Modell liefert uns die Klassen zurück und die dazugehörigen Zuversichtsscores. In unserem Fall ist es sich sehr sicher, dass es sich bei unserem Bild um eine Demonstration handelt.

Aufgabe
  1. Klassifizieren Sie selbstgewählte Bilder mit selbstgewählten Kategorien. Wie gut funktioniert die Zero-Shot-Klassifikation?

4.2 Zero-shot Textklassifikation

Für die zero-shot Textklassifikation stehen uns prinzipiell zwei Arten von Modellen zu Verfügung:

  1. Generative große Sprachmodelle wie etwa GPT 3.5
  2. Modelle, die für Natural Language Inference (NLI) trainiert wurden

In diesem Kapitel nutzen wir nur Modelle, die für NLI trainiert wurden, da diese frei verfügbar sind, während generative Modelle oft mit (erheblichen) Kosten verbunden sind. So ist die Verwendung von GPT 3.5 über die OpenAI API etwa kostenpflichtig. Frei verfügbare große Sprachmodelle benötigen hingegen meist spezielle Hardware, die über Cloudservices gemietet werden muss.

4.2.1 Generative LLM (z.B. ChatGPT)

Die Klassifikationsaufgabe wird bei generativen Modellen als sogenannter prompt, eine Art Aufgabenbeschreibung, gestellt. Dabei kann etwa die Rolle der KI sowie das erwartete Ausgabeformat spezifiziert werden. Die Formulierung des prompts hat Einfluss auf das Klassifikationsergebnis, weshalb häufig verschiedene Beschreibungen der Aufgabe ausprobiert werden, um die Klassifikationsgüte zu steigern. Ein prompt zum Thema Negative Campaigning (Wahlwerbung, die den politischen Gegner negativ darstellt) könnte etwa wie folgt formuliert werden:

Please assess the following German text and determine whether one of the parties 
is the target of negative campaigning. For each of the parties listed below, 
respond with a 1 if the text contains negative campaigning against them, 
and 0 if it does not. Please follow this format strictly.

SPD:
CDU/CSU:
FDP:
AfD:
die Grünen:
die Linke:

Here is the text:

Auch wenn die zu klassifizierenden Texte nicht englisch sind, sollte der prompt dennoch auf Englisch sein, da dies in der Regel zu besseren Ergebnissen führt. Grund dafür ist, dass die meisten Trainingsdaten für große Sprachmodelle in englischer Sprache vorliegen.

Das (fiktive) Ergebnis des obigen prompts sollte dann wie folgt aussehen:

SPD: 1
CDU/CSU: 0
FDP: 1
AfD: 0
die Grünen: 0
die Linke: 1

4.2.2 Natural Language Inference (NLI)

NLI behandelt die Folgerungsbeziehungen zwischen zwei Texteinheiten, der Hypothese und dem Text. Modelle, die für NLI trainiert werden, sollen lernen, ob aus dem Text die aufgestellte Hypothese folgt oder nicht (bzw. es keinen Zusammenhang gibt). Da sich alle Textklassifikationsaufgaben als Folgerungsbeziehungen umformulieren lassen, eignen sich diese Modelle für die zero-shot Textklassifikation. So lässt sich beispielsweise die Hypothese aufstellen “Dieser Text behandelt das Thema Immigration” und das Modell würde bei jedem Text im Datensatz prüfen, ob diese Hypothese aus dem jeweiligen Text folgt. Diese Herangehensweise wollen wir nachfolgend nutzen, um die Tweets von Donald Trump inhaltlich zu kategorisieren.

Zunächst laden wir den Datensatz:

load(url("http://varianceexplained.org/files/trump_tweets_df.rda"))
tweets <- trump_tweets_df |>
  select(id, statusSource, text, created) |>
  # Die verschiedenen Betriebssysteme extrahieren
  extract(statusSource, "source", "Twitter for (.*?)<") |>
  filter(source %in% c("iPhone", "Android")) |>
  mutate(text = stringi::stri_enc_toutf8(text, validate = T))
tweets
# A tibble: 1,390 × 4
  id                 source  text                            created            
  <chr>              <chr>   <chr>                           <dttm>             
1 762669882571980801 Android "My economic policy speech wil… 2016-08-08 15:20:44
2 762641595439190016 iPhone  "Join me in Fayetteville, Nort… 2016-08-08 13:28:20
3 762439658911338496 iPhone  "#ICYMI: \"Will Media Apologiz… 2016-08-08 00:05:54
4 762425371874557952 Android "Michael Morell, the lightweig… 2016-08-07 23:09:08
5 762400869858115588 Android "The media is going crazy. The… 2016-08-07 21:31:46
# ℹ 1,385 more rows

Für die zero-shot Textklassifikation benötigen wir die Funktion text_zeroshot() aus unserem hfapi Paket. Die Funktion benötigt wie image_zeroshot() eine Liste der Kategorien, die uns interessieren. Hier nutzen wir, wieder in englischer Sprache, “about immigration” und “about foreign politics”. Die Funktion wandelt die Kategorien intern in die Hypothese “This example is about immigration” um. Dazu wird standardmäßig ein Template mit der Form “This exmaple is {}” verwendet, wobei die geschweiften Klammern als Platzhalter.

tweets |>
  filter(id %in% c("760246732152311808", "760783130978648064", "761892829434183684")) |>
  pull(text) |> # Textspalte nach dem Filtern auswählen
  hfapi::text_zeroshot(url = "https://api-inference.huggingface.co/models/facebook/bart-large-mnli", labels = c("about immigration", "about foreign politics"))
# A tibble: 6 × 3
  sequence                                                        labels  scores
  <chr>                                                           <chr>    <dbl>
1 Hillary Clinton is being badly criticized for her poor perform… about… 0.698  
2 Hillary Clinton is being badly criticized for her poor perform… about… 0.302  
3 Our incompetent Secretary of State, Hillary Clinton, was the o… about… 0.991  
4 Our incompetent Secretary of State, Hillary Clinton, was the o… about… 0.00917
5 Hillary Clinton raked in money from regimes that horribly oppr… about… 0.983  
# ℹ 1 more row

Unser Output enthält den zu klassifizierenden Text für jede Kategorie ein Mal. Da wir zwei Kategorien haben, erhalten wir für jeden Text ein Duplikat. Sollten wir drei Kategorien untersuchen, erhalten wir jeden Text drei Mal (1 Original, 2 Duplikate). Des Weiteren erhalten wir die vorhergesagten Klassen und die Zuversichtsscores. Auf den ersten Blick sieht das Ergebnis plausibel aus. Lediglich für den ersten Tweet lässt sich feststellen, dass unsere Kategorien nicht wirklich passen, weshalb wir eher niedrige Zuversichtsscores erhalten.

Wir können diese Zuversichtsscores nutzen, um die Kategorien als vorhanden (1) oder nicht vorhanden (0) zu codieren. Nachfolgend wollen wir eine Klasse als vorhanden codieren, wenn der Zuversichtsscore über dem Schwellenwert .8 liegt.

tweets_topics <- tweets |>
  slice_head(n = 50) |> # Erste 50 Tweets
  pull(text) |> # Textspalte nach dem Filtern auswählen
  hfapi::text_zeroshot(
    url = "https://api-inference.huggingface.co/models/facebook/bart-large-mnli",
    labels = c("about immigration", "about foreign politics")
  ) |>
  mutate(prediction = if_else(scores >= .8, 1, 0))

Anschließend können wir auszählen wie häufig die beiden Kategorien vorkommen und das Ergebnis grafisch darstellen.

tweets_topics |>
  group_by(labels) |>
  count(prediction) |>
  mutate(prediction = as.character(prediction)) %>% # Nur für die grafische Darstellung
  ggplot(aes(x = labels, y = n, fill = prediction)) +
  geom_col(position = "dodge") +
  labs(x = "", y = "n")

Scheinbar enthalten die ersten 50 Tweets keine Aussagen zur Migration, dafür aber einige, wenige Bemerkungen zur Außenpolitik.

Aufgabe
  1. Klassifizieren Sie die Tweet-Texte mit Hilfe eines anderen Modells auf Hugging Face.
  2. Fügen Sie eine weitere Kategorie hinzu. Wiederholen Sie die Analyse.
Sentiment-Score mit NLI

Wir können NLI-Modelle ebenfalls für Sentimentanalysen nutzen. Entsprechend können wir einfach die Analyse des Sentiments aus der Sitzung zur Textanalyse wiederholen. Dazu müssen wir lediglich die labels in positive und negative ändern. Anschließend nutzen wir die Zuversichtsscores, um eine Dummy-Variable negative zu erstellen, die den Wert 1 besitzt, wenn der Tweet einen Wert größer .8 besitz. Abschließend können wir mit unserer neu konstruierten binären Variable die Anzahl negativer Tweets berechnen.

tweets_small <- tweets |>
  # Stratifiziertes Sample
  group_by(source) |>
  slice_head(n = 50)

tweets_small |>
  pull(text) |>
  hfapi::text_zeroshot(url = "https://api-inference.huggingface.co/models/facebook/bart-large-mnli", labels = c("negative", "positive")) |>
  filter(labels == "negative") |>
  # Binäre Variable erstellen mit if_else
  mutate(negative = if_else(scores > 0.8, 1, 0)) |>
  # Hinzufügen der Variablen aus dem Datensatz mit left_join()
  left_join(tweets_small, by = c("sequence" = "text")) |>
  count(source, negative)
# A tibble: 4 × 3
  source  negative     n
  <chr>      <dbl> <int>
1 Android        0    13
2 Android        1    37
3 iPhone         0    25
4 iPhone         1    25

Auch hier bestätigt sich das Bild unserer bisherigen Analysen. Wir kommen wieder zum Ergebnis, dass der Account des Android-Smartphones mehr Tweets mit negativen Inhalten absetzt.