Topic Modelling
Das Topic Modelling ist eine Methode aus dem Natural Language Processing, bei der ein Korpus aus Texten in Themen aufgeteilt wird bzw. dem Korpus verschiedene Themen basierend auf seinen Texten zugewiesen werden.
Eines der bekanntesten Topic Modelling Verfahren ist das sogenannte Latent Dirichlet Allocation (LDA), dieses ist allerdings schon über 20 Jahre alt und weist einige methodische Schwierigkeiten auf. Wir verwenden hier daher eine andere, neuere Methode. Ein Skript zur Verwendung von LDA gibt es aber hier auf KoDuP.
Top2Vec
Top2Vec ist eine Topic Modelling Methode, die es seit 2020 gibt und auf diesem Paper basiert. Die darunterliegende Methode ist ähnlich wie bei Word Embeddings: Den Wörtern sowie den Dokumenten werden Vektoren zugewiesen und diese werden in einem mehrdimensionalen Vektorraum projiziert und geclustert. Basierend darauf werden die Topics berechnet. Top2Vec, bzw. der Algorithmus dahinter, berechnet daher selbst wie viele Topics schlussendlich ausgegeben werden.
Bei Methoden der distributionellen Semantik, bzw. aus dem Natural Language Processing ist es oft sinnvoll mit grösseren Korpora zu arbeiten. Für das Topic Modelling mit Top2Vec sind mehrere tausend Artikel von Vorteil. Ab einer gewissen Anzahl Artikel dauert die Prozessierung dann recht lange. Berechnen Sie bei Analysen mit diesen Methoden also unbedingt den Zeitfaktor mit ein.
Top2Vec installieren und nutzen
Installation
Top2Vec kann mit pip installiert werden:
pip install top2vec
auf Windows sind möglicherweise noch folgende Installationen nötig, um top2vec zu installieren:
- python developer tools:
py -m pip install python-dev-tools —user —upgrade
- VisualStudio build tools: hier kannst du die Tools herunterladen.
um alle verschiedenen Varianten zu installieren, führe zudem folgende Befehle aus:
pip install top2vec[sentence_encoders]
pip install top2vec[sentence_transformers]
pip install top2vec[indexing]
bzw auf Mac in doppelten Anführungszeichen:
pip3 install "top2vec[sentence_encoders]"
pip3 install "top2vec[sentence_transformers]"
pip3 install "top2vec[indexing]"
für die Skripts brauchst du ausserdem, wenn nicht schon installiert:
pip install pandas
pip install plotly
Skripte für das Topic Modelling mit Top2Vec
⚠ Stellen im Code, welche geändert werden können oder müssen, sind mit TODO in einem Kommentar in der unmittelbaren Nähe markiert ⚠
top2vec_model.py
Hiermit können Sie ein top2vec Modell aus Ihrem Korpus erstellen. Das Format des Korpus sollte ein Dokument pro Zeile sein, wie es mit dem Skript vrt2docperline.py erstellt werden kann. In der Zeile, wo das Modell erstellt wird, können Sie verschiedene Anpassungen im Skript machen:
- embedding_model: ohne Änderungen ist doc2vec gewählt, was neue Word- und Document embeddings aufgrund deines Korpus erstellt. Andere Optionen sind vortrainierte Modelle, zur Auswahl stehen universal-sentence-encoder, universal-sentence-encoder-multilingual, distiluse-base-multilingual-cased. Um zu entscheiden, welches Modell für Ihr Korpus am besten geeignet ist, lesen Sie am besten die Beschreibung hier. Je nach Grösse und enthaltenen Sprachen kann ein anderes Modell besser sein.
- speed : falls doc2vec als embedding modell gewählt wurde, kann mit speed noch die Geschwindigkeit und, daran gekoppelt, die Qualität der Embeddings bestimmt werden. Die drei Optionen sind fast-learn, learn und deep-learn, wobei deep-learn am längsten dauert aber die genauesten Ergebnisse liefert.
- workers: wie viele Threads gleichzeitig am Training arbeiten. Je mehr, desto schneller; allerdings hängt die Anzahl machbarer Threads von der "Stärke" (genauer, der Anzahl CPUs) des Geräts ab. Neuere Macs und MacBooks (M2) sollten problemlos 6-8 worker laufen lassen können, schwächere oder ältere Geräte vielleicht nur 2-4. Im Task-Manager oder in der Aktivitätsanzeige können Sie beobachten, wie viel Ressourcen das Programm benötigt, und diese wenn nötig anpassen.
- min_count: Die Mindestanzahl die ein Wort erreichen muss, damit von diesem Wort ein Word Embedding entsteht, und entsprechend in die Topics einfliessen kann. Im Skript ist defaultmässig 30 angegeben, auch hier kann je nach Korpusgrösse und gewünschtem Resultat angepasst werden.
Für die Erstellung des Modelles gibt es noch weitere Parameter, welche hier, der erste Eintrag class top2vec.Top2Vec.Top2Vec nachgelesen werden können.
python3 top2vec_model.py
from top2vec import Top2Vec
def main():
docs = []
# TODO hier den Dateipfad vom momentanen Standort zu deinem Korpus angeben. Format: .txt, ein Dokument pro Zeile.
with open("Dateipfad/mein/Korpus.txt", "r", encoding="utf-8") as data_file:
for line in data_file.readlines():
docs.append(line)
#print(len(docs))
# hier kannst du die Anzahl der Dokumente künstlich auf etwa 4000 bis 8000 erhöhen,
# um für kleinere Korpora bessere Ergebnisse zu erzielen.
# dazu die folgende zeile auskommentieren.
# docs = docs*10
# TODO passe die Parameter an (siehe README oder Dokumentation)
model = Top2Vec(
docs, embedding_model="doc2vec", speed="deep-learn", workers=4, min_count=20
)
# TODO hier den Dateipfad zum Modell angeben. Format: .model
curr_model_path = "Dateipfad/mein/Modell.model"
model.save(curr_model_path)
if __name__ == "__main__":
main()
top2vec_overview.py
Dieses Skript erzeugt zusätzlich zum Topic Modell
-
Ein CSV-Skript mit der Topic ID, der Worten, die in einem Topic vorkommen und der Anzahl Dokumente pro Dokument,
-
Ein Balkendiagramm erstellt mit plotly, welches die Verteilung über die einzelnen Topics anzeigt.
Auch in diesem Skript müssen Sie vor der Ausführung einige Anpassungen machen. Diesmal sind es Dateipfade, Titel und Masse des Diagramms und einige optionale Änderungen.
python3 top2vec_overview.py
from top2vec import Top2Vec
import re
import plotly.express as px
import pandas as pd
def main():
# TODO pass den Dateipfad an, um dein erstelltes Modell zu laden
model = Top2Vec.load("Dateipfad/mein/top2vec/Modell.model")
# .get_topic_sizes() gibt eine Liste mit der Grösse der Topics im Korpus zurück, und eine mit den id's der Topics.
topic_sizes, topic_nums = model.get_topic_sizes()
"""
.get_topics() nimmt die Anzahl Topics des Modelles als Argument und gibt drei Listen zurück:
1. eine Liste mit einer Liste pro Topic mit den Top 50 Worten, die das Topic ausmachen
2. eine Liste mit einer Liste pro Topic mit der Cosinus Distanz jedes Wortes zum entsprechenden Topic-Embedding
3. eine Liste mit den IDs der Topics
"""
topic_words, word_scores, topic_nums = model.get_topics(model.get_num_topics())
# TODO pass den Dateipfad zum gewuenschten csvfile an.
csv_path = "Dateipfad/CSVfile/.csv"
with open(csv_path, "w", encoding="utf-8") as csv_out:
csv_out.write("topic_number,words_in_topic,number_of_documents\n")
for topic_num in topic_nums:
string_topic_words = re.sub(r"[\n,\[,\]]", " ", str(topic_words[topic_num]))
#print(string_topic_words)
csv_out.write(
f"{topic_num},{string_topic_words},{topic_sizes[topic_num]}\n"
)
"""
Visualisierung:
Balkendiagramm aus den Topics und ihrer Anzahl.
Anpassbar sind der Titel und die Beschriftungen der Achsen in .update_layout,
die Groesse des Diagramms in .update_layout,
die Breite der Balken in .update_traces,
die Anzahl angezeigter Worte pro Topic, momentan 7.
"""
topic_df = pd.read_csv(csv_path)
# TODO passe die Anzahl der angezeigten Worte pro Topic an, momentan 7, falls gewünscht
shortened_words = topic_df["words_in_topic"].str.split().str[:7].apply(" ".join)
short_words_with_ind = []
for ind, words in enumerate(shortened_words):
new_words = words + " :" + str(ind)
short_words_with_ind.append(new_words)
fig = px.bar(
topic_df,
x=topic_df["number_of_documents"],
y=short_words_with_ind,
orientation="h",
color="number_of_documents",
)
# TODO passe den Titel und evtl. die Achsenbeschriftungen an
fig.update_layout(
title="Balkendiagramm: Beispieltitel",
xaxis_title="Topics",
yaxis_title="Anzahl Dokumente",
yaxis={"categoryorder": "total ascending"},
)
# TODO passe die Breite der Balken und die Groesse des Diagramms an, falls gewünscht
fig.update_traces(width=0.85)
fig.update_layout(width=1400, height=1200)
fig.show()
if __name__ == "__main__":
main()
vrt2docperline.py
python3 vrt2docperline.py <inputfile.vrt> <outputfile.txt> -c <1>
#!/usr/bin/env python3
"""Wandelt .vrt-Files in 1-Text-pro-Zeile Files um.
Mit diesem Skript kann der .vrt-Output von Promethia oder Befehlen wie `cwb-decode -C
<CORPUS> -ALL` in ein Format umgewandelt werden, wie es von `topic-_modeling.py` in
diesem Verzeichnis verarbeitet werden kann.
Das schlussendliche Format enthält ein Dokument/Text pro Zeile. Die Tokens sind mit
Leerzeichen getrennt. Mit der Kommandozeilenoption `-c` (Column) kann angegeben werden,
welche Dimension des Tokens ausgegeben werden soll. Dazu muss im Urspungsfile (.vrt)
nachgeschaut werden, in welcher Token-Spalte sich die relevante Dimension befindet. Ist
es die erste Spalte, wird das Skript mit `-c 0` aufgerufen (Von 0 an zählen).
"""
import argparse
from typing import Generator, List, TextIO
def parse_arguments():
my_parser = argparse.ArgumentParser(
description="Transforms the output of cwb-decode to a tabular format for further processing."
)
my_parser.add_argument(
"input",
action="store",
type=argparse.FileType("r", encoding="utf-8"),
help="Output of cwb-decode",
)
my_parser.add_argument(
"output",
action="store",
type=argparse.FileType("w", encoding="utf-8"),
help="File with a tokenized Document per line",
)
my_parser.add_argument(
"-c",
"--column",
action="store",
type=int,
help="Which column of the token rows in the .vrt-file should be considered",
required=True,
)
return my_parser.parse_args()
def document_generator(infile: TextIO, column: int) -> Generator[List[str], None, None]:
"""Generates token-lists corresponding to a document/text.
Args:
infile (TextIO): Opened File to read from.
column (int): Column number (0-indexed) of the relevant token dimension.
Yields:
List[str]: A list of tokens.
"""
tokens = []
for line in infile:
line = line.strip()
if line == "":
continue
# Is the line part of a structural attribute
elif line.startswith("<") and line.endswith(">"):
if line.startswith("<text"):
tokens = []
elif line.startswith("</text>"):
yield tokens
# Structural attributes apart from `text` are ignored
else:
continue
# Line represents a token
else:
token_dimensions = line.split("\t")
tokens.append(token_dimensions[column])
def main():
args = parse_arguments()
documents = document_generator(args.input, column=args.column)
outfile = args.output
first_doc_str = " ".join(next(documents))
outfile.write(first_doc_str)
for doc in documents:
outfile.write("\n")
doc_str = " ".join(doc)
outfile.write(doc_str)
if __name__ == "__main__":
main()
Literatur
- Installationsanleitung und Skripte übernommen von KoDuP.