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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
## '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:
1 2 3 4 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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:
1 2 3 4 5 |
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") |
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:
1 2 |
findAssocs(myTDM.without.stop.words, "movie", 0.13) |
1 2 3 4 5 6 7 8 9 10 11 12 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:
1 2 |
findAssocs(myTDM.without.stop.words, "product", 0.12) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
## $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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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) |
1 2 3 4 |
## 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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) |
1 2 3 4 |
## 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
Hallo,
die Einleitung ist sehr gut!
Der Link zum Download des Codes führt nicht direkt zum Download. Können Sie mir hier den direkten Link zukommen lassen?
Haben Sie eventuell weitere Tutorials in dem Bereich Text Mining in R?
Ich bin Student und möchte das mächtige Tool für meine Abschlußarbeit nutzen.
Viele Grüße,
Christian
Hallo,
vielen Dank für das Beispiel. Kann man SVM auch bei sehr langen Texten anwenden ? Als Klassifizierung für den ganzen Text und nicht nur einzelne Wörter?
Grüße
Paula
Hallo,
aber natürlich kann man SVM’s auch für lange Texte anwenden. In der Tat ist es meine Erfahrung, dass SVMs bei sehr kurzen Texten (sagen wir weniger als 20 Worte) nicht ganz so gut sind. Bei sehr, sehr langen Texten (sagen wir als 10 Seiten) sollte man vermutlich den Text auch in Abschnitten klassifizieren, da längere Texte selten nur ein Thema abdecken. Aber das kommt natürlich auf den Anwendungsfall an.
Grüße,
Dietrich
Hallo,
mir ist noch nicht ganz klar wo man testet. Benutze ich nun als Testdaten die Trainingsdaten? Ich könnte doch zum Beipsiel oben nur die Daten von amazon und imdb nutzen und das Modell dann an yelp testen. Habe das leider nicht hinbekommen. War aber auch ein langer Tag…
Grüße
Hallo,
Testdaten sollten immer unabhängig von den Trainingsdaten sein. Ob das auch Cross-Domain funktioniert hängt aber von der Ähnlichkeit der Daten ab. Wenn es sich immer um Movie-Reviews handelt, dann sollte das funktionieren. Wenn es sich aber bei den Amazondaten um Bookreviews und bei imdb um Filme handelt, wird das vermutlich nicht so gut funktioneren.
Im Normalfall würde ich pro Domaine einen Teil der Daten als Trainingsdaten nehmen und einen anderen Teil als Testdaten. Z.B. könnte man nach dem Datum des Reviews oder (vermutlich sauberer) nach einzelnen Filmen oder Büchern trennen. Also z.B. alle Reviews zu den Star Wars Filmen zu den Trainingsdaten und alle Reviews zu “Das 5. Element” zu den Testdaten packen. Hier kommt es etwas auf das Anwendungsszenario an.
It’s really helpful, I learned a lot from this blog. Thanks for sharing such useful information.
Thanks for sharing such information with us, very informative, there is no doubt about it.
Thank you so much for posting this kind of content, your content delivery is awesome, would like to bookmark your site on my browser for reading out your new posts. Keep posting these kind of posts.