Korpusverarbeitung – Annotation mit spaCy#
Übersicht#
Im Folgenden wird exemplarisch ein Text (txt-Datei) mit der Bibliothek spaCy annotiert. Dafür werden folgendene Schritte durchgeführt:
Einlesen des Texts
Worthäufigkeiten ohne echte Tokenisierung
Aufteilen des Texts in Wörter auf Grundlage von Leerzeichen
Abfrage von Häufigkeiten
Annotation mit spaCy
Laden des Sprachmodells
Analysekomponenten auswählen
Text annotieren
Worthäufigkeiten anzeigen
Annotation speichern
Prozess für das gesamte Korpus ausführen
Show code cell content
# Laden von Bibliotheken
from pathlib import Path
from time import time
from collections import OrderedDict, Counter
from tqdm import tqdm
import pandas as pd
import numpy as np
import spacy
import json
from bokeh.io import output_notebook, show
from bokeh.layouts import column
from bokeh.models import CustomJS, TextInput, Div
1. Einlesen des Texts#
Um eine Datei mit Python bearbeiten zu können, muss die Datei zuerst ausgewählt werden, d.h der Pfad zur Datei wird gesetzt und dann eingelesen werden.
text_path = Path("../data/txt/SNP2719372X-19181015-0-0-0-0.txt")
text = text_path.read_text()
print(f"Textauszug:\n {text[10280:10400]}")
Textauszug:
irgendeine ins einzelne gehende Erklärung über die Lage abgeben werde. Der nächste Schritt für den Präsidenten Wilson wi
2. Worthäufigkeiten ohne echte Tokenisierung#
2.1 Text in Wörter aufteilen#
Der einfachste Weg einen Text automatisch in Wörter aufzuteilen, ist anzunehmen, das Wörter durch Leerzeichen getrennt sind.
words = text.split()
Prüfen: Wie sieht die Wortliste aus?
words[100:120]
['respektooller',
'Entfernung,',
'Die',
'beschränkten',
'sich',
'in',
'der',
'Hauptsache',
'de',
'Ortschaften',
'im',
'deutschen',
'Hinter-',
'<->',
'mit',
'Bombengeschwadern',
'anzugreisen.',
'der',
'Zwischenzeit',
'wurde']
Wie viele Wörter gibt es insgesamt?
len(words)
11612
Wie zu sehen ist, hat diese Art der “falschen” Tokenisierung den Nachteil, dass Satzzeichen nicht von Wörtern abgetrennt werden.
Die Wortanzahl ist dementsprechend auch nicht akkurat.
2.2 Anzeigen von Worthäufigkeiten#
Auf Grundlage dieser Wortliste kann trotzdem schon eine erste basale Häufigkeitenabfrage erfolgen:
Show code cell source
word_frequencies = dict(Counter(words))
# Ensure Bokeh output is displayed in the notebook
output_notebook()
# Convert the dictionary to a JSON string
word_freq_json = json.dumps(word_frequencies)
# Create the text input widget
text_input = TextInput(value='', title="Geben Sie ein Wort ein:")
# Create a Div to display the frequency
frequency_display = Div(text="Häufigkeit: ")
# JavaScript callback to update the frequency display
callback = CustomJS(args=dict(frequency_display=frequency_display, text_input=text_input), code=f"""
var word = text_input.value.trim();
console.log('Input value:', word);
// Parse the word frequency dictionary from Python
var word_freq = {word_freq_json};
var frequency = word in word_freq ? word_freq[word] : "not found";
frequency_display.text = "Häufigkeit: " + frequency;
""")
text_input.js_on_change('value', callback)
# Layout and display
layout = column(text_input, frequency_display)
show(layout)
3. Annotation mit spaCy#
Um eine präzisere Einteilung in Wörter zu erhalten (Tokenisierung) und um flektierte Wörter aufeinander abbildbar zu machen (Lemmatisierung), wird der Text im folgenden durch die Bibliothek spaCy annotiert. Dafür werden folgende Schritte ausgeführt:
Das sprachspezifische Modell wird geladen. Wir arbeiten mit dem weniger akkuraten aber schnellsten spaCy Modell
de_core_news_sm
.Für eine erhöhte Annotationsgeschwindigkeit werden nur bestimmte Analysekomponenten geladen. Dies ist vor allem für größere Textmengen sinnvoll.
Der Text wird annotiert und die Token sowie die dazugehörigen Lemmata werden extrahiert.
3.1 Sprachmodell laden#
Das sprachspezifische Modell wird geladen. Es handelt sich dabei um das am wenigsten akkurate aber schnellste Modell.
nlp = spacy.load('de_core_news_sm')
3.2 Analysekomponenten auswählen#
Es werden einige Analysekomponent wie z. B. das Aufteilen des Texts in Sätze (sentencizer) oder die Named Entity Recognition (ner) ausgeschlossen, da diese für die Tokenisierung und die Lemmatisierung nicht benötigt werden. Der Auschluss der Komponentnen erhöht die Annotationsgeschwindikgeit.
disable_components = ['ner', 'morphologizer', 'attribute_ruler', 'sentencizer']
3.3 Annotieren der Texte: Token, Lemma#
Der ausgewählte Text wird mit spaCy annotiert und die Token sowie die dazugehörigen Lemmata werden extrahiert und in einer Tabelle gespeichert. Das Tabellenformat wurde gewählt, da sich darin gut relationale Daten speichern lassen.
current = time()
doc = nlp(text)
text_annotated = {}
text_annotated['Token'] = [tok.text for tok in doc]
text_annotated['Lemma'] = [tok.lemma_ for tok in doc]
text_annotated_df = pd.DataFrame(text_annotated)
took = time() - current
print(f"Die Annotation hat {round(took, 2)} Sekunden gedauert.")
Die Annotation hat 1.49 Sekunden gedauert.
Auszug aus der Tabelle, in der der annotierte Text gespeichert ist:
Show code cell source
text_annotated_df.head()
Token | Lemma | |
---|---|---|
0 | BERLINER | BERLINER |
1 | ||
2 | R | R |
3 | M | M |
4 | AHIRE | AHIRE |
3.4 Worthäufigkeit mit echter Tokenization#
Durch die Tokenisierung wurden z. B. Satzzeichen von Wörtern abgetrennt. An der Textlänge lässt sich dies schon erkennen.
text_tokenized = text_annotated_df.Lemma
len(text_tokenized)
15062
Auf Grundlage des tokenisierten und lemmatisierten Texts, kann die Häufigkeitenabfrage erneut augeführt werden. Da durch die Lemmatisierung flektierte Wortformen auf die Grundformen zurückgeführt wurden, erwarten wir, dass die Häufigkeit einer Wortgrundform im Gegensatz zur vorherigen Abfrage erhöht ist.
Show code cell source
token_frequencies = Counter(text_tokenized)
# Ensure Bokeh output is displayed in the notebook
output_notebook()
# Convert the dictionary to a JSON string
tok_freq_json = json.dumps(token_frequencies)
# Create the text input widget
token_input = TextInput(value='', title="Geben Sie ein Wort ein:")
# Create a Div to display the frequency
token_frequency_display = Div(text="Häufigkeit: ")
# JavaScript callback to update the frequency display
tok_callback = CustomJS(args=dict(frequency_display=token_frequency_display, text_input=token_input), code=f"""
var tok = text_input.value.trim();
console.log('Input value:', tok);
// Parse the word frequency dictionary from Python
var word_freq = {tok_freq_json};
var frequency = tok in word_freq ? word_freq[tok] : "0";
frequency_display.text = "Häufigkeit: " + frequency;
""")
token_input.js_on_change('value', tok_callback)
# Layout and display
layout = column(token_input, token_frequency_display)
show(layout)
4. Annotation speichern#
Um den annotierten Text zu speichern, wird zuerst der Dateiname festgelegt. Dafür wird die Dateiendung ersetzt von .txt
zu .csv
.
CSV (comma-separated value) ist das Standardformat um tabellarische Daten im Klartext zu speichern.
output_path = Path(r"../data/csv") / text_path.with_suffix(".csv").name
text_annotated_df.to_csv(output_path, index=False)
5. Prozess für das gesamte Korpus ausführen#
def read_corpus_linewise(corpus_dir: Path) -> OrderedDict[str,str]:
"""
Reads txt files from a given directory. Returns a dictionary with the filename
as key and the txt file content as value.
:param Path corpus_dir: The directory in which the txt files are saved
:return OrderedDict[str, str]: The file names as keys, the file content as value
"""
corpus = OrderedDict()
for filepath in corpus_dir.iterdir():
if filepath.suffix == ".txt":
text = filepath.read_text()
corpus[filepath.name] = text
return corpus
def annotate_corpus(corpus: OrderedDict[str, str], disable_components: list[str]) -> dict[str, pd.DataFrame]:
"""
Annotate a corpus (filename: text) with spacy. Collect the Token and Lemma information.
Save the annotation information as a pandas DataFrame.
:param OrderedDict[str, str] corpus: The file names as keys, the file content as value
:param list[str] disable_components: spacy components to be diasbled in the annotation process
:return dict[str, pd.DataFrame]: The file name as keys, the annotated text as value
"""
# list to collect how long the annotation runs take in seconds
took_per_text = []
# define result dict
corpus_annotated = {}
filename_list = list(corpus.keys())
current = time()
# iterate over the corpus values, annotate them with spacy
for i, doc in tqdm(enumerate(nlp.pipe(list(corpus.values()), disable=disable_components))):
before = current
current = time()
took_per_text.append(current - before)
# Save the token and lemma information to a dictionary
text_annotated = {}
text_annotated['Token'] = [tok.text for tok in doc]
text_annotated['Lemma'] = [tok.lemma_ for tok in doc]
# Save the annotation as pandas DataFrame to the result dict
# Key is the current filename
corpus_annotated[filename_list[i]] = pd.DataFrame(text_annotated)
# print corpus size and performance
print(f"""Processed {len(corpus_annotated)} texts with spacy.
Took {round(np.mean(took_per_text), 4)} seconds per text on average.
Took {round(np.sum(took_per_text) / 60, 4)} minutes in total.""")
return corpus_annotated
# Korpus einlesen
corpus_dir = Path(r"../data/txt/")
corpus = read_corpus_linewise(corpus_dir)
print(f"Es wurden {len(corpus)} Dateien eingelesen.")
# Korpus annotieren
corpus_annotated = annotate_corpus(corpus, disable_components)
# Annotiertes Korpus speichern
output_dir = Path(r"../data/csv")
for filepath, text_annotated in corpus_annotated.items():
filepath = Path(filepath)
output_path = output_dir / filepath.with_suffix(".csv")
text_annotated.to_csv(output_path, index=False)