Na poprzednich zajęciach rozpatrywaliśmy przykłady pojedynczych tekstów, na których wykonywaliśmy proste operacje związane z ich eksploracją, takie jak rozdział na słowa, usuwanie słów funkcyjnych. W wielu przypadkach mamy jednak do czynienia z grupą dokumentów - poniżej (zabawkowy) przykład 3 "dokumentów"
# PRZYKŁAD 3.1
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidytext)
data <- tibble(doc_id = 1:3, text =
c("Once upon a time there was a king who wished to have a child.",
"Once upon a time, in a faraway land there lived a beautiful princess.",
"In the olden time, when wishing was having, there used to live a King."))
data
## # A tibble: 3 x 2
## doc_id text
## <int> <chr>
## 1 1 Once upon a time there was a king who wished to have a child.
## 2 2 Once upon a time, in a faraway land there lived a beautiful prin..
## 3 3 In the olden time, when wishing was having, there used to live a..
To, do czego będziemy zmierzać, to wyznaczenie omawianych na wykładzie wielkości związanych z popularnością słów, czyli TF oraz TF-IDF. Zacznijmy od tego, że potrzebne będą nam zliczenia słów w poszczególnych dokumantach
# PRZYKŁAD 3.2
doc_words <- data %>%
unnest_tokens(word, text) %>%
count(doc_id, word, sort = TRUE)
doc_words
## # A tibble: 37 x 3
## doc_id word n
## <int> <chr> <int>
## 1 1 a 3
## 2 2 a 3
## 3 1 child 1
## 4 1 have 1
## 5 1 king 1
## 6 1 once 1
## 7 1 there 1
## 8 1 time 1
## 9 1 to 1
## 10 1 upon 1
## # ... with 27 more rows
Z drugiej strony, chcemy też mieć całkowitą liczbę słów w dokumentach: wykorzystamy tu funkcje group_by() oraz summarize():
# PRZYKŁAD 3.3
total_words <- doc_words %>%
group_by(doc_id) %>%
summarize(total = sum(n))
total_words
## # A tibble: 3 x 2
## doc_id total
## <int> <int>
## 1 1 14
## 2 2 13
## 3 3 14
Na koniec powiążemy te dwie tabele za pomocą funkcji left_join():
# PRZYKŁAD 3.4
doc_words <- doc_words %>%
left_join(total_words)
## Joining, by = "doc_id"
doc_words
## # A tibble: 37 x 4
## doc_id word n total
## <int> <chr> <int> <int>
## 1 1 a 3 14
## 2 2 a 3 13
## 3 1 child 1 14
## 4 1 have 1 14
## 5 1 king 1 14
## 6 1 once 1 14
## 7 1 there 1 14
## 8 1 time 1 14
## 9 1 to 1 14
## 10 1 upon 1 14
## # ... with 27 more rows
Mając taką strukturę, możemy już bez problemu policzyć np term frequency, czyli czestość termów daną wzorem
\(TF_{ij} = \frac{n_{ij}}{\sum_k n_{kj}}\)
gdzie \(n_{ij}\) to liczba wystąpień słowa \(i\) w dokumencie \(j\). Wykorzystamy do tego funkcję mutate()
# PRZYKŁAD 3.5
doc_words %>%
mutate(tf = n / total)
## # A tibble: 37 x 5
## doc_id word n total tf
## <int> <chr> <int> <int> <dbl>
## 1 1 a 3 14 0.214
## 2 2 a 3 13 0.231
## 3 1 child 1 14 0.0714
## 4 1 have 1 14 0.0714
## 5 1 king 1 14 0.0714
## 6 1 once 1 14 0.0714
## 7 1 there 1 14 0.0714
## 8 1 time 1 14 0.0714
## 9 1 to 1 14 0.0714
## 10 1 upon 1 14 0.0714
## # ... with 27 more rows
Jednak łatwiej jest od razu otrzymać również wartości TF-IDF. Oczywiście możemy to policzyć "na piechotę", ale łatwiej jest skorzystać z funkcji bind_tf_idf(). Dodatkowo skierujemy strumień poprzez funkcję print() i każdemy jej wypisać pełną ramkę:
# PRZYKŁAD 3.6
doc_words %>%
bind_tf_idf(word, doc_id, n) %>%
arrange(desc(tf_idf)) %>%
print(n = Inf)
## # A tibble: 37 x 7
## doc_id word n total tf idf tf_idf
## <int> <chr> <int> <int> <dbl> <dbl> <dbl>
## 1 2 beautiful 1 13 0.0769 1.10 0.0845
## 2 2 faraway 1 13 0.0769 1.10 0.0845
## 3 2 land 1 13 0.0769 1.10 0.0845
## 4 2 lived 1 13 0.0769 1.10 0.0845
## 5 2 princess 1 13 0.0769 1.10 0.0845
## 6 1 child 1 14 0.0714 1.10 0.0785
## 7 1 have 1 14 0.0714 1.10 0.0785
## 8 1 who 1 14 0.0714 1.10 0.0785
## 9 1 wished 1 14 0.0714 1.10 0.0785
## 10 3 having 1 14 0.0714 1.10 0.0785
## 11 3 live 1 14 0.0714 1.10 0.0785
## 12 3 olden 1 14 0.0714 1.10 0.0785
## 13 3 the 1 14 0.0714 1.10 0.0785
## 14 3 used 1 14 0.0714 1.10 0.0785
## 15 3 when 1 14 0.0714 1.10 0.0785
## 16 3 wishing 1 14 0.0714 1.10 0.0785
## 17 2 in 1 13 0.0769 0.405 0.0312
## 18 2 once 1 13 0.0769 0.405 0.0312
## 19 2 upon 1 13 0.0769 0.405 0.0312
## 20 1 king 1 14 0.0714 0.405 0.0290
## 21 1 once 1 14 0.0714 0.405 0.0290
## 22 1 to 1 14 0.0714 0.405 0.0290
## 23 1 upon 1 14 0.0714 0.405 0.0290
## 24 1 was 1 14 0.0714 0.405 0.0290
## 25 3 in 1 14 0.0714 0.405 0.0290
## 26 3 king 1 14 0.0714 0.405 0.0290
## 27 3 to 1 14 0.0714 0.405 0.0290
## 28 3 was 1 14 0.0714 0.405 0.0290
## 29 1 a 3 14 0.214 0 0
## 30 2 a 3 13 0.231 0 0
## 31 1 there 1 14 0.0714 0 0
## 32 1 time 1 14 0.0714 0 0
## 33 2 there 1 13 0.0769 0 0
## 34 2 time 1 13 0.0769 0 0
## 35 3 a 1 14 0.0714 0 0
## 36 3 there 1 14 0.0714 0 0
## 37 3 time 1 14 0.0714 0 0
Inny sposobem pracy na wielu dokumentach jest wykorzystanie idei korpusu, czyli po prostu zbioru dokumentów. Taką możliwość daje pakiet tm. Aby stworzyć korpus, musimy podać źródło - w naszym przypadku będzie to ramka danych, więc skorzystamy z funkcji DataframeSource() (musimy jeszcze dokonac rzutowania tibble na data frame). Gdyby nasze dokumenty miały formę kolejnych wektorów, użylibysmy VectorSource. Zawartość korpusu obejrzymy przy pomocy funkcji inspect().
# PRZYKŁAD 3.7
library(tm)
## Loading required package: NLP
df.source <- DataframeSource(as.data.frame(data))
corpus <- VCorpus(df.source)
inspect(corpus)
## <<VCorpus>>
## Metadata: corpus specific: 0, document level (indexed): 0
## Content: documents: 3
##
## [[1]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 61
##
## [[2]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 69
##
## [[3]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 70
Do pojedynczego dokumentu możemy odwoływać się za pomocą indeksu listy oraz odpowiednich pól.
# PRZYKŁAD 3.8
corpus[[2]]
## <<PlainTextDocument>>
## Metadata: 7
## Content: chars: 69
corpus[[2]]$meta
## author : character(0)
## datetimestamp: 2018-11-05 09:08:51
## description : character(0)
## heading : character(0)
## id : 2
## language : en
## origin : character(0)
corpus[[2]]$content
## [1] "Once upon a time, in a faraway land there lived a beautiful princess."
Biblioteka tm jest wyposażona w kilka przydatnych funkcji, w tym tm_filter(), która umozliwia np. wyszukanie określonych słów w poszczególnych dokumentach
# PRZYKŁAD 3.9
corpus %>%
tm_index(FUN = function(x) any(grep("king", content(x))))
## [1] TRUE FALSE FALSE
Inną wygodną funkcją jest tm_map(), która zastosuje wybrane transformcje do poszczególnych dokumentów, przy czym w przypadku dedykowanych funkcji (np. reomvePunctation) wystarczy podać je w treści funkcji tm_map(), natomiast dla własnych tworów potrzebujemy "ubrać" je w tzw content_transformer().
# PRZYKŁAD 3.10
corpus1 <- corpus %>%
tm_map(removePunctuation)
content(corpus1[[3]])
## [1] "In the olden time when wishing was having there used to live a King"
# PRZYKŁAD 3.11
corpus1 <- corpus %>%
tm_map(content_transformer(tolower))
content(corpus1[[3]])
## [1] "in the olden time, when wishing was having, there used to live a king."
Bardzo wygodną reprezentacją dokumentu stanowi macierz termów-dokumentów. Jest to w pewnym sensie implementacja modelu bag-of-words lub (w zalażności od wykorzystania) modelu przestrzeni wektorowej. Każda kolumna macierzy odpowiada dokumentowi, natomiast każdy rząd - wyrazowi, otrzymanemu po procesie tokenizacji.
# PRZYKŁAD 3.12
tdm <- TermDocumentMatrix(corpus)
tdm
## <<TermDocumentMatrix (terms: 24, documents: 3)>>
## Non-/sparse entries: 30/42
## Sparsity : 58%
## Maximal term length: 9
## Weighting : term frequency (tf)
inspect(tdm)
## <<TermDocumentMatrix (terms: 24, documents: 3)>>
## Non-/sparse entries: 30/42
## Sparsity : 58%
## Maximal term length: 9
## Weighting : term frequency (tf)
## Sample :
## Docs
## Terms 1 2 3
## beautiful 0 1 0
## child. 1 0 0
## faraway 0 1 0
## have 1 0 0
## having, 0 0 1
## once 1 1 0
## there 1 1 1
## time, 0 1 1
## upon 1 1 0
## was 1 0 1
Powyższe instrukcje pokazują jedynie część macierzy \(tdm\), poza tym widać, że tokanizacja została przeprowadzona przy użyciu znaków interpunkcyjnych. Zanki usuwamy przekazując opcję removePunctuation = TRUE, natomiast obiekt rzutujemy na zwykłą macierz za pomocą as.matrix().
# PRZYKŁAD 3.13
tdm <- TermDocumentMatrix(corpus, control = list(removePunctuation = TRUE))
tdm <- as.matrix(tdm)
tdm
## Docs
## Terms 1 2 3
## beautiful 0 1 0
## child 1 0 0
## faraway 0 1 0
## have 1 0 0
## having 0 0 1
## king 1 0 1
## land 0 1 0
## live 0 0 1
## lived 0 1 0
## olden 0 0 1
## once 1 1 0
## princess 0 1 0
## the 0 0 1
## there 1 1 1
## time 1 1 1
## upon 1 1 0
## used 0 0 1
## was 1 0 1
## when 0 0 1
## who 1 0 0
## wished 1 0 0
## wishing 0 0 1
Aby jeszcze bardziej "obrobić" dokument, pozbywamy się słów kluczowych, a poza tym korzystamy z opcji do stemingu, co sprowadzi część wyrazów do wspólnych form.
# PRZYKŁAD 3.14
tdm <- TermDocumentMatrix(corpus, control = list(
removePunctuation = TRUE, stopwords = TRUE, stemming = TRUE))
tdm <- as.matrix(tdm)
tdm
## Docs
## Terms 1 2 3
## beauti 0 1 0
## child 1 0 0
## faraway 0 1 0
## king 1 0 1
## land 0 1 0
## live 0 1 1
## olden 0 0 1
## princess 0 1 0
## time 1 1 1
## upon 1 1 0
## use 0 0 1
## wish 1 0 1
I wreszcie, możemy również zastosowac transformację TF-IDF do naszej macierzy:
# PRZYKŁAD 3.15
tdm <- TermDocumentMatrix(corpus, control = list(weighting = weightTfIdf))
tdm
## <<TermDocumentMatrix (terms: 24, documents: 3)>>
## Non-/sparse entries: 27/45
## Sparsity : 62%
## Maximal term length: 9
## Weighting : term frequency - inverse document frequency (normalized) (tf-idf)
as.matrix(tdm)
## Docs
## Terms 1 2 3
## beautiful 0.00000000 0.17610694 0.00000000
## child. 0.15849625 0.00000000 0.00000000
## faraway 0.00000000 0.17610694 0.00000000
## have 0.15849625 0.00000000 0.00000000
## having, 0.00000000 0.00000000 0.14408750
## king 0.15849625 0.00000000 0.00000000
## king. 0.00000000 0.00000000 0.14408750
## land 0.00000000 0.17610694 0.00000000
## live 0.00000000 0.00000000 0.14408750
## lived 0.00000000 0.17610694 0.00000000
## olden 0.00000000 0.00000000 0.14408750
## once 0.05849625 0.06499583 0.00000000
## princess. 0.00000000 0.17610694 0.00000000
## the 0.00000000 0.00000000 0.14408750
## there 0.00000000 0.00000000 0.00000000
## time 0.15849625 0.00000000 0.00000000
## time, 0.00000000 0.06499583 0.05317841
## upon 0.05849625 0.06499583 0.00000000
## used 0.00000000 0.00000000 0.14408750
## was 0.05849625 0.00000000 0.05317841
## when 0.00000000 0.00000000 0.14408750
## who 0.15849625 0.00000000 0.00000000
## wished 0.15849625 0.00000000 0.00000000
## wishing 0.00000000 0.00000000 0.14408750
W przypadku, gdybyśmy chcieli wyznaczyć iloczyn skalarny, związany wystepowaniem słów w poszczególnych dokumentach, wystarczy wykonać odpowiednie mnożenie macierzowe
# PRZYKŁAD 3.14
tdm <- TermDocumentMatrix(corpus, control = list(
removePunctuation = TRUE, stopwords = TRUE, stemming = TRUE))
tdm <- as.matrix(tdm)
tdm[,1] %*% tdm
## Docs
## 1 2 3
## [1,] 5 2 3