Tag Archive for: Text Mining

Wie funktioniert Natural Language Processing in der Praxis? Ein Überblick

Natural Language Processing (NLP,auf Deutsch auch als Computerlinguistik bezeichnet) gilt als ein Teilbereich des Machine Learning und der Sprachwissenschaften.

Beim NLP geht es vom Prinzip um das Extrahieren und Verarbeiten von Informationen, die in den natürlichen Sprachen enthalten sind. Im Rahmen von NLP wird die natürliche Sprache durch den Rechner in Zahlenabfolgen umgewandelt. Diese Zahlenabfolgen kann wiederum der Rechner benutzen, um Rückschlüsse auf unsere Welt zu ziehen. Kurz gesagt erlaubt NLP dem Computer unsere Sprache in ihren verschiedenen Formen zu verarbeiten. 

Eine ausführlichere Definition von NLP wurde auf dem Data Science Blog von Christopher Kipp vorgenommen. 

In diesem Beitrag werde ich dagegen einen Überblick über die spezifischen Schritte im NLP als Prozess darstellen, denn NLP erfolgt in mehreren Phasen, die aufeinander Folgen und zum Teil als Kreislauf verstanden werden können. In ihren Grundlagen ähneln sich diese Phasen bei jeder NLP-Anwendung, sei es Chatbot Erstellung oder Sentiment Analyse.

1. Datenreinigung / Normalisierung 

In dieser Phase werden die rohen Sprachdaten aus ihrem ursprünglichen Format entnommen, sodass am Ende nur reine Textdaten ohne Format erhalten bleiben. 

Beispielsweise können die Textdaten für unsere Analyse aus Webseiten stammen und nach ihrer Erhebung in HTML Code eingebettet sein.

Das Bild zeigt eine Beispielseite. Der Text hier ist noch in einen HTML Kontext eingebettet. Der erste Schritt muss daher sein, den Text von den diversen HTML-Tags zu bereinigen. 

 

2. Tokenisierung und Normalisierung (Tokenizing and Normalizing) 

Nach dem ersten Schritt steht als Ergebnis idealerweise reiner Text da, der aber auch Sprachelemente wie Punkte, Kommata sowie Groß- und Kleinschreibung beinhaltet. 

Hier kommt der nächste Schritt ins Spiel – die Entfernung der Interpunktion vom Text. Der Text wird auf diese Weise auf seine Wort-Bestandteile (sog. Tokens) reduziert. 

Zusätzlich zu diesem Schritt kann auch Groß- und Kleinschreibung entfernt werden (Normalisierung). Dies spart vor allem die Rechenkapazität. 

So wird aus folgendem Abschnitt:

Auf diese Weise können wir die Daten aggregieren und in Subsets analysieren. Wir müssen nicht immer das ganze Machine Learning in Hadoop und Spark auf dem gesamten Datensatz starten.

folgender Text 

auf diese weise können wir die daten aggregieren und in subsets analysieren wir müssen nicht immer das ganze machine learning in hadoop und spark auf dem gesamten datensatz starten

 

3. Füllwörterentfernung / Stop words removal 

Im nächsten Schritt entfernen wir die sogenannten Füllwörter wie „und“, „sowie“, „etc.“. In den entsprechenden Python Bibliotheken sind die gängigen Füllwörter bereits gespeichert und können leicht entfernt werden. Trotzdem ist hier Vorsicht geboten. Die Bedeutung der Füllwörter in einer Sprache verändert sich je nach Kontext. Aus diesem Grund ist dieser Schritt optional und die zu entfernenden Füllwörter müssen kontextabhängig ausgewählt werden. 

Nach diesem Schritt bleibt dann in unserem Beispiel folgender Text erhalten: 

können daten aggregieren subsets analysieren müssen nicht immer machine learning hadoop spark datensatz starten

 

4. Pats of speech (POS) 
Als weiterer Schritt können die Wörter mit ihrer korrekten Wortart markiert werden. Der Rechner markiert sie entsprechend als Verben, Nomen, Adjektive etc. Dieser Schritt könnte für manche Fälle der Grundformreduktion/Lemmatization notwendig sein (dazu sogleich unten).

 

5. Stemming und Lemmatization/Grundformreduktion

In weiteren Schritten kann weiter das sogenannte Stemming und Lemmatization folgen. Vom Prinzip werden hier die einzelnen Wörter in ihre Grundform bzw. Wörterbuchform gebracht. 

Im Fall von Stemming werden die Wörter am Ende einfach abgeschnitten und auf den Wortstamm reduziert. So wäre zum Beispiel das Verb „gehen“, „geht“ auf die Form „geh“ reduziert. 

Im Fall der Lemmatization bzw. Grundformreduktion werden die Wörter in ihre ursprüngliche Wörterbuchform gebracht: das Verb „geht“ wäre dann ins „gehen“ transformiert. 

Parts of Speech, Stemming als auch Lemmatising sind vorteilhaft für die Komplexitätsreduktion. Sie führen deswegen zu mehr Effizienz und schnellerer Anwendbarkeit. Dies geschieht allerdings auf Kosten der Präzision. Die auf diese Weise erstellten Listen können dann im Fall einer Suchmaschine weniger relevante Ergebnisse liefern.

Nachfolgende Schritte beim NLP transformieren den Text in mathematische Zahlenfolgen, die der Rechner verstehen kann. Wie wir in diesem Schritt vorgehen, hängt stark davon ab, was das eigentliche Ziel des Projektes sei. Es gibt ein breites Angebot an Python Paketen, die die Zahlenbildung je nach Projektziel unterschiedlich gestalten

 

6a. Bag of Words Methoden in Python (https://en.wikipedia.org/wiki/Bag-of-words_model)

Zu den Bag of Words Methoden in Python gehört das sogenannte TF-IDF Vectorizer. Die Transformationsmethode mit dem TF-IDF eignet sich beispielsweise zum Bau eines Spamdetektors, da der TF-IDF Vectorizer die Wörter im Kontext des Gesamtdokumentes betrachtet.

 

6b. Word Embeddings Methoden in Python: Word2Vec, GloVe (https://en.wikipedia.org/wiki/Word_embedding)

Wie der Name bereits sagt transformiert Word2Vec die einzelnen Wörter zu Vektoren (Zahlenfolgen). Dabei werden ähnliche Wörter zu ähnlichen Vektoren transformiert. Die Methoden aus der Word Embeddings Kiste eignen sich zum Beispiel besser, um einen Chatbot zu erstellen. 

Im letzten Schritt des NLP können wir die so prozessierte Sprache in die gängigen Machine Learning Modelle einspeisen. Das Beste an den oben erwähnten NLP Techniken ist die Transformation der Sprache in Zahlensequenzen, die durch jeden ML Algorithmus analysiert werden können. Die weitere Vorgehensweise hängt hier nur noch vom Ziel des Projektes ab. 

Dies ist ein Überblick über die notwendigen (und optionalen) Schritte in einem NLP Verfahren. Natürlich hängt die Anwendung vom jeweiligen Use Case ab. Die hier beschriebenen NLP Phasen nehmen viele Ungenauigkeiten in Kauf, wie zum Beispiel die Reduzierung der Wörter auf Wortstämmen bzw. den Verzicht auf Großschreibung. Bei der Umsetzung in der Praxis müssen immer Kosten und Nutzen abgewogen werden und das Verfahren dem besonderen Fall angepasst werden. 

Quellen:
  • Mandy Gu: „Spam or Ham: Introduction to Natural Language Processing Part 2“ https://towardsdatascience.com/spam-or-ham-introduction-to-natural-language-processing-part-2-a0093185aebd
  • Christopher D. Manning, Prabhakar Raghavan & Hinrich Schütze: „Introduction to Information Retrieval”, Cambridge University Press, https://nlp.stanford.edu/IR-book/
  • Hobson Lane, Cole Howard, Hannes Max Hapke: „Natural Language Processing in Action. Understanding, analyzing, and generating text with Python.” Manning Shelter Island

Fuzzy Matching mit dem Jaro-Winkler-Score zur Auswertung von Markenbekanntheit und Werbeerinnerung

Für Unternehmen sind Markenbekanntheit und Werbeerinnerung wichtige Zielgrößen, denn anhand dieser lässt sich ableiten, ob Konsumenten ein Produkt einer Marke kaufen werden oder nicht. Zielgrößen wie diese werden von Marktforschungsinstituten über Befragungen ermittelt. Dafür wird in regelmäßigen Zeitabständen eine gleichbleibende Anzahl an Personen befragt, ob diese sich an Marken einer bestimmten Branche erinnern oder sich an Werbung erinnern. Die Personen füllen dafür in der Regel einen Onlinefragebogen aus.

Die Ergebnisse der Befragung liegen in einer Datenmatrix (siehe Tabelle) vor und müssen zur Auswertung zunächst bearbeitet werden.

Laufende Nummer Marke 1 Marke 2 Marke 3 Marke 4
1 ING-Diba Citigroup Sparkasse
2 Sparkasse Consorsbank
3 Commerbank Deutsche Bank Sparkasse ING-DiBa
4 Sparkasse Targobank

Ziel ist es aus diesen Daten folgende 0/1 codierte Matrix zu generieren. Wenn eine Marke bekannt ist, wird in die zur Marke gehörende Spalte eine Eins eingetragen, ansonsten eine Null.

Alle Marken ING-Diba Citigroup Sparkasse Targobank
ING-Diba, Citigroup, Sparkasse 1 1 1 0
Sparkasse, Consorsbank 0 0 1 0
Commerzbank, Deutsche Bank, Sparkasse, ING-Diba 1 0 0 0
Sparkasse, Targobank 0 0 1 1

Der Workflow um diese Datentransformation durchzuführen ist oftmals mittels eines Teilstrings einer Marke zu suchen ob diese in einem über alle Nennungen hinweg zusammengeführten String vorkommt oder nicht (z.B. „argo“ bei Targobank). Das Problem dieser Herangehensweise ist, dass viele falsch geschriebenen Wörter so nicht erfasst werden und die Erfahrung zeigt, dass falsch geschriebene Marken in vielfältigster Weise auftreten. Hier mussten in der Vergangenheit Mitarbeiter sich in stundenlangem Kampf durch die Ergebnisse wühlen und falsch zugeordnete oder nicht zugeordnete Marken händisch korrigieren und alle Variationen der Wörter notieren, um für die nächste Befragung das Suchpattern zu optimieren.

Eine Alternative diesen aufwändigen Workflow stellt die Ermittlung von falsch geschriebenen Wörtern mittels des Jaro-Winkler-Scores dar. Dafür muss zunächst die Jaro-Winkler-Distanz zwischen zwei Strings berechnet werden. Diese berechnet sich wie folgt:

d_j = frac{1}{3}(frac{m}{|s_1|}+frac{m}{|s_2|}+frac{m - t}{m})

  • m: Anzahl der übereinstimmenden Buchstaben
  • s: Länge des Strings
  • t: Hälfte der Anzahl der Umstellungen der Buchstaben die nötig sind, damit Strings identisch sind. („Ta“ und „gobank“ befinden sich bereits in der korrekten Reihenfolge, somit gilt: t = 0)

Aus dem Ergebnis lässt sich der Jaro-Winkler Score berechnen:
d_w = d_j + (l_p (1 - d_j))
ist dabei die Jaro-Winkler-Distanz, l die Länge der übereinstimmenden Buchstaben von Beginn des Wortes bis zum maximal vierten Buchstaben und p ein konstanter Faktor von 0,1.

Für die Strings „Targobank“ und „Tangobank“ ergibt sich die Jaro-Winkler-Distanz:

d_j = frac{1}{3}(frac{8}{9}+frac{8}{9}+frac{8 - 0}{9})

Daraus wird im nächsten Schritt der Jaro-Winkler Score berechnet:

d_w = 0,9259 + (2 cdot 0,1 (1 - 0,9259)) = 0,9407407

Bisherige Erfahrungen haben gezeigt, dass sich Scores ab 0,8 bzw. 0,9 am besten zur Suche von ähnlichen Wörtern eignen. Ein Schwellenwert darunter findet sehr viele Wörter, die sich z.B. auch anderen Wörtern zuordnen lassen. Ein Schwellenwert über 0,9 identifiziert falsch geschriebene Wörter oftmals nicht mehr.

Nach diesem theoretischen Exkurs möchte ich nun zeigen, wie sich das Ganze praktisch anwenden lässt. Da sich das Ganze um ein fiktives Beispiel handelt, werden zur Demonstration der Praxistauglichkeit Fakedaten mit folgendem Code erzeugt. Dabei wird angenommen, dass Personen unterschiedlich viele Banken kennen und diese mit einer bestimmten Wahrscheinlichkeit falsch schreiben.

# Erstellung von Fakeantworten
set.seed(1234)
library(stringi)
library(tidyr)
library(RecordLinkage)
library(xlsx)
library(tm)
library(qdap)
library(stringr)
library(openxlsx)

konsonant <- c("r", "n", "g", "h", "b")
vokal <- c("a", "e", "o", "i", "u")

# Funktion, die mit einer zu bestimmenden Wahrscheinlichkeit, einen zufälligen Buchstaben erzeugt.
generate_wrong_words <- function(x, p, k = TRUE) {
  if(runif(1, 0, 1) > p) { # Zufallswert zwischen 0 und 1
    if(k == TRUE) { # Konsonant oder Vokal erzeugen
      string <- konsonant[sample.int(5, 1)] # Zufallszahl, die Index des Konsonnanten-Vektors bestimmt.
    } else {
      string <- vokal[sample.int(5, 1)] # Zufallszahl, die Index eines Vokal-Vecktors bestimmt.
    }
  } else {
    string <- x
  }
  return(string)
}

randombank <- function(x) {
  random_num <- runif(1, 0, 1)
  if(random_num  > x) { ## Wahrscheinlichkeit, dass Person keine Bank kennt.
    number <- sample.int(7, 1)
    if(number == 1) {
      bank <- paste0("Ta", generate_wrong_words(x = "r", p = 0.7), "gob", generate_wrong_words(x = "a", p = 0.9), "nk")
    } else if (number == 2) {
      bank <- paste0("Ing-di", generate_wrong_words(x = "b", p = 0.6), "a")
    } else if (number == 3) {
      bank <- paste0("com", generate_wrong_words(x = "m", p = 0.7), "erzb", generate_wrong_words(x = "a", p = 0.8), "nk")
    } else if (number == 4){
      bank <- paste0("Deutsch", generate_wrong_words(x = "e", p = 0.6, k = FALSE), " Ban", generate_wrong_words(x = "k", p = 0.8))
    } else if (number == 5) {
      bank <- paste0("Spark", generate_wrong_words(x = "a", p = 0.7, k = FALSE), "sse")
    } else if (number == 6) {
      bank <- paste0("Cons", generate_wrong_words(x = "o", p = 0.7, k = FALSE), "rsbank")
    } else {
      bank <- paste0("Cit", generate_wrong_words(x = "i", p = 0.7, k = FALSE), "gro", generate_wrong_words(x = "u", p = 0.9, k = FALSE), "p")
    }
  } else {
    bank <- "" # Leerer String, wenn keine Bank bekannt.
  }
  return(bank)
}


# DataFrame erzeugen, in dem Werte gespeichert werden.
df_raw <- data.frame(matrix(ncol = 8, nrow = 2500))

# Erzeugen von richtig und falsch geschrieben Banken mit einer durch bestimmten Variabilität an Banken, welche die Personen kennen.
for(i in 1:2500) {
  df_raw [i, 1] <- i # Laufende Nummer des Befragten
  df_raw [i, 2] <- randombank(x = 0.05)
  if(df_raw [i, 2] == "") { df_raw [i, 3] <- "" } else {df_raw [i, 3] <- randombank(x = 0.1)}
  if(df_raw [i, 3] == "") { df_raw [i, 4] <- "" } else {df_raw [i, 4] <- randombank(x = 0.1)}
  if(df_raw [i, 4] == "") { df_raw [i, 5] <- "" } else {df_raw [i, 5] <- randombank(x = 0.15)} 
  if(df_raw [i, 5] == "") { df_raw [i, 6] <- "" } else {df_raw [i, 6] <- randombank(x = 0.15)}
  if(df_raw [i, 6] == "") { df_raw [i, 7] <- "" } else {df_raw [i, 7] <- randombank(x = 0.2)} 
  if(df_raw [i, 7] == "") { df_raw [i, 8] <- "" } else {df_raw [i, 8] <- randombank(x = 0.2)} 
}
colnames(df_raw)[1] <- "lfdn"

Ausführen:

head(df_raw)

Nun werden die Inhalte der Spalten in eine einzige Spalte zusammengefasst und jede Marke per Komma getrennt.

df <- unite(df_raw, united, c(2:ncol(df_raw)), sep = ",")
colnames(df)[2] <- "text"
# Gesuchte Banken (nur korrekt geschrieben)
startliste <- c("Targobank", "Ing-DiBa", "Commerzbank", "Deutsche Bank", "Sparkasse", "Consorsbank", "Citigroup")

Damit Sonderzeichen, Leerzeichen oder Groß- und Kleinschreibung keine Rolle spielen, werden alle Strings vereinheitlicht und störende Zeichen entfernt.

dftext <- tolower(dftext)
dftext <- str_trim(dftext)
dftext <- gsub(" ", "", dftext)
dftext <- gsub("[?]", "", dftext)
dftext <- gsub("[-]", "", dftext)
dftext <- gsub("[_]", "", dftext)

startliste <- tolower(startliste)
startliste <- str_trim(startliste)
startliste <- gsub(" ", "", startliste)
startliste <- gsub("[?]", "", startliste)
startliste <- gsub("[-]", "", startliste)
startliste <- gsub("[_]", "", startliste)

Im nächsten Schritt wird geprüft welche Schreibweisen überhaupt existieren. Dafür eignet sich eine Word-Frequency-Matrix, mit der alle einzigartigen Wörter und deren Häufigkeiten in einem Vektor gezählt wird.

words <- as.data.frame(wfm(dftext)) # Jedes einzigartige Wort und dazugehörige Häufigkeiten. words <- rownames(words) # wfm zählt Häufigkeiten jedes Wortes und schreibt Wörter in rownames, wir brauchen jedoch das Wort selbst. </pre> Danach wird eine leere Liste erstellt, in der iterativ für jedes Element des Suchvektors ein Charactervektor erzeugt wird, der Wörter enthält, die einen Jaro-Winker Score von 0,9 oder höher besitzen. <pre class="theme:github lang:r decode:true ">for(i in 1:length(startliste)) {   finalewortliste[[i]] <- words[which(jarowinkler(startliste[[i]], words) > 0.9)] } </pre> Jetzt wird ein leerer DataFrame erzeugt, der die Zeilenlänge des originalen DataFrames besitzt sowie die Anzahl der Marken als Spaltenlänge. <pre class="theme:github lang:r decode:true ">finaldf <- data.frame(matrix(nrow = nrow(df), ncol = length(startliste))) colnames(finaldf) <- startliste </pre> Im nächsten Schritt wird nun aus den ähnlichen Wörtern mit einer oder-Verknüpfung einen String erzeugt, der alle durch den Jaro-Winkler-Score identifizierten Wörter beinhaltet. Wenn ein Treffer gefunden wird, wird in der Suchspalte eine Eins eingetragen, ansonsten eine Null. <pre class="theme:github lang:r decode:true ">for(i in 1:ncol(finaldf)) {   finaldf[i] <- ifelse(str_detect(dftext, paste(finalewortliste[[i]], collapse = "|")) == TRUE, 1, 0) 
}

Zuletzt wird eine Spalte erzeugt, in die eine Eins geschrieben wird, wenn keine der Marken gefunden wurde.

finaldfkeinedergeannten <- ifelse(rowSums(finaldf) > 0, 0, 1) # Wenn nicht mindestens eine der gesuchten Banken bekannt </pre> Nach der fertigen Berechnung der Matrix können nun die finalen KPI´s berechnet und als Report in eine .xlsx Datei geschrieben werden. <pre class="theme:github lang:r decode:true "># Prozentuale Anteile berechnen. anteil <- as.data.frame(t(sapply(finaldf, sum) / nrow(finaldf) * 100)) # Ordne dem DataFrame die ursprünglichen Nenneungen zu. finaldf <- cbind(dftext, finaldf)
colnames(finaldf)[1] <- "text"

# Ergebnisse in eine .xlsx Datei schreiben.
wb <- createWorkbook()
addWorksheet(wb, "Ergebnisse")    
writeData(wb, "Ergebnisse", anteil, startCol = 2, startRow = 1, rowNames = FALSE)
writeData(wb, "Ergebnisse", finaldf, startCol = 1, startRow = 4, rowNames = FALSE)
saveWorkbook(wb, paste0("C:/Users/User/Desktop/Results_", Sys.Date(), ".xlsx"), overwrite = TRUE)  

Dieses Vorgehen kann natürlich nicht verhindern, dass sich jemand mit kritischem Auge die Daten anschauen muss. In mehreren Tests ergaben sich bei einer Fallzahl von ~10.000 Antworten Genauigkeiten zwischen 95% und 100%, was bisherige Ansätze um ein Vielfaches übertrifft.9407407

Einstieg in Natural Language Processing – Teil 2: Preprocessing von Rohtext mit Python

Dies ist der zweite Artikel der Artikelserie Einstieg in Natural Language Processing.

In diesem Artikel wird das so genannte Preprocessing von Texten behandelt, also Schritte die im Bereich des NLP in der Regel vor eigentlichen Textanalyse durchgeführt werden.

Tokenizing

Um eingelesenen Rohtext in ein Format zu überführen, welches in der späteren Analyse einfacher ausgewertet werden kann, sind eine ganze Reihe von Schritten notwendig. Ganz allgemein besteht der erste Schritt darin, den auszuwertenden Text in einzelne kurze Abschnitte – so genannte Tokens – zu zerlegen (außer man bastelt sich völlig eigene Analyseansätze, wie zum Beispiel eine Spracherkennung anhand von Buchstabenhäufigkeiten ect.).

Was genau ein Token ist, hängt vom verwendeten Tokenizer ab. So bringt NLTK bereits standardmäßig unter anderem BlankLine-, Line-, Sentence-, Word-, Wordpunkt- und SpaceTokenizer mit, welche Text entsprechend in Paragraphen, Zeilen, Sätze, Worte usw. aufsplitten. Weiterhin ist mit dem RegexTokenizer ein Tool vorhanden, mit welchem durch Wahl eines entsprechenden Regulären Ausdrucks beliebig komplexe eigene Tokenizer erstellt werden können.

Üblicherweise wird ein Text (evtl. nach vorherigem Aufsplitten in Paragraphen oder Sätze) schließlich in einzelne Worte und Interpunktionen (Satzzeichen) aufgeteilt. Hierfür kann, wie im folgenden Beispiel z. B. der WordTokenizer oder die diesem entsprechende Funktion word_tokenize() verwendet werden.

rawtext = 'This is a short example text that needs to be cleaned.'

tokens = nltk.word_tokenize(rawtext)

tokens
['This', 'is', 'a', 'short', 'example', 'text', 'that', 'needs', 'to',  'be',  'cleaned',  '.']

Stemming & Lemmatizing

Andere häufig durchgeführte Schritte sind Stemming sowie Lemmatizing. Hierbei werden die Suffixe der einzelnen Tokens des Textes mit Hilfe eines Stemmers in eine Form überführt, welche nur den Wortstamm zurücklässt. Dies hat den Zweck verschiedene grammatikalische Formen des selben Wortes (welche sich oft in ihrer Endung unterscheiden (ich gehe, du gehst, er geht, wir gehen, …) ununterscheidbar zu machen. Diese würden sonst als mehrere unabhängige Worte in die darauf folgende Analyse eingehen.

Neben bereits fertigen Stemmern bietet NLTK auch für diesen Schritt die Möglichkeit sich eigene Stemmer zu programmieren. Da verschiedene Stemmer Suffixe nach unterschiedlichen Regeln entfernen, sind nur die Wortstämme miteinander vergleichbar, welche mit dem selben Stemmer generiert wurden!

Im forlgenden Beispiel werden verschiedene vordefinierte Stemmer aus dem Paket NLTK auf den bereits oben verwendeten Beispielsatz angewendet und die Ergebnisse der gestemmten Tokens in einer Art einfachen Tabelle ausgegeben:

# Ready-to-use stemmers in nltk
porter = nltk.PorterStemmer()
lancaster = nltk.LancasterStemmer()
snowball = nltk.SnowballStemmer(language='english')

# Printing a table to compare the different stemmers
header = 'Token\tPorter\tLancas.\tSnowball'
print(header + '\n' + len(header) * '-')
for token in tokens:
    print('\t'.join([token, porter.stem(token), lancaster.stem(token), snowball.stem(token)]))


Token	Porter	Lancas.	Snowball
-----------------------------
This	thi 	thi 	this
is  	is  	is  	is
a    	a    	a    	a
short	short	short	short
example	exampl	exampl	exampl
text	text	text	text
that	that	that	that
needs	need	nee	need
to  	to  	to  	to
be  	be  	be  	be
cleaned	clean	cle 	clean
.   	.   	.   	.

Sehr ähnlich den Stemmern arbeiten Lemmatizer: Auch ihre Aufgabe ist es aus verschiedenen Formen eines Wortes die jeweilige Grundform zu bilden. Im Unterschied zu den Stemmern ist das Lemma eines Wortes jedoch klar als dessen Grundform definiert.

from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()

lemmas = [lemmatizer.lemmatize(t) for t in tokens()]

Vokabular

Auch das Vokabular, also die Menge aller verschiedenen Worte eines Textes, ist eine informative Kennzahl. Bezieht man die Größe des Vokabulars eines Textes auf seine gesamte Anzahl verwendeter Worte, so lassen sich hiermit Aussagen zu der Diversität des Textes machen.

Außerdem kann das auftreten bestimmter Worte später bei der automatischen Einordnung in Kategorien wichtig werden: Will man beispielsweise Nachrichtenmeldungen nach Themen kategorisieren und in einem Text tritt das Wort „DAX“ auf, so ist es deutlich wahrscheinlicher, dass es sich bei diesem Text um eine Meldung aus dem Finanzbereich handelt, als z. B. um das „Kochrezept des Tages“.

Dies mag auf den ersten Blick trivial erscheinen, allerdings können auch mit einfachen Modellen, wie dem so genannten „Bag-of-Words-Modell“, welches nur die Anzahl des Auftretens von Worten prüft, bereits eine Vielzahl von Informationen aus Texten gewonnen werden.

Das reine Vokabular eines Textes, welcher in der Variable “rawtext” gespeichert ist, kann wie folgt in der Variable “vocab” gespeichert werden. Auf die Ausgabe wurde in diesem Fall verzichtet, da diese im Falle des oben als Beispiel gewählten Satzes den einzelnen Tokens entspricht, da kein Wort öfter als ein Mal vorkommt.

from nltk import wordpunct_tokenizer
from nltk.stem import WordNetLemmatizer

lemma = WordNetLemmatizer()

vocab = set([WordNetLemmatizer().lemmatize(t) for t in wordpunct_tokenize(text.lower())])

Stopwords

Unter Stopwords werden Worte verstanden, welche zwar sehr häufig vorkommen, jedoch nur wenig Information zu einem Text beitragen. Beispiele in der beutschen Sprache sind: der, und, aber, mit, …

Sowohl NLTK als auch cpaCy bringen vorgefertigte Stopwordsets mit. 

from nltk.corpus import stopwords
stoplist = stopwords.words('english')
stopset = set(stopwords.words('english'))

[t for t in tokens if not t in stoplist]
['This', 'short', 'example', 'text', 'needs', 'cleaned', '.']

Vorsicht: NLTK besitzt eine Stopwordliste, welche erst in ein Set umgewandelt werden sollte um die lookup-Zeiten kurz zu halten – schließlich muss jedes einzelne Token des Textes auf das vorhanden sein in der Stopworditerable getestet werden!

%timeit [w for w in tokens if not w in stopset] # 1.11 ms
%timeit [w for w in tokens if not w in stoplist] # 26.6 ms

POS-Tagging

POS-Tagging steht für „Part of Speech Tagging“ und entspricht ungefähr den Aufgaben, die man noch aus dem Deutschunterricht kennt: „Unterstreiche alle Subjekte rot, alle Objekte blau…“. Wichtig ist diese Art von Tagging insbesondere, wenn man später tatsächlich strukturiert Informationen aus dem Text extrahieren möchte, da man hierfür wissen muss wer oder was als Subjekt mit wem oder was als Objekt interagiert.

Obwohl genau die selben Worte vorkommen, bedeutet der Satz „Die Katze frisst die Maus.“ etwas anderes als „Die Maus frisst die Katze.“, da hier Subjekt und Objekt aufgrund ihrer Reihenfolge vertauscht sind (Stichwort: Subjekt – Prädikat – Objekt ).

Weniger wichtig ist dieser Schritt bei der Kategorisierung von Dokumenten. Insbesondere bei dem bereits oben erwähnten Bag-of-Words-Modell, fließen POS-Tags überhaupt nicht mit ein.

Und weil es so schön einfach ist: Die obigen Schritte mit spaCy

Die obigen Methoden und Arbeitsschritte, welche Texte die in natürlicher Sprache geschrieben sind, allgemein computerzugänglicher und einfacher auswertbar machen, können beliebig genau den eigenen Wünschen angepasst, einzeln mit dem Paket NLTK durchgeführt werden. Dies zumindest einmal gemacht zu haben, erweitert das Verständnis für die funktionsweise einzelnen Schritte und insbesondere deren manchmal etwas versteckten Komplexität. (Wie muss beispielsweise ein Tokenizer funktionieren der den Satz “Schwierig ist z. B. dieser Satz.” korrekt in nur einen Satz aufspaltet, anstatt ihn an jedem Punkt welcher an einem Wortende auftritt in insgesamt vier Sätze aufzuspalten, von denen einer nur aus einem Leerzeichen besteht?) Hier soll nun aber, weil es so schön einfach ist, auch das analoge Vorgehen mit dem Paket spaCy beschrieben werden:

import spacy

nlp = spacy.load('en')
doc = nlp(rawtext)

Dieser kurze Codeabschnitt liest den an spaCy übergebenen Rohtext in ein spaCy Doc-Object ein und führt dabei automatisch bereits alle oben beschriebenen sowie noch eine Reihe weitere Operationen aus. So stehen neben dem immer noch vollständig gespeicherten Originaltext, die einzelnen Sätze, Worte, Lemmas, Noun-Chunks, Named Entities, Part-of-Speech-Tags, ect. direkt zur Verfügung und können.über die Methoden des Doc-Objektes erreicht werden. Des weiteren liegen auch verschiedene weitere Objekte wie beispielsweise Vektoren zur Bestimmung von Dokumentenähnlichkeiten bereits fertig vor.

Die Folgende Übersicht soll eine kurze (aber noch lange nicht vollständige) Übersicht über die automatisch von spaCy generierten Objekte und Methoden zur Textanalyse geben:

# Textabschnitte
doc.text                                 # Originaltext
sents = doc.sents                        # Sätze des Dokuments
tokens = [token for token in doc]        # Tokens/Worte des Dokuments
parags = doc.text_with_ws.split('\n\n')  # Absätze des Dokuments

# Eigenschaften einzelner Tokens
[t.lemma_ for t in doc]                  # Lemmata der einzelnen Tokens
[t.tag_ for t in doc]                    # POS-Tags der einzelnen Tokens

# Objekte zur Textanalyse
doc.vocab                                # Vokabular des Dokuments
doc.sentiment                            # Sentiment des Dokuments
doc.noun_chunks                          # NounChunks des Dokuments
entities = [ent for ent in doc.ents]     # Named Entities (Persons, Locations, Countrys)

# Objekte zur Dokumentenklassifikation
doc.vector                               # Vektor
doc.tensor                               # Tensor

Diese „Vollautomatisierung“ der Vorabschritte zur Textanalyse hat jedoch auch seinen Preis: spaCy geht nicht gerade sparsam mit Ressourcen wie Rechenleistung und Arbeitsspeicher um. Will man einen oder einige Texte untersuchen so ist spaCy oft die einfachste und schnellste Lösung für das Preprocessing. Anders sieht es aber beispielsweise aus, wenn eine bestimmte Analyse wie zum Beispiel die Einteilung in verschiedene Textkategorien auf eine sehr große Anzahl von Texten angewendet werden soll. In diesem Fall, sollte man in Erwägung ziehen auf ressourcenschonendere Alternativen wie zum Beispiel gensim auszuweichen.

Wer beim lesen genau aufgepasst hat, wird festgestellt haben, dass ich im Abschnitt POS-Tagging im Gegensatz zu den anderen Abschnitten auf ein kurzes Codebeispiel verzichtet habe. Dies möchte ich an dieser Stelle nachholen und dabei gleich eine Erweiterung des Pakets spaCy vorstellen: displaCy.

Displacy bietet die Möglichkeit, sich Zusammenhänge und Eigenschaften von Texten wie Named Entities oder eben POS-Tagging graphisch im Browser anzeigen zu lassen.

import spacy
from spacy import displacy

rawtext = 'This is a short example sentence that needs to be cleaned.'

nlp = spacy.load('en')
doc = nlp(rawtext)
displacy.serve(doc, style='dep')

Nach ausführen des obigen Codes erhält man eine Ausgabe die wie folgt aussieht:

Serving on port 5000...
Using the 'dep' visualizer

Nun öffnet man einen Browser und ruft die URL ‘http://127.0.0.1:5000’ auf (Achtung: localhost anstatt der IP funktioniert – warum auch immer – mit displacy nicht). Im Browser sollte nun eine Seite mit einem SVG-Bild geladen werden, welches wie folgt aussieht

Die Abbildung macht deutlich was POS-Tagging genau ist und warum es von Nutzen sein kann wenn man Informationen aus einem Text extrahieren will. Jedem Word (Token) ist eine Wortart zugeordnet und die Beziehung der einzelnen Worte durch Pfeile dargestellt. Dies ermöglicht es dem Computer zum Beispiel in dem Satzteil “der grüne Apfel”, das Adjektiv “grün” auf das Nomen “Apfel” zu beziehen und diesem somit als Eigenschaft zuzuordnen.

Nachdem dieser Artikel wichtige Schritte des Preprocessing von Texten beschrieben hat, geht es im nächsten Artikel darum was man an Texten eigentlich analysieren kann und welche Analysemöglichkeiten die verschiedenen für Python vorhandenen Module bieten.

Einstieg in Natural Language Processing – Teil 1: Natürliche vs. Formale Sprachen

Dies ist Artikel 1 von 4 der Artikelserie Einstieg in Natural Language Processing – Artikelserie.

Versuche und erste Ansätze, Maschinen beizubringen menschliche Sprache zu verstehen, gibt es bereits seit den 50er Jahren. Trotz der jahrzehntelangen Forschung und Entwicklung gelingt dies bis heute nicht umfassend. Woran liegt dies?

Um diese Frage zu beantworten, hilft es, sich die Unterschiede zwischen „natürlichen“, also sich selbstständig entwickelnden, typischerweise von Menschen gesprochenen Sprachen und den von Computern interpretieren formalen Sprachen klar zu machen. Formale Sprachen, wie zum Beispiel Python zum Ausführen der Codebeispiele in dieser Artikelserie, HTML (Hyper Text Markup Language) zur Darstellung von Webseiten und andere typische Programmier- und Skriptsprachen, sind üblicherweise sehr streng strukturiert.

Alle diese Sprachen weisen eine Reihe von Gemeinsamkeiten auf, welche es Computern einfach machen, sie korrekt zu interpretieren (also den Informationsinhalt zu “verstehen”). Das vermutlich auffälligste Merkmal formaler Sprachen ist eine relativ strikte Syntax, welche (wenn überhaupt) nur geringe Abweichungen von einem Standard erlaubt. Wie penibel die jeweilige Syntax oft einzuhalten ist, wird am ehesten deutlich, wenn diese verletzt wird:

>>> print('Correct Syntax')
Correct Syntax

>>> print{'Wrong Syntax'}
    print{'Wrong Syntax'}
         ^
SyntaxError: invalid syntax

Solche so genannten “Syntax Error”  gehören daher zu den häufigsten Fehlern beim Schreiben von Quellcode.

Ganz anders dagegen sieht es in der Kommunikation mit natürlichen Sprachen aus. Zwar fördert falsche Komma-Setzung in der Regel nicht die Leserlichkeit eines Textes, jedoch bleibt dieser in der Regel trotzdem verständlich. Auch macht es keinen Unterschied ob ich sage „Es ist heiß heute.“ oder „Heute ist es heiß.“. Genau wie in der deutschen Sprache funktioniert dieses Beispiel auch im Englischen sowie in anderen natürlichen Sprachen. Insbesondere Spanisch ist ein Beispiel für eine Sprache mit extrem variabler Satzstellung. Jedoch kann in anderen Fällen eine andere Reihenfolge der selben Worte deren Bedeutung auch verändern. So ist „Ist es heute heiß?“ ganz klar eine Frage, obwohl exakt die selben Worte wie in den Beispielsätzen oben vorkommen.

Ein weiterer wichtiger, hiermit verwandter Unterschied ist, dass es bei formalen Sprachen in der Regel einen Ausdruck gibt, welcher eine spezifische Bedeutung besitzt, während es in natürlichen Sprachen oft viele Synonyme gibt, die ein und dieselbe Sache (oder zumindest etwas sehr ähnliches) ausdrücken. Ein wahrer boolscher Wert wird in Python als

True

geschrieben. Es gibt keine andere Möglichkeit, diesen Wert auszudrücken (zumindest nicht ohne irgend eine Art von Operatoren wie das Doppelgleichheitszeichen zu benutzen und damit z. B. “0 == 0” zu schreiben).  Anders hingegen zum Beispiel in der Deutschen Sprache: Wahr, richtig, korrekt, stimmt, ja,

Um einen Vorstellung davon zu bekommen, wie verbreitet Synonyme in natürlichen Sprachen sind, lässt sich die Internetseite https://www.openthesaurus.de verwenden. Beispielshalber findet man dutzende Synonyme für das Wort „schnell“ hier: https://www.openthesaurus.de/synonyme/schnell

Eine weitere große Schwierigkeit, welche in den meisten natürlichen Sprachen und nahezu allen Arten von Texten zu finden ist, stellen verschiedene grammatikalische Formen eines Wortes dar. So sind die Worte bin, wäre, sind, waren, wirst, werden… alles Konjugationen desselben Verbs, nämlich sein. Eine durchaus beeindruckende Übersicht über die verwirrende Vielfalt von Konjugationen dieses kleinen Wörtchens, findet sich unter: https://www.verbformen.de/konjugation/sein.htm.

Dieses Problem wird um so schwerwiegender, da viele Verben, insbesondere die am häufigsten genutzten, sehr unregelmäßige Konjugationsformen besitzen und damit keiner generellen Regel folgen. Daher ist computerintern oft ein Mapping für jede mögliche Konjugationsform bei vielen Verben die einzige Möglichkeit, an die Grundform zu kommen (mehr dazu in Teil 3 dieser Artikelserie).

Die Liste der sprachlichen Schwierigkeiten beim computergestützten Auswerten natürlicher Sprache ließe sich an diesem Punkt noch beliebig weiter fortsetzen:

  • Rechtschreibfehler
  • falsche Grammatik
  • Smileys
  • der „Substantivverkettungswahn“ im Deutschen
  • mehrdeutige Worte und Abkürzungen
  • abwegige Redewendungen (z. B. “ins Gras beißen”)
  • Ironie
  • und, und, und …

Ob und welche Rolle jede dieser Schwierigkeiten im einzelnen spielt, hängt natürlich sehr stark von den jeweiligen Texten ab und kann nicht pauschalisiert werden – ein typischer Chatverlauf wird ganz andere Probleme bereithalten als ein Wikipedia-Artikel. Wie man einige dieser Probleme in der Praxis vereinfachen oder sogar lösen kann und welche Ansätze und Methoden zur Verfügung stehen und regelmäßig zur Anwendung kommen wird im nächsten Teil dieser Artikelserie an praktischen Codebeispielen genauer unter die Lupe genommen.

NLTK vs. Spacy – Eine kurze Übersicht

Möchte man einen (oder auch einige) Text(e) mit den Methoden des natural language processings untersuchen um die darin verwendete Sprache auswerten oder nach bestimmten Informationen suchen, so sind insbesondere die Pakete NLTK und spaCy zu empfehlen (bei sehr vielen Texten sieht das schon wieder anders aus und wird am Ende der Artikelserie mit dem Paket gensim vorgestellt); beide bieten eine unglaubliche Vielzahl von Analysemöglichkeiten, vorgefertigten Wortsets, vortrainierte Stemmer und Lemmatiser, POS Tagger und, und, und…

Ist man vor allem an den Ergebnissen der Analyse selbst interessiert, so bietet sich spaCy an, da hier bereits mit wenigen Zeilen Code viele interessante Informationen generiert werden können.

Wer dagegen gerne selber bastelt oder wissen möchte wie die einzelnen Tools und Teilschritte genau funktionieren oder sich seine eigenen Stemmer, Tagger ect. trainieren will, ist vermutlich mit NLTK besser beraten. Zwar ist hier oft mehr Quellcode für das gleiche Ergebnis notwendig, allerdings kann das Preprocessing der Texte hierbei relativ einfach exakt den eigenen Vorstellungen angepasst werden. Zudem bietet NLTK eine Vielzahl von Beispieltexten und bereits fertig getagte Daten, mit welchen eigene Tagger trainiert und getestet werden können.

Einstieg in Natural Language Processing – Artikelserie

Unter Natural Language Processing (NLP) versteht man ein Teilgebiet der Informatik bzw. der Datenwissenschaft, welches sich mit der Analyse und Auswertung , aber auch der Synthese natürlicher Sprache befasst. Mit natürlichen Sprachen werden Sprachen wie zum Beispiel Deutsch, Englisch oder Spanisch bezeichnet, welche nicht geplant entworfen wurden, sondern sich über lange Zeit allein durch ihre Benutzung entwickelt haben. Anders ausgedrückt geht es um die Schnittstelle zwischen unserer im Alltag verwendeten und für uns Menschen verständlichen Sprache auf der einen, und um deren computergestützte Auswertung auf der anderen Seite.

Diese Artikelserie soll eine Einführung in die Thematik des Natural Language Processing sein, dessen Methoden, Möglichkeiten, aber auch der Grenzen . Im einzelnen werden folgende Themen näher behandelt:

1. Artikel – Natürliche vs. Formale Sprachen
2. Artikel – Preprocessing von Rohtext mit Python (erscheint demnächst…)
3. Artikel – Möglichkeiten/Methoden der Textanalyse an Beispielen (erscheint demnächst…)
4. Artikel – NLP, was kann es? Und was nicht? (erscheint demnächst…)

Zur Verdeutlichung der beschriebenen Zusammenhänge und Methoden und um Interessierten einige Ideen für mögliche Startpunkte aufzuzeigen, werden im Verlauf der Artikelserie an verschiedenen Stellen Codebeispiele in der Programmiersprache Python vorgestellt.
Von den vielen im Internet zur Verfügung stehenden Python-Paketen zum Thema NLP, werden in diesem Artikel insbesondere die drei Pakete NLTK, Gensim und Spacy verwendet.

Aika: Ein semantisches neuronales Netzwerk

Wenn es darum geht Informationen aus natürlichsprachigen Texten zu extrahieren, stehen einem verschiedene Möglichkeiten zur Verfügung. Eine der ältesten und wohl auch am häufigsten genutzten Möglichkeiten ist die der regulären Ausdrücke. Hier werden exakte Muster definiert und in einem Textstring gematcht. Probleme bereiten diese allerdings, wenn kompliziertere semantische Muster gefunden werden sollen oder wenn verschiedene Muster aufeinander aufbauen oder miteinander interagieren sollen. Gerade das ist aber der Normalfall bei der Verarbeitung von natürlichem Text. Muster hängen voneinander ab, verstärken oder unterdrücken sich gegenseitig.
Prädestiniert um solche Beziehungen abzubilden wären eigentlich künstliche neuronale Netze. Diese haben nur das große Manko, dass sie keine strukturierten Informationen verarbeiten können. Neuronale Netze bringen von sich aus keine Möglichkeit mit, die relationalen Beziehungen zwischen Worten oder Phrasen zu verarbeiten. Ein weiteres Problem neuronaler Netze ist die Verarbeitung von Feedback-Schleifen, bei denen einzelne Neuronen von sich selbst abhängig sind. Genau diese Probleme versucht der Aika Algorithmus (www.aika-software.org) zu lösen.

Der Aika Algorithmus ist als Open Source Java-Bibliothek implementiert und dient dazu semantische Informationen in Texten zu erkennen und zu verarbeiten. Da semantische Informationen sehr häufig mehrdeutig sind, erzeugt die Bibliothek für jede dieser Bedeutungen eine eigene Interpretation und wählt zum Schluss die am höchsten gewichtete aus. Aika kombiniert dazu aktuelle Ideen und Konzepte aus den Bereichen des maschinellen Lernens und der künstlichen Intelligenz, wie etwa künstliche neuronale Netze, Frequent Pattern Mining und die auf formaler Logik basierenden Expertensysteme. Aika basiert auf der heute gängigen Architektur eines künstlichen neuronalen Netzwerks (KNN) und nutzt diese, um sprachliche Regeln und semantische Beziehungen abzubilden.

Die Knackpunkte: relationale Struktur und zyklische Abhängigkeiten

Das erste Problem: Texte haben eine von Grund auf relationale Struktur. Die einzelnen Worte stehen über ihre Reihenfolge in einer ganz bestimmten Beziehung zueinander. Gängige Methoden, um Texte für die Eingabe in ein KNN auszuflachen, sind beispielsweise Bag-of-Words oder Sliding-Window. Mittlerweile haben sich auch rekurrente neuronale Netze etabliert, die das gesamte Netz in einer Schleife für jedes Wort des Textes mehrfach hintereinander schalten. Aika geht hier allerdings einen anderen Weg. Aika propagiert die relationalen Informationen, also den Textbereich und die Wortposition, gemeinsam mit den Aktivierungen durch das Netzwerk. Die gesamte relationale Struktur des Textes bleibt also erhalten und lässt sich jederzeit zur weiteren Verarbeitung nutzen.

Das zweite Problem ist, dass bei der Verarbeitung von Text häufig nicht klar ist, in welcher Reihenfolge einzelne Informationen verarbeitet werden müssen. Wenn wir beispielsweise den Namen „August Schneider“ betrachten, können sowohl der Vor- als auch der Nachname in einem anderen Zusammenhang eine völlig andere Bedeutung annehmen. August könnte sich auch auf den Monat beziehen. Und genauso könnte Schneider eben auch den Beruf des Schneiders meinen. Einfache Regeln, um hier dennoch den Vor- und den Nachnamen zu erkennen, wären: „Wenn das nachfolgende Wort ein Nachname ist, handelt es sich bei August um einen Vornamen“ und „Wenn das vorherige Wort ein Vorname ist, dann handelt es sich bei Schneider um einen Nachnamen“. Das Problem dabei ist nur, dass unsere Regeln nun eine zyklische Abhängigkeit beinhalten. Aber ist das wirklich so schlimm? Aika erlaubt es, genau solche Feedback-Schleifen abzubilden. Wobei die Schleifen sowohl positive, als auch negative Gewichte haben können. Negative rekurrente Synapsen führen dazu, dass zwei sich gegenseitig ausschließende Interpretationen entstehen. Der Trick ist nun zunächst nur Annahmen zu treffen, also etwa dass es sich bei dem Wort „Schneider“ um den Beruf handelt und zu schauen wie das Netzwerk auf diese Annahme reagiert. Es bedarf also einer Evaluationsfunktion und einer Suche, die die Annahmen immer weiter variiert, bis schließlich eine optimale Interpretation des Textes gefunden ist. Genau wie schon der Textbereich und die Wortposition werden nun auch die Annahmen gemeinsam mit den Aktivierungen durch das Netzwerk propagiert.

Die zwei Ebenen des Aika Algorithmus

Aber wie lassen sich diese Informationen mit den Aktivierungen durch das Netzwerk propagieren, wo doch der Aktivierungswert eines Neurons für gewöhnlich nur eine Fließkommazahl ist? Genau hier liegt der Grund, weshalb Aika unter der neuronalen Ebene mit ihren Neuronen und kontinuierlich gewichteten Synapsen noch eine diskrete Ebene besitzt, in der es eine Darstellung aller Neuronen in boolscher Logik gibt. Aika verwendet als Aktivierungsfunktion die obere Hälfte der Tanh-Funktion. Alle negativen Werte werden auf 0 gesetzt und führen zu keiner Aktivierung des Neurons. Es gibt also einen klaren Schwellenwert, der zwischen aktiven und inaktiven Neuronen unterscheidet. Anhand dieses Schwellenwertes lassen sich die Gewichte der einzelnen Synapsen in boolsche Logik übersetzen und entlang der Gatter dieser Logik kann nun ein Aktivierungsobjekt mit den Informationen durch das Netzwerk propagiert werden. So verbindet Aika seine diskrete bzw. symbolische Ebene mit seiner subsymbolischen Ebene aus kontinuierlichen Synapsen-Gewichten.

Die Logik Ebene in Aika erlaubt außerdem einen enormen Effizienzgewinn im Vergleich zu einem herkömmlichen KNN, da die gewichtete Summe von Neuronen nur noch für solche Neuronen berechnet werden muss, die vorher durch die Logikebene aktiviert wurden. Im Falle eines UND-verknüpfenden Neurons bedeutet das, dass das Aktivierungsobjekt zunächst mehrere Ebenen einer Lattice-Datenstruktur aus UND-Knoten durchlaufen muss, bevor das eigentliche Neuron berechnet und aktiviert werden kann. Diese Lattice-Datenstruktur stammt aus dem Bereich des Frequent Pattern Mining und enthält in einem gerichteten azyklischen Graphen alle Teilmuster eines beliebigen größeren Musters. Ein solches Frequent Pattern Lattice kann in zwei Richtungen betrieben werden. Zum Einen können damit bereits bekannte Muster gematcht werden, und zum Anderen können auch völlig neue Muster damit erzeugt werden.

Da es schwierig ist Netze mit Millionen von Neuronen im Speicher zu halten, nutzt Aika das Provider Architekturpattern um selten verwendete Neuronen oder Logikknoten in einen externen Datenspeicher (z.B. eine Mongo DB) auszulagern, und bei Bedarf nachzuladen.

Ein Beispielneuron

Hier soll nun noch beispielhaft gezeigt werden wie ein Neuron innerhalb des semantischen Netzes angelegt werden kann. Zu beachten ist, dass Neuronen sowohl UND- als auch ODER-Verknüpfungen abbilden können. Das Verhalten hängt dabei alleine vom gewählten Bias ab. Liegt der Bias bei 0.0 oder einem nur schwach negativen Wert reicht schon die Aktivierung eines positiven Inputs aus um auch das aktuelle Neuron zu aktivieren. Es handelt sich dann um eine ODER-Verknüpfung. Liegt der Bias hingegen tiefer im negativen Bereich dann müssen mitunter mehrere positive Inputs gleichzeitig aktiviert werden damit das aktuelle Neuron dann auch aktiv wird. Jetzt handelt es sich dann um eine UND-Verknüpfung. Der Bias Wert kann der initNeuron einfach als Parameter übergeben werden. Um jedoch die Berechnung des Bias zu erleichtern bietet Aika bei den Inputs noch den Parameter BiasDelta an. Der Parameter BiasDelta nimmt einen Wert zwischen 0.0 und 1.0 entgegen. Bei 0.0 wirkt sich der Parameter gar nicht aus. Bei einem höheren Wert hingegen wird er mit dem Betrag des Synapsengewichts multipliziert und von dem Bias abgezogen. Der Gesamtbias lautet in diesem Beispiel also -55.0. Die beiden positiven Eingabesynapsen müssen also aktiviert werden und die negative Eingabesynapse darf nicht aktiviert werden, damit dieses Neuron selber aktiv werden kann. Das Zusammenspiel von Bias und Synpasengewichten ist aber nicht nur für die Aktivierung eines Neurons wichtig, sondern auch für die spätere Auswahl der finalen Interpretation. Je stärker die Aktivierungen innerhalb einer Interpretation aktiv sind, desto höher wird diese Interpretation gewichtet.
Um eine beliebige Graphstruktur abbilden zu können, trennt Aika das Anlegen der Neuronen von der Verknüpfung mit anderen Neuronen. Mit createNeuron(“E-Schneider (Nachname)”) wird also zunächst einmal ein unverknüpftes Neuron erzeugt, das dann über die initNeuron Funktion mit den Eingabeneuronen wortSchneiderNeuron, kategorieVornameNeuron und unterdrueckendesNeuron verknüpft wird. Über den Parameter RelativeRid wird hier angegeben auf welche relative Wortposition sich die Eingabesynapse bezieht. Die Eingabesynpase zu der Kategorie Vorname bezieht sich also mit -1 auf die vorherige Wortposition. Der Parameter Recurrent gibt an ob es sich bei dieser Synpase um eine Feedback-Schleife handelt. Über den Parameter RangeMatch wird angegeben wie sich der Textbereich, also die Start- und die Endposition zwischen der Eingabe- und der Ausgabeaktivierung verhält. Bei EQUALS sollen die Bereiche also genau übereinstimmen, bei CONTAINED_IN reicht es hingegen wenn der Bereich der Eingabeaktivierung innerhalb des Bereichs der Ausgabeaktivierung liegt. Dann kann noch über den Parameter RangeOutput angegeben werden, dass der Bereich der Eingabeaktivierung an die Ausgabeaktivierung weiterpropagiert werden soll.

    Neuron entitaetSchneiderNachnameNeuron = m.initNeuron(
            m.createNeuron("E-Schneider (Nachname)"),
            5.0,
            new Input()
                    .setNeuron(wortSchneiderNeuron)
                    .setWeight(10.0f)
                    .setBiasDelta(1.0)
                    .setRelativeRid(0)
                    .setRecurrent(false)
                    .setRangeMatch(EQUALS)
                    .setRangeOutput(true),
            new Input()
                    .setNeuron(kategorieVornameNeuron)
                    .setWeight(10.0f)
                    .setBiasDelta(1.0)
                    .setRelativeRid(-1)
                    .setRecurrent(true)
                    .setRangeMatch(NONE),
            new Input()
                    .setNeuron(unterdrueckendesNeuron)
                    .setWeight(-40.0f)
                    .setBiasDelta(1.0)
                    .setRecurrent(true)
                    .setRangeMatch(CONTAINED_IN)
    );

Fazit

Mit Aika können sehr flexibel umfangreiche semantische Modelle erzeugt und verarbeitet werden. Aus Begriffslisten verschiedener Kategorien, wie etwa: Vor- und Nachnamen, Orten, Berufen, Strassen, grammatikalischen Worttypen usw. können automatisch Neuronen generiert werden. Diese können dann dazu genutzt werden, Worte und Phrasen zu erkennen, einzelnen Begriffen eine Bedeutung zuzuordnen oder die Kategorie eines Begriffs zu bestimmen. Falls in dem zu verarbeitenden Text mehrdeutige Begriffe oder Phrasen auftauchen, kann Aika für diese jeweils eigene Interpretationen erzeugen und gewichten. Die sinnvollste Interpretation wird dann als Ergebnis zurück geliefert.

Toolkits & Services für Semantische Textanalysen

Named Entity Recognition ist ein Teilgebiet von Information Extraction. Ziel von Information Extraction ist die Gewinnung semantischer Informationen aus Texten (im Gegensatz zum verwandten Gebiet des Information Retrieval, bei dem es um das möglichst intelligente Finden von Informationen, die u.U. vorab mit Information Extraction gewonnen wurden, geht). Named Entity Recognition (kurz NER) bezeichnet die Erkennung von Entitäten wie z.B. Personen, Organisationen oder Orten in Texten.

[box]Beispiel:
Albert Einstein war ein theoretischer Physiker, der am 14. März 1879 in Ulm geboren wurde. Er erhielt 1921 den Nobelpreis für Physik. Isaac Newton, Einstein und Stephen Hawking werden oft als die größten Physiker seit der Antike bezeichnet.”[/box]

Die Disambiguierung von Entitäten ist ein weiterer wichtiger Schritt auf dem Weg zu einem semantischen Verständnis von Texten. Wenn man so in obigem Text erkennen kann, dass “Albert Einstein“, “Er” und “Einstein” die gleiche Person bezeichnen, so kann ein Analyseverfahren z.B. daraus schließen, dass in diesem Text Einstein eine wichtigere Rolle spielt, als Newton, der nur einmal erwähnt wurde. Die Hyperlinks hinter den jeweiligen Entitäten zeigen eine Möglichkeit der semantischen Anreicherung von Texten an – in diesem Fall wurden die Entitäten mit entsprechenden Einträgen bei DBpedia automatisch verlinkt.

Named Entity Recognition dient vorrangig zwei Zwecken:

  • Anreicherung von Texten mit Metadaten
  • Abstraktion von Texten zur besseren Erkennung von Mustern

Punkt 1 dient direkt dem Information Retrieval. Anwender können so z.B. gezielt nach bestimmten Personen suchen, ohne alle möglichen Schreibweisen oder Berufsbezeichnungen auflisten zu müssen.

Punkt 2 dient der Vorverarbeitung von Texten als Input für Machine Learning Verfahren. So ist es (je nach Anwendung!) oft nicht von Bedeutung, welche Person, welcher Ort oder auch welche Uhrzeit in einem Text steht sondern nur die Tatsache, dass Personen, Orte oder Zeiten erwähnt wurden.

Sirrus Shakeri veranschaulicht die zentrale Bedeutung semantischer Analyse in seinem Beitrag From Big Data to Intelligent Applications:

intelligent-applications-cirrus-shakeri

Abbildung 1: Von Big Data zu Intelligent Applications von Cirrus Shakeri

Sein “Semantic Graph” setzt voraus, dass Entitäten mittels “Natural Language Processing” erkannt und zueinander in Beziehung gesetzt wurden.

Es ist interessant zu vermerken, dass Natural Language Processing und Data Mining / Machine Learning über viele Jahre als Alternativen zueinander und nicht als Ergänzungen voneinander gesehen wurden. In der Tat springen die meisten Vorgehensmodelle heutzutage von “Data Preparation” zu “Machine Reasoning”. Wir argumentieren, dass sich in vielen Anwendungen, die auf unstrukturierten Daten basieren, signifikante Qualitätsverbesserungen erzielen lassen, wenn man zumindest NER (inklusive Disambiguierung) in die Pipeline mit einbezieht.

Toolkits und Services für NER

Es existiert eine Vielzahl von Toolkits für Natural Language Processing, die Sie mehr oder weniger direkt in Ihre Programme einbinden können. Exemplarisch seien drei Toolkits für Java, Python und R erwähnt:

Diese Toolkits enthalten Modelle, die auf Korpora für die jeweils unterstützten Sprachen trainiert wurden. Sie haben den Vorteil, dass sie auch vollkommen neue Entitäten erkennen können (wie z.B. neue Politiker oder Fernsehstars, die zur Trainingszeit noch unbekannt waren). Je nach Einstellung haben diese Systeme aber auch eine relativ hohe Falsch-Positiv-Rate.

Wer NER nur ausprobieren möchte oder lediglich gelegentlich kleinere Texte zu annotieren hat, sei auf die folgenden Web Services verwiesen, die auch jeweils eine REST-Schnittstelle anbieten.

DBpedia

Das DBpedia Projekt nutzt die strukturierten Informationen der verschieden-sprachigen Wikipedia Sites für den Spotlight Service. Im Unterschied zu den reinen Toolkits nutzen die nun genannten Werkzeuge zusätzlich zu den trainierten Modellen eine Wissensbasis zur Verringerung der Falsch-Positiv-Rate. Die mehrsprachige Version unter http://dbpedia-spotlight.github.io/demo zeigt die Möglichkeiten des Systems auf. Wählen Sie unter “Language” “German“) und dann über “SELECT TYPES…” die zu annotierenden Entitätstypen. Ein Beispieltext wird automatisch eingefügt. Sie können ihn natürlich durch beliebige andere Texte ersetzen. Im folgenden Beispiel wurden “Organisation”, “Person”, und “Place“ ausgewählt:

DBprediaSpotlight

Abbildung 2: DBpedia Demo (de.dbpedia.org)

Die erkannten Entitäten werden direkt mit ihren DBpedia Datenbankeinträgen verlinkt. Im Beispiel wurden die Orte Berlin, Brandenburg und Preußen sowie die Organisationen Deutsches Reich, Deutsche Demokratische Republik, Deutscher Bundestag und Bundesrat erkannt. Personen wurden in dem Beispieltext nicht erkannt. Die Frage, ob man “Sitz des Bundespräsidenten” als Ort (Sitz), Organisation (das Amt des Bundespräsidenten) und / oder Person (der Bundespräsident) bezeichnen sollte, hängt durchaus vom Anwendungsszenario ab.

OpeNER

Das OpeNER Projekt ist das Ergebnis eines europäischen Forschungsprojekts und erweitert die Funktionalität von DBpedia Spotlight mit weiteren semantischen Analysen. Die Demo unter http://demo2-opener.rhcloud.com/welcome.action (Tab “Live Analysis Demo“, “Named Entity Recognition and Classification” und “Named Entity Linking” auswählen und “Analyse” drücken, dann auf der rechten Seite das Tab “NERC” anwählen) ergibt für den gleichen Beispieltext:

opeNER-projekt

Abbildung 3: OpeNER Projekt (opener-project.eu)

Organisationen sind blau hinterlegt, während Orte orange markiert werden. Auch hier werden erkannte Entitäten mit ihren DBpedia Datenbankeinträgen verknüpft. Die Bedeutung dieser Verknüpfung erkennt man wenn man auf das Tab “Map” wechselt. Berlin wurde als Ort erkannt und über die Geo-Koordinaten (geo:long = 13.4083, geo.lat = 52.5186) im DBpedia Eintrag von Berlin konnte das Wort “Berlin” aus obigem Text automatisch auf der Weltkarte referenziert werden.

Es gibt eine Vielzahl weiterer Services für NLP wie z.B. OpenCalais. Einige dieser Services bieten bestimmte Funktionalitäten (wie z.B. Sentiment Analysis) oder andere Sprachen neben Englisch nur gegen eine Gebühr an.

Listen Tagger

Der Vollständigkeit halber sei noch erwähnt, dass in den meisten Anwendungsszenarien die oben genannten Werkzeuge durch sogenannte Listen-Tagger (englisch Dictionary Tagger) ergänzt werden. Diese Tagger verwenden Listen von Personen, Organisationen oder auch Marken, Bauteilen, Produktbezeichnern oder beliebigen anderen Gruppen von Entitäten. Listen-Tagger arbeiten entweder unabhängig von den oben genannten statistischen Taggern (wie z.B. dem Standford Tagger) oder nachgeschaltet. Im ersten Fall markieren diese Tagger alle Vorkommen bestimmter Worte im Text (z.B. „Zalando“ kann so direkt als Modemarke erkannt werden). Im zweiten Fall werden die Listen genutzt, um die statistisch erkannten Entitäten zu verifizieren. So könnte z.B. der Vorschlag des statistischen Taggers automatisch akzeptiert werden wenn die vorgeschlagene Person auch in der Liste gefunden wird. Ist die Person jedoch noch nicht in der Liste enthalten, dann könnte ein Mitarbeiter gebeten werden, diesen Vorschlag zu bestätigen oder zu verwerfen. Im Falle einer Bestätigung wird die neu erkannte Person dann in die Personenliste aufgenommen während sie im Falle einer Ablehnung in eine Negativliste übernommen werden könnte damit dieser Vorschlag in Zukunft automatisch unterdrückt wird.

Regular Expression Tagger

Manche Entitätstypen folgen klaren Mustern und können mit hoher Zuverlässigkeit durch reguläre Ausdrücke erkannt werden. Hierzu zählen z.B. Kreditkarten- oder Telefon- oder Versicherungsnummern aber auch in vielen Fällen Bauteilbezeichner oder andere firmeninterne Identifikatoren.

Fazit

Natural Language Processing und insbesondere Named Entity Recognition und Disambiguierung sollte Teil der Werkzeugkiste eines jeden Anwenders bei der Analyse von unstrukturierten Daten sein. Es existieren mehrere mächtige Toolkits und Services, die allerdings je nach Anwendungsgebiet kombiniert und verfeinert werden müssen. So erkennt DBpedia Spotlight nur Entitäten, die auch einen Wikipedia Eintrag haben, kann für diese aber reichhaltige Metadaten liefern. Der Stanford Tagger hingegen kann auch vollkommen unbekannte Personennamen aus dem textuellen Kontext erkennen, hat aber bei manchen Texten eine relativ hohe Falsch-Positiv-Rate. Eine Kombination der beiden Technologien und anwendungsspezifischen Listen von Entitäten kann daher zu qualitativ sehr hochwertigen Ergebnissen führen.

Text Mining mit R

R ist nicht nur ein mächtiges Werkzeug zur Analyse strukturierter Daten, sondern eignet sich durchaus auch für erste Analysen von Daten, die lediglich in textueller und somit unstrukturierter Form vorliegen. Im Folgenden zeige ich, welche typischen Vorverarbeitungs- und Analyseschritte auf Textdaten leicht durchzuführen sind. Um uns das Leben etwas leichter zu machen, verwenden wir dafür die eine oder andere zusätzliche R-Library.

Die gezeigten Schritte zeigen natürlich nur einen kleinen Ausschnitt dessen, was man mit Textdaten machen kann. Der Link zum kompletten R-Code (.RMD) findet sich am Ende des Artikels.

Sentimentanalyse

Wir verwenden das Anwendungsgebiet der Sentimentanalyse für diese Demonstration. Mittels der Sentimentanalyse versucht man, Stimmungen zu analysieren. Im Prinzip geht es darum, zu erkennen, ob ein Autor mit einer Aussage eine positive oder negative Stimmung oder Meinung ausdrückt. Je nach Anwendung werden auch neutrale Aussagen betrachtet.

Daten einlesen

Datenquelle: ‘From Group to Individual Labels using Deep Features’, Kotzias et. al,. KDD 2015

Die Daten liegen als cvs vor: Die erste Spalte enhält jeweils einen englischen Satz, gefolgt von einem Tab, gefolgt von einer 0 für negatives Sentiment und einer 1 für positives Sentiment. Nicht alle Sätze in den vorgegebenen Daten sind vorklassifiziert.

Wir lesen 3 Dateien ein, fügen eine Spalte mit der Angabe der Quelle hinzu und teilen die Daten dann in zwei Datensätze auf. Der Datensatz labelled enthält alle vorklassifizierten Sätze während alle anderen Sätze in unlabelled gespeichert werden.

## 'readSentiment' liest csv ein, benennt die Spalten und konvertiert die Spalte 'sentiment' zu einem Faktor 
amazon <-readSentiment("amazon_cells_labelled.txt")
amazon$source <- "amazon"
imdb <-readSentiment("imdb_labelled.txt")
imdb$source <- "imdb"
yelp <-readSentiment("yelp_labelled.txt")
yelp$source <- "yelp"

allText <- rbindlist(list(amazon, imdb, yelp), use.names=TRUE)
allText$source <- as.factor(allText$source)

unlabelled <- allText[is.na(allText$sentiment), ]
labelled <- allText[!is.na(allText$sentiment), ]

Wir haben nun 3000 vorklassifizierte Sätze, die entweder ein positives oder ein negatives Sentiment ausdrücken:

text               sentiment 	source    
Length:3000        0:1500    	amazon:1000  
Class :character   1:1500    	imdb  :1000  
Mode  :character             	yelp  :1000

Textkorpus anlegen

Zuerst konvertieren wir den Datensatz in einen Korpus der R-Package tm:

library(tm)
corpus <- Corpus(DataframeSource(data.frame(labelled$text)))
# meta data an Korpus anfügen:
meta(corpus, tag = "sentiment", type="indexed") <- labelled$sentiment
meta(corpus, tag = "source", type="indexed") <- labelled$source

myTDM  <- TermDocumentMatrix(corpus, control = list(minWordLength = 1))

## verschieden Möglichkeiten, den Korpus bzw die TermDocumentMatrix zu inspizieren:
#inspect(corpus[5:10])
#meta(corpus[1:10])
#inspect(myTDM[25:30, 1])
# Indices aller Dokumente, die das Wort "good" enthalten:
idxWithGood <- unlist(lapply(corpus, function(t) {grepl("good", as.character(t))}))
# Indices aller Dokumente mit negativem Sentiment, die das Wort "good" enthalten:
negIdsWithGood <- idxWithGood &  meta(corpus, "sentiment") == '0'

Wir können uns nun einen Eindruck über die Texte verschaffen, bevor wir erste Vorverarbeitungs- und Säuberungsschritte durchführen:

  • Fünf Dokumente mit negativem Sentiment, die das Wort “good” enthalten: Not a good bargain., Not a good item.. It worked for a while then started having problems in my auto reverse tape player., Not good when wearing a hat or sunglasses., If you are looking for a good quality Motorola Headset keep looking, this isn’t it., However, BT headsets are currently not good for real time games like first-person shooters since the audio delay messes me up.
  • Liste der meist verwendeten Worte im Text: all, and, are, but, film, for, from, good, great, had, have, it’s, just, like, movie, not, one, phone, that, the, this, very, was, were, with, you
  • Anzahl der Worte, die nur einmal verwendet werden: 4820, wie z.B.: ‘film’, ‘ive, ’must’, ‘so, ’stagey’, ’titta
  • Histogramm mit Wortfrequenzen:

Plotten wir, wie oft die häufigsten Worte verwendet werden:

Vorverarbeitung

Es ist leicht zu erkennen, dass sogenannte Stoppworte wie z.B. “the”, “that” und “you” die Statistiken dominieren. Der Informationsgehalt solcher Stopp- oder Füllworte ist oft gering und daher werden sie oft vom Korpus entfernt. Allerdings sollte man dabei Vorsicht walten lassen: not ist zwar ein Stoppwort, könnte aber z.B. bei der Sentimentanalyse durchaus von Bedeutung sein.

Ein paar rudimentäre Vorverarbeitungen:

Wir konvertieren den gesamten Text zu Kleinbuchstaben und entfernen die Stoppworte unter Verwendung der mitgelieferten R-Stoppwortliste für Englisch (stopwords(“english”)). Eine weitere Standardoperation ist Stemming, das wir heute auslassen. Zusätzlich entfernen wir alle Sonderzeichen und Zahlen und behalten nur die Buchstaben a bis z:

replaceSpecialChars <- function(d) {
  ## normalerweise würde man nicht alle Sonderzeichen entfernen
  gsub("[^a-z]", " ", d)
}
# tolower ist eine built-in function
corpus <- tm_map(corpus, content_transformer(tolower)) 
# replaceSpecialChars ist eine selbst geschriebene Funktion:
corpus <- tm_map(corpus, content_transformer(replaceSpecialChars))
corpus <- tm_map(corpus, stripWhitespace)
englishStopWordsWithoutNot <- stopwords("en")[ - which(stopwords("en") %in% "not")]
corpus <- tm_map(corpus, removeWords, englishStopWordsWithoutNot)
## corpus <- tm_map(corpus, stemDocument, language="english")

myTDM.without.stop.words <- TermDocumentMatrix(corpus, 
                                      control = list(minWordLength = 1))

 

Schlagwortwolke bzw Tag Cloud

Schließlich erzeugen wir eine Tag-Cloud aller Worte, die mindestens 25 mal im Text verwendet werden. Tag-Clouds eignen sich hervorragend zur visuellen Inspektion von Texten, allerdings lassen sich daraus nur bedingt direkte Handlungsanweisungen ableiten:

wordfreq <- findFreqTerms(myTDM.without.stop.words, lowfreq=25)
termFrequency <- rowSums(as.matrix(myTDM.without.stop.words[wordfreq,])) 
# eine Alternative ist 'tagcloud'
library(wordcloud)
wordcloud(words=names(termFrequency),freq=termFrequency,min.freq=5,max.words=50,random.order=F,colors="red")

schlagwortwolke

Word-Assoziationen

Wir können uns für bestimmte Worte anzeigen lassen, wie oft sie gemeinsam mit anderen Worten im gleichen Text verwendet werden:

  • Worte, die häufig gemeinsam mit movie verwendet werden:
findAssocs(myTDM.without.stop.words, "movie", 0.13)
## $movie
##   beginning        duet fascinating        june       angel   astronaut 
##        0.17        0.15        0.15        0.15        0.14        0.14 
##         bec       coach     columbo   considers     curtain       dodge 
##        0.14        0.14        0.14        0.14        0.14        0.14 
##     edition   endearing    funniest    girolamo         hes         ive 
##        0.14        0.14        0.14        0.14        0.14        0.14 
##     latched         lid      makers     peaking     planned  restrained 
##        0.14        0.14        0.14        0.14        0.14        0.14 
##       scamp     shelves     stratus       titta        ussr      vision 
##        0.14        0.14        0.14        0.14        0.14        0.14 
##       yelps 
##        0.14
  • Worte, die häufig gemeinsam mit product verwendet werden:
findAssocs(myTDM.without.stop.words, "product", 0.12)
## $product
##        allot     avoiding        beats   cellphones       center 
##         0.13         0.13         0.13         0.13         0.13 
##      clearer   contacting       copier       dollar    equipment 
##         0.13         0.13         0.13         0.13         0.13 
##      fingers      greater      humming        ideal      learned 
##         0.13         0.13         0.13         0.13         0.13 
##       lesson        motor        murky   negatively          oem 
##         0.13         0.13         0.13         0.13         0.13 
##     official       online       owning         pens    petroleum 
##         0.13         0.13         0.13         0.13         0.13 
##     planning      related replacementr    sensitive     shipment 
##         0.13         0.13         0.13         0.13         0.13 
##        steer      voltage        waaay        whose    worthless 
##         0.13         0.13         0.13         0.13         0.13

 

Text-Mining

Wir erzeugen einen Entscheidungsbaum zur Vorhersage des Sentiments. Entscheidungsbäume sind nicht unbedingt das Werkzeug der Wahl für Text-Mining aber für einen ersten Eindruck lassen sie sich bei kleinen Datensätzen durchaus gewinnbringend einsetzen:

trainingData <- data.frame(as.matrix(myDTM))
trainingData$sentiment <- labelled$sentiment
trainingData$source <- labelled$source

formula <- sentiment ~ . 

if (rerun) {
  tree <- rpart(formula, data = trainingData)
  save(tree, file=sprintf("%s-tree.RData", prefix))
} else {
  load(file=sprintf("c:/tmp/%s-tree.RData", prefix))
}

myPredictTree(tree)

 

##          isPosSentiment
## sentiment FALSE TRUE
##         0  1393  107
##         1   780  720

Eine Fehlerrate von über 50% auf den Trainingsdaten für positive Sentiments ist natürlich nicht berauschend und daher testen wir zum Schluß noch Support Vector Machines:

library(e1071)
  if (rerun) {
    svmModel <- svm(formula, data = trainingData)
    save(svmModel, file=sprintf("%s-svm.RData", prefix))
  } else {
    load(file=sprintf("c:/tmp/%s-svm.RData", prefix))
  }

myPredictSVM <- function(model) {
  predictions <- predict(model, trainingData)

  trainPerf <- data.frame(trainingData$sentiment, predictions, trainingData$source)
  names(trainPerf) <- c("sentiment", "isPosSentiment", "source")
  
  with(trainPerf, {
    table(sentiment, isPosSentiment, deparse.level = 2)
  })
  
}
myPredictSVM(svmModel)
##          isPosSentiment
## sentiment    FALSE 	TRUE
##         0 	1456   	  44
##         1   	  23 	1477

Die Ergebnisse sehen deutlich besser aus, müssten aber natürlich noch auf unabhängigen Daten verifiziert werden, um z. B. ein Overfittung zu vermeiden.

Download-Link zum kompletten R-Code für dieses Text-Mining-Beispiel: https://www.data-science-blog.com/download/textMiningTeaser.rmd

Text-Mining mit dem Aika Algorithmus

In diesem Beitrag möchte ich das Open Source Projekt Aika vorstellen. Ziel des Projektes ist es einen Text-Mining Algorithmus zu entwickeln, der ein künstliches Neuronales Netz (kNN) mit einem Pattern Mining Algorithmus kombiniert. Dabei dient die Silbentrennung von Wörtern als initiale Aufgabe, anhand derer der Algorithmus weiterentwickelt wird. Für diese Aufgabe soll allerdings kein vordefiniertes Wörterbuch verwendet werden. Stattdessen sollen die Silben in ihrer Eigenschaft als häufig auftretende Muster in rohem Text erkannt werden. Hier reicht es allerdings nicht einen Mining Algorithmus nach häufig auftretenden Strings suchen zu lassen, da sich viele der Strings überlappen oder schlicht keinen Sinn ergeben würden. Es ist also wichtig, dass sich die erkannten Silben gegenseitig unterdrücken können und dass der Algorithmus in der Lage ist, die so entstehenden unterschiedlichen Interpretationen eines Wortes miteinander zu vergleichen und die am höchsten gewichtete auszuwählen. Beispielsweise taucht die Silbe ‘der’ zu Beginn des Wortes ‘de-re-gu-lie-ren’ auf. In diesem Fall muss der Algorithmus erkennen, dass die erste Silbe des Wortes nicht ‘der’ sondern nur ‘de’ ist.

Wenn nun nach häufig auftretenden Mustern in Text gesucht werden soll, warum verwenden wir nicht einen reinen Pattern Mining Algorithmus? Der Grund für die Kombination mit einem kNN liegt darin, dass die erkannten Muster innerhalb einer kNN Topologie aufeinander aufsetzen können. Wenn z. B. das Wort “hausboot” als Muster erkannt werden soll, dann entstünden in der Datenstruktur des Mining Algorithmus sehr viele Teilmuster, die alle evaluiert werden müssten. Viel leichter wäre es für den Algorithmus, wenn die Muster “haus” und “boot” bereits erkannt worden wären und nun als Eingaben für die Erkennung des Wortes “hausboot” dienen könnten. So ist der Algorithmus zum einen in der Lage komplexere Muster zu erkennen und muss gleichzeitig weniger Teilmuster untersuchen. Ausserdem erlaubt es ein kNN ‘weiche’ Muster zu erlernen, also Muster bei denen einzelne Eingänge optional sind, die aber trotzdem noch sicher erkannt werden. Dadurch kann eine höhere Toleranz gegenüber Fehlern erreicht werden.

Im Gegensatz zu einem klassischen kNN nutzt Aika einen eher mit Googles Pagerank vergleichbaren Ansatz um Gewichte zwischen den einzelnen Neuronen des Netzwerks zu propagieren. Der Grutext-pattern-knnndgedanke dabei ist es, dass Neuronen entsprechend höher gewichtet werden sollten, wenn sie mit anderen hoch gewichteten Neuronen in Beziehung stehen. Wenn also beispielsweise eine Silbe in vielen hoch gewichteten Worten auftaucht, wird sie selbst entsprechend höher gewichtet.

Neuronen eines kNN erlauben es aber nicht nur Konjunktionen wie etwa bei Mustern zu erlernen, sondern auch Disjunktionen. Disjunktionen sind insbesondere beim Erlernen von Grammatikregeln wichtig, wenn z. B. einzelne Worte als Nomen erkannt werden sollen. Wenn nun solche Disjunktionen erlernt werden sollen, können auch hier häufige Muster behilflich sein. Angenommen, es wurden durch den Mining Algorithmus bereits die folgenden häufigen Muster gefunden: “der Baum” (f=4), “der Hammer” (f=3) und “der Nagel” (f=6). Dann können diese Muster so umgeformt werden, dass ein neues, deutlich häufigeres Muster “der <NOMEN>” (f=13) und eine Disjunktion <NOMEN> = “Baum” oder “Hammer” oder “Nagel”, entsteht.

Top 10 der Python Bibliotheken für Data Science

Python gilt unter Data Scientists als Alternative zu R Statistics. Ich bevorzuge Python auf Grund seiner Syntax und Einfachheit gegenüber R, komme hinsichtlich der vielen Module jedoch häufig etwas durcheinander. Aus diesem Grund liste ich hier die – meiner Einschätzung nach – zehn nützlichsten Bibliotheken für Python, um einfache Datenanalysen, aber auch semantische Textanalysen, Predictive Analytics und Machine Learning in die Tat umzusetzen.

NumPy – Numerische Analyse

NumPy ist eine Open Source Erweiterung für Python. Das Modul stellt vorkompilierte Funktionen für die numerische Analyse zur Verfügung. Insbesondere ermöglicht es den einfachen Umgang mit sehr großen, multidimensionalen Arrays (Listen) und Matrizen, bietet jedoch auch viele weitere grundlegende Features (z. B. Funktionen der Zufallszahlenbildung, Fourier Transformation, linearen Algebra). Ferner stellt das NumPy sehr viele Funktionen mathematische Funktionen für das Arbeiten mit den Arrays und Matrizen bereit.

matplotlib – 2D/3D Datenvisualisierung

Die matplotlib erweitert NumPy um grafische Darstellungsmöglichkeiten in 2D und 3D. Das Modul ist in Kombination mit NumPy wohl die am häufigsten eingesetzte Visualisierungsbibliothek für Python.

Die matplotlib bietet eine objektorientierte API, um die dynamischen Grafiken in Pyhton GUI-Toolkits einbinden zu können (z. B. GTL+ oder wxPython).

NumPy und matplotlib werden auch mit den nachfolgenden Bibliotheken kombiniert.

Bokeh – Interaktive Datenvisualisierung

Während die Plot-Funktionen von matplotlib statisch angezeigt werden, kann in den Visualsierungsplots von Bokeh der Anwender interaktiv im Chart klicken und es verändern. Bokeh ist besonders dann geeignet, wenn die Datenvisualisierung als Dashboard im Webbrowser erfolgen soll.

Das Bild über diesen Artikel zeigt Visualiserungen mit dem Python Package Bokeh.

Pandas – Komplexe Datenanalyse

Pandas ist eine Bibliothek für die Datenverarbeitung und Datenanalyse mit Python. Es erweitert Python um Datenstrukturen und Funktionen zur Verarbeitung von Datentabellen. Eine besondere Stärke von Pandas ist die Zeitreihenanalyse. Pandas ist freie Software (BSD License).

Statsmodels – Statistische Datenanalyse

Statsmodels is a Python module that allows users to explore data, estimate statistical models, and perform statistical tests. An extensive list of descriptive statistics, statistical tests, plotting functions, and result statistics are available for different types of data and each estimator.

Die explorative Datenanalyse, statistische Modellierung und statistische Tests ermöglicht das Modul Statsmodels. Das Modul bringt neben vielen statistischen Funktionen auch eigene Plots (Visualisierungen) mit. Mit dem Modul wird Predictive Analytics möglich. Statsmodels wird häufig mit NumPy, matplotlib und Pandas kombiniert.

SciPy – Lineare Optimierung

SciPy ist ein sehr verbreitetes Mathematik-Modul für Python, welches den Schwerpunkt auf die mathematische Optimierung legt. Funktionen der linearen Algebra, Differenzialrechnung, Interpolation, Signal- und Bildverarbeitung sind in SciPy enthalten.

scikit-learn – Machine Learning

scikit-learn ist eine Framework für Python, das auf NumPy, matplotlob und SciPy aufsetzt, dieses jedoch um Funktionen für das maschinelle Lernen (Machine Learning) erweitert. Das Modul umfasst für das maschinelle Lernen notwendige Algorithmen für Klassifikationen, Regressionen, Clustering und Dimensionsreduktion.

Mlpy – Machine Learning

Alternativ zu scikit-learn, bietet auch Mlpy eine mächtige Bibliothek an Funktionen für Machine Learning. Mlpy setzt ebenfalls auf NumPy und SciPy, auf, erweitert den Funktionsumfang jedoch um Methoden des überwachten und unüberwachten maschinellen Lernens.

NLTK – Text Mining

NLTK steht für Natural Language Toolkit und ermöglicht den effektiven Einstieg ins Text Mining mit Python. Das Modul beinhaltet eigene (eher einfache) Visualisierungsmöglichkeiten zur Darstellung von Textmuster-Zusammenhängen, z. B. in Baumstrukturen. Für Text Mining und semantische Textanalysen mit Python gibt es wohl nichts besseres als NLTK.

Theano – Multidimensionale Berechnungen & GPU-Processing

Theano is a Python library that allows you to define, optimize, and evaluate mathematical expressions involving multi-dimensional arrays efficiently

Für multidimensionale Datenanalysen bzw. die Verarbeitung und Auswertung von multidimensionalen Arrays gibt es wohl nichts schnelleres als die Bibliothek Theano. Theano ist dabei eng mit NumPy verbunden.

Theano ermöglicht die Auslagerung der Berechnung auf die GPU (Grafikprozessor), was bis zu 140 mal schneller als auf der CPU sein soll. Getestet habe ich es zwar nicht, aber grundsätzlich ist es wahr, dass die GPU multidimensionale Arrays schneller verarbeiten kann, als die CPU. Zwar ist die CPU universeller (kann quasi alles berechnen), die GPU ist aber auf die Berechnung von 3D-Grafiken optimiert, die ebenfalls über multidimensionalen Vektoren verarbeitet werden.